mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 09:54:33 +01:00
Image Compression Android, Jio-Saavn Complete,Code Cleanup
This commit is contained in:
parent
3913bfa4b1
commit
cceb6d04dc
@ -22,6 +22,7 @@
|
|||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.gaana" />
|
<package android:name="com.gaana" />
|
||||||
<package android:name="com.spotify.music" />
|
<package android:name="com.spotify.music" />
|
||||||
|
<package android:name="com.jio.media.jiobeats" />
|
||||||
<package android:name="com.google.android.youtube" />
|
<package android:name="com.google.android.youtube" />
|
||||||
<package android:name="com.google.android.apps.youtube.music" />
|
<package android:name="com.google.android.apps.youtube.music" />
|
||||||
</queries>
|
</queries>
|
||||||
|
@ -41,7 +41,7 @@ fun AnalyticsDialog(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Rounded.Insights,"Analytics", Modifier.size(52.dp))
|
Icon(Icons.Rounded.Insights,"Analytics", Modifier.size(52.dp))
|
||||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
Text("Grant Analytics Access",style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center)
|
Text("Grant Analytics",style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor = Color.DarkGray,
|
backgroundColor = Color.DarkGray,
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// App's Version (To be bumped at each update)
|
// App's Version (To be bumped at each update)
|
||||||
const val versionName = "3.0.1"
|
const val versionName = "3.1.0"
|
||||||
|
const val versionCode = 20
|
||||||
|
|
||||||
// Kotlin
|
// Kotlin
|
||||||
const val kotlinVersion = "1.4.32"
|
const val kotlinVersion = "1.4.32"
|
||||||
@ -45,7 +46,6 @@ object Versions {
|
|||||||
const val slf4j = "1.7.30"
|
const val slf4j = "1.7.30"
|
||||||
|
|
||||||
// Android
|
// Android
|
||||||
const val versionCode = 19
|
|
||||||
const val minSdkVersion = 21
|
const val minSdkVersion = 21
|
||||||
const val compileSdkVersion = 29
|
const val compileSdkVersion = 29
|
||||||
const val targetSdkVersion = 29
|
const val targetSdkVersion = 29
|
||||||
|
@ -43,9 +43,11 @@ kotlin {
|
|||||||
implementation(MVIKotlin.coroutines)
|
implementation(MVIKotlin.coroutines)
|
||||||
implementation(MVIKotlin.mvikotlin)
|
implementation(MVIKotlin.mvikotlin)
|
||||||
|
|
||||||
|
implementation(compose.ui)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material)
|
implementation(compose.material)
|
||||||
|
implementation(compose.animation)
|
||||||
|
|
||||||
implementation(Extras.kermit)
|
implementation(Extras.kermit)
|
||||||
implementation("dev.icerock.moko:parcelize:0.6.1")
|
implementation("dev.icerock.moko:parcelize:0.6.1")
|
||||||
|
@ -28,6 +28,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(compose.material)
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(project(":common:root"))
|
implementation(project(":common:root"))
|
||||||
implementation(project(":common:main"))
|
implementation(project(":common:main"))
|
||||||
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.withContext
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun ImageLoad(
|
actual fun ImageLoad(
|
||||||
link: String,
|
link: String,
|
||||||
loader: suspend (String) -> Picture,
|
loader: suspend () -> Picture,
|
||||||
desc: String,
|
desc: String,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
// placeholder: ImageVector
|
// placeholder: ImageVector
|
||||||
@ -27,7 +27,7 @@ actual fun ImageLoad(
|
|||||||
|
|
||||||
LaunchedEffect(link) {
|
LaunchedEffect(link) {
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
pic = loader(link).image
|
pic = loader().image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +82,9 @@ actual fun HeartIcon() = painterResource(R.drawable.ic_heart)
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun SpotifyLogo() = painterResource(R.drawable.ic_spotify_logo)
|
actual fun SpotifyLogo() = painterResource(R.drawable.ic_spotify_logo)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun SaavnLogo() = painterResource(R.drawable.ic_jio_saavn_logo)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GaanaLogo() = painterResource(R.drawable.ic_gaana)
|
actual fun GaanaLogo() = painterResource(R.drawable.ic_gaana)
|
||||||
|
|
||||||
@ -97,6 +100,9 @@ actual fun GithubLogo() = painterResource(R.drawable.ic_github)
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun PaypalLogo() = painterResource(R.drawable.ic_paypal_logo)
|
actual fun PaypalLogo() = painterResource(R.drawable.ic_paypal_logo)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun OpenCollectiveLogo() = painterResource(R.drawable.ic_opencollective_icon)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun RazorPay() = painterResource(R.drawable.ic_indian_rupee)
|
actual fun RazorPay() = painterResource(R.drawable.ic_indian_rupee)
|
||||||
|
|
||||||
|
@ -4,15 +4,19 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
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.material.ButtonDefaults
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -26,7 +30,8 @@ import com.shabinder.common.models.methods
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun DonationDialog(
|
actual fun DonationDialog(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit,
|
||||||
|
onSnooze: () -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
isVisible
|
isVisible
|
||||||
@ -39,13 +44,36 @@ actual fun DonationDialog(
|
|||||||
) {
|
) {
|
||||||
Column(Modifier.padding(16.dp)) {
|
Column(Modifier.padding(16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
"Support Us",
|
"We Need Your Support!",
|
||||||
style = SpotiFlyerTypography.h5,
|
style = SpotiFlyerTypography.h5,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = colorAccent,
|
color = colorAccent,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(vertical = 4.dp))
|
Spacer(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().clickable(
|
||||||
|
onClick = {
|
||||||
|
onDismiss()
|
||||||
|
methods.value.openPlatform("", "https://opencollective.com/spotiflyer/donate")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(OpenCollectiveLogo(), "Open Collective Logo", Modifier.size(24.dp), tint = Color(0xFFCCCCCC))
|
||||||
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Open Collective",
|
||||||
|
style = SpotiFlyerTypography.h6
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Worldwide Donations",
|
||||||
|
style = SpotiFlyerTypography.subtitle2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().clickable(
|
modifier = Modifier.fillMaxWidth().clickable(
|
||||||
@ -56,7 +84,7 @@ actual fun DonationDialog(
|
|||||||
)
|
)
|
||||||
.padding(vertical = 6.dp)
|
.padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(PaypalLogo(), "Paypal Logo", tint = Color(0xFFCCCCCC))
|
Icon(PaypalLogo(), "Paypal Logo", Modifier.size(24.dp), tint = Color(0xFFCCCCCC))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -79,7 +107,7 @@ actual fun DonationDialog(
|
|||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(RazorPay(), "Indian Rupee Logo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
|
Icon(RazorPay(), "Indian Rupee Logo", Modifier.size(24.dp), tint = Color(0xFFCCCCCC))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -92,39 +120,21 @@ actual fun DonationDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*AlertDialog(
|
|
||||||
buttons = {
|
|
||||||
*//* TextButton({
|
|
||||||
//Retry Network Connection
|
|
||||||
},
|
|
||||||
Modifier.padding(bottom = 16.dp,start = 16.dp,end = 16.dp).fillMaxWidth().background(Color(0xFFFC5C7D),shape = RoundedCornerShape(size = 8.dp)).padding(horizontal = 8.dp),
|
|
||||||
){
|
|
||||||
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
|
|
||||||
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
|
|
||||||
}
|
|
||||||
*//*},
|
|
||||||
*//*title = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
"Support Us",
|
|
||||||
style = SpotiFlyerTypography.h5,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = colorAccent,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.padding(vertical = 16.dp))
|
Spacer(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = onSnooze) {
|
||||||
|
Text("Dismiss.")
|
||||||
|
}
|
||||||
|
TextButton(onClick = onDismiss, colors = ButtonDefaults.buttonColors()) {
|
||||||
|
Text("Remind Later!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},*//*
|
|
||||||
backgroundColor = Color.DarkGray,
|
|
||||||
text = {
|
|
||||||
|
|
||||||
}
|
|
||||||
,shape = SpotiFlyerShapes.medium,
|
|
||||||
onDismissRequest = onDismiss
|
|
||||||
)*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import com.shabinder.common.di.Picture
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun ImageLoad(
|
expect fun ImageLoad(
|
||||||
link: String,
|
link: String,
|
||||||
loader: suspend (String) -> Picture,
|
loader: suspend () -> Picture,
|
||||||
desc: String = "Album Art",
|
desc: String = "Album Art",
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// placeholder:Painter = PlaceHolderImage()
|
// placeholder:Painter = PlaceHolderImage()
|
@ -39,6 +39,9 @@ expect fun SpotiFlyerLogo(): Painter
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun SpotifyLogo(): Painter
|
expect fun SpotifyLogo(): Painter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun SaavnLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun YoutubeLogo(): Painter
|
expect fun YoutubeLogo(): Painter
|
||||||
|
|
||||||
@ -54,6 +57,9 @@ expect fun GithubLogo(): Painter
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun PaypalLogo(): Painter
|
expect fun PaypalLogo(): Painter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun OpenCollectiveLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun RazorPay(): Painter
|
expect fun RazorPay(): Painter
|
||||||
|
|
||||||
@ -69,5 +75,6 @@ expect fun DownloadImageArrow(modifier: Modifier)
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun DonationDialog(
|
expect fun DonationDialog(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit,
|
||||||
|
onSnooze: () -> Unit
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.ExtendedFloatingActionButton
|
import androidx.compose.material.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@ -39,6 +40,9 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -54,6 +58,7 @@ import com.shabinder.common.models.DownloadStatus
|
|||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerListContent(
|
fun SpotiFlyerListContent(
|
||||||
component: SpotiFlyerList,
|
component: SpotiFlyerList,
|
||||||
@ -68,7 +73,6 @@ fun SpotiFlyerListContent(
|
|||||||
component.onBackPressed()
|
component.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
val result = model.queryResult
|
val result = model.queryResult
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@ -92,16 +96,34 @@ fun SpotiFlyerListContent(
|
|||||||
TrackCard(
|
TrackCard(
|
||||||
track = item,
|
track = item,
|
||||||
downloadTrack = { component.onDownloadClicked(item) },
|
downloadTrack = { component.onDownloadClicked(item) },
|
||||||
loadImage = component::loadImage
|
loadImage = { component.loadImage(item.albumArtURL) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
// Donation Dialog Visibility
|
||||||
|
var visibilty by remember { mutableStateOf(false) }
|
||||||
|
DonationDialog(
|
||||||
|
isVisible = visibilty,
|
||||||
|
onDismiss = {
|
||||||
|
visibilty = false
|
||||||
|
},
|
||||||
|
onSnooze = {
|
||||||
|
visibilty = false
|
||||||
|
component.snoozeDonationDialog()
|
||||||
|
}
|
||||||
|
)
|
||||||
DownloadAllButton(
|
DownloadAllButton(
|
||||||
onClick = { component.onDownloadAllClicked(model.trackList) },
|
onClick = {
|
||||||
|
component.onDownloadAllClicked(model.trackList)
|
||||||
|
// Check If we are allowed to show donation Dialog
|
||||||
|
if (model.askForDonation) {
|
||||||
|
// Show Donation Dialog
|
||||||
|
visibilty = true
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,12 +143,12 @@ fun SpotiFlyerListContent(
|
|||||||
fun TrackCard(
|
fun TrackCard(
|
||||||
track: TrackDetails,
|
track: TrackDetails,
|
||||||
downloadTrack: () -> Unit,
|
downloadTrack: () -> Unit,
|
||||||
loadImage: suspend (String) -> Picture
|
loadImage: suspend () -> Picture
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
track.albumArtURL,
|
track.albumArtURL,
|
||||||
loadImage,
|
{ loadImage() },
|
||||||
"Album Art",
|
"Album Art",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(70.dp)
|
.width(70.dp)
|
||||||
@ -177,7 +199,7 @@ fun TrackCard(
|
|||||||
fun CoverImage(
|
fun CoverImage(
|
||||||
title: String,
|
title: String,
|
||||||
coverURL: String,
|
coverURL: String,
|
||||||
loadImage: suspend (String) -> Picture,
|
loadImage: suspend (URL: String, isCover: Boolean) -> Picture,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -186,7 +208,7 @@ fun CoverImage(
|
|||||||
) {
|
) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
coverURL,
|
coverURL,
|
||||||
loadImage,
|
{ loadImage(coverURL, true) },
|
||||||
"Cover Image",
|
"Cover Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
|
@ -256,7 +256,16 @@ fun AboutColumn(
|
|||||||
"Open Gaana",
|
"Open Gaana",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { methods.value.openPlatform("com.gaana", "http://gaana.com") }
|
onClick = { methods.value.openPlatform("com.gaana", "https://www.gaana.com") }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
|
Icon(
|
||||||
|
SaavnLogo(),
|
||||||
|
"Open Jio Saavn",
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = { methods.value.openPlatform("com.jio.media.jiobeats", "https://www.jiosaavn.com/") }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 16.dp))
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
@ -265,7 +274,7 @@ fun AboutColumn(
|
|||||||
"Open Youtube",
|
"Open Youtube",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { methods.value.openPlatform("com.google.android.youtube", "http://m.youtube.com") }
|
onClick = { methods.value.openPlatform("com.google.android.youtube", "https://m.youtube.com") }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 12.dp))
|
Spacer(modifier = modifier.padding(start = 12.dp))
|
||||||
@ -299,7 +308,7 @@ fun AboutColumn(
|
|||||||
)
|
)
|
||||||
.padding(vertical = 6.dp)
|
.padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(GithubLogo(), "Open Project Repo", tint = Color(0xFFCCCCCC))
|
Icon(GithubLogo(), "Open Project Repo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -337,6 +346,9 @@ fun AboutColumn(
|
|||||||
isDonationDialogVisible,
|
isDonationDialogVisible,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
isDonationDialogVisible = false
|
isDonationDialogVisible = false
|
||||||
|
},
|
||||||
|
onSnooze = {
|
||||||
|
isDonationDialogVisible = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -350,7 +362,7 @@ fun AboutColumn(
|
|||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
|
Icon(Icons.Rounded.CardGiftcard, "Support Developer", Modifier.size(32.dp))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -373,7 +385,7 @@ fun AboutColumn(
|
|||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Share, "Share SpotiFlyer App")
|
Icon(Icons.Rounded.Share, "Share SpotiFlyer App", Modifier.size(32.dp))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -455,7 +467,7 @@ fun DownloadRecordItem(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
item.coverUrl,
|
item.coverUrl,
|
||||||
loadImage,
|
{ loadImage(item.coverUrl) },
|
||||||
"Album Art",
|
"Album Art",
|
||||||
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
|
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.withContext
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun ImageLoad(
|
actual fun ImageLoad(
|
||||||
link: String,
|
link: String,
|
||||||
loader: suspend (String) -> Picture,
|
loader: suspend () -> Picture,
|
||||||
desc: String,
|
desc: String,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
// placeholder: ImageVector
|
// placeholder: ImageVector
|
||||||
@ -26,7 +26,7 @@ actual fun ImageLoad(
|
|||||||
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
LaunchedEffect(link) {
|
LaunchedEffect(link) {
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
pic = loader(link).image
|
pic = loader().image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +84,9 @@ actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_he
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun SpotifyLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_spotify_logo.xml")) as Painter
|
actual fun SpotifyLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_spotify_logo.xml")) as Painter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun SaavnLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_jio_saavn_logo.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
|
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
|
||||||
|
|
||||||
@ -99,5 +102,8 @@ actual fun GithubLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_g
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun PaypalLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_paypal_logo.xml")) as Painter
|
actual fun PaypalLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_paypal_logo.xml")) as Painter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun OpenCollectiveLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_opencollective_icon")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun RazorPay() = rememberVectorPainter(vectorXmlResource("drawable/ic_indian_rupee.xml")) as Painter
|
actual fun RazorPay() = rememberVectorPainter(vectorXmlResource("drawable/ic_indian_rupee.xml")) as Painter
|
||||||
|
@ -27,7 +27,8 @@ import com.shabinder.common.models.methods
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun DonationDialog(
|
actual fun DonationDialog(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit,
|
||||||
|
onSnooze: () -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
isVisible
|
isVisible
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<vector android:height="42dp" android:viewportHeight="250"
|
||||||
|
android:viewportWidth="488" android:width="81.984dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M483.73,36A53.1,53.1 0,0 0,452 4.28C438.49,0 425.94,0 400.84,0H325.16C300.07,0 287.52,0 274,4.28A53.08,53.08 0,0 0,242.28 36a76.64,76.64 0,0 0,-2 7.74,140.32 140.32,0 0,1 14,24.86c0.38,-9.57 1.27,-17.22 3.46,-24.14 4.68,-12.86 11.88,-20.06 24.74,-24.74C294.25,16 308.12,16 330,16h66c21.88,0 35.76,0 47.54,3.73 12.86,4.68 20,11.88 24.74,24.74C472,56.25 472,70.13 472,92v66c0,21.88 0,35.76 -3.72,47.53 -4.69,12.86 -11.88,20.06 -24.74,24.74C431.76,234 417.88,234 396,234H330c-21.89,0 -35.76,0 -47.54,-3.73 -12.86,-4.68 -20.06,-11.88 -24.74,-24.74 -2.19,-6.92 -3.09,-14.58 -3.46,-24.15a140.51,140.51 0,0 1,-14 24.85,77.18 77.18,0 0,0 2,7.77A53.08,53.08 0,0 0,274 245.73C287.52,250 300.07,250 325.16,250h75.68c25.1,0 37.65,0 51.16,-4.27A53.11,53.11 0,0 0,483.73 214C488,200.49 488,187.94 488,162.84V87.17C488,62.07 488,49.52 483.73,36Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M422,217L380.33,217c-1.76,0 -5.83,-2.79 -2.63,-6.67 21.36,-23 48,-30.93 73.4,-39.42 3.32,-1 3.91,2.51 3.91,3.48v8.68C455,202.61 441.57,217 422,217ZM343.73,212.69c-4,-29.73 -18.06,-80.79 -71,-118.55A3.78,3.78 0,0 1,271 90.63L271,66.36c0,-26.69 18,-33.31 26.37,-33.31a4.3,4.3 0,0 1,4.07 2.1c25.24,55 41,89.86 50.7,172.83 0.05,1.62 0.31,2.39 1.28,0 6.86,-15.07 39.35,-92 26.44,-170.68a3.64,3.64 0,0 1,3.5 -4.25L422,33.05c19.54,0 33,13.43 33,33.36L455,100.5a3.63,3.63 0,0 1,-2.07 3.36,180.12 180.12,0 0,0 -90.3,109.25c-0.79,2.21 -1.25,3.9 -3.71,3.9h-11.8C344.77,217 344.27,216.05 343.73,212.7ZM304.35,217c-20,0 -33.35,-12.37 -33.35,-33.93v-2.24c0,-0.9 0.71,-4.29 4.09,-3.63 20.24,6.23 41.92,12.52 57.77,33.49 1.82,2.56 0.23,6.3 -2.91,6.31Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M124.991,239.991a115,115 54.655,1 0,2.007 -229.991a115,115 54.655,1 0,-2.007 229.991z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M180.77,114.59c-8.62,0 -15.61,7.39 -15.61,16.49s7,16.5 15.61,16.5 15.62,-7.38 15.62,-16.5S189.4,114.59 180.77,114.59Z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M125,0A125,125 0,1 0,250 125,125 125,0 0,0 125,0ZM95.37,132.09c0,63.82 -101.74,35.68 -60.49,2.93 9.65,13.39 28.18,12.5 30.15,-0.72l0.37,-52.05c0.95,-13.32 26.85,-16 30,0ZM133.31,156.32a12.05,12.05 0,0 1,-12 12L116.1,168.32a12.05,12.05 0,0 1,-12 -12L104.1,106a12,12 0,0 1,12 -12h5.21a12,12 0,0 1,12 12ZM133.31,74.56a11.84,11.84 0,0 1,-11.79 11.79L115.9,86.35a11.84,11.84 0,0 1,-11.81 -11.79L104.09,71.65A11.83,11.83 0,0 1,115.9 59.86h5.62a11.82,11.82 0,0 1,11.79 11.79ZM180.77,169.9c-22,0 -39.82,-17.37 -39.82,-38.82s17.84,-38.81 39.82,-38.81 39.81,17.38 39.81,38.81S202.76,169.9 180.77,169.9Z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,8 @@
|
|||||||
|
<vector android:height="42dp" android:viewportHeight="250"
|
||||||
|
android:viewportWidth="488" android:width="81.984dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M483.73,36A53.1,53.1 0,0 0,452 4.28C438.49,0 425.94,0 400.84,0H325.16C300.07,0 287.52,0 274,4.28A53.08,53.08 0,0 0,242.28 36a76.64,76.64 0,0 0,-2 7.74,140.32 140.32,0 0,1 14,24.86c0.38,-9.57 1.27,-17.22 3.46,-24.14 4.68,-12.86 11.88,-20.06 24.74,-24.74C294.25,16 308.12,16 330,16h66c21.88,0 35.76,0 47.54,3.73 12.86,4.68 20,11.88 24.74,24.74C472,56.25 472,70.13 472,92v66c0,21.88 0,35.76 -3.72,47.53 -4.69,12.86 -11.88,20.06 -24.74,24.74C431.76,234 417.88,234 396,234H330c-21.89,0 -35.76,0 -47.54,-3.73 -12.86,-4.68 -20.06,-11.88 -24.74,-24.74 -2.19,-6.92 -3.09,-14.58 -3.46,-24.15a140.51,140.51 0,0 1,-14 24.85,77.18 77.18,0 0,0 2,7.77A53.08,53.08 0,0 0,274 245.73C287.52,250 300.07,250 325.16,250h75.68c25.1,0 37.65,0 51.16,-4.27A53.11,53.11 0,0 0,483.73 214C488,200.49 488,187.94 488,162.84V87.17C488,62.07 488,49.52 483.73,36Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M422,217L380.33,217c-1.76,0 -5.83,-2.79 -2.63,-6.67 21.36,-23 48,-30.93 73.4,-39.42 3.32,-1 3.91,2.51 3.91,3.48v8.68C455,202.61 441.57,217 422,217ZM343.73,212.69c-4,-29.73 -18.06,-80.79 -71,-118.55A3.78,3.78 0,0 1,271 90.63L271,66.36c0,-26.69 18,-33.31 26.37,-33.31a4.3,4.3 0,0 1,4.07 2.1c25.24,55 41,89.86 50.7,172.83 0.05,1.62 0.31,2.39 1.28,0 6.86,-15.07 39.35,-92 26.44,-170.68a3.64,3.64 0,0 1,3.5 -4.25L422,33.05c19.54,0 33,13.43 33,33.36L455,100.5a3.63,3.63 0,0 1,-2.07 3.36,180.12 180.12,0 0,0 -90.3,109.25c-0.79,2.21 -1.25,3.9 -3.71,3.9h-11.8C344.77,217 344.27,216.05 343.73,212.7ZM304.35,217c-20,0 -33.35,-12.37 -33.35,-33.93v-2.24c0,-0.9 0.71,-4.29 4.09,-3.63 20.24,6.23 41.92,12.52 57.77,33.49 1.82,2.56 0.23,6.3 -2.91,6.31Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M124.991,239.991a115,115 54.655,1 0,2.007 -229.991a115,115 54.655,1 0,-2.007 229.991z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M180.77,114.59c-8.62,0 -15.61,7.39 -15.61,16.49s7,16.5 15.61,16.5 15.62,-7.38 15.62,-16.5S189.4,114.59 180.77,114.59Z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M125,0A125,125 0,1 0,250 125,125 125,0 0,0 125,0ZM95.37,132.09c0,63.82 -101.74,35.68 -60.49,2.93 9.65,13.39 28.18,12.5 30.15,-0.72l0.37,-52.05c0.95,-13.32 26.85,-16 30,0ZM133.31,156.32a12.05,12.05 0,0 1,-12 12L116.1,168.32a12.05,12.05 0,0 1,-12 -12L104.1,106a12,12 0,0 1,12 -12h5.21a12,12 0,0 1,12 12ZM133.31,74.56a11.84,11.84 0,0 1,-11.79 11.79L115.9,86.35a11.84,11.84 0,0 1,-11.81 -11.79L104.09,71.65A11.83,11.83 0,0 1,115.9 59.86h5.62a11.82,11.82 0,0 1,11.79 11.79ZM180.77,169.9c-22,0 -39.82,-17.37 -39.82,-38.82s17.84,-38.81 39.82,-38.81 39.81,17.38 39.81,38.81S202.76,169.9 180.77,169.9Z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:viewportHeight="64"
|
||||||
|
android:viewportWidth="64" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#BBFFFFFF" android:fillType="evenOdd" android:pathData="M52.402,31.916c0,4.03 -1.17,7.895 -3.178,11.087l8.196,8.23c4.014,-5.375 6.523,-12.094 6.523,-19.318s-2.51,-13.942 -6.523,-19.318l-8.196,8.23c2.007,3.192 3.178,6.887 3.178,11.087z"/>
|
||||||
|
<path android:fillColor="#FFFFFF" android:fillType="evenOdd" android:pathData="M32.004,52.41c-11.207,0 -20.406,-9.24 -20.406,-20.493s9.2,-20.493 20.406,-20.493c4.182,0 7.86,1.176 11.04,3.36l8.196,-8.23C45.887,2.52 39.197,0 32.004,0 14.44,0 0.057,14.278 0.057,32.084S14.44,64 32.004,64c7.36,0 14.05,-2.52 19.403,-6.55l-8.196,-8.23c-3.178,2.016 -7.025,3.192 -11.207,3.192z"/>
|
||||||
|
</vector>
|
@ -24,17 +24,15 @@ import co.touchlab.kermit.Kermit
|
|||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||||
|
import com.shabinder.common.di.utils.ParallelExecutor
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
@ -45,33 +43,9 @@ import java.net.URL
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val settings: Settings,
|
settingsPref: Settings,
|
||||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val DirKey = "downloadDir"
|
|
||||||
const val AnalyticsKey = "analytics"
|
|
||||||
const val firstLaunch = "firstLaunch"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
|
|
||||||
|
|
||||||
actual fun firstLaunchDone() {
|
|
||||||
settings.putBoolean(firstLaunch, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Do we have Analytics Permission?
|
|
||||||
* - Defaults to `False`
|
|
||||||
* */
|
|
||||||
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
|
||||||
|
|
||||||
actual fun enableAnalytics() {
|
|
||||||
settings.putBoolean(AnalyticsKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
||||||
|
|
||||||
@ -171,20 +145,17 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
|
actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO) {
|
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture = withContext(dispatcherIO) {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = imageCacheDir() + getNameURL(url)
|
||||||
Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap())
|
Picture(image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage(url, reqWidth, reqHeight))?.asImageBitmap())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCachedImage(cachePath: String): Bitmap? {
|
private fun loadCachedImage(cachePath: String, reqWidth: Int, reqHeight: Int): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
BitmapFactory.decodeFile(cachePath)
|
getMemoryEfficientBitmap(cachePath, reqWidth, reqHeight)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
} catch (e: OutOfMemoryError) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,27 +171,36 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
private suspend fun freshImage(url: String): Bitmap? = withContext(dispatcherIO) {
|
private suspend fun freshImage(url: String, reqWidth: Int, reqHeight: Int): Bitmap? = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val source = URL(url)
|
val source = URL(url)
|
||||||
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
||||||
connection.connectTimeout = 5000
|
connection.connectTimeout = 5000
|
||||||
connection.connect()
|
connection.connect()
|
||||||
|
|
||||||
val input: InputStream = connection.inputStream
|
val input: ByteArray = connection.inputStream.readBytes()
|
||||||
val result: Bitmap? = BitmapFactory.decodeStream(input)
|
|
||||||
|
|
||||||
if (result != null) {
|
// Get Memory Efficient Bitmap
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
val bitmap: Bitmap? = getMemoryEfficientBitmap(input, reqWidth, reqHeight)
|
||||||
cacheImage(result, imageCacheDir() + getNameURL(url))
|
|
||||||
|
parallelExecutor.execute {
|
||||||
|
// Decode and Cache Full Sized Image in Background
|
||||||
|
cacheImage(BitmapFactory.decodeByteArray(input, 0, input.size), imageCacheDir() + getNameURL(url))
|
||||||
}
|
}
|
||||||
result
|
bitmap // return Memory Efficient Bitmap
|
||||||
} else null
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parallel Executor with 4 concurrent operation at a time.
|
||||||
|
* - We will use this to queue up operations and decode Full Sized Images
|
||||||
|
* - Will Decode Only 4 at a time , to avoid going into `Out of Memory`
|
||||||
|
* */
|
||||||
|
private val parallelExecutor = ParallelExecutor(Dispatchers.IO)
|
||||||
|
|
||||||
actual val db: Database? = spotiFlyerDatabase.instance
|
actual val db: Database? = spotiFlyerDatabase.instance
|
||||||
|
actual val settings: Settings = settingsPref
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,67 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
actual data class Picture(
|
actual data class Picture(
|
||||||
var image: ImageBitmap?
|
var image: ImageBitmap?
|
||||||
)
|
)
|
||||||
|
fun getMemoryEfficientBitmap(
|
||||||
|
input: ByteArray,
|
||||||
|
reqWidth: Int,
|
||||||
|
reqHeight: Int,
|
||||||
|
offset: Int = 0,
|
||||||
|
size: Int = input.size
|
||||||
|
): Bitmap? {
|
||||||
|
return BitmapFactory.Options().run {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeByteArray(input, offset, size, this)
|
||||||
|
|
||||||
|
// Calculate inSampleSize
|
||||||
|
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
||||||
|
|
||||||
|
// Decode bitmap with inSampleSize set
|
||||||
|
inJustDecodeBounds = false
|
||||||
|
// Return Mem. Efficient Bitmap
|
||||||
|
BitmapFactory.decodeByteArray(input, offset, size, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getMemoryEfficientBitmap(
|
||||||
|
filePath: String,
|
||||||
|
reqWidth: Int,
|
||||||
|
reqHeight: Int,
|
||||||
|
): Bitmap? {
|
||||||
|
return BitmapFactory.Options().run {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeFile(filePath, this)
|
||||||
|
|
||||||
|
// Calculate inSampleSize
|
||||||
|
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
||||||
|
|
||||||
|
// Decode bitmap with inSampleSize set
|
||||||
|
inJustDecodeBounds = false
|
||||||
|
|
||||||
|
BitmapFactory.decodeFile(filePath, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||||
|
// Raw height and width of image
|
||||||
|
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||||
|
var inSampleSize = 1
|
||||||
|
|
||||||
|
if (height > reqHeight || width > reqWidth) {
|
||||||
|
|
||||||
|
val halfHeight: Int = height / 2
|
||||||
|
val halfWidth: Int = width / 2
|
||||||
|
|
||||||
|
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
|
// height and width larger than the requested height and width.
|
||||||
|
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||||
|
inSampleSize *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inSampleSize
|
||||||
|
}
|
||||||
|
@ -32,29 +32,72 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
const val DirKey = "downloadDir"
|
||||||
|
const val AnalyticsKey = "analytics"
|
||||||
|
const val FirstLaunch = "firstLaunch"
|
||||||
|
const val DonationInterval = "donationInterval"
|
||||||
|
|
||||||
expect class Dir(
|
expect class Dir(
|
||||||
logger: Kermit,
|
logger: Kermit,
|
||||||
settings: Settings,
|
settingsPref: Settings,
|
||||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
val db: Database?
|
val db: Database?
|
||||||
val isAnalyticsEnabled: Boolean
|
val settings: Settings
|
||||||
val isFirstLaunch: Boolean
|
|
||||||
fun enableAnalytics()
|
|
||||||
fun firstLaunchDone()
|
|
||||||
fun isPresent(path: String): Boolean
|
fun isPresent(path: String): Boolean
|
||||||
fun fileSeparator(): String
|
fun fileSeparator(): String
|
||||||
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, reqWidth: Int = 150, reqHeight: Int = 150): Picture
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit = {})
|
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit = {})
|
||||||
fun addToLibrary(path: String)
|
fun addToLibrary(path: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Do we have Analytics Permission?
|
||||||
|
* - Defaults to `False`
|
||||||
|
* */
|
||||||
|
val Dir.isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
||||||
|
fun Dir.enableAnalytics() = settings.putBoolean(AnalyticsKey, true)
|
||||||
|
|
||||||
|
fun Dir.setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
|
||||||
|
|
||||||
|
val Dir.getDonationOffset: Int get() = (settings.getIntOrNull(DonationInterval) ?: 3).also {
|
||||||
|
// Min. Donation Asking Interval is `3`
|
||||||
|
if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1)
|
||||||
|
}
|
||||||
|
fun Dir.setDonationOffset(offset: Int = 5) = settings.putInt(DonationInterval, offset)
|
||||||
|
|
||||||
|
val Dir.isFirstLaunch get() = settings.getBooleanOrNull(FirstLaunch) ?: true
|
||||||
|
fun Dir.firstLaunchDone() {
|
||||||
|
settings.putBoolean(FirstLaunch, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Call this function at startup!
|
||||||
|
* */
|
||||||
|
fun Dir.createDirectories() {
|
||||||
|
createDirectory(defaultDir())
|
||||||
|
createDirectory(imageCacheDir())
|
||||||
|
createDirectory(defaultDir() + "Tracks/")
|
||||||
|
createDirectory(defaultDir() + "Albums/")
|
||||||
|
createDirectory(defaultDir() + "Playlists/")
|
||||||
|
createDirectory(defaultDir() + "YT_Downloads/")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
||||||
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
|
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
||||||
|
removeIllegalChars(itemName) + extension
|
||||||
|
/*DIR Specific Operation End*/
|
||||||
|
|
||||||
|
fun getNameURL(url: String): String {
|
||||||
|
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
||||||
return flow {
|
return flow {
|
||||||
try {
|
try {
|
||||||
@ -95,24 +138,3 @@ suspend fun downloadByteArray(
|
|||||||
client.close()
|
client.close()
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNameURL(url: String): String {
|
|
||||||
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Call this function at startup!
|
|
||||||
* */
|
|
||||||
fun Dir.createDirectories() {
|
|
||||||
createDirectory(defaultDir())
|
|
||||||
createDirectory(imageCacheDir())
|
|
||||||
createDirectory(defaultDir() + "Tracks/")
|
|
||||||
createDirectory(defaultDir() + "Albums/")
|
|
||||||
createDirectory(defaultDir() + "Playlists/")
|
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
|
||||||
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
|
||||||
removeIllegalChars(itemName) + extension
|
|
||||||
|
@ -13,9 +13,7 @@ import io.github.shabinder.utils.getJsonArray
|
|||||||
import io.github.shabinder.utils.getJsonObject
|
import io.github.shabinder.utils.getJsonObject
|
||||||
import io.github.shabinder.utils.getString
|
import io.github.shabinder.utils.getString
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.forms.FormDataContent
|
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.http.Parameters
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -88,13 +86,7 @@ interface JioSaavnRequests {
|
|||||||
private suspend fun getSongID(
|
private suspend fun getSongID(
|
||||||
URL: String,
|
URL: String,
|
||||||
): String {
|
): String {
|
||||||
val res = httpClient.get<String>(URL) {
|
val res = httpClient.get<String>(URL)
|
||||||
body = FormDataContent(
|
|
||||||
Parameters.build {
|
|
||||||
append("bitrate", "320")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return try {
|
return try {
|
||||||
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
|
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
@ -40,28 +40,10 @@ 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,
|
settingsPref: Settings,
|
||||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DirKey = "downloadDir"
|
|
||||||
const val AnalyticsKey = "analytics"
|
|
||||||
const val firstLaunch = "firstLaunch"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
|
|
||||||
|
|
||||||
actual fun firstLaunchDone() {
|
|
||||||
settings.putBoolean(firstLaunch, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
|
||||||
|
|
||||||
actual fun enableAnalytics() {
|
|
||||||
settings.putBoolean(AnalyticsKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
createDirectories()
|
createDirectories()
|
||||||
}
|
}
|
||||||
@ -76,8 +58,6 @@ actual class Dir actual constructor(
|
|||||||
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
|
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) {
|
||||||
@ -177,14 +157,14 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
actual fun addToLibrary(path: String) {}
|
actual fun addToLibrary(path: String) {}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture {
|
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = imageCacheDir() + getNameURL(url)
|
||||||
var picture: ImageBitmap? = loadCachedImage(cachePath)
|
var picture: ImageBitmap? = loadCachedImage(cachePath, reqWidth, reqHeight)
|
||||||
if (picture == null) picture = freshImage(url)
|
if (picture == null) picture = freshImage(url, reqWidth, reqHeight)
|
||||||
return Picture(image = picture)
|
return Picture(image = picture)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCachedImage(cachePath: String): ImageBitmap? {
|
private fun loadCachedImage(cachePath: String, reqWidth: Int, reqHeight: Int): ImageBitmap? {
|
||||||
return try {
|
return try {
|
||||||
ImageIO.read(File(cachePath))?.toImageBitmap()
|
ImageIO.read(File(cachePath))?.toImageBitmap()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -194,7 +174,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
private suspend fun freshImage(url: String): ImageBitmap? {
|
private suspend fun freshImage(url: String, reqWidth: Int, reqHeight: Int): ImageBitmap? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val source = URL(url)
|
val source = URL(url)
|
||||||
@ -218,7 +198,8 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual val db: Database? get() = spotiFlyerDatabase.instance
|
actual val db: Database? = spotiFlyerDatabase.instance
|
||||||
|
actual val settings: Settings = settingsPref
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(
|
fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(
|
||||||
|
@ -24,26 +24,9 @@ import platform.UIKit.UIImageJPEGRepresentation
|
|||||||
|
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
val logger: Kermit,
|
val logger: Kermit,
|
||||||
private val settings: Settings,
|
settingsPref: Settings,
|
||||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val DirKey = "downloadDir"
|
|
||||||
const val AnalyticsKey = "analytics"
|
|
||||||
const val firstLaunch = "firstLaunch"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
|
|
||||||
|
|
||||||
actual fun firstLaunchDone() {
|
|
||||||
settings.putBoolean(firstLaunch, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
|
||||||
|
|
||||||
actual fun enableAnalytics() {
|
|
||||||
settings.putBoolean(AnalyticsKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
|
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
|
||||||
|
|
||||||
@ -55,8 +38,6 @@ actual class Dir actual constructor(
|
|||||||
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
|
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
|
||||||
fileSeparator() + "SpotiFlyer" + fileSeparator()
|
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)!!
|
||||||
musicDir.URLByAppendingPathComponent("SpotiFlyer", true)!!
|
musicDir.URLByAppendingPathComponent("SpotiFlyer", true)!!
|
||||||
@ -97,7 +78,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO) {
|
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val cachePath = imageCacheURL.URLByAppendingPathComponent(getNameURL(url))
|
val cachePath = imageCacheURL.URLByAppendingPathComponent(getNameURL(url))
|
||||||
Picture(image = cachePath?.path?.let { loadCachedImage(it) } ?: loadFreshImage(url))
|
Picture(image = cachePath?.path?.let { loadCachedImage(it) } ?: loadFreshImage(url))
|
||||||
@ -107,7 +88,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCachedImage(filePath: String): UIImage? {
|
private fun loadCachedImage(filePath: String, reqWidth: Int = 150, reqHeight: Int = 150): UIImage? {
|
||||||
return try {
|
return try {
|
||||||
UIImage.imageWithContentsOfFile(filePath)
|
UIImage.imageWithContentsOfFile(filePath)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -116,7 +97,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadFreshImage(url: String): UIImage? = withContext(dispatcherIO) {
|
private suspend fun loadFreshImage(url: String, reqWidth: Int = 150, reqHeight: Int = 150): UIImage? = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val nsURL = NSURL(string = url)
|
val nsURL = NSURL(string = url)
|
||||||
val data = NSURLConnection.sendSynchronousRequest(NSURLRequest.requestWithURL(nsURL), null, null)
|
val data = NSURLConnection.sendSynchronousRequest(NSURLRequest.requestWithURL(nsURL), null, null)
|
||||||
@ -195,5 +176,6 @@ actual class Dir actual constructor(
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual val settings: Settings = settingsPref
|
||||||
actual val db: Database? = spotiFlyerDatabase.instance
|
actual val db: Database? = spotiFlyerDatabase.instance
|
||||||
}
|
}
|
||||||
|
@ -34,29 +34,9 @@ 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,
|
settingsPref: Settings,
|
||||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val DirKey = "downloadDir"
|
|
||||||
const val AnalyticsKey = "analytics"
|
|
||||||
const val firstLaunch = "firstLaunch"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
|
|
||||||
|
|
||||||
actual fun firstLaunchDone() {
|
|
||||||
settings.putBoolean(firstLaunch, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
|
||||||
|
|
||||||
actual fun enableAnalytics() {
|
|
||||||
settings.putBoolean(AnalyticsKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
|
|
||||||
|
|
||||||
/*init {
|
/*init {
|
||||||
createDirectories()
|
createDirectories()
|
||||||
}*/
|
}*/
|
||||||
@ -127,7 +107,7 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
actual fun addToLibrary(path: String) {}
|
actual fun addToLibrary(path: String) {}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture {
|
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||||
return Picture(url)
|
return Picture(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +115,8 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
private suspend fun freshImage(url: String): ImageBitmap? = null
|
private suspend fun freshImage(url: String): ImageBitmap? = null
|
||||||
|
|
||||||
actual val db: Database? get() = spotiFlyerDatabase.instance
|
actual val db: Database? = spotiFlyerDatabase.instance
|
||||||
|
actual val settings: Settings = settingsPref
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
||||||
|
@ -51,13 +51,18 @@ interface SpotiFlyerList {
|
|||||||
/*
|
/*
|
||||||
* Load Image from cache/Internet and cache it
|
* Load Image from cache/Internet and cache it
|
||||||
* */
|
* */
|
||||||
suspend fun loadImage(url: String): Picture
|
suspend fun loadImage(url: String, isCover: Boolean = false): Picture
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sync Tracks Statuses
|
* Sync Tracks Statuses
|
||||||
* */
|
* */
|
||||||
fun onRefreshTracksStatuses()
|
fun onRefreshTracksStatuses()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snooze Donation Dialog
|
||||||
|
* */
|
||||||
|
fun snoozeDonationDialog()
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
val storeFactory: StoreFactory
|
val storeFactory: StoreFactory
|
||||||
val fetchQuery: FetchPlatformQueryResult
|
val fetchQuery: FetchPlatformQueryResult
|
||||||
@ -78,7 +83,8 @@ interface SpotiFlyerList {
|
|||||||
val queryResult: PlatformQueryResult? = null,
|
val queryResult: PlatformQueryResult? = null,
|
||||||
val link: String = "",
|
val link: String = "",
|
||||||
val trackList: List<TrackDetails> = emptyList(),
|
val trackList: List<TrackDetails> = emptyList(),
|
||||||
val errorOccurred: Exception? = null
|
val errorOccurred: Exception? = null,
|
||||||
|
val askForDonation: Boolean = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import com.arkivanov.decompose.ComponentContext
|
|||||||
import com.arkivanov.decompose.value.Value
|
import com.arkivanov.decompose.value.Value
|
||||||
import com.shabinder.common.caching.Cache
|
import com.shabinder.common.caching.Cache
|
||||||
import com.shabinder.common.di.Picture
|
import com.shabinder.common.di.Picture
|
||||||
|
import com.shabinder.common.di.setDonationOffset
|
||||||
import com.shabinder.common.di.utils.asValue
|
import com.shabinder.common.di.utils.asValue
|
||||||
import com.shabinder.common.list.SpotiFlyerList
|
import com.shabinder.common.list.SpotiFlyerList
|
||||||
import com.shabinder.common.list.SpotiFlyerList.Dependencies
|
import com.shabinder.common.list.SpotiFlyerList.Dependencies
|
||||||
@ -52,7 +53,7 @@ internal class SpotiFlyerListImpl(
|
|||||||
|
|
||||||
private val cache = Cache.Builder
|
private val cache = Cache.Builder
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.maximumCacheSize(150)
|
.maximumCacheSize(75)
|
||||||
.build<String, Picture>()
|
.build<String, Picture>()
|
||||||
|
|
||||||
override val models: Value<State> = store.asValue()
|
override val models: Value<State> = store.asValue()
|
||||||
@ -73,9 +74,14 @@ internal class SpotiFlyerListImpl(
|
|||||||
store.accept(Intent.RefreshTracksStatuses)
|
store.accept(Intent.RefreshTracksStatuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadImage(url: String): Picture {
|
override fun snoozeDonationDialog() {
|
||||||
|
dir.setDonationOffset(offset = 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadImage(url: String, isCover: Boolean): Picture {
|
||||||
return cache.get(url) {
|
return cache.get(url) {
|
||||||
dir.loadImage(url)
|
if (isCover) dir.loadImage(url, 350, 350)
|
||||||
|
else dir.loadImage(url, 150, 150)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import com.shabinder.common.database.getLogger
|
|||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
import com.shabinder.common.di.downloadTracks
|
import com.shabinder.common.di.downloadTracks
|
||||||
|
import com.shabinder.common.di.getDonationOffset
|
||||||
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.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
@ -59,6 +60,7 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
||||||
data class UpdateTrackItem(val item: TrackDetails) : Result()
|
data class UpdateTrackItem(val item: TrackDetails) : Result()
|
||||||
data class ErrorOccurred(val error: Exception) : Result()
|
data class ErrorOccurred(val error: Exception) : Result()
|
||||||
|
data class AskForDonation(val isAllowed: Boolean) : Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
|
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
|
||||||
@ -66,6 +68,18 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
override suspend fun executeAction(action: Unit, getState: () -> State) {
|
override suspend fun executeAction(action: Unit, getState: () -> State) {
|
||||||
executeIntent(Intent.SearchLink(link), getState)
|
executeIntent(Intent.SearchLink(link), getState)
|
||||||
|
|
||||||
|
dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also {
|
||||||
|
// See if It's Time we can request for support for maintaining this project or not
|
||||||
|
logger.d(message = "Database List Last ID: $it", tag = "Database Last ID")
|
||||||
|
val offset = dir.getDonationOffset
|
||||||
|
dispatch(
|
||||||
|
Result.AskForDonation(
|
||||||
|
// Every 3rd Interval or After some offset
|
||||||
|
isAllowed = offset < 4 && (it % offset == 0L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
downloadProgressFlow.collectLatest { map ->
|
downloadProgressFlow.collectLatest { map ->
|
||||||
logger.d(map.size.toString(), "ListStore: flow Updated")
|
logger.d(map.size.toString(), "ListStore: flow Updated")
|
||||||
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
|
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
|
||||||
@ -119,6 +133,7 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
is Result.UpdateTrackList -> copy(trackList = result.list)
|
is Result.UpdateTrackList -> copy(trackList = result.list)
|
||||||
is Result.UpdateTrackItem -> updateTrackItem(result.item)
|
is Result.UpdateTrackItem -> updateTrackItem(result.item)
|
||||||
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
|
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
|
||||||
|
is Result.AskForDonation -> copy(askForDonation = result.isAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun State.updateTrackItem(item: TrackDetails): State {
|
private fun State.updateTrackItem(item: TrackDetails): State {
|
||||||
|
@ -73,7 +73,7 @@ internal class SpotiFlyerMainImpl(
|
|||||||
|
|
||||||
override suspend fun loadImage(url: String): Picture {
|
override suspend fun loadImage(url: String): Picture {
|
||||||
return cache.get(url) {
|
return cache.get(url) {
|
||||||
dir.loadImage(url)
|
dir.loadImage(url, 150, 150)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,10 +100,12 @@ internal class SpotiFlyerRootImpl(
|
|||||||
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() {
|
||||||
|
if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
|
||||||
router.popWhile {
|
router.popWhile {
|
||||||
it !is Configuration.Main
|
it !is Configuration.Main
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun showToast(text: String) { toastState.value = text }
|
override fun showToast(text: String) { toastState.value = text }
|
||||||
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
|
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
|
||||||
}
|
}
|
||||||
@ -125,7 +127,9 @@ internal class SpotiFlyerRootImpl(
|
|||||||
private fun onListOutput(output: SpotiFlyerList.Output): Unit =
|
private fun onListOutput(output: SpotiFlyerList.Output): Unit =
|
||||||
when (output) {
|
when (output) {
|
||||||
is SpotiFlyerList.Output.Finished -> {
|
is SpotiFlyerList.Output.Finished -> {
|
||||||
|
if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
|
||||||
router.pop()
|
router.pop()
|
||||||
|
}
|
||||||
analytics.homeScreenVisit()
|
analytics.homeScreenVisit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import com.shabinder.common.di.DownloadProgressFlow
|
|||||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
import com.shabinder.common.di.initKoin
|
import com.shabinder.common.di.initKoin
|
||||||
import com.shabinder.common.di.isInternetAccessible
|
import com.shabinder.common.di.isInternetAccessible
|
||||||
|
import com.shabinder.common.di.setDownloadDirectory
|
||||||
import com.shabinder.common.models.Actions
|
import com.shabinder.common.models.Actions
|
||||||
import com.shabinder.common.models.PlatformActions
|
import com.shabinder.common.models.PlatformActions
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<vector android:height="42dp" android:viewportHeight="250"
|
||||||
|
android:viewportWidth="488" android:width="81.984dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M483.73,36A53.1,53.1 0,0 0,452 4.28C438.49,0 425.94,0 400.84,0H325.16C300.07,0 287.52,0 274,4.28A53.08,53.08 0,0 0,242.28 36a76.64,76.64 0,0 0,-2 7.74,140.32 140.32,0 0,1 14,24.86c0.38,-9.57 1.27,-17.22 3.46,-24.14 4.68,-12.86 11.88,-20.06 24.74,-24.74C294.25,16 308.12,16 330,16h66c21.88,0 35.76,0 47.54,3.73 12.86,4.68 20,11.88 24.74,24.74C472,56.25 472,70.13 472,92v66c0,21.88 0,35.76 -3.72,47.53 -4.69,12.86 -11.88,20.06 -24.74,24.74C431.76,234 417.88,234 396,234H330c-21.89,0 -35.76,0 -47.54,-3.73 -12.86,-4.68 -20.06,-11.88 -24.74,-24.74 -2.19,-6.92 -3.09,-14.58 -3.46,-24.15a140.51,140.51 0,0 1,-14 24.85,77.18 77.18,0 0,0 2,7.77A53.08,53.08 0,0 0,274 245.73C287.52,250 300.07,250 325.16,250h75.68c25.1,0 37.65,0 51.16,-4.27A53.11,53.11 0,0 0,483.73 214C488,200.49 488,187.94 488,162.84V87.17C488,62.07 488,49.52 483.73,36Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M422,217L380.33,217c-1.76,0 -5.83,-2.79 -2.63,-6.67 21.36,-23 48,-30.93 73.4,-39.42 3.32,-1 3.91,2.51 3.91,3.48v8.68C455,202.61 441.57,217 422,217ZM343.73,212.69c-4,-29.73 -18.06,-80.79 -71,-118.55A3.78,3.78 0,0 1,271 90.63L271,66.36c0,-26.69 18,-33.31 26.37,-33.31a4.3,4.3 0,0 1,4.07 2.1c25.24,55 41,89.86 50.7,172.83 0.05,1.62 0.31,2.39 1.28,0 6.86,-15.07 39.35,-92 26.44,-170.68a3.64,3.64 0,0 1,3.5 -4.25L422,33.05c19.54,0 33,13.43 33,33.36L455,100.5a3.63,3.63 0,0 1,-2.07 3.36,180.12 180.12,0 0,0 -90.3,109.25c-0.79,2.21 -1.25,3.9 -3.71,3.9h-11.8C344.77,217 344.27,216.05 343.73,212.7ZM304.35,217c-20,0 -33.35,-12.37 -33.35,-33.93v-2.24c0,-0.9 0.71,-4.29 4.09,-3.63 20.24,6.23 41.92,12.52 57.77,33.49 1.82,2.56 0.23,6.3 -2.91,6.31Z"/>
|
||||||
|
<path android:fillColor="#fff" android:pathData="M124.991,239.991a115,115 54.655,1 0,2.007 -229.991a115,115 54.655,1 0,-2.007 229.991z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M180.77,114.59c-8.62,0 -15.61,7.39 -15.61,16.49s7,16.5 15.61,16.5 15.62,-7.38 15.62,-16.5S189.4,114.59 180.77,114.59Z"/>
|
||||||
|
<path android:fillColor="#2bc5b4" android:pathData="M125,0A125,125 0,1 0,250 125,125 125,0 0,0 125,0ZM95.37,132.09c0,63.82 -101.74,35.68 -60.49,2.93 9.65,13.39 28.18,12.5 30.15,-0.72l0.37,-52.05c0.95,-13.32 26.85,-16 30,0ZM133.31,156.32a12.05,12.05 0,0 1,-12 12L116.1,168.32a12.05,12.05 0,0 1,-12 -12L104.1,106a12,12 0,0 1,12 -12h5.21a12,12 0,0 1,12 12ZM133.31,74.56a11.84,11.84 0,0 1,-11.79 11.79L115.9,86.35a11.84,11.84 0,0 1,-11.81 -11.79L104.09,71.65A11.83,11.83 0,0 1,115.9 59.86h5.62a11.82,11.82 0,0 1,11.79 11.79ZM180.77,169.9c-22,0 -39.82,-17.37 -39.82,-38.82s17.84,-38.81 39.82,-38.81 39.81,17.38 39.81,38.81S202.76,169.9 180.77,169.9Z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:viewportHeight="64"
|
||||||
|
android:viewportWidth="64" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#BBFFFFFF" android:fillType="evenOdd" android:pathData="M52.402,31.916c0,4.03 -1.17,7.895 -3.178,11.087l8.196,8.23c4.014,-5.375 6.523,-12.094 6.523,-19.318s-2.51,-13.942 -6.523,-19.318l-8.196,8.23c2.007,3.192 3.178,6.887 3.178,11.087z"/>
|
||||||
|
<path android:fillColor="#FFFFFF" android:fillType="evenOdd" android:pathData="M32.004,52.41c-11.207,0 -20.406,-9.24 -20.406,-20.493s9.2,-20.493 20.406,-20.493c4.182,0 7.86,1.176 11.04,3.36l8.196,-8.23C45.887,2.52 39.197,0 32.004,0 14.44,0 0.057,14.278 0.057,32.084S14.44,64 32.004,64c7.36,0 14.05,-2.52 19.403,-6.55l-8.196,-8.23c-3.178,2.016 -7.025,3.192 -11.207,3.192z"/>
|
||||||
|
</vector>
|
Loading…
Reference in New Issue
Block a user