Maintenance Tasks, workflows, code cleanup

This commit is contained in:
shabinder 2021-05-18 20:02:33 +05:30
parent 68dc52c6a5
commit e7a983fe00
73 changed files with 305 additions and 299 deletions

35
.github/workflows/maintenance.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Maintenance
on:
workflow_dispatch:
schedule:
- cron: '*/30 * * * *' #every 30 minutes
jobs:
build:
name: Test and Build
runs-on: ubuntu-latest
steps:
# Setup Java environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 15
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.3.1
# Run Maintenance Tasks
- name: Android App
run: ./gradlew :maintenance-tasks:run
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
OWNER_NAME: 'Shabinder'
REPO_NAME: 'SpotiFlyer'
BRANCH_NAME: 'main'
FILE_PATH: 'README.md'
IMAGE_DESCRIPTION: 'Analytics'
COMMIT_MESSAGE: 'Analytics Updated'
TAG_NAME: 'HTI'

3
.gitignore vendored
View File

@ -10,4 +10,5 @@ terraform.tfvars
/fastlane/report.xml /fastlane/report.xml
/fastlane/README.md /fastlane/README.md
Gemfile Gemfile
Gemfile.lock Gemfile.lock
/maintenance-tasks/build/

View File

@ -77,7 +77,7 @@ android {
} }
compileOptions { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true coreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }

View File

@ -31,9 +31,8 @@ repositories {
} }
dependencies { dependencies {
implementation("com.android.tools.build:gradle:4.2.0") implementation("com.android.tools.build:gradle:4.0.2")
implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}") implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}")
//implementation("io.github.gradle-nexus.publish-plugin:1.1.0")
implementation(JetBrains.Compose.gradlePlugin) implementation(JetBrains.Compose.gradlePlugin)
implementation(JetBrains.Kotlin.gradlePlugin) implementation(JetBrains.Kotlin.gradlePlugin)
implementation(JetBrains.Kotlin.serialization) implementation(JetBrains.Kotlin.serialization)

View File

@ -36,7 +36,7 @@ object Versions {
// Internet // Internet
const val ktor = "1.5.4" const val ktor = "1.5.4"
const val kotlinxSerialization = "1.2.0" const val kotlinxSerialization = "1.2.1"
// Database // Database
const val sqlDelight = "1.5.0" const val sqlDelight = "1.5.0"
@ -158,7 +158,7 @@ object JetpackDataStore {
} }
object Serialization { object Serialization {
val core = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}" val json = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}"
} }
object SqlDelight { object SqlDelight {

View File

@ -28,7 +28,6 @@ import androidx.compose.ui.text.font.FontWeight
import com.shabinder.common.database.R import com.shabinder.common.database.R
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
actual fun montserratFont() = FontFamily( actual fun montserratFont() = FontFamily(
Font(R.font.montserrat_light, FontWeight.Light), Font(R.font.montserrat_light, FontWeight.Light),
Font(R.font.montserrat_regular, FontWeight.Normal), Font(R.font.montserrat_regular, FontWeight.Normal),

View File

@ -3,7 +3,6 @@ package com.shabinder.common.uikit
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -30,4 +29,4 @@ actual fun VerticalScrollbar(
modifier: Modifier, modifier: Modifier,
adapter: ScrollbarAdapter adapter: ScrollbarAdapter
) { ) {
} }

View File

@ -10,37 +10,24 @@ 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.AlertDialog
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CardGiftcard
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import com.shabinder.common.uikit.PaypalLogo
import com.shabinder.common.uikit.RazorPay
import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorAccent
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
actual fun DonationDialog( actual fun DonationDialog(
isVisible:Boolean, isVisible: Boolean,
onDismiss:()->Unit onDismiss: () -> Unit
){ ) {
AnimatedVisibility( AnimatedVisibility(
isVisible isVisible
) { ) {
@ -84,10 +71,12 @@ actual fun DonationDialog(
} }
Row( Row(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp) modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
.clickable(onClick = { .clickable(
onDismiss() onClick = {
methods.value.giveDonation() onDismiss()
}), methods.value.giveDonation()
}
),
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(32.dp), tint = Color(0xFFCCCCCC))
@ -138,4 +127,4 @@ actual fun DonationDialog(
onDismissRequest = onDismiss onDismissRequest = onDismiss
)*/ )*/
} }
} }

View File

@ -2,7 +2,12 @@ package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.runtime.* 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -10,7 +15,6 @@ import com.shabinder.common.di.Picture
import com.shabinder.common.di.dispatcherIO import com.shabinder.common.di.dispatcherIO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Composable @Composable
actual fun ImageLoad( actual fun ImageLoad(
link: String, link: String,

View File

@ -20,7 +20,6 @@ package com.shabinder.common.uikit
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import com.shabinder.common.di.Picture
@Composable @Composable
expect fun DownloadImageTick() expect fun DownloadImageTick()
@ -69,6 +68,6 @@ expect fun DownloadImageArrow(modifier: Modifier)
@Composable @Composable
expect fun DonationDialog( expect fun DonationDialog(
isVisible:Boolean, isVisible: Boolean,
onDismiss:()->Unit onDismiss: () -> Unit
) )

View File

@ -11,4 +11,4 @@ expect fun ImageLoad(
desc: String = "Album Art", desc: String = "Album Art",
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
// placeholder:Painter = PlaceHolderImage() // placeholder:Painter = PlaceHolderImage()
) )

View File

@ -3,7 +3,6 @@ package com.shabinder.common.uikit
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -27,4 +26,4 @@ expect fun rememberScrollbarAdapter(
expect fun VerticalScrollbar( expect fun VerticalScrollbar(
modifier: Modifier, modifier: Modifier,
adapter: ScrollbarAdapter adapter: ScrollbarAdapter
) )

View File

@ -223,7 +223,7 @@ fun SearchPanel(
@Composable @Composable
fun AboutColumn( fun AboutColumn(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
donationDialogOpenEvent:() -> Unit donationDialogOpenEvent: () -> Unit
) { ) {
Box { Box {
@ -342,10 +342,12 @@ fun AboutColumn(
Row( Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { .clickable(
isDonationDialogVisible = true onClick = {
donationDialogOpenEvent() isDonationDialogVisible = true
}), donationDialogOpenEvent()
}
),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Rounded.CardGiftcard, "Support Developer") Icon(Icons.Rounded.CardGiftcard, "Support Developer")
@ -357,7 +359,7 @@ fun AboutColumn(
) )
Text( Text(
text = "If you think I deserve to get paid for my work, you can support me here.", text = "If you think I deserve to get paid for my work, you can support me here.",
//text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.", // text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.",
style = SpotiFlyerTypography.subtitle2 style = SpotiFlyerTypography.subtitle2
) )
} }

View File

@ -37,15 +37,16 @@ import androidx.compose.foundation.layout.fillMaxSize
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.* import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -110,7 +111,7 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, modifier: Modifier = Modifi
} }
@Composable @Composable
fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp, component: SpotiFlyerRoot) { fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.dp, component: SpotiFlyerRoot) {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f) val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
@ -128,7 +129,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
val callBacks = component.callBacks val callBacks = component.callBacks
AppBar( AppBar(
backgroundColor = appBarColor, backgroundColor = appBarColor,
onBackPressed = callBacks::popBackToHomeScreen , onBackPressed = callBacks::popBackToHomeScreen,
setDownloadDirectory = callBacks::setDownloadDirectory, setDownloadDirectory = callBacks::setDownloadDirectory,
isBackButtonVisible = activeComponent.value.activeChild.instance is Child.List, isBackButtonVisible = activeComponent.value.activeChild.instance is Child.List,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@ -150,8 +151,8 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
@Composable @Composable
fun AppBar( fun AppBar(
backgroundColor: Color, backgroundColor: Color,
onBackPressed:()->Unit, onBackPressed: () -> Unit,
setDownloadDirectory:()->Unit, setDownloadDirectory: () -> Unit,
isBackButtonVisible: Boolean, isBackButtonVisible: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -181,11 +182,11 @@ fun AppBar(
} }
}, },
actions = { actions = {
IconButton( IconButton(
onClick = { setDownloadDirectory() } onClick = { setDownloadDirectory() }
) { ) {
Icon(Icons.Filled.Settings,"Preferences", tint = Color.Gray) Icon(Icons.Filled.Settings, "Preferences", tint = Color.Gray)
} }
}, },
modifier = modifier, modifier = modifier,
elevation = 0.dp elevation = 0.dp

View File

@ -17,8 +17,6 @@
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
enum class ToastDuration(val value: Int) { enum class ToastDuration(val value: Int) {

View File

@ -1,2 +1 @@
package com.shabinder.common.uikit.dialogs package com.shabinder.common.uikit.dialogs

View File

@ -17,27 +17,17 @@
@file:Suppress("FunctionName") @file:Suppress("FunctionName")
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorXmlResource import androidx.compose.ui.res.vectorXmlResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font import androidx.compose.ui.text.platform.Font
import com.shabinder.common.di.Picture
import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.models.methods
import kotlinx.coroutines.withContext
@Composable @Composable
actual fun DownloadImageTick() { actual fun DownloadImageTick() {
@ -79,11 +69,11 @@ actual fun DownloadImageArrow(modifier: Modifier) {
actual fun DownloadAllImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_download_arrow.xml")) as Painter actual fun DownloadAllImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_download_arrow.xml")) as Painter
@Composable @Composable
actual fun ShareImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_share_open.xml")) as Painter actual fun ShareImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_share_open.xml")) as Painter
@Composable @Composable
actual fun PlaceHolderImage() = rememberVectorPainter(vectorXmlResource("drawable/music.xml")) actual fun PlaceHolderImage() = rememberVectorPainter(vectorXmlResource("drawable/music.xml"))
as Painter as Painter
@Composable @Composable
actual fun SpotiFlyerLogo() = actual fun SpotiFlyerLogo() =
rememberVectorPainter(vectorXmlResource("drawable/ic_spotiflyer_logo.xml")) as Painter rememberVectorPainter(vectorXmlResource("drawable/ic_spotiflyer_logo.xml")) as Painter
@ -92,10 +82,10 @@ actual fun SpotiFlyerLogo() =
actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_heart.xml")) as Painter actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_heart.xml")) as Painter
@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 @Composable
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
@Composable @Composable
actual fun GaanaLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_gaana.xml")) as Painter actual fun GaanaLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_gaana.xml")) as Painter

View File

@ -43,4 +43,4 @@ actual fun VerticalScrollbar(
modifier = modifier, modifier = modifier,
adapter = adapter adapter = adapter
) )
} }

View File

@ -26,16 +26,13 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -57,7 +54,7 @@ actual fun Toast(
val state = flow.collectAsState("") val state = flow.collectAsState("")
val message = state.value val message = state.value
AnimatedVisibility ( AnimatedVisibility(
visible = message != "", visible = message != "",
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 4 }), enter = fadeIn() + slideInVertically(initialOffsetY = { it / 4 }),
exit = slideOutHorizontally(targetOffsetX = { it / 4 }) + fadeOut() exit = slideOutHorizontally(targetOffsetX = { it / 4 }) + fadeOut()
@ -67,12 +64,12 @@ actual fun Toast(
contentAlignment = Alignment.BottomEnd contentAlignment = Alignment.BottomEnd
) { ) {
Surface( Surface(
modifier = Modifier.sizeIn(maxWidth = 250.dp,maxHeight = 80.dp), modifier = Modifier.sizeIn(maxWidth = 250.dp, maxHeight = 80.dp),
color = Color(23, 23, 23), color = Color(23, 23, 23),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, colorOffWhite) border = BorderStroke(1.dp, colorOffWhite)
) { ) {
Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){ Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Text( Text(
text = message, text = message,
color = Color(210, 210, 210), color = Color(210, 210, 210),

View File

@ -26,9 +26,9 @@ import com.shabinder.common.models.methods
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
actual fun DonationDialog( actual fun DonationDialog(
isVisible:Boolean, isVisible: Boolean,
onDismiss:()->Unit onDismiss: () -> Unit
){ ) {
AnimatedVisibility( AnimatedVisibility(
isVisible isVisible
) { ) {
@ -72,10 +72,12 @@ actual fun DonationDialog(
} }
Row( Row(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp) modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
.clickable(onClick = { .clickable(
onDismiss() onClick = {
methods.value.giveDonation() onDismiss()
}), methods.value.giveDonation()
}
),
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(32.dp), tint = Color(0xFFCCCCCC))
@ -95,4 +97,4 @@ actual fun DonationDialog(
} }
} }
} }
} }

View File

@ -1,9 +1,13 @@
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.runtime.* 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T) actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -17,7 +17,7 @@ actual interface PlatformActions {
fun sendTracksToService(array: ArrayList<TrackDetails>) fun sendTracksToService(array: ArrayList<TrackDetails>)
} }
actual val StubPlatformActions = object: PlatformActions { actual val StubPlatformActions = object : PlatformActions {
override val imageCacheDir = "" override val imageCacheDir = ""
override val sharedPreferences: SharedPreferences? = null override val sharedPreferences: SharedPreferences? = null
@ -25,4 +25,4 @@ actual val StubPlatformActions = object: PlatformActions {
override fun addToLibrary(path: String) {} override fun addToLibrary(path: String) {}
override fun sendTracksToService(array: ArrayList<TrackDetails>) {} override fun sendTracksToService(array: ArrayList<TrackDetails>) {}
} }

View File

@ -185,7 +185,7 @@ internal class RealCache<Key : Any, Value : Any>(
*/ */
private fun CacheEntry<Key, Value>.isExpired(): Boolean { private fun CacheEntry<Key, Value>.isExpired(): Boolean {
return expiresAfterAccess && (accessTimeMark.get() + expireAfterAccessDuration).hasPassedNow() || return expiresAfterAccess && (accessTimeMark.get() + expireAfterAccessDuration).hasPassedNow() ||
expiresAfterWrite && (writeTimeMark.get() + expireAfterWriteDuration).hasPassedNow() expiresAfterWrite && (writeTimeMark.get() + expireAfterWriteDuration).hasPassedNow()
} }
/** /**

View File

@ -41,8 +41,7 @@ interface Actions {
fun writeMp3Tags(trackDetails: TrackDetails) fun writeMp3Tags(trackDetails: TrackDetails)
} }
private fun stubActions(): Actions = object : Actions {
private fun stubActions(): Actions = object :Actions {
override val platformActions = StubPlatformActions override val platformActions = StubPlatformActions
override fun showPopUpMessage(string: String, long: Boolean) {} override fun showPopUpMessage(string: String, long: Boolean) {}
override fun setDownloadDirectoryAction() {} override fun setDownloadDirectoryAction() {}
@ -53,4 +52,4 @@ private fun stubActions(): Actions = object :Actions {
override fun writeMp3Tags(trackDetails: TrackDetails) {} override fun writeMp3Tags(trackDetails: TrackDetails) {}
override val isInternetAvailable: Boolean = true override val isInternetAvailable: Boolean = true
} }

View File

@ -20,4 +20,4 @@ sealed class AllPlatforms {
object Js : AllPlatforms() object Js : AllPlatforms()
object Jvm : AllPlatforms() object Jvm : AllPlatforms()
object Native : AllPlatforms() object Native : AllPlatforms()
} }

View File

@ -2,4 +2,4 @@ package com.shabinder.common.models
expect interface PlatformActions expect interface PlatformActions
expect val StubPlatformActions : PlatformActions expect val StubPlatformActions: PlatformActions

View File

@ -58,7 +58,5 @@ enum class Status constructor(val value: Int) {
else -> NONE else -> NONE
} }
} }
} }
}
}

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T) actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -1,5 +1,5 @@
package com.shabinder.common.models package com.shabinder.common.models
actual interface PlatformActions {} actual interface PlatformActions
actual val StubPlatformActions = object: PlatformActions {} actual val StubPlatformActions = object : PlatformActions {}

View File

@ -2,8 +2,8 @@ package com.shabinder.common.models
import kotlin.native.concurrent.AtomicReference import kotlin.native.concurrent.AtomicReference
actual interface PlatformActions {} actual interface PlatformActions
actual val StubPlatformActions = object: PlatformActions {} actual val StubPlatformActions = object : PlatformActions {}
actual typealias NativeAtomicReference<T> = AtomicReference<T> actual typealias NativeAtomicReference<T> = AtomicReference<T>

View File

@ -1,4 +1,4 @@
package com.shabinder.common.models package com.shabinder.common.models
actual interface PlatformActions {} actual interface PlatformActions
actual val StubPlatformActions = object: PlatformActions {} actual val StubPlatformActions = object : PlatformActions {}

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T) actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -54,7 +54,7 @@ kotlin {
} }
} }
if(HostOS.isMac) { if (HostOS.isMac) {
val iosMain by getting { val iosMain by getting {
dependencies { dependencies {
implementation(SqlDelight.nativeDriver) implementation(SqlDelight.nativeDriver)

View File

@ -3,8 +3,8 @@ package com.shabinder.common.database
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import co.touchlab.kermit.NSLogLogger import co.touchlab.kermit.NSLogLogger
import com.shabinder.database.Database import com.shabinder.database.Database
import org.koin.dsl.module
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import org.koin.dsl.module
@Suppress("RedundantNullableReturnType") @Suppress("RedundantNullableReturnType")
actual fun databaseModule() = module { actual fun databaseModule() = module {
@ -14,4 +14,4 @@ actual fun databaseModule() = module {
} }
} }
actual fun getLogger(): Logger = NSLogLogger() actual fun getLogger(): Logger = NSLogLogger()

View File

@ -18,7 +18,6 @@ package com.shabinder.common.database
import co.touchlab.kermit.CommonLogger import co.touchlab.kermit.CommonLogger
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.shabinder.database.Database
import org.koin.dsl.module import org.koin.dsl.module
actual fun databaseModule() = module { single { SpotiFlyerDatabase(null) } } actual fun databaseModule() = module { single { SpotiFlyerDatabase(null) } }

View File

@ -36,4 +36,4 @@ actual suspend fun downloadTracks(
if (!list.isNullOrEmpty()) { if (!list.isNullOrEmpty()) {
methods.value.platformActions.sendTracksToService(ArrayList(list)) methods.value.platformActions.sendTracksToService(ArrayList(list))
} }
} }

View File

@ -54,10 +54,10 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch" const val firstLaunch = "firstLaunch"
} }
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone(){ actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false) settings.putBoolean(firstLaunch, false)
} }
/* /*
@ -67,10 +67,10 @@ actual class Dir actual constructor(
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() { actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true) settings.putBoolean(AnalyticsKey, true)
} }
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath) 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()
@ -81,7 +81,7 @@ actual class Dir actual constructor(
// fun call in order to always access Updated Value // fun call in order to always access Updated Value
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
File.separator + "SpotiFlyer" + File.separator File.separator + "SpotiFlyer" + File.separator
actual fun isPresent(path: String): Boolean = File(path).exists() actual fun isPresent(path: String): Boolean = File(path).exists()
@ -106,14 +106,14 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata( actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray, mp3ByteArray: ByteArray,
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess: (track: TrackDetails) -> Unit
) = withContext(dispatcherIO) { ) = withContext(dispatcherIO) {
val songFile = File(trackDetails.outputFilePath) val songFile = File(trackDetails.outputFilePath)
try { try {
/* /*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received * Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */ * */
if(!songFile.exists()) { if (!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/ /*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile?.mkdirs() songFile.parentFile?.mkdirs()
} }
@ -163,15 +163,15 @@ actual class Dir actual constructor(
} catch (e: Exception) { e.printStackTrace() } } catch (e: Exception) { e.printStackTrace() }
} }
} }
}catch (e:Exception){ } catch (e: Exception) {
if(songFile.exists()) songFile.delete() if (songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" } logger.e { "${songFile.absolutePath} could not be created" }
} }
} }
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): Picture = withContext(dispatcherIO) {
val cachePath = imageCacheDir() + getNameURL(url) val cachePath = imageCacheDir() + getNameURL(url)
Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap()) Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap())
} }
@ -189,7 +189,7 @@ actual class Dir actual constructor(
} }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) { actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
try { try {
FileOutputStream(path).use { out -> FileOutputStream(path).use { out ->
(image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out) (image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out)

View File

@ -24,7 +24,10 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkRequest import android.net.NetworkRequest
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.lang.Exception import java.lang.Exception
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -104,7 +107,7 @@ class ConnectionLiveData(context: Context) : LiveData<Boolean>() {
* If successful, that means we have internet. * If successful, that means we have internet.
*/ */
object DoesNetworkHaveInternet { object DoesNetworkHaveInternet {
suspend fun execute(network: Network): Boolean = withContext(Dispatchers.IO) { suspend fun execute(network: Network): Boolean = withContext(Dispatchers.IO) {
try { try {
Log.d(TAG, "PINGING google.") Log.d(TAG, "PINGING google.")
val socket = network.socketFactory.createSocket() ?: throw IOException("Socket is null.") val socket = network.socketFactory.createSocket() ?: throw IOException("Socket is null.")
@ -112,7 +115,7 @@ class ConnectionLiveData(context: Context) : LiveData<Boolean>() {
socket.close() socket.close()
Log.d(TAG, "PING success.") Log.d(TAG, "PING success.")
true true
}catch (e:Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
// Handle VPN Connection / Google DNS Blocked Cases // Handle VPN Connection / Google DNS Blocked Cases
isInternetAccessible() isInternetAccessible()

View File

@ -17,13 +17,13 @@
package com.shabinder.common.di package com.shabinder.common.di
import android.util.Log import android.util.Log
import com.shabinder.common.models.TrackDetails
import java.io.File
import com.mpatric.mp3agic.ID3v1Tag import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
fun Mp3File.removeAllTags(): Mp3File { fun Mp3File.removeAllTags(): Mp3File {

View File

@ -33,7 +33,10 @@ import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.* import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.R
import com.shabinder.common.di.downloadFile
import com.shabinder.common.di.providers.get import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
@ -120,7 +123,8 @@ class ForegroundService : Service(), CoroutineScope {
val downloadObjects: ArrayList<TrackDetails>? = ( val downloadObjects: ArrayList<TrackDetails>? = (
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList( it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object") "object"
)
) )
downloadObjects?.let { list -> downloadObjects?.let { list ->
@ -184,7 +188,7 @@ class ForegroundService : Service(), CoroutineScope {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID) val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if (url == null) { if (url == null) {
val audioData: Format = ytDownloader?.getVideo(videoID)?.get() ?: throw Exception("Java YT Dependency Error") val audioData: Format = ytDownloader?.getVideo(videoID)?.get() ?: throw Exception("Java YT Dependency Error")
val ytUrl = audioData.url!! //We Will catch NPE val ytUrl = audioData.url!! // We Will catch NPE
enqueueDownload(ytUrl, track) enqueueDownload(ytUrl, track)
} else enqueueDownload(url, track) } else enqueueDownload(url, track)
} catch (e: Exception) { } catch (e: Exception) {
@ -210,7 +214,7 @@ class ForegroundService : Service(), CoroutineScope {
removeFromNotification("Downloading ${track.title}") removeFromNotification("Downloading ${track.title}")
failed++ failed++
updateNotification() updateNotification()
sendTrackBroadcast(Status.FAILED.name,track) sendTrackBroadcast(Status.FAILED.name, track)
} }
} }
@ -229,7 +233,7 @@ class ForegroundService : Service(), CoroutineScope {
is DownloadResult.Success -> { is DownloadResult.Success -> {
try { try {
// Save File and Embed Metadata // Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track){} } val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
allTracksStatus[track.title] = DownloadStatus.Converting allTracksStatus[track.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting", track) sendTrackBroadcast("Converting", track)
addToNotification("Processing ${track.title}") addToNotification("Processing ${track.title}")
@ -288,7 +292,7 @@ class ForegroundService : Service(), CoroutineScope {
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
downloadService.close() downloadService.close()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir()),logger) cleanFiles(File(dir.defaultDir()), logger)
// TODO cleanFiles(File(dir.imageCacheDir())) // TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("", "", "", "", "") messageList = mutableListOf("", "", "", "", "")
releaseWakeLock() releaseWakeLock()

View File

@ -8,7 +8,7 @@ import java.io.File
/** /**
* Cleaning All Residual Files except Mp3 Files * Cleaning All Residual Files except Mp3 Files
**/ **/
fun cleanFiles(dir: File,logger: Kermit) { fun cleanFiles(dir: File, logger: Kermit) {
try { try {
logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " } logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " }
val fList = dir.listFiles() val fList = dir.listFiles()
@ -24,12 +24,12 @@ fun cleanFiles(dir: File,logger: Kermit) {
} }
} }
} }
} catch (e:Exception) { e.printStackTrace() } } catch (e: Exception) { e.printStackTrace() }
} }
/** /**
* Cleaning All Residual Files except Mp3 Files * Cleaning All Residual Files except Mp3 Files
**/ **/
fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) { fun cleanFiles(directory: AbstractFile, fm: FileManager, logger: Kermit) {
try { try {
logger.d("Files Cleaning") { "Starting Cleaning in ${directory.getFullPath()} " } logger.d("Files Cleaning") { "Starting Cleaning in ${directory.getFullPath()} " }
val fList = fm.listFiles(directory) val fList = fm.listFiles(directory)
@ -37,14 +37,13 @@ fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) {
if (fm.isDirectory(file)) { if (fm.isDirectory(file)) {
cleanFiles(file, fm, logger) cleanFiles(file, fm, logger)
} else if (fm.isFile(file)) { } else if (fm.isFile(file)) {
if (file.getFullPath().substringAfterLast(".") != "mp3" if (file.getFullPath().substringAfterLast(".") != "mp3" ||
||
fm.getLength(file) == 0L fm.getLength(file) == 0L
) { ) {
logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" } logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" }
fm.delete(file) fm.delete(file)
} }
} }
} }
} catch (e:Exception) { e.printStackTrace() } } catch (e: Exception) { e.printStackTrace() }
} }

View File

@ -25,10 +25,13 @@ import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider import com.shabinder.common.di.providers.YoutubeProvider
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.features.json.* import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.* import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.* import io.ktor.client.features.logging.DEFAULT
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logger
import io.ktor.client.features.logging.Logging
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.KoinAppDeclaration

View File

@ -23,7 +23,8 @@ import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database import com.shabinder.database.Database
import io.ktor.client.request.* import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.HttpStatement import io.ktor.client.statement.HttpStatement
import io.ktor.http.contentLength import io.ktor.http.contentLength
import io.ktor.http.isSuccess import io.ktor.http.isSuccess
@ -31,14 +32,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt import kotlin.math.roundToInt
expect class Dir ( expect class Dir(
logger: Kermit, logger: Kermit,
settings: Settings, settings: Settings,
spotiFlyerDatabase: SpotiFlyerDatabase, spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
val db: Database? val db: Database?
val isAnalyticsEnabled:Boolean val isAnalyticsEnabled: Boolean
val isFirstLaunch:Boolean val isFirstLaunch: Boolean
fun enableAnalytics() fun enableAnalytics()
fun firstLaunchDone() fun firstLaunchDone()
fun isPresent(path: String): Boolean fun isPresent(path: String): Boolean
@ -46,11 +47,11 @@ expect class Dir (
fun defaultDir(): String fun defaultDir(): String
fun imageCacheDir(): String fun imageCacheDir(): String
fun createDirectory(dirPath: String) fun createDirectory(dirPath: String)
fun setDownloadDirectory(newBasePath:String) fun setDownloadDirectory(newBasePath: String)
suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage
suspend fun loadImage(url: String): Picture suspend fun loadImage(url: String): Picture
suspend fun clearCache() suspend fun clearCache()
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)
} }
@ -74,7 +75,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
emit(DownloadResult.Error("File not downloaded")) emit(DownloadResult.Error("File not downloaded"))
} }
client.close() client.close()
} catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
emit(DownloadResult.Error(e.message ?: "File not downloaded")) emit(DownloadResult.Error(e.message ?: "File not downloaded"))
} }
@ -83,12 +84,12 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
suspend fun downloadByteArray( suspend fun downloadByteArray(
url: String, url: String,
httpBuilder: HttpRequestBuilder.()->Unit = {} httpBuilder: HttpRequestBuilder.() -> Unit = {}
): ByteArray? { ): ByteArray? {
val client = createHttpClient() val client = createHttpClient()
val response = try { val response = try {
client.get<ByteArray>(url,httpBuilder) client.get<ByteArray>(url, httpBuilder)
} catch (e: Exception){ } catch (e: Exception) {
return null return null
} }
client.close() client.close()

View File

@ -18,8 +18,7 @@ package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods import io.ktor.client.request.head
import io.ktor.client.request.*
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -31,7 +30,6 @@ expect suspend fun downloadTracks(
dir: Dir dir: Dir
) )
// IO-Dispatcher // IO-Dispatcher
@SharedImmutable @SharedImmutable
expect val dispatcherIO: CoroutineDispatcher expect val dispatcherIO: CoroutineDispatcher

View File

@ -79,7 +79,7 @@ class GaanaProvider(
it.updateStatusIfPresent(folderType, subFolder) it.updateStatusIfPresent(folderType, subFolder)
trackList = listOf(it).toTrackDetailsList(folderType, subFolder) trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.track_title title = it.track_title
coverUrl = it.artworkLink.replace("http:","https:") coverUrl = it.artworkLink.replace("http:", "https:")
} }
} }
"album" -> { "album" -> {
@ -91,7 +91,7 @@ class GaanaProvider(
} }
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList() trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
title = link title = link
coverUrl = it.custom_artworks.size_480p.replace("http:","https:") coverUrl = it.custom_artworks.size_480p.replace("http:", "https:")
} }
} }
"playlist" -> { "playlist" -> {
@ -114,7 +114,7 @@ class GaanaProvider(
getGaanaArtistDetails(seokey = link).artist.firstOrNull() getGaanaArtistDetails(seokey = link).artist.firstOrNull()
?.also { ?.also {
title = it.name title = it.name
coverUrl = it.artworkLink?.replace("http:","https:") ?: gaanaPlaceholderImageUrl coverUrl = it.artworkLink?.replace("http:", "https:") ?: gaanaPlaceholderImageUrl
} }
getGaanaArtistTracks(seokey = link).also { getGaanaArtistTracks(seokey = link).also {
it.tracks?.forEach { track -> it.tracks?.forEach { track ->
@ -143,7 +143,7 @@ class GaanaProvider(
trackUrl = it.lyrics_url, trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana, source = Source.Gaana,
albumArtURL = it.artworkLink.replace("http:","https:"), albumArtURL = it.artworkLink.replace("http:", "https:"),
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/) outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
) )
} }

View File

@ -62,9 +62,9 @@ class SpotifyProvider(
defaultRequest { defaultRequest {
header("Authorization", "Bearer ${token.access_token}") header("Authorization", "Bearer ${token.access_token}")
} }
install(JsonFeature) { install(JsonFeature) {
serializer = KotlinxSerializer(globalJson) serializer = KotlinxSerializer(globalJson)
} }
}.also { httpClientRef.value = it } }.also { httpClientRef.value = it }
} }
} }
@ -98,7 +98,7 @@ class SpotifyProvider(
type, type,
link link
) )
}catch (e: Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions // Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
authenticateSpotifyClient(true) authenticateSpotifyClient(true)
@ -108,7 +108,7 @@ class SpotifyProvider(
type, type,
link link
) )
} catch (e:Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }

View File

@ -21,7 +21,6 @@ import com.shabinder.common.di.Dir
import com.shabinder.common.di.currentPlatform import com.shabinder.common.di.currentPlatform
import com.shabinder.common.di.youtubeMp3.Yt1sMp3 import com.shabinder.common.di.youtubeMp3.Yt1sMp3
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.methods
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
class YoutubeMp3( class YoutubeMp3(

View File

@ -54,7 +54,7 @@ class YoutubeMusic constructor(
trackArtists = trackDetails.artists, trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec trackDurationSec = trackDetails.durationSec
).keys.firstOrNull() ).keys.firstOrNull()
} catch (e:Exception) { } catch (e: Exception) {
// All Internet/Client Related Errors // All Internet/Client Related Errors
e.printStackTrace() e.printStackTrace()
null null

View File

@ -38,7 +38,7 @@ class YoutubeProvider(
private val dir: Dir, private val dir: Dir,
) { ) {
// Youtube Downloader isn't fully compatible with JS Yet // Youtube Downloader isn't fully compatible with JS Yet
val ytDownloader: YoutubeDownloader? = if(currentPlatform == AllPlatforms.Js) null else YoutubeDownloader() val ytDownloader: YoutubeDownloader? = if (currentPlatform == AllPlatforms.Js) null else YoutubeDownloader()
/* /*
* YT Album Art Schema * YT Album Art Schema
@ -102,25 +102,25 @@ class YoutubeProvider(
val videos = playlist.videos val videos = playlist.videos
coverUrl = "https://i.ytimg.com/vi/${ coverUrl = "https://i.ytimg.com/vi/${
videos.firstOrNull()?.videoId videos.firstOrNull()?.videoId
}/hqdefault.jpg" }/hqdefault.jpg"
title = name title = name
trackList = videos.map { trackList = videos.map {
TrackDetails( TrackDetails(
title = it.title ?: "N/A", title = it.title ?: "N/A",
artists = listOf(it.author ?: "N/A"), artists = listOf(it.author ?: "N/A"),
durationSec = it.lengthSeconds, durationSec = it.lengthSeconds,
albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg", albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
source = Source.YouTube, source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg", albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
downloaded = if (dir.isPresent( downloaded = if (dir.isPresent(
dir.finalOutputDir( dir.finalOutputDir(
itemName = it.title ?: "N/A", itemName = it.title ?: "N/A",
type = folderType, type = folderType,
subFolder = subFolder, subFolder = subFolder,
dir.defaultDir() dir.defaultDir()
) )
) )
) )
DownloadStatus.Downloaded DownloadStatus.Downloaded
@ -170,11 +170,11 @@ class YoutubeProvider(
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (dir.isPresent( downloaded = if (dir.isPresent(
dir.finalOutputDir( dir.finalOutputDir(
itemName = name, itemName = name,
type = folderType, type = folderType,
subFolder = subFolder, subFolder = subFolder,
defaultDir = dir.defaultDir() defaultDir = dir.defaultDir()
) )
) )
) )
DownloadStatus.Downloaded DownloadStatus.Downloaded

View File

@ -34,7 +34,7 @@ suspend fun authenticateSpotify(): TokenData? {
if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") { if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") }) body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
} else null } else null
}catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }

View File

@ -23,14 +23,14 @@ import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
import com.shabinder.common.models.spotify.Playlist import com.shabinder.common.models.spotify.Playlist
import com.shabinder.common.models.spotify.Track import com.shabinder.common.models.spotify.Track
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.* import io.ktor.client.request.get
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1" private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
interface SpotifyRequests { interface SpotifyRequests {
val httpClientRef: NativeAtomicReference<HttpClient> val httpClientRef: NativeAtomicReference<HttpClient>
val httpClient:HttpClient get() = httpClientRef.value val httpClient: HttpClient get() = httpClientRef.value
suspend fun authenticateSpotifyClient(override: Boolean = false) suspend fun authenticateSpotifyClient(override: Boolean = false)

View File

@ -22,7 +22,6 @@ package com.shabinder.common.di.utils
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e // Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
import com.shabinder.common.di.dispatcherIO import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.models.methods
import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.core.Closeable
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException

View File

@ -16,21 +16,16 @@
package com.shabinder.common.di.utils package com.shabinder.common.di.utils
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import kotlin.native.concurrent.SharedImmutable
import kotlin.native.concurrent.ThreadLocal import kotlin.native.concurrent.ThreadLocal
@ThreadLocal @ThreadLocal
val json by lazy { Json { val json by lazy {
isLenient = true Json {
ignoreUnknownKeys = true isLenient = true
} } ignoreUnknownKeys = true
}
}
/** /**
* Removing Illegal Chars from File Name * Removing Illegal Chars from File Name

View File

@ -59,7 +59,7 @@ actual suspend fun downloadTracks(
) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) } ) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) }
) )
} else { // Found Youtube Video ID } else { // Found Youtube Video ID
downloadTrack(videoId, it, dir::saveFileWithMetadata,fetcher.youtubeMp3) downloadTrack(videoId, it, dir::saveFileWithMetadata, fetcher.youtubeMp3)
} }
} }
} }
@ -102,7 +102,7 @@ suspend fun downloadTrack(
) )
} }
is DownloadResult.Success -> { // Todo clear map is DownloadResult.Success -> { // Todo clear map
saveFileWithMetaData(it.byteArray, trackDetails){} saveFileWithMetaData(it.byteArray, trackDetails) {}
DownloadProgressFlow.emit( DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0

View File

@ -50,16 +50,16 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch" const val firstLaunch = "firstLaunch"
} }
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone(){ actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false) settings.putBoolean(firstLaunch, false)
} }
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() { actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true) settings.putBoolean(AnalyticsKey, true)
} }
init { init {
@ -76,7 +76,7 @@ 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 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()
@ -110,19 +110,19 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata( actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray, mp3ByteArray: ByteArray,
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess: (track: TrackDetails) -> Unit
) { ) {
val songFile = File(trackDetails.outputFilePath) val songFile = File(trackDetails.outputFilePath)
try { try {
/* /*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received * Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */ * */
if(!songFile.exists()) { if (!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/ /*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile.mkdirs() songFile.parentFile.mkdirs()
} }
if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray) if (mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
when (trackDetails.outputFilePath.substringAfterLast('.')) { when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> { ".mp3" -> {
@ -167,11 +167,11 @@ actual class Dir actual constructor(
} catch (e: Exception) { e.printStackTrace() } } catch (e: Exception) { e.printStackTrace() }
} }
} }
}catch (e:Exception){ } catch (e: Exception) {
withContext(Dispatchers.Main){ withContext(Dispatchers.Main) {
//Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show() // Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
} }
if(songFile.exists()) songFile.delete() if (songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" } logger.e { "${songFile.absolutePath} could not be created" }
} }
} }

View File

@ -1,6 +1,6 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.di.providers.getData import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
@ -30,7 +30,7 @@ actual suspend fun downloadTracks(
Downloader.execute { Downloader.execute {
if (!track.videoID.isNullOrBlank()) { // Video ID already known! if (!track.videoID.isNullOrBlank()) { // Video ID already known!
dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" } dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" }
downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata,fetcher) downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata, fetcher)
} else { } else {
val searchQuery = "${track.title} - ${track.artists.joinToString(",")}" val searchQuery = "${track.title} - ${track.artists.joinToString(",")}"
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, track) val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, track)
@ -42,7 +42,7 @@ actual suspend fun downloadTracks(
) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) } ) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
) )
} else { // Found Youtube Video ID } else { // Found Youtube Video ID
downloadTrack(videoId, track, dir::saveFileWithMetadata,fetcher) downloadTrack(videoId, track, dir::saveFileWithMetadata, fetcher)
} }
} }
} }
@ -63,7 +63,7 @@ suspend fun downloadTrack(
fetcher.dir.logger.i { "LINK: $videoID -> $link" } fetcher.dir.logger.i { "LINK: $videoID -> $link" }
if (link == null) { if (link == null) {
link = fetcher.youtubeProvider.ytDownloader.getVideo(videoID).get()?.url ?: return link = fetcher.youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url ?: return
} }
fetcher.dir.logger.i { "LINK: $videoID -> $link" } fetcher.dir.logger.i { "LINK: $videoID -> $link" }
downloadFile(link).collect { downloadFile(link).collect {
@ -81,7 +81,7 @@ suspend fun downloadTrack(
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0
) { hashMapOf() }.toMutableMap().apply { ) { hashMapOf() }.toMutableMap().apply {
set(trackDetails.title,DownloadStatus.Downloading(it.progress)) set(trackDetails.title, DownloadStatus.Downloading(it.progress))
} }
} }
is DownloadResult.Success -> { // Todo clear map is DownloadResult.Success -> { // Todo clear map

View File

@ -1,17 +1,15 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.models.DownloadStatus
import kotlinx.coroutines.flow.MutableSharedFlow
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
/* /*
* Dependency Provider for IOS * Dependency Provider for IOS
* */ * */
object IOSDeps: KoinComponent { object IOSDeps : KoinComponent {
val dir: Dir by inject() // = get() val dir: Dir by inject() // = get()
val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get() val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get()
val database get() = dir.db val database get() = dir.db
val sharedFlow = DownloadProgressFlow val sharedFlow = DownloadProgressFlow
val defaultDispatcher = dispatcherDefault val defaultDispatcher = dispatcherDefault
} }

View File

@ -33,71 +33,71 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch" const val firstLaunch = "firstLaunch"
} }
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone() { actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false) settings.putBoolean(firstLaunch, false)
} }
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() { actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true) settings.putBoolean(AnalyticsKey, true)
} }
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path) actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
actual fun fileSeparator(): String = "/" actual fun fileSeparator(): String = "/"
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!!.path!! private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!.path!!
// TODO Error Handling // TODO Error Handling
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) 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)!!
} }
actual fun imageCacheDir(): String = imageCacheURL.path!! + fileSeparator() actual fun imageCacheDir(): String = imageCacheURL.path!! + fileSeparator()
private val imageCacheURL: NSURL by lazy { private val imageCacheURL: NSURL by lazy {
val cacheDir = NSFileManager.defaultManager.URLForDirectory(NSCachesDirectory, NSUserDomainMask,null,true,null) val cacheDir = NSFileManager.defaultManager.URLForDirectory(NSCachesDirectory, NSUserDomainMask, null, true, null)
cacheDir?.URLByAppendingPathComponent("SpotiFlyer",true)!! cacheDir?.URLByAppendingPathComponent("SpotiFlyer", true)!!
} }
actual fun createDirectory(dirPath: String) { actual fun createDirectory(dirPath: String) {
try { try {
NSFileManager.defaultManager.createDirectoryAtPath(dirPath,true,null,null) NSFileManager.defaultManager.createDirectoryAtPath(dirPath, true, null, null)
} catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
fun createDirectory(dirURL: NSURL) { fun createDirectory(dirURL: NSURL) {
try { try {
NSFileManager.defaultManager.createDirectoryAtURL(dirURL,true,null,null) NSFileManager.defaultManager.createDirectoryAtURL(dirURL, true, null, null)
} catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO){ actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
try { try {
(image as? UIImage)?.let { (image as? UIImage)?.let {
// We Will Be Using JPEG as default format everywhere // We Will Be Using JPEG as default format everywhere
UIImageJPEGRepresentation(it,1.0) UIImageJPEGRepresentation(it, 1.0)
?.writeToFile(path,true) ?.writeToFile(path, true)
} }
}catch (e:Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){ actual suspend fun loadImage(url: String): 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))
@ -110,24 +110,24 @@ actual class Dir actual constructor(
private fun loadCachedImage(filePath: String): UIImage? { private fun loadCachedImage(filePath: String): UIImage? {
return try { return try {
UIImage.imageWithContentsOfFile(filePath) UIImage.imageWithContentsOfFile(filePath)
}catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }
} }
private suspend fun loadFreshImage(url: String):UIImage? = withContext(dispatcherIO){ private suspend fun loadFreshImage(url: String): 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)
if (data != null) { if (data != null) {
UIImage.imageWithData(data)?.also { UIImage.imageWithData(data)?.also {
GlobalScope.launch { GlobalScope.launch {
cacheImage(it, imageCacheDir() + getNameURL(url)) cacheImage(it, imageCacheDir() + getNameURL(url))
} }
} }
}else null } else null
}catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }
@ -136,7 +136,8 @@ actual class Dir actual constructor(
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) { actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
try { try {
val fileManager = NSFileManager.defaultManager val fileManager = NSFileManager.defaultManager
val paths = fileManager.contentsOfDirectoryAtURL(imageCacheURL, val paths = fileManager.contentsOfDirectoryAtURL(
imageCacheURL,
null, null,
NSDirectoryEnumerationSkipsHiddenFiles, NSDirectoryEnumerationSkipsHiddenFiles,
null null
@ -144,20 +145,19 @@ actual class Dir actual constructor(
paths?.forEach { paths?.forEach {
(it as? NSURL)?.let { nsURL -> (it as? NSURL)?.let { nsURL ->
// Lets Remove Cached File // Lets Remove Cached File
fileManager.removeItemAtURL(nsURL,null) fileManager.removeItemAtURL(nsURL, null)
} }
} }
}catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
actual suspend fun saveFileWithMetadata( actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray, mp3ByteArray: ByteArray,
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess: (track: TrackDetails) -> Unit
) : Unit = withContext(dispatcherIO) { ): Unit = withContext(dispatcherIO) {
try { try {
if (mp3ByteArray.isNotEmpty()) { if (mp3ByteArray.isNotEmpty()) {
mp3ByteArray.toNSData().writeToFile( mp3ByteArray.toNSData().writeToFile(
@ -167,9 +167,9 @@ actual class Dir actual constructor(
} }
when (trackDetails.outputFilePath.substringAfterLast('.')) { when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> { ".mp3" -> {
if(!isPresent(trackDetails.albumArtPath)) { if (!isPresent(trackDetails.albumArtPath)) {
val imageData = downloadByteArray( val imageData = downloadByteArray(
trackDetails.albumArtURL trackDetails.albumArtURL
)?.toNSData() )?.toNSData()
if (imageData != null) { if (imageData != null) {
UIImage.imageWithData(imageData)?.also { UIImage.imageWithData(imageData)?.also {
@ -186,7 +186,7 @@ actual class Dir actual constructor(
)*/ )*/
} }
} }
}catch (e:Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
@ -196,4 +196,4 @@ actual class Dir actual constructor(
} }
actual val db: Database? = spotiFlyerDatabase.instance actual val db: Database? = spotiFlyerDatabase.instance
} }

View File

@ -4,4 +4,4 @@ import platform.UIKit.UIImage
actual data class Picture( actual data class Picture(
val image: UIImage? val image: UIImage?
) )

View File

@ -21,4 +21,4 @@ fun NSData.toByteArray(): ByteArray = memScoped {
val nsData = ByteArray(size) val nsData = ByteArray(size)
memcpy(nsData.refTo(0), bytes, size.toULong()) memcpy(nsData.refTo(0), bytes, size.toULong())
return nsData return nsData
} }

View File

@ -20,7 +20,6 @@ import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus 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 kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -75,7 +74,7 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPl
when (it) { when (it) {
is DownloadResult.Success -> { is DownloadResult.Success -> {
println("Download Completed") println("Download Completed")
dir.saveFileWithMetadata(it.byteArray, track){} dir.saveFileWithMetadata(it.byteArray, track) {}
} }
is DownloadResult.Error -> { is DownloadResult.Error -> {
allTracksStatus[track.title] = DownloadStatus.Failed allTracksStatus[track.title] = DownloadStatus.Failed

View File

@ -43,19 +43,19 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch" const val firstLaunch = "firstLaunch"
} }
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone() { actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false) settings.putBoolean(firstLaunch, false)
} }
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() { actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true) settings.putBoolean(AnalyticsKey, true)
} }
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath) actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
/*init { /*init {
createDirectories() createDirectories()
@ -84,7 +84,7 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata( actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray, mp3ByteArray: ByteArray,
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess: (track: TrackDetails) -> Unit
) { ) {
val writer = ID3Writer(mp3ByteArray.toArrayBuffer()) val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
val albumArt = downloadFile(corsApi + trackDetails.albumArtURL) val albumArt = downloadFile(corsApi + trackDetails.albumArtURL)

View File

@ -68,9 +68,7 @@ interface SpotiFlyerList {
val listAnalytics: Analytics val listAnalytics: Analytics
} }
interface Analytics { interface Analytics
}
sealed class Output { sealed class Output {
object Finished : Output() object Finished : Output()

View File

@ -19,6 +19,7 @@ package com.shabinder.common.list.integration
import co.touchlab.stately.ensureNeverFrozen import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext 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.di.Picture import com.shabinder.common.di.Picture
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
@ -28,7 +29,6 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.list.store.SpotiFlyerListStoreProvider import com.shabinder.common.list.store.SpotiFlyerListStoreProvider
import com.shabinder.common.list.store.getStore import com.shabinder.common.list.store.getStore
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.caching.Cache
internal class SpotiFlyerListImpl( internal class SpotiFlyerListImpl(
componentContext: ComponentContext, componentContext: ComponentContext,

View File

@ -24,7 +24,6 @@ import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.database.getLogger 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.dispatcherIO
import com.shabinder.common.di.downloadTracks import com.shabinder.common.di.downloadTracks
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
@ -34,7 +33,6 @@ import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
internal class SpotiFlyerListStoreProvider( internal class SpotiFlyerListStoreProvider(
private val dir: Dir, private val dir: Dir,
@ -80,14 +78,14 @@ internal class SpotiFlyerListStoreProvider(
is Intent.SearchLink -> { is Intent.SearchLink -> {
try { try {
val result = fetchQuery.query(link) val result = fetchQuery.query(link)
if( result != null) { if (result != null) {
result.trackList = result.trackList.toMutableList() result.trackList = result.trackList.toMutableList()
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))) dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
executeIntent(Intent.RefreshTracksStatuses, getState) executeIntent(Intent.RefreshTracksStatuses, getState)
} else { } else {
throw Exception("An Error Occurred, Check your Link / Connection") throw Exception("An Error Occurred, Check your Link / Connection")
} }
} catch (e:Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
dispatch(Result.ErrorOccurred(e)) dispatch(Result.ErrorOccurred(e))
} }
@ -101,7 +99,6 @@ internal class SpotiFlyerListStoreProvider(
} }
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))) dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))
val finalList = val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded } intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isNullOrEmpty()) methods.value.showPopUpMessage("All Songs are Processed") if (finalList.isNullOrEmpty()) methods.value.showPopUpMessage("All Songs are Processed")

View File

@ -19,6 +19,7 @@ package com.shabinder.common.main.integration
import co.touchlab.stately.ensureNeverFrozen import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext 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.di.Picture import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue import com.shabinder.common.di.utils.asValue
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
@ -30,7 +31,6 @@ import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
import com.shabinder.common.main.store.getStore import com.shabinder.common.main.store.getStore
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import com.shabinder.common.caching.Cache
internal class SpotiFlyerMainImpl( internal class SpotiFlyerMainImpl(
componentContext: ComponentContext, componentContext: ComponentContext,

View File

@ -38,17 +38,17 @@ fun org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer.generateFramewor
kotlin { kotlin {
/*IOS Target Can be only built on Mac*/ /*IOS Target Can be only built on Mac*/
if(HostOS.isMac) { if (HostOS.isMac) {
val sdkName: String? = System.getenv("SDK_NAME") val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos") val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) { if (isiOSDevice) {
iosArm64("ios"){ iosArm64("ios") {
binaries { binaries {
generateFramework() generateFramework()
} }
} }
} else { } else {
iosX64("ios"){ iosX64("ios") {
binaries { binaries {
generateFramework() generateFramework()
} }
@ -68,7 +68,7 @@ kotlin {
} }
} }
} }
if(HostOS.isMac){ if (HostOS.isMac) {
/*Required to Export `packForXcode`*/ /*Required to Export `packForXcode`*/
sourceSets { sourceSets {
named("iosMain") { named("iosMain") {
@ -88,7 +88,7 @@ kotlin {
} }
val packForXcode by tasks.creating(Sync::class) { val packForXcode by tasks.creating(Sync::class) {
if(HostOS.isMac){ if (HostOS.isMac) {
group = "build" group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG" val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val targetName = "ios" val targetName = "ios"

View File

@ -30,7 +30,6 @@ import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.root.integration.SpotiFlyerRootImpl import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.database.Database import com.shabinder.database.Database
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -53,7 +52,7 @@ interface SpotiFlyerRoot {
val fetchPlatformQueryResult: FetchPlatformQueryResult val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir val directories: Dir
val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>>
val actions:Actions val actions: Actions
val analytics: Analytics val analytics: Analytics
} }

View File

@ -18,7 +18,7 @@ package com.shabinder.common.root.callbacks
interface SpotiFlyerRootCallBacks { interface SpotiFlyerRootCallBacks {
fun searchLink(link: String) fun searchLink(link: String)
fun showToast(text:String) fun showToast(text: String)
fun popBackToHomeScreen() fun popBackToHomeScreen()
fun setDownloadDirectory() fun setDownloadDirectory()
} }

View File

@ -48,8 +48,8 @@ import kotlinx.coroutines.launch
internal class SpotiFlyerRootImpl( internal class SpotiFlyerRootImpl(
componentContext: ComponentContext, componentContext: ComponentContext,
private val main: (ComponentContext, output:Consumer<SpotiFlyerMain.Output>)->SpotiFlyerMain, private val main: (ComponentContext, output: Consumer<SpotiFlyerMain.Output>) -> SpotiFlyerMain,
private val list: (ComponentContext, link:String, output:Consumer<SpotiFlyerList.Output>)->SpotiFlyerList, private val list: (ComponentContext, link: String, output: Consumer<SpotiFlyerList.Output>) -> SpotiFlyerList,
private val actions: Actions, private val actions: Actions,
private val analytics: Analytics private val analytics: Analytics
) : SpotiFlyerRoot, ComponentContext by componentContext { ) : SpotiFlyerRoot, ComponentContext by componentContext {
@ -57,9 +57,9 @@ internal class SpotiFlyerRootImpl(
constructor( constructor(
componentContext: ComponentContext, componentContext: ComponentContext,
dependencies: Dependencies, dependencies: Dependencies,
):this( ) : this(
componentContext = componentContext, componentContext = componentContext,
main = { childContext,output -> main = { childContext, output ->
spotiFlyerMain( spotiFlyerMain(
childContext, childContext,
output, output,
@ -104,7 +104,7 @@ internal class SpotiFlyerRootImpl(
it !is Configuration.Main it !is Configuration.Main
} }
} }
override fun showToast(text:String) { toastState.value = text } override fun showToast(text: String) { toastState.value = text }
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() } override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
} }
@ -130,7 +130,7 @@ internal class SpotiFlyerRootImpl(
} }
} }
private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){ private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override: Boolean) {
GlobalScope.launch(Dispatchers.Default) { GlobalScope.launch(Dispatchers.Default) {
analytics.appLaunchEvent() analytics.appLaunchEvent()
/*Authenticate Spotify Client*/ /*Authenticate Spotify Client*/
@ -147,7 +147,7 @@ internal class SpotiFlyerRootImpl(
} }
} }
private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<SpotiFlyerMain.Output> ,dependencies: Dependencies): SpotiFlyerMain = private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<SpotiFlyerMain.Output>, dependencies: Dependencies): SpotiFlyerMain =
SpotiFlyerMain( SpotiFlyerMain(
componentContext = componentContext, componentContext = componentContext,
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies { dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {

View File

@ -28,7 +28,7 @@ version = Versions.versionName
kotlin { kotlin {
jvm { jvm {
compilations.all { compilations.all {
kotlinOptions.jvmTarget = "14" kotlinOptions.jvmTarget = "1.8"
} }
} }

View File

@ -28,5 +28,6 @@ include(
":fuzzywuzzy:app", ":fuzzywuzzy:app",
":android", ":android",
":desktop", ":desktop",
":web-app" ":web-app",
) ":maintenance-tasks"
)