mirror of
https://github.com/PaulWoitaschek/Voice.git
synced 2026-03-13 08:02:45 +08:00
Move folder picker to settings behind feature flag (#3280)
* Refactor: Introduce a feature flag module Introduces a new `core:featureflag` module to abstract feature flag implementations. This module provides a `FeatureFlag` interface and a `FeatureFlagFactory` to create feature flags backed by `RemoteConfig`. Key changes include: - Creating a new `:core:featureflag` Gradle module. - Replacing direct `RemoteConfig` usage in `features/cover` and `features/review` with the new `FeatureFlag` abstraction. - Using a `MemoryFeatureFlag` for testing purposes. * Move folder picker to settings behind feature flag This change introduces a feature flag `folder_picker_in_settings`. When this flag is enabled: - The folder picker icon is removed from the book overview top bar. - A new entry to access the audiobook folder management screen is added to the settings menu. Additionally, feature flag definitions have been centralized into a single `FeatureFlagBindingContainer` for better organization. * Delete docs/feature-specs/folder_picker_in_settings.md
This commit is contained in:
1
.idea/inspectionProfiles/ktlint.xml
generated
1
.idea/inspectionProfiles/ktlint.xml
generated
@@ -73,5 +73,6 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="RedundantCompanionReference" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,41 @@
|
||||
package voice.core.featureflag
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
|
||||
@BindingContainer
|
||||
@ContributesTo(AppScope::class)
|
||||
object FeatureFlagBindingContainer {
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@ReviewEnabledFeatureFlagQualifier
|
||||
fun reviewEnabledFeatureFlag(factory: FeatureFlagFactory): FeatureFlag<Boolean> = factory.boolean("review_enabled", defaultValue = false)
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@UserAgentFeatureFlagQualifier
|
||||
fun userAgentFeatureFlag(factory: FeatureFlagFactory): FeatureFlag<String> {
|
||||
return factory.string(key = "user_agent", defaultValue = "Mozilla/5.0")
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@FolderPickerInSettingsFeatureFlagQualifier
|
||||
fun folderPickerInSettingsFeatureFlag(factory: FeatureFlagFactory): FeatureFlag<Boolean> {
|
||||
return factory.boolean(key = "folder_picker_in_settings")
|
||||
}
|
||||
}
|
||||
|
||||
@Qualifier
|
||||
annotation class ReviewEnabledFeatureFlagQualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class UserAgentFeatureFlagQualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class FolderPickerInSettingsFeatureFlagQualifier
|
||||
@@ -6,9 +6,12 @@ import voice.core.remoteconfig.api.RemoteConfig
|
||||
@Inject
|
||||
class FeatureFlagFactory(private val remoteConfig: RemoteConfig) {
|
||||
|
||||
fun boolean(key: String): FeatureFlag<Boolean> {
|
||||
fun boolean(
|
||||
key: String,
|
||||
defaultValue: Boolean = false,
|
||||
): FeatureFlag<Boolean> {
|
||||
return RemoteConfigFeatureFlag(remoteConfig = remoteConfig) {
|
||||
it.boolean(key = key)
|
||||
it.boolean(key = key, defaultValue = defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<string name="mark_as_current">Mark as Current</string>
|
||||
<string name="mark_as_not_started">Mark as Not Started Yet</string>
|
||||
<string name="audiobook_folders_title">Audiobook folders</string>
|
||||
<string name="pref_audiobook_folders_explanation">Manage folders scanned for audiobooks</string>
|
||||
<string name="playback_speed">Playback speed</string>
|
||||
<string name="bookmark">Bookmark</string>
|
||||
<string name="bookmark_edit_title">Edit bookmark</string>
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies {
|
||||
implementation(projects.core.playback)
|
||||
implementation(projects.core.data.api)
|
||||
implementation(projects.core.scanner)
|
||||
implementation(projects.core.featureflag)
|
||||
|
||||
implementation(libs.lifecycle)
|
||||
api(libs.immutable)
|
||||
|
||||
@@ -25,6 +25,8 @@ import voice.core.data.repo.BookRepository
|
||||
import voice.core.data.repo.internals.dao.RecentBookSearchDao
|
||||
import voice.core.data.store.CurrentBookStore
|
||||
import voice.core.data.store.GridModeStore
|
||||
import voice.core.featureflag.FeatureFlag
|
||||
import voice.core.featureflag.FolderPickerInSettingsFeatureFlagQualifier
|
||||
import voice.core.playback.PlayerController
|
||||
import voice.core.playback.playstate.PlayStateManager
|
||||
import voice.core.scanner.DeviceHasStoragePermissionBug
|
||||
@@ -53,6 +55,8 @@ class BookOverviewViewModel(
|
||||
private val search: BookSearch,
|
||||
private val contentRepo: BookContentRepo,
|
||||
private val deviceHasStoragePermissionBug: DeviceHasStoragePermissionBug,
|
||||
@FolderPickerInSettingsFeatureFlagQualifier
|
||||
private val folderPickerInSettingsFeatureFlag: FeatureFlag<Boolean>,
|
||||
) {
|
||||
|
||||
private val scope = MainScope()
|
||||
@@ -123,6 +127,7 @@ class BookOverviewViewModel(
|
||||
searchActive = searchActive,
|
||||
searchViewState = bookSearchViewState,
|
||||
showStoragePermissionBugCard = hasStoragePermissionBug,
|
||||
showFolderPickerIcon = !folderPickerInSettingsFeatureFlag.get(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ data class BookOverviewViewState(
|
||||
val searchActive: Boolean,
|
||||
val searchViewState: BookSearchViewState,
|
||||
val showStoragePermissionBugCard: Boolean,
|
||||
val showFolderPickerIcon: Boolean,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -33,6 +34,7 @@ data class BookOverviewViewState(
|
||||
query = "",
|
||||
),
|
||||
showStoragePermissionBugCard = false,
|
||||
showFolderPickerIcon = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -276,6 +276,7 @@ internal class BookOverviewPreviewParameterProvider : PreviewParameterProvider<B
|
||||
query = "",
|
||||
),
|
||||
showStoragePermissionBugCard = false,
|
||||
showFolderPickerIcon = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ internal fun ColumnScope.BookOverviewSearchBar(
|
||||
onSearchBookClick: (BookId) -> Unit,
|
||||
searchActive: Boolean,
|
||||
showAddBookHint: Boolean,
|
||||
showFolderPickerIcon: Boolean,
|
||||
searchViewState: BookSearchViewState,
|
||||
) {
|
||||
SearchBar(
|
||||
@@ -47,6 +48,7 @@ internal fun ColumnScope.BookOverviewSearchBar(
|
||||
TopBarTrailingIcon(
|
||||
searchActive = searchActive,
|
||||
showAddBookHint = showAddBookHint,
|
||||
showFolderPickerIcon = showFolderPickerIcon,
|
||||
onBookFolderClick = onBookFolderClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ internal fun BookOverviewTopBar(
|
||||
onSearchBookClick = onSearchBookClick,
|
||||
searchActive = viewState.searchActive,
|
||||
showAddBookHint = viewState.showAddBookHint,
|
||||
showFolderPickerIcon = viewState.showFolderPickerIcon,
|
||||
searchViewState = viewState.searchViewState,
|
||||
)
|
||||
var showLoading by remember { mutableStateOf(true) }
|
||||
@@ -84,6 +85,7 @@ private fun BookOverviewTopBarPreview() {
|
||||
query = "",
|
||||
),
|
||||
showStoragePermissionBugCard = false,
|
||||
showFolderPickerIcon = true,
|
||||
),
|
||||
onBookFolderClick = {},
|
||||
onSettingsClick = {},
|
||||
|
||||
@@ -13,6 +13,7 @@ import voice.features.bookOverview.views.SettingsIcon
|
||||
internal fun ColumnScope.TopBarTrailingIcon(
|
||||
searchActive: Boolean,
|
||||
showAddBookHint: Boolean,
|
||||
showFolderPickerIcon: Boolean,
|
||||
onBookFolderClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
) {
|
||||
@@ -22,7 +23,9 @@ internal fun ColumnScope.TopBarTrailingIcon(
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Row {
|
||||
BookFolderIcon(withHint = showAddBookHint, onClick = onBookFolderClick)
|
||||
if (showFolderPickerIcon) {
|
||||
BookFolderIcon(withHint = showAddBookHint, onClick = onBookFolderClick)
|
||||
}
|
||||
SettingsIcon(onSettingsClick)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import retrofit2.create
|
||||
import voice.core.featureflag.FeatureFlag
|
||||
import voice.core.featureflag.FeatureFlagFactory
|
||||
import voice.core.featureflag.UserAgentFeatureFlagQualifier
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@BindingContainer
|
||||
@@ -52,11 +52,4 @@ object CoverModule {
|
||||
.build()
|
||||
.create()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@UserAgentFeatureFlagQualifier
|
||||
fun userAgentFeatureFlag(factory: FeatureFlagFactory): FeatureFlag<String> {
|
||||
return factory.string(key = "user_agent", defaultValue = "Mozilla/5.0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies {
|
||||
implementation(projects.core.data.api)
|
||||
implementation(projects.core.documentfile)
|
||||
implementation(projects.navigation)
|
||||
implementation(projects.core.featureflag)
|
||||
|
||||
implementation(libs.datastore)
|
||||
implementation(libs.coil)
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.first
|
||||
import voice.core.data.repo.BookRepository
|
||||
import voice.core.data.store.ReviewDialogShownStore
|
||||
import voice.core.featureflag.FeatureFlag
|
||||
import voice.core.featureflag.ReviewEnabledFeatureFlagQualifier
|
||||
import voice.core.playback.playstate.PlayStateManager
|
||||
import java.time.Clock
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@@ -8,11 +8,11 @@ dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.navigation)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.featureflag)
|
||||
implementation(projects.core.playback)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.data.api)
|
||||
|
||||
implementation(libs.androidxCore)
|
||||
implementation(libs.androidxCore)
|
||||
implementation(libs.material)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ interface SettingsListener {
|
||||
fun setAutoSleepTimerStart(time: LocalTime)
|
||||
fun setAutoSleepTimerEnd(time: LocalTime)
|
||||
fun toggleAnalytics()
|
||||
fun openFolderPicker()
|
||||
|
||||
companion object {
|
||||
fun noop() = object : SettingsListener {
|
||||
@@ -40,6 +41,7 @@ interface SettingsListener {
|
||||
override fun setAutoSleepTimerStart(time: LocalTime) {}
|
||||
override fun setAutoSleepTimerEnd(time: LocalTime) {}
|
||||
override fun toggleAnalytics() {}
|
||||
override fun openFolderPicker() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import voice.core.data.store.DarkThemeStore
|
||||
import voice.core.data.store.GridModeStore
|
||||
import voice.core.data.store.SeekTimeStore
|
||||
import voice.core.data.store.SleepTimerPreferenceStore
|
||||
import voice.core.featureflag.FeatureFlag
|
||||
import voice.core.featureflag.FolderPickerInSettingsFeatureFlagQualifier
|
||||
import voice.core.ui.DARK_THEME_SETTABLE
|
||||
import voice.core.ui.GridCount
|
||||
import voice.navigation.Destination
|
||||
@@ -44,6 +46,8 @@ class SettingsViewModel(
|
||||
@AnalyticsConsentStore
|
||||
private val analyticsConsentStore: DataStore<Boolean>,
|
||||
private val gridCount: GridCount,
|
||||
@FolderPickerInSettingsFeatureFlagQualifier
|
||||
private val folderPickerInSettingsFeatureFlag: FeatureFlag<Boolean>,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
) : SettingsListener {
|
||||
|
||||
@@ -60,6 +64,9 @@ class SettingsViewModel(
|
||||
initial = SleepTimerPreference.Default,
|
||||
)
|
||||
val analyticsEnabled by remember { analyticsConsentStore.data }.collectAsState(initial = false)
|
||||
val showFolderPickerEntry = remember {
|
||||
folderPickerInSettingsFeatureFlag.get()
|
||||
}
|
||||
return SettingsViewState(
|
||||
useDarkTheme = useDarkTheme,
|
||||
showDarkThemePref = DARK_THEME_SETTABLE,
|
||||
@@ -79,6 +86,7 @@ class SettingsViewModel(
|
||||
),
|
||||
analyticsEnabled = analyticsEnabled,
|
||||
showAnalyticSetting = appInfoProvider.analyticsIncluded,
|
||||
showFolderPickerEntry = showFolderPickerEntry,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,6 +168,10 @@ class SettingsViewModel(
|
||||
navigator.goTo(Destination.Website("https://voice.woitaschek.de/faq/"))
|
||||
}
|
||||
|
||||
override fun openFolderPicker() {
|
||||
navigator.goTo(Destination.FolderPicker)
|
||||
}
|
||||
|
||||
override fun setAutoSleepTimer(checked: Boolean) {
|
||||
mainScope.launch {
|
||||
sleepTimerPreferenceStore.updateData { currentPrefs ->
|
||||
|
||||
@@ -13,6 +13,7 @@ data class SettingsViewState(
|
||||
val autoSleepTimer: AutoSleepTimerViewState,
|
||||
val showAnalyticSetting: Boolean,
|
||||
val analyticsEnabled: Boolean,
|
||||
val showFolderPickerEntry: Boolean,
|
||||
) {
|
||||
|
||||
enum class Dialog {
|
||||
@@ -33,6 +34,7 @@ data class SettingsViewState(
|
||||
autoSleepTimer = AutoSleepTimerViewState.preview(),
|
||||
analyticsEnabled = false,
|
||||
showAnalyticSetting = true,
|
||||
showFolderPickerEntry = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.automirrored.outlined.ViewList
|
||||
import androidx.compose.material.icons.outlined.Analytics
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.GridView
|
||||
@@ -81,6 +82,25 @@ private fun Settings(
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(contentPadding = contentPadding) {
|
||||
if (viewState.showFolderPickerEntry) {
|
||||
item {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { listener.openFolderPicker() },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Book,
|
||||
contentDescription = stringResource(StringsR.string.audiobook_folders_title),
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(stringResource(StringsR.string.audiobook_folders_title))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(stringResource(StringsR.string.pref_audiobook_folders_explanation))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (viewState.showDarkThemePref) {
|
||||
item {
|
||||
DarkThemeRow(viewState.useDarkTheme, listener::toggleDarkTheme)
|
||||
|
||||
Reference in New Issue
Block a user