Image Compression Android, Jio-Saavn Complete,Code Cleanup

This commit is contained in:
shabinder 2021-05-26 19:41:43 +05:30
parent 3913bfa4b1
commit cceb6d04dc
33 changed files with 357 additions and 226 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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")

View File

@ -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"))

View File

@ -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
} }
} }

View File

@ -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)

View File

@ -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(
) )
} }
} }
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
)*/
} }
} }

View File

@ -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()

View File

@ -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
) )

View File

@ -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)

View File

@ -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)
) )

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {
result // Decode and Cache Full Sized Image in Background
} else null cacheImage(BitmapFactory.decodeByteArray(input, 0, input.size), imageCacheDir() + getNameURL(url))
}
bitmap // return Memory Efficient Bitmap
} 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
} }

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -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(

View File

@ -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
} }

View File

@ -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 {

View File

@ -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,
) )
} }

View File

@ -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)
} }
} }
} }

View File

@ -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 {

View File

@ -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)
} }
} }
} }

View File

@ -100,8 +100,10 @@ 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() {
router.popWhile { if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
it !is Configuration.Main router.popWhile {
it !is Configuration.Main
}
} }
} }
override fun showToast(text: String) { toastState.value = text } override fun showToast(text: String) { toastState.value = text }
@ -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 -> {
router.pop() if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) {
router.pop()
}
analytics.homeScreenVisit() analytics.homeScreenVisit()
} }
} }

View File

@ -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

View File

@ -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>

View File

@ -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>