From cceb6d04dcb7c5173a24b1a6af045a64d65a71e3 Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 26 May 2021 19:41:43 +0530 Subject: [PATCH] Image Compression Android, Jio-Saavn Complete,Code Cleanup --- android/src/main/AndroidManifest.xml | 1 + .../spotiflyer/ui/AnalyticsDialog.kt | 2 +- buildSrc/buildSrc/src/main/kotlin/Versions.kt | 6 +- .../multiplatform-compose-setup.gradle.kts | 2 + common/compose/build.gradle.kts | 1 + .../{ImageLoad.kt => AndroidImageLoad.kt} | 4 +- .../shabinder/common/uikit/AndroidImages.kt | 6 ++ .../shabinder/common/uikit/DonationDialog.kt | 80 +++++++++++-------- .../{ImageLoad.kt => ExpectImageLoad.kt} | 2 +- .../shabinder/common/uikit/ExpectImages.kt | 9 ++- .../common/uikit/SpotiFlyerListUi.kt | 38 +++++++-- .../common/uikit/SpotiFlyerMainUi.kt | 24 ++++-- .../{ImageLoad.kt => DesktopImageLoad.kt} | 4 +- .../shabinder/common/uikit/DesktopImages.kt | 6 ++ .../shabinder/common/uikit/DonationDialog.kt | 3 +- .../androidMain/res/drawable/jio_saavn.xml | 8 ++ .../main/res/drawable/ic_jio_saavn_logo.xml | 8 ++ .../res/drawable/ic_opencollective_icon.xml | 5 ++ .../com/shabinder/common/di/AndroidDir.kt | 68 ++++++---------- .../com/shabinder/common/di/AndroidPicture.kt | 59 ++++++++++++++ .../kotlin/com/shabinder/common/di/Dir.kt | 78 +++++++++++------- .../common/di/saavn/JioSaavnRequests.kt | 10 +-- .../com/shabinder/common/di/DesktopDir.kt | 37 +++------ .../kotlin/com.shabinder.common.di/IOSDir.kt | 30 ++----- .../kotlin/com/shabinder/common/di/WebDir.kt | 29 ++----- .../shabinder/common/list/SpotiFlyerList.kt | 10 ++- .../list/integration/SpotiFlyerListImpl.kt | 12 ++- .../list/store/SpotiFlyerListStoreProvider.kt | 15 ++++ .../main/integration/SpotiFlyerMainImpl.kt | 2 +- .../root/integration/SpotiFlyerRootImpl.kt | 10 ++- desktop/src/jvmMain/kotlin/Main.kt | 1 + .../resources/drawable/ic_jio_saavn_logo.xml | 8 ++ .../drawable/ic_opencollective_icon.xml | 5 ++ 33 files changed, 357 insertions(+), 226 deletions(-) rename common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/{ImageLoad.kt => AndroidImageLoad.kt} (93%) rename common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/{ImageLoad.kt => ExpectImageLoad.kt} (88%) rename common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/{ImageLoad.kt => DesktopImageLoad.kt} (93%) create mode 100644 common/data-models/src/androidMain/res/drawable/jio_saavn.xml create mode 100644 common/data-models/src/main/res/drawable/ic_jio_saavn_logo.xml create mode 100644 common/data-models/src/main/res/drawable/ic_opencollective_icon.xml create mode 100644 desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml create mode 100644 desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index be6184bc..5fa70abf 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ + diff --git a/android/src/main/java/com/shabinder/spotiflyer/ui/AnalyticsDialog.kt b/android/src/main/java/com/shabinder/spotiflyer/ui/AnalyticsDialog.kt index 8453cea3..9d47b52b 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/ui/AnalyticsDialog.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/ui/AnalyticsDialog.kt @@ -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, diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index f84e6a6d..e9666e52 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -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" @@ -26,7 +27,7 @@ object Versions { // Code Formatting const val ktLint = "10.0.0" - + // DI const val koin = "3.0.2" @@ -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 diff --git a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts index 37374c07..3586e838 100644 --- a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts @@ -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") diff --git a/common/compose/build.gradle.kts b/common/compose/build.gradle.kts index fda7d277..ac27f094 100644 --- a/common/compose/build.gradle.kts +++ b/common/compose/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { } commonMain { dependencies { + implementation(compose.material) implementation(compose.materialIconsExtended) implementation(project(":common:root")) implementation(project(":common:main")) diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImageLoad.kt similarity index 93% rename from common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt rename to common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImageLoad.kt index 005706a1..ec019e31 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImageLoad.kt @@ -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 } } diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt index d0fae5a8..47cc0d40 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt @@ -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) diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt index fa09a983..71eeb0a2 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt @@ -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 - )*/ } } diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImageLoad.kt similarity index 88% rename from common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt rename to common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImageLoad.kt index c72268b5..9c8cce2d 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImageLoad.kt @@ -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() diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt index 15b2a140..2bdf26b0 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt @@ -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 ) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt index 228b0e0f..bdf96874 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt @@ -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) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt index d506a073..1691eb49 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt @@ -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) ) diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImageLoad.kt similarity index 93% rename from common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt rename to common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImageLoad.kt index 528383ec..045da16d 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/ImageLoad.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImageLoad.kt @@ -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(null) } LaunchedEffect(link) { withContext(dispatcherIO) { - pic = loader(link).image + pic = loader().image } } diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt index 86887f09..d5e9625d 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt @@ -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 diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt index c693ced1..4d04bba3 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt @@ -27,7 +27,8 @@ import com.shabinder.common.models.methods @Composable actual fun DonationDialog( isVisible: Boolean, - onDismiss: () -> Unit + onDismiss: () -> Unit, + onSnooze: () -> Unit ) { AnimatedVisibility( isVisible diff --git a/common/data-models/src/androidMain/res/drawable/jio_saavn.xml b/common/data-models/src/androidMain/res/drawable/jio_saavn.xml new file mode 100644 index 00000000..1a84ca9a --- /dev/null +++ b/common/data-models/src/androidMain/res/drawable/jio_saavn.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/common/data-models/src/main/res/drawable/ic_jio_saavn_logo.xml b/common/data-models/src/main/res/drawable/ic_jio_saavn_logo.xml new file mode 100644 index 00000000..1a84ca9a --- /dev/null +++ b/common/data-models/src/main/res/drawable/ic_jio_saavn_logo.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/common/data-models/src/main/res/drawable/ic_opencollective_icon.xml b/common/data-models/src/main/res/drawable/ic_opencollective_icon.xml new file mode 100644 index 00000000..b1ac9100 --- /dev/null +++ b/common/data-models/src/main/res/drawable/ic_opencollective_icon.xml @@ -0,0 +1,5 @@ + + + + diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index 5d6e8258..27bc641b 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -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 } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidPicture.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidPicture.kt index 35b485a7..2350a801 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidPicture.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidPicture.kt @@ -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 +} diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index 63bc9a08..453d3e79 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -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 { 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 diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt index 3e9fc99b..fd5ba910 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt @@ -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(URL) { - body = FormDataContent( - Parameters.build { - append("bitrate", "320") - } - ) - } + val res = httpClient.get(URL) return try { res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last() } catch (e: IndexOutOfBoundsException) { diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt index d75ffe32..54026083 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt @@ -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( diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt index bd4c37f3..10dae478 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt @@ -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 } diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt index 0b7cc8f3..c909d10e 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt @@ -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 { diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index 0f890395..97eb28f4 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -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 = emptyList(), - val errorOccurred: Exception? = null + val errorOccurred: Exception? = null, + val askForDonation: Boolean = false, ) } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 8bdaf173..8d367b1f 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -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() override val models: Value = 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) } } } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index 076eeda2..e1368c05 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -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) : 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() { @@ -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 { diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index 229f8c20..5b4b30ad 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -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) } } } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index 8dca868f..a0921a43 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -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() } } diff --git a/desktop/src/jvmMain/kotlin/Main.kt b/desktop/src/jvmMain/kotlin/Main.kt index 2df6a75f..5b651ac8 100644 --- a/desktop/src/jvmMain/kotlin/Main.kt +++ b/desktop/src/jvmMain/kotlin/Main.kt @@ -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 diff --git a/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml new file mode 100644 index 00000000..1a84ca9a --- /dev/null +++ b/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml b/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml new file mode 100644 index 00000000..b1ac9100 --- /dev/null +++ b/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml @@ -0,0 +1,5 @@ + + + +