From 277198948929e6f514e8b5df3b91ddd0c41cdd3a Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 26 Mar 2024 11:45:44 +0800 Subject: [PATCH] feat(reading): support for specifying the composition of shared content (#660) --- .../infrastructure/preference/Preference.kt | 1 + .../infrastructure/preference/Settings.kt | 9 +-- .../preference/SharedContentPreference.kt | 61 +++++++++++++++++++ .../java/me/ash/reader/ui/ext/ContextExt.kt | 10 --- .../java/me/ash/reader/ui/ext/DataStoreExt.kt | 15 ++++- .../java/me/ash/reader/ui/ext/StringExt.kt | 3 + .../ash/reader/ui/page/home/flow/FlowPage.kt | 41 ++++++++++--- .../ash/reader/ui/page/home/reading/TopBar.kt | 8 +-- .../settings/accounts/AccountViewModel.kt | 9 +-- .../settings/interaction/InteractionPage.kt | 29 +++++++++ app/src/main/res/values/strings.xml | 3 + 11 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/infrastructure/preference/SharedContentPreference.kt diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt index 21690c3c5..0d5575b39 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt @@ -82,6 +82,7 @@ fun Preferences.toSettings(): Settings { pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this), openLink = OpenLinkPreference.fromPreferences(this), openLinkSpecificBrowser = OpenLinkSpecificBrowserPreference.fromPreferences(this), + sharedContent = SharedContentPreference.fromPreferences(this), // Languages languages = LanguagesPreference.fromPreferences(this), diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt index 68f1d1de8..0d3bc3cc2 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt @@ -81,6 +81,7 @@ data class Settings( val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default, val openLink: OpenLinkPreference = OpenLinkPreference.default, val openLinkSpecificBrowser: OpenLinkSpecificBrowserPreference = OpenLinkSpecificBrowserPreference.default, + val sharedContent: SharedContentPreference = SharedContentPreference.default, // Languages val languages: LanguagesPreference = LanguagesPreference.default, @@ -195,10 +196,9 @@ val LocalInitialFilter = val LocalArticleListSwipeEndAction = compositionLocalOf { SwipeEndActionPreference.default } val LocalArticleListSwipeStartAction = compositionLocalOf { SwipeStartActionPreference.default } val LocalPullToSwitchArticle = compositionLocalOf { PullToSwitchArticlePreference.default } -val LocalOpenLink = - compositionLocalOf { OpenLinkPreference.default } -val LocalOpenLinkSpecificBrowser = - compositionLocalOf { OpenLinkSpecificBrowserPreference.default } +val LocalOpenLink = compositionLocalOf { OpenLinkPreference.default } +val LocalOpenLinkSpecificBrowser = compositionLocalOf { OpenLinkSpecificBrowserPreference.default } +val LocalSharedContent = compositionLocalOf { SharedContentPreference.default } // Languages val LocalLanguages = @@ -287,6 +287,7 @@ fun SettingsProvider( LocalPullToSwitchArticle provides settings.pullToSwitchArticle, LocalOpenLink provides settings.openLink, LocalOpenLinkSpecificBrowser provides settings.openLinkSpecificBrowser, + LocalSharedContent provides settings.sharedContent, // Languages LocalLanguages provides settings.languages, diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/SharedContentPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/SharedContentPreference.kt new file mode 100644 index 000000000..64ee925b5 --- /dev/null +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/SharedContentPreference.kt @@ -0,0 +1,61 @@ +package me.ash.reader.infrastructure.preference + +import android.content.Context +import android.content.Intent +import androidx.compose.runtime.Stable +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.R +import me.ash.reader.ui.ext.DataStoreKeys +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.orNotEmpty +import me.ash.reader.ui.ext.put + +sealed class SharedContentPreference(val value: Int) : Preference() { + object OnlyLink : SharedContentPreference(0) + object TitleAndLink : SharedContentPreference(1) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put( + DataStoreKeys.SharedContent, + value + ) + } + } + + @Stable + fun toDesc(context: Context): String = + when (this) { + OnlyLink -> context.getString(R.string.only_link) + TitleAndLink -> context.getString(R.string.title_and_link) + } + + fun share(context: Context, title: String?, link: String?) { + when (this) { + OnlyLink -> share(context, link.orEmpty()) + TitleAndLink -> share(context, title.orNotEmpty { it + "\n" } + link.orEmpty()) + } + } + + private fun share(context: Context, content: String) { + context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, content) + type = "text/plain" + }, context.getString(R.string.share))) + } + + companion object { + + val default = OnlyLink + val values = listOf(OnlyLink, TitleAndLink) + + fun fromPreferences(preferences: Preferences): SharedContentPreference = + when (preferences[DataStoreKeys.SharedContent.key]) { + 0 -> OnlyLink + 1 -> TitleAndLink + else -> default + } + } +} diff --git a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt index 6910acc2e..e263dda24 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt @@ -66,16 +66,6 @@ fun Context.showToastLong(message: String?) { showToast(message, Toast.LENGTH_LONG) } -fun Context.share(content: String) { - startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { - putExtra( - Intent.EXTRA_TEXT, - content, - ) - type = "text/plain" - }, getString(R.string.share))) -} - fun Context.openURL( url: String?, openLink: OpenLinkPreference, diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index dd48d5a6a..37748651c 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -3,7 +3,14 @@ package me.ash.reader.ui.ext import android.content.Context import android.util.Log import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.doublePreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch @@ -439,6 +446,12 @@ sealed class DataStoreKeys { get() = stringPreferencesKey("openLppSpecificBrowser") } + object SharedContent : DataStoreKeys() { + + override val key: Preferences.Key + get() = intPreferencesKey("sharedContent") + } + // Languages object Languages : DataStoreKeys() { diff --git a/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt b/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt index fd66ae340..c453e7b78 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt @@ -35,3 +35,6 @@ fun String.md5(): String = .toString(16).padStart(32, '0') fun String?.decodeHTML(): String? = this?.run { Html.fromHtml(this).toString() } + +fun String?.orNotEmpty(l: (value: String) -> String): String = + if (this.isNullOrBlank()) "" else l(this) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index d2d87d3e6..d7e5e0c8d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -1,7 +1,13 @@ package me.ash.reader.ui.page.home.flow import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons @@ -9,11 +15,18 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -28,11 +41,22 @@ import me.ash.reader.R import me.ash.reader.domain.model.article.ArticleWithFeed import me.ash.reader.domain.model.general.Filter import me.ash.reader.domain.model.general.MarkAsReadConditions -import me.ash.reader.infrastructure.preference.* +import me.ash.reader.infrastructure.preference.LocalFlowArticleListDateStickyHeader +import me.ash.reader.infrastructure.preference.LocalFlowArticleListFeedIcon +import me.ash.reader.infrastructure.preference.LocalFlowArticleListTonalElevation +import me.ash.reader.infrastructure.preference.LocalFlowFilterBarFilled +import me.ash.reader.infrastructure.preference.LocalFlowFilterBarPadding +import me.ash.reader.infrastructure.preference.LocalFlowFilterBarStyle +import me.ash.reader.infrastructure.preference.LocalFlowFilterBarTonalElevation +import me.ash.reader.infrastructure.preference.LocalFlowTopBarTonalElevation +import me.ash.reader.infrastructure.preference.LocalSharedContent import me.ash.reader.ui.component.FilterBar -import me.ash.reader.ui.component.base.* +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYExtensibleVisibility +import me.ash.reader.ui.component.base.RYScaffold +import me.ash.reader.ui.component.base.SwipeRefresh import me.ash.reader.ui.ext.collectAsStateValue -import me.ash.reader.ui.ext.share import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.home.HomeViewModel @@ -55,6 +79,7 @@ fun FlowPage( val filterBarFilled = LocalFlowFilterBarFilled.current val filterBarPadding = LocalFlowFilterBarPadding.current val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current + val sharedContent = LocalSharedContent.current val context = LocalContext.current val homeUiState = homeViewModel.homeUiState.collectAsStateValue() @@ -126,9 +151,7 @@ fun FlowPage( val onShare: ((ArticleWithFeed) -> Unit)? = remember { { articleWithFeed -> with(articleWithFeed.article) { - context.share( - arrayOf(title, link).filter { it.isNotBlank() }.joinToString(separator = "\n") - ) + sharedContent.share(context, title, link) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt index 8b105582b..205ab715e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavHostController import me.ash.reader.R import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation +import me.ash.reader.infrastructure.preference.LocalSharedContent import me.ash.reader.ui.component.base.FeedbackIconButton import me.ash.reader.ui.component.base.RYExtensibleVisibility -import me.ash.reader.ui.ext.share import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.page.common.RouteName @@ -40,6 +40,7 @@ fun TopBar( ) { val context = LocalContext.current val tonalElevation = LocalReadingPageTonalElevation.current + val sharedContent = LocalSharedContent.current Box( modifier = Modifier @@ -78,10 +79,7 @@ fun TopBar( contentDescription = stringResource(R.string.share), tint = MaterialTheme.colorScheme.onSurface, ) { - context.share(title - ?.takeIf { it.isNotBlank() } - ?.let { it + "\n" } + link - ) + sharedContent.share(context, title, link) } }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(tonalElevation.value.dp), diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt index 32da0a009..192261702 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt @@ -1,6 +1,5 @@ package me.ash.reader.ui.page.settings.accounts -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -60,12 +59,8 @@ class AccountViewModel @Inject constructor( fun exportAsOPML(accountId: Int, callback: (String) -> Unit = {}) { viewModelScope.launch(defaultDispatcher) { - try { - callback(opmlService.saveToString(accountId, - _accountUiState.value.exportOPMLMode == ExportOPMLMode.ATTACH_INFO)) - } catch (e: Exception) { - Log.e("FeedsViewModel", "exportAsOpml: ", e) - } + callback(opmlService.saveToString(accountId, + _accountUiState.value.exportOPMLMode == ExportOPMLMode.ATTACH_INFO)) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt index a4d729928..ce35decfd 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt @@ -31,7 +31,9 @@ import me.ash.reader.infrastructure.preference.LocalInitialPage import me.ash.reader.infrastructure.preference.LocalOpenLink import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle +import me.ash.reader.infrastructure.preference.LocalSharedContent import me.ash.reader.infrastructure.preference.OpenLinkPreference +import me.ash.reader.infrastructure.preference.SharedContentPreference import me.ash.reader.infrastructure.preference.SwipeEndActionPreference import me.ash.reader.infrastructure.preference.SwipeStartActionPreference import me.ash.reader.ui.component.base.DisplayText @@ -57,6 +59,7 @@ fun InteractionPage( val pullToSwitchArticle = LocalPullToSwitchArticle.current val openLink = LocalOpenLink.current val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current + val sharedContent = LocalSharedContent.current val scope = rememberCoroutineScope() val isOpenLinkSpecificBrowserItemEnabled = remember(openLink) { openLink == OpenLinkPreference.SpecificBrowser @@ -67,6 +70,7 @@ fun InteractionPage( var swipeEndDialogVisible by remember { mutableStateOf(false) } var openLinkDialogVisible by remember { mutableStateOf(false) } var openLinkSpecificBrowserDialogVisible by remember { mutableStateOf(false) } + var sharedContentDialogVisible by remember { mutableStateOf(false) } RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, @@ -158,6 +162,17 @@ fun InteractionPage( } }, ) {} + Subtitle( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource(R.string.share), + ) + SettingItem( + title = stringResource(R.string.shared_content), + desc = sharedContent.toDesc(context), + onClick = { + sharedContentDialogVisible = true + }, + ) {} } item { Spacer(modifier = Modifier.height(24.dp)) @@ -264,4 +279,18 @@ fun InteractionPage( } ) + RadioDialog( + visible = sharedContentDialogVisible, + title = stringResource(R.string.shared_content), + options = SharedContentPreference.values.map { + RadioDialogOption( + text = it.toDesc(context), + selected = it == sharedContent, + ) { + it.put(context, scope) + } + }, + ) { + sharedContentDialogVisible = false + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc2621230..13653fd0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -427,4 +427,7 @@ Save Image saved Permission denied + Shared content + Only link + Title and link