diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml
index 87e170f75..75a5b63dd 100644
--- a/.idea/inspectionProfiles/ktlint.xml
+++ b/.idea/inspectionProfiles/ktlint.xml
@@ -73,5 +73,6 @@
+
\ No newline at end of file
diff --git a/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagBindingContainer.kt b/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagBindingContainer.kt
new file mode 100644
index 000000000..ba498b4b0
--- /dev/null
+++ b/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagBindingContainer.kt
@@ -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 = factory.boolean("review_enabled", defaultValue = false)
+
+ @Provides
+ @SingleIn(AppScope::class)
+ @UserAgentFeatureFlagQualifier
+ fun userAgentFeatureFlag(factory: FeatureFlagFactory): FeatureFlag {
+ return factory.string(key = "user_agent", defaultValue = "Mozilla/5.0")
+ }
+
+ @Provides
+ @SingleIn(AppScope::class)
+ @FolderPickerInSettingsFeatureFlagQualifier
+ fun folderPickerInSettingsFeatureFlag(factory: FeatureFlagFactory): FeatureFlag {
+ return factory.boolean(key = "folder_picker_in_settings")
+ }
+}
+
+@Qualifier
+annotation class ReviewEnabledFeatureFlagQualifier
+
+@Qualifier
+annotation class UserAgentFeatureFlagQualifier
+
+@Qualifier
+annotation class FolderPickerInSettingsFeatureFlagQualifier
diff --git a/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagFactory.kt b/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagFactory.kt
index 2dc0a446c..8e2637fb1 100644
--- a/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagFactory.kt
+++ b/core/featureflag/src/main/kotlin/voice/core/featureflag/FeatureFlagFactory.kt
@@ -6,9 +6,12 @@ import voice.core.remoteconfig.api.RemoteConfig
@Inject
class FeatureFlagFactory(private val remoteConfig: RemoteConfig) {
- fun boolean(key: String): FeatureFlag {
+ fun boolean(
+ key: String,
+ defaultValue: Boolean = false,
+ ): FeatureFlag {
return RemoteConfigFeatureFlag(remoteConfig = remoteConfig) {
- it.boolean(key = key)
+ it.boolean(key = key, defaultValue = defaultValue)
}
}
diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml
index 925492eba..dc6efa402 100644
--- a/core/strings/src/main/res/values/strings.xml
+++ b/core/strings/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
Mark as Current
Mark as Not Started Yet
Audiobook folders
+ Manage folders scanned for audiobooks
Playback speed
Bookmark
Edit bookmark
diff --git a/features/bookOverview/build.gradle.kts b/features/bookOverview/build.gradle.kts
index 033905f17..e951c99cc 100644
--- a/features/bookOverview/build.gradle.kts
+++ b/features/bookOverview/build.gradle.kts
@@ -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)
diff --git a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewModel.kt b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewModel.kt
index e55a01293..e6381227a 100644
--- a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewModel.kt
+++ b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewModel.kt
@@ -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,
) {
private val scope = MainScope()
@@ -123,6 +127,7 @@ class BookOverviewViewModel(
searchActive = searchActive,
searchViewState = bookSearchViewState,
showStoragePermissionBugCard = hasStoragePermissionBug,
+ showFolderPickerIcon = !folderPickerInSettingsFeatureFlag.get(),
)
}
diff --git a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewState.kt b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewState.kt
index 81b858576..04932f4ad 100644
--- a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewState.kt
+++ b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/overview/BookOverviewViewState.kt
@@ -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,
)
}
diff --git a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/BookOverview.kt b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/BookOverview.kt
index a6f731597..b6c33e8c8 100644
--- a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/BookOverview.kt
+++ b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/BookOverview.kt
@@ -276,6 +276,7 @@ internal class BookOverviewPreviewParameterProvider : PreviewParameterProvider 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,
)
diff --git a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/BookOverviewTopBar.kt b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/BookOverviewTopBar.kt
index 609ffbe19..5ddb01f16 100644
--- a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/BookOverviewTopBar.kt
+++ b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/BookOverviewTopBar.kt
@@ -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 = {},
diff --git a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/TopBarTrailingIcon.kt b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/TopBarTrailingIcon.kt
index ff5d4ffbd..8a663b2eb 100644
--- a/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/TopBarTrailingIcon.kt
+++ b/features/bookOverview/src/main/kotlin/voice/features/bookOverview/views/topbar/TopBarTrailingIcon.kt
@@ -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)
}
}
diff --git a/features/cover/src/main/kotlin/voice/features/cover/api/CoverModule.kt b/features/cover/src/main/kotlin/voice/features/cover/api/CoverModule.kt
index 96ae88c81..541680dc5 100644
--- a/features/cover/src/main/kotlin/voice/features/cover/api/CoverModule.kt
+++ b/features/cover/src/main/kotlin/voice/features/cover/api/CoverModule.kt
@@ -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 {
- return factory.string(key = "user_agent", defaultValue = "Mozilla/5.0")
- }
}
diff --git a/features/folderPicker/build.gradle.kts b/features/folderPicker/build.gradle.kts
index dee711741..db62a5159 100644
--- a/features/folderPicker/build.gradle.kts
+++ b/features/folderPicker/build.gradle.kts
@@ -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)
diff --git a/features/review/play/src/main/kotlin/voice/features/review/ShouldShowReviewDialog.kt b/features/review/play/src/main/kotlin/voice/features/review/ShouldShowReviewDialog.kt
index cf483086b..cefd45e43 100644
--- a/features/review/play/src/main/kotlin/voice/features/review/ShouldShowReviewDialog.kt
+++ b/features/review/play/src/main/kotlin/voice/features/review/ShouldShowReviewDialog.kt
@@ -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
diff --git a/features/settings/build.gradle.kts b/features/settings/build.gradle.kts
index b2446f8ad..8822e8c63 100644
--- a/features/settings/build.gradle.kts
+++ b/features/settings/build.gradle.kts
@@ -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)
}
diff --git a/features/settings/src/main/kotlin/voice/features/settings/SettingsListener.kt b/features/settings/src/main/kotlin/voice/features/settings/SettingsListener.kt
index 9d43cb2a7..2fc13aa64 100644
--- a/features/settings/src/main/kotlin/voice/features/settings/SettingsListener.kt
+++ b/features/settings/src/main/kotlin/voice/features/settings/SettingsListener.kt
@@ -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() {}
}
}
}
diff --git a/features/settings/src/main/kotlin/voice/features/settings/SettingsViewModel.kt b/features/settings/src/main/kotlin/voice/features/settings/SettingsViewModel.kt
index 218c79a12..553b7f7e2 100644
--- a/features/settings/src/main/kotlin/voice/features/settings/SettingsViewModel.kt
+++ b/features/settings/src/main/kotlin/voice/features/settings/SettingsViewModel.kt
@@ -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,
private val gridCount: GridCount,
+ @FolderPickerInSettingsFeatureFlagQualifier
+ private val folderPickerInSettingsFeatureFlag: FeatureFlag,
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 ->
diff --git a/features/settings/src/main/kotlin/voice/features/settings/SettingsViewState.kt b/features/settings/src/main/kotlin/voice/features/settings/SettingsViewState.kt
index d4a484e27..b93613d7b 100644
--- a/features/settings/src/main/kotlin/voice/features/settings/SettingsViewState.kt
+++ b/features/settings/src/main/kotlin/voice/features/settings/SettingsViewState.kt
@@ -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,
)
}
}
diff --git a/features/settings/src/main/kotlin/voice/features/settings/views/Settings.kt b/features/settings/src/main/kotlin/voice/features/settings/views/Settings.kt
index b05fad1ef..00759bebc 100644
--- a/features/settings/src/main/kotlin/voice/features/settings/views/Settings.kt
+++ b/features/settings/src/main/kotlin/voice/features/settings/views/Settings.kt
@@ -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)