From 1028b748a934c8afd853dceecb61aa77f11890fb Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Fri, 19 Jun 2026 18:12:16 +0200 Subject: [PATCH] refactor: move subtitle package to standalone data module, update dependencies --- data/subtitle/build.gradle.kts | 53 +++++++++++++++++++ .../gwenedeg/subtitle/data/LrcParser.kt | 2 +- .../subtitle/data/LrcSubtitleRepository.kt | 9 ++-- .../gwenedeg/subtitle/di/SubtitleModule.kt | 16 ++++++ .../domain/GetCurrentSubtitleIndexUseCase.kt | 0 .../gwenedeg/subtitle/domain/Subtitle.kt | 0 .../gwenedeg/subtitle/domain/SubtitleLine.kt | 0 .../subtitle/domain/SubtitleRepository.kt | 2 +- .../model/CantGetSubtitleException.kt | 0 .../GetCurrentSubtitleIndexUseCaseTest.kt | 0 .../ajaury/gwenedeg/subtitle/LrcParserTest.kt | 0 settings.gradle.kts | 3 ++ shared/build.gradle.kts | 3 ++ .../kotlin/fr/ajaury/gwenedeg/App.kt | 4 +- .../kotlin/fr/ajaury/gwenedeg/di/Modules.kt | 10 ++-- .../player/ui/viewmodel/PlayerViewModel.kt | 5 +- .../data/ComposeResourceReader.kt | 8 +++ .../resourcereader/domain/ResourceReader.kt | 9 ++++ 18 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 data/subtitle/build.gradle.kts rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt (98%) rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt (65%) create mode 100644 data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/di/SubtitleModule.kt rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt (100%) rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/Subtitle.kt (100%) rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleLine.kt (100%) rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt (56%) rename {shared => data/subtitle}/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/model/CantGetSubtitleException.kt (100%) rename {shared => data/subtitle}/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt (100%) rename {shared => data/subtitle}/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/LrcParserTest.kt (100%) create mode 100644 shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt create mode 100644 shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/domain/ResourceReader.kt diff --git a/data/subtitle/build.gradle.kts b/data/subtitle/build.gradle.kts new file mode 100644 index 0000000..85a7630 --- /dev/null +++ b/data/subtitle/build.gradle.kts @@ -0,0 +1,53 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) +} + +kotlin { + iosArm64() + iosSimulatorArm64() + + jvm() + + js { + browser() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + androidLibrary { + namespace = "fr.ajaury.gwenedeg.subtitle" + compileSdk = + libs.versions.androidCompileSdk + .get() + .toInt() + minSdk = + libs.versions.androidMinSdk + .get() + .toInt() + + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } + } + + sourceSets { + commonMain.dependencies { + // Logging + implementation(projects.core.logging) + + // DI + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt similarity index 98% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt index 5ea004c..57c57b6 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt +++ b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcParser.kt @@ -9,7 +9,7 @@ import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine * Only numeric timestamp tags `[mm:ss.xx]` / `[mm:ss.xxx]` are kept. A line may carry several * leading timestamps that all share the same text. */ -class LrcParser { +internal class LrcParser { private val timeTag = Regex("""\[(\d{1,2}):(\d{2})[.:](\d{2,3})]""") private val leadingTimeTags = Regex("""^(?:\[\d{1,2}:\d{2}[.:]\d{2,3}])+""") diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt similarity index 65% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt index c987749..84f6d27 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt +++ b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/data/LrcSubtitleRepository.kt @@ -4,17 +4,16 @@ import fr.ajaury.gwenedeg.core.logging.domain.Logger import fr.ajaury.gwenedeg.subtitle.domain.Subtitle import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository import fr.ajaury.gwenedeg.subtitle.model.CantGetSubtitleException -import gwenedeg.shared.generated.resources.Res -class LrcSubtitleRepository( +internal class LrcSubtitleRepository( private val parser: LrcParser, private val logger: Logger, ) : SubtitleRepository { - override suspend fun getSubtitle(resourcePath: String): Subtitle = + override suspend fun getSubtitle(content: String): Subtitle = try { - parser.parse(Res.readBytes(resourcePath).decodeToString()) + parser.parse(content = content) } catch (exception: Exception) { - val message = "Failed to load subtitle: $resourcePath" + val message = "Failed to load subtitle: $content" logger.error(message = message, throwable = exception) throw CantGetSubtitleException(message) } diff --git a/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/di/SubtitleModule.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/di/SubtitleModule.kt new file mode 100644 index 0000000..853e477 --- /dev/null +++ b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/di/SubtitleModule.kt @@ -0,0 +1,16 @@ +package fr.ajaury.gwenedeg.subtitle.di + +import fr.ajaury.gwenedeg.subtitle.data.LrcParser +import fr.ajaury.gwenedeg.subtitle.data.LrcSubtitleRepository +import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase +import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository +import org.koin.core.module.Module +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val subtitleModule: Module = module { + factoryOf(::LrcParser) + factoryOf(::LrcSubtitleRepository) { bind() } + factoryOf(::GetCurrentSubtitleIndexUseCase) +} diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt similarity index 100% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/Subtitle.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/Subtitle.kt similarity index 100% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/Subtitle.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/Subtitle.kt diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleLine.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleLine.kt similarity index 100% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleLine.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleLine.kt diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt similarity index 56% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt index 397c832..4427d09 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt +++ b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleRepository.kt @@ -1,5 +1,5 @@ package fr.ajaury.gwenedeg.subtitle.domain interface SubtitleRepository { - suspend fun getSubtitle(resourcePath: String): Subtitle + suspend fun getSubtitle(content: String): Subtitle } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/model/CantGetSubtitleException.kt b/data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/model/CantGetSubtitleException.kt similarity index 100% rename from shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/model/CantGetSubtitleException.kt rename to data/subtitle/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/model/CantGetSubtitleException.kt diff --git a/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt b/data/subtitle/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt similarity index 100% rename from shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt rename to data/subtitle/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt diff --git a/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/LrcParserTest.kt b/data/subtitle/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/LrcParserTest.kt similarity index 100% rename from shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/LrcParserTest.kt rename to data/subtitle/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/LrcParserTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d3ff06..f0c9e08 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,3 +40,6 @@ include(":shared") // Core modules include(":core:audioplayer") include(":core:logging") + +// Data modules +include(":data:subtitle") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 1b89ba9..57d3a15 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -62,6 +62,9 @@ kotlin { implementation(projects.core.audioplayer) implementation(projects.core.logging) + // Data modules + implementation(projects.data.subtitle) + implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.material3) diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt index 7971921..84a0cc8 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt @@ -13,6 +13,7 @@ import fr.ajaury.gwenedeg.navigation.Route import fr.ajaury.gwenedeg.player.di.audioPlayerModule import fr.ajaury.gwenedeg.player.ui.PlayerScreen import fr.ajaury.gwenedeg.records.ui.RecordsScreen +import fr.ajaury.gwenedeg.subtitle.di.subtitleModule import fr.ajaury.gwenedeg.theme.GwenedegTheme import org.koin.compose.KoinApplication import org.koin.dsl.koinConfiguration @@ -25,7 +26,8 @@ fun App() { modules( sharedModule, audioPlayerModule, - loggingModule + loggingModule, + subtitleModule ) }, ) { diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt index adeab9d..edfc8ee 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt @@ -4,10 +4,8 @@ import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel import fr.ajaury.gwenedeg.records.data.InMemoryRecordRepository import fr.ajaury.gwenedeg.records.domain.RecordRepository import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel -import fr.ajaury.gwenedeg.subtitle.data.LrcParser -import fr.ajaury.gwenedeg.subtitle.data.LrcSubtitleRepository -import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase -import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository +import fr.ajaury.gwenedeg.resourcereader.data.ComposeResourceReader +import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.viewModelOf @@ -15,9 +13,7 @@ import org.koin.dsl.module val sharedModule = module { factoryOf(::InMemoryRecordRepository) { bind() } - factoryOf(::LrcParser) - factoryOf(::LrcSubtitleRepository) { bind() } - factoryOf(::GetCurrentSubtitleIndexUseCase) viewModelOf(::RecordsViewModel) viewModelOf(::PlayerViewModel) + factoryOf(::ComposeResourceReader) { bind() } } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt index c829b76..09413a6 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt @@ -6,6 +6,7 @@ import fr.ajaury.gwenedeg.core.logging.domain.Logger import fr.ajaury.gwenedeg.player.domain.AudioPlayer import fr.ajaury.gwenedeg.player.domain.AudioSessionManager import fr.ajaury.gwenedeg.player.model.PlaybackState +import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.Subtitle import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository @@ -26,6 +27,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class PlayerViewModel( + private val resourceReader: ResourceReader, private val audioPlayer: AudioPlayer, private val audioSessionManager: AudioSessionManager, private val subtitleRepository: SubtitleRepository, @@ -92,7 +94,8 @@ class PlayerViewModel( viewModelScope.launch { subtitle.value = try { - subtitleRepository.getSubtitle(resourcePath) + val content = resourceReader.read(resourcePath = resourcePath) + subtitleRepository.getSubtitle(content = content) } catch (exception: Exception) { logger.error( message = "Failed to load subtitle: $resourcePath", diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt new file mode 100644 index 0000000..aa60731 --- /dev/null +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/data/ComposeResourceReader.kt @@ -0,0 +1,8 @@ +package fr.ajaury.gwenedeg.resourcereader.data + +import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader +import gwenedeg.shared.generated.resources.Res + +internal class ComposeResourceReader : ResourceReader { + override suspend fun read(resourcePath: String): String = Res.readBytes(resourcePath).decodeToString() +} diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/domain/ResourceReader.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/domain/ResourceReader.kt new file mode 100644 index 0000000..63d3fe6 --- /dev/null +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/resourcereader/domain/ResourceReader.kt @@ -0,0 +1,9 @@ +package fr.ajaury.gwenedeg.resourcereader.data.domain + +/** + * Reads the raw text of a subtitle file. Implemented by the host app, which owns the resource + * bundle, so this module stays free of any platform/resource dependency. + */ +interface ResourceReader { + suspend fun read(resourcePath: String): String +}