: FlowUseCase {
+abstract class FlowUseCase internal constructor(dispatcher: KatanaDispatcher) {
private val paramState = MutableSharedFlow(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
- override val flow: Flow = paramState.flatMapLatest {
+ val flow: Flow = paramState.flatMapLatest {
createFlow(it).distinctUntilChanged()
- }
+ }.flowOn(dispatcher.io)
protected abstract fun createFlow(params: P): Flow
- override operator fun invoke(params: P) {
+ operator fun invoke(params: P) {
paramState.tryEmit(params)
}
}
-abstract class FlowEitherUseCase : BaseFlowUseCase>()
-abstract class FlowOptionUseCase : BaseFlowUseCase>()
+operator fun FlowUseCase.invoke() {
+ invoke(Unit)
+}
+
+abstract class FlowEitherUseCase(dispatcher: KatanaDispatcher) :
+ FlowUseCase>(dispatcher)
+
+abstract class FlowOptionUseCase(dispatcher: KatanaDispatcher) :
+ FlowUseCase>(dispatcher)
diff --git a/core/domain/src/commonMain/kotlin/dev/alvr/katana/core/domain/usecases/UseCase.kt b/core/domain/src/commonMain/kotlin/dev/alvr/katana/core/domain/usecases/UseCase.kt
index 746079f15..3ba66fb1d 100644
--- a/core/domain/src/commonMain/kotlin/dev/alvr/katana/core/domain/usecases/UseCase.kt
+++ b/core/domain/src/commonMain/kotlin/dev/alvr/katana/core/domain/usecases/UseCase.kt
@@ -2,17 +2,29 @@ package dev.alvr.katana.core.domain.usecases
import arrow.core.Either
import arrow.core.Option
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.failures.Failure
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+abstract class UseCase internal constructor(private val dispatcher: KatanaDispatcher) {
+
+ abstract suspend fun run(params: P): R
+
+ suspend operator fun invoke(params: P): R = withContext(dispatcher.io) {
+ run(params)
+ }
-sealed interface UseCase : suspend (P) -> R {
fun sync(params: P): R = runBlocking {
invoke(params)
}
}
-interface EitherUseCase : UseCase>
-interface OptionUseCase : UseCase>
+abstract class EitherUseCase(dispatcher: KatanaDispatcher) :
+ UseCase>(dispatcher)
+
+abstract class OptionUseCase(dispatcher: KatanaDispatcher) :
+ UseCase>(dispatcher)
-suspend operator fun UseCase.invoke(): R = this(Unit)
+suspend operator fun UseCase.invoke(): R = invoke(Unit)
fun UseCase.sync(): R = sync(Unit)
diff --git a/core/preferences/build.gradle.kts b/core/preferences/build.gradle.kts
index 50540e6b1..55240e6f6 100644
--- a/core/preferences/build.gradle.kts
+++ b/core/preferences/build.gradle.kts
@@ -1,6 +1,5 @@
plugins {
id("katana.multiplatform.data.preferences")
- alias(libs.plugins.serialization)
}
katanaMultiplatform {
diff --git a/core/preferences/src/commonMain/kotlin/dev/alvr/katana/core/preferences/di/module.kt b/core/preferences/src/commonMain/kotlin/dev/alvr/katana/core/preferences/di/module.kt
index 78549b58e..80b24e9af 100644
--- a/core/preferences/src/commonMain/kotlin/dev/alvr/katana/core/preferences/di/module.kt
+++ b/core/preferences/src/commonMain/kotlin/dev/alvr/katana/core/preferences/di/module.kt
@@ -5,6 +5,6 @@ import org.koin.dsl.module
internal expect fun encryptionModule(): Module
-val dataPreferencesBaseModule = module {
+val corePreferencesModule = module {
includes(encryptionModule())
}
diff --git a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt
index 87c939a0d..2c008ee03 100644
--- a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt
+++ b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt
@@ -112,7 +112,7 @@ private val apolloInterceptorsModule = module {
factoryOf(::ReloadInterceptor).bind()
}
-val dataRemoteBaseModule = module {
+val coreRemoteModule = module {
includes(apolloClientModule, apolloDatabaseModule, apolloInterceptorsModule)
}
diff --git a/core/tests/build.gradle.kts b/core/tests/build.gradle.kts
index 9e3e63910..7252b5033 100644
--- a/core/tests/build.gradle.kts
+++ b/core/tests/build.gradle.kts
@@ -4,6 +4,8 @@ plugins {
katanaMultiplatform {
commonMainDependencies {
+ implementation(projects.core.common)
+
implementation(libs.arrow)
implementation(libs.koin)
implementation(libs.koin.test.get().toString()) { exclude(group = "junit", module = "junit") }
diff --git a/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/KoinExtension.kt b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/KoinExtension.kt
index 0de3efdc2..ec277078e 100644
--- a/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/KoinExtension.kt
+++ b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/KoinExtension.kt
@@ -11,18 +11,17 @@ import org.koin.core.module.Module
import org.koin.test.mock.MockProvider
import org.koin.test.mock.Provider
-class KoinExtension(
+fun koinExtension(
+ vararg modules: Module,
+ mockProvider: Provider<*>? = null,
+ mode: KoinLifecycleMode = KoinLifecycleMode.Test,
+): TestCaseExtension = KoinExtensionImpl(modules.toList(), mockProvider, mode)
+
+private class KoinExtensionImpl(
private val modules: List,
private val mockProvider: Provider<*>? = null,
private val mode: KoinLifecycleMode = KoinLifecycleMode.Test,
) : TestCaseExtension {
-
- constructor(
- module: Module,
- mockProvider: Provider<*>? = null,
- mode: KoinLifecycleMode = KoinLifecycleMode.Test,
- ) : this(listOf(module), mockProvider, mode)
-
@Suppress("TooGenericExceptionCaught")
override suspend fun intercept(testCase: TestCase, execute: suspend (TestCase) -> TestResult): TestResult {
return if (testCase.isApplicable()) {
@@ -46,8 +45,8 @@ class KoinExtension(
private fun TestCase.isApplicable() =
mode == KoinLifecycleMode.Root && isRootTest() ||
mode == KoinLifecycleMode.Test && type == TestType.Test
+}
- enum class KoinLifecycleMode {
- Root, Test
- }
+enum class KoinLifecycleMode {
+ Root, Test
}
diff --git a/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/coroutines/TestKatanaDispatcher.kt b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/coroutines/TestKatanaDispatcher.kt
new file mode 100644
index 000000000..185e6f40c
--- /dev/null
+++ b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/coroutines/TestKatanaDispatcher.kt
@@ -0,0 +1,13 @@
+package dev.alvr.katana.core.tests.coroutines
+
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class TestKatanaDispatcher : KatanaDispatcher {
+ override val main = StandardTestDispatcher(name = "main")
+ override val io = UnconfinedTestDispatcher(name = "io")
+ override val default = UnconfinedTestDispatcher(name = "default")
+}
diff --git a/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/di/module.kt b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/di/module.kt
new file mode 100644
index 000000000..16a9933bf
--- /dev/null
+++ b/core/tests/src/commonMain/kotlin/dev/alvr/katana/core/tests/di/module.kt
@@ -0,0 +1,15 @@
+package dev.alvr.katana.core.tests.di
+
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
+import dev.alvr.katana.core.tests.coroutines.TestKatanaDispatcher
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+private val dispatcherModule = module {
+ singleOf(::TestKatanaDispatcher) bind KatanaDispatcher::class
+}
+
+val coreTestsModule = module {
+ includes(dispatcherModule)
+}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index f2f517f06..b607bdbd5 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -6,6 +6,7 @@ katanaMultiplatform {
commonMainDependencies {
implementation(projects.core.common)
implementation(libs.compose.placeholder)
+ implementation(libs.compose.windowsize)
implementation(libs.materialkolor)
}
diff --git a/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBar.kt b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBar.kt
new file mode 100644
index 000000000..43a900a15
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBar.kt
@@ -0,0 +1,107 @@
+package dev.alvr.katana.core.ui.components.navigation
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import dev.alvr.katana.core.ui.utils.isLandscape
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun KatanaNavigationBar(
+ items: ImmutableList,
+ type: KatanaNavigationBarType,
+ isSelected: (T) -> Boolean,
+ onClick: (T) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isLandscape = isLandscape()
+
+ when {
+ type == KatanaNavigationBarType.Bottom && !isLandscape -> BottomNavigationBar(
+ items = items,
+ isSelected = isSelected,
+ onClick = onClick,
+ modifier = modifier,
+ )
+
+ type == KatanaNavigationBarType.Rail && isLandscape -> RailNavigationBar(
+ items = items,
+ isSelected = isSelected,
+ onClick = onClick,
+ modifier = modifier,
+ )
+ }
+}
+
+@Composable
+private fun BottomNavigationBar(
+ items: ImmutableList,
+ isSelected: (T) -> Boolean,
+ onClick: (T) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavigationBar(modifier = modifier) {
+ items.forEach { item ->
+ val selected = isSelected(item)
+
+ NavigationBarItem(
+ icon = { NavigationBarIcon(item, selected) },
+ label = { NavigationBarLabel(item) },
+ selected = selected,
+ onClick = { onClick(item) },
+ alwaysShowLabel = false,
+ )
+ }
+ }
+}
+
+@Composable
+private fun RailNavigationBar(
+ items: ImmutableList,
+ isSelected: (T) -> Boolean,
+ onClick: (T) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavigationRail(modifier = modifier) {
+ Spacer(Modifier.weight(1f))
+ items.forEach { item ->
+ val selected = isSelected(item)
+
+ NavigationRailItem(
+ icon = { NavigationBarIcon(item, selected) },
+ label = { NavigationBarLabel(item) },
+ selected = selected,
+ onClick = { onClick(item) },
+ alwaysShowLabel = false,
+ )
+ }
+ Spacer(Modifier.weight(1f))
+ }
+}
+
+@Composable
+private fun NavigationBarIcon(
+ destination: KatanaNavigationBarItem,
+ selected: Boolean,
+) {
+ Icon(
+ imageVector = if (selected) destination.selectedIcon else destination.unselectedIcon,
+ contentDescription = destination.label,
+ )
+}
+
+@Composable
+private fun NavigationBarLabel(destination: KatanaNavigationBarItem) {
+ Text(text = destination.label)
+}
+
+enum class KatanaNavigationBarType {
+ Bottom,
+ Rail,
+}
diff --git a/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBarItem.kt b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBarItem.kt
new file mode 100644
index 000000000..9a33b5788
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/components/navigation/KatanaNavigationBarItem.kt
@@ -0,0 +1,11 @@
+package dev.alvr.katana.core.ui.components.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+
+interface KatanaNavigationBarItem {
+ val key: Any
+ val selectedIcon: ImageVector
+ val unselectedIcon: ImageVector
+ @get:Composable val label: String
+}
diff --git a/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/utils/landscape.kt b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/utils/landscape.kt
new file mode 100644
index 000000000..b9592227b
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/utils/landscape.kt
@@ -0,0 +1,10 @@
+package dev.alvr.katana.core.ui.utils
+
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
+import androidx.compose.runtime.Composable
+
+@Composable
+@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+fun isLandscape() = calculateWindowSizeClass().widthSizeClass > WindowWidthSizeClass.Medium
diff --git a/features/account/data/src/commonMain/kotlin/dev/alvr/katana/features/account/data/di/module.kt b/features/account/data/src/commonMain/kotlin/dev/alvr/katana/features/account/data/di/module.kt
index 0c01ca702..29178fcc2 100644
--- a/features/account/data/src/commonMain/kotlin/dev/alvr/katana/features/account/data/di/module.kt
+++ b/features/account/data/src/commonMain/kotlin/dev/alvr/katana/features/account/data/di/module.kt
@@ -2,5 +2,5 @@ package dev.alvr.katana.features.account.data.di
import org.koin.dsl.module
-val dataRemoteAccountModule = module {
+val featuresAccountDataModule = module {
}
diff --git a/features/account/domain/src/commonMain/kotlin/dev/alvr/katana/features/account/domain/di/module.kt b/features/account/domain/src/commonMain/kotlin/dev/alvr/katana/features/account/domain/di/module.kt
index 497ac648b..9b2077616 100644
--- a/features/account/domain/src/commonMain/kotlin/dev/alvr/katana/features/account/domain/di/module.kt
+++ b/features/account/domain/src/commonMain/kotlin/dev/alvr/katana/features/account/domain/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.account.domain.di
import org.koin.dsl.module
-val domainAccountModule = module { }
+val featuresAccountDomainModule = module { }
diff --git a/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/di/module.kt b/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/di/module.kt
index d3490d2f6..c1a6ab861 100644
--- a/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/di/module.kt
+++ b/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/di/module.kt
@@ -8,6 +8,6 @@ private val viewModelsModule = module {
factoryOf(::AccountViewModel)
}
-val uiAccountModule = module {
+val featuresAccountUiModule = module {
includes(viewModelsModule)
}
diff --git a/features/explore/data/src/commonMain/kotlin/dev/alvr/katana/features/explore/data/di/module.kt b/features/explore/data/src/commonMain/kotlin/dev/alvr/katana/features/explore/data/di/module.kt
index 58a76ed65..e4b014c57 100644
--- a/features/explore/data/src/commonMain/kotlin/dev/alvr/katana/features/explore/data/di/module.kt
+++ b/features/explore/data/src/commonMain/kotlin/dev/alvr/katana/features/explore/data/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.explore.data.di
import org.koin.dsl.module
-val dataRemoteExploreModule = module { }
+val featuresExploreDataModule = module { }
diff --git a/features/explore/domain/src/commonMain/kotlin/dev/alvr/katana/features/explore/domain/di/module.kt b/features/explore/domain/src/commonMain/kotlin/dev/alvr/katana/features/explore/domain/di/module.kt
index a0c3fdb1a..593630225 100644
--- a/features/explore/domain/src/commonMain/kotlin/dev/alvr/katana/features/explore/domain/di/module.kt
+++ b/features/explore/domain/src/commonMain/kotlin/dev/alvr/katana/features/explore/domain/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.explore.domain.di
import org.koin.dsl.module
-val domainExploreModule = module { }
+val featuresExploreDomainModule = module { }
diff --git a/features/explore/ui/src/commonMain/kotlin/dev/alvr/katana/features/explore/ui/di/module.kt b/features/explore/ui/src/commonMain/kotlin/dev/alvr/katana/features/explore/ui/di/module.kt
index bdeba7b59..1f10f649c 100644
--- a/features/explore/ui/src/commonMain/kotlin/dev/alvr/katana/features/explore/ui/di/module.kt
+++ b/features/explore/ui/src/commonMain/kotlin/dev/alvr/katana/features/explore/ui/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.explore.ui.di
import org.koin.dsl.module
-val uiExploreModule = module { }
+val featuresExploreUiModule = module { }
diff --git a/features/home/ui/.gitignore b/features/home/ui/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/features/home/ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/home/ui/build.gradle.kts b/features/home/ui/build.gradle.kts
new file mode 100644
index 000000000..64835d7e2
--- /dev/null
+++ b/features/home/ui/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ id("katana.multiplatform.ui")
+}
+
+katanaMultiplatform {
+ commonMainDependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.ui)
+ }
+
+ commonTestDependencies {
+ implementation(projects.core.tests)
+ }
+}
diff --git a/features/home/ui/src/commonMain/kotlin/dev/alvr/katana/features/home/ui/di/module.kt b/features/home/ui/src/commonMain/kotlin/dev/alvr/katana/features/home/ui/di/module.kt
new file mode 100644
index 000000000..d3f2f0ee6
--- /dev/null
+++ b/features/home/ui/src/commonMain/kotlin/dev/alvr/katana/features/home/ui/di/module.kt
@@ -0,0 +1,6 @@
+package dev.alvr.katana.features.home.ui.di
+
+import org.koin.dsl.module
+
+val featuresHomeUiModule = module {
+}
diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt
index 39f852a80..d278696e0 100644
--- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt
+++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt
@@ -22,6 +22,6 @@ private val sourcesModule = module {
singleOf(::MangaListsRemoteSourceImpl).bind()
}
-val dataRemoteListsModule = module {
+val featuresListsDataModule = module {
includes(repositoriesModule, sourcesModule)
}
diff --git a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/di/module.kt b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/di/module.kt
index aef9d57c9..065df4c6d 100644
--- a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/di/module.kt
+++ b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/di/module.kt
@@ -12,6 +12,6 @@ private val useCasesModule = module {
factoryOf(::UpdateListUseCase)
}
-val domainListsModule = module {
+val featuresListsDomainModule = module {
includes(useCasesModule)
}
diff --git a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCase.kt b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCase.kt
index 4f7553543..86c36efd6 100644
--- a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCase.kt
+++ b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCase.kt
@@ -1,12 +1,14 @@
package dev.alvr.katana.features.lists.domain.usecases
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.usecases.FlowEitherUseCase
import dev.alvr.katana.features.lists.domain.models.MediaCollection
import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry
import dev.alvr.katana.features.lists.domain.repositories.ListsRepository
class ObserveAnimeListUseCase(
+ dispatcher: KatanaDispatcher,
private val repository: ListsRepository,
-) : FlowEitherUseCase>() {
+) : FlowEitherUseCase>(dispatcher) {
override fun createFlow(params: Unit) = repository.animeCollection
}
diff --git a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCase.kt b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCase.kt
index 47b9896b9..2a73549b5 100644
--- a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCase.kt
+++ b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCase.kt
@@ -1,12 +1,14 @@
package dev.alvr.katana.features.lists.domain.usecases
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.usecases.FlowEitherUseCase
import dev.alvr.katana.features.lists.domain.models.MediaCollection
import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry
import dev.alvr.katana.features.lists.domain.repositories.ListsRepository
class ObserveMangaListUseCase(
+ dispatcher: KatanaDispatcher,
private val repository: ListsRepository,
-) : FlowEitherUseCase>() {
+) : FlowEitherUseCase>(dispatcher) {
override fun createFlow(params: Unit) = repository.mangaCollection
}
diff --git a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCase.kt b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCase.kt
index 1b9e691af..8429d4ac2 100644
--- a/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCase.kt
+++ b/features/lists/domain/src/commonMain/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCase.kt
@@ -1,11 +1,13 @@
package dev.alvr.katana.features.lists.domain.usecases
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.usecases.EitherUseCase
import dev.alvr.katana.features.lists.domain.models.lists.MediaList
import dev.alvr.katana.features.lists.domain.repositories.ListsRepository
class UpdateListUseCase(
+ dispatcher: KatanaDispatcher,
private val repository: ListsRepository,
-) : EitherUseCase {
- override suspend fun invoke(entry: MediaList) = repository.updateList(entry)
+) : EitherUseCase(dispatcher) {
+ override suspend fun run(params: MediaList) = repository.updateList(params)
}
diff --git a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt
index f7829800c..6178838ea 100644
--- a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt
+++ b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt
@@ -3,7 +3,10 @@ package dev.alvr.katana.features.lists.domain.usecases
import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.usecases.invoke
+import dev.alvr.katana.core.tests.di.coreTestsModule
+import dev.alvr.katana.core.tests.koinExtension
import dev.alvr.katana.core.tests.shouldBeLeft
import dev.alvr.katana.core.tests.shouldBeRight
import dev.alvr.katana.features.lists.domain.failures.ListsFailure
@@ -15,13 +18,17 @@ import dev.mokkery.every
import dev.mokkery.mock
import dev.mokkery.verify
import io.kotest.core.spec.style.FreeSpec
+import io.kotest.core.test.TestCase
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.flowOf
+import org.koin.test.KoinTest
+import org.koin.test.inject
-internal class ObserveAnimeListUseCaseTest : FreeSpec() {
+internal class ObserveAnimeListUseCaseTest : FreeSpec(), KoinTest {
+ private val dispatcher by inject()
private val repo = mock()
- private val useCase = ObserveAnimeListUseCase(repo)
+ private lateinit var useCase: ObserveAnimeListUseCase
init {
"successfully observe the anime lists" {
@@ -54,4 +61,10 @@ internal class ObserveAnimeListUseCaseTest : FreeSpec() {
verify { repo.animeCollection }
}
}
+
+ override suspend fun beforeEach(testCase: TestCase) {
+ useCase = ObserveAnimeListUseCase(dispatcher, repo)
+ }
+
+ override fun extensions() = listOf(koinExtension(coreTestsModule))
}
diff --git a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt
index 7faf8c9ca..373b97155 100644
--- a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt
+++ b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt
@@ -3,7 +3,10 @@ package dev.alvr.katana.features.lists.domain.usecases
import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.usecases.invoke
+import dev.alvr.katana.core.tests.di.coreTestsModule
+import dev.alvr.katana.core.tests.koinExtension
import dev.alvr.katana.core.tests.shouldBeLeft
import dev.alvr.katana.core.tests.shouldBeRight
import dev.alvr.katana.features.lists.domain.failures.ListsFailure
@@ -15,13 +18,17 @@ import dev.mokkery.every
import dev.mokkery.mock
import dev.mokkery.verify
import io.kotest.core.spec.style.FreeSpec
+import io.kotest.core.test.TestCase
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.flowOf
+import org.koin.test.KoinTest
+import org.koin.test.inject
-internal class ObserveMangaListUseCaseTest : FreeSpec() {
+internal class ObserveMangaListUseCaseTest : FreeSpec(), KoinTest {
+ private val dispatcher by inject()
private val repo = mock()
- private val useCase = ObserveMangaListUseCase(repo)
+ private lateinit var useCase: ObserveMangaListUseCase
init {
"successfully observe the manga lists" {
@@ -54,4 +61,10 @@ internal class ObserveMangaListUseCaseTest : FreeSpec() {
verify { repo.mangaCollection }
}
}
+
+ override suspend fun beforeEach(testCase: TestCase) {
+ useCase = ObserveMangaListUseCase(dispatcher, repo)
+ }
+
+ override fun extensions() = listOf(koinExtension(coreTestsModule))
}
diff --git a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCaseTest.kt b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCaseTest.kt
index a8e063d2c..219a3b8dc 100644
--- a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCaseTest.kt
+++ b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/UpdateListUseCaseTest.kt
@@ -2,7 +2,10 @@ package dev.alvr.katana.features.lists.domain.usecases
import arrow.core.left
import arrow.core.right
+import dev.alvr.katana.core.common.coroutines.KatanaDispatcher
import dev.alvr.katana.core.domain.failures.Failure
+import dev.alvr.katana.core.tests.di.coreTestsModule
+import dev.alvr.katana.core.tests.koinExtension
import dev.alvr.katana.core.tests.shouldBeLeft
import dev.alvr.katana.core.tests.shouldBeRight
import dev.alvr.katana.features.lists.domain.failures.ListsFailure
@@ -14,11 +17,15 @@ import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verifySuspend
import io.kotest.core.spec.style.FreeSpec
+import io.kotest.core.test.TestCase
+import org.koin.test.KoinTest
+import org.koin.test.inject
-internal class UpdateListUseCaseTest : FreeSpec() {
+internal class UpdateListUseCaseTest : FreeSpec(), KoinTest {
+ private val dispatcher by inject()
private val repo = mock()
- private val useCase = UpdateListUseCase(repo)
+ private lateinit var useCase: UpdateListUseCase
init {
"successfully updating the list" {
@@ -38,4 +45,10 @@ internal class UpdateListUseCaseTest : FreeSpec() {
}
}
}
+
+ override suspend fun beforeEach(testCase: TestCase) {
+ useCase = UpdateListUseCase(dispatcher, repo)
+ }
+
+ override fun extensions() = listOf(koinExtension(coreTestsModule))
}
diff --git a/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/di/module.kt b/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/di/module.kt
index 73eb5b39e..5b5f29941 100644
--- a/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/di/module.kt
+++ b/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/di/module.kt
@@ -10,6 +10,6 @@ private val viewModelsModule = module {
factoryOf(::MangaListsViewModel)
}
-val uiListsModule = module {
+val featuresListsUiModule = module {
includes(viewModelsModule)
}
diff --git a/features/login/ui/src/commonMain/kotlin/dev/alvr/katana/features/login/ui/di/module.kt b/features/login/ui/src/commonMain/kotlin/dev/alvr/katana/features/login/ui/di/module.kt
index 0abadb30f..162e4b3a1 100644
--- a/features/login/ui/src/commonMain/kotlin/dev/alvr/katana/features/login/ui/di/module.kt
+++ b/features/login/ui/src/commonMain/kotlin/dev/alvr/katana/features/login/ui/di/module.kt
@@ -7,6 +7,6 @@ private val viewModelsModule = module {
factory { params -> LoginViewModel(params[0], get(), get()) }
}
-val uiLoginModule = module {
+val featuresLoginUiModule = module {
includes(viewModelsModule)
}
diff --git a/features/social/data/src/commonMain/kotlin/dev/alvr/katana/features/social/data/di/module.kt b/features/social/data/src/commonMain/kotlin/dev/alvr/katana/features/social/data/di/module.kt
index 2469068d0..a5316df7d 100644
--- a/features/social/data/src/commonMain/kotlin/dev/alvr/katana/features/social/data/di/module.kt
+++ b/features/social/data/src/commonMain/kotlin/dev/alvr/katana/features/social/data/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.social.data.di
import org.koin.dsl.module
-val dataRemoteSocialModule = module { }
+val featuresSocialDataModule = module { }
diff --git a/features/social/domain/src/commonMain/kotlin/dev/alvr/katana/features/social/domain/di/module.kt b/features/social/domain/src/commonMain/kotlin/dev/alvr/katana/features/social/domain/di/module.kt
index 92c0cd776..c64b6a709 100644
--- a/features/social/domain/src/commonMain/kotlin/dev/alvr/katana/features/social/domain/di/module.kt
+++ b/features/social/domain/src/commonMain/kotlin/dev/alvr/katana/features/social/domain/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.social.domain.di
import org.koin.dsl.module
-val domainSocialModule = module { }
+val featuresSocialDomainModule = module { }
diff --git a/features/social/ui/src/commonMain/kotlin/dev/alvr/katana/features/social/ui/di/module.kt b/features/social/ui/src/commonMain/kotlin/dev/alvr/katana/features/social/ui/di/module.kt
index b7df4a83c..800774888 100644
--- a/features/social/ui/src/commonMain/kotlin/dev/alvr/katana/features/social/ui/di/module.kt
+++ b/features/social/ui/src/commonMain/kotlin/dev/alvr/katana/features/social/ui/di/module.kt
@@ -2,4 +2,4 @@ package dev.alvr.katana.features.social.ui.di
import org.koin.dsl.module
-val uiSocialModule = module { }
+val featuresSocialUiModule = module { }
diff --git a/gradle.properties b/gradle.properties
index 68e2bcca0..7ae50ebdd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,7 @@
# Katana
katana.enableComposeCompilerMetrics=true
katana.enableComposeCompilerReports=true
+katana.strongSkipping=true
# Android
android.enableJetifier=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 469dcfc88..7d5cfe2ec 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,12 +1,16 @@
[versions]
-activity = "1.8.2"
android = "8.3.0"
+androidx-activity = "1.8.2"
+androidx-lifecycle = "2.7.0"
+androidx-splashscreen = "1.0.1"
apollo = "3.8.2"
arrow = "1.2.3"
buildconfig = "5.3.5"
complete-kotlin = "1.1.0"
compose = "1.6.0"
compose-compiler = "1.5.10.1"
+compose-placeholder = "1.0.7"
+compose-windowsize = "0.5.0"
datastore = "1.1.0-beta02"
destinations = "1.10.2"
detekt = "1.23.5"
@@ -27,8 +31,7 @@ kotlinx-serialization = "1.6.3"
kover = "0.7.6"
ksp = "1.9.23-1.0.19"
ktor = "2.3.9"
-lifecycle = "2.7.0"
-lyricist = "1.6.2-1.8.20"
+lyricist = "1.6.2"
materialkolor = "1.4.3"
mockk = "1.13.10"
mokkery = "1.9.23-1.5.0"
@@ -36,14 +39,11 @@ okio = "3.9.0"
orbit = "6.1.1"
parcelable = "1.3.0"
parcelize = "0.2.4"
-placeholder = "1.0.7"
sentry = "7.6.0"
sentry-multiplatform = "0.5.0"
sentry-plugin = "4.3.1"
-splashscreen = "1.0.1"
tink = "1.12.0"
turbine = "1.1.0"
-window-size = "0.5.0"
yamlbeans = "1.17"
[plugins]
@@ -65,8 +65,10 @@ serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref
[libraries]
# AndroidX
-androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "activity" }
-androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" }
+androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
+androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
# Apollo
apollo = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollo" }
@@ -79,8 +81,8 @@ arrow = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
# Compose
compose-compiler = { module = "org.jetbrains.compose.compiler:compiler", version.ref = "compose-compiler" }
-compose-material3-windowsize = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "window-size" }
-compose-placeholder = { module = "com.eygraber:compose-placeholder-material3", version.ref = "placeholder" }
+compose-placeholder = { module = "com.eygraber:compose-placeholder-material3", version.ref = "compose-placeholder" }
+compose-windowsize = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "compose-windowsize" }
# Datastore
datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
@@ -139,10 +141,6 @@ kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
ktor-android = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-ios = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
-# Lifecycle
-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
-
# Lyricist
lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
@@ -318,8 +316,8 @@ ui-common = [
ui-android = [
"destinations",
"koin-compose-android",
- "lifecycle-runtime",
- "lifecycle-viewmodel",
+ "androidx-lifecycle-runtime",
+ "androidx-lifecycle-viewmodel",
]
ui-ios = []
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 79c5a92a3..9ee35e3e9 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -23,6 +23,8 @@ katanaMultiplatform {
api(projects.features.explore.domain)
api(projects.features.explore.ui)
+ api(projects.features.home.ui)
+
api(projects.features.lists.data)
api(projects.features.lists.domain)
api(projects.features.lists.ui)
@@ -33,8 +35,7 @@ katanaMultiplatform {
api(projects.features.social.domain)
api(projects.features.social.ui)
- api(libs.compose.material3.windowsize)
- api(libs.sentry.multiplatform)
+ implementation(libs.sentry.multiplatform)
}
androidMainDependencies {
diff --git a/shared/src/androidMain/kotlin/dev/alvr/katana/shared/KatanaApp.android.kt b/shared/src/androidMain/kotlin/dev/alvr/katana/shared/KatanaApp.android.kt
index 588aa1533..9ab833669 100644
--- a/shared/src/androidMain/kotlin/dev/alvr/katana/shared/KatanaApp.android.kt
+++ b/shared/src/androidMain/kotlin/dev/alvr/katana/shared/KatanaApp.android.kt
@@ -1,17 +1,14 @@
package dev.alvr.katana.shared
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
-import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
+import dev.alvr.katana.core.ui.utils.isLandscape
import dev.alvr.katana.shared.navigation.KatanaDestinations
import org.koin.androidx.compose.koinViewModel
@Composable
-@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
internal actual fun KatanaContent() {
KatanaDestinations(
- useNavRail = calculateWindowSizeClass().widthSizeClass > WindowWidthSizeClass.Medium,
+ useNavRail = isLandscape(),
vm = koinViewModel(),
)
}
diff --git a/shared/src/commonMain/kotlin/dev/alvr/katana/shared/di/module.kt b/shared/src/commonMain/kotlin/dev/alvr/katana/shared/di/module.kt
index f72ee382d..060fe3fa5 100644
--- a/shared/src/commonMain/kotlin/dev/alvr/katana/shared/di/module.kt
+++ b/shared/src/commonMain/kotlin/dev/alvr/katana/shared/di/module.kt
@@ -1,60 +1,76 @@
package dev.alvr.katana.shared.di
-import dev.alvr.katana.common.session.data.di.dataPreferencesSessionModule
-import dev.alvr.katana.common.session.domain.di.domainSessionModule
-import dev.alvr.katana.common.user.data.di.dataRemoteUserModule
-import dev.alvr.katana.common.user.domain.di.domainUserModule
-import dev.alvr.katana.core.preferences.di.dataPreferencesBaseModule
-import dev.alvr.katana.core.remote.di.dataRemoteBaseModule
-import dev.alvr.katana.features.account.data.di.dataRemoteAccountModule
-import dev.alvr.katana.features.account.domain.di.domainAccountModule
-import dev.alvr.katana.features.account.ui.di.uiAccountModule
-import dev.alvr.katana.features.explore.data.di.dataRemoteExploreModule
-import dev.alvr.katana.features.explore.domain.di.domainExploreModule
-import dev.alvr.katana.features.explore.ui.di.uiExploreModule
-import dev.alvr.katana.features.lists.data.di.dataRemoteListsModule
-import dev.alvr.katana.features.lists.domain.di.domainListsModule
-import dev.alvr.katana.features.lists.ui.di.uiListsModule
-import dev.alvr.katana.features.login.ui.di.uiLoginModule
-import dev.alvr.katana.features.social.data.di.dataRemoteSocialModule
-import dev.alvr.katana.features.social.domain.di.domainSocialModule
-import dev.alvr.katana.features.social.ui.di.uiSocialModule
+import dev.alvr.katana.common.session.data.di.commonSessionDataModule
+import dev.alvr.katana.common.session.domain.di.commonSessionDomainModule
+import dev.alvr.katana.common.user.data.di.commonUserDataModule
+import dev.alvr.katana.common.user.domain.di.commonUserDomainModule
+import dev.alvr.katana.core.common.di.coreCommonModule
+import dev.alvr.katana.core.preferences.di.corePreferencesModule
+import dev.alvr.katana.core.remote.di.coreRemoteModule
+import dev.alvr.katana.features.account.data.di.featuresAccountDataModule
+import dev.alvr.katana.features.account.domain.di.featuresAccountDomainModule
+import dev.alvr.katana.features.account.ui.di.featuresAccountUiModule
+import dev.alvr.katana.features.explore.data.di.featuresExploreDataModule
+import dev.alvr.katana.features.explore.domain.di.featuresExploreDomainModule
+import dev.alvr.katana.features.explore.ui.di.featuresExploreUiModule
+import dev.alvr.katana.features.home.ui.di.featuresHomeUiModule
+import dev.alvr.katana.features.lists.data.di.featuresListsDataModule
+import dev.alvr.katana.features.lists.domain.di.featuresListsDomainModule
+import dev.alvr.katana.features.lists.ui.di.featuresListsUiModule
+import dev.alvr.katana.features.login.ui.di.featuresLoginUiModule
+import dev.alvr.katana.features.social.data.di.featuresSocialDataModule
+import dev.alvr.katana.features.social.domain.di.featuresSocialDomainModule
+import dev.alvr.katana.features.social.ui.di.featuresSocialUiModule
import dev.alvr.katana.shared.viewmodel.MainViewModel
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
-private val uiMainModule = module {
+private val sharedModule = module {
factoryOf(::MainViewModel)
}
val katanaModule = module {
includes(
- // Domain
- domainAccountModule,
- domainExploreModule,
- domainListsModule,
- domainSessionModule,
- domainSocialModule,
- domainUserModule,
-
- // Data Preferences
- dataPreferencesBaseModule,
- dataPreferencesSessionModule,
-
- // Data Remote
- dataRemoteAccountModule,
- dataRemoteExploreModule,
- dataRemoteBaseModule,
- dataRemoteListsModule,
- dataRemoteSocialModule,
- dataRemoteUserModule,
-
- // Ui
- uiAccountModule,
- uiExploreModule,
- uiListsModule,
- uiLoginModule,
- uiMainModule,
- uiSocialModule,
+ // Core
+ coreCommonModule,
+ corePreferencesModule,
+ coreRemoteModule,
+
+ // Common Session
+ commonSessionDataModule,
+ commonSessionDomainModule,
+
+ // Common User
+ commonUserDataModule,
+ commonUserDomainModule,
+
+ // Feature Account
+ featuresAccountDataModule,
+ featuresAccountDomainModule,
+ featuresAccountUiModule,
+
+ // Feature Explore
+ featuresExploreDataModule,
+ featuresExploreDomainModule,
+ featuresExploreUiModule,
+
+ // Feature Home
+ featuresHomeUiModule,
+
+ // Feature Lists
+ featuresListsDataModule,
+ featuresListsDomainModule,
+ featuresListsUiModule,
+
+ // Feature Login
+ featuresLoginUiModule,
+
+ // Feature Social
+ featuresSocialDataModule,
+ featuresSocialDomainModule,
+ featuresSocialUiModule,
+
+ // Shared
+ sharedModule,
)
}