mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 01:04:31 +01:00
Multiplatform Settings & Desktop App Fixes. Release Soon!
This commit is contained in:
parent
5118aa10c4
commit
b8c2ee5b47
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
||||||
)
|
)
|
||||||
|
@ -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 { }
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()) }
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
.removeAllTags()
|
|
||||||
.setId3v1Tags(trackDetails)
|
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
".mp3" -> {
|
||||||
|
Mp3File(File(songFile.absolutePath))
|
||||||
|
.removeAllTags()
|
||||||
|
.setId3v1Tags(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) {}
|
||||||
|
|
||||||
|
@ -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)!!
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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*/}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user