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>
|
||||
<package android:name="com.gaana" />
|
||||
<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.apps.youtube.music" />
|
||||
</queries>
|
||||
|
@ -41,7 +41,7 @@ fun AnalyticsDialog(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Rounded.Insights,"Analytics", Modifier.size(52.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,
|
||||
|
@ -18,7 +18,8 @@
|
||||
|
||||
object Versions {
|
||||
// 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
|
||||
const val kotlinVersion = "1.4.32"
|
||||
@ -45,7 +46,6 @@ object Versions {
|
||||
const val slf4j = "1.7.30"
|
||||
|
||||
// Android
|
||||
const val versionCode = 19
|
||||
const val minSdkVersion = 21
|
||||
const val compileSdkVersion = 29
|
||||
const val targetSdkVersion = 29
|
||||
|
@ -43,9 +43,11 @@ kotlin {
|
||||
implementation(MVIKotlin.coroutines)
|
||||
implementation(MVIKotlin.mvikotlin)
|
||||
|
||||
implementation(compose.ui)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material)
|
||||
implementation(compose.animation)
|
||||
|
||||
implementation(Extras.kermit)
|
||||
implementation("dev.icerock.moko:parcelize:0.6.1")
|
||||
|
@ -28,6 +28,7 @@ kotlin {
|
||||
}
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(compose.material)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(project(":common:root"))
|
||||
implementation(project(":common:main"))
|
||||
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.withContext
|
||||
@Composable
|
||||
actual fun ImageLoad(
|
||||
link: String,
|
||||
loader: suspend (String) -> Picture,
|
||||
loader: suspend () -> Picture,
|
||||
desc: String,
|
||||
modifier: Modifier,
|
||||
// placeholder: ImageVector
|
||||
@ -27,7 +27,7 @@ actual fun ImageLoad(
|
||||
|
||||
LaunchedEffect(link) {
|
||||
withContext(dispatcherIO) {
|
||||
pic = loader(link).image
|
||||
pic = loader().image
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,9 @@ actual fun HeartIcon() = painterResource(R.drawable.ic_heart)
|
||||
@Composable
|
||||
actual fun SpotifyLogo() = painterResource(R.drawable.ic_spotify_logo)
|
||||
|
||||
@Composable
|
||||
actual fun SaavnLogo() = painterResource(R.drawable.ic_jio_saavn_logo)
|
||||
|
||||
@Composable
|
||||
actual fun GaanaLogo() = painterResource(R.drawable.ic_gaana)
|
||||
|
||||
@ -97,6 +100,9 @@ actual fun GithubLogo() = painterResource(R.drawable.ic_github)
|
||||
@Composable
|
||||
actual fun PaypalLogo() = painterResource(R.drawable.ic_paypal_logo)
|
||||
|
||||
@Composable
|
||||
actual fun OpenCollectiveLogo() = painterResource(R.drawable.ic_opencollective_icon)
|
||||
|
||||
@Composable
|
||||
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.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -26,7 +30,8 @@ import com.shabinder.common.models.methods
|
||||
@Composable
|
||||
actual fun DonationDialog(
|
||||
isVisible: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
onSnooze: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
isVisible
|
||||
@ -39,13 +44,36 @@ actual fun DonationDialog(
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
"Support Us",
|
||||
"We Need Your Support!",
|
||||
style = SpotiFlyerTypography.h5,
|
||||
textAlign = TextAlign.Center,
|
||||
color = colorAccent,
|
||||
modifier = Modifier
|
||||
)
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().clickable(
|
||||
@ -56,7 +84,7 @@ actual fun DonationDialog(
|
||||
)
|
||||
.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))
|
||||
Column {
|
||||
Text(
|
||||
@ -79,7 +107,7 @@ actual fun DonationDialog(
|
||||
),
|
||||
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))
|
||||
Column {
|
||||
Text(
|
||||
@ -92,39 +120,21 @@ actual fun DonationDialog(
|
||||
)
|
||||
}
|
||||
}
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*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))
|
||||
}
|
||||
},*//*
|
||||
backgroundColor = Color.DarkGray,
|
||||
text = {
|
||||
|
||||
}
|
||||
,shape = SpotiFlyerShapes.medium,
|
||||
onDismissRequest = onDismiss
|
||||
)*/
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import com.shabinder.common.di.Picture
|
||||
@Composable
|
||||
expect fun ImageLoad(
|
||||
link: String,
|
||||
loader: suspend (String) -> Picture,
|
||||
loader: suspend () -> Picture,
|
||||
desc: String = "Album Art",
|
||||
modifier: Modifier = Modifier,
|
||||
// placeholder:Painter = PlaceHolderImage()
|
@ -39,6 +39,9 @@ expect fun SpotiFlyerLogo(): Painter
|
||||
@Composable
|
||||
expect fun SpotifyLogo(): Painter
|
||||
|
||||
@Composable
|
||||
expect fun SaavnLogo(): Painter
|
||||
|
||||
@Composable
|
||||
expect fun YoutubeLogo(): Painter
|
||||
|
||||
@ -54,6 +57,9 @@ expect fun GithubLogo(): Painter
|
||||
@Composable
|
||||
expect fun PaypalLogo(): Painter
|
||||
|
||||
@Composable
|
||||
expect fun OpenCollectiveLogo(): Painter
|
||||
|
||||
@Composable
|
||||
expect fun RazorPay(): Painter
|
||||
|
||||
@ -69,5 +75,6 @@ expect fun DownloadImageArrow(modifier: Modifier)
|
||||
@Composable
|
||||
expect fun DonationDialog(
|
||||
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.rememberLazyListState
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ExtendedFloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@ -39,6 +40,9 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.Modifier
|
||||
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.methods
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SpotiFlyerListContent(
|
||||
component: SpotiFlyerList,
|
||||
@ -68,7 +73,6 @@ fun SpotiFlyerListContent(
|
||||
component.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
val result = model.queryResult
|
||||
if (result == null) {
|
||||
@ -92,16 +96,34 @@ fun SpotiFlyerListContent(
|
||||
TrackCard(
|
||||
track = item,
|
||||
downloadTrack = { component.onDownloadClicked(item) },
|
||||
loadImage = component::loadImage
|
||||
loadImage = { component.loadImage(item.albumArtURL) }
|
||||
)
|
||||
}
|
||||
},
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
// Donation Dialog Visibility
|
||||
var visibilty by remember { mutableStateOf(false) }
|
||||
DonationDialog(
|
||||
isVisible = visibilty,
|
||||
onDismiss = {
|
||||
visibilty = false
|
||||
},
|
||||
onSnooze = {
|
||||
visibilty = false
|
||||
component.snoozeDonationDialog()
|
||||
}
|
||||
)
|
||||
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)
|
||||
)
|
||||
|
||||
@ -121,12 +143,12 @@ fun SpotiFlyerListContent(
|
||||
fun TrackCard(
|
||||
track: TrackDetails,
|
||||
downloadTrack: () -> Unit,
|
||||
loadImage: suspend (String) -> Picture
|
||||
loadImage: suspend () -> Picture
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
||||
ImageLoad(
|
||||
track.albumArtURL,
|
||||
loadImage,
|
||||
{ loadImage() },
|
||||
"Album Art",
|
||||
modifier = Modifier
|
||||
.width(70.dp)
|
||||
@ -177,7 +199,7 @@ fun TrackCard(
|
||||
fun CoverImage(
|
||||
title: String,
|
||||
coverURL: String,
|
||||
loadImage: suspend (String) -> Picture,
|
||||
loadImage: suspend (URL: String, isCover: Boolean) -> Picture,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@ -186,7 +208,7 @@ fun CoverImage(
|
||||
) {
|
||||
ImageLoad(
|
||||
coverURL,
|
||||
loadImage,
|
||||
{ loadImage(coverURL, true) },
|
||||
"Cover Image",
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
|
@ -256,7 +256,16 @@ fun AboutColumn(
|
||||
"Open Gaana",
|
||||
tint = Color.Unspecified,
|
||||
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))
|
||||
@ -265,7 +274,7 @@ fun AboutColumn(
|
||||
"Open Youtube",
|
||||
tint = Color.Unspecified,
|
||||
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))
|
||||
@ -299,7 +308,7 @@ fun AboutColumn(
|
||||
)
|
||||
.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))
|
||||
Column {
|
||||
Text(
|
||||
@ -337,6 +346,9 @@ fun AboutColumn(
|
||||
isDonationDialogVisible,
|
||||
onDismiss = {
|
||||
isDonationDialogVisible = false
|
||||
},
|
||||
onSnooze = {
|
||||
isDonationDialogVisible = false
|
||||
}
|
||||
)
|
||||
|
||||
@ -350,7 +362,7 @@ fun AboutColumn(
|
||||
),
|
||||
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))
|
||||
Column {
|
||||
Text(
|
||||
@ -373,7 +385,7 @@ fun AboutColumn(
|
||||
),
|
||||
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))
|
||||
Column {
|
||||
Text(
|
||||
@ -455,7 +467,7 @@ fun DownloadRecordItem(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
||||
ImageLoad(
|
||||
item.coverUrl,
|
||||
loadImage,
|
||||
{ loadImage(item.coverUrl) },
|
||||
"Album Art",
|
||||
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.withContext
|
||||
@Composable
|
||||
actual fun ImageLoad(
|
||||
link: String,
|
||||
loader: suspend (String) -> Picture,
|
||||
loader: suspend () -> Picture,
|
||||
desc: String,
|
||||
modifier: Modifier,
|
||||
// placeholder: ImageVector
|
||||
@ -26,7 +26,7 @@ actual fun ImageLoad(
|
||||
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
||||
LaunchedEffect(link) {
|
||||
withContext(dispatcherIO) {
|
||||
pic = loader(link).image
|
||||
pic = loader().image
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,9 @@ actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_he
|
||||
@Composable
|
||||
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
|
||||
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
|
||||
|
||||
@ -99,5 +102,8 @@ actual fun GithubLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_g
|
||||
@Composable
|
||||
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
|
||||
actual fun RazorPay() = rememberVectorPainter(vectorXmlResource("drawable/ic_indian_rupee.xml")) as Painter
|
||||
|
@ -27,7 +27,8 @@ import com.shabinder.common.models.methods
|
||||
@Composable
|
||||
actual fun DonationDialog(
|
||||
isVisible: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
onSnooze: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
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.russhwolf.settings.Settings
|
||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||
import com.shabinder.common.di.utils.ParallelExecutor
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.methods
|
||||
import com.shabinder.database.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
@ -45,33 +43,9 @@ import java.net.URL
|
||||
@Suppress("DEPRECATION")
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val settings: Settings,
|
||||
settingsPref: Settings,
|
||||
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")
|
||||
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 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)
|
||||
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 {
|
||||
BitmapFactory.decodeFile(cachePath)
|
||||
getMemoryEfficientBitmap(cachePath, reqWidth, reqHeight)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,27 +171,36 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
@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 {
|
||||
val source = URL(url)
|
||||
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 5000
|
||||
connection.connect()
|
||||
|
||||
val input: InputStream = connection.inputStream
|
||||
val result: Bitmap? = BitmapFactory.decodeStream(input)
|
||||
val input: ByteArray = connection.inputStream.readBytes()
|
||||
|
||||
if (result != null) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
cacheImage(result, imageCacheDir() + getNameURL(url))
|
||||
}
|
||||
result
|
||||
} else null
|
||||
// Get Memory Efficient Bitmap
|
||||
val bitmap: Bitmap? = getMemoryEfficientBitmap(input, reqWidth, reqHeight)
|
||||
|
||||
parallelExecutor.execute {
|
||||
// Decode and Cache Full Sized Image in Background
|
||||
cacheImage(BitmapFactory.decodeByteArray(input, 0, input.size), imageCacheDir() + getNameURL(url))
|
||||
}
|
||||
bitmap // return Memory Efficient Bitmap
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
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 settings: Settings = settingsPref
|
||||
}
|
||||
|
@ -16,8 +16,67 @@
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual data class Picture(
|
||||
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 kotlin.math.roundToInt
|
||||
|
||||
const val DirKey = "downloadDir"
|
||||
const val AnalyticsKey = "analytics"
|
||||
const val FirstLaunch = "firstLaunch"
|
||||
const val DonationInterval = "donationInterval"
|
||||
|
||||
expect class Dir(
|
||||
logger: Kermit,
|
||||
settings: Settings,
|
||||
settingsPref: Settings,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) {
|
||||
val db: Database?
|
||||
val isAnalyticsEnabled: Boolean
|
||||
val isFirstLaunch: Boolean
|
||||
fun enableAnalytics()
|
||||
fun firstLaunchDone()
|
||||
val settings: Settings
|
||||
fun isPresent(path: String): Boolean
|
||||
fun fileSeparator(): String
|
||||
fun defaultDir(): String
|
||||
fun imageCacheDir(): String
|
||||
fun createDirectory(dirPath: String)
|
||||
fun setDownloadDirectory(newBasePath: String)
|
||||
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 saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit = {})
|
||||
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> {
|
||||
return flow {
|
||||
try {
|
||||
@ -95,24 +138,3 @@ suspend fun downloadByteArray(
|
||||
client.close()
|
||||
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.getString
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.Parameters
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@ -88,13 +86,7 @@ interface JioSaavnRequests {
|
||||
private suspend fun getSongID(
|
||||
URL: String,
|
||||
): String {
|
||||
val res = httpClient.get<String>(URL) {
|
||||
body = FormDataContent(
|
||||
Parameters.build {
|
||||
append("bitrate", "320")
|
||||
}
|
||||
)
|
||||
}
|
||||
val res = httpClient.get<String>(URL)
|
||||
return try {
|
||||
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
|
@ -40,28 +40,10 @@ import javax.imageio.ImageIO
|
||||
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val settings: Settings,
|
||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
settingsPref: Settings,
|
||||
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 {
|
||||
createDirectories()
|
||||
}
|
||||
@ -76,8 +58,6 @@ actual class Dir actual constructor(
|
||||
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
|
||||
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
|
||||
actual fun createDirectory(dirPath: String) {
|
||||
@ -177,14 +157,14 @@ actual class Dir actual constructor(
|
||||
}
|
||||
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)
|
||||
var picture: ImageBitmap? = loadCachedImage(cachePath)
|
||||
if (picture == null) picture = freshImage(url)
|
||||
var picture: ImageBitmap? = loadCachedImage(cachePath, reqWidth, reqHeight)
|
||||
if (picture == null) picture = freshImage(url, reqWidth, reqHeight)
|
||||
return Picture(image = picture)
|
||||
}
|
||||
|
||||
private fun loadCachedImage(cachePath: String): ImageBitmap? {
|
||||
private fun loadCachedImage(cachePath: String, reqWidth: Int, reqHeight: Int): ImageBitmap? {
|
||||
return try {
|
||||
ImageIO.read(File(cachePath))?.toImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
@ -194,7 +174,7 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun freshImage(url: String): ImageBitmap? {
|
||||
private suspend fun freshImage(url: String, reqWidth: Int, reqHeight: Int): ImageBitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
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(
|
||||
|
@ -24,26 +24,9 @@ import platform.UIKit.UIImageJPEGRepresentation
|
||||
|
||||
actual class Dir actual constructor(
|
||||
val logger: Kermit,
|
||||
private val settings: Settings,
|
||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
settingsPref: Settings,
|
||||
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)
|
||||
|
||||
@ -55,8 +38,6 @@ actual class Dir actual constructor(
|
||||
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 {
|
||||
val musicDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!
|
||||
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 {
|
||||
val cachePath = imageCacheURL.URLByAppendingPathComponent(getNameURL(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 {
|
||||
UIImage.imageWithContentsOfFile(filePath)
|
||||
} 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 {
|
||||
val nsURL = NSURL(string = url)
|
||||
val data = NSURLConnection.sendSynchronousRequest(NSURLRequest.requestWithURL(nsURL), null, null)
|
||||
@ -195,5 +176,6 @@ actual class Dir actual constructor(
|
||||
// TODO
|
||||
}
|
||||
|
||||
actual val settings: Settings = settingsPref
|
||||
actual val db: Database? = spotiFlyerDatabase.instance
|
||||
}
|
||||
|
@ -34,29 +34,9 @@ import org.w3c.dom.ImageBitmap
|
||||
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val settings: Settings,
|
||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
settingsPref: Settings,
|
||||
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 {
|
||||
createDirectories()
|
||||
}*/
|
||||
@ -127,7 +107,7 @@ actual class Dir actual constructor(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -135,7 +115,8 @@ actual class Dir actual constructor(
|
||||
|
||||
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 {
|
||||
|
@ -51,13 +51,18 @@ interface SpotiFlyerList {
|
||||
/*
|
||||
* 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
|
||||
* */
|
||||
fun onRefreshTracksStatuses()
|
||||
|
||||
/*
|
||||
* Snooze Donation Dialog
|
||||
* */
|
||||
fun snoozeDonationDialog()
|
||||
|
||||
interface Dependencies {
|
||||
val storeFactory: StoreFactory
|
||||
val fetchQuery: FetchPlatformQueryResult
|
||||
@ -78,7 +83,8 @@ interface SpotiFlyerList {
|
||||
val queryResult: PlatformQueryResult? = null,
|
||||
val link: String = "",
|
||||
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.shabinder.common.caching.Cache
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.di.setDonationOffset
|
||||
import com.shabinder.common.di.utils.asValue
|
||||
import com.shabinder.common.list.SpotiFlyerList
|
||||
import com.shabinder.common.list.SpotiFlyerList.Dependencies
|
||||
@ -52,7 +53,7 @@ internal class SpotiFlyerListImpl(
|
||||
|
||||
private val cache = Cache.Builder
|
||||
.newBuilder()
|
||||
.maximumCacheSize(150)
|
||||
.maximumCacheSize(75)
|
||||
.build<String, Picture>()
|
||||
|
||||
override val models: Value<State> = store.asValue()
|
||||
@ -73,9 +74,14 @@ internal class SpotiFlyerListImpl(
|
||||
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) {
|
||||
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.FetchPlatformQueryResult
|
||||
import com.shabinder.common.di.downloadTracks
|
||||
import com.shabinder.common.di.getDonationOffset
|
||||
import com.shabinder.common.list.SpotiFlyerList.State
|
||||
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
@ -59,6 +60,7 @@ internal class SpotiFlyerListStoreProvider(
|
||||
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
||||
data class UpdateTrackItem(val item: TrackDetails) : 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>() {
|
||||
@ -66,6 +68,18 @@ internal class SpotiFlyerListStoreProvider(
|
||||
override suspend fun executeAction(action: Unit, getState: () -> State) {
|
||||
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 ->
|
||||
logger.d(map.size.toString(), "ListStore: flow Updated")
|
||||
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
|
||||
@ -119,6 +133,7 @@ internal class SpotiFlyerListStoreProvider(
|
||||
is Result.UpdateTrackList -> copy(trackList = result.list)
|
||||
is Result.UpdateTrackItem -> updateTrackItem(result.item)
|
||||
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
|
||||
is Result.AskForDonation -> copy(askForDonation = result.isAllowed)
|
||||
}
|
||||
|
||||
private fun State.updateTrackItem(item: TrackDetails): State {
|
||||
|
@ -73,7 +73,7 @@ internal class SpotiFlyerMainImpl(
|
||||
|
||||
override suspend fun loadImage(url: String): Picture {
|
||||
return cache.get(url) {
|
||||
dir.loadImage(url)
|
||||
dir.loadImage(url, 150, 150)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,8 +100,10 @@ internal class SpotiFlyerRootImpl(
|
||||
override val callBacks = object : SpotiFlyerRootCallBacks {
|
||||
override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link))
|
||||
override fun popBackToHomeScreen() {
|
||||
router.popWhile {
|
||||
it !is Configuration.Main
|
||||
if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
|
||||
router.popWhile {
|
||||
it !is Configuration.Main
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun showToast(text: String) { toastState.value = text }
|
||||
@ -125,7 +127,9 @@ internal class SpotiFlyerRootImpl(
|
||||
private fun onListOutput(output: SpotiFlyerList.Output): Unit =
|
||||
when (output) {
|
||||
is SpotiFlyerList.Output.Finished -> {
|
||||
router.pop()
|
||||
if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
|
||||
router.pop()
|
||||
}
|
||||
analytics.homeScreenVisit()
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import com.shabinder.common.di.DownloadProgressFlow
|
||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||
import com.shabinder.common.di.initKoin
|
||||
import com.shabinder.common.di.isInternetAccessible
|
||||
import com.shabinder.common.di.setDownloadDirectory
|
||||
import com.shabinder.common.models.Actions
|
||||
import com.shabinder.common.models.PlatformActions
|
||||
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