From 8142a6811bfa84b5eaf761d5afc5a99c122a970a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 16 Jul 2025 20:04:35 +0300 Subject: [PATCH] Add option to hide fab (close #1466) --- .../kotatsu/core/prefs/AppSettings.kt | 18 ++++++- .../kotatsu/core/prefs/AppSettingsObserver.kt | 4 +- .../kotatsu/core/util/ext/Preferences.kt | 4 +- .../kotatsu/history/data/HistoryRepository.kt | 6 +-- .../kotatsu/list/ui/MangaListViewModel.kt | 2 +- .../domain/ReadingResumeEnabledUseCase.kt | 27 ++++++---- .../koitharu/kotatsu/main/ui/MainActivity.kt | 1 + .../kotatsu/main/ui/MainNavigationDelegate.kt | 13 +---- .../kotatsu/reader/data/TapGridSettings.kt | 4 +- .../reader/ui/config/ReaderSettings.kt | 2 +- .../reader/ReaderTapGridConfigViewModel.kt | 2 +- .../sources/manage/SourcesListProducer.kt | 2 +- app/src/main/res/values/strings.xml | 3 ++ app/src/main/res/xml/pref_appearance.xml | 53 +++++++++++-------- 14 files changed, 81 insertions(+), 60 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index d3d40f5f4..3ddd8b1ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -15,12 +15,17 @@ import androidx.core.os.LocaleListCompat import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.getEnumValue -import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeChanges import org.koitharu.kotatsu.core.util.ext.putAll import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.takeIfReadable @@ -82,6 +87,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isNavBarPinned: Boolean get() = prefs.getBoolean(KEY_NAV_PINNED, false) + val isMainFabEnabled: Boolean + get() = prefs.getBoolean(KEY_MAIN_FAB, true) + var gridSize: Int get() = prefs.getInt(KEY_GRID_SIZE, 100) set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } @@ -598,7 +606,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { prefs.unregisterOnSharedPreferenceChangeListener(listener) } - fun observe() = prefs.observe() + fun observeChanges() = prefs.observeChanges() + + fun observe(vararg keys: String): Flow = prefs.observeChanges() + .filter { key -> key == null || key in keys } + .onStart { emit(null) } + .flowOn(Dispatchers.IO) fun getAllValues(): Map = prefs.all @@ -743,6 +756,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_NAV_MAIN = "nav_main" const val KEY_NAV_LABELS = "nav_labels" const val KEY_NAV_PINNED = "nav_pinned" + const val KEY_MAIN_FAB = "main_fab" const val KEY_32BIT_COLOR = "enhanced_colors" const val KEY_SOURCES_ORDER = "sources_sort_order" const val KEY_SOURCES_CATALOG = "sources_catalog" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt index ed6d14f68..20557823c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.transform fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow { var lastValue: T = valueProducer() emit(lastValue) - observe().collect { + observeChanges().collect { if (it == key) { val value = valueProducer() if (value != lastValue) { @@ -25,7 +25,7 @@ fun AppSettings.observeAsStateFlow( scope: CoroutineScope, key: String, valueProducer: AppSettings.() -> T, -): StateFlow = observe().transform { +): StateFlow = observeChanges().transform { if (it == key) { emit(valueProducer()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt index b48ca8488..954fd82d4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt @@ -37,7 +37,7 @@ fun > SharedPreferences.Editor.putEnumValue(key: String, value: E?) putString(key, value?.name) } -fun SharedPreferences.observe(): Flow = callbackFlow { +fun SharedPreferences.observeChanges(): Flow = callbackFlow { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> trySendBlocking(key) } @@ -49,7 +49,7 @@ fun SharedPreferences.observe(): Flow = callbackFlow { fun SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow = flow { emit(valueProducer()) - observe().collect { upstreamKey -> + observeChanges().collect { upstreamKey -> if (upstreamKey == key) { emit(valueProducer()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index e6883a1a4..3d5f052de 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -4,9 +4,7 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga @@ -204,9 +202,7 @@ class HistoryRepository @Inject constructor( fun shouldSkip(manga: Manga): Boolean = settings.isIncognitoModeEnabled(manga.isNsfw()) fun observeShouldSkip(manga: Manga): Flow { - return settings.observe() - .filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_INCOGNITO_NSFW } - .onStart { emit("") } + return settings.observe(AppSettings.KEY_INCOGNITO_MODE, AppSettings.KEY_INCOGNITO_NSFW) .map { shouldSkip(manga) } .distinctUntilChanged() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 13921f1c2..55ed61bda 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -63,7 +63,7 @@ abstract class MangaListViewModel( protected fun observeListModeWithTriggers(): Flow = combine( listMode, mangaDataRepository.observeOverridesTrigger(emitInitialState = true), - settings.observe().filter { key -> + settings.observeChanges().filter { key -> key == AppSettings.KEY_PROGRESS_INDICATORS || key == AppSettings.KEY_TRACKER_ENABLED || key == AppSettings.KEY_QUICK_FILTER diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt index 8828edcfd..5c712cc73 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt @@ -2,12 +2,13 @@ package org.koitharu.kotatsu.main.domain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.history.data.HistoryRepository import javax.inject.Inject @@ -17,15 +18,21 @@ class ReadingResumeEnabledUseCase @Inject constructor( private val settings: AppSettings, ) { - operator fun invoke(): Flow = settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { - isIncognitoModeEnabled - }.flatMapLatest { incognito -> - if (incognito) { - flowOf(false) - } else { - combine(networkState, historyRepository.observeLast()) { isOnline, last -> - last != null && (isOnline || last.isLocal) + operator fun invoke(): Flow = settings.observe( + AppSettings.KEY_MAIN_FAB, + AppSettings.KEY_INCOGNITO_MODE, + ).map { + settings.isMainFabEnabled && !settings.isIncognitoModeEnabled + }.distinctUntilChanged() + .flatMapLatest { isFabEnabled -> + if (isFabEnabled) { + observeCanResume() + } else { + flowOf(false) } } - } + + private fun observeCanResume() = combine(networkState, historyRepository.observeLast()) { isOnline, last -> + last != null && (isOnline || last.isLocal) + }.distinctUntilChanged() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 0a02b33d2..867243c03 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -320,6 +320,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav topFragment: Fragment? = navigationDelegate.primaryFragment, isSearchOpened: Boolean = viewBinding.searchView.isShowing, ) { + navigationDelegate.navRailHeader?.railFab?.isVisible = isResumeEnabled val fab = viewBinding.fab ?: return if (isResumeEnabled && !actionModeDelegate.isActionModeStarted && !isSearchOpened && topFragment is HistoryListFragment) { if (!fab.isVisible) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt index d9e7ad11e..f0add4a24 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt @@ -16,16 +16,12 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.transition.MaterialFadeThrough -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.R import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment import org.koitharu.kotatsu.core.prefs.AppSettings @@ -56,7 +52,7 @@ class MainNavigationDelegate( NavigationBarView.OnItemReselectedListener, View.OnClickListener { private val listeners = LinkedList() - private val navRailHeader = (navBar as? NavigationRailView)?.headerView?.let { + val navRailHeader = (navBar as? NavigationRailView)?.headerView?.let { NavigationRailFabBinding.bind(it) } @@ -267,12 +263,7 @@ class MainNavigationDelegate( } private fun observeSettings(lifecycleOwner: LifecycleOwner) { - settings.observe() - .filter { x -> - x == AppSettings.KEY_TRACKER_ENABLED || x == AppSettings.KEY_SUGGESTIONS || x == AppSettings.KEY_NAV_LABELS - } - .onStart { emit("") } - .flowOn(Dispatchers.IO) + settings.observe(AppSettings.KEY_TRACKER_ENABLED, AppSettings.KEY_SUGGESTIONS, AppSettings.KEY_NAV_LABELS) .onEach { setItemVisibility(R.id.nav_suggestions, settings.isSuggestionsEnabled) setItemVisibility(R.id.nav_feed, settings.isTrackerEnabled) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt index 3d856bbeb..cb95ca040 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt @@ -8,7 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import org.koitharu.kotatsu.core.util.ext.getEnumValue -import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeChanges import org.koitharu.kotatsu.core.util.ext.putAll import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.reader.domain.TapGridArea @@ -44,7 +44,7 @@ class TapGridSettings @Inject constructor(@ApplicationContext context: Context) initPrefs(withDefaultValues = false) } - fun observe() = prefs.observe().flowOn(Dispatchers.IO) + fun observeChanges() = prefs.observeChanges().flowOn(Dispatchers.IO) fun getAllValues(): Map = prefs.all diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt index 3b7634181..0b1ac813d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt @@ -122,7 +122,7 @@ data class ReaderSettings( private suspend fun observeImpl() { combine( mangaId.flatMapLatest { mangaDataRepository.observeColorFilter(it) }, - settings.observe().filter { x -> x == null || x in settingsKeys }.onStart { emit(null) }, + settings.observeChanges().filter { x -> x == null || x in settingsKeys }.onStart { emit(null) }, ) { mangaCf, settingsKey -> ReaderSettings(settings, mangaCf) }.collect { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigViewModel.kt index e31068d3a..aa7d045a4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigViewModel.kt @@ -20,7 +20,7 @@ class ReaderTapGridConfigViewModel @Inject constructor( private val tapGridSettings: TapGridSettings, ) : BaseViewModel() { - val content = tapGridSettings.observe() + val content = tapGridSettings.observeChanges() .onStart { emit(null) } .map { getData() } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyMap()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt index d7bcc5e0c..21aea3463 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt @@ -45,7 +45,7 @@ class SourcesListProducer @Inject constructor( } init { - settings.observe() + settings.observeChanges() .filter { it == AppSettings.KEY_TIPS_CLOSED || it == AppSettings.KEY_DISABLE_NSFW } .flowOn(Dispatchers.Default) .onEach { onInvalidated(emptySet()) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f610ca3a..c42a30453 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -856,4 +856,7 @@ Yellowish background (blue filter) Local storage cleanup Failed to create backup + Main screen + Show floating Continue button + Allows to continue reading in a one click. This button will not appear in incognito mode or when the history is empty diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml index 568e5d883..fa8f626a0 100644 --- a/app/src/main/res/xml/pref_appearance.xml +++ b/app/src/main/res/xml/pref_appearance.xml @@ -23,6 +23,10 @@ android:summary="@string/black_dark_theme_summary" android:title="@string/black_dark_theme" /> + + - + - + - + - + - + + + + +