diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index c6a7c7e7..eb86617b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,16 +1,11 @@ - - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b614cf68..2aa9c895 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,7 +48,7 @@ android { return@signingConfigs } - create("release") { + val release by creating { val props = Properties() signingFile.inputStream().use { props.load(it) } @@ -113,6 +113,10 @@ play { } dependencies { + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation(platform(libs.kotlin.bom)) + implementation(project(":core-common")) implementation(project(":core-domain")) @@ -121,9 +125,6 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.activity.ktx) - //region Compose - val composeBom = platform(libs.androidx.compose.bom) - implementation(composeBom) debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.bundles.androidx.compose) implementation(libs.androidx.compose.material2) // Required by AboutLibraries @@ -134,8 +135,6 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.mikepenz.aboutlibraries.compose) implementation(libs.google.accompanist.systemuicontroller) - androidTestImplementation(composeBom) - //endregion implementation(libs.androidx.startup.runtime) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -148,6 +147,8 @@ dependencies { debugImplementation(libs.facebook.stetho) + androidTestImplementation(composeBom) + testImplementation(libs.kotest.runner.junit5) testImplementation(libs.kotest.assertions.core) testImplementation(libs.mockk) diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/MainActivity.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/MainActivity.kt index bad9c0bb..b1ee80db 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/MainActivity.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/MainActivity.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -23,15 +23,15 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.compose.runtime.mutableStateOf import androidx.core.view.WindowCompat -import com.svenjacobs.app.leon.ui.screens.main.MainScreen -import com.svenjacobs.app.leon.ui.screens.main.model.MainScreenViewModel +import com.svenjacobs.app.leon.ui.MainRouter class MainActivity : ComponentActivity() { - private val mainScreenViewModel: MainScreenViewModel by viewModels() + + private val sourceText = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,8 +41,8 @@ class MainActivity : ComponentActivity() { onIntent(intent) setContent { - MainScreen( - viewModel = mainScreenViewModel, + MainRouter( + sourceText = sourceText, ) } } @@ -69,16 +69,18 @@ class MainActivity : ComponentActivity() { } else { null } + Intent.ACTION_VIEW -> if (intent.scheme.orEmpty().startsWith("http")) { intent.dataString } else { null } + else -> null } } - mainScreenViewModel.setText(text) + sourceText.value = text } private fun warmupCustomTabsService() { diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/AppDataStoreManager.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/AppDataStoreManager.kt index a74e5bd0..23e6b13e 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/AppDataStoreManager.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/AppDataStoreManager.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -22,16 +22,16 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.AppContext -import javax.inject.Singleton +import com.svenjacobs.app.leon.core.domain.action.ActionAfterClean +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.AppContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Manages app specific preferences stored via [DataStore]. */ -@Singleton class AppDataStoreManager( private val context: Context = AppContext, ) { @@ -43,12 +43,21 @@ class AppDataStoreManager( } } - val versionCode: Flow = + suspend fun setActionAfterClean(actionAfterClean: ActionAfterClean) { + context.dataStore.edit { + it[KEY_ACTION_AFTER_CLEAN] = actionAfterClean.name + } + } + + val actionAfterClean: Flow = context.dataStore.data.map { preferences -> - preferences[KEY_VERSION_CODE] ?: 0 + runCatching { + preferences[KEY_ACTION_AFTER_CLEAN]?.let(ActionAfterClean::valueOf) + }.getOrNull() } private companion object { private val KEY_VERSION_CODE = intPreferencesKey("version_code") + private val KEY_ACTION_AFTER_CLEAN = stringPreferencesKey("action_after_clean") } } diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/SanitizerDataStoreManager.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/SanitizerDataStoreManager.kt index 2d9d5600..ad1765c8 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/SanitizerDataStoreManager.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/datastore/SanitizerDataStoreManager.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -24,16 +24,14 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.AppContext +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.AppContext import com.svenjacobs.app.leon.core.domain.sanitizer.Sanitizer -import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Manages [Sanitizer] specific preferences stored via [DataStore]. */ -@Singleton class SanitizerDataStoreManager( private val context: Context = AppContext, ) { diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/inject/AppContainer.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/inject/AppContainer.kt new file mode 100644 index 00000000..bafac6ab --- /dev/null +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/inject/AppContainer.kt @@ -0,0 +1,28 @@ +/* + * Léon - The URL Cleaner + * Copyright (C) 2023 Sven Jacobs + * + * 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 . + */ + +package com.svenjacobs.app.leon.inject + +import com.svenjacobs.app.leon.datastore.AppDataStoreManager +import com.svenjacobs.app.leon.datastore.SanitizerDataStoreManager + +object AppContainer { + + val AppDataStoreManager: AppDataStoreManager by lazy { AppDataStoreManager() } + val SanitizerDataStoreManager: SanitizerDataStoreManager by lazy { SanitizerDataStoreManager() } +} diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/sanitizer/SanitizerRepositoryImpl.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/sanitizer/SanitizerRepositoryImpl.kt index 9f5af589..2e23fdf6 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/sanitizer/SanitizerRepositoryImpl.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/sanitizer/SanitizerRepositoryImpl.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -18,12 +18,13 @@ package com.svenjacobs.app.leon.sanitizer -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.Sanitizers +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.Sanitizers import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizerId import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizerRepository import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizerRepository.SanitizerState import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizersCollection import com.svenjacobs.app.leon.datastore.SanitizerDataStoreManager +import com.svenjacobs.app.leon.inject.AppContainer.SanitizerDataStoreManager import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -31,7 +32,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map class SanitizerRepositoryImpl( - private val dataStoreManager: SanitizerDataStoreManager = SanitizerDataStoreManager(), + private val dataStoreManager: SanitizerDataStoreManager = SanitizerDataStoreManager, private val sanitizers: SanitizersCollection = Sanitizers, ) : SanitizerRepository { diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/startup/AppInitializer.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/startup/AppInitializer.kt index 6f189011..94ed0867 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/startup/AppInitializer.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/startup/AppInitializer.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -21,7 +21,7 @@ package com.svenjacobs.app.leon.startup import android.content.Context import androidx.startup.Initializer import com.svenjacobs.app.leon.BuildConfig -import com.svenjacobs.app.leon.datastore.AppDataStoreManager +import com.svenjacobs.app.leon.inject.AppContainer.AppDataStoreManager import kotlinx.coroutines.runBlocking /** @@ -31,13 +31,12 @@ import kotlinx.coroutines.runBlocking class AppInitializer : Initializer { override fun create(context: Context) { - val appDataStoreManager = AppDataStoreManager() val stethoHelper = StethoHelper() stethoHelper.initialize(context) runBlocking { - appDataStoreManager.setVersionCode(BuildConfig.VERSION_CODE) + AppDataStoreManager.setVersionCode(BuildConfig.VERSION_CODE) } } diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/startup/ContainerInitializer.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/startup/ContainerInitializer.kt index 5924dc6c..582b6b08 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/startup/ContainerInitializer.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/startup/ContainerInitializer.kt @@ -19,7 +19,7 @@ package com.svenjacobs.app.leon.startup import android.content.Context -import com.svenjacobs.app.leon.core.domain.inject.AppContainer +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer import com.svenjacobs.app.leon.core.domain.sanitizer.aliexpress.AliexpressSanitizer import com.svenjacobs.app.leon.core.domain.sanitizer.amazon.AmazonProductSanitizer import com.svenjacobs.app.leon.core.domain.sanitizer.amazon.AmazonSanitizer @@ -54,7 +54,7 @@ import kotlinx.collections.immutable.persistentListOf class ContainerInitializer : DistinctInitializer { override fun create(context: Context) { - AppContainer.init( + DomainContainer.init( appContext = context, sanitizerRepositoryProvider = { SanitizerRepositoryImpl() }, sanitizers = persistentListOf( diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/MainRouter.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/MainRouter.kt new file mode 100644 index 00000000..0c3ff061 --- /dev/null +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/MainRouter.kt @@ -0,0 +1,66 @@ +/* + * Léon - The URL Cleaner + * Copyright (C) 2023 Sven Jacobs + * + * 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 . + */ + +package com.svenjacobs.app.leon.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.svenjacobs.app.leon.ui.screens.main.MainScreen +import com.svenjacobs.app.leon.ui.screens.settings.SettingsLicensesScreen +import com.svenjacobs.app.leon.ui.screens.settings.SettingsSanitizersScreen + +@Composable +fun MainRouter(sourceText: State, modifier: Modifier = Modifier) { + val navController = rememberNavController() + + NavHost( + modifier = modifier, + navController = navController, + startDestination = Routes.Main, + ) { + composable(Routes.Main) { + MainScreen( + sourceText = sourceText, + onNavigateToSettingsSanitizers = { navController.navigate(Routes.SettingsSanitizers) }, + onNavigateToSettingsLicenses = { navController.navigate(Routes.SettingsLicenses) }, + ) + } + + composable(Routes.SettingsSanitizers) { + SettingsSanitizersScreen( + onBackClick = { navController.popBackStack() }, + ) + } + + composable(Routes.SettingsLicenses) { + SettingsLicensesScreen( + onBackClick = { navController.popBackStack() }, + ) + } + } +} + +private object Routes { + const val Main = "main" + const val SettingsSanitizers = "settings_sanitizers" + const val SettingsLicenses = "settings_licenses" +} diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/MainScreen.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/MainScreen.kt index 47108b02..48773cdf 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/MainScreen.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/MainScreen.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -44,11 +44,11 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -68,6 +68,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.svenjacobs.app.leon.R +import com.svenjacobs.app.leon.core.domain.action.ActionAfterClean import com.svenjacobs.app.leon.ui.common.views.TopAppBar import com.svenjacobs.app.leon.ui.screens.main.model.MainScreenViewModel import com.svenjacobs.app.leon.ui.screens.main.model.MainScreenViewModel.UiState.Result @@ -81,10 +82,15 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @Composable -fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { +fun MainScreen( + sourceText: State, + onNavigateToSettingsSanitizers: () -> Unit, + onNavigateToSettingsLicenses: () -> Unit, + modifier: Modifier = Modifier, + viewModel: MainScreenViewModel = viewModel(), +) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - var hideBars by rememberSaveable { mutableStateOf(false) } val navController = rememberNavController() val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() @@ -92,8 +98,18 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { val isDarkTheme = isSystemInDarkTheme() val context = LocalContext.current val clipboard = LocalClipboardManager.current + val shareTitle = stringResource(R.string.share) + var didPerformActionAfterClean by remember(uiState.result) { mutableStateOf(false) } + + LaunchedEffect(sourceText.value) { + viewModel.setText(sourceText.value) + } + + LaunchedEffect(Unit) { + systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !isDarkTheme) + } - fun onShareButtonClick(result: Result.Success, title: String) { + fun openShareMenu(result: Result.Success) { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" addCategory(Intent.CATEGORY_DEFAULT) @@ -103,12 +119,12 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { context.startActivity( Intent.createChooser( intent, - title, + shareTitle, ), ) } - fun onVerifyButtonClick(result: Result.Success) { + fun launchBrowser(result: Result.Success) { result.urls.firstOrNull()?.let { url -> val intent = CustomTabsIntent.Builder() .setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) @@ -118,22 +134,18 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { } } - fun onCopyToClipboardClick(result: Result.Success) { + fun copyToClipboard(result: Result.Success) { clipboard.setText(AnnotatedString(result.cleanedText)) coroutineScope.launch { snackbarHostState.showSnackbar(context.getString(R.string.clipboard_message)) } } - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !isDarkTheme) - } - AppTheme { Scaffold( modifier = modifier, - topBar = { if (!hideBars) TopAppBar() }, - bottomBar = { if (!hideBars) BottomBar(navController = navController) }, + topBar = { TopAppBar() }, + bottomBar = { BottomBar(navController = navController) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> Box( @@ -146,7 +158,19 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { startDestination = Screen.Main.route, ) { composable(Screen.Main.route) { - val shareTitle = stringResource(R.string.share) + LaunchedEffect(uiState.result, uiState.actionAfterClean) { + if (didPerformActionAfterClean) return@LaunchedEffect + + (uiState.result as? Result.Success)?.let { result -> + when (uiState.actionAfterClean) { + ActionAfterClean.OpenShareMenu -> openShareMenu(result) + ActionAfterClean.CopyToClipboard -> copyToClipboard(result) + ActionAfterClean.DoNothing -> {} + } + + didPerformActionAfterClean = true + } + } Content( result = uiState.result, @@ -157,9 +181,9 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { clipboard.getText()?.toString(), ) }, - onShareClick = { result -> onShareButtonClick(result, shareTitle) }, - onCopyToClipboardClick = ::onCopyToClipboardClick, - onVerifyClick = ::onVerifyButtonClick, + onShareClick = ::openShareMenu, + onCopyToClipboardClick = ::copyToClipboard, + onVerifyClick = ::launchBrowser, onResetClick = viewModel::onResetClick, onUrlDecodeCheckedChange = viewModel::onUrlDecodeCheckedChange, onExtractUrlCheckedChange = viewModel::onExtractUrlCheckedChange, @@ -168,8 +192,8 @@ fun MainScreen(viewModel: MainScreenViewModel, modifier: Modifier = Modifier) { composable(Screen.Settings.route) { SettingsScreen( - viewModel = viewModel(), - onHideBars = { hideBars = it }, + onNavigateToSettingsSanitizers = onNavigateToSettingsSanitizers, + onNavigateToSettingsLicenses = onNavigateToSettingsLicenses, ) } } @@ -224,6 +248,7 @@ private fun Content( onUrlDecodeCheckedChange = onUrlDecodeCheckedChange, onExtractUrlCheckedChange = onExtractUrlCheckedChange, ) + else -> HowToBody( onImportFromClipboardClick = onImportFromClipboardClick, ) diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/model/MainScreenViewModel.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/model/MainScreenViewModel.kt index 3c4ebc67..1988a15f 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/model/MainScreenViewModel.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/main/model/MainScreenViewModel.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -21,6 +21,9 @@ package com.svenjacobs.app.leon.ui.screens.main.model import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.svenjacobs.app.leon.core.domain.CleanerService +import com.svenjacobs.app.leon.core.domain.action.ActionAfterClean +import com.svenjacobs.app.leon.datastore.AppDataStoreManager +import com.svenjacobs.app.leon.inject.AppContainer.AppDataStoreManager import com.svenjacobs.app.leon.ui.screens.main.model.MainScreenViewModel.UiState.Result import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -29,6 +32,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn class MainScreenViewModel( + appDataStoreManager: AppDataStoreManager = AppDataStoreManager, private val cleanerService: CleanerService = CleanerService(), ) : ViewModel() { @@ -37,6 +41,7 @@ class MainScreenViewModel( val isUrlDecodeEnabled: Boolean = false, val isExtractUrlEnabled: Boolean = false, val result: Result = Result.Empty, + val actionAfterClean: ActionAfterClean = ActionAfterClean.DoNothing, ) { sealed interface Result { @@ -61,7 +66,8 @@ class MainScreenViewModel( text, urlDecodeEnabled, extractUrlEnabled, - ) { text, urlDecodeEnabled, extractUrlEnabled -> + appDataStoreManager.actionAfterClean, + ) { text, urlDecodeEnabled, extractUrlEnabled, actionAfterClean -> val result = text?.let { clean( text = text, @@ -75,6 +81,7 @@ class MainScreenViewModel( isUrlDecodeEnabled = urlDecodeEnabled, isExtractUrlEnabled = extractUrlEnabled, result = result, + actionAfterClean = actionAfterClean ?: ActionAfterClean.DoNothing, ) }.stateIn( scope = viewModelScope, diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsSanitizersScreen.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsSanitizersScreen.kt index 31d88359..4864fb22 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsSanitizersScreen.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsSanitizersScreen.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -34,15 +34,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.svenjacobs.app.leon.R import com.svenjacobs.app.leon.ui.common.views.TopAppBar import com.svenjacobs.app.leon.ui.screens.settings.model.SettingsSanitizersScreenViewModel @Composable fun SettingsSanitizersScreen( - viewModel: SettingsSanitizersScreenViewModel, onBackClick: () -> Unit, modifier: Modifier = Modifier, + viewModel: SettingsSanitizersScreenViewModel = viewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsScreen.kt index a47f8169..cee62b93 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/SettingsScreen.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -24,14 +24,21 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.TextField import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -39,76 +46,49 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import com.svenjacobs.app.leon.BuildConfig import com.svenjacobs.app.leon.R +import com.svenjacobs.app.leon.core.domain.action.ActionAfterClean import com.svenjacobs.app.leon.ui.screens.settings.model.SettingsScreenViewModel import com.svenjacobs.app.leon.ui.theme.AppTheme @Composable fun SettingsScreen( - onHideBars: (Boolean) -> Unit, + onNavigateToSettingsSanitizers: () -> Unit, + onNavigateToSettingsLicenses: () -> Unit, modifier: Modifier = Modifier, viewModel: SettingsScreenViewModel = viewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val navController = rememberNavController() - NavHost( + Content( modifier = modifier, - navController = navController, - startDestination = SCREEN_SETTINGS, - ) { - composable( - route = SCREEN_SETTINGS, - ) { - LaunchedEffect(Unit) { onHideBars(false) } - - Content( - browserEnabled = uiState.browserEnabled, - onSanitizersClick = { navController.navigate(SCREEN_SANITIZERS) }, - onLicensesClick = { navController.navigate(SCREEN_LICENSES) }, - onBrowserSwitchCheckedChange = viewModel::onBrowserSwitchCheckedChange, - ) - } - - composable( - route = SCREEN_SANITIZERS, - ) { - LaunchedEffect(Unit) { onHideBars(true) } - - SettingsSanitizersScreen( - viewModel = viewModel(), - onBackClick = { navController.popBackStack() }, - ) - } - - composable( - route = SCREEN_LICENSES, - ) { - LaunchedEffect(Unit) { onHideBars(true) } - - SettingsLicensesScreen( - onBackClick = { navController.popBackStack() }, - ) - } - } + isLoading = uiState.isLoading, + browserEnabled = uiState.browserEnabled, + actionAfterClean = uiState.actionAfterClean, + onSanitizersClick = onNavigateToSettingsSanitizers, + onLicensesClick = onNavigateToSettingsLicenses, + onBrowserSwitchCheckedChange = viewModel::onBrowserSwitchCheckedChange, + onActionAfterCleanClick = viewModel::onActionAfterCleanClick, + ) } @Composable +@OptIn(ExperimentalMaterialApi::class) private fun Content( - browserEnabled: Boolean?, + isLoading: Boolean, + browserEnabled: Boolean, + actionAfterClean: ActionAfterClean, onSanitizersClick: () -> Unit, onLicensesClick: () -> Unit, onBrowserSwitchCheckedChange: (Boolean) -> Unit, + onActionAfterCleanClick: (ActionAfterClean) -> Unit, modifier: Modifier = Modifier, ) { Box( modifier = modifier.fillMaxSize(), ) { - if (browserEnabled == null) { + if (isLoading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), ) @@ -150,6 +130,63 @@ private fun Content( onCheckedChange = onBrowserSwitchCheckedChange, ) } + + Column( + modifier = Modifier.padding(top = 8.dp), + ) { + var expanded by rememberSaveable { mutableStateOf(false) } + + Text(stringResource(R.string.action_after_clean)) + + ExposedDropdownMenuBox( + modifier = Modifier.padding(top = 8.dp), + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + TextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + value = actionAfterClean.text(), + onValueChange = {}, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + ) + + ExposedDropdownMenu( + modifier = Modifier.exposedDropdownSize(), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.do_nothing)) }, + onClick = { + expanded = false + onActionAfterCleanClick(ActionAfterClean.DoNothing) + }, + ) + + DropdownMenuItem( + text = { Text(stringResource(R.string.open_share_menu)) }, + onClick = { + expanded = false + onActionAfterCleanClick(ActionAfterClean.OpenShareMenu) + }, + ) + + DropdownMenuItem( + text = { Text(stringResource(R.string.copy_to_clipboard)) }, + onClick = { + expanded = false + onActionAfterCleanClick(ActionAfterClean.CopyToClipboard) + }, + ) + } + } + } } } @@ -167,15 +204,24 @@ private fun Content( } @Composable -@Preview -private fun SettingsScreenPreview() { +private fun ActionAfterClean.text(): String = when (this) { + ActionAfterClean.DoNothing -> stringResource(R.string.do_nothing) + ActionAfterClean.OpenShareMenu -> stringResource(R.string.open_share_menu) + ActionAfterClean.CopyToClipboard -> stringResource(R.string.copy_to_clipboard) +} + +@Composable +@Preview(showBackground = true) +private fun ContentPreview() { AppTheme { - SettingsScreen( - onHideBars = {}, + Content( + isLoading = false, + browserEnabled = false, + actionAfterClean = ActionAfterClean.OpenShareMenu, + onSanitizersClick = {}, + onLicensesClick = {}, + onBrowserSwitchCheckedChange = {}, + onActionAfterCleanClick = {}, ) } } - -private const val SCREEN_SETTINGS = "settings" -private const val SCREEN_SANITIZERS = "sanitizers" -private const val SCREEN_LICENSES = "licenses" diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsSanitizersScreenViewModel.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsSanitizersScreenViewModel.kt index 6291f50c..3851f191 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsSanitizersScreenViewModel.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsSanitizersScreenViewModel.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -22,9 +22,9 @@ import android.annotation.SuppressLint import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.AppContext -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.SanitizerRepository -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.Sanitizers +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.AppContext +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.SanitizerRepository +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.Sanitizers import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizerId import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizerRepository import com.svenjacobs.app.leon.core.domain.sanitizer.SanitizersCollection diff --git a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsScreenViewModel.kt b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsScreenViewModel.kt index 94e1b884..2324e27d 100644 --- a/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsScreenViewModel.kt +++ b/app/src/main/kotlin/com/svenjacobs/app/leon/ui/screens/settings/model/SettingsScreenViewModel.kt @@ -1,6 +1,6 @@ /* * Léon - The URL Cleaner - * Copyright (C) 2022 Sven Jacobs + * Copyright (C) 2023 Sven Jacobs * * 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 @@ -24,36 +24,46 @@ import android.content.Context import android.content.pm.PackageManager import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.svenjacobs.app.leon.core.domain.inject.AppContainer.AppContext +import com.svenjacobs.app.leon.core.domain.action.ActionAfterClean +import com.svenjacobs.app.leon.core.domain.inject.DomainContainer.AppContext +import com.svenjacobs.app.leon.datastore.AppDataStoreManager +import com.svenjacobs.app.leon.inject.AppContainer.AppDataStoreManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @SuppressLint("StaticFieldLeak") class SettingsScreenViewModel( private val context: Context = AppContext, + private val appDataStoreManager: AppDataStoreManager = AppDataStoreManager, ) : ViewModel() { data class UiState( - val browserEnabled: Boolean? = null, + val isLoading: Boolean = true, + val browserEnabled: Boolean = false, + val actionAfterClean: ActionAfterClean = ActionAfterClean.DoNothing, ) - private val browserEnabled = MutableStateFlow(null) + private val browserEnabled = MutableStateFlow(false) val uiState: StateFlow = - browserEnabled - .mapLatest { - UiState( - browserEnabled = it, - ) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = UiState(), + combine( + browserEnabled, + appDataStoreManager.actionAfterClean, + ) { browserEnabled, actionAfterClean -> + UiState( + isLoading = false, + browserEnabled = browserEnabled, + actionAfterClean = actionAfterClean ?: ActionAfterClean.DoNothing, ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = UiState(), + ) init { val enabledSetting = packageManager.getComponentEnabledSetting(componentName) @@ -73,6 +83,12 @@ class SettingsScreenViewModel( ) } + fun onActionAfterCleanClick(actionAfterClean: ActionAfterClean) { + viewModelScope.launch { + appDataStoreManager.setActionAfterClean(actionAfterClean) + } + } + private val packageManager: PackageManager get() = context.packageManager diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2ed2c887..c07d61d2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,6 +1,6 @@ + Do nothing + Open share menu + Copy to clipboard diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 059e2d64..43af7abe 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,6 @@ + Register Léon as browser + Action after clean + Do nothing + Open share menu + Copy to clipboard diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a760b012..48e6cac7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@