mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
SpotiFlyerList and more porting
This commit is contained in:
parent
97f9606863
commit
c7c61e51d6
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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<State>
|
||||
|
||||
/*
|
||||
* For Single Track Download -> list(that track)
|
||||
* For Download All -> Model.tracks
|
||||
* Download All Tracks(after filtering already Downloaded)
|
||||
* */
|
||||
fun onDownloadClicked(trackList:List<TrackDetails>)
|
||||
fun onDownloadAllClicked(trackList:List<TrackDetails>)
|
||||
/*
|
||||
* Download All Tracks(after filtering already Downloaded)
|
||||
* */
|
||||
fun onDownloadClicked(wholeTrackList:List<TrackDetails>,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<Output>
|
||||
}
|
||||
sealed class Output {
|
||||
object Finished : Output()
|
||||
}
|
||||
data class State(
|
||||
val result:PlatformQueryResult? = null,
|
||||
val queryResult:PlatformQueryResult? = null,
|
||||
val link:String = ""
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -20,15 +20,19 @@ internal class SpotiFlyerListImpl(
|
||||
instanceKeeper.getStore {
|
||||
SpotiFlyerListStoreProvider(
|
||||
storeFactory = storeFactory,
|
||||
database = database,
|
||||
fetchQuery = fetchQuery,
|
||||
link = link
|
||||
).provide()
|
||||
}
|
||||
|
||||
override val models: Flow<State> = store.states
|
||||
|
||||
override fun onDownloadClicked(trackList: List<TrackDetails>) {
|
||||
store.accept(Intent.StartDownload(trackList))
|
||||
override fun onDownloadAllClicked(trackList: List<TrackDetails>) {
|
||||
store.accept(Intent.StartDownloadAll(trackList))
|
||||
}
|
||||
|
||||
override fun onDownloadClicked(wholeTrackList: List<TrackDetails>, trackIndex: Int) {
|
||||
store.accept(Intent.StartDownload(wholeTrackList,trackIndex))
|
||||
}
|
||||
|
||||
override fun onBackPressed(){
|
||||
|
@ -9,7 +9,8 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.*
|
||||
|
||||
internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> {
|
||||
sealed class Intent {
|
||||
data class StartDownload(val trackList: List<TrackDetails>): Intent()
|
||||
data class StartDownloadAll(val trackList: List<TrackDetails>): Intent()
|
||||
data class StartDownload(val wholeTrackList: List<TrackDetails>, val trackIndex:Int): Intent()
|
||||
data class SearchLink(val link: String): Intent()
|
||||
}
|
||||
}
|
||||
|
@ -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<TrackDetails>): Result()
|
||||
}
|
||||
|
||||
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
|
||||
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<State, Result> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -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<RouterState<*, Child>>
|
||||
|
||||
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)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Configuration, Child>(
|
||||
initialConfiguration = Configuration.Main,
|
||||
handleBackButton = true,
|
||||
componentFactory = ::createChild
|
||||
)
|
||||
|
||||
override val routerState: Value<RouterState<*, Child>> = 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<SpotiFlyerMain.Output> = 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<SpotiFlyerList.Output> =
|
||||
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()
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<PlatformQueryResult?>(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<TrackDetails>,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) }
|
||||
}
|
@ -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
|
||||
){
|
||||
|
||||
}
|
@ -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<TrackDetails>){
|
||||
//TODO
|
||||
}
|
||||
|
||||
actual open class Dir actual constructor(logger: Kermit) {
|
||||
|
||||
private val context:Context
|
@ -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<String>("http://google.com")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
||||
install(JsonFeature) {
|
||||
this.serializer = serializer
|
||||
@ -49,3 +62,4 @@ fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer =
|
||||
}
|
||||
}
|
||||
}
|
||||
val ktorHttpClient = HttpClient {}
|
||||
|
@ -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<TrackDetails>)
|
||||
|
||||
expect open class Dir(
|
||||
logger: Kermit
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<TrackDetails>){
|
||||
//TODO
|
||||
}
|
||||
|
||||
actual open class Dir actual constructor(private val logger: Kermit) {
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
Loading…
Reference in New Issue
Block a user