diff --git a/android/build.gradle.kts b/android/build.gradle.kts index c738d302..1a89c8d4 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -72,7 +72,7 @@ dependencies { implementation(project(":common:data-models")) implementation(Koin.android) - implementation(Koin.androidViewModel) + implementation(Koin.compose) //DECOMPOSE implementation(Decompose.decompose) diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index b31a41fd..d3aa7cf6 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -48,6 +48,7 @@ import com.tonyodev.fetch2.Status import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koin.android.ext.android.inject +import com.shabinder.common.uikit.showPopUpMessage as uikitShowPopUpMessage const val disableDozeCode = 1223 @@ -117,6 +118,7 @@ class MainActivity : ComponentActivity(), PaymentResultListener { override val database = this@MainActivity.database override val fetchPlatformQueryResult = this@MainActivity.fetcher override val directories: Dir = this@MainActivity.dir + override val showPopUpMessage: (String) -> Unit = ::uikitShowPopUpMessage override val downloadProgressReport: MutableSharedFlow> = trackStatusFlow } ) @@ -234,7 +236,7 @@ class MainActivity : ComponentActivity(), PaymentResultListener { override fun onPaymentError(errorCode: Int, response: String?) { try{ - showPopUpMessage("Payment Failed, Response:$response") + uikitShowPopUpMessage("Payment Failed, Response:$response") }catch (e: Exception){ Log.d("Razorpay Payment","Exception in onPaymentSuccess $response") } @@ -242,9 +244,9 @@ class MainActivity : ComponentActivity(), PaymentResultListener { override fun onPaymentSuccess(razorpayPaymentId: String?) { try{ - showPopUpMessage("Payment Successful, ThankYou!") + uikitShowPopUpMessage("Payment Successful, ThankYou!") }catch (e: Exception){ - showPopUpMessage("Razorpay Payment, Error Occurred.") + uikitShowPopUpMessage("Razorpay Payment, Error Occurred.") Log.d("Razorpay Payment","Exception in onPaymentSuccess, ${e.message}") } } diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 2e7fece5..1dbae366 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -9,13 +9,13 @@ object Versions { const val coilVersion = "0.4.1" //DI - const val koin = "3.0.0-alpha-4" + const val koin = "3.0.1-beta-1" //Logger const val kermit = "0.1.8" //Internet - const val ktor = "1.5.1" + const val ktor = "1.5.2" const val kotlinxSerialization = "1.1.0-RC" //Database @@ -32,10 +32,10 @@ object Versions { const val androidLifecycle = "2.3.0" } object Koin { - val core = "org.koin:koin-core:${Versions.koin}" - val test = "org.koin:koin-test:${Versions.koin}" - val android = "org.koin:koin-android:${Versions.koin}" - val androidViewModel = "org.koin:koin-androidx-viewmodel:${Versions.koin}" + val core = "io.insert-koin:koin-core:${Versions.koin}" + val test = "io.insert-koin:koin-test:${Versions.koin}" + val android = "io.insert-koin:koin-android:${Versions.koin}" + val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}" } object Androidx{ const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha02" diff --git a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts index 0d7d02c7..5ef5ddea 100644 --- a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts @@ -13,6 +13,7 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3") } } diff --git a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts index 7d71551c..16f050e5 100644 --- a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts @@ -2,9 +2,11 @@ import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.compose import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.kotlin import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.sourceSets import org.gradle.kotlin.dsl.withType +import org.jetbrains.compose.compose plugins { - id("com.android.library") +// id("com.android.library") + id("android-setup") id("kotlin-multiplatform") id("org.jetbrains.compose") } @@ -19,9 +21,7 @@ kotlin { sourceSets { named("commonMain") { dependencies { - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3") } } @@ -29,12 +29,22 @@ kotlin { dependencies { implementation("androidx.appcompat:appcompat:1.2.0") implementation(Androidx.core) + implementation(compose.runtime) + implementation(compose.material) + implementation(compose.foundation) + implementation(compose.materialIconsExtended) + implementation(Decompose.extensionsCompose) } } named("desktopMain") { dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) implementation(compose.desktop.common) + implementation(compose.materialIconsExtended) + implementation(Decompose.extensionsCompose) } } named("jsMain") { diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt index e1bf1338..04b5cbac 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import com.shabinder.common.database.R +import com.shabinder.common.database.appContext import com.shabinder.common.di.Picture import com.shabinder.common.di.dispatcherIO import kotlinx.coroutines.withContext @@ -102,4 +103,16 @@ actual fun YoutubeMusicLogo() = vectorResource(R.drawable.ic_youtube_music_logo) actual fun GithubLogo() = vectorResource(R.drawable.ic_github) @Composable -fun vectorResource(@DrawableRes id: Int) = ImageVector.Companion.vectorResource(id) \ No newline at end of file +fun vectorResource(@DrawableRes id: Int) = ImageVector.Companion.vectorResource(id) + +@Composable +actual fun Toast( + text: String, + visibility: MutableState, + duration: ToastDuration +){ + //We Have Android's Implementation of Toast so its just Empty +} +actual fun showPopUpMessage(text: String){ + android.widget.Toast.makeText(appContext,text, android.widget.Toast.LENGTH_SHORT).show() +} \ No newline at end of file diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt index c4ce7164..8a08c2b6 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt @@ -85,7 +85,6 @@ fun MainScreen(modifier: Modifier = Modifier, topPadding: Dp = 0.dp,statusBarHei Spacer(Modifier.padding(top = topPadding)) Children( routerState = component.routerState, - //TODO animation = crossfade() ) { child, _ -> when (child) { is Child.Main -> SpotiFlyerMainContent(component = child.component) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Toast.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Toast.kt similarity index 82% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Toast.kt rename to common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Toast.kt index 7dab481e..0d3c3dc4 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Toast.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Toast.kt @@ -1,4 +1,4 @@ -package com.shabinder.common.di +package com.shabinder.common.uikit import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -8,6 +8,8 @@ enum class ToastDuration(val value: Int) { Short(1000), Long(3000) } +expect fun showPopUpMessage(text: String) + @Composable expect fun Toast( text: String, diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopToast.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopToast.kt similarity index 95% rename from common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopToast.kt rename to common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopToast.kt index cf7d634b..e4ad188d 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopToast.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopToast.kt @@ -1,4 +1,4 @@ -package com.shabinder.common.di +package com.shabinder.common.uikit import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -15,13 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -actual val dispatcherIO = Dispatchers.IO - private val message: MutableState = mutableStateOf("") private val state: MutableState = mutableStateOf(false) diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts index 19f8892c..54c39f78 100644 --- a/common/data-models/build.gradle.kts +++ b/common/data-models/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("multiplatform-compose-setup") + id("multiplatform-setup") id("android-setup") id("kotlin-parcelize") kotlin("plugin.serialization") @@ -10,7 +10,7 @@ kotlin { commonMain { dependencies { api("dev.icerock.moko:parcelize:0.6.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") } } } diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index f739c658..a336d9e8 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -10,15 +10,14 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(compose.materialIconsExtended) - api(project(":common:data-models")) - api(project(":common:database")) + implementation(project(":common:data-models")) + implementation(project(":common:database")) implementation(project(":fuzzywuzzy:app")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1") implementation(Ktor.clientCore) - implementation(Ktor.clientCio) + //implementation(Ktor.clientCio) implementation(Ktor.clientSerialization) implementation(Ktor.clientLogging) implementation(Ktor.clientJson) @@ -34,6 +33,7 @@ kotlin { } androidMain { dependencies{ + implementation(compose.materialIconsExtended) implementation(Ktor.clientAndroid) implementation(Extras.Android.fetch) implementation(Koin.android) @@ -43,12 +43,14 @@ kotlin { } desktopMain { dependencies{ + implementation(compose.materialIconsExtended) implementation(Ktor.clientApache) implementation(Ktor.slf4j) } } jsMain { dependencies { + implementation(Ktor.clientJs) implementation(project(":common:data-models")) } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt index 3ad0c174..a086de76 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt @@ -34,21 +34,9 @@ actual fun openPlatform(packageID:String, platformLink:String){ } actual val dispatcherIO = Dispatchers.IO -@Composable -actual fun Toast( - text: String, - visibility: MutableState, - duration: ToastDuration -){ - //We Have Android's Implementation of Toast so its just Empty -} - actual val isInternetAvailable:Boolean get() = internetAvailability.value ?: true -actual fun showPopUpMessage(text: String){ - android.widget.Toast.makeText(appContext,text, android.widget.Toast.LENGTH_SHORT).show() -} actual fun shareApp(){ val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index d7754bb2..a1a247e7 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -11,10 +11,6 @@ import io.ktor.client.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* import io.ktor.client.features.logging.* -import io.ktor.client.request.* -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt index 4679adae..79ae1be1 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt @@ -9,8 +9,6 @@ expect fun shareApp() expect fun giveDonation() -expect fun showPopUpMessage(text: String) - expect val dispatcherIO: CoroutineDispatcher expect val isInternetAvailable:Boolean diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index b6b64b0d..756dec09 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -51,7 +51,7 @@ class SpotifyProvider( override suspend fun authenticateSpotify(): HttpClient?{ val token = tokenStore.getToken() return if(token == null) { - showPopUpMessage("Please Check your Network Connection") + logger.d{ "Please Check your Network Connection" } null } else{ diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index 0b8f1f8d..06c209cf 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -12,6 +12,7 @@ import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails import io.ktor.client.request.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect @@ -22,6 +23,8 @@ actual fun openPlatform(packageID:String, platformLink:String){ //TODO } +actual val dispatcherIO = Dispatchers.IO + actual fun shareApp(){ //TODO } diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt index cced663e..c8236534 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt @@ -19,7 +19,6 @@ actual fun giveDonation(){ } actual fun queryActiveTracks(){} -actual fun showPopUpMessage(text:String){} actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default @@ -40,6 +39,7 @@ private suspend fun isInternetAvailable(): Boolean { actual val isInternetAvailable:Boolean get(){ + return true var result = false val job = GlobalScope.launch { result = isInternetAvailable() } while(job.isActive){} @@ -52,11 +52,11 @@ actual suspend fun downloadTracks( list: List, getYTIDBestMatch:suspend (String, TrackDetails)->String?, saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit -){ +){/* list.forEach { if (!it.videoID.isNullOrBlank()) {//Video ID already known! } else { } - } + }*/ } diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt new file mode 100644 index 00000000..1125425b --- /dev/null +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt @@ -0,0 +1,17 @@ +package com.shabinder.common.di + +import co.touchlab.kermit.Kermit +import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.database.Database +import io.ktor.client.* + +actual class YoutubeProvider actual constructor( + httpClient: HttpClient, + database: Database, + logger: Kermit, + dir: Dir +) { + actual suspend fun query(fullLink: String): PlatformQueryResult? { + return null // TODO + } +} \ No newline at end of file diff --git a/common/list/build.gradle.kts b/common/list/build.gradle.kts index 6a190f51..cdbfea39 100644 --- a/common/list/build.gradle.kts +++ b/common/list/build.gradle.kts @@ -15,7 +15,6 @@ kotlin { implementation(MVIKotlin.coroutines) implementation(MVIKotlin.mvikotlin) implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) } } } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index 0973728f..114ab8ab 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -48,6 +48,7 @@ interface SpotiFlyerList { val dir: Dir val link: String val listOutput: Consumer + val showPopUpMessage:(String)->Unit val downloadProgressFlow: MutableSharedFlow> } sealed class Output { diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 3acf73e3..cbc3d64e 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -1,6 +1,5 @@ package com.shabinder.common.list.integration -import androidx.compose.ui.graphics.ImageBitmap import com.arkivanov.decompose.ComponentContext import com.arkivanov.mvikotlin.extensions.coroutines.states import com.shabinder.common.di.Picture @@ -24,8 +23,9 @@ internal class SpotiFlyerListImpl( dir = this.dir, storeFactory = storeFactory, fetchQuery = fetchQuery, + downloadProgressFlow = downloadProgressFlow, link = link, - downloadProgressFlow = downloadProgressFlow + showPopUpMessage = showPopUpMessage ).provide() } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index e56fe0b4..5cdd4b3a 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -17,6 +17,7 @@ internal class SpotiFlyerListStoreProvider( private val storeFactory: StoreFactory, private val fetchQuery: FetchPlatformQueryResult, private val link: String, + private val showPopUpMessage: (String) -> Unit, private val downloadProgressFlow: MutableSharedFlow> ) { val logger = getLogger() diff --git a/common/main/build.gradle.kts b/common/main/build.gradle.kts index 1a4961ad..69106fa4 100644 --- a/common/main/build.gradle.kts +++ b/common/main/build.gradle.kts @@ -10,16 +10,13 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(compose.materialIconsExtended) implementation(project(":common:dependency-injection")) - //implementation("com.alialbaali.kamel:kamel-image:0.1.0") implementation(project(":common:data-models")) implementation(project(":common:database")) implementation(SqlDelight.coroutineExtensions) implementation(MVIKotlin.coroutines) implementation(MVIKotlin.mvikotlin) implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) } } } diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 9d72e6ea..72f453c8 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -1,6 +1,5 @@ package com.shabinder.common.main -import androidx.compose.ui.graphics.ImageBitmap import com.arkivanov.decompose.ComponentContext import com.arkivanov.mvikotlin.core.store.StoreFactory import com.shabinder.common.di.Dir @@ -39,8 +38,9 @@ interface SpotiFlyerMain { interface Dependencies { val mainOutput: Consumer val storeFactory: StoreFactory - val database: Database + val database: Database? val dir: Dir + val showPopUpMessage:(String)->Unit } sealed class Output { data class Search(val link: String) : Output() diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index 680c0707..d4ee42d3 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -1,11 +1,9 @@ package com.shabinder.common.main.integration -import androidx.compose.ui.graphics.ImageBitmap import com.arkivanov.decompose.ComponentContext import com.arkivanov.mvikotlin.extensions.coroutines.states import com.shabinder.common.di.Picture import com.shabinder.common.di.isInternetAvailable -import com.shabinder.common.di.showPopUpMessage import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain.* import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent @@ -22,7 +20,8 @@ internal class SpotiFlyerMainImpl( instanceKeeper.getStore { SpotiFlyerMainStoreProvider( storeFactory = storeFactory, - database = database + database = database, + showPopUpMessage = showPopUpMessage ).provide() } diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt index fbe289de..eef2edd9 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt @@ -22,7 +22,8 @@ import kotlinx.coroutines.flow.map internal class SpotiFlyerMainStoreProvider( private val storeFactory: StoreFactory, - database: Database + private val showPopUpMessage: (String)->Unit, + private val database: Database? ) { fun provide(): SpotiFlyerMainStore = @@ -34,12 +35,12 @@ internal class SpotiFlyerMainStoreProvider( reducer = ReducerImpl ) {} - val updates: Flow> = - database.downloadRecordDatabaseQueries - .selectAll() - .asFlow() - .mapToList(Dispatchers.Default) - .map { + val updates: Flow>? = + database?.downloadRecordDatabaseQueries + ?.selectAll() + ?.asFlow() + ?.mapToList(Dispatchers.Default) + ?.map { it.map { record -> record.run{ DownloadRecord(id, type, name, link, coverUrl, totalFiles) @@ -56,7 +57,7 @@ internal class SpotiFlyerMainStoreProvider( private inner class ExecutorImpl : SuspendExecutor() { override suspend fun executeAction(action: Unit, getState: () -> State) { - updates.collect { + updates?.collect { dispatch(Result.ItemsLoaded(it)) } } diff --git a/common/root/build.gradle.kts b/common/root/build.gradle.kts index 6539f6b7..6b0320ce 100644 --- a/common/root/build.gradle.kts +++ b/common/root/build.gradle.kts @@ -10,7 +10,6 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(compose.materialIconsExtended) implementation(project(":common:dependency-injection")) implementation(project(":common:data-models")) implementation(project(":common:database")) @@ -20,7 +19,6 @@ kotlin { implementation(MVIKotlin.coroutines) implementation(MVIKotlin.mvikotlin) implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) } } } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index 327c1a18..b0fb40f0 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -28,9 +28,10 @@ interface SpotiFlyerRoot { interface Dependencies { val storeFactory: StoreFactory - val database: Database + val database: Database? val fetchPlatformQueryResult: FetchPlatformQueryResult val directories: Dir + val showPopUpMessage:(String)->Unit val downloadProgressReport: MutableSharedFlow> } } diff --git a/desktop/src/jvmMain/kotlin/Main.kt b/desktop/src/jvmMain/kotlin/Main.kt index 2c2647d7..4d05d03e 100644 --- a/desktop/src/jvmMain/kotlin/Main.kt +++ b/desktop/src/jvmMain/kotlin/Main.kt @@ -17,8 +17,7 @@ import com.shabinder.common.di.initKoin import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.uikit.* import com.shabinder.database.Database -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch +import com.shabinder.common.uikit.showPopUpMessage as uikitShowPopUpMessage private val koin = initKoin(enableNetworkLogs = true).koin @@ -49,9 +48,10 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot = componentContext = componentContext, dependencies = object : SpotiFlyerRoot.Dependencies { override val storeFactory = DefaultStoreFactory - override val database: Database = koin.get() + override val database: Database? = koin.get() override val fetchPlatformQueryResult: FetchPlatformQueryResult = koin.get() override val directories: Dir = koin.get() + override val showPopUpMessage: (String) -> Unit = ::uikitShowPopUpMessage override val downloadProgressReport = DownloadProgressFlow } ) \ No newline at end of file diff --git a/fuzzywuzzy/app/build.gradle b/fuzzywuzzy/app/build.gradle index 20628a94..68ee6e7d 100644 --- a/fuzzywuzzy/app/build.gradle +++ b/fuzzywuzzy/app/build.gradle @@ -14,7 +14,8 @@ final pathSeparator = System.properties["path.separator"] kotlin { jvm() - /*js() { + js() { + browser() [compileKotlinJs, compileTestKotlinJs].each { configuration -> configuration.kotlinOptions { moduleKind = 'umd' @@ -22,7 +23,7 @@ kotlin { metaInfo = true } } - }*/ + } /* wasm32("wasm") @@ -63,7 +64,7 @@ kotlin { implementation 'junit:junit:4.12' } } - /*jsMain { + jsMain { kotlin.srcDir('src/jsMain/kotlin') dependencies { implementation kotlin("stdlib-js") @@ -77,7 +78,7 @@ kotlin { kotlinOptions.moduleKind = "umd" } } - jsTest { + /*jsTest { dependencies { implementation kotlin("test-js") implementation kotlin("stdlib-js") diff --git a/web-app/build.gradle.kts b/web-app/build.gradle.kts index b1e7a41b..ac0baecf 100644 --- a/web-app/build.gradle.kts +++ b/web-app/build.gradle.kts @@ -14,6 +14,16 @@ repositories { dependencies { implementation(kotlin("stdlib-js")) + implementation(Decompose.decompose) + implementation(Koin.core) + implementation(MVIKotlin.mvikotlin) + implementation(MVIKotlin.mvikotlinMain) + implementation(project(":common:root")) + implementation(project(":common:main")) + implementation(project(":common:list")) + implementation(project(":common:database")) + implementation(project(":common:data-models")) + implementation(project(":common:dependency-injection")) implementation("org.jetbrains:kotlin-react:17.0.1-pre.148-kotlin-1.4.30") implementation("org.jetbrains:kotlin-react-dom:17.0.1-pre.148-kotlin-1.4.30") implementation("org.jetbrains:kotlin-styled:1.0.0-pre.115-kotlin-1.4.10") diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt new file mode 100644 index 00000000..77bbb440 --- /dev/null +++ b/web-app/src/main/kotlin/App.kt @@ -0,0 +1,58 @@ +import co.touchlab.kermit.Kermit +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.decompose.lifecycle.LifecycleRegistry +import com.arkivanov.decompose.lifecycle.destroy +import com.arkivanov.decompose.lifecycle.resume +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory +import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.database.Database +import extras.renderableChild +import kotlinx.coroutines.flow.MutableSharedFlow +import react.* +import root.RootR + +external interface AppProps : RProps { + var dependencies: AppDependencies +} + +fun RBuilder.app(attrs: AppProps.() -> Unit): ReactElement { + return child(App::class){ + this.attrs(attrs) + } +} + +class App(props: AppProps): RComponent(props) { + + private val lifecycle = LifecycleRegistry() + private val ctx = DefaultComponentContext(lifecycle = lifecycle) + private val dependencies = props.dependencies + private val logger:Kermit + get() = dependencies.logger + + private val root = SpotiFlyerRoot(ctx, + object : SpotiFlyerRoot.Dependencies{ + override val storeFactory: StoreFactory = DefaultStoreFactory + override val database: Database? = null + override val fetchPlatformQueryResult = dependencies.fetchPlatformQueryResult + override val directories = dependencies.directories + override val showPopUpMessage: (String) -> Unit = {}//TODO + override val downloadProgressReport: MutableSharedFlow> + = MutableSharedFlow(1) + + } + ) + + override fun componentDidMount() { + lifecycle.resume() + } + + override fun componentWillUnmount() { + lifecycle.destroy() + } + + override fun RBuilder.render() { + renderableChild(RootR::class, root) + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/client.kt b/web-app/src/main/kotlin/client.kt index cf1cf79d..bd57486a 100644 --- a/web-app/src/main/kotlin/client.kt +++ b/web-app/src/main/kotlin/client.kt @@ -1,16 +1,34 @@ -import home.homeScreen +import co.touchlab.kermit.Kermit +import com.shabinder.common.di.Dir +import com.shabinder.common.di.FetchPlatformQueryResult +import com.shabinder.common.di.initKoin import react.dom.render import kotlinx.browser.document import kotlinx.browser.window import navbar.navBar +import org.koin.core.component.KoinComponent +import org.koin.core.component.get fun main() { window.onload = { render(document.getElementById("root")) { navBar {} - homeScreen { - link = "" + app { + dependencies = AppDependencies } } } +} + + +object AppDependencies : KoinComponent { + val logger: Kermit + val directories: Dir + val fetchPlatformQueryResult: FetchPlatformQueryResult + init { + initKoin() + directories = get() + logger = get() + fetchPlatformQueryResult = get() + } } \ No newline at end of file diff --git a/web-app/src/main/kotlin/extras/RenderableComponent.kt b/web-app/src/main/kotlin/extras/RenderableComponent.kt new file mode 100644 index 00000000..0c9aea86 --- /dev/null +++ b/web-app/src/main/kotlin/extras/RenderableComponent.kt @@ -0,0 +1,27 @@ +package extras + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import react.RState +import react.setState + +abstract class RenderableComponent< + T : Any, + S : RState + >( + props: Props, + initialState: S +) : RenderableRootComponent(props,initialState) { + + protected abstract val stateFlow: Flow + + override fun componentDidMount() { + super.componentDidMount() + scope.launch { + stateFlow.collectLatest { + setState { state = it } + } + } + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/extras/RenderableRootComponent.kt b/web-app/src/main/kotlin/extras/RenderableRootComponent.kt new file mode 100644 index 00000000..f26542c4 --- /dev/null +++ b/web-app/src/main/kotlin/extras/RenderableRootComponent.kt @@ -0,0 +1,63 @@ +package extras + +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.ValueObserver +import extras.RenderableRootComponent.Props +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import react.RComponent +import react.RProps +import react.RState +import react.setState + +abstract class RenderableRootComponent< + T : Any, + S : RState + >( + props: Props, + initialState: S +) : RComponent, S>(props) { + + protected val model: T get() = props.model + private val subscriptions = ArrayList>() + protected lateinit var scope: CoroutineScope + + init { + state = initialState + } + + override fun componentDidMount() { + subscriptions.forEach { subscribe(it) } + scope = CoroutineScope(Dispatchers.Default) + } + + private fun subscribe(subscription: Subscription) { + subscription.value.subscribe(subscription.observer) + } + + override fun componentWillUnmount() { + subscriptions.forEach { unsubscribe(it) } + scope.cancel("Component Unmounted") + } + + private fun unsubscribe(subscription: Subscription) { + subscription.value.unsubscribe(subscription.observer) + } + + protected fun Value.bindToState(buildState: S.(T) -> Unit) { + subscriptions += Subscription(this) { data -> setState { buildState(data) } } + } + + interface Props : RProps { + var model: T + } + + protected class Subscription( + val value: Value, + val observer: ValueObserver + ) +} diff --git a/web-app/src/main/kotlin/extras/UniqueId.kt b/web-app/src/main/kotlin/extras/UniqueId.kt new file mode 100644 index 00000000..cbe115dc --- /dev/null +++ b/web-app/src/main/kotlin/extras/UniqueId.kt @@ -0,0 +1,16 @@ +package extras + +import kotlinext.js.Object +import kotlinext.js.jsObject + +var uniqueId: Long = 0L + +internal fun Any.uniqueId(): Long { + var id: dynamic = asDynamic().__unique_id + if (id == undefined) { + id = ++uniqueId + Object.defineProperty(this, "__unique_id", jsObject { value = id }) + } + + return id +} diff --git a/web-app/src/main/kotlin/extras/Utils.kt b/web-app/src/main/kotlin/extras/Utils.kt new file mode 100644 index 00000000..ba6cb5c5 --- /dev/null +++ b/web-app/src/main/kotlin/extras/Utils.kt @@ -0,0 +1,11 @@ +package extras + +import react.RBuilder +import kotlin.reflect.KClass + +fun > RBuilder.renderableChild(clazz: KClass, model: M) { + child(clazz) { + key = model.uniqueId().toString() + attrs.model = model + } +} diff --git a/web-app/src/main/kotlin/home/HomeScreen.kt b/web-app/src/main/kotlin/home/HomeScreen.kt index 75817775..1c40b2b2 100644 --- a/web-app/src/main/kotlin/home/HomeScreen.kt +++ b/web-app/src/main/kotlin/home/HomeScreen.kt @@ -1,24 +1,22 @@ package home +import com.shabinder.common.main.SpotiFlyerMain +import extras.RenderableComponent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.css.* import react.* import styled.css import styled.styledDiv +class HomeScreen( + props: Props, + override val stateFlow: Flow = props.model.models.map { State(it) } +) : RenderableComponent( + props, + initialState = State(data = SpotiFlyerMain.State()) +) { -data class HomeScreenState(var link:String): RState - -external interface HomeScreenProps : RProps { - var link: String -} - -fun RBuilder.homeScreen(attrs:HomeScreenProps.() -> Unit): ReactElement { - return child(HomeScreen::class){ - this.attrs(attrs) - } -} - -class HomeScreen(props:HomeScreenProps):RComponent(props) { override fun RBuilder.render() { styledDiv{ css { @@ -28,24 +26,34 @@ class HomeScreen(props:HomeScreenProps):RComponent Unit): ReactElement { - return child(IconList::class){ - this.attrs(attrs) +@Suppress("FunctionName") +fun RBuilder.IconList(handler:IconListProps.() -> Unit): ReactElement { + return child(iconList){ + attrs { + handler() + } } } -class IconList(props: IconListProps):RComponent(props) { - override fun RBuilder.render() { - styledDiv { - css { - +Styles.makeRow - margin(18.px) - if(props.isBadge) { - alignItems = Align.end - } +private val iconList = functionalComponent("IconList") { props -> + styledDiv { + css { + +Styles.makeRow + margin(18.px) + if(props.isBadge) { + alignItems = Align.end } - for((icon,platformLink) in props.iconsAndPlatforms){ - styledA(href = platformLink){ - styledImg { - attrs { - src = icon - } - css { - classes = mutableListOf("glow-button") - margin(8.px) - if (!props.isBadge) { - height = 42.px - width = 42.px - borderRadius = 50.px - } + } + for((icon,platformLink) in props.iconsAndPlatforms){ + styledA(href = platformLink){ + styledImg { + attrs { + src = icon + } + css { + classes = mutableListOf("glow-button") + margin(8.px) + if (!props.isBadge) { + height = 42.px + width = 42.px + borderRadius = 50.px } } } diff --git a/web-app/src/main/kotlin/home/Message.kt b/web-app/src/main/kotlin/home/Message.kt index da938dcb..df5bc852 100644 --- a/web-app/src/main/kotlin/home/Message.kt +++ b/web-app/src/main/kotlin/home/Message.kt @@ -6,28 +6,26 @@ import styled.css import styled.styledDiv import styled.styledH1 - -data class MessageState(var link:String): RState - external interface MessageProps : RProps { var text: String } -fun RBuilder.message(attrs:MessageProps.() -> Unit): ReactElement { - return child(Message::class){ - this.attrs(attrs) +@Suppress("FunctionName") +fun RBuilder.Message(handler:MessageProps.() -> Unit): ReactElement { + return child(message){ + attrs { + handler() + } } } -class Message(props:MessageProps): RComponent(props) { - override fun RBuilder.render() { - styledDiv { - styledH1 { - +"Your Gateway to Nirvana, for FREE!" - css { - classes = mutableListOf("headingTitle") - fontSize = 3.2.rem - } +private val message = functionalComponent("Message") { props-> + styledDiv { + styledH1 { + + props.text + css { + classes = mutableListOf("headingTitle") + fontSize = 3.2.rem } } } diff --git a/web-app/src/main/kotlin/home/Searchbar.kt b/web-app/src/main/kotlin/home/Searchbar.kt index baa19879..d064cc8c 100644 --- a/web-app/src/main/kotlin/home/Searchbar.kt +++ b/web-app/src/main/kotlin/home/Searchbar.kt @@ -7,59 +7,54 @@ import org.w3c.dom.HTMLInputElement import react.* import styled.* -data class SearchbarState(var link:String):RState - external interface SearchbarProps : RProps { var link: String + var search:(String)->Unit } -fun RBuilder.searchBar(attrs:SearchbarProps.() -> Unit): ReactElement { - return child(Searchbar::class){ - this.attrs(attrs) +@Suppress("FunctionName") +fun RBuilder.SearchBar(handler:SearchbarProps.() -> Unit) = child(searchbar){ + attrs { + handler() } } -class Searchbar(props: SearchbarProps): RComponent(props) { - init { - state = SearchbarState(props.link) - } - override fun RBuilder.render() { - styledDiv{ + +val searchbar = functionalComponent("SearchBar"){ props -> + val (link,setLink) = useState(props.link) + + styledDiv{ + css { + classes = mutableListOf("searchBox") + } + styledInput(type = InputType.url){ + attrs { + placeholder = "Search" + onChangeFunction = { + val target = it.target as HTMLInputElement + setLink(target.value) + } + value = link + } css { - classes = mutableListOf("searchBox") + classes = mutableListOf("searchInput") } - styledInput(type = InputType.url){ - attrs { - placeholder = "Search" - onChangeFunction = { - val target = it.target as HTMLInputElement - setState{ - link = target.value - } - } - value = state.link - } - css { - classes = mutableListOf("searchInput") + } + styledButton { + attrs { + onClickFunction = { + props.search(link) } } - styledButton { - attrs { - onClickFunction = { - - } - } + css { + classes = mutableListOf("searchButton") + } + styledImg(src = "search.svg") { css { - classes = mutableListOf("searchButton") - } - styledImg(src = "search.svg") { - css { - classes = mutableListOf("search-icon") - } + classes = mutableListOf("search-icon") } } } } - -} \ No newline at end of file +} diff --git a/web-app/src/main/kotlin/list/CoverImage.kt b/web-app/src/main/kotlin/list/CoverImage.kt new file mode 100644 index 00000000..7a5294e0 --- /dev/null +++ b/web-app/src/main/kotlin/list/CoverImage.kt @@ -0,0 +1,45 @@ +package list + +import kotlinx.css.* +import kotlinx.html.id +import react.RProps +import react.rFunction +import react.useState +import styled.css +import styled.styledDiv +import styled.styledH1 +import styled.styledImg + + +external interface CoverImageProps : RProps { + var coverImageURL: String + var coverName: String +} + +val CoverImage = rFunction("CoverImage"){ props -> + val (coverURL,setCoverURL) = useState(props.coverImageURL) + val (coverName,setCoverName) = useState(props.coverName) + + styledDiv { + styledImg(src=coverURL){ + css { + height = 300.px + width = 300.px + } + } + styledH1 { + +coverName + css { + textAlign = TextAlign.center + } + } + attrs { + id = "cover-image" + } + css { + display = Display.flex + alignItems = Align.center + flexDirection = FlexDirection.column + } + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/ListScreen.kt b/web-app/src/main/kotlin/list/ListScreen.kt new file mode 100644 index 00000000..c74d4815 --- /dev/null +++ b/web-app/src/main/kotlin/list/ListScreen.kt @@ -0,0 +1,39 @@ +package list + +import com.shabinder.common.list.SpotiFlyerList +import extras.RenderableComponent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.css.* +import kotlinx.html.id +import react.RBuilder +import react.RState +import styled.css +import styled.styledDiv + +class ListScreen( + props: Props, + override val stateFlow: Flow = props.model.models.map { State(it) } +) : RenderableComponent(props,initialState = State(SpotiFlyerList.State())) { + + override fun RBuilder.render() { + styledDiv { + attrs { + id = "list-screen-div" + } + css { + classes = mutableListOf("list-screen") + display = Display.flex + flexDirection = FlexDirection.column + flexGrow = 1.0 + justifyContent = JustifyContent.center + alignItems = Align.center + backgroundColor = Color.white + } + } + } + + class State( + var data: SpotiFlyerList.State + ):RState +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/TrackItem.kt b/web-app/src/main/kotlin/list/TrackItem.kt new file mode 100644 index 00000000..269e9198 --- /dev/null +++ b/web-app/src/main/kotlin/list/TrackItem.kt @@ -0,0 +1,13 @@ +package list + +import react.RProps +import react.rFunction + +external interface TrackItemProps : RProps { + var coverImageURL: String + var coverName: String +} + +val trackItem = rFunction("Track-Item"){ + +} diff --git a/web-app/src/main/kotlin/root/RootR.kt b/web-app/src/main/kotlin/root/RootR.kt new file mode 100644 index 00000000..1467c018 --- /dev/null +++ b/web-app/src/main/kotlin/root/RootR.kt @@ -0,0 +1,34 @@ +package root + +import com.arkivanov.decompose.RouterState +import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.* +import extras.RenderableRootComponent +import extras.renderableChild +import home.HomeScreen +import list.ListScreen +import react.RBuilder +import react.RState + +class RootR(props: Props) : RenderableRootComponent( + props = props, + initialState = State(routerState = props.model.routerState.value) +) { + private val component: Child + get() = model.routerState.value.activeChild.component + + override fun RBuilder.render() { + when(component){ + is Child.Main -> renderableChild(HomeScreen::class, (component as Child.Main).component) + is Child.List -> renderableChild(ListScreen::class, (component as Child.List).component) + } + } + + init { + model.routerState.bindToState { routerState = it } + } + class State( + var routerState: RouterState<*, Child>, + ) : RState + +}