Merge pull request #3028 from PaulWoitaschek/common_modularlization

Fully modularlize common and app
This commit is contained in:
Paul Woitaschek
2025-08-26 22:40:21 +02:00
committed by GitHub
177 changed files with 641 additions and 494 deletions

View File

@@ -21,14 +21,39 @@ Voice is a minimal, userfocused audiobook player for Android, built for relia
Each module contains its own `build.gradle.kts`, `src/main/kotlin`, and `src/test/kotlin`:
* **app**: Main application
* **common**: Shared utilities
* **data**: Repositories and data layer
* **playback**: Audio playback logic
* **scanner**: File scanning and metadata extraction
* **cover**: Cover art handling
* **settings**: Configuration UI
* …additional feature modules
**Infrastructure**:
* `:app` - Main application entry point and DI setup
* `:navigation` - Navigation framework
* `:plugins` - Gradle build plugins
* `:scripts` - Build and utility scripts
**Core Modules** (shared domain logic):
* `:core:common` - Legacy, to be removed
* `:core:ui` - UI components and theming
* `:core:data:api` & `:core:data:impl` - Data layer interfaces and implementations
* `:core:playback` - Audio playback logic
* `:core:scanner` - File scanning and metadata extraction
* `:core:strings` - Localized strings
* `:core:search` - Search functionality
* `:core:documentfile` - File system abstractions
* `:core:logging:core`, `:core:logging:crashlytics`, `:core:logging:debug` - Logging implementations
* `:core:remoteconfig:core`, `:core:remoteconfig:firebase`, `:core:remoteconfig:noop` - Remote configuration
* `:core:sleeptimer:api` & `:core:sleeptimer:impl` - Sleep timer core logic
**Feature Modules** (UI screens and features):
* `:features:playbackScreen` - Book playing interface
* `:features:bookOverview` - Library/book list
* `:features:sleepTimer` - Sleep timer functionality
* `:features:settings` - App settings
* `:features:folderPicker` - Folder selection
* `:features:cover` - Cover art management
* `:features:onboarding` - First-time user flow
* `:features:bookmark` - Bookmark management
* `:features:widget` - Home screen widget functionality
* `:features:review:play` & `:features:review:noop` - App review prompts
## Build & Run

View File

@@ -147,7 +147,7 @@ android {
dependencies {
implementation(projects.core.strings)
implementation(projects.core.datastore)
implementation(projects.core.ui)
implementation(projects.core.common)
implementation(projects.core.data.api)
implementation(projects.core.data.impl)
@@ -155,6 +155,8 @@ dependencies {
implementation(projects.core.scanner)
implementation(projects.features.playbackScreen)
implementation(projects.navigation)
implementation(projects.core.sleeptimer.api)
implementation(projects.core.sleeptimer.impl)
implementation(projects.features.sleepTimer)
implementation(projects.features.settings)
implementation(projects.features.folderPicker)
@@ -164,6 +166,7 @@ dependencies {
implementation(projects.core.documentfile)
implementation(projects.features.onboarding)
implementation(projects.features.bookmark)
implementation(projects.features.widget)
implementation(libs.appCompat)
implementation(libs.material)
@@ -174,8 +177,6 @@ dependencies {
implementation(libs.serialization.json)
implementation(libs.materialDialog.core)
implementation(libs.materialDialog.input)
implementation(libs.coil)
if (includeProprietaryLibraries()) {

View File

@@ -10,20 +10,20 @@ import io.kotest.matchers.longs.shouldBeGreaterThan
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import voice.core.common.pref.CurrentBookStore
import voice.core.common.pref.FadeOutStore
import voice.core.common.pref.SleepTimerPreferenceStore
import voice.core.common.rootGraphAs
import voice.core.common.sleepTimer.SleepTimerPreference
import voice.core.data.BookContent
import voice.core.data.BookId
import voice.core.data.Chapter
import voice.core.data.ChapterId
import voice.core.data.repo.BookContentRepo
import voice.core.data.repo.ChapterRepo
import voice.core.data.sleeptimer.SleepTimerPreference
import voice.core.data.store.CurrentBookStore
import voice.core.data.store.FadeOutStore
import voice.core.data.store.SleepTimerPreferenceStore
import voice.core.playback.PlayerController
import voice.core.playback.playstate.PlayStateManager
import voice.core.playback.session.SleepTimer
import voice.core.sleeptimer.SleepTimer
import java.io.File
import java.time.Instant
import java.util.UUID

View File

@@ -1,8 +1,8 @@
package voice.app
import dev.zacsweers.metro.createGraphFactory
import voice.app.injection.App
import voice.app.injection.AppGraph
import voice.app.di.App
import voice.app.di.AppGraph
class TestApp : App() {

View File

@@ -5,7 +5,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import voice.app.injection.AppGraph
import voice.app.di.AppGraph
@SingleIn(AppScope::class)
@DependencyGraph(

View File

@@ -14,7 +14,7 @@
<application
android:enableOnBackInvokedCallback="true"
android:name=".injection.App"
android:name=".di.App"
android:icon="@drawable/ic_launcher"
android:appCategory="audio"
android:label="@string/app_name"
@@ -37,10 +37,21 @@
android:name="com.google.android.gms.car.application.theme"
android:resource="@style/Theme.Material3.DayNight" />
<activity
<activity-alias
android:name=".features.MainActivity"
android:targetActivity=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -1,4 +1,4 @@
package voice.app.features
package voice.app
import android.content.ActivityNotFoundException
import android.content.Context
@@ -18,10 +18,11 @@ import androidx.navigation3.ui.NavDisplay
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Inject
import voice.app.StartDestinationProvider
import voice.core.common.compose.VoiceTheme
import voice.app.navigation.NavEntryResolver
import voice.app.navigation.StartDestinationProvider
import voice.core.common.rootGraphAs
import voice.core.logging.core.Logger
import voice.core.ui.VoiceTheme
import voice.features.review.ReviewFeature
import voice.navigation.Destination
import voice.navigation.NavigationCommand

View File

@@ -1,4 +1,4 @@
package voice.app.injection
package voice.app.di
import android.app.Application
import android.content.Context
@@ -8,7 +8,6 @@ import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json
import voice.app.misc.AppInfoProviderImpl
import voice.app.misc.MainActivityIntentProviderImpl
@@ -48,10 +47,7 @@ object AndroidModule {
@Provides
@SingleIn(AppScope::class)
fun dispatcherProvider(): DispatcherProvider {
return DispatcherProvider(
main = Dispatchers.Main,
io = Dispatchers.IO,
)
return DispatcherProvider()
}
@Provides

View File

@@ -1,4 +1,4 @@
package voice.app.injection
package voice.app.di
import android.app.Application
import androidx.annotation.VisibleForTesting
@@ -12,12 +12,12 @@ import dev.zacsweers.metro.createGraphFactory
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import voice.app.features.widget.TriggerWidgetOnChange
import voice.core.common.DARK_THEME_SETTABLE
import voice.core.common.pref.DarkThemeStore
import voice.core.common.rootGraph
import voice.core.data.store.DarkThemeStore
import voice.core.scanner.MediaScanTrigger
import voice.features.sleepTimer.AutoEnableSleepTimer
import voice.core.sleeptimer.AutoEnableSleepTimer
import voice.core.ui.DARK_THEME_SETTABLE
import voice.features.widget.TriggerWidgetOnChange
open class App : Application() {

View File

@@ -0,0 +1,10 @@
package voice.app.di
import voice.app.features.widget.BaseWidgetProvider
import voice.features.widget.WidgetGraph
interface AppGraph : WidgetGraph {
fun inject(target: App)
override fun inject(target: BaseWidgetProvider)
}

View File

@@ -1,4 +1,4 @@
package voice.app.injection
package voice.app.di
import android.app.Application
import dev.zacsweers.metro.AppScope

View File

@@ -1,10 +0,0 @@
package voice.app.injection
import voice.app.features.MainActivity
import voice.app.features.widget.BaseWidgetProvider
interface AppGraph {
fun inject(target: App)
fun inject(target: BaseWidgetProvider)
}

View File

@@ -6,8 +6,5 @@ import voice.core.common.AppInfoProvider
@Inject
class AppInfoProviderImpl : AppInfoProvider {
override val applicationID = BuildConfig.APPLICATION_ID
override val versionName: String = BuildConfig.VERSION_NAME
}

View File

@@ -3,7 +3,7 @@ package voice.app.misc
import android.app.PendingIntent
import android.content.Context
import dev.zacsweers.metro.Inject
import voice.app.features.MainActivity
import voice.app.MainActivity
import voice.core.playback.notification.MainActivityIntentProvider
@Inject

View File

@@ -1,4 +1,4 @@
package voice.app.features
package voice.app.navigation
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry

View File

@@ -1,15 +1,15 @@
package voice.app
package voice.app.navigation
import android.content.Intent
import androidx.datastore.core.DataStore
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import voice.app.features.MainActivity.Companion.NI_GO_TO_BOOK
import voice.core.common.pref.CurrentBookStore
import voice.core.common.pref.OnboardingCompletedStore
import voice.app.MainActivity
import voice.core.data.BookId
import voice.core.data.folders.AudiobookFolders
import voice.core.data.store.CurrentBookStore
import voice.core.data.store.OnboardingCompletedStore
import voice.core.playback.PlayerController
import voice.navigation.Destination
@@ -29,7 +29,7 @@ class StartDestinationProvider(
return listOf(Destination.OnboardingWelcome)
}
val goToBook = intent.getBooleanExtra(NI_GO_TO_BOOK, false)
val goToBook = intent.getBooleanExtra(MainActivity.Companion.NI_GO_TO_BOOK, false)
if (goToBook) {
val bookId = runBlocking { currentBookStore.data.first() }
if (bookId != null) {

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12 8c1.1 0 2-0.9 2-2s-0.9-2-2-2-2 0.9-2 2 0.9 2 2 2zm0 2c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm0 6c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/coverImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/content_cover" />
<voice.app.features.imagepicker.CropOverlay
android:id="@+id/cropOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="colorAccentDark" format="reference" />
</resources>

View File

@@ -1,11 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- copied private resources from appcompat -->
<color name="copy_primary_text_disabled_material_dark">#4Dffffff</color>
<color name="copy_primary_text_default_material_dark">#ffffffff</color>
<color name="copy_background_material_dark">@color/material_grey_850</color>
<color name="copy_secondary_text_disabled_material_dark">#36ffffff</color>
<color name="copy_secondary_text_default_material_dark">#b3ffffff</color>
<color name="ic_shortcut_play_background">#FFFFFF</color>
</resources>

View File

@@ -5,7 +5,7 @@
android:shortcutShortLabel="@string/play_current">
<intent
android:action="playCurrent"
android:targetClass="voice.app.features.MainActivity"
android:targetClass="voice.app.MainActivity"
android:targetPackage="de.ph1b.audiobook" />
</shortcut>
</shortcuts>

View File

@@ -11,6 +11,7 @@ import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import org.junit.Test
import voice.app.navigation.NavEntryResolver
import voice.navigation.Destination
import kotlin.reflect.KClass

View File

@@ -2,7 +2,7 @@ package voice.app.misc
import io.kotest.matchers.shouldBe
import org.junit.Test
import voice.core.common.formatTime
import voice.core.ui.formatTime
import java.util.concurrent.TimeUnit
class FormatTimeKtTest {

View File

@@ -1,31 +1,10 @@
plugins {
id("voice.library")
id("voice.compose")
alias(libs.plugins.metro)
alias(libs.plugins.kotlin.serialization)
}
android {
androidResources {
enable = true
}
}
dependencies {
implementation(projects.core.strings)
implementation(libs.appCompat)
implementation(libs.material)
api(libs.immutable)
api(libs.datastore)
implementation(libs.androidxCore)
api(libs.navigation3.runtime)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.serialization.json)
implementation(libs.androidxCore)
testImplementation(kotlin("reflect"))
testImplementation(libs.junit)
testImplementation(libs.androidX.test.core)
testImplementation(libs.androidX.test.junit)
testImplementation(libs.androidX.test.runner)
testImplementation(libs.robolectric)
testImplementation(libs.bundles.testing.jvm)
}

View File

@@ -1,6 +1,5 @@
package voice.core.common
interface AppInfoProvider {
val applicationID: String
val versionName: String
}

View File

@@ -1,15 +1,17 @@
package voice.core.common
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlin.coroutines.CoroutineContext
data class DispatcherProvider(
val main: CoroutineContext,
val io: CoroutineContext,
val main: CoroutineContext = Dispatchers.Main,
val io: CoroutineContext = Dispatchers.IO,
val mainImmediate: CoroutineContext = Dispatchers.Main.immediate,
)
@Suppress("FunctionName")
fun MainScope(dispatcherProvider: DispatcherProvider): CoroutineScope {
return CoroutineScope(SupervisorJob() + dispatcherProvider.main)
return CoroutineScope(SupervisorJob() + dispatcherProvider.mainImmediate)
}

View File

@@ -1,27 +0,0 @@
package voice.core.common.pref
import dev.zacsweers.metro.Qualifier
@Qualifier
annotation class OnboardingCompletedStore
@Qualifier
annotation class CurrentBookStore
@Qualifier
annotation class AutoRewindAmountStore
@Qualifier
annotation class SeekTimeStore
@Qualifier
annotation class SleepTimerPreferenceStore
@Qualifier
annotation class GridModeStore
@Qualifier
annotation class DarkThemeStore
@Qualifier
annotation class FadeOutStore

View File

@@ -11,6 +11,7 @@ kotlin {
dependencies {
api(projects.core.common)
api(projects.core.documentfile)
implementation(libs.metro.runtime)
implementation(libs.appCompat)
implementation(libs.androidxCore)
implementation(libs.serialization.json)

View File

@@ -1,9 +1,9 @@
package voice.core.common.grid
package voice.core.data
import kotlinx.serialization.Serializable
@Serializable
enum class GridMode {
public enum class GridMode {
LIST,
GRID,
FOLLOW_DEVICE,

View File

@@ -1,4 +1,4 @@
package voice.core.common.sleepTimer
package voice.core.data.sleeptimer
import kotlinx.serialization.Serializable
import voice.core.common.serialization.LocalTimeSerializer
@@ -7,7 +7,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@Serializable
data class SleepTimerPreference(
public data class SleepTimerPreference(
/**
* The custom sleep time duration
*/
@@ -22,8 +22,8 @@ data class SleepTimerPreference(
val autoSleepEndTime: LocalTime,
) {
companion object {
val Default = SleepTimerPreference(
public companion object {
public val Default: SleepTimerPreference = SleepTimerPreference(
autoSleepTimerEnabled = false,
autoSleepStartTime = LocalTime.of(22, 0),
autoSleepEndTime = LocalTime.of(6, 0),

View File

@@ -0,0 +1,33 @@
package voice.core.data.store
import dev.zacsweers.metro.Qualifier
@Qualifier
public annotation class OnboardingCompletedStore
@Qualifier
public annotation class CurrentBookStore
@Qualifier
public annotation class AutoRewindAmountStore
@Qualifier
public annotation class SeekTimeStore
@Qualifier
public annotation class SleepTimerPreferenceStore
@Qualifier
public annotation class GridModeStore
@Qualifier
public annotation class DarkThemeStore
@Qualifier
public annotation class FadeOutStore
@Qualifier
public annotation class AmountOfBatteryOptimizationRequestedStore
@Qualifier
public annotation class ReviewDialogShownStore

View File

@@ -32,8 +32,8 @@ dependencies {
api(projects.core.data.api)
api(projects.core.common)
api(projects.core.documentfile)
implementation(projects.core.datastore)
implementation(libs.datastore)
implementation(libs.androidxCore)
implementation(libs.serialization.json)
implementation(libs.coroutines.core)

View File

@@ -9,7 +9,7 @@ import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import kotlinx.serialization.builtins.SetSerializer
import voice.core.common.serialization.UriSerializer
import voice.core.datastore.VoiceDataStoreFactory
import voice.core.data.store.VoiceDataStoreFactory
@BindingContainer
@ContributesTo(AppScope::class)

View File

@@ -1,4 +1,4 @@
package voice.core.datastore
package voice.core.data.store
import androidx.datastore.core.Serializer
import kotlinx.serialization.ExperimentalSerializationApi
@@ -9,7 +9,7 @@ import kotlinx.serialization.json.encodeToStream
import java.io.InputStream
import java.io.OutputStream
class KotlinxDataStoreSerializer<T>(
internal class KotlinxDataStoreSerializer<T>(
override val defaultValue: T,
private val json: Json,
private val serializer: KSerializer<T>,

View File

@@ -1,10 +1,10 @@
package voice.app.injection
package voice.core.data.store
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.datastore.core.DataMigration
class PrefsDataMigration<T>(
internal class PrefsDataMigration<T>(
private val sharedPreferences: SharedPreferences,
private val key: String,
private val getFromSharedPreferences: () -> T,
@@ -24,7 +24,7 @@ class PrefsDataMigration<T>(
}
}
fun booleanPrefsDataMigration(
internal fun booleanPrefsDataMigration(
sharedPreferences: SharedPreferences,
key: String,
): DataMigration<Boolean> {
@@ -37,7 +37,7 @@ fun booleanPrefsDataMigration(
)
}
fun intPrefsDataMigration(
internal fun intPrefsDataMigration(
sharedPreferences: SharedPreferences,
key: String,
): DataMigration<Int> {

View File

@@ -1,5 +1,6 @@
package voice.app.injection
package voice.core.data.store
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStore
@@ -10,30 +11,23 @@ import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import voice.app.BuildConfig
import voice.core.common.grid.GridMode
import voice.core.common.pref.AutoRewindAmountStore
import voice.core.common.pref.CurrentBookStore
import voice.core.common.pref.DarkThemeStore
import voice.core.common.pref.FadeOutStore
import voice.core.common.pref.GridModeStore
import voice.core.common.pref.OnboardingCompletedStore
import voice.core.common.pref.SeekTimeStore
import voice.core.common.pref.SleepTimerPreferenceStore
import voice.core.common.sleepTimer.SleepTimerPreference
import voice.core.data.BookId
import voice.core.datastore.VoiceDataStoreFactory
import voice.core.data.GridMode
import voice.core.data.sleeptimer.SleepTimerPreference
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@BindingContainer
@ContributesTo(AppScope::class)
object PrefsModule {
internal object StoreModule {
@Provides
@SingleIn(AppScope::class)
fun sharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_preferences", Context.MODE_PRIVATE)
fun sharedPreferences(context: Application): SharedPreferences {
return context.getSharedPreferences(
"${context.packageName}_preferences",
Context.MODE_PRIVATE,
)
}
@Provides
@@ -73,7 +67,7 @@ object PrefsModule {
return factory.create(
fileName = "fadeOut",
defaultValue = 10.seconds,
serializer = Duration.serializer(),
serializer = Duration.Companion.serializer(),
)
}
@@ -96,9 +90,9 @@ object PrefsModule {
@SleepTimerPreferenceStore
fun sleepTimerPreference(factory: VoiceDataStoreFactory): DataStore<SleepTimerPreference> {
return factory.create(
serializer = SleepTimerPreference.serializer(),
serializer = SleepTimerPreference.Companion.serializer(),
fileName = "sleepTime3",
defaultValue = SleepTimerPreference.Default,
defaultValue = SleepTimerPreference.Companion.Default,
)
}
@@ -146,4 +140,18 @@ object PrefsModule {
defaultValue = null,
)
}
@Provides
@SingleIn(AppScope::class)
@AmountOfBatteryOptimizationRequestedStore
fun amountOfBatteryOptimizationsRequestedStore(factory: VoiceDataStoreFactory): DataStore<Int> {
return factory.int("amountOfBatteryOptimizationsRequestedStore", 0)
}
@Provides
@SingleIn(AppScope::class)
@ReviewDialogShownStore
fun reviewDialogShown(factory: VoiceDataStoreFactory): DataStore<Boolean> {
return factory.create(Boolean.serializer(), false, "reviewDialogShown")
}
}

View File

@@ -1,6 +1,6 @@
package voice.core.datastore
package voice.core.data.store
import android.content.Context
import android.app.Application
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
@@ -11,9 +11,9 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
@Inject
class VoiceDataStoreFactory(
internal class VoiceDataStoreFactory(
private val json: Json,
private val context: Context,
private val context: Application,
) {
fun <T> create(

View File

@@ -1,5 +1,6 @@
package voice.app.injection
package vocie.core.data.store
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
@@ -12,7 +13,9 @@ import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import voice.core.datastore.VoiceDataStoreFactory
import voice.core.data.store.VoiceDataStoreFactory
import voice.core.data.store.booleanPrefsDataMigration
import voice.core.data.store.intPrefsDataMigration
@RunWith(AndroidJUnit4::class)
class DataMigrationTests {
@@ -22,7 +25,7 @@ class DataMigrationTests {
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
val context = ApplicationProvider.getApplicationContext<Application>()
sharedPreferences = context.getSharedPreferences("de.ph1b.audiobook_preferences", Context.MODE_PRIVATE)
factory = VoiceDataStoreFactory(Json { ignoreUnknownKeys = true }, context)
}

View File

@@ -1,4 +1,4 @@
package voice.app.injection
package vocie.core.data.store
import android.app.Application
import android.content.SharedPreferences
@@ -18,12 +18,14 @@ import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.Test
import org.junit.runner.RunWith
import voice.core.common.grid.GridMode
import voice.core.common.pref.AutoRewindAmountStore
import voice.core.common.pref.DarkThemeStore
import voice.core.common.pref.GridModeStore
import voice.core.common.pref.SeekTimeStore
import voice.core.datastore.VoiceDataStoreFactory
import voice.core.common.AppInfoProvider
import voice.core.data.GridMode
import voice.core.data.store.AutoRewindAmountStore
import voice.core.data.store.DarkThemeStore
import voice.core.data.store.GridModeStore
import voice.core.data.store.SeekTimeStore
import voice.core.data.store.VoiceDataStoreFactory
import voice.core.data.store.intPrefsDataMigration
@SingleIn(AppScope::class)
@DependencyGraph(
@@ -46,6 +48,16 @@ interface MigrationTestGraph {
@get:Provides
val application: Application get() = ApplicationProvider.getApplicationContext()
@get:Provides
val json: Json get() = Json.Default
@get:Provides
val appInfoProvider: AppInfoProvider
get() = object : AppInfoProvider {
override val versionName: String
get() = "1.2.3"
}
val sharedPreferences: SharedPreferences
}

View File

@@ -9,7 +9,7 @@ import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import dev.zacsweers.metro.createGraphFactory
import voice.core.common.pref.DarkThemeStore
import voice.core.data.store.DarkThemeStore
@SingleIn(AppScope::class)
@DependencyGraph(

View File

@@ -1,9 +0,0 @@
plugins {
id("voice.library")
alias(libs.plugins.metro)
}
dependencies {
api(libs.serialization.json)
api(libs.datastore)
}

View File

@@ -4,6 +4,5 @@ plugins {
}
dependencies {
implementation(projects.core.common)
implementation(libs.androidxCore)
}

View File

@@ -15,10 +15,10 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.launch
import voice.core.common.pref.CurrentBookStore
import voice.core.data.BookId
import voice.core.data.ChapterId
import voice.core.data.repo.BookRepository
import voice.core.data.store.CurrentBookStore
import voice.core.logging.core.Logger
import voice.core.playback.misc.Decibel
import voice.core.playback.session.CustomCommand

View File

@@ -11,14 +11,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import voice.core.common.pref.AutoRewindAmountStore
import voice.core.common.pref.CurrentBookStore
import voice.core.common.pref.SeekTimeStore
import voice.core.data.BookContent
import voice.core.data.BookId
import voice.core.data.Chapter
import voice.core.data.repo.BookRepository
import voice.core.data.repo.ChapterRepo
import voice.core.data.store.AutoRewindAmountStore
import voice.core.data.store.CurrentBookStore
import voice.core.data.store.SeekTimeStore
import voice.core.logging.core.Logger
import voice.core.playback.misc.Decibel
import voice.core.playback.misc.VolumeGain

View File

@@ -24,10 +24,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.guava.future
import kotlinx.coroutines.launch
import voice.core.common.pref.CurrentBookStore
import voice.core.data.Book
import voice.core.data.BookId
import voice.core.data.repo.BookRepository
import voice.core.data.store.CurrentBookStore
import voice.core.logging.core.Logger
import voice.core.playback.player.VoicePlayer
import voice.core.playback.session.search.BookSearchHandler

View File

@@ -9,7 +9,6 @@ import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import voice.core.common.pref.CurrentBookStore
import voice.core.data.Book
import voice.core.data.BookComparator
import voice.core.data.BookContent
@@ -18,6 +17,7 @@ import voice.core.data.Chapter
import voice.core.data.repo.BookContentRepo
import voice.core.data.repo.BookRepository
import voice.core.data.repo.ChapterRepo
import voice.core.data.store.CurrentBookStore
import voice.core.data.toUri
import java.io.File
import voice.core.strings.R as StringsR

View File

@@ -4,10 +4,10 @@ import android.provider.MediaStore
import androidx.datastore.core.DataStore
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.first
import voice.core.common.pref.CurrentBookStore
import voice.core.data.Book
import voice.core.data.BookId
import voice.core.data.repo.BookRepository
import voice.core.data.store.CurrentBookStore
import voice.core.logging.core.Logger
@Inject

View File

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 782 B

View File

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 496 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -5,6 +5,5 @@ plugins {
dependencies {
implementation(projects.core.remoteconfig.core)
implementation(projects.core.common)
implementation(libs.firebase.remoteconfig)
}

View File

@@ -5,5 +5,4 @@ plugins {
dependencies {
implementation(projects.core.remoteconfig.core)
implementation(projects.core.common)
}

View File

@@ -9,7 +9,6 @@ kotlin {
dependencies {
implementation(projects.core.data.api)
implementation(projects.core.common)
implementation(libs.slf4j.noop)
implementation(libs.jebml)

View File

@@ -0,0 +1,11 @@
plugins {
id("voice.library")
}
dependencies {
testImplementation(libs.junit)
testImplementation(libs.androidX.test.core)
testImplementation(libs.androidX.test.junit)
testImplementation(libs.androidX.test.runner)
testImplementation(libs.robolectric)
}

View File

@@ -1,4 +1,4 @@
package voice.core.playback.session
package voice.core.sleeptimer
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration
@@ -7,4 +7,5 @@ interface SleepTimer {
val leftSleepTimeFlow: Flow<Duration>
fun sleepTimerActive(): Boolean
fun setActive(enable: Boolean)
fun setActive(sleepTime: Duration)
}

View File

@@ -0,0 +1,26 @@
plugins {
id("voice.library")
alias(libs.plugins.metro)
}
dependencies {
implementation(projects.core.sleeptimer.api)
implementation(projects.core.data.api)
implementation(projects.core.common)
implementation(projects.core.playback)
implementation(projects.core.logging.core)
implementation(libs.datastore)
implementation(libs.androidxCore)
implementation(libs.seismic)
implementation(libs.appCompat)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.koTest.assert)
testImplementation(libs.coroutines.test)
testImplementation(libs.androidX.test.core)
testImplementation(libs.androidX.test.junit)
testImplementation(libs.androidX.test.runner)
testImplementation(libs.robolectric)
}

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import androidx.datastore.core.DataStore
import dev.zacsweers.metro.Inject
@@ -7,15 +7,14 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import voice.core.common.DispatcherProvider
import voice.core.common.MainScope
import voice.core.common.pref.CurrentBookStore
import voice.core.common.pref.SleepTimerPreferenceStore
import voice.core.common.sleepTimer.SleepTimerPreference
import voice.core.data.BookId
import voice.core.data.repo.BookRepository
import voice.core.data.repo.BookmarkRepo
import voice.core.data.sleeptimer.SleepTimerPreference
import voice.core.data.store.CurrentBookStore
import voice.core.data.store.SleepTimerPreferenceStore
import voice.core.playback.playstate.PlayStateManager
import voice.core.playback.playstate.PlayStateManager.PlayState.Playing
import voice.core.playback.session.SleepTimer
import java.time.Clock
@Inject

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import java.time.LocalTime

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import android.content.Context
import android.hardware.SensorManager

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import androidx.datastore.core.DataStore
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
@@ -14,9 +14,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import voice.core.common.pref.FadeOutStore
import voice.core.common.pref.SleepTimerPreferenceStore
import voice.core.common.sleepTimer.SleepTimerPreference
import voice.core.data.sleeptimer.SleepTimerPreference
import voice.core.data.store.FadeOutStore
import voice.core.data.store.SleepTimerPreferenceStore
import voice.core.logging.core.Logger
import voice.core.playback.PlayerController
import voice.core.playback.playstate.PlayStateManager
@@ -24,12 +24,12 @@ import voice.core.playback.playstate.PlayStateManager.PlayState.Playing
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import voice.core.playback.session.SleepTimer as PlaybackSleepTimer
import voice.core.sleeptimer.SleepTimer as SleepTimerApi
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
@Inject
class SleepTimer(
class SleepTimerImpl(
private val playStateManager: PlayStateManager,
private val shakeDetector: ShakeDetector,
@SleepTimerPreferenceStore
@@ -37,7 +37,7 @@ class SleepTimer(
private val playerController: PlayerController,
@FadeOutStore
private val fadeOutStore: DataStore<Duration>,
) : PlaybackSleepTimer {
) : SleepTimerApi {
private val scope = MainScope()
@@ -68,7 +68,7 @@ class SleepTimer(
}
}
fun setActive(sleepTime: Duration) {
override fun setActive(sleepTime: Duration) {
Logger.i("Starting sleepTimer. Pause in $sleepTime.")
leftSleepTime = sleepTime
playerController.setVolume(1F)

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import androidx.datastore.core.DataStore
import io.mockk.coEvery
@@ -20,13 +20,12 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import voice.core.common.DispatcherProvider
import voice.core.common.sleepTimer.SleepTimerPreference
import voice.core.data.Book
import voice.core.data.BookId
import voice.core.data.repo.BookRepository
import voice.core.data.repo.BookmarkRepo
import voice.core.data.sleeptimer.SleepTimerPreference
import voice.core.playback.playstate.PlayStateManager
import voice.core.playback.session.SleepTimer
import java.time.Instant
import java.time.LocalDateTime
import java.time.LocalTime
@@ -63,7 +62,7 @@ class AutoEnableSleepTimerTest {
bookRepository = mockk(relaxUnitFun = true)
currentBookStore = mockk<DataStore<BookId?>>(relaxUnitFun = true)
val dispatcherProvider = DispatcherProvider(main = testDispatcher, testDispatcher)
val dispatcherProvider = DispatcherProvider(testDispatcher, testDispatcher, testDispatcher)
autoEnableSleepTimer = AutoEnableSleepTimer(
sleepTimerPreferenceStore = sleepTimerPreferenceStore,
dispatcherProvider = dispatcherProvider,
@@ -122,7 +121,7 @@ class AutoEnableSleepTimerTest {
playStateFlow.value = PlayStateManager.PlayState.Playing
advanceUntilIdle()
coVerify(exactly = 0) { sleepTimer.setActive(any()) }
coVerify(exactly = 0) { sleepTimer.setActive(any<Boolean>()) }
coVerify(exactly = 0) { bookmarkRepo.addBookmarkAtBookPosition(any(), any(), any()) }
}
@@ -138,7 +137,7 @@ class AutoEnableSleepTimerTest {
playStateFlow.value = PlayStateManager.PlayState.Playing
advanceUntilIdle()
coVerify(exactly = 0) { sleepTimer.setActive(any()) }
coVerify(exactly = 0) { sleepTimer.setActive(any<Boolean>()) }
coVerify(exactly = 0) { bookmarkRepo.addBookmarkAtBookPosition(any(), any(), any()) }
}
@@ -156,7 +155,7 @@ class AutoEnableSleepTimerTest {
playStateFlow.value = PlayStateManager.PlayState.Playing
advanceUntilIdle()
coVerify(exactly = 0) { sleepTimer.setActive(any()) }
coVerify(exactly = 0) { sleepTimer.setActive(any<Boolean>()) }
coVerify(exactly = 0) { bookmarkRepo.addBookmarkAtBookPosition(any(), any(), any()) }
}
@@ -173,7 +172,7 @@ class AutoEnableSleepTimerTest {
advanceUntilIdle()
coVerify(exactly = 0) { sleepTimerPreferenceStore.data }
coVerify(exactly = 0) { sleepTimer.setActive(any()) }
coVerify(exactly = 0) { sleepTimer.setActive(any<Boolean>()) }
coVerify(exactly = 0) { bookmarkRepo.addBookmarkAtBookPosition(any(), any(), any()) }
}
@@ -233,7 +232,7 @@ class AutoEnableSleepTimerTest {
}
advanceUntilIdle()
coVerify(exactly = 0) { sleepTimer.setActive(any()) }
coVerify(exactly = 0) { sleepTimer.setActive(any<Boolean>()) }
coVerify(exactly = 0) { bookmarkRepo.addBookmarkAtBookPosition(any(), any(), any()) }
}

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import io.kotest.matchers.shouldBe
import org.junit.Test

View File

@@ -1,4 +1,4 @@
package voice.features.sleepTimer
package voice.core.sleeptimer
import java.time.Clock
import java.time.Instant

17
core/ui/build.gradle.kts Normal file
View File

@@ -0,0 +1,17 @@
plugins {
id("voice.library")
id("voice.compose")
alias(libs.plugins.metro)
}
android {
androidResources.enable = true
}
dependencies {
api(libs.datastore)
implementation(libs.material)
implementation(projects.core.data.api)
implementation(projects.core.strings)
implementation(libs.lifecycle.viewmodel.compose)
}

View File

@@ -1,4 +1,4 @@
package voice.core.common
package voice.core.ui
import android.os.Build

View File

@@ -1,4 +1,4 @@
package voice.core.common
package voice.core.ui
import android.content.Context
import android.util.TypedValue

View File

@@ -1,4 +1,4 @@
package voice.core.common
package voice.core.ui
import java.util.concurrent.TimeUnit

View File

@@ -1,4 +1,4 @@
package voice.core.common.grid
package voice.core.ui
import android.content.Context
import dev.zacsweers.metro.Inject

View File

@@ -1,4 +1,4 @@
package voice.core.common.compose
package voice.core.ui
import androidx.compose.runtime.Immutable
import java.io.File

View File

@@ -1,6 +1,6 @@
@file:Suppress("ktlint:standard:filename")
package voice.core.common.compose
package voice.core.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.Dp

View File

@@ -1,4 +1,4 @@
package voice.core.common.compose
package voice.core.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.graphics.res.animatedVectorResource
@@ -15,7 +15,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import voice.core.common.R as CommonR
import voice.core.strings.R as StringsR
@Composable
@@ -53,7 +52,7 @@ fun PlayButton(
private fun rememberPlayIconPainter(playing: Boolean): Painter {
return rememberAnimatedVectorPainter(
animatedImageVector = AnimatedImageVector.animatedVectorResource(
id = CommonR.drawable.avd_pause_to_play,
id = R.drawable.avd_pause_to_play,
),
atEnd = !playing,
)

View File

@@ -1,9 +1,9 @@
package voice.core.common.compose
package voice.core.ui
import androidx.datastore.core.DataStore
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import voice.core.common.pref.DarkThemeStore
import voice.core.data.store.DarkThemeStore
@ContributesTo(AppScope::class)
interface SharedGraph {

View File

@@ -1,4 +1,4 @@
package voice.core.common.compose
package voice.core.ui
import androidx.compose.runtime.Composable
import androidx.lifecycle.ViewModel

View File

@@ -1,23 +1,17 @@
package voice.core.common.compose
package voice.core.ui
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.Dispatchers
import voice.core.common.DARK_THEME_SETTABLE
import voice.core.common.rootGraphAs
import androidx.compose.material3.MaterialTheme as Material3Theme
@Composable
fun VoiceTheme(content: @Composable () -> Unit) {
Material3Theme(
MaterialTheme(
colorScheme = if (isDarkTheme()) {
if (Build.VERSION.SDK_INT >= 31) {
dynamicDarkColorScheme(LocalContext.current)
@@ -35,15 +29,3 @@ fun VoiceTheme(content: @Composable () -> Unit) {
content()
}
}
@Composable
fun isDarkTheme(): Boolean {
return if (DARK_THEME_SETTABLE) {
val darkThemeFlow = remember {
rootGraphAs<SharedGraph>().useDarkThemeStore.data
}
darkThemeFlow.collectAsState(initial = false, context = Dispatchers.Unconfined).value
} else {
isSystemInDarkTheme()
}
}

View File

@@ -0,0 +1,20 @@
package voice.core.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import kotlinx.coroutines.Dispatchers
import voice.core.common.rootGraphAs
@Composable
fun isDarkTheme(): Boolean {
return if (DARK_THEME_SETTABLE) {
val darkThemeFlow = remember {
rootGraphAs<SharedGraph>().useDarkThemeStore.data
}
darkThemeFlow.collectAsState(initial = false, context = Dispatchers.Unconfined).value
} else {
isSystemInDarkTheme()
}
}

View File

Before

Width:  |  Height:  |  Size: 230 B

After

Width:  |  Height:  |  Size: 230 B

View File

Before

Width:  |  Height:  |  Size: 96 B

After

Width:  |  Height:  |  Size: 96 B

View File

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 220 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

View File

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 180 B

View File

Before

Width:  |  Height:  |  Size: 76 B

After

Width:  |  Height:  |  Size: 76 B

View File

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 88 B

View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

View File

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 274 B

View File

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 404 B

View File

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 86 B

View File

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 334 B

View File

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 382 B

View File

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 462 B

View File

Before

Width:  |  Height:  |  Size: 76 B

After

Width:  |  Height:  |  Size: 76 B

Some files were not shown because too many files have changed in this diff Show More