SpotiFlyerList and more porting

This commit is contained in:
shabinder 2021-02-03 01:35:12 +05:30
parent 97f9606863
commit c7c61e51d6
20 changed files with 500 additions and 284 deletions

View File

@ -22,6 +22,7 @@ android {
versionCode = Versions.versionCode versionCode = Versions.versionCode
versionName = Versions.versionName versionName = Versions.versionName
} }
buildTypes { buildTypes {
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
@ -29,6 +30,7 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
} }
compileOptions { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled = true //coreLibraryDesugaringEnabled = true

View File

@ -18,6 +18,12 @@ kotlin {
//implementation(Badoo.Reaktive.reaktive) //implementation(Badoo.Reaktive.reaktive)
implementation(Decompose.decompose) implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose) implementation(Decompose.extensionsCompose)
//Coil-Image Loading
Versions.coilVersion.let{
implementation("dev.chrisbanes.accompanist:accompanist-coil:$it")
implementation("dev.chrisbanes.accompanist:accompanist-insets:$it")
}
} }
} }
} }

View File

@ -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
)
}

View File

@ -2,9 +2,11 @@ package com.shabinder.common.list
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.FetchPlatformQueryResult
import com.shabinder.common.PlatformQueryResult import com.shabinder.common.PlatformQueryResult
import com.shabinder.common.TrackDetails import com.shabinder.common.TrackDetails
import com.shabinder.common.list.integration.SpotiFlyerListImpl import com.shabinder.common.list.integration.SpotiFlyerListImpl
import com.shabinder.common.utils.Consumer
import com.shabinder.database.Database import com.shabinder.database.Database
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -13,10 +15,13 @@ interface SpotiFlyerList {
val models: Flow<State> val models: Flow<State>
/* /*
* For Single Track Download -> list(that track) * Download All Tracks(after filtering already Downloaded)
* For Download All -> Model.tracks
* */ * */
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 * To Pop and return back to Main Screen
@ -25,15 +30,15 @@ interface SpotiFlyerList {
interface Dependencies { interface Dependencies {
val storeFactory: StoreFactory val storeFactory: StoreFactory
val database: Database val fetchQuery: FetchPlatformQueryResult
val link: String val link: String
fun listOutput(finished: Output.Finished) fun listOutput(finished: Output.Finished): Consumer<Output>
} }
sealed class Output { sealed class Output {
object Finished : Output() object Finished : Output()
} }
data class State( data class State(
val result:PlatformQueryResult? = null, val queryResult:PlatformQueryResult? = null,
val link:String = "" val link:String = ""
) )
} }

View File

@ -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
)
}

View File

@ -20,15 +20,19 @@ internal class SpotiFlyerListImpl(
instanceKeeper.getStore { instanceKeeper.getStore {
SpotiFlyerListStoreProvider( SpotiFlyerListStoreProvider(
storeFactory = storeFactory, storeFactory = storeFactory,
database = database, fetchQuery = fetchQuery,
link = link link = link
).provide() ).provide()
} }
override val models: Flow<State> = store.states override val models: Flow<State> = store.states
override fun onDownloadClicked(trackList: List<TrackDetails>) { override fun onDownloadAllClicked(trackList: List<TrackDetails>) {
store.accept(Intent.StartDownload(trackList)) store.accept(Intent.StartDownloadAll(trackList))
}
override fun onDownloadClicked(wholeTrackList: List<TrackDetails>, trackIndex: Int) {
store.accept(Intent.StartDownload(wholeTrackList,trackIndex))
} }
override fun onBackPressed(){ override fun onBackPressed(){

View File

@ -9,7 +9,8 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.*
internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> { internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> {
sealed class Intent { 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() data class SearchLink(val link: String): Intent()
} }
} }

View File

@ -2,18 +2,13 @@ package com.shabinder.common.list.store
import com.arkivanov.mvikotlin.core.store.* import com.arkivanov.mvikotlin.core.store.*
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.FetchPlatformQueryResult import com.shabinder.common.*
import com.shabinder.common.PlatformQueryResult
import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent 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( internal class SpotiFlyerListStoreProvider(
private val storeFactory: StoreFactory, private val storeFactory: StoreFactory,
private val database: Database, private val fetchQuery: FetchPlatformQueryResult,
private val link: String private val link: String
) { ) {
fun provide(): SpotiFlyerListStore = fun provide(): SpotiFlyerListStore =
@ -28,21 +23,45 @@ internal class SpotiFlyerListStoreProvider(
private sealed class Result { private sealed class Result {
data class ResultFetched(val result: PlatformQueryResult) : Result() data class ResultFetched(val result: PlatformQueryResult) : Result()
data class SearchLink(val link: String) : 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>() { private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
override suspend fun executeAction(action: Unit, getState: () -> State) { override suspend fun executeAction(action: Unit, getState: () -> State) {
FetchPlatformQueryResult().query(link)?.let{ fetchQuery.query(link)?.let{
dispatch(Result.ResultFetched(it)) dispatch(Result.ResultFetched(it))
} }
} }
override suspend fun executeIntent(intent: Intent, getState: () -> State) { override suspend fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) {//TODO: Add Dispatchers where needed when (intent) {//TODO: Add Dispatchers where needed
is Intent.StartDownload -> {}//TODO() is Intent.SearchLink -> fetchQuery.query(link)?.let{
is Intent.SearchLink -> FetchPlatformQueryResult().query(link)?.let{
dispatch((Result.ResultFetched(it))) 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> { private object ReducerImpl : Reducer<State, Result> {
override fun State.reduce(result: Result): State = override fun State.reduce(result: Result): State =
when (result) { when (result) {
is Result.ResultFetched -> copy(result = result.result) is Result.ResultFetched -> copy(queryResult = result.result)
is Result.SearchLink -> copy(link = result.link) is Result.SearchLink -> copy(link = result.link)
is Result.UpdateTrackList -> copy(queryResult = this.queryResult?.apply { trackList = result.list })
} }
} }
} }

View File

@ -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())
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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
)

View File

@ -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) }
}

View File

@ -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
){
}

View File

@ -6,6 +6,22 @@ import co.touchlab.kermit.Kermit
import com.shabinder.common.database.appContext import com.shabinder.common.database.appContext
import java.io.File 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) { actual open class Dir actual constructor(logger: Kermit) {
private val context:Context private val context:Context

View File

@ -10,6 +10,8 @@ import io.ktor.client.*
import io.ktor.client.features.json.* import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.* import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.* import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.KoinAppDeclaration
@ -38,6 +40,17 @@ val kotlinxSerializer = KotlinxSerializer( Json {
ignoreUnknownKeys = true 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 { fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
install(JsonFeature) { install(JsonFeature) {
this.serializer = serializer this.serializer = serializer
@ -49,3 +62,4 @@ fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer =
} }
} }
} }
val ktorHttpClient = HttpClient {}

View File

@ -1,5 +1,6 @@
package com.shabinder.common package com.shabinder.common
import androidx.compose.runtime.Composable
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.utils.removeIllegalChars import com.shabinder.common.utils.removeIllegalChars
@ -9,6 +10,7 @@ expect fun shareApp()
expect fun giveDonation() expect fun giveDonation()
expect fun downloadTracks(list: List<TrackDetails>)
expect open class Dir( expect open class Dir(
logger: Kermit logger: Kermit

View File

@ -1,8 +1,46 @@
package com.shabinder.common package com.shabinder.common
//TODO import com.shabinder.common.database.DownloadRecordDatabaseQueries
class FetchPlatformQueryResult { 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?{ 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
} }
} }

View File

@ -3,6 +3,22 @@ package com.shabinder.common
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import java.io.File 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 open class Dir actual constructor(private val logger: Kermit) {
actual fun fileSeparator(): String = File.separator actual fun fileSeparator(): String = File.separator