diff --git a/app/src/main/java/com/capyreader/app/common/AppPreferences.kt b/app/src/main/java/com/capyreader/app/common/AppPreferences.kt index 7355b976..e5aa0688 100644 --- a/app/src/main/java/com/capyreader/app/common/AppPreferences.kt +++ b/app/src/main/java/com/capyreader/app/common/AppPreferences.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.preference.PreferenceManager import com.capyreader.app.refresher.RefreshInterval import com.capyreader.app.ui.articles.ArticleListFontScale +import com.capyreader.app.ui.settings.ArticleVerticalSwipe import com.jocmp.capy.ArticleFilter import com.jocmp.capy.articles.FontOption import com.jocmp.capy.articles.TextSize @@ -19,6 +20,8 @@ class AppPreferences(context: Context) { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) ) + val readerOptions = ReaderOptions(preferenceStore) + val articleListOptions = ArticleListOptions(preferenceStore) val accountID: Preference @@ -50,17 +53,25 @@ class AppPreferences(context: Context) { val enableStickyFullContent: Preference get() = preferenceStore.getBoolean("enable_sticky_full_content", false) - val pinArticleTopBar: Preference - get() = preferenceStore.getBoolean("article_pin_top_bar", true) + fun clearAll() { + preferenceStore.clearAll() + } + + class ReaderOptions(private val preferenceStore: PreferenceStore) { + val pinToolbars: Preference + get() = preferenceStore.getBoolean("article_pin_top_bar", true) - val textSize: Preference - get() = preferenceStore.getEnum("article_text_size", TextSize.default) + val textSize: Preference + get() = preferenceStore.getEnum("article_text_size", TextSize.default) - val fontOption: Preference - get() = preferenceStore.getEnum("article_font_family", FontOption.default) + val fontFamily: Preference + get() = preferenceStore.getEnum("article_font_family", FontOption.default) - fun clearAll() { - preferenceStore.clearAll() + val topSwipeGesture: Preference + get() = preferenceStore.getEnum("article_top_swipe_gesture", ArticleVerticalSwipe.PREVIOUS_ARTICLE) + + val bottomSwipeGesture: Preference + get() = preferenceStore.getEnum("article_bottom_swipe_gesture", ArticleVerticalSwipe.NEXT_ARTICLE) } class ArticleListOptions(private val preferenceStore: PreferenceStore) { diff --git a/app/src/main/java/com/capyreader/app/common/ImagePreview.kt b/app/src/main/java/com/capyreader/app/common/ImagePreview.kt index e2e24b01..d027b616 100644 --- a/app/src/main/java/com/capyreader/app/common/ImagePreview.kt +++ b/app/src/main/java/com/capyreader/app/common/ImagePreview.kt @@ -1,10 +1,20 @@ package com.capyreader.app.common -enum class ImagePreview { +import com.capyreader.app.R +import com.capyreader.app.ui.settings.Translated + +enum class ImagePreview : Translated { NONE, SMALL, LARGE; + override val translationKey: Int + get() = when (this) { + NONE -> R.string.image_preview_menu_option_none + SMALL -> R.string.image_preview_menu_option_small + LARGE -> R.string.image_preview_menu_option_large + } + companion object { val default = SMALL diff --git a/app/src/main/java/com/capyreader/app/common/ThemeOption.kt b/app/src/main/java/com/capyreader/app/common/ThemeOption.kt index 453be687..2c150ef6 100644 --- a/app/src/main/java/com/capyreader/app/common/ThemeOption.kt +++ b/app/src/main/java/com/capyreader/app/common/ThemeOption.kt @@ -1,10 +1,20 @@ package com.capyreader.app.common -enum class ThemeOption { +import com.capyreader.app.R +import com.capyreader.app.ui.settings.Translated + +enum class ThemeOption : Translated { LIGHT, DARK, SYSTEM_DEFAULT; + override val translationKey: Int + get() = when(this) { + ThemeOption.LIGHT -> R.string.theme_menu_option_light + ThemeOption.DARK -> R.string.theme_menu_option_dark + ThemeOption.SYSTEM_DEFAULT -> R.string.theme_menu_option_system_default + } + companion object { val default = SYSTEM_DEFAULT diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt index 53fe3e2d..d5f8f5df 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt @@ -133,8 +133,8 @@ fun ArticleLayout( } fun scrollToArticle(index: Int) { - if (index > -1) { - coroutineScope.launch { + coroutineScope.launch { + if (index > -1) { listState.animateScrollToItem(index) } } @@ -336,8 +336,9 @@ fun ArticleLayout( CapyPlaceholder() } } else if (article != null) { + val compact = isCompact() val indexedArticles = - rememberIndexedArticles(article = article, articles = articles) + rememberIndexedArticles(article = article, articles = pagingArticles) ArticleView( article = article, @@ -353,6 +354,12 @@ fun ArticleLayout( onSelectArticle(id) }, ) + + LaunchedEffect(article.id, indexedArticles.index) { + if (!compact) { + scrollToArticle(indexedArticles.index) + } + } } } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt index 9d16d496..273683fa 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt @@ -162,6 +162,8 @@ class ArticleScreenViewModel( refreshJob?.cancel() refreshJob = viewModelScope.launch(Dispatchers.IO) { + updateArticlesSince() + account.refresh().onFailure { throwable -> if (throwable is UnauthorizedError && _showUnauthorizedMessage == UnauthorizedMessageState.HIDE) { _showUnauthorizedMessage = UnauthorizedMessageState.SHOW @@ -177,8 +179,6 @@ class ArticleScreenViewModel( val article = buildArticle(articleID) ?: return@launch _article = article - updateArticlesSince() - markRead(articleID) if (article.fullContent == Article.FullContentState.LOADING) { @@ -220,8 +220,6 @@ class ArticleScreenViewModel( } fun clearArticle() { - updateArticlesSince() - _article = null } @@ -271,6 +269,8 @@ class ArticleScreenViewModel( } private fun selectArticleFilter(nextFilter: ArticleFilter) { + updateArticlesSince() + updateFilterValue(nextFilter) clearArticle() diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt index 716bac5c..34a24de0 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt @@ -21,8 +21,8 @@ internal val articlesModule = module { single { ArticleRenderer( context = get(), - textSize = get().textSize, - fontOption = get().fontOption, + textSize = get().readerOptions.textSize, + fontOption = get().readerOptions.fontFamily, ) } viewModel { diff --git a/app/src/main/java/com/capyreader/app/ui/articles/IndexedArticles.kt b/app/src/main/java/com/capyreader/app/ui/articles/IndexedArticles.kt index df815092..d4d08c2a 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/IndexedArticles.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/IndexedArticles.kt @@ -3,12 +3,13 @@ package com.capyreader.app.ui.articles import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.jocmp.capy.Article import kotlinx.coroutines.flow.Flow data class IndexedArticles( - private val index: Int, + val index: Int, private val next: Int, private val previous: Int, private val articles: List, @@ -22,16 +23,18 @@ data class IndexedArticles( } fun hasNext(): Boolean { - return next < articles.size + return next < size } + + val size = articles.size } @Composable fun rememberIndexedArticles( article: Article, - articles: Flow> + articles: LazyPagingItems
): IndexedArticles { - val snapshot = articles.collectAsLazyPagingItems().itemSnapshotList + val snapshot = articles.itemSnapshotList return remember(article, snapshot.size) { val index = snapshot.indexOfFirst { it?.id == article.id } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStyleListener.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStyleListener.kt index ac399358..0219a174 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStyleListener.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStyleListener.kt @@ -14,8 +14,8 @@ import org.koin.compose.koinInject @Composable fun ArticleStyleListener(webView: WebView?, appPreferences: AppPreferences = koinInject()) { - val textSize by appPreferences.textSize.collectChanges() - val fontFamily by appPreferences.fontOption.collectChanges() + val textSize by appPreferences.readerOptions.textSize.collectChanges() + val fontFamily by appPreferences.readerOptions.fontFamily.collectChanges() LaunchedEffect(fontFamily) { if (webView != null) { diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStylePicker.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStylePicker.kt index f1aec47b..d78fa1b4 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStylePicker.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleStylePicker.kt @@ -29,9 +29,9 @@ fun ArticleStylePicker( ) { val textSizes = TextSize.sorted - var fontFamily by remember { mutableStateOf(appPreferences.fontOption.get()) } + var fontFamily by remember { mutableStateOf(appPreferences.readerOptions.fontFamily.get()) } var sliderPosition by remember { - mutableFloatStateOf(textSizes.indexOf(appPreferences.textSize.get()).toFloat()) + mutableFloatStateOf(textSizes.indexOf(appPreferences.readerOptions.textSize.get()).toFloat()) } Column( @@ -40,7 +40,7 @@ fun ArticleStylePicker( ArticleFontMenu( updateFontFamily = { font -> fontFamily = font - appPreferences.fontOption.set(font) + appPreferences.readerOptions.fontFamily.set(font) onChange() }, fontOption = fontFamily @@ -57,7 +57,7 @@ fun ArticleStylePicker( value = sliderPosition, onValueChange = { sliderPosition = it - appPreferences.textSize.set(TextSize.sorted[it.roundToInt()]) + appPreferences.readerOptions.textSize.set(TextSize.sorted[it.roundToInt()]) onChange() } ) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt index f951168a..c2f34c9a 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt @@ -8,13 +8,16 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Article +import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -29,6 +32,8 @@ import com.capyreader.app.common.AppPreferences import com.capyreader.app.ui.articles.IndexedArticles import com.capyreader.app.ui.articles.LocalFullContent import com.capyreader.app.ui.components.pullrefresh.SwipeRefresh +import com.capyreader.app.ui.settings.ArticleVerticalSwipe +import com.capyreader.app.ui.settings.ArticleVerticalSwipe.LOAD_FULL_CONTENT import com.jocmp.capy.Article import org.koin.compose.koinInject @@ -59,7 +64,7 @@ fun ArticleView( selectArticle { articles.next() } } - fun onToggleExtractContent() { + val onToggleFullContent = { if (article.fullContent == Article.FullContentState.LOADED) { fullContent.reset() } else if (article.fullContent != Article.FullContentState.LOADING) { @@ -81,16 +86,18 @@ fun ArticleView( ) { Column { ArticlePullRefresh( - article, showBars, - articles = articles, - onRequestPrevious = onRequestPrevious, + onToggleFullContent = onToggleFullContent, onRequestNext = onRequestNext, + onRequestPrevious = onRequestPrevious, + articles = articles, ) { - ArticleReader( - article = article, - scrollState = scrollState - ) + key(article.id) { + ArticleReader( + article = article, + scrollState = scrollState + ) + } } } @@ -99,7 +106,8 @@ fun ArticleView( visible = showBars ) { ArticleBottomBar( - onRequestNext = onRequestNext + onRequestNext = onRequestNext, + showNext = articles.hasNext() ) } } @@ -107,7 +115,7 @@ fun ArticleView( BarVisibility(visible = showBars) { ArticleTopBar( article = article, - onToggleExtractContent = ::onToggleExtractContent, + onToggleExtractContent = onToggleFullContent, onToggleRead = onToggleRead, onToggleStar = onToggleStar, onClose = onBackPressed @@ -123,28 +131,45 @@ fun ArticleView( @Composable fun ArticlePullRefresh( - article: Article, showBars: Boolean, + onToggleFullContent: () -> Unit, onRequestNext: () -> Unit, onRequestPrevious: () -> Unit, articles: IndexedArticles, content: @Composable () -> Unit, ) { + val (topSwipe, bottomSwipe) = rememberSwipePreferences() val haptics = LocalHapticFeedback.current val triggerThreshold = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) } + val onPullUp = { + if (topSwipe == LOAD_FULL_CONTENT) { + onToggleFullContent() + } else { + onRequestPrevious() + } + } + + val enableTopSwipe = topSwipe.enabled && + (topSwipe == LOAD_FULL_CONTENT || (topSwipe.openArticle && articles.hasPrevious())) + SwipeRefresh( - onRefresh = { onRequestPrevious() }, - swipeEnabled = articles.hasPrevious(), + onRefresh = { onPullUp() }, + swipeEnabled = enableTopSwipe, + icon = if (topSwipe == LOAD_FULL_CONTENT) { + Icons.AutoMirrored.Rounded.Article + } else { + Icons.Rounded.KeyboardArrowUp + }, indicatorPadding = PaddingValues(top = TopBarContainerHeight), onTriggerThreshold = { triggerThreshold() } ) { SwipeRefresh( onRefresh = { onRequestNext() }, - swipeEnabled = articles.hasNext(), + swipeEnabled = bottomSwipe.enabled && articles.hasNext(), onTriggerThreshold = { triggerThreshold() }, indicatorPadding = PaddingValues( bottom = if (showBars) { @@ -155,9 +180,7 @@ fun ArticlePullRefresh( ), indicatorAlignment = Alignment.BottomCenter, ) { - key(article.id) { - content() - } + content() } } } @@ -165,7 +188,7 @@ fun ArticlePullRefresh( private val TopBarContainerHeight = 64.dp @Composable -fun BoxScope.BarVisibility( +fun BarVisibility( modifier: Modifier = Modifier, visible: Boolean, content: @Composable () -> Unit @@ -185,7 +208,7 @@ fun canShowBars( scrollState: ScrollState, appPreferences: AppPreferences = koinInject(), ): Boolean { - val pinBars by appPreferences.pinArticleTopBar + val pinBars by appPreferences.readerOptions.pinToolbars .stateIn(rememberCoroutineScope()) .collectAsState() @@ -193,3 +216,23 @@ fun canShowBars( scrollState.lastScrolledBackward || scrollState.value == 0 } + +@Composable +private fun rememberSwipePreferences(appPreferences: AppPreferences = koinInject()): SwipePreferences { + val coroutineScope = rememberCoroutineScope() + val topSwipe by appPreferences.readerOptions.topSwipeGesture + .stateIn(coroutineScope) + .collectAsState() + + val bottomSwipe by appPreferences.readerOptions.bottomSwipeGesture + .stateIn(coroutineScope) + .collectAsState() + + return SwipePreferences(topSwipe, bottomSwipe) +} + +@Stable +private data class SwipePreferences( + val topSwipe: ArticleVerticalSwipe, + val bottomSwipe: ArticleVerticalSwipe, +) diff --git a/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/PullRefreshIndicator.kt b/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/PullRefreshIndicator.kt index 0372b629..adf53edb 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/PullRefreshIndicator.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/PullRefreshIndicator.kt @@ -23,9 +23,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -90,6 +92,7 @@ fun SwipeRefreshIndicator( refreshTriggerDistance: Dp, modifier: Modifier = Modifier, clockwise: Boolean = true, + icon: ImageVector, fade: Boolean = true, scale: Boolean = false, backgroundColor: Color = MaterialTheme.colorScheme.onSurface, @@ -175,11 +178,15 @@ fun SwipeRefreshIndicator( contentAlignment = Alignment.Center ) { Image( - imageVector = if (clockwise) { - Icons.Rounded.KeyboardArrowUp - } else { - Icons.Rounded.KeyboardArrowDown - }, contentDescription = null + imageVector = icon, + contentDescription = null, + modifier = Modifier.rotate( + if (clockwise) { + 0f + } else { + 180f + } + ) ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/SwipeRefresh.kt b/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/SwipeRefresh.kt index 3ed4b6d5..35220bad 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/SwipeRefresh.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/pullrefresh/SwipeRefresh.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -21,6 +23,7 @@ import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -236,10 +239,12 @@ fun SwipeRefresh( refreshTriggerDistance: Dp = 80.dp, indicatorAlignment: Alignment = Alignment.TopCenter, indicatorPadding: PaddingValues = PaddingValues(0.dp), + icon: ImageVector = Icons.Rounded.KeyboardArrowUp, indicator: @Composable (state: SwipeRefreshState, refreshTrigger: Dp) -> Unit = { s, trigger -> SwipeRefreshIndicator( state = s, refreshTriggerDistance = trigger, + icon = icon, clockwise = (indicatorAlignment as BiasAlignment).verticalBias != 1f ) }, diff --git a/app/src/main/java/com/capyreader/app/ui/settings/ArticleListSettings.kt b/app/src/main/java/com/capyreader/app/ui/settings/ArticleListSettings.kt index db6e326c..7f0bc0b2 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/ArticleListSettings.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/ArticleListSettings.kt @@ -55,9 +55,13 @@ fun ArticleListSettings( checked = options.showSummary, title = stringResource(R.string.settings_article_list_summary) ) - ImagePreviewMenu( - onUpdateImagePreview = options.updateImagePreview, - imagePreview = options.imagePreview + + PreferenceDropdown( + selected = options.imagePreview, + update = options.updateImagePreview, + options = ImagePreview.sorted, + label = R.string.image_preview_label, + disabledOption = ImagePreview.NONE ) FormSection( diff --git a/app/src/main/java/com/capyreader/app/ui/settings/ArticleVerticalSwipe.kt b/app/src/main/java/com/capyreader/app/ui/settings/ArticleVerticalSwipe.kt new file mode 100644 index 00000000..51c4c0bc --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/ArticleVerticalSwipe.kt @@ -0,0 +1,37 @@ +package com.capyreader.app.ui.settings + +import com.capyreader.app.R + +enum class ArticleVerticalSwipe : Translated { + DISABLED, + PREVIOUS_ARTICLE, + NEXT_ARTICLE, + LOAD_FULL_CONTENT; + + override val translationKey: Int + get() = when (this) { + DISABLED -> R.string.article_vertical_swipe_disabled + PREVIOUS_ARTICLE -> R.string.article_vertical_swipe_previous_article + NEXT_ARTICLE -> R.string.article_vertical_swipe_next_article + LOAD_FULL_CONTENT -> R.string.article_vertical_swipe_full_content + } + + val enabled: Boolean + get() = this != DISABLED + + val openArticle: Boolean + get() = this == PREVIOUS_ARTICLE || this == NEXT_ARTICLE + + companion object { + val topOptions = listOf( + DISABLED, + PREVIOUS_ARTICLE, + LOAD_FULL_CONTENT, + ) + + val bottomOptions = listOf( + DISABLED, + NEXT_ARTICLE, + ) + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsPanel.kt index 6b732b6f..cc129598 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsPanel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsPanel.kt @@ -19,7 +19,6 @@ fun DisplaySettingsPanel( viewModel: DisplaySettingsViewModel = koinViewModel(), ) { DisplaySettingsPanelView( - onUpdateTheme = viewModel::updateTheme, theme = viewModel.theme, readerOptions = ReaderOptions( @@ -55,7 +54,12 @@ fun DisplaySettingsPanelView( ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { RowItem { - ThemeMenu(onUpdateTheme = onUpdateTheme, theme = theme) + PreferenceDropdown( + selected = theme, + update = onUpdateTheme, + options = ThemeOption.sorted, + label = R.string.theme_menu_label + ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsViewModel.kt b/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsViewModel.kt index 7c545e7b..02cac528 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/DisplaySettingsViewModel.kt @@ -32,7 +32,7 @@ class DisplaySettingsViewModel( var fontScale by mutableStateOf(appPreferences.articleListOptions.fontScale.get()) private set - var pinArticleTopBar by mutableStateOf(appPreferences.pinArticleTopBar.get()) + var pinArticleTopBar by mutableStateOf(appPreferences.readerOptions.pinToolbars.get()) private set val imagePreview: ImagePreview @@ -57,7 +57,7 @@ class DisplaySettingsViewModel( } fun updatePinTopBar(pinTopBar: Boolean) { - appPreferences.pinArticleTopBar.set(pinTopBar) + appPreferences.readerOptions.pinToolbars.set(pinTopBar) this.pinArticleTopBar = pinTopBar } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsPanel.kt new file mode 100644 index 00000000..8093c02a --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsPanel.kt @@ -0,0 +1,63 @@ +package com.capyreader.app.ui.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.capyreader.app.R +import com.capyreader.app.ui.components.FormSection +import com.capyreader.app.ui.theme.CapyTheme +import org.koin.androidx.compose.koinViewModel + +@Composable +fun GesturesSettingPanel( + viewModel: GesturesSettingsViewModel = koinViewModel(), +) { + GesturesSettingsPanelView( + updateReaderTopSwipe = viewModel::updateReaderTopSwipe, + updateReaderBottomSwipe = viewModel::updateReaderBottomSwipe, + topSwipe = viewModel.topSwipe, + bottomSwipe = viewModel.bottomSwipe, + ) +} + +@Composable +private fun GesturesSettingsPanelView( + updateReaderTopSwipe: (swipe: ArticleVerticalSwipe) -> Unit, + updateReaderBottomSwipe: (swipe: ArticleVerticalSwipe) -> Unit, + topSwipe: ArticleVerticalSwipe, + bottomSwipe: ArticleVerticalSwipe, +) { + FormSection(title = stringResource(R.string.settings_reader_title)) { + RowItem { + PreferenceDropdown( + selected = topSwipe, + update = updateReaderTopSwipe, + options = ArticleVerticalSwipe.topOptions, + label = R.string.settings_gestures_reader_swipe_down, + disabledOption = ArticleVerticalSwipe.DISABLED, + ) + } + RowItem { + PreferenceDropdown( + selected = bottomSwipe, + update = updateReaderBottomSwipe, + options = ArticleVerticalSwipe.bottomOptions, + label = R.string.settings_gestures_reader_swipe_up, + disabledOption = ArticleVerticalSwipe.DISABLED, + ) + } + } +} + +@Preview +@Composable +fun GesturesSettingsPanelPreview() { + CapyTheme { + GesturesSettingsPanelView( + updateReaderTopSwipe = {}, + updateReaderBottomSwipe = {}, + topSwipe = ArticleVerticalSwipe.PREVIOUS_ARTICLE, + bottomSwipe = ArticleVerticalSwipe.NEXT_ARTICLE + ) + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsViewModel.kt b/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsViewModel.kt new file mode 100644 index 00000000..e4061b59 --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/GesturesSettingsViewModel.kt @@ -0,0 +1,30 @@ +package com.capyreader.app.ui.settings + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.capyreader.app.common.AppPreferences + +class GesturesSettingsViewModel(private val appPreferences: AppPreferences) : ViewModel() { + var topSwipe by mutableStateOf(readerOptions.topSwipeGesture.get()) + private set + + var bottomSwipe by mutableStateOf(readerOptions.bottomSwipeGesture.get()) + private set + + fun updateReaderTopSwipe(swipe: ArticleVerticalSwipe) { + topSwipe = swipe + + readerOptions.topSwipeGesture.set(swipe) + } + + fun updateReaderBottomSwipe(swipe: ArticleVerticalSwipe) { + bottomSwipe = swipe + + readerOptions.bottomSwipeGesture.set(swipe) + } + + val readerOptions: AppPreferences.ReaderOptions + get() = appPreferences.readerOptions +} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/ImagePreviewMenu.kt b/app/src/main/java/com/capyreader/app/ui/settings/ImagePreviewMenu.kt deleted file mode 100644 index 942acc97..00000000 --- a/app/src/main/java/com/capyreader/app/ui/settings/ImagePreviewMenu.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.capyreader.app.ui.settings - -import android.content.Context -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MenuAnchorType.Companion.PrimaryNotEditable -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.capyreader.app.R -import com.capyreader.app.common.ImagePreview - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ImagePreviewMenu( - onUpdateImagePreview: (preview: ImagePreview) -> Unit, - imagePreview: ImagePreview, -) { - val context = LocalContext.current - val (expanded, setExpanded) = remember { mutableStateOf(false) } - val options = ImagePreview.sorted.map { - it to context.translationKey(it) - } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { setExpanded(it) }, - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - value = context.translationKey(imagePreview), - onValueChange = {}, - label = { Text(stringResource(R.string.image_preview_label)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { setExpanded(false) } - ) { - options.forEach { (option, text) -> - DropdownMenuItem( - text = { Text(text) }, - onClick = { - onUpdateImagePreview(option) - setExpanded(false) - } - ) - if (option == ImagePreview.NONE) { - HorizontalDivider() - } - } - } - } -} - -private fun Context.translationKey(option: ImagePreview): String { - return when (option) { - ImagePreview.NONE -> getString(R.string.image_preview_menu_option_none) - ImagePreview.SMALL -> getString(R.string.image_preview_menu_option_small) - ImagePreview.LARGE -> getString(R.string.image_preview_menu_option_large) - } -} - -@Preview -@Composable -fun ImagePreviewMenuPreview() { - val (preview, setPreview) = remember { - mutableStateOf(ImagePreview.SMALL) - } - - ImagePreviewMenu( - onUpdateImagePreview = setPreview, - imagePreview = preview - ) -} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/ThemeMenu.kt b/app/src/main/java/com/capyreader/app/ui/settings/PreferenceDropdown.kt similarity index 58% rename from app/src/main/java/com/capyreader/app/ui/settings/ThemeMenu.kt rename to app/src/main/java/com/capyreader/app/ui/settings/PreferenceDropdown.kt index 24466551..3fdd5f0b 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/ThemeMenu.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/PreferenceDropdown.kt @@ -1,11 +1,12 @@ package com.capyreader.app.ui.settings -import android.content.Context +import androidx.annotation.StringRes import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MenuAnchorType.Companion.PrimaryNotEditable import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -13,23 +14,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.capyreader.app.R -import com.capyreader.app.common.ThemeOption @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ThemeMenu( - onUpdateTheme: (theme: ThemeOption) -> Unit, - theme: ThemeOption, +fun PreferenceDropdown( + selected: T, + update: (T) -> Unit, + options: List, + disabledOption: T? = null, + @StringRes label: Int, ) { - val context = LocalContext.current val (expanded, setExpanded) = remember { mutableStateOf(false) } - val options = ThemeOption.sorted.map { - it to context.translationKey(it) - } ExposedDropdownMenuBox( expanded = expanded, @@ -40,9 +36,9 @@ fun ThemeMenu( .menuAnchor(PrimaryNotEditable) .fillMaxWidth(), readOnly = true, - value = context.translationKey(theme), + value = stringResource(id = selected.translationKey), onValueChange = {}, - label = { Text(stringResource(R.string.theme_menu_label)) }, + label = { Text(stringResource(label)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), ) @@ -50,32 +46,18 @@ fun ThemeMenu( expanded = expanded, onDismissRequest = { setExpanded(false) } ) { - options.forEach { (option, text) -> + options.forEach { option -> DropdownMenuItem( - text = { Text(text) }, + text = { Text(stringResource(id = option.translationKey)) }, onClick = { - onUpdateTheme(option) + update(option) setExpanded(false) } ) + if (option == disabledOption) { + HorizontalDivider() + } } } } } - -private fun Context.translationKey(option: ThemeOption): String { - return when (option) { - ThemeOption.LIGHT -> getString(R.string.theme_menu_option_light) - ThemeOption.DARK -> getString(R.string.theme_menu_option_dark) - ThemeOption.SYSTEM_DEFAULT -> getString(R.string.theme_menu_option_system_default) - } -} - -@Preview -@Composable -fun ThemeMenuPreview() { - ThemeMenu( - onUpdateTheme = {}, - theme = ThemeOption.SYSTEM_DEFAULT, - ) -} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/ReaderGestures.kt b/app/src/main/java/com/capyreader/app/ui/settings/ReaderGestures.kt new file mode 100644 index 00000000..7cb30c2b --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/ReaderGestures.kt @@ -0,0 +1,6 @@ +package com.capyreader.app.ui.settings + +class ReaderGestures( + val topSwipe: ArticleVerticalSwipe, + val bottomSwipe: ArticleVerticalSwipe, +) diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsList.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsList.kt index ba58eebe..d19dfa6f 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsList.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsList.kt @@ -1,5 +1,6 @@ package com.capyreader.app.ui.settings +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -81,6 +82,9 @@ fun SettingsList( } ) { ListItem( + leadingContent = { + Icon(panel.icon(), contentDescription = null) + }, colors = ListItemDefaults.colors( containerColor = if (!isAtMostMedium() && panel == selected) { MaterialTheme.colorScheme.surfaceContainerHigh diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsModule.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsModule.kt index d19186e1..42a27531 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsModule.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsModule.kt @@ -27,5 +27,10 @@ val settingsModule = module { appPreferences = get(), ) } + viewModel { + GesturesSettingsViewModel( + appPreferences = get(), + ) + } worker { OPMLImportWorker(get(), get()) } } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanel.kt index 461c75b4..d420a5b8 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanel.kt @@ -2,27 +2,52 @@ package com.capyreader.app.ui.settings import android.os.Parcelable import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material.icons.rounded.Build +import androidx.compose.material.icons.rounded.Gesture +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Palette +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector import com.capyreader.app.R import kotlinx.parcelize.Parcelize sealed class SettingsPanel(@StringRes val title: Int) { + abstract fun icon(): ImageVector + + @Parcelize + data object General : SettingsPanel(title = R.string.settings_panel_general_title), Parcelable { + override fun icon() = Icons.Rounded.Build + } + @Parcelize - data object General : SettingsPanel(title = R.string.settings_panel_general_title), Parcelable + data object Display : SettingsPanel(title = R.string.settings_panel_display_title), Parcelable { + override fun icon() = Icons.Rounded.Palette + } @Parcelize - data object Display : SettingsPanel(title = R.string.settings_panel_display_title), Parcelable + data object Gestures : SettingsPanel(title = R.string.settings_panel_gestures_title), + Parcelable { + override fun icon() = Icons.Rounded.Gesture + } @Parcelize - data object Account : SettingsPanel(title = R.string.settings_account_title), Parcelable + data object Account : SettingsPanel(title = R.string.settings_account_title), Parcelable { + override fun icon() = Icons.Rounded.AccountCircle + } @Parcelize - data object About : SettingsPanel(title = R.string.settings_about_title), Parcelable + data object About : SettingsPanel(title = R.string.settings_about_title), Parcelable { + override fun icon() = Icons.Rounded.Info + } companion object { val items: List get() = listOf( General, Display, + Gestures, Account, About, ) diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt index 40c86a29..76955308 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt @@ -57,6 +57,7 @@ fun SettingsView( when (currentPanel) { SettingsPanel.General -> GeneralSettingsPanel() SettingsPanel.Display -> DisplaySettingsPanel() + SettingsPanel.Gestures -> GesturesSettingPanel() SettingsPanel.Account -> AccountSettingsPanel(onRemoveAccount = onRemoveAccount) SettingsPanel.About -> AboutSettingsPanel() } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/Translated.kt b/app/src/main/java/com/capyreader/app/ui/settings/Translated.kt new file mode 100644 index 00000000..ffda860c --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/Translated.kt @@ -0,0 +1,5 @@ +package com.capyreader.app.ui.settings + +interface Translated { + val translationKey: Int +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee5a2db0..93a36fa8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,6 +134,7 @@ Dismiss search General Display & Appearance + Gestures Account About Settings @@ -155,4 +156,10 @@ Reader Pin Toolbars Go to next article + Swipe Down + Swipe Up + Previous article + Next article + Disabled + Load full content