diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index 688db9601..233a285d4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -35,7 +35,6 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) val loginViewModel: LoginViewModel by viewModels() - val settingsViewModel: SettingsViewModel by viewModels() val searchViewModel: SearchViewModel by viewModels() val labelsViewModel: LabelsViewModel by viewModels() val saveViewModel: SaveViewModel by viewModels() @@ -64,7 +63,6 @@ class MainActivity : ComponentActivity() { RootView( loginViewModel, searchViewModel, - settingsViewModel, labelsViewModel, saveViewModel, editInfoViewModel diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/ui/LinkIcon.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/ui/LinkIcon.kt new file mode 100644 index 000000000..79e6f83fa --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/ui/LinkIcon.kt @@ -0,0 +1,33 @@ +package app.omnivore.omnivore.core.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp + +@Composable +fun LinkIcon( + label: String, + icon: ImageVector, + url: String, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + IconButton( + modifier = modifier.padding(4.dp), + onClick = { uriHandler.openUri(url) }, + ) { + Icon( + imageVector = icon, + contentDescription = label, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt index d749a709d..fea94ab3d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt @@ -411,7 +411,7 @@ fun LibraryViewContent( dismissContent = { val selected = currentItem.savedItemId == selectedItem?.savedItem?.savedItemId - val test = SavedItemWithLabelsAndHighlights( + val savedItem = SavedItemWithLabelsAndHighlights( savedItem = cardDataWithLabels.savedItem, labels = cardDataWithLabels.labels, highlights = cardDataWithLabels.highlights @@ -419,7 +419,7 @@ fun LibraryViewContent( SavedItemCard( selected = selected, savedItemViewModel = libraryViewModel, - savedItem = test, + savedItem = savedItem, onClickHandler = { libraryViewModel.actionsMenuItemLiveData.postValue(null) val activityClass = diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt index f68b250b8..5e48ad2ff 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt @@ -1,10 +1,23 @@ package app.omnivore.omnivore.feature.root import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -16,41 +29,57 @@ import app.omnivore.omnivore.feature.library.LibraryView import app.omnivore.omnivore.feature.library.SearchView import app.omnivore.omnivore.feature.library.SearchViewModel import app.omnivore.omnivore.feature.save.SaveViewModel -import app.omnivore.omnivore.feature.settings.PolicyWebView -import app.omnivore.omnivore.feature.settings.SettingsView -import app.omnivore.omnivore.feature.settings.SettingsViewModel +import app.omnivore.omnivore.feature.settings.SettingsScreen +import app.omnivore.omnivore.feature.settings.about.AboutScreen +import app.omnivore.omnivore.feature.settings.account.AccountScreen +import app.omnivore.omnivore.feature.web.WebViewScreen import app.omnivore.omnivore.navigation.Routes @Composable fun RootView( loginViewModel: LoginViewModel, searchViewModel: SearchViewModel, - settingsViewModel: SettingsViewModel, labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel, editInfoViewModel: EditInfoViewModel, ) { val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false) + val snackbarHostState = remember { SnackbarHostState() } - Box { - if (hasAuthToken) { - PrimaryNavigator( - loginViewModel = loginViewModel, - searchViewModel = searchViewModel, - settingsViewModel = settingsViewModel, - labelsViewModel = labelsViewModel, - saveViewModel = saveViewModel, - editInfoViewModel = editInfoViewModel, - ) - } else { - WelcomeScreen(viewModel = loginViewModel) - } - - DisposableEffect(hasAuthToken) { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal, + ), + ) + ){ if (hasAuthToken) { - loginViewModel.registerUser() + PrimaryNavigator( + loginViewModel = loginViewModel, + searchViewModel = searchViewModel, + labelsViewModel = labelsViewModel, + saveViewModel = saveViewModel, + editInfoViewModel = editInfoViewModel, + snackbarHostState = snackbarHostState + + ) + } else { + WelcomeScreen(viewModel = loginViewModel) + } + + DisposableEffect(hasAuthToken) { + if (hasAuthToken) { + loginViewModel.registerUser() + } + onDispose {} } - onDispose {} } } } @@ -59,14 +88,17 @@ fun RootView( fun PrimaryNavigator( loginViewModel: LoginViewModel, searchViewModel: SearchViewModel, - settingsViewModel: SettingsViewModel, labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel, editInfoViewModel: EditInfoViewModel, + snackbarHostState: SnackbarHostState ) { val navController = rememberNavController() - NavHost(navController = navController, startDestination = Routes.Library.route) { + NavHost( + navController = navController, + startDestination = Routes.Library.route + ) { composable(Routes.Library.route) { LibraryView( navController = navController, @@ -78,29 +110,40 @@ fun PrimaryNavigator( composable(Routes.Search.route) { SearchView( - viewModel = searchViewModel, - navController = navController + viewModel = searchViewModel, navController = navController ) } composable(Routes.Settings.route) { - SettingsView( + SettingsScreen( loginViewModel = loginViewModel, - settingsViewModel = settingsViewModel, navController = navController ) } + composable(Routes.About.route) { + AboutScreen( + navController = navController + ) + } + + composable(Routes.Account.route) { + AccountScreen( + navController = navController, + snackbarHostState = snackbarHostState, + ) + } + composable(Routes.Documentation.route) { - PolicyWebView(navController = navController, url = "https://docs.omnivore.app") + WebViewScreen(navController = navController, url = "https://docs.omnivore.app") } composable(Routes.PrivacyPolicy.route) { - PolicyWebView(navController = navController, url = "https://omnivore.app/privacy") + WebViewScreen(navController = navController, url = "https://omnivore.app/privacy") } composable(Routes.TermsAndConditions.route) { - PolicyWebView(navController = navController, url = "https://omnivore.app/app/terms") + WebViewScreen(navController = navController, url = "https://omnivore.app/app/terms") } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt index 993944f8f..02ca000c8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/LogoutDialog.kt @@ -1,8 +1,10 @@ package app.omnivore.omnivore.feature.settings import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -12,31 +14,37 @@ import com.google.android.gms.auth.api.signin.GoogleSignInOptions @Composable fun LogoutDialog(onClose: (Boolean) -> Unit) { - val context = LocalContext.current + val context = LocalContext.current - AlertDialog( - onDismissRequest = { onClose(false) }, - title = { Text(text = stringResource(R.string.logout_dialog_title)) }, - text = { - Text(stringResource(R.string.logout_dialog_confirm_msg)) - }, - confirmButton = { - Button(onClick = { - // Sign out google users - val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .build() + AlertDialog(onDismissRequest = { onClose(false) }, + title = { Text(text = stringResource(R.string.logout_dialog_title)) }, + text = { + Text(stringResource(R.string.logout_dialog_confirm_msg)) + }, + containerColor = MaterialTheme.colorScheme.background, + confirmButton = { + TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ), + onClick = { + val signInOptions = + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build() - val googleSignIn = GoogleSignIn.getClient(context, signInOptions) - googleSignIn.signOut() - onClose(true) - }) { - Text(stringResource(R.string.logout_dialog_action_confirm)) - } - }, - dismissButton = { - Button(onClick = { onClose(false) }) { - Text(stringResource(R.string.logout_dialog_action_cancel)) - } - } - ) + val googleSignIn = GoogleSignIn.getClient(context, signInOptions) + googleSignIn.signOut() + onClose(true) + }) { + Text(stringResource(R.string.logout_dialog_action_confirm)) + } + }, + dismissButton = { + TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ), + onClick = { onClose(false) }) { + Text(stringResource(R.string.logout_dialog_action_cancel)) + } + }) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt deleted file mode 100644 index daeec8318..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/ManageAccount.kt +++ /dev/null @@ -1,73 +0,0 @@ -package app.omnivore.omnivore.feature.settings - -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import app.omnivore.omnivore.R - -@Composable -fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewModel) { - Dialog(onDismissRequest = { onDismiss() }) { - Surface( - shape = RoundedCornerShape(16.dp), - color = Color.White, - modifier = Modifier - .height(300.dp) - ) { - ManageAccountView(settingsViewModel = settingsViewModel) - } - } -} - -@Composable -fun ManageAccountView(settingsViewModel: SettingsViewModel) { - Column( - modifier = Modifier - .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 12.dp), - horizontalArrangement = Arrangement.Center - ) { - Text(stringResource(R.string.manage_account_title)) - } - - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = { settingsViewModel.resetDataCache() }) - ) { - Text(stringResource(R.string.manage_account_action_reset_data_cache)) - Spacer(modifier = Modifier.weight(1.0F)) - Icon(imageVector = Icons.Filled.Refresh, contentDescription = null) - } - } - } -} - diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt deleted file mode 100644 index b7f7d6d8f..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/PolicyWebView.kt +++ /dev/null @@ -1,77 +0,0 @@ -package app.omnivore.omnivore.feature.settings - -import android.annotation.SuppressLint -import android.view.ViewGroup -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.viewinterop.AndroidView -import androidx.navigation.NavHostController -import app.omnivore.omnivore.navigation.Routes -import app.omnivore.omnivore.R - -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SetJavaScriptEnabled") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PolicyWebView(navController: NavHostController, url: String) { - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.policy_webview_title)) }, - actions = { - IconButton(onClick = { navController.navigate(Routes.Settings.route) }) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null - ) - } - }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - ) { - val isDarkMode = isSystemInDarkTheme() - - Box { - AndroidView(factory = { - WebView(it).apply { - if (isDarkMode) { - setBackgroundColor(Color.Transparent.hashCode()) - } - - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - - settings.javaScriptEnabled = true - settings.allowContentAccess = true - settings.allowFileAccess = true - settings.domStorageEnabled = true - - alpha = 0.0f - - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - view?.animate()?.alpha(1.0f)?.duration = 200 - } - } - -// val themeID = if (isDarkMode) "Gray" else "LightGray" -// loadUrl(url, mutableMapOf("Set-Cookie" to "theme=$themeID; Max-Age=31536000;")) - loadUrl(url, mutableMapOf()) - } - }, update = {} - ) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt deleted file mode 100644 index a2e34294d..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsContent.kt +++ /dev/null @@ -1,175 +0,0 @@ -package app.omnivore.omnivore.feature.settings - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavHostController -import app.omnivore.omnivore.BuildConfig -import app.omnivore.omnivore.R -import app.omnivore.omnivore.navigation.Routes -import app.omnivore.omnivore.feature.auth.LoginViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingsView( - loginViewModel: LoginViewModel, - settingsViewModel: SettingsViewModel, - navController: NavHostController, -) { - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.settings_view_title)) }, - actions = { - IconButton(onClick = { navController.navigate(Routes.Library.route) }) { - Icon( - imageVector = Icons.Default.Home, - contentDescription = null - ) - } - }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - ) { paddingValues -> - SettingsViewContent( - loginViewModel = loginViewModel, - settingsViewModel = settingsViewModel, - navController = navController, - modifier = Modifier - .padding( - top = paddingValues.calculateTopPadding(), - bottom = paddingValues.calculateBottomPadding() - ) - ) - } -} - -@Composable -fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, modifier: Modifier) { - val showLogoutDialog = remember { mutableStateOf(false) } - val showManageAccountDialog = remember { mutableStateOf(false ) } - - Box( - modifier = modifier.fillMaxSize() - ) { - - val version = "Omnivore Version: " + BuildConfig.VERSION_NAME - - Column( - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .padding(horizontal = 6.dp) - .verticalScroll(rememberScrollState()) - ) { - - - SettingRow(text = stringResource(R.string.settings_view_setting_row_documentation)) { - navController.navigate(Routes.Documentation.route) - } - RowDivider() - SettingRow(text = stringResource(R.string.settings_view_setting_row_feedback)) { - settingsViewModel.presentIntercom() - } - RowDivider() - SettingRow(text = stringResource(R.string.settings_view_setting_row_privacy_policy)) { - navController.navigate(Routes.PrivacyPolicy.route) - } - RowDivider() - SettingRow(text = stringResource(R.string.settings_view_setting_row_terms_and_conditions)) { - navController.navigate(Routes.TermsAndConditions.route) - } - - SectionSpacer() - - SettingRow(text = stringResource(R.string.settings_view_setting_row_manage_account)) { - showManageAccountDialog.value = true - } - RowDivider() - SettingRow(text = stringResource(R.string.settings_view_setting_row_logout), includeIcon = false) { - showLogoutDialog.value = true - } - RowDivider() - - Text( - text = version, - fontSize = 12.sp, - modifier = Modifier - .padding(15.dp) - ) - } - - if (showLogoutDialog.value) { - LogoutDialog { performLogout -> - if (performLogout) { - loginViewModel.logout() - } - showLogoutDialog.value = false - } - } - - if (showManageAccountDialog.value) { - ManageAccountDialog( - onDismiss = { showManageAccountDialog.value = false }, - settingsViewModel = settingsViewModel - ) - } - } -} - -@Composable -private fun RowDivider() { - Divider( - modifier = Modifier.padding(horizontal = 12.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) - ) -} - -@Composable -private fun SectionSpacer() { - RowDivider() - Spacer(Modifier.height(60.dp)) - RowDivider() -} - -@Composable -private fun SettingRow(text: String, includeIcon: Boolean = true, tapAction: () -> Unit) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { tapAction() } - ) { - Text( - text = text, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(16.dp), - style = MaterialTheme.typography.titleMedium - ) - - if (includeIcon) { - Icon( - painter = painterResource(id = R.drawable.chevron_right), - contentDescription = null - ) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsScreen.kt new file mode 100644 index 000000000..4d99f2bdb --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsScreen.kt @@ -0,0 +1,155 @@ +package app.omnivore.omnivore.feature.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import app.omnivore.omnivore.R +import app.omnivore.omnivore.feature.auth.LoginViewModel +import app.omnivore.omnivore.navigation.Routes + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsScreen( + loginViewModel: LoginViewModel, + navController: NavHostController +) { + Scaffold(topBar = { + TopAppBar( + title = { Text(stringResource(R.string.settings_view_title)) }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = null + ) + } + + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + ) + }) { paddingValues -> + SettingsViewContent( + loginViewModel = loginViewModel, + navController = navController, + paddingValues = paddingValues + ) + } +} + +@Composable +fun SettingsViewContent( + loginViewModel: LoginViewModel, + navController: NavHostController, + paddingValues: PaddingValues +) { + val showLogoutDialog = remember { mutableStateOf(false) } + + val state = rememberLazyListState() + + LazyColumn( + state = state, + contentPadding = paddingValues, + ) { + + item { + SettingRow(title = stringResource(R.string.settings_view_setting_row_manage_account)) { + navController.navigate(Routes.Account.route) + } + } + + item { + SettingRow( + title = stringResource(R.string.settings_view_setting_row_logout) + ) { + showLogoutDialog.value = true + } + } + + item { + SettingRow(title = stringResource(R.string.about_view_title)) { + navController.navigate(Routes.About.route) + } + } + } + + if (showLogoutDialog.value) { + LogoutDialog { performLogout -> + if (performLogout) { + loginViewModel.logout() + } + showLogoutDialog.value = false + } + } +} + + +@Composable +internal fun SettingRow( + title: String, subtitle: String? = null, onClick: (() -> Unit)? +) { + Row( + modifier = Modifier + .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = SettingsVerticalPadding) + ) { + Text( + modifier = Modifier.padding(horizontal = SettingsHorizontalPadding), + text = title, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + style = MaterialTheme.typography.titleLarge, + fontSize = SettingsTitleFontSize, + ) + if (!subtitle.isNullOrBlank()) { + Text( + text = subtitle, + modifier = Modifier + .padding(horizontal = SettingsHorizontalPadding) + .alpha(SettingsSecondaryItemAlpha), + style = MaterialTheme.typography.bodySmall, + maxLines = 10, + ) + } + } + } +} + +internal val SettingsHorizontalPadding = 16.dp +internal val SettingsVerticalPadding = 16.dp +internal const val SettingsSecondaryItemAlpha = .78f +internal val SettingsTitleFontSize = 16.sp + +internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases" diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt index 6dde3d23f..0058ba321 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/SettingsViewModel.kt @@ -1,40 +1,58 @@ package app.omnivore.omnivore.feature.settings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.utils.DatastoreKeys -import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.data.DataService +import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.network.Networker import app.omnivore.omnivore.core.network.viewer +import app.omnivore.omnivore.utils.DatastoreKeys import dagger.hilt.android.lifecycle.HiltViewModel import io.intercom.android.sdk.Intercom import io.intercom.android.sdk.IntercomSpace +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( - private val networker: Networker, - private val dataService: DataService, - private val datastoreRepo: DatastoreRepository -): ViewModel() { - fun resetDataCache() { - viewModelScope.launch { - datastoreRepo.clearValue(DatastoreKeys.libraryLastSyncTimestamp) - dataService.clearDatabase() - } - } + private val networker: Networker, + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository +) : ViewModel() { - fun presentIntercom() { - viewModelScope.launch { - val viewer = networker.viewer() - viewer?.let { v -> - v.intercomHash?.let { intercomHash -> - Intercom.client().setUserHash(intercomHash) + var snackbarMessage by mutableStateOf("Resetting data...") + + var isResettingData by mutableStateOf(false) + var isDataResetCompleted by mutableStateOf(false) + + fun resetDataCache() { + isResettingData = true + + viewModelScope.launch { + datastoreRepo.clearValue(DatastoreKeys.libraryLastSyncTimestamp) + dataService.clearDatabase() + delay(1000) + isResettingData = false + + if (!isDataResetCompleted) { + isDataResetCompleted = true + } + } + } + + fun presentIntercom() { + viewModelScope.launch { + val viewer = networker.viewer() + viewer?.let { v -> + v.intercomHash?.let { intercomHash -> + Intercom.client().setUserHash(intercomHash) + } + Intercom.client().present(space = IntercomSpace.Messages) + } } - Intercom.client().present(space = IntercomSpace.Messages) - } } - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/AboutScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/AboutScreen.kt new file mode 100644 index 000000000..13ee241d3 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/AboutScreen.kt @@ -0,0 +1,152 @@ +package app.omnivore.omnivore.feature.settings.about + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import app.omnivore.omnivore.BuildConfig +import app.omnivore.omnivore.R +import app.omnivore.omnivore.core.ui.LinkIcon +import app.omnivore.omnivore.feature.settings.RELEASE_URL +import app.omnivore.omnivore.feature.settings.SettingRow +import app.omnivore.omnivore.feature.settings.SettingsViewModel +import app.omnivore.omnivore.navigation.Routes + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AboutScreen( + navController: NavHostController, + settingsViewModel: SettingsViewModel = hiltViewModel() +) { + + val uriHandler = LocalUriHandler.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.about_view_title)) }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = null + ) + } + + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + ) + }, + ) { contentPadding -> + LazyColumn( + contentPadding = contentPadding, + ) { + item { + LogoHeader() + } + + item { + SettingRow( + title = stringResource(R.string.about_view_row_whats_new), + onClick = { uriHandler.openUri(RELEASE_URL) }, + ) + } + + item { + SettingRow(title = stringResource(R.string.settings_view_setting_row_documentation)) { + navController.navigate(Routes.Documentation.route) + } + } + + item { + SettingRow(title = stringResource(R.string.settings_view_setting_row_feedback)) { + settingsViewModel.presentIntercom() + } + } + + item { + SettingRow(title = stringResource(R.string.settings_view_setting_row_privacy_policy)) { + navController.navigate(Routes.PrivacyPolicy.route) + } + } + + item { + SettingRow(title = stringResource(R.string.settings_view_setting_row_terms_and_conditions)) { + navController.navigate(Routes.TermsAndConditions.route) + } + } + + item { + SettingRow( + title = stringResource(R.string.about_view_row_version), + subtitle = getVersionName(), + onClick = { }, + ) + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + LinkIcon( + label = stringResource(R.string.website), + icon = Icons.Outlined.Public, + url = "https://omnivore.app", + ) + LinkIcon( + label = "Discord", + icon = ImageVector.vectorResource(R.drawable.ic_discord), + url = "https://discord.gg/h2z5rppzz9", + ) + LinkIcon( + label = "X", + icon = ImageVector.vectorResource(R.drawable.ic_x), + url = "https://x.com/omnivoreapp", + ) + + LinkIcon( + label = "GitHub", + icon = ImageVector.vectorResource(R.drawable.ic_github), + url = "https://github.com/omnivore-app", + ) + } + } + } + } +} + +fun getVersionName(): String { + return when { + BuildConfig.DEBUG -> { + "Debug ${BuildConfig.VERSION_NAME}" + } + else -> { + "Stable ${BuildConfig.VERSION_NAME}" + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/LogoHeader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/LogoHeader.kt new file mode 100644 index 000000000..9f2a77933 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/about/LogoHeader.kt @@ -0,0 +1,34 @@ +package app.omnivore.omnivore.feature.settings.about + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.R + +@Composable +fun LogoHeader() { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(R.drawable.ic_omnivore), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(vertical = 32.dp) + .size(128.dp), + ) + + HorizontalDivider() + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/account/AccountScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/account/AccountScreen.kt new file mode 100644 index 000000000..bb1988e84 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/settings/account/AccountScreen.kt @@ -0,0 +1,84 @@ +package app.omnivore.omnivore.feature.settings.account + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import app.omnivore.omnivore.R +import app.omnivore.omnivore.feature.settings.SettingRow +import app.omnivore.omnivore.feature.settings.SettingsViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AccountScreen( + navController: NavHostController, + snackbarHostState: SnackbarHostState, + settingsViewModel: SettingsViewModel = hiltViewModel() +) { + LaunchedEffect(settingsViewModel.isResettingData) { + if (settingsViewModel.isResettingData) { + val result = snackbarHostState.showSnackbar( + message = settingsViewModel.snackbarMessage, + duration = SnackbarDuration.Indefinite, + ) + when (result) { + SnackbarResult.Dismissed -> { + settingsViewModel.isResettingData = false + } + SnackbarResult.ActionPerformed -> { } + } + } else if (settingsViewModel.isDataResetCompleted) { + snackbarHostState.showSnackbar( + message = "Data reset completed.", + duration = SnackbarDuration.Short + ) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.manage_account_title)) }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = null + ) + } + + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + ) + }, + ) { contentPadding -> + LazyColumn( + contentPadding = contentPadding, + ) { + item { + SettingRow( + title = stringResource(R.string.manage_account_action_reset_data_cache), + onClick = { + settingsViewModel.resetDataCache() + }, + ) + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/web/WebViewScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/web/WebViewScreen.kt new file mode 100644 index 000000000..d0dfc0f9c --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/web/WebViewScreen.kt @@ -0,0 +1,74 @@ +package app.omnivore.omnivore.feature.web + +import android.annotation.SuppressLint +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavHostController +import app.omnivore.omnivore.R +import app.omnivore.omnivore.navigation.Routes + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SetJavaScriptEnabled") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WebViewScreen(navController: NavHostController, url: String) { + Scaffold(topBar = { + TopAppBar(title = { Text(stringResource(R.string.policy_webview_title)) }, actions = { + IconButton(onClick = { navController.navigate(Routes.Settings.route) }) { + Icon( + imageVector = Icons.Default.Settings, contentDescription = null + ) + } + }, colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + }) { + val isDarkMode = isSystemInDarkTheme() + + Box { + AndroidView(factory = { + WebView(it).apply { + if (isDarkMode) { + setBackgroundColor(Color.Transparent.hashCode()) + } + + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + settings.javaScriptEnabled = true + settings.allowContentAccess = true + settings.allowFileAccess = true + settings.domStorageEnabled = true + + alpha = 0.0f + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.animate()?.alpha(1.0f)?.duration = 200 + } + } + loadUrl(url, mutableMapOf()) + } + }, update = {}) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt index 62f1a780b..53a39b62d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/navigation/Routes.kt @@ -3,6 +3,8 @@ package app.omnivore.omnivore.navigation sealed class Routes(val route: String) { object Library : Routes("Library") object Settings : Routes("Settings") + object About : Routes("About") + object Account : Routes("Account") object Search : Routes("Search") object Documentation : Routes("Documentation") object PrivacyPolicy : Routes("PrivacyPolicy") diff --git a/android/Omnivore/app/src/main/res/drawable/ic_discord.xml b/android/Omnivore/app/src/main/res/drawable/ic_discord.xml new file mode 100644 index 000000000..bdec4191f --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_discord.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/Omnivore/app/src/main/res/drawable/ic_github.xml b/android/Omnivore/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..34241eaa3 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/Omnivore/app/src/main/res/drawable/ic_omnivore.xml b/android/Omnivore/app/src/main/res/drawable/ic_omnivore.xml new file mode 100644 index 000000000..ecaad383b --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_omnivore.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/Omnivore/app/src/main/res/drawable/ic_x.xml b/android/Omnivore/app/src/main/res/drawable/ic_x.xml new file mode 100644 index 000000000..9177522c5 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_x.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index d1cce247c..fba0f250b 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -212,6 +212,12 @@ Manage Account Logout + + About + Version + What\'s new + Website + Inbox Non-Feed Items