Multiplatform Settings & Desktop App Fixes. Release Soon!

This commit is contained in:
shabinder 2021-05-08 02:16:50 +05:30
parent 5118aa10c4
commit b8c2ee5b47
18 changed files with 218 additions and 66 deletions

View File

@ -39,6 +39,7 @@ import com.shabinder.common.database.R
import com.shabinder.common.di.Picture import com.shabinder.common.di.Picture
import com.shabinder.common.di.dispatcherIO import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Composable @Composable
@ -138,8 +139,7 @@ actual fun RazorPay() = painterResource(R.drawable.ic_indian_rupee)
@Composable @Composable
actual fun Toast( actual fun Toast(
text: String, flow: MutableStateFlow<String>,
visibility: MutableState<Boolean>,
duration: ToastDuration duration: ToastDuration
) { ) {
// We Have Android's Implementation of Toast so its just Empty // We Have Android's Implementation of Toast so its just Empty

View File

@ -18,6 +18,8 @@
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Spring.StiffnessLow import androidx.compose.animation.core.Spring.StiffnessLow
import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDp
@ -26,6 +28,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -37,8 +40,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -48,6 +55,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.extensions.compose.jetbrains.Children import com.arkivanov.decompose.extensions.compose.jetbrains.Children
import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.crossfadeScale import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.crossfadeScale
import com.arkivanov.decompose.extensions.compose.jetbrains.asState
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Child
import com.shabinder.common.uikit.splash.Splash import com.shabinder.common.uikit.splash.Splash
@ -93,6 +101,10 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, modifier: Modifier = Modifi
contentTopPadding, contentTopPadding,
component component
) )
Toast(
flow = component.toastState,
duration = ToastDuration.Long
)
} }
return component return component
} }
@ -112,9 +124,13 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
).then(modifier) ).then(modifier)
) { ) {
val activeComponent = component.routerState.asState()
val callBacks = component.callBacks
AppBar( AppBar(
backgroundColor = appBarColor, backgroundColor = appBarColor,
setDownloadDirectory = component.callBacks::setDownloadDirectory, onBackPressed = callBacks::popBackToHomeScreen ,
setDownloadDirectory = callBacks::setDownloadDirectory,
isBackButtonVisible = activeComponent.value.activeChild.instance is Child.List,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.padding(top = topPadding)) Spacer(Modifier.padding(top = topPadding))
@ -130,16 +146,28 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
} }
} }
@OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun AppBar( fun AppBar(
backgroundColor: Color, backgroundColor: Color,
onBackPressed:()->Unit,
setDownloadDirectory:()->Unit, setDownloadDirectory:()->Unit,
isBackButtonVisible: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
TopAppBar( TopAppBar(
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
AnimatedVisibility(isBackButtonVisible) {
Icon(
Icons.Rounded.ArrowBackIosNew,
contentDescription = "Back Button",
modifier = Modifier.clickable { onBackPressed() },
tint = Color.LightGray
)
Spacer(Modifier.padding(horizontal = 4.dp))
}
Image( Image(
SpotiFlyerLogo(), SpotiFlyerLogo(),
"SpotiFlyer Logo", "SpotiFlyer Logo",

View File

@ -19,14 +19,14 @@ package com.shabinder.common.uikit
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.MutableStateFlow
enum class ToastDuration(val value: Int) { enum class ToastDuration(val value: Int) {
Short(1000), Long(3000) Short(1000), Long(2500)
} }
@Composable @Composable
expect fun Toast( expect fun Toast(
text: String, flow: MutableStateFlow<String>,
visibility: MutableState<Boolean> = mutableStateOf(false), duration: ToastDuration
duration: ToastDuration = ToastDuration.Long
) )

View File

@ -16,67 +16,76 @@
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.dp
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private val message: MutableState<String> = mutableStateOf("") @OptIn(ExperimentalAnimationApi::class)
private val state: MutableState<Boolean> = mutableStateOf(false)
fun showToast(text: String) {
message.value = text
state.value = true
}
private var isShown: Boolean = false
@Composable @Composable
actual fun Toast( actual fun Toast(
text: String, flow: MutableStateFlow<String>,
visibility: MutableState<Boolean>,
duration: ToastDuration duration: ToastDuration
) { ) {
if (isShown) {
return
}
if (visibility.value) { val state = flow.collectAsState("")
isShown = true val message = state.value
AnimatedVisibility (
visible = message != "",
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 4 }),
exit = slideOutHorizontally(targetOffsetX = { it / 4 }) + fadeOut()
) {
Box( Box(
modifier = Modifier.fillMaxSize().padding(bottom = 20.dp), modifier = Modifier.fillMaxSize().padding(bottom = 16.dp).padding(end = 16.dp),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomEnd
) { ) {
Surface( Surface(
modifier = Modifier.size(300.dp, 70.dp), modifier = Modifier.sizeIn(maxWidth = 250.dp,maxHeight = 80.dp),
color = Color(23, 23, 23), color = Color(23, 23, 23),
shape = RoundedCornerShape(4.dp) shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, colorOffWhite)
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){
Text( Text(
text = text, text = message,
color = Color(210, 210, 210) color = Color(210, 210, 210),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
style = SpotiFlyerTypography.body2,
modifier = Modifier.padding(8.dp)
) )
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
GlobalScope.launch { GlobalScope.launch {
delay(duration.value.toLong()) delay(duration.value.toLong())
isShown = false flow.value = ""
visibility.value = false
} }
onDispose { } onDispose { }
} }

View File

@ -32,6 +32,7 @@ kotlin {
implementation("org.jetbrains.kotlinx:atomicfu:0.16.1") implementation("org.jetbrains.kotlinx:atomicfu:0.16.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.0")
implementation("com.shabinder.fuzzywuzzy:fuzzywuzzy:1.0") implementation("com.shabinder.fuzzywuzzy:fuzzywuzzy:1.0")
implementation("com.russhwolf:multiplatform-settings-no-arg:0.7.6")
implementation(Extras.youtubeDownloader) implementation(Extras.youtubeDownloader)
implementation(MVIKotlin.rx) implementation(MVIKotlin.rx)
} }
@ -49,7 +50,6 @@ kotlin {
dependencies { dependencies {
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
implementation(Extras.mp3agic) implementation(Extras.mp3agic)
//implementation(Extras.jaudioTagger)
} }
} }
jsMain { jsMain {

View File

@ -16,13 +16,13 @@
package com.shabinder.common.di package com.shabinder.common.di
import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Environment import android.os.Environment
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
@ -40,18 +40,14 @@ import java.net.URL
actual class Dir actual constructor( actual class Dir actual constructor(
private val logger: Kermit, private val logger: Kermit,
private val settings: Settings,
private val spotiFlyerDatabase: SpotiFlyerDatabase, private val spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
companion object { companion object {
const val DirKey = "downloadDir" const val DirKey = "downloadDir"
} }
// This Wont throw `NPE` as We will never pass null actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
private val sharedPreferences:SharedPreferences by lazy { methods.value.platformActions.sharedPreferences!! }
fun setDownloadDirectory(newBasePath:String){
sharedPreferences.edit().putString(DirKey,newBasePath).apply()
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
@ -61,8 +57,8 @@ actual class Dir actual constructor(
actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
// fun call in order to always access Updated Value // fun call in order to always access Updated Value
actual fun defaultDir(): String = sharedPreferences.getString(DirKey,defaultBaseDir)!! + File.separator + actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
"SpotiFlyer" + File.separator File.separator + "SpotiFlyer" + File.separator
actual fun isPresent(path: String): Boolean = File(path).exists() actual fun isPresent(path: String): Boolean = File(path).exists()

View File

@ -18,6 +18,7 @@ package com.shabinder.common.di
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import co.touchlab.stately.ensureNeverFrozen import co.touchlab.stately.ensureNeverFrozen
import com.russhwolf.settings.Settings
import com.shabinder.common.database.databaseModule import com.shabinder.common.database.databaseModule
import com.shabinder.common.database.getLogger import com.shabinder.common.database.getLogger
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
@ -51,7 +52,8 @@ fun initKoin() = initKoin(enableNetworkLogs = false) { }
fun commonModule(enableNetworkLogs: Boolean) = module { fun commonModule(enableNetworkLogs: Boolean) = module {
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) } single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
single { Dir(get(), get()) } single { Dir(get(), get(), get()) }
single { Settings() }
single { Kermit(getLogger()) } single { Kermit(getLogger()) }
single { TokenStore(get(), get()) } single { TokenStore(get(), get()) }
single { YoutubeMusic(get(), get()) } single { YoutubeMusic(get(), get()) }

View File

@ -17,6 +17,8 @@
package com.shabinder.common.di package com.shabinder.common.di
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SettingsListener
import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
@ -32,6 +34,7 @@ import kotlin.math.roundToInt
expect class Dir ( expect class Dir (
logger: Kermit, logger: Kermit,
settings: Settings,
spotiFlyerDatabase: SpotiFlyerDatabase, spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
val db: Database? val db: Database?
@ -40,6 +43,7 @@ expect class Dir (
fun defaultDir(): String fun defaultDir(): String
fun imageCacheDir(): String fun imageCacheDir(): String
fun createDirectory(dirPath: String) fun createDirectory(dirPath: String)
fun setDownloadDirectory(newBasePath:String)
suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage
suspend fun loadImage(url: String): Picture suspend fun loadImage(url: String): Picture
suspend fun clearCache() suspend fun clearCache()

View File

@ -74,10 +74,15 @@ suspend fun downloadTrack(
youtubeMp3: YoutubeMp3 youtubeMp3: YoutubeMp3
) { ) {
try { try {
var link = youtubeMp3.getMp3DownloadLink(videoID) val link = youtubeMp3.getMp3DownloadLink(videoID) ?: ytDownloader.getVideo(videoID).getData()?.url
if (link == null) { if (link == null) {
link = ytDownloader.getVideo(videoID).getData()?.url ?: return DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
)
return
} }
downloadFile(link).collect { downloadFile(link).collect {
when (it) { when (it) {

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database import com.shabinder.database.Database
@ -39,8 +40,12 @@ import javax.imageio.ImageIO
actual class Dir actual constructor( actual class Dir actual constructor(
private val logger: Kermit, private val logger: Kermit,
private val settings: Settings,
private val spotiFlyerDatabase: SpotiFlyerDatabase, private val spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
companion object {
const val DirKey = "downloadDir"
}
init { init {
createDirectories() createDirectories()
@ -51,9 +56,13 @@ actual class Dir actual constructor(
actual fun imageCacheDir(): String = System.getProperty("user.home") + actual fun imageCacheDir(): String = System.getProperty("user.home") +
fileSeparator() + "SpotiFlyer/.images" + fileSeparator() fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() + private val defaultBaseDir = System.getProperty("user.home")!!
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
"SpotiFlyer" + fileSeparator() "SpotiFlyer" + fileSeparator()
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
actual fun isPresent(path: String): Boolean = File(path).exists() actual fun isPresent(path: String): Boolean = File(path).exists()
actual fun createDirectory(dirPath: String) { actual fun createDirectory(dirPath: String) {
@ -88,13 +97,68 @@ actual class Dir actual constructor(
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess:(track: TrackDetails)->Unit
) { ) {
val file = File(trackDetails.outputFilePath) val songFile = File(trackDetails.outputFilePath)
file.writeBytes(mp3ByteArray) try {
/*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */
if(!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile.mkdirs()
}
Mp3File(file) if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> {
Mp3File(File(songFile.absolutePath))
.removeAllTags() .removeAllTags()
.setId3v1Tags(trackDetails) .setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails) .setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
}
".m4a" -> {
/*FFmpeg.executeAsync(
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
){ _, returnCode ->
when (returnCode) {
Config.RETURN_CODE_SUCCESS -> {
//FFMPEG task Completed
logger.d{ "Async command execution completed successfully." }
scope.launch {
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
}
}
Config.RETURN_CODE_CANCEL -> {
logger.d{"Async command execution cancelled by user."}
}
else -> {
logger.d { "Async command execution failed with rc=$returnCode" }
}
}
}*/
}
else -> {
try {
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
} catch (e: Exception) { e.printStackTrace() }
}
}
}catch (e:Exception){
withContext(Dispatchers.Main){
//Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
}
if(songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" }
}
} }
actual fun addToLibrary(path: String) {} actual fun addToLibrary(path: String) {}

View File

@ -1,6 +1,7 @@
package com.shabinder.common.di package com.shabinder.common.di
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database import com.shabinder.database.Database
@ -20,18 +21,28 @@ import platform.Foundation.sendSynchronousRequest
import platform.Foundation.writeToFile import platform.Foundation.writeToFile
import platform.UIKit.UIImage import platform.UIKit.UIImage
import platform.UIKit.UIImageJPEGRepresentation import platform.UIKit.UIImageJPEGRepresentation
import java.lang.System
actual class Dir actual constructor( actual class Dir actual constructor(
val logger: Kermit, val logger: Kermit,
private val settings: Settings,
private val spotiFlyerDatabase: SpotiFlyerDatabase, private val spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
companion object {
const val DirKey = "downloadDir"
}
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path) actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
actual fun fileSeparator(): String = "/" actual fun fileSeparator(): String = "/"
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!!.path!!
// TODO Error Handling // TODO Error Handling
actual fun defaultDir(): String = defaultDirURL.path!! + fileSeparator() actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
fileSeparator() + "SpotiFlyer" + fileSeparator()
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
private val defaultDirURL: NSURL by lazy { private val defaultDirURL: NSURL by lazy {
val musicDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!! val musicDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!!

View File

@ -17,6 +17,7 @@
package com.shabinder.common.di package com.shabinder.common.di
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.di.gaana.corsApi import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.di.utils.removeIllegalChars
@ -33,8 +34,12 @@ import org.w3c.dom.ImageBitmap
actual class Dir actual constructor( actual class Dir actual constructor(
private val logger: Kermit, private val logger: Kermit,
private val settings: Settings,
private val spotiFlyerDatabase: SpotiFlyerDatabase, private val spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
companion object {
const val DirKey = "downloadDir"
}
/*init { /*init {
createDirectories() createDirectories()

View File

@ -94,21 +94,22 @@ internal class SpotiFlyerListStoreProvider(
} }
is Intent.StartDownloadAll -> { is Intent.StartDownloadAll -> {
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isNullOrEmpty()) methods.value.showPopUpMessage("All Songs are Processed")
else downloadTracks(finalList, fetchQuery, dir)
val list = intent.trackList.map { val list = intent.trackList.map {
if (it.downloaded == DownloadStatus.NotDownloaded) if (it.downloaded == DownloadStatus.NotDownloaded)
return@map it.copy(downloaded = DownloadStatus.Queued) return@map it.copy(downloaded = DownloadStatus.Queued)
it it
} }
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))) dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isNullOrEmpty()) methods.value.showPopUpMessage("All Songs are Processed")
else downloadTracks(finalList, fetchQuery, dir)
} }
is Intent.StartDownload -> { is Intent.StartDownload -> {
downloadTracks(listOf(intent.track), fetchQuery, dir)
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued))) dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
downloadTracks(listOf(intent.track), fetchQuery, dir)
} }
is Intent.RefreshTracksStatuses -> methods.value.queryActiveTracks() is Intent.RefreshTracksStatuses -> methods.value.queryActiveTracks()
} }

View File

@ -30,12 +30,16 @@ import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.root.integration.SpotiFlyerRootImpl import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.database.Database import com.shabinder.database.Database
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
interface SpotiFlyerRoot { interface SpotiFlyerRoot {
val routerState: Value<RouterState<*, Child>> val routerState: Value<RouterState<*, Child>>
val toastState: MutableStateFlow<String>
val callBacks: SpotiFlyerRootCallBacks val callBacks: SpotiFlyerRootCallBacks
sealed class Child { sealed class Child {

View File

@ -18,6 +18,7 @@ package com.shabinder.common.root.callbacks
interface SpotiFlyerRootCallBacks { interface SpotiFlyerRootCallBacks {
fun searchLink(link: String) fun searchLink(link: String)
fun showToast(text:String)
fun popBackToHomeScreen() fun popBackToHomeScreen()
fun setDownloadDirectory() fun setDownloadDirectory()
} }

View File

@ -42,6 +42,7 @@ import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
internal class SpotiFlyerRootImpl( internal class SpotiFlyerRootImpl(
@ -91,6 +92,8 @@ internal class SpotiFlyerRootImpl(
override val routerState: Value<RouterState<*, Child>> = router.state override val routerState: Value<RouterState<*, Child>> = router.state
override val toastState = MutableStateFlow("")
override val callBacks = object : SpotiFlyerRootCallBacks { override val callBacks = object : SpotiFlyerRootCallBacks {
override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link)) override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link))
override fun popBackToHomeScreen() { override fun popBackToHomeScreen() {
@ -98,6 +101,7 @@ internal class SpotiFlyerRootImpl(
it !is Configuration.Main it !is Configuration.Main
} }
} }
override fun showToast(text:String) { toastState.value = text }
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() } override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
} }

View File

@ -40,10 +40,16 @@ kotlin {
implementation(project(":common:compose")) implementation(project(":common:compose"))
implementation(project(":common:data-models")) implementation(project(":common:data-models"))
implementation(project(":common:root")) implementation(project(":common:root"))
// Decompose
implementation(Decompose.decompose) implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose) implementation(Decompose.extensionsCompose)
// MVI
implementation(MVIKotlin.mvikotlin) implementation(MVIKotlin.mvikotlin)
implementation(MVIKotlin.mvikotlinMain) implementation(MVIKotlin.mvikotlinMain)
// Koin
implementation(Koin.core)
} }
} }
val jvmTest by getting val jvmTest by getting
@ -55,6 +61,7 @@ compose.desktop {
mainClass = "MainKt" mainClass = "MainKt"
description = "Music Downloader for Spotify, Gaana, Youtube Music." description = "Music Downloader for Spotify, Gaana, Youtube Music."
nativeDistributions { nativeDistributions {
modules("java.sql", "java.security.jgss")
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "SpotiFlyer" packageName = "SpotiFlyer"
} }

View File

@ -40,12 +40,12 @@ import com.shabinder.common.uikit.SpotiFlyerRootContent
import com.shabinder.common.uikit.SpotiFlyerShapes import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorOffWhite import com.shabinder.common.uikit.colorOffWhite
import com.shabinder.common.uikit.showToast
import com.shabinder.database.Database import com.shabinder.database.Database
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
private val koin = initKoin(enableNetworkLogs = true).koin private val koin = initKoin(enableNetworkLogs = true).koin
private lateinit var showToast: (String)->Unit
fun main() { fun main() {
@ -63,7 +63,8 @@ fun main() {
typography = SpotiFlyerTypography, typography = SpotiFlyerTypography,
shapes = SpotiFlyerShapes shapes = SpotiFlyerShapes
) { ) {
SpotiFlyerRootContent(rememberRootComponent(factory = ::spotiFlyerRoot)) val root = SpotiFlyerRootContent(rememberRootComponent(factory = ::spotiFlyerRoot))
showToast = root.callBacks::showToast
} }
} }
} }
@ -81,17 +82,27 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
override val actions = object: Actions { override val actions = object: Actions {
override val platformActions = object : PlatformActions {} override val platformActions = object : PlatformActions {}
override fun showPopUpMessage(string: String, long: Boolean) = showToast(string) override fun showPopUpMessage(string: String, long: Boolean) {
if(::showToast.isInitialized){
showToast(string)
}
}
override fun setDownloadDirectoryAction() {} override fun setDownloadDirectoryAction() {
showToast("TODO: Still needs to be Implemented")
}
override fun queryActiveTracks() {} override fun queryActiveTracks() {}
override fun giveDonation() {} override fun giveDonation() {
}
override fun shareApp() {} override fun shareApp() {}
override fun openPlatform(packageID: String, platformLink: String) {} override fun openPlatform(packageID: String, platformLink: String) {
showToast("TODO: Still needs to be Implemented")
}
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/} override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}