From c7c61e51d64eb0877524d140ca38d9088d23423e Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 3 Feb 2021 01:35:12 +0530 Subject: [PATCH] SpotiFlyerList and more porting --- android/build.gradle.kts | 2 + common/compose-ui/build.gradle.kts | 6 + .../kotlin/com/shabinder/common/ui/Actual.kt | 31 +++ .../shabinder/common/list/SpotiFlyerList.kt | 17 +- .../shabinder/common/list/SpotiFlyerListUi.kt | 151 ++++++++++ .../list/integration/SpotiFlyerListImpl.kt | 10 +- .../common/list/store/SpotiFlyerListStore.kt | 3 +- .../list/store/SpotiFlyerListStoreProvider.kt | 46 +++- .../shabinder/common/main/SpotiFlyerMainUi.kt | 10 + .../shabinder/common/root/SpotiFlyerRoot.kt | 32 +++ .../shabinder/common/root/SpotiFlyerRootUi.kt | 20 ++ .../root/integration/SpotiFlyerRootImpl.kt | 75 +++++ .../kotlin/com/shabinder/common/ui/Expect.kt | 14 + .../common/ui/tracklist/TrackList.kt | 257 ------------------ .../kotlin/com/shabinder/common/ui/Actual.kt | 16 ++ .../common/{PlatformImp.kt => Actual.kt} | 16 ++ .../kotlin/com/shabinder/common/DI.kt | 16 +- .../kotlin/com/shabinder/common/Expect.kt | 2 + .../common/FetchPlatformQueryResult.kt | 44 ++- .../common/{PlatformImpl.kt => Actual.kt} | 16 ++ 20 files changed, 500 insertions(+), 284 deletions(-) create mode 100644 common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRootUi.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt create mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt delete mode 100644 common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt create mode 100644 common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt rename common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/{PlatformImp.kt => Actual.kt} (78%) rename common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/{PlatformImpl.kt => Actual.kt} (81%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 0185725f..d3620fc1 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -22,6 +22,7 @@ android { versionCode = Versions.versionCode versionName = Versions.versionName } + buildTypes { getByName("release") { isMinifyEnabled = false @@ -29,6 +30,7 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } + compileOptions { // Flag to enable support for the new language APIs //coreLibraryDesugaringEnabled = true diff --git a/common/compose-ui/build.gradle.kts b/common/compose-ui/build.gradle.kts index f140db88..1c926d66 100644 --- a/common/compose-ui/build.gradle.kts +++ b/common/compose-ui/build.gradle.kts @@ -18,6 +18,12 @@ kotlin { //implementation(Badoo.Reaktive.reaktive) implementation(Decompose.decompose) implementation(Decompose.extensionsCompose) + + //Coil-Image Loading + Versions.coilVersion.let{ + implementation("dev.chrisbanes.accompanist:accompanist-coil:$it") + implementation("dev.chrisbanes.accompanist:accompanist-insets:$it") + } } } } diff --git a/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt new file mode 100644 index 00000000..a4c62da4 --- /dev/null +++ b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt @@ -0,0 +1,31 @@ +package com.shabinder.common.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import androidx.core.net.toUri +import dev.chrisbanes.accompanist.coil.CoilImage + +@Composable +actual fun ImageLoad( + url:String, + loadingResource:ImageBitmap?, + errorResource:ImageBitmap?, + modifier: Modifier +){ + val imgUri = url.toUri().buildUpon().scheme("https").build() + CoilImage( + data = imgUri, + contentScale = ContentScale.Crop, + loading = { loadingResource?.let { Image(it,"loading image") } }, + error = { errorResource?.let { it1 -> Image(it1,"Error Image") } }, + modifier = modifier + ) +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index ef4598a8..b82ea4b1 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -2,9 +2,11 @@ package com.shabinder.common.list import com.arkivanov.decompose.ComponentContext import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.shabinder.common.FetchPlatformQueryResult import com.shabinder.common.PlatformQueryResult import com.shabinder.common.TrackDetails import com.shabinder.common.list.integration.SpotiFlyerListImpl +import com.shabinder.common.utils.Consumer import com.shabinder.database.Database import kotlinx.coroutines.flow.Flow @@ -13,10 +15,13 @@ interface SpotiFlyerList { val models: Flow /* - * For Single Track Download -> list(that track) - * For Download All -> Model.tracks + * Download All Tracks(after filtering already Downloaded) * */ - fun onDownloadClicked(trackList:List) + fun onDownloadAllClicked(trackList:List) + /* + * Download All Tracks(after filtering already Downloaded) + * */ + fun onDownloadClicked(wholeTrackList:List,trackIndex:Int) /* * To Pop and return back to Main Screen @@ -25,15 +30,15 @@ interface SpotiFlyerList { interface Dependencies { val storeFactory: StoreFactory - val database: Database + val fetchQuery: FetchPlatformQueryResult val link: String - fun listOutput(finished: Output.Finished) + fun listOutput(finished: Output.Finished): Consumer } sealed class Output { object Finished : Output() } data class State( - val result:PlatformQueryResult? = null, + val queryResult:PlatformQueryResult? = null, val link:String = "" ) } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt new file mode 100644 index 00000000..85648587 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt @@ -0,0 +1,151 @@ +package com.shabinder.common.list + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.shabinder.common.DownloadStatus +import com.shabinder.common.TrackDetails +import com.shabinder.common.ui.ImageLoad +import com.shabinder.spotiflyer.ui.SpotiFlyerTypography +import com.shabinder.spotiflyer.ui.colorAccent +import kotlinx.coroutines.CoroutineScope + +@Composable +fun SpotiFlyerListContent( + component: SpotiFlyerList, + modifier: Modifier = Modifier +) { + val model by component.models.collectAsState(SpotiFlyerList.State()) + val coroutineScope = rememberCoroutineScope() + + Box(modifier = modifier.fillMaxSize()) { + //TODO Null Handling + val result = model.queryResult!! + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + content = { + item { + CoverImage(result.title, result.coverUrl, coroutineScope) + } + itemsIndexed(result.trackList) { index, item -> + TrackCard( + track = item, + downloadTrack = { component.onDownloadClicked(result.trackList,index) }, + ) + } + }, + modifier = Modifier.fillMaxSize(), + ) + DownloadAllButton( + onClick = {component.onDownloadAllClicked(result.trackList)}, + modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) + ) + } +} + +@Composable +fun TrackCard( + track: TrackDetails, + downloadTrack:()->Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + ImageLoad( + url = track.albumArtURL, + modifier = Modifier + .preferredWidth(75.dp) + .preferredHeight(90.dp) + .clip(MaterialTheme.shapes.medium) + ) + Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { + Text(track.title,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() + ){ + Text("${track.artists.firstOrNull()}...",fontSize = 12.sp,maxLines = 1) + Text("${track.durationSec/60} min, ${track.durationSec%60} sec",fontSize = 12.sp,maxLines = 1,overflow = TextOverflow.Ellipsis) + } + } + when(track.downloaded){ + DownloadStatus.Downloaded -> { + //Image(vectorResource(id = R.drawable.ic_tick)) + } + DownloadStatus.Queued -> { + CircularProgressIndicator() + } + DownloadStatus.Failed -> { + //Image(vectorResource(id = R.drawable.ic_error)) + } + DownloadStatus.Downloading -> { + CircularProgressIndicator(progress = track.progress.toFloat()/100f) + } + DownloadStatus.Converting -> { + CircularProgressIndicator(progress = 100f,color = colorAccent) + } + DownloadStatus.NotDownloaded -> { + /*Image(vectorResource(id = R.drawable.ic_arrow), Modifier.clickable(onClick = { + downloadTrack() + }))*/ + } + } + } +} + +@Composable +fun CoverImage( + title: String, + coverURL: String, + scope: CoroutineScope, + modifier: Modifier = Modifier, +) { + Column( + modifier.padding(vertical = 8.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ImageLoad( + url = coverURL, + modifier = Modifier + .preferredWidth(210.dp) + .preferredHeight(230.dp) + .clip(MaterialTheme.shapes.medium) + ) + Text( + text = title, + style = SpotiFlyerTypography.h5, + maxLines = 2, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + //color = colorAccent, + ) + } + /*scope.launch { + updateGradient(coverURL, ctx) + }*/ +} + +@Composable +fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + ExtendedFloatingActionButton( + text = { Text("Download All") }, + onClick = onClick, + //icon = { Icon(imageVector = Image(R.drawable.ic_download_arrow),tint = Color.Black) }, + backgroundColor = colorAccent, + modifier = modifier + ) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index b3cf65d1..4f596124 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -20,15 +20,19 @@ internal class SpotiFlyerListImpl( instanceKeeper.getStore { SpotiFlyerListStoreProvider( storeFactory = storeFactory, - database = database, + fetchQuery = fetchQuery, link = link ).provide() } override val models: Flow = store.states - override fun onDownloadClicked(trackList: List) { - store.accept(Intent.StartDownload(trackList)) + override fun onDownloadAllClicked(trackList: List) { + store.accept(Intent.StartDownloadAll(trackList)) + } + + override fun onDownloadClicked(wholeTrackList: List, trackIndex: Int) { + store.accept(Intent.StartDownload(wholeTrackList,trackIndex)) } override fun onBackPressed(){ diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt index 563a983d..a19c508d 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt @@ -9,7 +9,8 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.* internal interface SpotiFlyerListStore: Store { sealed class Intent { - data class StartDownload(val trackList: List): Intent() + data class StartDownloadAll(val trackList: List): Intent() + data class StartDownload(val wholeTrackList: List, val trackIndex:Int): Intent() data class SearchLink(val link: String): Intent() } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index 6bdfb1fd..278a7af1 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -2,18 +2,13 @@ package com.shabinder.common.list.store import com.arkivanov.mvikotlin.core.store.* import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor -import com.shabinder.common.FetchPlatformQueryResult -import com.shabinder.common.PlatformQueryResult +import com.shabinder.common.* import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.store.SpotiFlyerListStore.Intent -import com.shabinder.database.Database -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch internal class SpotiFlyerListStoreProvider( private val storeFactory: StoreFactory, - private val database: Database, + private val fetchQuery: FetchPlatformQueryResult, private val link: String ) { fun provide(): SpotiFlyerListStore = @@ -28,21 +23,45 @@ internal class SpotiFlyerListStoreProvider( private sealed class Result { data class ResultFetched(val result: PlatformQueryResult) : Result() data class SearchLink(val link: String) : Result() + data class UpdateTrackList(val list:List): Result() } private inner class ExecutorImpl : SuspendExecutor() { override suspend fun executeAction(action: Unit, getState: () -> State) { - FetchPlatformQueryResult().query(link)?.let{ + fetchQuery.query(link)?.let{ dispatch(Result.ResultFetched(it)) } } override suspend fun executeIntent(intent: Intent, getState: () -> State) { when (intent) {//TODO: Add Dispatchers where needed - is Intent.StartDownload -> {}//TODO() - is Intent.SearchLink -> FetchPlatformQueryResult().query(link)?.let{ + is Intent.SearchLink -> fetchQuery.query(link)?.let{ dispatch((Result.ResultFetched(it))) } + is Intent.StartDownloadAll -> { + val finalList = + intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded } + if (finalList.isNullOrEmpty()) //TODO showDialog("All Songs are Processed") + else downloadTracks(finalList) + + val list = intent.trackList.map { + if (it.downloaded == DownloadStatus.NotDownloaded) { + it.downloaded = DownloadStatus.Queued + } + it + } + dispatch(Result.UpdateTrackList(list)) + } + is Intent.StartDownload -> { + val trackList = intent.wholeTrackList.toMutableList() + val track = trackList.getOrNull(intent.trackIndex) + ?.apply { downloaded = DownloadStatus.Queued } + track?.let { + trackList[intent.trackIndex] = it + dispatch(Result.UpdateTrackList(trackList)) + } + + } } } } @@ -50,8 +69,9 @@ internal class SpotiFlyerListStoreProvider( private object ReducerImpl : Reducer { override fun State.reduce(result: Result): State = when (result) { - is Result.ResultFetched -> copy(result = result.result) - is Result.SearchLink -> copy(link = result.link) - } + is Result.ResultFetched -> copy(queryResult = result.result) + is Result.SearchLink -> copy(link = result.link) + is Result.UpdateTrackList -> copy(queryResult = this.queryResult?.apply { trackList = result.list }) + } } } \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt new file mode 100644 index 00000000..03cbf629 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt @@ -0,0 +1,10 @@ +package com.shabinder.common.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue + +@Composable +fun SpotiFlyerMainContent(component: SpotiFlyerMain){ + val model by component.models.collectAsState(SpotiFlyerMain.State()) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt new file mode 100644 index 00000000..636c52e9 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -0,0 +1,32 @@ +package com.shabinder.common.root + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.RouterState +import com.arkivanov.decompose.value.Value +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.shabinder.common.FetchPlatformQueryResult +import com.shabinder.common.list.SpotiFlyerList +import com.shabinder.common.main.SpotiFlyerMain +import com.shabinder.common.root.SpotiFlyerRoot.Dependencies +import com.shabinder.database.Database +import com.shabinder.common.root.integration.SpotiFlyerRootImpl + +interface SpotiFlyerRoot { + + val routerState: Value> + + sealed class Child { + data class Main(val component: SpotiFlyerMain) : Child() + data class List(val component: SpotiFlyerList) : Child() + } + + interface Dependencies { + val storeFactory: StoreFactory + val database: Database + val fetchPlatformQueryResult: FetchPlatformQueryResult + } +} + +@Suppress("FunctionName") // Factory function +fun SpotiFlyerRoot(componentContext: ComponentContext, dependencies: Dependencies): SpotiFlyerRoot = + SpotiFlyerRootImpl(componentContext, dependencies) diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRootUi.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRootUi.kt new file mode 100644 index 00000000..c1b9c103 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRootUi.kt @@ -0,0 +1,20 @@ +package com.shabinder.common.root + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.extensions.compose.jetbrains.Children +import com.shabinder.common.list.SpotiFlyerListContent +import com.shabinder.common.main.SpotiFlyerMainContent +import com.shabinder.common.root.SpotiFlyerRoot.Child + +@Composable +fun SpotiFlyerRootContent(component: SpotiFlyerRoot) { + Children( + routerState = component.routerState, + //TODO animation = crossfade() + ) { child, _ -> + when (child) { + is Child.Main -> SpotiFlyerMainContent(component = child.component) + is Child.List -> SpotiFlyerListContent(component = child.component) + } + } +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt new file mode 100644 index 00000000..4c1503fb --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -0,0 +1,75 @@ +package com.shabinder.common.root.integration + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.RouterState +import com.arkivanov.decompose.pop +import com.arkivanov.decompose.push +import com.arkivanov.decompose.router +import com.arkivanov.decompose.statekeeper.Parcelable +import com.arkivanov.decompose.statekeeper.Parcelize +import com.arkivanov.decompose.value.Value +import com.shabinder.common.list.SpotiFlyerList +import com.shabinder.common.main.SpotiFlyerMain +import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.Child +import com.shabinder.common.root.SpotiFlyerRoot.Dependencies +import com.shabinder.common.utils.Consumer + +internal class SpotiFlyerRootImpl( + componentContext: ComponentContext, + dependencies: Dependencies +) : SpotiFlyerRoot, ComponentContext by componentContext, Dependencies by dependencies { + + private val router = + router( + initialConfiguration = Configuration.Main, + handleBackButton = true, + componentFactory = ::createChild + ) + + override val routerState: Value> = router.state + + private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child = + when (configuration) { + is Configuration.Main -> Child.Main(spotiFlyerMain(componentContext)) + is Configuration.Edit -> Child.List(spotiFlyerList(componentContext, link = configuration.link)) + } + + private fun spotiFlyerMain(componentContext: ComponentContext): SpotiFlyerMain = + SpotiFlyerMain( + componentContext = componentContext, + dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by this { + override fun mainOutput(searched: SpotiFlyerMain.Output): Consumer = Consumer(::onMainOutput) + } + ) + + private fun spotiFlyerList(componentContext: ComponentContext, link: String): SpotiFlyerList = + SpotiFlyerList( + componentContext = componentContext, + dependencies = object : SpotiFlyerList.Dependencies, Dependencies by this { + override val fetchQuery = fetchPlatformQueryResult + override val link: String = link + + override fun listOutput(finished: SpotiFlyerList.Output.Finished): Consumer = + Consumer(::onListOutput) + } + ) + + private fun onMainOutput(output: SpotiFlyerMain.Output): Unit = + when (output) { + is SpotiFlyerMain.Output.Search -> router.push(Configuration.Edit(link = output.link)) + } + + private fun onListOutput(output: SpotiFlyerList.Output): Unit = + when (output) { + is SpotiFlyerList.Output.Finished -> router.pop() + } + + private sealed class Configuration : Parcelable { + @Parcelize + object Main : Configuration() + + @Parcelize + data class Edit(val link: String) : Configuration() + } +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt new file mode 100644 index 00000000..d277e1ff --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt @@ -0,0 +1,14 @@ +package com.shabinder.common.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.Dp + +@Composable +expect fun ImageLoad( + url:String, + loadingResource: ImageBitmap? = null, + errorResource: ImageBitmap? = null, + modifier: Modifier = Modifier +) \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt deleted file mode 100644 index 694543ad..00000000 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (c) 2021 Shabinder Singh - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.ui.tracklist - -import android.content.Context -import android.content.Intent -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.AmbientContext -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import androidx.navigation.NavController -import com.shabinder.spotiflyer.R -import com.shabinder.spotiflyer.models.DownloadStatus -import com.shabinder.spotiflyer.models.PlatformQueryResult -import com.shabinder.spotiflyer.models.TrackDetails -import com.shabinder.spotiflyer.providers.GaanaProvider -import com.shabinder.spotiflyer.providers.SpotifyProvider -import com.shabinder.spotiflyer.providers.YoutubeProvider -import com.shabinder.spotiflyer.ui.SpotiFlyerTypography -import com.shabinder.spotiflyer.ui.colorAccent -import com.shabinder.spotiflyer.ui.utils.calculateDominantColor -import com.shabinder.spotiflyer.utils.downloadTracks -import com.shabinder.spotiflyer.utils.sharedViewModel -import com.shabinder.spotiflyer.utils.showDialog -import com.shabinder.spotiflyer.worker.ForegroundService -import dev.chrisbanes.accompanist.coil.CoilImage -import kotlinx.coroutines.* - -/* -* UI for List of Tracks to be universally used. -**/ -@Composable -fun TrackList( - fullLink: String, - navController: NavController, - spotifyProvider: SpotifyProvider, - gaanaProvider: GaanaProvider, - youtubeProvider: YoutubeProvider, - modifier: Modifier = Modifier -){ - val context = AmbientContext.current - val coroutineScope = rememberCoroutineScope() - - var result by remember(fullLink) { mutableStateOf(null) } - - coroutineScope.launch(Dispatchers.Default) { - @Suppress("UnusedEquals")//Add Delay if result is not Initialized yet. - try{result == null}catch(e:java.lang.IllegalStateException){delay(100)} - if(result == null){ - result = when{ - /* - * Using SharedViewModel's Link as NAVIGATION's Arg is buggy for links. - * */ - //SPOTIFY - sharedViewModel.link.contains("spotify",true) -> - spotifyProvider.query(sharedViewModel.link) - - //YOUTUBE - sharedViewModel.link.contains("youtube.com",true) || sharedViewModel.link.contains("youtu.be",true) -> - youtubeProvider.query(sharedViewModel.link) - - //GAANA - sharedViewModel.link.contains("gaana",true) -> - gaanaProvider.query(sharedViewModel.link) - - else -> { - showDialog("Link is Not Valid") - null - } - } - } - withContext(Dispatchers.Main){ - //Error Occurred And Has Been Shown to User - if(result == null) navController.popBackStack() - } - } - - sharedViewModel.updateTrackList(result?.trackList ?: listOf()) - queryActiveTracks(context) - - result?.let{ - val ctx = AmbientContext.current - Box(modifier = modifier.fillMaxSize()){ - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - content = { - item { - CoverImage(it.title,it.coverUrl,coroutineScope) - } - itemsIndexed(sharedViewModel.trackList) { index, item -> - TrackCard( - track = item, - onDownload = { - downloadTracks(arrayListOf(item),ctx) - sharedViewModel.updateTrackStatus(index,DownloadStatus.Queued) - }, - ) - } - }, - modifier = Modifier.fillMaxSize(), - ) - DownloadAllButton( - onClick = { - val finalList = sharedViewModel.trackList.filter{it.downloaded == DownloadStatus.NotDownloaded} - if (finalList.isNullOrEmpty()) showDialog("All Songs are Processed") - else downloadTracks(finalList as ArrayList,ctx) - val list = sharedViewModel.trackList.map { - if(it.downloaded == DownloadStatus.NotDownloaded){ - it.downloaded = DownloadStatus.Queued - } - it - } - sharedViewModel.updateTrackList(list) - }, - modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) - ) - } - } -} - -@Composable -fun CoverImage( - title: String, - coverURL: String, - scope: CoroutineScope, - modifier: Modifier = Modifier, -) { - val ctx = AmbientContext.current - Column( - modifier.padding(vertical = 8.dp).fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val imgUri = coverURL.toUri().buildUpon().scheme("https").build() - CoilImage( - data = imgUri, - contentScale = ContentScale.Crop, - loading = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, - modifier = Modifier - .preferredWidth(210.dp) - .preferredHeight(230.dp) - .clip(MaterialTheme.shapes.medium) - ) - Text( - text = title, - style = SpotiFlyerTypography.h5, - maxLines = 2, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - //color = colorAccent, - ) - } - scope.launch { - updateGradient(coverURL, ctx) - } -} - -@Composable -fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - ExtendedFloatingActionButton( - text = { Text("Download All") }, - onClick = onClick, - icon = { Icon(imageVector = vectorResource(R.drawable.ic_download_arrow),tint = Color.Black) }, - backgroundColor = colorAccent, - modifier = modifier - ) -} - -@Composable -fun TrackCard( - track:TrackDetails, - onDownload:(TrackDetails)->Unit, -) { - Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { - val imgUri = track.albumArtURL.toUri().buildUpon().scheme("https").build() - CoilImage( - data = imgUri, - //Loading Placeholder Makes Scrolling very stuttery -// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, - error = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, - contentScale = ContentScale.Inside, -// fadeIn = true, - modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp) - ) - Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { - Text(track.title,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() - ){ - Text("${track.artists.firstOrNull()}...",fontSize = 12.sp,maxLines = 1) - Text("${track.durationSec/60} min, ${track.durationSec%60} sec",fontSize = 12.sp,maxLines = 1,overflow = TextOverflow.Ellipsis) - } - } - when(track.downloaded){ - DownloadStatus.Downloaded -> { - Image(vectorResource(id = R.drawable.ic_tick)) - } - DownloadStatus.Queued -> { - CircularProgressIndicator() - } - DownloadStatus.Failed -> { - Image(vectorResource(id = R.drawable.ic_error)) - } - DownloadStatus.Downloading -> { - CircularProgressIndicator(progress = track.progress.toFloat()/100f) - } - DownloadStatus.Converting -> { - CircularProgressIndicator(progress = 100f,color = colorAccent) - } - DownloadStatus.NotDownloaded -> { - Image(vectorResource(id = R.drawable.ic_arrow), Modifier.clickable(onClick = { - onDownload(track) - })) - } - } - } -} -private fun queryActiveTracks(context:Context?) { - val serviceIntent = Intent(context, ForegroundService::class.java).apply { - action = "query" - } - context?.let { ContextCompat.startForegroundService(it, serviceIntent) } -} -suspend fun updateGradient(imageURL:String,ctx:Context){ - calculateDominantColor(imageURL,ctx)?.color - ?.let { sharedViewModel.updateGradientColor(it) } -} \ No newline at end of file diff --git a/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt new file mode 100644 index 00000000..e92f72d0 --- /dev/null +++ b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt @@ -0,0 +1,16 @@ +package com.shabinder.common.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.Dp + +@Composable +actual fun ImageLoad( + url:String, + loadingResource: ImageBitmap?, + errorResource: ImageBitmap?, + modifier: Modifier +){ + +} \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/PlatformImp.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt similarity index 78% rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/PlatformImp.kt rename to common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt index 1eff0620..ead69ddd 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/PlatformImp.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt @@ -6,6 +6,22 @@ import co.touchlab.kermit.Kermit import com.shabinder.common.database.appContext import java.io.File +actual fun openPlatform(platformID:String ,platformLink:String){ + //TODO +} + +actual fun shareApp(){ + //TODO +} + +actual fun giveDonation(){ + //TODO +} + +actual fun downloadTracks(list: List){ + //TODO +} + actual open class Dir actual constructor(logger: Kermit) { private val context:Context diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt index c2ff8317..2f8bfa16 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt @@ -10,6 +10,8 @@ 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.runBlocking import kotlinx.serialization.json.Json import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration @@ -38,6 +40,17 @@ val kotlinxSerializer = KotlinxSerializer( Json { ignoreUnknownKeys = true }) +fun isInternetAvailable(): Boolean { + return runBlocking { + try { + ktorHttpClient.head("http://google.com") + true + } catch (e: Exception) { + println(e.message) + false + } + } +} fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient { install(JsonFeature) { this.serializer = serializer @@ -48,4 +61,5 @@ fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = level = LogLevel.INFO } } -} \ No newline at end of file +} +val ktorHttpClient = HttpClient {} diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt index b92f4fe8..89defefb 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt @@ -1,5 +1,6 @@ package com.shabinder.common +import androidx.compose.runtime.Composable import co.touchlab.kermit.Kermit import com.shabinder.common.utils.removeIllegalChars @@ -9,6 +10,7 @@ expect fun shareApp() expect fun giveDonation() +expect fun downloadTracks(list: List) expect open class Dir( logger: Kermit diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/FetchPlatformQueryResult.kt index cf711413..4ad22ec4 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/FetchPlatformQueryResult.kt @@ -1,8 +1,46 @@ package com.shabinder.common -//TODO -class FetchPlatformQueryResult { +import com.shabinder.common.database.DownloadRecordDatabaseQueries +import com.shabinder.common.providers.GaanaProvider +import com.shabinder.common.providers.SpotifyProvider +import com.shabinder.database.Database +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class FetchPlatformQueryResult( + private val gaanaProvider: GaanaProvider, + private val spotifyProvider: SpotifyProvider, + private val youtubeProvider: YoutubeProvider, + private val database: Database +) { + private val db:DownloadRecordDatabaseQueries + get() = database.downloadRecordDatabaseQueries + suspend fun query(link:String): PlatformQueryResult?{ - return null + val result = when{ + //SPOTIFY + link.contains("spotify",true) -> + spotifyProvider.query(link) + + //YOUTUBE + link.contains("youtube.com",true) || link.contains("youtu.be",true) -> + youtubeProvider.query(link) + + //GAANA + link.contains("gaana",true) -> + gaanaProvider.query(link) + + else -> { + null + } + } + result?.run { + withContext(Dispatchers.Default){ + db.add( + folderType, title, link, coverUrl, trackList.size.toLong() + ) + } + } + return result } } \ No newline at end of file diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt similarity index 81% rename from common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt rename to common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt index be4a00fa..84ca723c 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt @@ -3,6 +3,22 @@ package com.shabinder.common import co.touchlab.kermit.Kermit import java.io.File +actual fun openPlatform(platformID:String ,platformLink:String){ + //TODO +} + +actual fun shareApp(){ + //TODO +} + +actual fun giveDonation(){ + //TODO +} + +actual fun downloadTracks(list: List){ + //TODO +} + actual open class Dir actual constructor(private val logger: Kermit) { actual fun fileSeparator(): String = File.separator