TrackList Ui Fix

This commit is contained in:
shabinder 2021-02-21 19:51:43 +05:30
parent 50fa91fbca
commit 80e6ecf1f3
13 changed files with 118 additions and 54 deletions

View File

@ -19,6 +19,7 @@ import com.shabinder.android.utils.requestStoragePermission
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.createDirectories
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRootContent
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
@ -26,6 +27,8 @@ import com.shabinder.common.ui.SpotiFlyerTheme
import com.shabinder.common.ui.colorOffWhite
import com.shabinder.database.Database
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.android.ext.android.inject
const val disableDozeCode = 1223
@ -38,6 +41,9 @@ class MainActivity : ComponentActivity() {
private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks
get() = root.callBacks
//TODO pass updates from Foreground Service
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -66,6 +72,7 @@ class MainActivity : ComponentActivity() {
override val database = this@MainActivity.database
override val fetchPlatformQueryResult = this@MainActivity.fetcher
override val directories: Dir = this@MainActivity.dir
override val downloadProgressReport: StateFlow<HashMap<String, DownloadStatus>> = downloadFlow
}
)

View File

@ -1,16 +1,20 @@
package com.shabinder.common.list
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.ImageBitmap
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.list.integration.SpotiFlyerListImpl
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.spotify.Source
import com.shabinder.common.utils.Consumer
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerList {
@ -20,10 +24,11 @@ interface SpotiFlyerList {
* Download All Tracks(after filtering already Downloaded)
* */
fun onDownloadAllClicked(trackList:List<TrackDetails>)
/*
* Download All Tracks(after filtering already Downloaded)
* */
fun onDownloadClicked(wholeTrackList:List<TrackDetails>, trackIndex:Int)
fun onDownloadClicked(track:TrackDetails)
/*
* To Pop and return back to Main Screen
@ -41,6 +46,7 @@ interface SpotiFlyerList {
val dir: Dir
val link: String
val listOutput: Consumer<Output>
val downloadProgressFlow: StateFlow<HashMap<String,DownloadStatus>>
}
sealed class Output {
object Finished : Output()
@ -50,7 +56,8 @@ interface SpotiFlyerList {
"","",
"Loading","", emptyList(),
Source.Spotify),
val link:String = ""
val link:String = "",
val trackList:SnapshotStateList<TrackDetails> = mutableStateListOf()
)
}

View File

@ -23,6 +23,7 @@ import com.shabinder.common.ui.colorAccent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -32,6 +33,7 @@ fun SpotiFlyerListContent(
modifier: Modifier = Modifier
) {
val model by component.models.collectAsState(SpotiFlyerList.State())
val coroutineScope = rememberCoroutineScope()
Box(modifier = modifier.fillMaxSize()) {
@ -44,10 +46,10 @@ fun SpotiFlyerListContent(
item {
CoverImage(result.title, result.coverUrl, coroutineScope,component::loadImage)
}
itemsIndexed(result.trackList) { index, item ->
itemsIndexed(model.trackList) { index, item ->
TrackCard(
track = item,
downloadTrack = { component.onDownloadClicked(result.trackList,index) },
downloadTrack = { component.onDownloadClicked(item) },
loadImage = component::loadImage
)
}
@ -88,22 +90,22 @@ fun TrackCard(
}
}
when(track.downloaded){
DownloadStatus.Downloaded -> {
is DownloadStatus.Downloaded -> {
DownloadImageTick()
}
DownloadStatus.Queued -> {
is DownloadStatus.Queued -> {
CircularProgressIndicator()
}
DownloadStatus.Failed -> {
is DownloadStatus.Failed -> {
DownloadImageError()
}
DownloadStatus.Downloading -> {
CircularProgressIndicator(progress = track.progress.toFloat()/100f)
is DownloadStatus.Downloading -> {
CircularProgressIndicator(progress = (track.downloaded as DownloadStatus.Downloading).progress.toFloat()/100f)
}
DownloadStatus.Converting -> {
is DownloadStatus.Converting -> {
CircularProgressIndicator(progress = 100f,color = colorAccent)
}
DownloadStatus.NotDownloaded -> {
is DownloadStatus.NotDownloaded -> {
DownloadImageArrow(Modifier.clickable(onClick = {
downloadTrack()
}))

View File

@ -23,7 +23,8 @@ internal class SpotiFlyerListImpl(
dir = this.dir,
storeFactory = storeFactory,
fetchQuery = fetchQuery,
link = link
link = link,
downloadProgressFlow = downloadProgressFlow
).provide()
}
@ -33,8 +34,8 @@ internal class SpotiFlyerListImpl(
store.accept(Intent.StartDownloadAll(trackList))
}
override fun onDownloadClicked(wholeTrackList: List<TrackDetails>, trackIndex: Int) {
store.accept(Intent.StartDownload(wholeTrackList,trackIndex))
override fun onDownloadClicked(track:TrackDetails) {
store.accept(Intent.StartDownload(track))
}
override fun onBackPressed(){

View File

@ -7,8 +7,8 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.*
internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> {
sealed class 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 StartDownload(val track:TrackDetails): Intent()
data class StartDownloadAll(val trackList: List<TrackDetails>): Intent()
}
}

View File

@ -1,5 +1,7 @@
package com.shabinder.common.list.store
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import com.arkivanov.mvikotlin.core.store.*
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.di.Dir
@ -11,12 +13,15 @@ import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.ui.showPopUpMessage
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
internal class SpotiFlyerListStoreProvider(
private val dir: Dir,
private val storeFactory: StoreFactory,
private val fetchQuery: FetchPlatformQueryResult,
private val link: String
private val link: String,
private val downloadProgressFlow: StateFlow<HashMap<String, DownloadStatus>>
) {
fun provide(): SpotiFlyerListStore =
object : SpotiFlyerListStore, Store<Intent, State, Nothing> by storeFactory.create(
@ -28,23 +33,29 @@ 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()
data class ResultFetched(val result: PlatformQueryResult,val trackList: SnapshotStateList<TrackDetails>) : Result()
data class UpdateTrackList(val list:SnapshotStateList<TrackDetails>): Result()
data class UpdateTrackItem(val item:TrackDetails): Result()
}
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
override suspend fun executeAction(action: Unit, getState: () -> State) {
fetchQuery.query(link)?.let{
dispatch(Result.ResultFetched(it))
executeIntent(Intent.SearchLink(link),getState)
downloadProgressFlow.collectLatest { map ->
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
if(updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
}
}
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) {//TODO: Add Dispatchers where needed
is Intent.SearchLink -> fetchQuery.query(link)?.let{
dispatch((Result.ResultFetched(it)))
when (intent) {
is Intent.SearchLink -> fetchQuery.query(link)?.let{ result ->
result.trackList = result.trackList.toMutableList()
dispatch((Result.ResultFetched(result,result.trackList.toMutableList().updateTracksStatuses(downloadProgressFlow.value))))
}
is Intent.StartDownloadAll -> {
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
@ -57,17 +68,11 @@ internal class SpotiFlyerListStoreProvider(
}
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))
dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))
}
is Intent.StartDownload -> {
dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued }))
}
}
}
@ -76,9 +81,31 @@ internal class SpotiFlyerListStoreProvider(
private object ReducerImpl : Reducer<State, Result> {
override fun State.reduce(result: Result): State =
when (result) {
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 })
is Result.ResultFetched -> copy(queryResult = result.result, trackList = result.trackList ,link = link)
is Result.UpdateTrackList -> copy(trackList = result.list)
is Result.UpdateTrackItem -> updateTrackItem(result.item)
}
private fun State.updateTrackItem(item: TrackDetails):State{
val position = this.trackList.map { it.title }.indexOf(item.title)
if(position != -1){
return copy(trackList = trackList.apply { set(position,item) })
}
return this
}
}
}
private fun MutableList<TrackDetails>.updateTracksStatuses(map:HashMap<String,DownloadStatus>):SnapshotStateList<TrackDetails>{
val titleList = this.map { it.title }
val newStateList = mutableStateListOf<TrackDetails>()
for(newTrack in map){
titleList.indexOf(newTrack.key).let { position ->
this.getOrNull(position)?.apply { downloaded = newTrack.value }?.also { updatedTrack ->
this[position] = updatedTrack
}
}
}
newStateList.addAll(this)
return newStateList
}

View File

@ -8,11 +8,13 @@ import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.database.Database
import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.common.utils.Consumer
import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerRoot {
@ -30,6 +32,7 @@ interface SpotiFlyerRoot {
val database: Database
val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir
val downloadProgressReport: StateFlow<HashMap<String,DownloadStatus>>
}
}

View File

@ -11,11 +11,13 @@ import com.arkivanov.decompose.value.Value
import com.shabinder.common.di.Dir
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Child
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.utils.Consumer
import kotlinx.coroutines.flow.StateFlow
internal class SpotiFlyerRootImpl(
componentContext: ComponentContext,
@ -58,6 +60,7 @@ internal class SpotiFlyerRootImpl(
override val dir: Dir = directories
override val link: String = link
override val listOutput : Consumer<SpotiFlyerList.Output> = Consumer(::onListOutput)
override val downloadProgressFlow = downloadProgressReport
}
)

View File

@ -72,16 +72,15 @@ actual class Dir actual constructor(
@Suppress("BlockingMethodInNonBlockingContext")
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
path: String,
trackDetails: TrackDetails
) {
val file = File(path)
val file = File(trackDetails.outputFilePath)
file.writeBytes(mp3ByteArray)
Mp3File(file)
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path)
.setId3v2TagsAndSaveFile(trackDetails)
}
actual suspend fun loadImage(url: String): ImageBitmap? {

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import co.touchlab.kermit.Logger
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
@ -13,7 +14,7 @@ import kotlin.math.absoluteValue
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
class YoutubeMusic constructor(
private val logger: Logger,
private val logger: Kermit,
private val httpClient:HttpClient,
) {
private val tag = "YT Music"
@ -166,7 +167,7 @@ class YoutubeMusic constructor(
}
}
}
logger.i(youtubeTracks.joinToString(" abc \n"),tag)
//logger.d(youtubeTracks.joinToString(" abc \n"),tag)
return youtubeTracks
}

View File

@ -7,9 +7,7 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
actual fun openPlatform(packageID:String, platformLink:String){
@ -24,7 +22,7 @@ actual fun giveDonation(){
//TODO
}
val DownloadProgressFlow = MutableStateFlow(Pair<String,DownloadStatus>("",DownloadStatus.Queued))
val DownloadProgressFlow: MutableStateFlow<HashMap<String,DownloadStatus>> = MutableStateFlow(hashMapOf())
actual suspend fun downloadTracks(
list: List<TrackDetails>,
@ -38,7 +36,7 @@ actual suspend fun downloadTracks(
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = getYTIDBestMatch(searchQuery,it)
if (videoId.isNullOrBlank()) {
DownloadProgressFlow.emit(Pair(it.title,DownloadStatus.Failed))
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(it.title,DownloadStatus.Failed) })
} else {//Found Youtube Video ID
downloadTrack(videoId, it,saveFileWithMetaData)
}
@ -46,7 +44,7 @@ actual suspend fun downloadTracks(
}
}
val ytDownloader = YoutubeDownloader()
private val ytDownloader = YoutubeDownloader()
suspend fun downloadTrack(
videoID: String,
@ -61,14 +59,14 @@ suspend fun downloadTrack(
downloadFile(url).collect {
when(it){
is DownloadResult.Error -> {
//TODO()
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Failed) })
}
is DownloadResult.Progress -> {
DownloadProgressFlow.emit(Pair(trackDetails.title,DownloadStatus.Downloading(it.progress)))
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) })
}
is DownloadResult.Success -> {
is DownloadResult.Success -> {//Todo clear map
saveFileWithMetaData(it.byteArray,trackDetails)
DownloadProgressFlow.emit(Pair(trackDetails.title,DownloadStatus.Downloaded))
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloaded) })
}
}
}

View File

@ -17,6 +17,10 @@ import javax.imageio.ImageIO
actual class Dir actual constructor(private val logger: Kermit) {
init {
createDirectories()
}
actual fun fileSeparator(): String = File.separator
actual fun imageCacheDir(): String = System.getProperty("user.home") +

View File

@ -2,6 +2,7 @@ import androidx.compose.desktop.DesktopMaterialTheme
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.arkivanov.decompose.ComponentContext
@ -10,6 +11,7 @@ import com.arkivanov.mvikotlin.core.lifecycle.LifecycleRegistry
import com.arkivanov.mvikotlin.core.lifecycle.resume
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.DownloadProgressFlow
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.initKoin
import com.shabinder.common.root.SpotiFlyerRoot
@ -19,6 +21,9 @@ import com.shabinder.common.ui.SpotiFlyerShapes
import com.shabinder.common.ui.SpotiFlyerTypography
import com.shabinder.common.ui.colorOffWhite
import com.shabinder.database.Database
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
private val koin = initKoin(enableNetworkLogs = true).koin
@ -38,7 +43,13 @@ fun main(){
typography = SpotiFlyerTypography,
shapes = SpotiFlyerShapes
) {
SpotiFlyerRootContent(rootComponent(factory = ::spotiFlyerRoot))
val callBacks = SpotiFlyerRootContent(rootComponent(factory = ::spotiFlyerRoot)).callBacks
val scope = rememberCoroutineScope()
scope.launch {
DownloadProgressFlow.collect {
}
}
}
}
}
@ -52,5 +63,6 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
override val database: Database = koin.get()
override val fetchPlatformQueryResult: FetchPlatformQueryResult = koin.get()
override val directories: Dir = koin.get()
override val downloadProgressReport = DownloadProgressFlow
}
)