Preference Screen & Preference Manager (WIP)

This commit is contained in:
shabinder 2021-06-23 16:43:26 +05:30
parent bb3776af56
commit c010916953
46 changed files with 249 additions and 185 deletions

View File

@ -52,6 +52,7 @@ import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsHeight
import com.google.accompanist.insets.statusBarsPadding
import com.shabinder.common.di.*
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformActions
@ -78,6 +79,7 @@ class MainActivity : ComponentActivity() {
private val fetcher: FetchPlatformQueryResult by inject()
private val dir: Dir by inject()
private val preferenceManager: PreferenceManager by inject()
private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
@ -129,18 +131,18 @@ class MainActivity : ComponentActivity() {
AnalyticsDialog(
askForAnalyticsPermission,
enableAnalytics = {
dir.toggleAnalytics(true)
dir.firstLaunchDone()
preferenceManager.toggleAnalytics(true)
preferenceManager.firstLaunchDone()
},
dismissDialog = {
askForAnalyticsPermission = false
dir.firstLaunchDone()
preferenceManager.firstLaunchDone()
}
)
LaunchedEffect(view) {
permissionGranted.value = checkPermissions()
if(dir.isFirstLaunch) {
if(preferenceManager.isFirstLaunch) {
delay(2500)
// Ask For Analytics Permission on first Dialog
askForAnalyticsPermission = true
@ -161,7 +163,7 @@ class MainActivity : ComponentActivity() {
* for `Github Downloads` we will track Downloads using : https://tooomm.github.io/github-release-stats/?username=Shabinder&repository=SpotiFlyer
* */
if(isGithubRelease) { checkIfLatestVersion() }
if(dir.isAnalyticsEnabled && !isGithubRelease) {
if(preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
// Download/App Install Event for F-Droid builds
TrackHelper.track().download().with(tracker)
}
@ -246,9 +248,10 @@ class MainActivity : ComponentActivity() {
dependencies = object : SpotiFlyerRoot.Dependencies{
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
override val database = this@MainActivity.dir.db
override val fetchPlatformQueryResult = this@MainActivity.fetcher
override val directories: Dir = this@MainActivity.dir
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
override val fetchQuery = this@MainActivity.fetcher
override val dir: Dir = this@MainActivity.dir
override val preferenceManager = this@MainActivity.preferenceManager
override val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
override val actions = object: Actions {
override val platformActions = object : PlatformActions {
@ -316,7 +319,7 @@ class MainActivity : ComponentActivity() {
* */
override val analytics = object: Analytics {
override fun appLaunchEvent() {
if(dir.isAnalyticsEnabled){
if(preferenceManager.isAnalyticsEnabled){
TrackHelper.track()
.event("events","App_Launch")
.name("App Launch").with(tracker)
@ -324,7 +327,7 @@ class MainActivity : ComponentActivity() {
}
override fun homeScreenVisit() {
if(dir.isAnalyticsEnabled){
if(preferenceManager.isAnalyticsEnabled){
// HomeScreen Visit Event
TrackHelper.track().screen("/main_activity/home_screen")
.title("HomeScreen").with(tracker)
@ -332,7 +335,7 @@ class MainActivity : ComponentActivity() {
}
override fun listScreenVisit() {
if(dir.isAnalyticsEnabled){
if(preferenceManager.isAnalyticsEnabled){
// ListScreen Visit Event
TrackHelper.track().screen("/main_activity/list_screen")
.title("ListScreen").with(tracker)
@ -340,7 +343,7 @@ class MainActivity : ComponentActivity() {
}
override fun donationDialogVisit() {
if (dir.isAnalyticsEnabled) {
if (preferenceManager.isAnalyticsEnabled) {
// Donation Dialog Open Event
TrackHelper.track().screen("/main_activity/donation_dialog")
.title("DonationDialog").with(tracker)
@ -384,7 +387,7 @@ class MainActivity : ComponentActivity() {
val f = File(path)
if (f.canWrite()) {
// hell yeah :)
dir.setDownloadDirectory(path)
preferenceManager.setDownloadDirectory(path)
showPopUpMessage(
"Download Directory Set to:\n${dir.defaultDir()} "
)

View File

@ -33,12 +33,17 @@ allprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
useIR = true
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn"
}
}
afterEvaluate {
project.extensions.findByType<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension>()?.let { kmpExt ->
kmpExt.sourceSets.removeAll { it.name == "androidAndroidTestRelease" }
kmpExt.sourceSets.run {
all {
languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi")
}
removeAll { it.name == "androidAndroidTestRelease" }
}
}
}
}

View File

@ -60,6 +60,10 @@ object HostOS {
val isLinux = hostOs.startsWith("Linux",true)
}
object MultiPlatformSettings {
const val dep = "com.russhwolf:multiplatform-settings-no-arg:0.7.7"
}
object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}"

View File

@ -10,6 +10,11 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag
override val message: String = "MP3 Converter unreachable, probably BUSY ! \nCAUSE:$extraInfo"
): SpotiFlyerException(message)
data class UnknownReason(
val exception: Throwable? = null,
override val message: String = "Unknown Error"
): SpotiFlyerException(message)
data class NoMatchFound(
val trackName: String? = null,
override val message: String = "$trackName : NO Match Found!"

View File

@ -32,7 +32,7 @@ kotlin {
implementation(project(":common:database"))
implementation("org.jetbrains.kotlinx:atomicfu:0.16.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
implementation("com.russhwolf:multiplatform-settings-no-arg:0.7.7")
api(MultiPlatformSettings.dep)
implementation(Extras.youtubeDownloader)
implementation(Extras.fuzzyWuzzy)
implementation(MVIKotlin.rx)

View File

@ -22,8 +22,8 @@ import android.os.Environment
import androidx.compose.ui.graphics.asImageBitmap
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.preference.PreferenceManager
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
@ -43,7 +43,7 @@ import java.net.URL
@Suppress("DEPRECATION")
actual class Dir actual constructor(
private val logger: Kermit,
settingsPref: Settings,
private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@Suppress("DEPRECATION")
@ -54,7 +54,7 @@ actual class Dir actual constructor(
actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
// fun call in order to always access Updated Value
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
File.separator + "SpotiFlyer" + File.separator
actual fun isPresent(path: String): Boolean = File(path).exists()
@ -202,5 +202,4 @@ actual class Dir actual constructor(
private val parallelExecutor = ParallelExecutor(Dispatchers.IO)
actual val db: Database? = spotiFlyerDatabase.instance
actual val settings: Settings = settingsPref
}

View File

@ -1,8 +1,7 @@
package com.shabinder.common.di.saavn
package com.shabinder.common.di.providers.requests.saavn
import android.annotation.SuppressLint
import io.ktor.util.InternalAPI
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.*
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey

View File

@ -20,13 +20,8 @@ import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.databaseModule
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.providers.providersModule
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
@ -42,7 +37,11 @@ import kotlin.native.concurrent.ThreadLocal
fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
startKoin {
appDeclaration()
modules(commonModule(enableNetworkLogs = enableNetworkLogs), databaseModule())
modules(
commonModule(enableNetworkLogs = enableNetworkLogs),
providersModule(),
databaseModule()
)
}
// Called by IOS
@ -52,16 +51,9 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
single { Dir(get(), get(), get()) }
single { Settings() }
single { PreferenceManager(get()) }
single { Kermit(getLogger()) }
single { TokenStore(get(), get()) }
single { AudioToMp3(get(), get()) }
single { SpotifyProvider(get(), get(), get()) }
single { GaanaProvider(get(), get(), get()) }
single { SaavnProvider(get(), get(), get(), get()) }
single { YoutubeProvider(get(), get(), get()) }
single { YoutubeMp3(get(), get()) }
single { YoutubeMusic(get(), get(), get(), get(), get()) }
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
}
@ThreadLocal

View File

@ -17,8 +17,8 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
@ -30,18 +30,12 @@ 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,
settingsPref: Settings,
preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
val db: Database?
val settings: Settings
fun isPresent(path: String): Boolean
fun fileSeparator(): String
fun defaultDir(): String
@ -54,22 +48,6 @@ expect class Dir(
fun addToLibrary(path: String)
}
val Dir.isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
fun Dir.toggleAnalytics(enabled: Boolean) = settings.putBoolean(AnalyticsKey, enabled)
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!
* */

View File

@ -18,7 +18,6 @@ package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider
@ -26,6 +25,7 @@ import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider
import com.shabinder.common.di.providers.get
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
@ -35,6 +35,7 @@ import com.shabinder.common.models.event.coroutines.flatMapError
import com.shabinder.common.models.event.coroutines.success
import com.shabinder.common.models.spotify.Source
import com.shabinder.common.requireNotNull
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -137,6 +138,7 @@ class FetchPlatformQueryResult(
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
GlobalScope.launch(dispatcherIO) {
db?.add(

View File

@ -18,7 +18,7 @@ package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.shabinder.common.database.TokenDBQueries
import com.shabinder.common.di.spotify.authenticateSpotify
import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify
import com.shabinder.common.models.spotify.TokenData
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

View File

@ -0,0 +1,35 @@
package com.shabinder.common.di.preference
import com.russhwolf.settings.Settings
class PreferenceManager(settings: Settings): Settings by settings {
companion object {
const val DirKey = "downloadDir"
const val AnalyticsKey = "analytics"
const val FirstLaunch = "firstLaunch"
const val DonationInterval = "donationInterval"
}
/* ANALYTICS */
val isAnalyticsEnabled get() = getBooleanOrNull(AnalyticsKey) ?: false
fun toggleAnalytics(enabled: Boolean) = putBoolean(AnalyticsKey, enabled)
/* DOWNLOAD DIRECTORY */
val downloadDir get() = getStringOrNull(DirKey)
fun setDownloadDirectory(newBasePath: String) = putString(DirKey, newBasePath)
/* OFFSET FOR WHEN TO ASK FOR SUPPORT */
val getDonationOffset: Int get() = (getIntOrNull(DonationInterval) ?: 3).also {
// Min. Donation Asking Interval is `3`
if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1)
}
fun setDonationOffset(offset: Int = 5) = putInt(DonationInterval, offset)
/* TO CHECK IF THIS IS APP's FIRST LAUNCH */
val isFirstLaunch get() = getBooleanOrNull(FirstLaunch) ?: true
fun firstLaunchDone() = putBoolean(FirstLaunch, false)
}

View File

@ -19,7 +19,7 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.gaana.GaanaRequests
import com.shabinder.common.di.providers.requests.gaana.GaanaRequests
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.SpotiFlyerException

View File

@ -0,0 +1,16 @@
package com.shabinder.common.di.providers
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import org.koin.dsl.module
fun providersModule() = module {
single { AudioToMp3(get(), get()) }
single { SpotifyProvider(get(), get(), get()) }
single { GaanaProvider(get(), get(), get()) }
single { SaavnProvider(get(), get(), get(), get()) }
single { YoutubeProvider(get(), get(), get()) }
single { YoutubeMp3(get(), get()) }
single { YoutubeMusic(get(), get(), get(), get(), get()) }
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
}

View File

@ -2,9 +2,9 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.saavn.JioSaavnRequests
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.requests.saavn.JioSaavnRequests
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult

View File

@ -22,8 +22,8 @@ import com.shabinder.common.di.TokenStore
import com.shabinder.common.di.createHttpClient
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.globalJson
import com.shabinder.common.di.spotify.SpotifyRequests
import com.shabinder.common.di.spotify.authenticateSpotify
import com.shabinder.common.di.providers.requests.spotify.SpotifyRequests
import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.NativeAtomicReference
import com.shabinder.common.models.PlatformQueryResult

View File

@ -17,7 +17,7 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
import com.shabinder.common.di.providers.requests.youtubeMp3.Yt1sMp3
import com.shabinder.common.models.corsApi
import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.event.coroutines.map

View File

@ -17,7 +17,7 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack

View File

@ -1,4 +1,4 @@
package com.shabinder.common.di.audioToMp3
package com.shabinder.common.di.providers.requests.audioToMp3
import co.touchlab.kermit.Kermit
import com.shabinder.common.models.AudioQuality

View File

@ -14,7 +14,7 @@
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.di.gaana
package com.shabinder.common.di.providers.requests.gaana
import com.shabinder.common.models.corsApi
import com.shabinder.common.models.gaana.GaanaAlbum

View File

@ -1,8 +1,8 @@
package com.shabinder.common.di.saavn
package com.shabinder.common.di.providers.requests.saavn
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.globalJson
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import com.shabinder.common.models.corsApi
import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.event.coroutines.map

View File

@ -1,4 +1,6 @@
package com.shabinder.common.di.saavn
package com.shabinder.common.di.providers.requests.saavn
import com.shabinder.common.di.utils.unescape
expect suspend fun decryptURL(url: String): String

View File

@ -14,7 +14,7 @@
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.di.spotify
package com.shabinder.common.di.providers.requests.spotify
import com.shabinder.common.di.globalJson
import com.shabinder.common.models.SpotiFlyerException

View File

@ -14,7 +14,7 @@
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.di.spotify
package com.shabinder.common.di.providers.requests.spotify
import com.shabinder.common.models.NativeAtomicReference
import com.shabinder.common.models.corsApi

View File

@ -14,7 +14,7 @@
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.di.youtubeMp3
package com.shabinder.common.di.providers.requests.youtubeMp3
import co.touchlab.kermit.Kermit
import com.shabinder.common.models.corsApi

View File

@ -19,6 +19,7 @@ package com.shabinder.common.di
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@ -40,41 +41,43 @@ actual suspend fun downloadTracks(
) {
list.forEach { trackDetails ->
DownloadScope.execute { // Send Download to Pool.
val url = fetcher.findMp3DownloadLink(trackDetails)
if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
downloadFile(url).collect {
when (it) {
is DownloadResult.Error -> {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
)
}
is DownloadResult.Progress -> {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
)
}
is DownloadResult.Success -> { // Todo clear map
dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
)
fetcher.findMp3DownloadLink(trackDetails).fold(
success = { url ->
downloadFile(url).collect {
when (it) {
is DownloadResult.Error -> {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause))) }
)
}
is DownloadResult.Progress -> {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
)
}
is DownloadResult.Success -> { // Todo clear map
dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
)
}
}
}
},
failure = { error ->
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(error)) }
)
}
} else {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
)
}
)
}
}
}

View File

@ -20,8 +20,8 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
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.preference.PreferenceManager
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinx.coroutines.Dispatchers
@ -40,7 +40,7 @@ import javax.imageio.ImageIO
actual class Dir actual constructor(
private val logger: Kermit,
settingsPref: Settings,
private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@ -55,7 +55,7 @@ actual class Dir actual constructor(
private val defaultBaseDir = System.getProperty("user.home")
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() +
"SpotiFlyer" + fileSeparator()
actual fun isPresent(path: String): Boolean = File(path).exists()
@ -199,7 +199,6 @@ actual class Dir actual constructor(
}
actual val db: Database? = spotiFlyerDatabase.instance
actual val settings: Settings = settingsPref
}
fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(

View File

@ -1,7 +1,6 @@
package com.shabinder.common.di.saavn
package com.shabinder.common.di.providers.requests.saavn
import io.ktor.util.InternalAPI
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.*
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey

View File

@ -1,8 +1,8 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinx.coroutines.GlobalScope
@ -24,7 +24,7 @@ import platform.UIKit.UIImageJPEGRepresentation
actual class Dir actual constructor(
val logger: Kermit,
settingsPref: Settings,
private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@ -35,7 +35,7 @@ actual class Dir actual constructor(
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!.path!!
// TODO Error Handling
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
fileSeparator() + "SpotiFlyer" + fileSeparator()
private val defaultDirURL: NSURL by lazy {
@ -176,6 +176,5 @@ actual class Dir actual constructor(
// TODO
}
actual val settings: Settings = settingsPref
actual val db: Database? = spotiFlyerDatabase.instance
}

View File

@ -18,6 +18,7 @@ package com.shabinder.common.di
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@ -41,29 +42,31 @@ actual suspend fun downloadTracks(
list.forEach { track ->
withContext(dispatcherIO) {
allTracksStatus[track.title] = DownloadStatus.Queued
val url = fetcher.findMp3DownloadLink(track)
if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
downloadFile(url).collect {
when (it) {
is DownloadResult.Success -> {
println("Download Completed")
dir.saveFileWithMetadata(it.byteArray, track) {}
}
is DownloadResult.Error -> {
allTracksStatus[track.title] = DownloadStatus.Failed
println("Download Error: ${track.title}")
}
is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
println("Download Progress: ${it.progress} : ${track.title}")
fetcher.findMp3DownloadLink(track).fold(
success = { url ->
downloadFile(url).collect {
when (it) {
is DownloadResult.Success -> {
println("Download Completed")
dir.saveFileWithMetadata(it.byteArray, track) {}
}
is DownloadResult.Error -> {
allTracksStatus[track.title] = DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause))
println("Download Error: ${track.title}")
}
is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
println("Download Progress: ${it.progress} : ${track.title}")
}
}
DownloadProgressFlow.emit(allTracksStatus)
}
},
failure = { error ->
allTracksStatus[track.title] = DownloadStatus.Failed(error)
DownloadProgressFlow.emit(allTracksStatus)
}
} else {
allTracksStatus[track.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
}
)
}
}
}

View File

@ -17,13 +17,13 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.corsApi
import com.shabinder.database.Database
import kotlinext.js.Object
import kotlinext.js.js
@ -34,7 +34,7 @@ import org.w3c.dom.ImageBitmap
actual class Dir actual constructor(
private val logger: Kermit,
settingsPref: Settings,
private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
/*init {
@ -116,7 +116,6 @@ actual class Dir actual constructor(
private suspend fun freshImage(url: String): ImageBitmap? = null
actual val db: Database? = spotiFlyerDatabase.instance
actual val settings: Settings = settingsPref
}
fun ByteArray.toArrayBuffer(): ArrayBuffer {

View File

@ -1,4 +1,4 @@
package com.shabinder.common.di.saavn
package com.shabinder.common.di.providers.requests.saavn
actual suspend fun decryptURL(url: String): String {
TODO("Not yet implemented")

View File

@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.Picture
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.list.integration.SpotiFlyerListImpl
import com.shabinder.common.models.Consumer
import com.shabinder.common.models.DownloadStatus
@ -67,6 +68,7 @@ interface SpotiFlyerList {
val storeFactory: StoreFactory
val fetchQuery: FetchPlatformQueryResult
val dir: Dir
val preferenceManager: PreferenceManager
val link: String
val listOutput: Consumer<Output>
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>

View File

@ -22,7 +22,6 @@ import com.arkivanov.decompose.lifecycle.doOnResume
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
@ -48,6 +47,7 @@ internal class SpotiFlyerListImpl(
instanceKeeper.getStore {
SpotiFlyerListStoreProvider(
dir = this.dir,
preferenceManager = preferenceManager,
storeFactory = storeFactory,
fetchQuery = fetchQuery,
downloadProgressFlow = downloadProgressFlow,
@ -79,7 +79,7 @@ internal class SpotiFlyerListImpl(
}
override fun snoozeDonationDialog() {
dir.setDonationOffset(offset = 10)
preferenceManager.setDonationOffset(offset = 10)
}
override suspend fun loadImage(url: String, isCover: Boolean): Picture {

View File

@ -24,7 +24,7 @@ import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
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.di.preference.PreferenceManager
import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.models.DownloadStatus
@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.collect
internal class SpotiFlyerListStoreProvider(
private val dir: Dir,
private val preferenceManager: PreferenceManager,
private val storeFactory: StoreFactory,
private val fetchQuery: FetchPlatformQueryResult,
private val link: String,
@ -68,7 +69,7 @@ internal class SpotiFlyerListStoreProvider(
dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also {
// See if It's Time we can request for support for maintaining this project or not
fetchQuery.logger.d(message = { "Database List Last ID: $it" }, tag = "Database Last ID")
val offset = dir.getDonationOffset
val offset = preferenceManager.getDonationOffset
dispatch(
Result.AskForSupport(
// Every 3rd Interval or After some offset

View File

@ -21,6 +21,7 @@ import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.Picture
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.main.integration.SpotiFlyerMainImpl
import com.shabinder.common.models.Consumer
import com.shabinder.common.models.DownloadRecord
@ -63,6 +64,7 @@ interface SpotiFlyerMain {
val storeFactory: StoreFactory
val database: Database?
val dir: Dir
val preferenceManager: PreferenceManager
val mainAnalytics: Analytics
}

View File

@ -23,7 +23,10 @@ import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.*
import com.shabinder.common.main.SpotiFlyerMain.Dependencies
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
import com.shabinder.common.main.SpotiFlyerMain.Output
import com.shabinder.common.main.SpotiFlyerMain.State
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
import com.shabinder.common.main.store.getStore
@ -41,6 +44,7 @@ internal class SpotiFlyerMainImpl(
private val store =
instanceKeeper.getStore {
SpotiFlyerMainStoreProvider(
preferenceManager = preferenceManager,
storeFactory = storeFactory,
database = database,
dir = dir

View File

@ -22,8 +22,7 @@ import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.di.Dir
import com.shabinder.common.di.isAnalyticsEnabled
import com.shabinder.common.di.toggleAnalytics
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.State
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
@ -39,6 +38,7 @@ import kotlinx.coroutines.flow.map
internal class SpotiFlyerMainStoreProvider(
private val storeFactory: StoreFactory,
private val preferenceManager: PreferenceManager,
private val dir: Dir,
database: Database?
) {
@ -76,7 +76,7 @@ internal class SpotiFlyerMainStoreProvider(
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
override suspend fun executeAction(action: Unit, getState: () -> State) {
dispatch(Result.ToggleAnalytics(dir.isAnalyticsEnabled))
dispatch(Result.ToggleAnalytics(preferenceManager.isAnalyticsEnabled))
updates?.collect {
dispatch(Result.ItemsLoaded(it))
}
@ -91,7 +91,7 @@ internal class SpotiFlyerMainStoreProvider(
is Intent.SelectCategory -> dispatch(Result.CategoryChanged(intent.category))
is Intent.ToggleAnalytics -> {
dispatch(Result.ToggleAnalytics(intent.enabled))
dir.toggleAnalytics(intent.enabled)
preferenceManager.toggleAnalytics(intent.enabled)
}
}
}

View File

@ -22,6 +22,7 @@ import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.models.Actions
@ -49,9 +50,10 @@ interface SpotiFlyerRoot {
interface Dependencies {
val storeFactory: StoreFactory
val database: Database?
val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir
val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>>
val fetchQuery: FetchPlatformQueryResult
val dir: Dir
val preferenceManager: PreferenceManager
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
val actions: Actions
val analytics: Analytics
}

View File

@ -27,7 +27,6 @@ import com.arkivanov.decompose.router
import com.arkivanov.decompose.statekeeper.Parcelable
import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value
import com.shabinder.common.di.Dir
import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
@ -39,6 +38,7 @@ import com.shabinder.common.root.SpotiFlyerRoot.Analytics
import com.shabinder.common.root.SpotiFlyerRoot.Child
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@ -77,7 +77,7 @@ internal class SpotiFlyerRootImpl(
instanceKeeper.ensureNeverFrozen()
methods.value = dependencies.actions.freeze()
/*Init App Launch & Authenticate Spotify Client*/
initAppLaunchAndAuthenticateSpotify(dependencies.fetchPlatformQueryResult::authenticateSpotifyClient)
initAppLaunchAndAuthenticateSpotify(dependencies.fetchQuery::authenticateSpotifyClient)
}
private val router =
@ -128,6 +128,7 @@ internal class SpotiFlyerRootImpl(
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) {
GlobalScope.launch(dispatcherIO) {
analytics.appLaunchEvent()
@ -150,10 +151,7 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<
componentContext = componentContext,
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
override val mainOutput: Consumer<SpotiFlyerMain.Output> = output
override val dir: Dir = directories
override val mainAnalytics = object : SpotiFlyerMain.Analytics {
override fun donationDialogVisit() = analytics.donationDialogVisit()
}
override val mainAnalytics = object : SpotiFlyerMain.Analytics , Analytics by analytics {}
}
)
@ -161,11 +159,8 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out
SpotiFlyerList(
componentContext = componentContext,
dependencies = object : SpotiFlyerList.Dependencies, Dependencies by dependencies {
override val fetchQuery = fetchPlatformQueryResult
override val dir: Dir = directories
override val link: String = link
override val listOutput: Consumer<SpotiFlyerList.Output> = output
override val downloadProgressFlow = downloadProgressReport
override val listAnalytics = object : SpotiFlyerList.Analytics {}
override val listAnalytics = object : SpotiFlyerList.Analytics, Analytics by analytics {}
}
)

View File

@ -27,12 +27,21 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
import com.arkivanov.mvikotlin.core.lifecycle.LifecycleRegistry
import com.arkivanov.mvikotlin.core.lifecycle.resume
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.shabinder.common.di.*
import com.shabinder.common.di.Dir
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.preference.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.uikit.*
import com.shabinder.common.uikit.SpotiFlyerColors
import com.shabinder.common.uikit.SpotiFlyerRootContent
import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorOffWhite
import com.shabinder.database.Database
import kotlinx.coroutines.runBlocking
import org.piwik.java.tracking.PiwikTracker
@ -79,10 +88,11 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
componentContext = componentContext,
dependencies = object : SpotiFlyerRoot.Dependencies {
override val storeFactory = DefaultStoreFactory
override val fetchPlatformQueryResult: FetchPlatformQueryResult = koin.get()
override val directories: Dir = koin.get()
override val database: Database? = directories.db
override val downloadProgressReport = DownloadProgressFlow
override val fetchQuery: FetchPlatformQueryResult = koin.get()
override val dir: Dir = koin.get()
override val database: Database? = dir.db
override val preferenceManager: PreferenceManager = koin.get()
override val downloadProgressFlow = DownloadProgressFlow
override val actions: Actions = object: Actions {
override val platformActions = object : PlatformActions {}
@ -100,7 +110,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
APPROVE_OPTION -> {
val directory = fileChooser.selectedFile
if(directory.canWrite()){
directories.setDownloadDirectory(directory.absolutePath)
preferenceManager.setDownloadDirectory(directory.absolutePath)
showPopUpMessage("Set New Download Directory:\n${directory.absolutePath}")
} else {
showPopUpMessage("Cant Write to Selected Directory!")
@ -137,10 +147,10 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
}
override val analytics = object: SpotiFlyerRoot.Analytics {
override fun appLaunchEvent() {
if(directories.isFirstLaunch) {
if(preferenceManager.isFirstLaunch) {
// Enable Analytics on First Launch
directories.toggleAnalytics(true)
directories.firstLaunchDone()
preferenceManager.toggleAnalytics(true)
preferenceManager.firstLaunchDone()
}
tracker.trackAsync {
eventName = "App Launch"

View File

@ -22,6 +22,7 @@ include(
":common:root",
":common:main",
":common:list",
":common:preference",
":common:data-models",
":common:dependency-injection",
":android",

View File

@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.shabinder.common.di.DownloadProgressFlow
import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.TrackDetails
@ -58,10 +59,11 @@ class App(props: AppProps): RComponent<AppProps, RState>(props) {
private val root = SpotiFlyerRoot(ctx,
object : SpotiFlyerRoot.Dependencies {
override val storeFactory: StoreFactory = LoggingStoreFactory(DefaultStoreFactory)
override val fetchPlatformQueryResult = dependencies.fetchPlatformQueryResult
override val directories = dependencies.directories
override val database: Database? = directories.db
override val downloadProgressReport = DownloadProgressFlow
override val fetchQuery = dependencies.fetchPlatformQueryResult
override val dir = dependencies.directories
override val preferenceManager: PreferenceManager = dependencies.preferenceManager
override val database: Database? = dir.db
override val downloadProgressFlow = DownloadProgressFlow
override val actions = object : Actions {
override val platformActions = object : PlatformActions {}

View File

@ -18,11 +18,12 @@ import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.initKoin
import react.dom.render
import com.shabinder.common.di.preference.PreferenceManager
import kotlinx.browser.document
import kotlinx.browser.window
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import react.dom.render
fun main() {
window.onload = {
@ -38,10 +39,12 @@ object AppDependencies : KoinComponent {
val logger: Kermit
val directories: Dir
val fetchPlatformQueryResult: FetchPlatformQueryResult
val preferenceManager: PreferenceManager
init {
initKoin()
directories = get()
logger = get()
fetchPlatformQueryResult = get()
preferenceManager = get()
}
}