Add option to hide fab (close #1466)

This commit is contained in:
Koitharu
2025-07-16 20:04:35 +03:00
parent 3e36e1e11c
commit 8142a6811b
14 changed files with 81 additions and 60 deletions

View File

@@ -15,12 +15,17 @@ import androidx.core.os.LocaleListCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext 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.R
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.getEnumValue 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.putAll
import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
@@ -82,6 +87,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isNavBarPinned: Boolean val isNavBarPinned: Boolean
get() = prefs.getBoolean(KEY_NAV_PINNED, false) get() = prefs.getBoolean(KEY_NAV_PINNED, false)
val isMainFabEnabled: Boolean
get() = prefs.getBoolean(KEY_MAIN_FAB, true)
var gridSize: Int var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100) get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
@@ -598,7 +606,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
prefs.unregisterOnSharedPreferenceChangeListener(listener) prefs.unregisterOnSharedPreferenceChangeListener(listener)
} }
fun observe() = prefs.observe() fun observeChanges() = prefs.observeChanges()
fun observe(vararg keys: String): Flow<String?> = prefs.observeChanges()
.filter { key -> key == null || key in keys }
.onStart { emit(null) }
.flowOn(Dispatchers.IO)
fun getAllValues(): Map<String, *> = prefs.all fun getAllValues(): Map<String, *> = prefs.all
@@ -743,6 +756,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_NAV_MAIN = "nav_main" const val KEY_NAV_MAIN = "nav_main"
const val KEY_NAV_LABELS = "nav_labels" const val KEY_NAV_LABELS = "nav_labels"
const val KEY_NAV_PINNED = "nav_pinned" const val KEY_NAV_PINNED = "nav_pinned"
const val KEY_MAIN_FAB = "main_fab"
const val KEY_32BIT_COLOR = "enhanced_colors" const val KEY_32BIT_COLOR = "enhanced_colors"
const val KEY_SOURCES_ORDER = "sources_sort_order" const val KEY_SOURCES_ORDER = "sources_sort_order"
const val KEY_SOURCES_CATALOG = "sources_catalog" const val KEY_SOURCES_CATALOG = "sources_catalog"

View File

@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.transform
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow { fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
var lastValue: T = valueProducer() var lastValue: T = valueProducer()
emit(lastValue) emit(lastValue)
observe().collect { observeChanges().collect {
if (it == key) { if (it == key) {
val value = valueProducer() val value = valueProducer()
if (value != lastValue) { if (value != lastValue) {
@@ -25,7 +25,7 @@ fun <T> AppSettings.observeAsStateFlow(
scope: CoroutineScope, scope: CoroutineScope,
key: String, key: String,
valueProducer: AppSettings.() -> T, valueProducer: AppSettings.() -> T,
): StateFlow<T> = observe().transform { ): StateFlow<T> = observeChanges().transform {
if (it == key) { if (it == key) {
emit(valueProducer()) emit(valueProducer())
} }

View File

@@ -37,7 +37,7 @@ fun <E : Enum<E>> SharedPreferences.Editor.putEnumValue(key: String, value: E?)
putString(key, value?.name) putString(key, value?.name)
} }
fun SharedPreferences.observe(): Flow<String?> = callbackFlow { fun SharedPreferences.observeChanges(): Flow<String?> = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
trySendBlocking(key) trySendBlocking(key)
} }
@@ -49,7 +49,7 @@ fun SharedPreferences.observe(): Flow<String?> = callbackFlow {
fun <T> SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow<T> = flow { fun <T> SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow<T> = flow {
emit(valueProducer()) emit(valueProducer())
observe().collect { upstreamKey -> observeChanges().collect { upstreamKey ->
if (upstreamKey == key) { if (upstreamKey == key) {
emit(valueProducer()) emit(valueProducer())
} }

View File

@@ -4,9 +4,7 @@ import androidx.room.withTransaction
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga 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 shouldSkip(manga: Manga): Boolean = settings.isIncognitoModeEnabled(manga.isNsfw())
fun observeShouldSkip(manga: Manga): Flow<Boolean> { fun observeShouldSkip(manga: Manga): Flow<Boolean> {
return settings.observe() return settings.observe(AppSettings.KEY_INCOGNITO_MODE, AppSettings.KEY_INCOGNITO_NSFW)
.filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_INCOGNITO_NSFW }
.onStart { emit("") }
.map { shouldSkip(manga) } .map { shouldSkip(manga) }
.distinctUntilChanged() .distinctUntilChanged()
} }

View File

@@ -63,7 +63,7 @@ abstract class MangaListViewModel(
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine( protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
listMode, listMode,
mangaDataRepository.observeOverridesTrigger(emitInitialState = true), mangaDataRepository.observeOverridesTrigger(emitInitialState = true),
settings.observe().filter { key -> settings.observeChanges().filter { key ->
key == AppSettings.KEY_PROGRESS_INDICATORS key == AppSettings.KEY_PROGRESS_INDICATORS
|| key == AppSettings.KEY_TRACKER_ENABLED || key == AppSettings.KEY_TRACKER_ENABLED
|| key == AppSettings.KEY_QUICK_FILTER || key == AppSettings.KEY_QUICK_FILTER

View File

@@ -2,12 +2,13 @@ package org.koitharu.kotatsu.main.domain
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import javax.inject.Inject import javax.inject.Inject
@@ -17,15 +18,21 @@ class ReadingResumeEnabledUseCase @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
) { ) {
operator fun invoke(): Flow<Boolean> = settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { operator fun invoke(): Flow<Boolean> = settings.observe(
isIncognitoModeEnabled AppSettings.KEY_MAIN_FAB,
}.flatMapLatest { incognito -> AppSettings.KEY_INCOGNITO_MODE,
if (incognito) { ).map {
flowOf(false) settings.isMainFabEnabled && !settings.isIncognitoModeEnabled
} else { }.distinctUntilChanged()
combine(networkState, historyRepository.observeLast()) { isOnline, last -> .flatMapLatest { isFabEnabled ->
last != null && (isOnline || last.isLocal) if (isFabEnabled) {
observeCanResume()
} else {
flowOf(false)
} }
} }
}
private fun observeCanResume() = combine(networkState, historyRepository.observeLast()) { isOnline, last ->
last != null && (isOnline || last.isLocal)
}.distinctUntilChanged()
} }

View File

@@ -320,6 +320,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
topFragment: Fragment? = navigationDelegate.primaryFragment, topFragment: Fragment? = navigationDelegate.primaryFragment,
isSearchOpened: Boolean = viewBinding.searchView.isShowing, isSearchOpened: Boolean = viewBinding.searchView.isShowing,
) { ) {
navigationDelegate.navRailHeader?.railFab?.isVisible = isResumeEnabled
val fab = viewBinding.fab ?: return val fab = viewBinding.fab ?: return
if (isResumeEnabled && !actionModeDelegate.isActionModeStarted && !isSearchOpened && topFragment is HistoryListFragment) { if (isResumeEnabled && !actionModeDelegate.isActionModeStarted && !isSearchOpened && topFragment is HistoryListFragment) {
if (!fab.isVisible) { if (!fab.isVisible) {

View File

@@ -16,16 +16,12 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -56,7 +52,7 @@ class MainNavigationDelegate(
NavigationBarView.OnItemReselectedListener, View.OnClickListener { NavigationBarView.OnItemReselectedListener, View.OnClickListener {
private val listeners = LinkedList<OnFragmentChangedListener>() private val listeners = LinkedList<OnFragmentChangedListener>()
private val navRailHeader = (navBar as? NavigationRailView)?.headerView?.let { val navRailHeader = (navBar as? NavigationRailView)?.headerView?.let {
NavigationRailFabBinding.bind(it) NavigationRailFabBinding.bind(it)
} }
@@ -267,12 +263,7 @@ class MainNavigationDelegate(
} }
private fun observeSettings(lifecycleOwner: LifecycleOwner) { private fun observeSettings(lifecycleOwner: LifecycleOwner) {
settings.observe() settings.observe(AppSettings.KEY_TRACKER_ENABLED, AppSettings.KEY_SUGGESTIONS, AppSettings.KEY_NAV_LABELS)
.filter { x ->
x == AppSettings.KEY_TRACKER_ENABLED || x == AppSettings.KEY_SUGGESTIONS || x == AppSettings.KEY_NAV_LABELS
}
.onStart { emit("") }
.flowOn(Dispatchers.IO)
.onEach { .onEach {
setItemVisibility(R.id.nav_suggestions, settings.isSuggestionsEnabled) setItemVisibility(R.id.nav_suggestions, settings.isSuggestionsEnabled)
setItemVisibility(R.id.nav_feed, settings.isTrackerEnabled) setItemVisibility(R.id.nav_feed, settings.isTrackerEnabled)

View File

@@ -8,7 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.core.util.ext.getEnumValue 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.putAll
import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.reader.domain.TapGridArea import org.koitharu.kotatsu.reader.domain.TapGridArea
@@ -44,7 +44,7 @@ class TapGridSettings @Inject constructor(@ApplicationContext context: Context)
initPrefs(withDefaultValues = false) initPrefs(withDefaultValues = false)
} }
fun observe() = prefs.observe().flowOn(Dispatchers.IO) fun observeChanges() = prefs.observeChanges().flowOn(Dispatchers.IO)
fun getAllValues(): Map<String, *> = prefs.all fun getAllValues(): Map<String, *> = prefs.all

View File

@@ -122,7 +122,7 @@ data class ReaderSettings(
private suspend fun observeImpl() { private suspend fun observeImpl() {
combine( combine(
mangaId.flatMapLatest { mangaDataRepository.observeColorFilter(it) }, 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 -> ) { mangaCf, settingsKey ->
ReaderSettings(settings, mangaCf) ReaderSettings(settings, mangaCf)
}.collect { }.collect {

View File

@@ -20,7 +20,7 @@ class ReaderTapGridConfigViewModel @Inject constructor(
private val tapGridSettings: TapGridSettings, private val tapGridSettings: TapGridSettings,
) : BaseViewModel() { ) : BaseViewModel() {
val content = tapGridSettings.observe() val content = tapGridSettings.observeChanges()
.onStart { emit(null) } .onStart { emit(null) }
.map { getData() } .map { getData() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyMap()) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyMap())

View File

@@ -45,7 +45,7 @@ class SourcesListProducer @Inject constructor(
} }
init { init {
settings.observe() settings.observeChanges()
.filter { it == AppSettings.KEY_TIPS_CLOSED || it == AppSettings.KEY_DISABLE_NSFW } .filter { it == AppSettings.KEY_TIPS_CLOSED || it == AppSettings.KEY_DISABLE_NSFW }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.onEach { onInvalidated(emptySet()) } .onEach { onInvalidated(emptySet()) }

View File

@@ -856,4 +856,7 @@
<string name="book_effect">Yellowish background (blue filter)</string> <string name="book_effect">Yellowish background (blue filter)</string>
<string name="local_storage_cleanup">Local storage cleanup</string> <string name="local_storage_cleanup">Local storage cleanup</string>
<string name="packup_creation_failed">Failed to create backup</string> <string name="packup_creation_failed">Failed to create backup</string>
<string name="main_screen">Main screen</string>
<string name="main_screen_fab">Show floating Continue button</string>
<string name="main_screen_fab_summary">Allows to continue reading in a one click. This button will not appear in incognito mode or when the history is empty</string>
</resources> </resources>

View File

@@ -23,6 +23,10 @@
android:summary="@string/black_dark_theme_summary" android:summary="@string/black_dark_theme_summary"
android:title="@string/black_dark_theme" /> android:title="@string/black_dark_theme" />
<org.koitharu.kotatsu.settings.utils.ActivityListPreference
android:key="app_locale"
android:title="@string/language" />
<PreferenceCategory android:title="@string/manga_list"> <PreferenceCategory android:title="@string/manga_list">
<ListPreference <ListPreference
@@ -84,31 +88,36 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceScreen <PreferenceCategory android:title="@string/main_screen">
android:fragment="org.koitharu.kotatsu.settings.nav.NavConfigFragment"
android:key="nav_main"
android:title="@string/main_screen_sections"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat <PreferenceScreen
android:defaultValue="true" android:fragment="org.koitharu.kotatsu.settings.nav.NavConfigFragment"
android:key="nav_labels" android:key="nav_main"
android:title="@string/show_labels_in_navbar" /> android:title="@string/main_screen_sections" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="true"
android:key="nav_pinned" android:key="main_fab"
android:summary="@string/pin_navigation_ui_summary" android:summary="@string/main_screen_fab_summary"
android:title="@string/pin_navigation_ui" /> android:title="@string/main_screen_fab" />
<org.koitharu.kotatsu.settings.utils.ActivityListPreference <SwitchPreferenceCompat
android:key="app_locale" android:defaultValue="true"
android:title="@string/language" /> android:key="nav_labels"
android:title="@string/show_labels_in_navbar" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="exit_confirm" android:key="nav_pinned"
android:summary="@string/exit_confirmation_summary" android:summary="@string/pin_navigation_ui_summary"
android:title="@string/exit_confirmation" /> android:title="@string/pin_navigation_ui" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="exit_confirm"
android:summary="@string/exit_confirmation_summary"
android:title="@string/exit_confirmation" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>