Merge pull request #3570 from stefanosansone/refactor/android-settings

Android - Settings screen refactor
This commit is contained in:
Jackson Harper 2024-02-26 10:36:42 +08:00 committed by GitHub
commit f318923eed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 727 additions and 406 deletions

View file

@ -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

View file

@ -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
)
}
}

View file

@ -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 =

View file

@ -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")
}
}
}

View file

@ -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))
}
})
}

View file

@ -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)
}
}
}
}

View file

@ -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 = {}
)
}
}
}

View file

@ -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
)
}
}
}

View file

@ -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"

View file

@ -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)
}
}
}
}

View file

@ -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}"
}
}
}

View file

@ -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()
}
}

View file

@ -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()
},
)
}
}
}
}

View file

@ -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 = {})
}
}
}

View file

@ -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")

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="127.14dp"
android:height="96.36dp"
android:viewportWidth="127.14"
android:viewportHeight="96.36">
<path
android:pathData="M107.7,8.07A105.15,105.15 0,0 0,81.47 0a72.06,72.06 0,0 0,-3.36 6.83A97.68,97.68 0,0 0,49 6.83,72.37 72.37,0 0,0 45.64,0 105.89,105.89 0,0 0,19.39 8.09C2.79,32.65 -1.71,56.6 0.54,80.21h0A105.73,105.73 0,0 0,32.71 96.36,77.7 77.7,0 0,0 39.6,85.25a68.42,68.42 0,0 1,-10.85 -5.18c0.91,-0.66 1.8,-1.34 2.66,-2a75.57,75.57 0,0 0,64.32 0c0.87,0.71 1.76,1.39 2.66,2a68.68,68.68 0,0 1,-10.87 5.19,77 77,0 0,0 6.89,11.1A105.25,105.25 0,0 0,126.6 80.22h0C129.24,52.84 122.09,29.11 107.7,8.07ZM42.45,65.69C36.18,65.69 31,60 31,53s5,-12.74 11.43,-12.74S54,46 53.89,53 48.84,65.69 42.45,65.69ZM84.69,65.69C78.41,65.69 73.25,60 73.25,53s5,-12.74 11.44,-12.74S96.23,46 96.12,53 91.08,65.69 84.69,65.69Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="98dp"
android:height="96dp"
android:viewportWidth="98"
android:viewportHeight="96">
<path
android:pathData="M48.854,0C21.839,0 0,22 0,49.217c0,21.756 13.993,40.172 33.405,46.69 2.427,0.49 3.316,-1.059 3.316,-2.362 0,-1.141 -0.08,-5.052 -0.08,-9.127 -13.59,2.934 -16.42,-5.867 -16.42,-5.867 -2.184,-5.704 -5.42,-7.17 -5.42,-7.17 -4.448,-3.015 0.324,-3.015 0.324,-3.015 4.934,0.326 7.523,5.052 7.523,5.052 4.367,7.496 11.404,5.378 14.235,4.074 0.404,-3.178 1.699,-5.378 3.074,-6.6 -10.839,-1.141 -22.243,-5.378 -22.243,-24.283 0,-5.378 1.94,-9.778 5.014,-13.2 -0.485,-1.222 -2.184,-6.275 0.486,-13.038 0,0 4.125,-1.304 13.426,5.052a46.97,46.97 0,0 1,12.214 -1.63c4.125,0 8.33,0.571 12.213,1.63 9.302,-6.356 13.427,-5.052 13.427,-5.052 2.67,6.763 0.97,11.816 0.485,13.038 3.155,3.422 5.015,7.822 5.015,13.2 0,18.905 -11.404,23.06 -22.324,24.283 1.78,1.548 3.316,4.481 3.316,9.126 0,6.6 -0.08,11.897 -0.08,13.526 0,1.304 0.89,2.853 3.316,2.364 19.412,-6.52 33.405,-24.935 33.405,-46.691C97.707,22 75.788,0 48.854,0z"
android:fillColor="#24292f"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="256dp"
android:height="256dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M84.86,53.22C84.86,55.2 84.38,57.6 83.89,59.69V59.71L83.89,59.72C83.29,62.16 81.89,64.33 79.91,65.88C77.93,67.42 75.49,68.26 72.98,68.26H72.84C66.49,68.26 62.49,63.05 62.49,57.21V48.85L57.76,55.86L57.5,56.07C56.52,56.89 55.28,57.34 54,57.34C52.72,57.34 51.48,56.89 50.5,56.07L50.24,55.87L45.37,48.76V66H40.72V47.23C40.72,43.68 44.93,41.61 47.73,44.1L47.94,44.29L53.64,52.61C53.75,52.66 53.87,52.69 53.99,52.69C54.12,52.69 54.23,52.66 54.34,52.61L59.93,44.33L60.18,44.12C62.84,41.91 67.14,43.58 67.14,47.3V57.21C67.14,61 69.55,63.61 72.84,63.61H72.98C76.02,63.61 78.64,61.55 79.37,58.62C79.86,56.54 80.21,54.61 80.21,53.24C80.14,38.26 67.47,26.82 52.13,27.86C39.2,28.75 28.75,39.2 27.85,52.14C27.6,55.72 28.09,59.32 29.3,62.71C30.5,66.1 32.38,69.2 34.84,71.83C37.29,74.45 40.26,76.55 43.55,77.98C46.85,79.41 50.41,80.15 54,80.14V84.79C49.77,84.8 45.58,83.93 41.7,82.24C37.82,80.56 34.33,78.09 31.44,75C28.55,71.91 26.33,68.26 24.91,64.27C23.5,60.28 22.92,56.05 23.21,51.82C24.27,36.56 36.56,24.27 51.81,23.21C69.69,22.01 84.78,35.42 84.86,53.22Z"
android:fillColor="#FFFFFF"
tools:ignore="VectorPath" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1200dp"
android:height="1227dp"
android:viewportWidth="1200"
android:viewportHeight="1227">
<path
android:pathData="M714.2,519.3L1160.9,0H1055L667.1,450.9L357.3,0H0L468.5,681.8L0,1226.4H105.9L515.5,750.2L842.7,1226.4H1200L714.1,519.3H714.2ZM569.2,687.8L521.7,619.9L144,79.7H306.6L611.4,515.7L658.9,583.6L1055.1,1150.3H892.5L569.2,687.9V687.8Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -212,6 +212,12 @@
<string name="settings_view_setting_row_manage_account">Manage Account</string>
<string name="settings_view_setting_row_logout">Logout</string>
<!-- About -->
<string name="about_view_title">About</string>
<string name="about_view_row_version">Version</string>
<string name="about_view_row_whats_new">What\'s new</string>
<string name="website">Website</string>
<!-- stock filters -->
<string name="library_filter_inbox">Inbox</string>
<string name="library_filter_non_feed">Non-Feed Items</string>