mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-26 02:24:31 +01:00
(WIP)Android SAF
This commit is contained in:
parent
a3e9b7c3c1
commit
96fdd52ef4
@ -120,12 +120,6 @@ dependencies {
|
|||||||
implementation(MVIKotlin.mvikotlinLogging)
|
implementation(MVIKotlin.mvikotlinLogging)
|
||||||
implementation(MVIKotlin.mvikotlinTimeTravel)
|
implementation(MVIKotlin.mvikotlinTimeTravel)
|
||||||
|
|
||||||
// Firebase
|
|
||||||
implementation(platform("com.google.firebase:firebase-bom:27.1.0"))
|
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
|
||||||
implementation("com.google.firebase:firebase-perf-ktx")
|
|
||||||
|
|
||||||
// Extras
|
// Extras
|
||||||
Extras.Android.apply {
|
Extras.Android.apply {
|
||||||
implementation(Acra.notification)
|
implementation(Acra.notification)
|
||||||
@ -134,8 +128,8 @@ dependencies {
|
|||||||
implementation(matomo)
|
implementation(matomo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
implementation("dev.icerock.moko:parcelize:0.6.1")
|
implementation("dev.icerock.moko:parcelize:0.6.1")
|
||||||
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
|
|
||||||
implementation("com.google.accompanist:accompanist-insets:0.9.1")
|
implementation("com.google.accompanist:accompanist-insets:0.9.1")
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
@ -29,8 +29,29 @@ import org.koin.android.ext.koin.androidContext
|
|||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.logger.Level
|
import org.koin.core.logger.Level
|
||||||
|
import org.matomo.sdk.Matomo
|
||||||
|
import org.matomo.sdk.Tracker
|
||||||
|
import org.matomo.sdk.TrackerBuilder
|
||||||
|
import timber.log.Timber
|
||||||
|
import timber.log.Timber.DebugTree
|
||||||
|
|
||||||
|
|
||||||
class App: Application(), KoinComponent {
|
class App: Application(), KoinComponent {
|
||||||
|
|
||||||
|
val tracker: Tracker by lazy {
|
||||||
|
TrackerBuilder.createDefault(
|
||||||
|
"https://kind-grasshopper-73.telebit.io/matomo/matomo.php", 1)
|
||||||
|
.build(Matomo.getInstance(this)).apply {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Timber.plant(DebugTree())
|
||||||
|
addTrackingCallback {
|
||||||
|
Timber.d(it.toMap().toString())
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
@ -39,13 +39,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
|
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
|
||||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
||||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||||
import com.codekidlabs.storagechooser.R
|
import com.github.k1rakishou.fsaf.FileChooser
|
||||||
import com.codekidlabs.storagechooser.StorageChooser
|
import com.github.k1rakishou.fsaf.FileManager
|
||||||
|
import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
import com.google.accompanist.insets.statusBarsHeight
|
import com.google.accompanist.insets.statusBarsHeight
|
||||||
@ -56,22 +58,23 @@ import com.shabinder.common.models.Actions
|
|||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformActions
|
import com.shabinder.common.models.PlatformActions
|
||||||
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
|
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
|
||||||
|
import com.shabinder.common.models.SpotiFlyerBaseDir
|
||||||
|
import com.shabinder.common.models.Status
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot
|
import com.shabinder.common.root.SpotiFlyerRoot
|
||||||
|
import com.shabinder.common.root.SpotiFlyerRoot.Analytics
|
||||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||||
import com.shabinder.common.uikit.*
|
import com.shabinder.common.uikit.*
|
||||||
import com.shabinder.spotiflyer.utils.*
|
|
||||||
import com.shabinder.common.models.Status
|
|
||||||
import com.shabinder.common.models.methods
|
|
||||||
import com.shabinder.spotiflyer.ui.NetworkDialog
|
import com.shabinder.spotiflyer.ui.NetworkDialog
|
||||||
import com.shabinder.spotiflyer.ui.PermissionDialog
|
import com.shabinder.spotiflyer.ui.PermissionDialog
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.matomo.sdk.extra.TrackHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
const val disableDozeCode = 1223
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@ -85,6 +88,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
private lateinit var updateUIReceiver: BroadcastReceiver
|
private lateinit var updateUIReceiver: BroadcastReceiver
|
||||||
private lateinit var queryReceiver: BroadcastReceiver
|
private lateinit var queryReceiver: BroadcastReceiver
|
||||||
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
|
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
|
||||||
|
private val tracker get() = (application as App).tracker
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -118,7 +122,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
PermissionDialog(
|
PermissionDialog(
|
||||||
permissionGranted.value,
|
permissionGranted.value,
|
||||||
{ requestStoragePermission() },
|
{ requestStoragePermission() },
|
||||||
{ disableDozeMode(disableDozeCode) }
|
{ disableDozeMode(disableDozeCode) },
|
||||||
|
dir::enableAnalytics
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +135,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun initialise() {
|
private fun initialise() {
|
||||||
checkIfLatestVersion()
|
checkIfLatestVersion()
|
||||||
handleIntentFromExternalActivity()
|
handleIntentFromExternalActivity()
|
||||||
|
if(dir.isAnalyticsEnabled){
|
||||||
|
// Download/App Install Event
|
||||||
|
TrackHelper.track().download().with(tracker)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -139,38 +148,34 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun setUpOnPrefClickListener() {
|
private fun setUpOnPrefClickListener() {
|
||||||
// Initialize Builder
|
/*Get User Permission to access External SD*//*
|
||||||
val chooser = StorageChooser.Builder()
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
.withActivity(this)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.withFragmentManager(fragmentManager)
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
.withMemoryBar(true)
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
.setTheme(StorageChooser.Theme(applicationContext).apply {
|
|
||||||
scheme = applicationContext.resources.getIntArray(R.array.default_dark)
|
|
||||||
})
|
|
||||||
.setDialogTitle("Set Download Directory")
|
|
||||||
.allowCustomPath(true)
|
|
||||||
.setType(StorageChooser.DIRECTORY_CHOOSER)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// get path that the user has chosen
|
|
||||||
chooser.setOnSelectListener { path ->
|
|
||||||
Log.d("Setting Base Path", path)
|
|
||||||
val f = File(path)
|
|
||||||
if (f.canWrite()) {
|
|
||||||
// hell yeah :)
|
|
||||||
dir.setDownloadDirectory(path)
|
|
||||||
showPopUpMessage(
|
|
||||||
"Download Directory Set to:\n${dir.defaultDir()} "
|
|
||||||
)
|
|
||||||
}else{
|
|
||||||
showPopUpMessage(
|
|
||||||
"NO WRITE ACCESS on \n$path ,\nReverting Back to Previous"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
startActivityForResult(intent, externalSDWriteAccess)*/
|
||||||
|
val fileChooser = FileChooser(applicationContext)
|
||||||
|
fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() {
|
||||||
|
override fun onResult(uri: Uri) {
|
||||||
|
println("treeUri = $uri")
|
||||||
|
// Can be only used using SAF
|
||||||
|
contentResolver.takePersistableUriPermission(uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
val treeDocumentFile = DocumentFile.fromTreeUri(applicationContext, uri)
|
||||||
|
|
||||||
// Show dialog whenever you want by
|
dir.setDownloadDirectory(uri)
|
||||||
chooser.show()
|
showPopUpMessage("New Download Directory Set")
|
||||||
|
GlobalScope.launch {
|
||||||
|
dir.createDirectories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(reason: String) {
|
||||||
|
println("Canceled by user")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showPopUpMessage(string: String, long: Boolean = false) {
|
private fun showPopUpMessage(string: String, long: Boolean = false) {
|
||||||
@ -193,12 +198,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
|
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
|
||||||
override val database = this@MainActivity.dir.db
|
override val database = this@MainActivity.dir.db
|
||||||
override val fetchPlatformQueryResult = this@MainActivity.fetcher
|
override val fetchPlatformQueryResult = this@MainActivity.fetcher
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
override val directories: Dir = this@MainActivity.dir
|
override val directories: Dir = this@MainActivity.dir
|
||||||
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
|
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
|
||||||
override val actions = object: Actions {
|
override val actions = object: Actions {
|
||||||
|
|
||||||
override val platformActions = object : PlatformActions {
|
override val platformActions = object : PlatformActions {
|
||||||
override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator
|
override val imageCacheDir: File = applicationContext.cacheDir
|
||||||
override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey,
|
override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey,
|
||||||
MODE_PRIVATE
|
MODE_PRIVATE
|
||||||
)
|
)
|
||||||
@ -264,6 +270,37 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override val isInternetAvailable get() = internetAvailability.value ?: true
|
override val isInternetAvailable get() = internetAvailability.value ?: true
|
||||||
}
|
}
|
||||||
|
override val analytics = object: Analytics {
|
||||||
|
override fun appLaunchEvent() {
|
||||||
|
TrackHelper.track()
|
||||||
|
.event("events","App_Launch")
|
||||||
|
.name("App Launch").with(tracker)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun homeScreenVisit() {
|
||||||
|
if(dir.isAnalyticsEnabled){
|
||||||
|
// HomeScreen Visit Event
|
||||||
|
TrackHelper.track().screen("/main_activity/home_screen")
|
||||||
|
.title("HomeScreen").with(tracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun listScreenVisit() {
|
||||||
|
if(dir.isAnalyticsEnabled){
|
||||||
|
// ListScreen Visit Event
|
||||||
|
TrackHelper.track().screen("/main_activity/list_screen")
|
||||||
|
.title("ListScreen").with(tracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun donationDialogVisit() {
|
||||||
|
if (dir.isAnalyticsEnabled) {
|
||||||
|
// Donation Dialog Open Event
|
||||||
|
TrackHelper.track().screen("/main_activity/donation_dialog")
|
||||||
|
.title("DonationDialog").with(tracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -271,19 +308,44 @@ class MainActivity : ComponentActivity() {
|
|||||||
@SuppressLint("ObsoleteSdkInt")
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
if (requestCode == disableDozeCode) {
|
when(requestCode) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
disableDozeCode -> {
|
||||||
val pm =
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pm =
|
||||||
val isIgnoringBatteryOptimizations =
|
getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
pm.isIgnoringBatteryOptimizations(packageName)
|
val isIgnoringBatteryOptimizations =
|
||||||
if (isIgnoringBatteryOptimizations) {
|
pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
// Ignoring battery optimization
|
if (isIgnoringBatteryOptimizations) {
|
||||||
permissionGranted.value = true
|
// Already Ignoring battery optimization
|
||||||
} else {
|
permissionGranted.value = true
|
||||||
disableDozeMode(disableDozeCode)//Again Ask For Permission!!
|
} else {
|
||||||
|
//Again Ask For Permission!!
|
||||||
|
disableDozeMode(disableDozeCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
externalSDWriteAccess -> {
|
||||||
|
// Can be only used using SAF
|
||||||
|
/*if (resultCode == RESULT_OK) {
|
||||||
|
val treeUri: Uri? = data?.data
|
||||||
|
if (treeUri == null){
|
||||||
|
showPopUpMessage("Some Error Occurred While Setting New Download Directory")
|
||||||
|
}else {
|
||||||
|
// Persistently save READ & WRITE Access to whole Selected Directory Tree
|
||||||
|
contentResolver.takePersistableUriPermission(treeUri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
dir.setDownloadDirectory(com.shabinder.common.models.File(
|
||||||
|
DocumentFile.fromTreeUri(applicationContext,treeUri)?.createDirectory("SpotiFlyer")!!)
|
||||||
|
)
|
||||||
|
showPopUpMessage("New Download Directory Set")
|
||||||
|
GlobalScope.launch {
|
||||||
|
dir.createDirectories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,4 +442,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val disableDozeCode = 1223
|
||||||
|
const val externalSDWriteAccess = 1224
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.AlertDialog
|
import androidx.compose.material.AlertDialog
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Insights
|
||||||
import androidx.compose.material.icons.rounded.SdStorage
|
import androidx.compose.material.icons.rounded.SdStorage
|
||||||
import androidx.compose.material.icons.rounded.SystemSecurityUpdate
|
import androidx.compose.material.icons.rounded.SystemSecurityUpdate
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -28,6 +30,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
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.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.shabinder.common.uikit.SpotiFlyerShapes
|
import com.shabinder.common.uikit.SpotiFlyerShapes
|
||||||
import com.shabinder.common.uikit.SpotiFlyerTypography
|
import com.shabinder.common.uikit.SpotiFlyerTypography
|
||||||
import com.shabinder.common.uikit.colorPrimary
|
import com.shabinder.common.uikit.colorPrimary
|
||||||
@ -38,13 +41,50 @@ import kotlinx.coroutines.delay
|
|||||||
fun PermissionDialog(
|
fun PermissionDialog(
|
||||||
permissionGranted: Boolean,
|
permissionGranted: Boolean,
|
||||||
requestStoragePermission:() -> Unit,
|
requestStoragePermission:() -> Unit,
|
||||||
disableDozeMode:() -> Unit
|
disableDozeMode:() -> Unit,
|
||||||
|
enableAnalytics:() -> Unit
|
||||||
){
|
){
|
||||||
var askForPermission by remember { mutableStateOf(false) }
|
var askForPermission by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(2000)
|
delay(2000)
|
||||||
askForPermission = true
|
askForPermission = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analytics Permission Dialog
|
||||||
|
var askForAnalyticsPermission by remember { mutableStateOf(false) }
|
||||||
|
AnimatedVisibility(askForAnalyticsPermission) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
askForAnalyticsPermission = false
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Rounded.Insights,"Analytics",Modifier.size(52.dp))
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
Text("Grant Analytics Access",style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor = Color.DarkGray,
|
||||||
|
buttons = {
|
||||||
|
TextButton(
|
||||||
|
{
|
||||||
|
askForAnalyticsPermission = false
|
||||||
|
enableAnalytics()
|
||||||
|
},
|
||||||
|
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
|
||||||
|
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
) {
|
||||||
|
Text("Sure!",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text("Your Data is Anonymized and will never be shared with any 3rd party service",style = SpotiFlyerTypography.body2,textAlign = TextAlign.Center)
|
||||||
|
},
|
||||||
|
properties = DialogProperties(dismissOnBackPress = true,dismissOnClickOutside = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
askForPermission && !permissionGranted
|
askForPermission && !permissionGranted
|
||||||
) {
|
) {
|
||||||
@ -55,6 +95,7 @@ fun PermissionDialog(
|
|||||||
{
|
{
|
||||||
requestStoragePermission()
|
requestStoragePermission()
|
||||||
disableDozeMode()
|
disableDozeMode()
|
||||||
|
askForAnalyticsPermission = true
|
||||||
},
|
},
|
||||||
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
|
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
|
||||||
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
|
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
|
||||||
@ -100,6 +141,23 @@ fun PermissionDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Insights,"Analytics")
|
||||||
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Analytics",
|
||||||
|
style = SpotiFlyerTypography.h6.copy(fontWeight = FontWeight.SemiBold)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Share Analytics Data (optional) with App Devs (Self-Hosted), It will never be used/shared/sold to any third party service.",
|
||||||
|
style = SpotiFlyerTypography.subtitle2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- <color name="colorPrimary">#2d6c55</color>
|
|
||||||
<color name="colorPrimaryDark">#235644</color>
|
|
||||||
<color name="colorAccent">#ff9c40</color>
|
|
||||||
|
|
||||||
<color name="colorCyanListClick">#d6c8f5f3</color>
|
|
||||||
<color name="colorListDivider">#89606060</color>
|
|
||||||
|
|
||||||
<color name="pathLayoutBgColor">#70ffffff</color>
|
|
||||||
<color name="chevronBgColor">#50ffffff</color>
|
|
||||||
<color name="inactiveGradientColor">#53000000</color>
|
|
||||||
|
|
||||||
<!– dialog colors –>
|
|
||||||
<color name="memory_status_color">#de6565</color>
|
|
||||||
<color name="memory_bar_color">#7bde65</color>
|
|
||||||
<color name="new_folder_color">#c14b84</color>
|
|
||||||
<color name="select_color">#6b3fa1</color>
|
|
||||||
<color name="cancel_color">#3fa19f</color>-->
|
|
||||||
|
|
||||||
<array name="default_light">
|
|
||||||
<!-- Overview -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- Top Header bg -->
|
|
||||||
<item>@android:color/white</item> <!-- header text -->
|
|
||||||
<item>@android:color/white</item> <!-- list bg -->
|
|
||||||
<item>@android:color/black</item> <!-- storage list name text -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- free space text -->
|
|
||||||
<item>@color/colorAccent</item> <!-- memory bar -->
|
|
||||||
|
|
||||||
<!-- secondary dialog colors -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- address bar bg -->
|
|
||||||
<item>@android:color/white</item> <!-- list bg -->
|
|
||||||
<item>@android:color/black</item> <!-- list text -->
|
|
||||||
<item>@android:color/white</item> <!-- address bar tint -->
|
|
||||||
<item>@color/chevronBgColor</item> <!-- new folder hint tint -->
|
|
||||||
<item>#da6c6c</item> <!-- select button color -->
|
|
||||||
<item>#da6c6c</item> <!-- new folder layour bg -->
|
|
||||||
<item>#da6c6c</item> <!-- new folder layour bg -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- new folder layour bg -->
|
|
||||||
</array>
|
|
||||||
|
|
||||||
<array name="default_dark">
|
|
||||||
<!-- Overview -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- Top Header bg -->
|
|
||||||
<item>@android:color/white</item> <!-- header text -->
|
|
||||||
<item>@android:color/black</item> <!-- list bg -->
|
|
||||||
<item>@android:color/white</item> <!-- storage list name text -->
|
|
||||||
<item>#da6c6c</item> <!-- free space text -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- memory bar -->
|
|
||||||
|
|
||||||
<!-- secondary dialog colors -->
|
|
||||||
<item>@color/colorPrimary</item> <!-- address bar bg -->
|
|
||||||
<item>@android:color/black</item> <!-- list bg -->
|
|
||||||
<item>@android:color/white</item> <!-- list text -->
|
|
||||||
<item>@android:color/white</item> <!-- address bar tint -->
|
|
||||||
<item>@color/grey</item> <!-- new folder hint tint -->
|
|
||||||
<item>#da6c6c</item> <!-- select button color -->
|
|
||||||
<item>#da6c6c</item> <!-- new folder layour bg -->
|
|
||||||
<item>#da6c6c</item> <!-- new multi fab -->
|
|
||||||
<item>#da6c6c</item> <!-- new multi fab -->
|
|
||||||
</array>
|
|
||||||
</resources>
|
|
@ -102,7 +102,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
when (model.selectedCategory) {
|
when (model.selectedCategory) {
|
||||||
HomeCategory.About -> AboutColumn()
|
HomeCategory.About -> AboutColumn { component.analytics.donationDialogVisit() }
|
||||||
HomeCategory.History -> HistoryColumn(
|
HomeCategory.History -> HistoryColumn(
|
||||||
model.records.sortedByDescending { it.id },
|
model.records.sortedByDescending { it.id },
|
||||||
component::loadImage,
|
component::loadImage,
|
||||||
@ -221,7 +221,10 @@ fun SearchPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutColumn(modifier: Modifier = Modifier) {
|
fun AboutColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
donationDialogOpenEvent:() -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
val stateVertical = rememberScrollState(0)
|
val stateVertical = rememberScrollState(0)
|
||||||
@ -331,14 +334,18 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
var isDonationDialogVisible by remember { mutableStateOf(false) }
|
var isDonationDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DonationDialog(
|
DonationDialog(
|
||||||
isDonationDialogVisible
|
isDonationDialogVisible,
|
||||||
) {
|
onDismiss = {
|
||||||
isDonationDialogVisible = false
|
isDonationDialogVisible = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||||
.clickable(onClick = { isDonationDialogVisible = true }),
|
.clickable(onClick = {
|
||||||
|
isDonationDialogVisible = true
|
||||||
|
donationDialogOpenEvent()
|
||||||
|
}),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
|
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
|
||||||
|
@ -45,5 +45,11 @@ kotlin {
|
|||||||
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
|
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api("com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.1")
|
||||||
|
api("androidx.documentfile:documentfile:1.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
import com.github.k1rakishou.fsaf.file.AbstractFile
|
||||||
|
|
||||||
|
// Use Storage Access Framework `SAF`
|
||||||
|
actual data class File(
|
||||||
|
val documentFile: AbstractFile?
|
||||||
|
)
|
@ -8,7 +8,7 @@ actual interface PlatformActions {
|
|||||||
const val SharedPreferencesKey = "configurations"
|
const val SharedPreferencesKey = "configurations"
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageCacheDir: String
|
val imageCacheDir: java.io.File
|
||||||
|
|
||||||
val sharedPreferences: SharedPreferences?
|
val sharedPreferences: SharedPreferences?
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ actual interface PlatformActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual val StubPlatformActions = object: PlatformActions {
|
actual val StubPlatformActions = object: PlatformActions {
|
||||||
override val imageCacheDir: String = ""
|
override val imageCacheDir = java.io.File("/")
|
||||||
|
|
||||||
override val sharedPreferences: SharedPreferences? = null
|
override val sharedPreferences: SharedPreferences? = null
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SpotiFlyerBaseDir(
|
||||||
|
private val getDirType: ()-> ActiveBaseDirType,
|
||||||
|
private val getJavaFile: ()-> File?,
|
||||||
|
private val getSAFUri: ()-> Uri?
|
||||||
|
): BaseDirectory() {
|
||||||
|
|
||||||
|
override fun currentActiveBaseDirType(): ActiveBaseDirType = getDirType()
|
||||||
|
|
||||||
|
override fun getDirFile(): File? = getJavaFile()
|
||||||
|
|
||||||
|
override fun getDirUri(): Uri? = getSAFUri()
|
||||||
|
}
|
@ -32,12 +32,12 @@ data class TrackDetails(
|
|||||||
var comment: String? = null,
|
var comment: String? = null,
|
||||||
var lyrics: String? = null,
|
var lyrics: String? = null,
|
||||||
var trackUrl: String? = null,
|
var trackUrl: String? = null,
|
||||||
var albumArtPath: String,
|
var albumArtPath: String, // UriString in Android
|
||||||
var albumArtURL: String,
|
var albumArtURL: String,
|
||||||
var source: Source,
|
var source: Source,
|
||||||
val progress: Int = 2,
|
val progress: Int = 2,
|
||||||
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||||
var outputFilePath: String,
|
var outputFilePath: String, // UriString in Android
|
||||||
var videoID: String? = null,
|
var videoID: String? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
expect class File
|
@ -0,0 +1,3 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
actual typealias File = java.io.File
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
actual data class File(
|
||||||
|
val path: String
|
||||||
|
)
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
actual data class File(
|
||||||
|
val path: String
|
||||||
|
)
|
@ -41,7 +41,6 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(Extras.mp3agic)
|
implementation(Extras.mp3agic)
|
||||||
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
|
|
||||||
// implementation(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
// implementation(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,26 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.github.k1rakishou.fsaf.FileManager
|
||||||
|
import com.github.k1rakishou.fsaf.file.AbstractFile
|
||||||
|
import com.github.k1rakishou.fsaf.file.DirectorySegment
|
||||||
|
import com.github.k1rakishou.fsaf.file.FileSegment
|
||||||
|
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||||
|
import com.shabinder.common.di.utils.removeIllegalChars
|
||||||
|
import com.shabinder.common.models.File
|
||||||
|
import com.shabinder.common.models.SpotiFlyerBaseDir
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
@ -31,81 +43,225 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ignore Deprecation
|
||||||
|
* Deprecation is only a Suggestion P-)
|
||||||
|
* */
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val settings: Settings,
|
private val settings: Settings,
|
||||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
): KoinComponent {
|
||||||
companion object {
|
|
||||||
const val DirKey = "downloadDir"
|
private val context: Context = get()
|
||||||
|
val fileManager = FileManager(context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
fileManager.registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir({ getDirType() },
|
||||||
|
getJavaFile = {
|
||||||
|
java.io.File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
||||||
|
+ "/SpotiFlyer/"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getSAFUri = { null }
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
|
companion object {
|
||||||
|
const val DirKey = "downloadDir"
|
||||||
|
const val AnalyticsKey = "analytics"
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
/*
|
||||||
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
* Do we have Analytics Permission?
|
||||||
|
* - Defaults to `False`
|
||||||
|
* */
|
||||||
|
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
actual fun enableAnalytics() {
|
||||||
|
settings.putBoolean(AnalyticsKey,true)
|
||||||
|
}
|
||||||
|
|
||||||
actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
|
private fun getDirType() :BaseDirectory.ActiveBaseDirType{
|
||||||
|
return if(settings.getStringOrNull(DirKey) == null) {
|
||||||
// fun call in order to always access Updated Value
|
// Default Dir
|
||||||
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
|
BaseDirectory.ActiveBaseDirType.JavaFileBaseDir
|
||||||
File.separator + "SpotiFlyer" + File.separator
|
}else {
|
||||||
|
// User Updated Dir
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
BaseDirectory.ActiveBaseDirType.SafBaseDir
|
||||||
|
|
||||||
actual fun createDirectory(dirPath: String) {
|
|
||||||
val yourAppDir = File(dirPath)
|
|
||||||
|
|
||||||
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
|
||||||
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
|
||||||
logger.e { "Unable to create Dir: $dirPath!" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.i { "$dirPath already exists" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun setDownloadDirectory(newBasePath:File) = settings.putString(
|
||||||
|
DirKey,
|
||||||
|
newBasePath.documentFile?.getFullPath()!!
|
||||||
|
)
|
||||||
|
|
||||||
|
fun setDownloadDirectory(treeUri:Uri) {
|
||||||
|
fileManager.registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir(
|
||||||
|
{ getDirType() },
|
||||||
|
getJavaFile = {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
getSAFUri = {
|
||||||
|
treeUri
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")// By Default Save Files to /Music/SpotiFlyer/
|
||||||
|
private val defaultBaseDir = SpotiFlyerBaseDir({ getDirType() },
|
||||||
|
getJavaFile = {java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + "/SpotiFlyer/")},
|
||||||
|
getSAFUri = { null }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image Cache Path
|
||||||
|
// We Will Handling Image relating operations using java.io.File (reason: Faster)
|
||||||
|
actual val imageCachePath: String get() = methods.value.platformActions.imageCacheDir.absolutePath + "/"
|
||||||
|
|
||||||
|
actual fun imageCacheDir(): File = File(fileManager.fromPath(imageCachePath))
|
||||||
|
|
||||||
|
// fun call in order to always access Updated Value
|
||||||
|
actual fun defaultDir(): File = File(fileManager.newBaseDirectoryFile<SpotiFlyerBaseDir>())
|
||||||
|
|
||||||
|
actual fun isPresent(file: File): Boolean = file.documentFile?.let { fileManager.exists(it) } ?: false
|
||||||
|
|
||||||
|
actual fun createDirectory(dirPath: File , subDirectory:String?) {
|
||||||
|
if(dirPath.documentFile != null) {
|
||||||
|
if (subDirectory != null) {
|
||||||
|
fileManager.createDir(dirPath.documentFile!!,subDirectory)
|
||||||
|
}else {
|
||||||
|
fileManager.create(dirPath.documentFile!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*try {
|
||||||
|
val yourAppDir = File(dirPath)
|
||||||
|
|
||||||
|
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
||||||
|
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
||||||
|
logger.e { "Unable to create Dir: $dirPath!" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.i { "$dirPath already exists" }
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
//TRY USING SAF
|
||||||
|
Log.d("Directory","USING SAF to create $dirPath")
|
||||||
|
val file = DocumentFile.fromTreeUri(context, Uri.parse(defaultDir()))
|
||||||
|
DocumentFile.fromFile()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
||||||
File(imageCacheDir()).deleteRecursively()
|
try {
|
||||||
|
java.io.File(imageCachePath).deleteRecursively()
|
||||||
|
}catch (e:Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
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) {
|
): Unit = withContext(dispatcherIO) {
|
||||||
val songFile = File(trackDetails.outputFilePath)
|
val songFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3")
|
||||||
try {
|
try {
|
||||||
/*
|
|
||||||
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
|
/*Make intermediate Dirs if they don't exist yet*/
|
||||||
* */
|
|
||||||
if(!songFile.exists()) {
|
if(!songFile.exists()) {
|
||||||
/*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('.')) {
|
Mp3File(songFile)
|
||||||
|
.removeAllTags()
|
||||||
|
.setId3v1Tags(trackDetails)
|
||||||
|
.setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath)
|
||||||
|
|
||||||
|
// Copy File to Desired Location
|
||||||
|
val documentFile = when(getDirType()){
|
||||||
|
BaseDirectory.ActiveBaseDirType.SafBaseDir -> {
|
||||||
|
fileManager.fromUri(Uri.parse(trackDetails.outputFilePath))
|
||||||
|
}
|
||||||
|
BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> {
|
||||||
|
fileManager.fromPath(trackDetails.outputFilePath)
|
||||||
|
}
|
||||||
|
}.also { fileManager.create(it!!) }
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileManager.copyFileContents(
|
||||||
|
fileManager.fromRawFile(songFile),
|
||||||
|
documentFile!!
|
||||||
|
)
|
||||||
|
songFile.deleteOnExit()
|
||||||
|
/*val inStream = FileInputStream(songFile)
|
||||||
|
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var readLen: Int
|
||||||
|
while (inStream.read(buffer).also { readLen = it } != -1) {
|
||||||
|
outStream?.write(buffer, 0, readLen)
|
||||||
|
}
|
||||||
|
inStream.close()
|
||||||
|
// write the output file (You have now copied the file)
|
||||||
|
outStream?.flush()
|
||||||
|
outStream?.close()*/
|
||||||
|
|
||||||
|
}catch (e:Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
documentFile?.let {
|
||||||
|
addToLibrary(File(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||||
".mp3" -> {
|
".mp3" -> {
|
||||||
Mp3File(File(songFile.absolutePath))
|
Mp3File(songFile)
|
||||||
.removeAllTags()
|
.removeAllTags()
|
||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
.setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath)
|
||||||
addToLibrary(songFile.absolutePath)
|
|
||||||
|
// Copy File to DocumentUri
|
||||||
|
val documentFile = DocumentFile.fromSingleUri(context,Uri.parse(trackDetails.outputFilePath))
|
||||||
|
try {
|
||||||
|
val outStream = context.contentResolver.openOutputStream(documentFile?.uri!!)
|
||||||
|
val inStream = FileInputStream(songFile)
|
||||||
|
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var readLen: Int
|
||||||
|
while (inStream.read(buffer).also { readLen = it } != -1) {
|
||||||
|
outStream?.write(buffer, 0, readLen)
|
||||||
|
}
|
||||||
|
inStream.close()
|
||||||
|
// write the output file (You have now copied the file)
|
||||||
|
outStream?.flush()
|
||||||
|
outStream?.close()
|
||||||
|
|
||||||
|
}catch (e:Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
documentFile?.let {
|
||||||
|
addToLibrary(File(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
".m4a" -> {
|
".m4a" -> {
|
||||||
/*FFmpeg.executeAsync(
|
*//*FFmpeg.executeAsync(
|
||||||
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
||||||
){ _, returnCode ->
|
){ _, returnCode ->
|
||||||
when (returnCode) {
|
when (returnCode) {
|
||||||
@ -127,18 +283,12 @@ actual class Dir actual constructor(
|
|||||||
logger.d { "Async command execution failed with rc=$returnCode" }
|
logger.d { "Async command execution failed with rc=$returnCode" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}*//*
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
// TODO
|
||||||
Mp3File(File(songFile.absolutePath))
|
|
||||||
.removeAllTags()
|
|
||||||
.setId3v1Tags(trackDetails)
|
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
|
||||||
addToLibrary(songFile.absolutePath)
|
|
||||||
} 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()
|
||||||
@ -148,10 +298,12 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
|
actual fun addToLibrary(file: File) {
|
||||||
|
// 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 = imageCachePath + getNameURL(url)
|
||||||
Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap())
|
Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +321,7 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) {
|
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
|
java.io.File(path).parentFile?.mkdirs()
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -189,7 +342,7 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
cacheImage(result, imageCacheDir() + getNameURL(url))
|
cacheImage(result, imageCachePath + getNameURL(url))
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
} else null
|
} else null
|
||||||
@ -200,4 +353,78 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual val db: Database? = spotiFlyerDatabase.instance
|
actual val db: Database? = spotiFlyerDatabase.instance
|
||||||
|
|
||||||
|
actual fun finalOutputPath(
|
||||||
|
itemName: String,
|
||||||
|
type: String,
|
||||||
|
subFolder: String,
|
||||||
|
extension: String,
|
||||||
|
):String = finalOutputFile(
|
||||||
|
itemName,
|
||||||
|
type,
|
||||||
|
subFolder,
|
||||||
|
extension,
|
||||||
|
).documentFile?.getFullPath() ?: throw(Exception("no path for $itemName"))
|
||||||
|
|
||||||
|
actual fun finalOutputFile(
|
||||||
|
itemName: String,
|
||||||
|
type: String,
|
||||||
|
subFolder: String,
|
||||||
|
extension: String,
|
||||||
|
):File {
|
||||||
|
val file = fileManager.create(
|
||||||
|
defaultDir().documentFile!!, //Base Dir
|
||||||
|
DirectorySegment(removeIllegalChars(type)),
|
||||||
|
DirectorySegment(removeIllegalChars(subFolder)),
|
||||||
|
|
||||||
|
)
|
||||||
|
return File(file?.clone(FileSegment(removeIllegalChars(itemName) + extension)))/*.also {
|
||||||
|
if(fileManager.getLength(it.documentFile!!) == 0L){
|
||||||
|
fileManager.delete(it.documentFile!!)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
/*GlobalScope.launch {
|
||||||
|
// Create Intermediate Directories
|
||||||
|
var file = defaultDir().documentFile
|
||||||
|
file = file.findFile(removeIllegalChars(type))
|
||||||
|
?: file.createDirectory(removeIllegalChars(type))
|
||||||
|
?: throw Exception("Couldn't Find/Create $type Directory")
|
||||||
|
|
||||||
|
if (subFolder.isNotEmpty()) file.findFile(removeIllegalChars(subFolder))
|
||||||
|
?: file.createDirectory(removeIllegalChars(subFolder))
|
||||||
|
?: throw Exception("Couldn't Find/Create $subFolder Directory")
|
||||||
|
|
||||||
|
}
|
||||||
|
val sep = "%2F"
|
||||||
|
val finalUri = defaultDir().documentFile.uri.toString() + sep +
|
||||||
|
removeIllegalChars(type) + sep +
|
||||||
|
removeIllegalChars(subFolder) + sep +
|
||||||
|
removeIllegalChars(itemName) + extension
|
||||||
|
return File(
|
||||||
|
DocumentFile.fromSingleUri(context,Uri.parse(finalUri))!!
|
||||||
|
).also {
|
||||||
|
Log.d("Final Output File",it.documentFile.uri.toString())
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
/*file = file?.findFile(removeIllegalChars(type))
|
||||||
|
?: file?.createDirectory(removeIllegalChars(type))
|
||||||
|
?: throw Exception("Couldn't Find/Create $type Directory")
|
||||||
|
|
||||||
|
if (subFolder.isNotEmpty()) file = file.findFile(removeIllegalChars(subFolder))
|
||||||
|
?: file.createDirectory(removeIllegalChars(subFolder))
|
||||||
|
?: throw Exception("Couldn't Find/Create $subFolder Directory")
|
||||||
|
|
||||||
|
// TODO check Mime
|
||||||
|
file = file.findFile(removeIllegalChars(itemName))
|
||||||
|
?: file.createFile("audio/mpeg",removeIllegalChars(itemName))
|
||||||
|
?: throw Exception("Couldn't Find/Create ${removeIllegalChars(itemName) + extension} File")
|
||||||
|
Log.d("Final Output File",file.uri.toString())
|
||||||
|
|
||||||
|
return File(file).also {
|
||||||
|
val size = it.documentFile.length()
|
||||||
|
Log.d("File size", size.toString())
|
||||||
|
if(size == 0L) it.documentFile.delete()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,9 +99,8 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails,tempMp3Path:String) {
|
||||||
val id3v2Tag = ID3v24Tag().apply {
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
|
|
||||||
artist = track.artists.joinToString(", ")
|
artist = track.artists.joinToString(", ")
|
||||||
title = track.title
|
title = track.title
|
||||||
album = track.albumName
|
album = track.albumName
|
||||||
@ -118,9 +117,9 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
|||||||
fis.close()
|
fis.close()
|
||||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(tempMp3Path)
|
||||||
} catch (e: java.io.FileNotFoundException) {
|
} catch (e: java.io.FileNotFoundException) {
|
||||||
Log.e("Error", "Couldn't Write Cached Mp3 Album Art, error: ${e.stackTrace}")
|
Log.e("Error", "Couldn't Write Cached Mp3 Album Art, Downloading And Trying Again, error: ${e.message}")
|
||||||
try {
|
try {
|
||||||
// Image Still Not Downloaded!
|
// Image Still Not Downloaded!
|
||||||
// Lets Download Now and Write it into Album Art
|
// Lets Download Now and Write it into Album Art
|
||||||
@ -130,7 +129,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
|||||||
is DownloadResult.Success -> {
|
is DownloadResult.Success -> {
|
||||||
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(tempMp3Path)
|
||||||
}
|
}
|
||||||
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
||||||
}
|
}
|
||||||
@ -143,6 +142,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Mp3File.saveFile(filePath: String) {
|
fun Mp3File.saveFile(filePath: String) {
|
||||||
|
Log.d("Mp3 File Save",filePath)
|
||||||
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
m4aFile.delete()
|
m4aFile.delete()
|
||||||
|
@ -38,7 +38,10 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.github.k1rakishou.fsaf.FileManager
|
||||||
|
import com.github.k1rakishou.fsaf.file.AbstractFile
|
||||||
import com.shabinder.common.di.*
|
import com.shabinder.common.di.*
|
||||||
|
import com.shabinder.common.di.providers.getData
|
||||||
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
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
@ -47,6 +50,7 @@ import com.shabinder.downloader.models.formats.Format
|
|||||||
import com.shabinder.common.models.Status
|
import com.shabinder.common.models.Status
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
@ -337,34 +341,14 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
service.createNotificationChannel(channel)
|
service.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleaning All Residual Files except Mp3 Files
|
|
||||||
**/
|
|
||||||
private fun cleanFiles(dir: File) {
|
|
||||||
logger.d(tag) { "Starting Cleaning in ${dir.path} " }
|
|
||||||
val fList = dir.listFiles()
|
|
||||||
fList?.let {
|
|
||||||
for (file in fList) {
|
|
||||||
if (file.isDirectory) {
|
|
||||||
cleanFiles(file)
|
|
||||||
} else if (file.isFile) {
|
|
||||||
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
|
||||||
logger.d(tag) { "Cleaning ${file.path}" }
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun killService() {
|
private fun killService() {
|
||||||
launch {
|
launch {
|
||||||
logger.d(tag) { "Killing Self" }
|
logger.d(tag) { "Killing Self" }
|
||||||
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
|
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
|
||||||
downloadService.close()
|
downloadService.close()
|
||||||
updateNotification()
|
updateNotification()
|
||||||
cleanFiles(File(dir.defaultDir()))
|
// dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) }
|
||||||
// TODO cleanFiles(File(dir.imageCacheDir()))
|
cleanFiles(File(dir.imageCachePath + "Tracks/"),logger)
|
||||||
messageList = mutableListOf("", "", "", "", "")
|
messageList = mutableListOf("", "", "", "", "")
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.shabinder.common.di.worker
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.github.k1rakishou.fsaf.FileManager
|
||||||
|
import com.github.k1rakishou.fsaf.file.AbstractFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleaning All Residual Files except Mp3 Files
|
||||||
|
**/
|
||||||
|
fun cleanFiles(dir: File,logger: Kermit) {
|
||||||
|
try {
|
||||||
|
logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " }
|
||||||
|
val fList = dir.listFiles()
|
||||||
|
fList?.let {
|
||||||
|
for (file in fList) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
cleanFiles(file, logger)
|
||||||
|
} else if (file.isFile) {
|
||||||
|
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
||||||
|
logger.d("Files Cleaning") { "Cleaning ${file.path}" }
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e:Exception) { e.printStackTrace() }
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cleaning All Residual Files except Mp3 Files
|
||||||
|
**/
|
||||||
|
fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) {
|
||||||
|
try {
|
||||||
|
logger.d("Files Cleaning") { "Starting Cleaning in ${directory.getFullPath()} " }
|
||||||
|
val fList = fm.listFiles(directory)
|
||||||
|
for (file in fList) {
|
||||||
|
if (fm.isDirectory(file)) {
|
||||||
|
cleanFiles(file, fm, logger)
|
||||||
|
} else if (fm.isFile(file)) {
|
||||||
|
if (file.getFullPath().substringAfterLast(".") != "mp3") {
|
||||||
|
logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" }
|
||||||
|
fm.delete(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e:Exception) { e.printStackTrace() }
|
||||||
|
}
|
@ -17,7 +17,6 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import co.touchlab.stately.ensureNeverFrozen
|
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.shabinder.common.database.databaseModule
|
import com.shabinder.common.database.databaseModule
|
||||||
import com.shabinder.common.database.getLogger
|
import com.shabinder.common.database.getLogger
|
||||||
@ -25,7 +24,7 @@ import com.shabinder.common.di.providers.GaanaProvider
|
|||||||
import com.shabinder.common.di.providers.SpotifyProvider
|
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.models.NativeAtomicReference
|
import com.shabinder.common.di.providers.YoutubeProvider
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.features.HttpTimeout
|
import io.ktor.client.features.HttpTimeout
|
||||||
import io.ktor.client.features.json.JsonFeature
|
import io.ktor.client.features.json.JsonFeature
|
||||||
@ -87,12 +86,6 @@ fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}*/
|
}*/
|
||||||
// Timeout
|
|
||||||
install(HttpTimeout) {
|
|
||||||
// requestTimeoutMillis = 20000L
|
|
||||||
connectTimeoutMillis = 15000L
|
|
||||||
socketTimeoutMillis = 15000L
|
|
||||||
}
|
|
||||||
if (enableNetworkLogs) {
|
if (enableNetworkLogs) {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
|
@ -18,10 +18,10 @@ package com.shabinder.common.di
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.russhwolf.settings.SettingsListener
|
|
||||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||||
import com.shabinder.common.di.utils.removeIllegalChars
|
import com.shabinder.common.di.utils.removeIllegalChars
|
||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
|
import com.shabinder.common.models.File
|
||||||
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.*
|
||||||
@ -38,17 +38,27 @@ expect class Dir (
|
|||||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
) {
|
) {
|
||||||
val db: Database?
|
val db: Database?
|
||||||
fun isPresent(path: String): Boolean
|
val isAnalyticsEnabled:Boolean
|
||||||
fun fileSeparator(): String
|
fun enableAnalytics()
|
||||||
fun defaultDir(): String
|
fun isPresent(file: File): Boolean
|
||||||
fun imageCacheDir(): String
|
//fun fileSeparator(): String
|
||||||
fun createDirectory(dirPath: String)
|
fun defaultDir(): File
|
||||||
fun setDownloadDirectory(newBasePath:String)
|
fun imageCacheDir(): File
|
||||||
|
val imageCachePath: String
|
||||||
|
|
||||||
|
/*
|
||||||
|
* on Android dirPath represents Directory Name
|
||||||
|
* */
|
||||||
|
fun createDirectory(dirPath: File , subDirectory:String? = null)
|
||||||
|
|
||||||
|
fun setDownloadDirectory(newBasePath:File)
|
||||||
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(file: File)
|
||||||
|
fun finalOutputFile(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): File
|
||||||
|
fun finalOutputPath(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): String
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
||||||
@ -92,8 +102,9 @@ suspend fun downloadByteArray(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to get cache Image Name
|
||||||
fun getNameURL(url: String): String {
|
fun getNameURL(url: String): String {
|
||||||
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_') + ".jpeg"
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -102,13 +113,13 @@ fun getNameURL(url: String): String {
|
|||||||
fun Dir.createDirectories() {
|
fun Dir.createDirectories() {
|
||||||
createDirectory(defaultDir())
|
createDirectory(defaultDir())
|
||||||
createDirectory(imageCacheDir())
|
createDirectory(imageCacheDir())
|
||||||
createDirectory(defaultDir() + "Tracks/")
|
createDirectory(defaultDir(),"Tracks/")
|
||||||
createDirectory(defaultDir() + "Albums/")
|
createDirectory(defaultDir(),"Albums/")
|
||||||
createDirectory(defaultDir() + "Playlists/")
|
createDirectory(defaultDir(),"Playlists/")
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
createDirectory(defaultDir(),"YT_Downloads/")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
/*fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
||||||
removeIllegalChars(itemName) + extension
|
removeIllegalChars(itemName) + extension*/
|
||||||
|
@ -21,13 +21,10 @@ import com.shabinder.common.di.providers.GaanaProvider
|
|||||||
import com.shabinder.common.di.providers.SpotifyProvider
|
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.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class FetchPlatformQueryResult(
|
class FetchPlatformQueryResult(
|
||||||
val gaanaProvider: GaanaProvider,
|
val gaanaProvider: GaanaProvider,
|
||||||
|
@ -18,8 +18,8 @@ package com.shabinder.common.di.providers
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.common.di.finalOutputDir
|
|
||||||
import com.shabinder.common.di.gaana.GaanaRequests
|
import com.shabinder.common.di.gaana.GaanaRequests
|
||||||
|
import com.shabinder.common.di.getNameURL
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
@ -136,7 +136,7 @@ class GaanaProvider(
|
|||||||
title = it.track_title,
|
title = it.track_title,
|
||||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
durationSec = it.duration,
|
durationSec = it.duration,
|
||||||
albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
albumArtPath = dir.imageCachePath + getNameURL(it.artworkLink),
|
||||||
albumName = it.album_title,
|
albumName = it.album_title,
|
||||||
year = it.release_date,
|
year = it.release_date,
|
||||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||||
@ -144,16 +144,15 @@ class GaanaProvider(
|
|||||||
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.finalOutputPath(it.track_title, type, subFolder /*,".m4a"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String) {
|
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||||
if (dir.isPresent(
|
if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputFile(
|
||||||
track_title,
|
track_title,
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
dir.defaultDir()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) { // Download Already Present!!
|
) { // Download Already Present!!
|
||||||
|
@ -17,14 +17,10 @@
|
|||||||
package com.shabinder.common.di.providers
|
package com.shabinder.common.di.providers
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import co.touchlab.stately.ensureNeverFrozen
|
|
||||||
import co.touchlab.stately.freeze
|
|
||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.common.di.TokenStore
|
import com.shabinder.common.di.TokenStore
|
||||||
import com.shabinder.common.di.createHttpClient
|
import com.shabinder.common.di.createHttpClient
|
||||||
import com.shabinder.common.di.finalOutputDir
|
import com.shabinder.common.di.getNameURL
|
||||||
import com.shabinder.common.di.kotlinxSerializer
|
|
||||||
import com.shabinder.common.di.ktorHttpClient
|
|
||||||
import com.shabinder.common.di.spotify.SpotifyRequests
|
import com.shabinder.common.di.spotify.SpotifyRequests
|
||||||
import com.shabinder.common.di.spotify.authenticateSpotify
|
import com.shabinder.common.di.spotify.authenticateSpotify
|
||||||
import com.shabinder.common.models.NativeAtomicReference
|
import com.shabinder.common.models.NativeAtomicReference
|
||||||
@ -35,12 +31,8 @@ import com.shabinder.common.models.spotify.Image
|
|||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
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.features.auth.*
|
|
||||||
import io.ktor.client.features.auth.providers.*
|
|
||||||
import io.ktor.client.features.defaultRequest
|
import io.ktor.client.features.defaultRequest
|
||||||
import io.ktor.client.features.json.JsonFeature
|
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import kotlin.native.concurrent.SharedImmutable
|
|
||||||
|
|
||||||
class SpotifyProvider(
|
class SpotifyProvider(
|
||||||
private val tokenStore: TokenStore,
|
private val tokenStore: TokenStore,
|
||||||
@ -224,28 +216,28 @@ class SpotifyProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Track>.toTrackDetailsList(type: String, subFolder: String) = this.map {
|
private fun List<Track>.toTrackDetailsList(type: String, subFolder: String) = this.map {
|
||||||
|
val albumArtLink = it.album?.images?.firstOrNull()?.url.toString()
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = it.name.toString(),
|
title = it.name.toString(),
|
||||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||||
durationSec = (it.duration_ms / 1000).toInt(),
|
durationSec = (it.duration_ms / 1000).toInt(),
|
||||||
albumArtPath = dir.imageCacheDir() + (it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
albumArtPath = dir.imageCachePath + getNameURL(albumArtLink),
|
||||||
albumName = it.album?.name,
|
albumName = it.album?.name,
|
||||||
year = it.album?.release_date,
|
year = it.album?.release_date,
|
||||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
trackUrl = it.href,
|
trackUrl = it.href,
|
||||||
downloaded = it.downloaded,
|
downloaded = it.downloaded,
|
||||||
source = Source.Spotify,
|
source = Source.Spotify,
|
||||||
albumArtURL = it.album?.images?.firstOrNull()?.url.toString(),
|
albumArtURL = albumArtLink,
|
||||||
outputFilePath = dir.finalOutputDir(it.name.toString(), type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
outputFilePath = dir.finalOutputPath(it.name.toString(), type, subFolder/*,".m4a"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private fun Track.updateStatusIfPresent(folderType: String, subFolder: String) {
|
private fun Track.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||||
if (dir.isPresent(
|
if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputFile(
|
||||||
name.toString(),
|
name.toString(),
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
dir.defaultDir()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) { // Download Already Present!!
|
) { // Download Already Present!!
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di.providers
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.shabinder.common.di.Dir
|
||||||
|
import com.shabinder.common.di.getNameURL
|
||||||
import com.shabinder.common.di.utils.removeIllegalChars
|
import com.shabinder.common.di.utils.removeIllegalChars
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
@ -106,15 +108,14 @@ class YoutubeProvider(
|
|||||||
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.imageCachePath + getNameURL(coverUrl),
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
albumArtURL = coverUrl,
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputFile(
|
||||||
itemName = it.title ?: "N/A",
|
itemName = it.title ?: "N/A",
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
dir.defaultDir()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -122,7 +123,7 @@ class YoutubeProvider(
|
|||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
outputFilePath = dir.finalOutputPath(it.title ?: "N/A", folderType, subFolder/*,".m4a"*/),
|
||||||
videoID = it.videoId
|
videoID = it.videoId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -160,15 +161,14 @@ class YoutubeProvider(
|
|||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail.author ?: "N/A"),
|
artists = listOf(detail.author ?: "N/A"),
|
||||||
durationSec = detail.lengthSeconds,
|
durationSec = detail.lengthSeconds,
|
||||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
albumArtPath = dir.imageCachePath + getNameURL(coverUrl),
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = coverUrl,
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputFile(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = dir.defaultDir()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -176,7 +176,7 @@ class YoutubeProvider(
|
|||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
outputFilePath = dir.finalOutputPath(name, folderType, subFolder /*,".m4a"*/),
|
||||||
videoID = searchId
|
videoID = searchId
|
||||||
)
|
)
|
||||||
)
|
)
|
@ -90,6 +90,7 @@ fun removeIllegalChars(fileName: String): String {
|
|||||||
name = fileName.replace(c, '_')
|
name = fileName.replace(c, '_')
|
||||||
}
|
}
|
||||||
name = name.replace("\\s".toRegex(), "_")
|
name = name.replace("\\s".toRegex(), "_")
|
||||||
|
name = name.replace("/".toRegex(), "_")
|
||||||
name = name.replace("\\)".toRegex(), "")
|
name = name.replace("\\)".toRegex(), "")
|
||||||
name = name.replace("\\(".toRegex(), "")
|
name = name.replace("\\(".toRegex(), "")
|
||||||
name = name.replace("\\[".toRegex(), "")
|
name = name.replace("\\[".toRegex(), "")
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import com.shabinder.common.di.providers.YoutubeMp3
|
import com.shabinder.common.di.providers.YoutubeMp3
|
||||||
|
import com.shabinder.common.di.providers.getData
|
||||||
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
|
||||||
|
@ -45,6 +45,13 @@ actual class Dir actual constructor(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DirKey = "downloadDir"
|
const val DirKey = "downloadDir"
|
||||||
|
const val AnalyticsKey = "analytics"
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
||||||
|
|
||||||
|
actual fun enableAnalytics() {
|
||||||
|
settings.putBoolean(AnalyticsKey,true)
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import com.shabinder.common.di.providers.getData
|
||||||
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
|
||||||
|
@ -21,7 +21,6 @@ import platform.Foundation.sendSynchronousRequest
|
|||||||
import platform.Foundation.writeToFile
|
import platform.Foundation.writeToFile
|
||||||
import platform.UIKit.UIImage
|
import platform.UIKit.UIImage
|
||||||
import platform.UIKit.UIImageJPEGRepresentation
|
import platform.UIKit.UIImageJPEGRepresentation
|
||||||
import java.lang.System
|
|
||||||
|
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
val logger: Kermit,
|
val logger: Kermit,
|
||||||
@ -30,6 +29,13 @@ actual class Dir actual constructor(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DirKey = "downloadDir"
|
const val DirKey = "downloadDir"
|
||||||
|
const val AnalyticsKey = "analytics"
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
||||||
|
|
||||||
|
actual fun enableAnalytics() {
|
||||||
|
settings.putBoolean(AnalyticsKey,true)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
|
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
|
||||||
|
@ -39,8 +39,17 @@ actual class Dir actual constructor(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DirKey = "downloadDir"
|
const val DirKey = "downloadDir"
|
||||||
|
const val AnalyticsKey = "analytics"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
|
||||||
|
|
||||||
|
actual fun enableAnalytics() {
|
||||||
|
settings.putBoolean(AnalyticsKey,true)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
|
||||||
|
|
||||||
/*init {
|
/*init {
|
||||||
createDirectories()
|
createDirectories()
|
||||||
}*/
|
}*/
|
||||||
|
@ -65,6 +65,11 @@ interface SpotiFlyerList {
|
|||||||
val link: String
|
val link: String
|
||||||
val listOutput: Consumer<Output>
|
val listOutput: Consumer<Output>
|
||||||
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
|
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
|
||||||
|
val listAnalytics: Analytics
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Analytics {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Output {
|
sealed class Output {
|
||||||
|
@ -25,12 +25,13 @@ import com.shabinder.common.main.integration.SpotiFlyerMainImpl
|
|||||||
import com.shabinder.common.models.Consumer
|
import com.shabinder.common.models.Consumer
|
||||||
import com.shabinder.common.models.DownloadRecord
|
import com.shabinder.common.models.DownloadRecord
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface SpotiFlyerMain {
|
interface SpotiFlyerMain {
|
||||||
|
|
||||||
val models: Value<State>
|
val models: Value<State>
|
||||||
|
|
||||||
|
val analytics: Analytics
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We Intend to Move to List Screen
|
* We Intend to Move to List Screen
|
||||||
* Note: Implementation in Root
|
* Note: Implementation in Root
|
||||||
@ -57,6 +58,11 @@ interface SpotiFlyerMain {
|
|||||||
val storeFactory: StoreFactory
|
val storeFactory: StoreFactory
|
||||||
val database: Database?
|
val database: Database?
|
||||||
val dir: Dir
|
val dir: Dir
|
||||||
|
val mainAnalytics: Analytics
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Analytics {
|
||||||
|
fun donationDialogVisit()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Output {
|
sealed class Output {
|
||||||
|
@ -18,7 +18,6 @@ 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.lifecycle.doOnDestroy
|
|
||||||
import com.arkivanov.decompose.value.Value
|
import com.arkivanov.decompose.value.Value
|
||||||
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
|
||||||
@ -40,9 +39,6 @@ internal class SpotiFlyerMainImpl(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
instanceKeeper.ensureNeverFrozen()
|
instanceKeeper.ensureNeverFrozen()
|
||||||
lifecycle.doOnDestroy {
|
|
||||||
cache.invalidateAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val store =
|
private val store =
|
||||||
@ -55,11 +51,13 @@ internal class SpotiFlyerMainImpl(
|
|||||||
|
|
||||||
private val cache = Cache.Builder
|
private val cache = Cache.Builder
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.maximumCacheSize(20)
|
.maximumCacheSize(25)
|
||||||
.build<String, Picture>()
|
.build<String, Picture>()
|
||||||
|
|
||||||
override val models: Value<State> = store.asValue()
|
override val models: Value<State> = store.asValue()
|
||||||
|
|
||||||
|
override val analytics = mainAnalytics
|
||||||
|
|
||||||
override fun onLinkSearch(link: String) {
|
override fun onLinkSearch(link: String) {
|
||||||
if (methods.value.isInternetAvailable) mainOutput.callback(Output.Search(link = link))
|
if (methods.value.isInternetAvailable) mainOutput.callback(Output.Search(link = link))
|
||||||
else methods.value.showPopUpMessage("Check Network Connection Please")
|
else methods.value.showPopUpMessage("Check Network Connection Please")
|
||||||
|
@ -54,6 +54,14 @@ interface SpotiFlyerRoot {
|
|||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Analytics {
|
||||||
|
fun appLaunchEvent()
|
||||||
|
fun homeScreenVisit()
|
||||||
|
fun listScreenVisit()
|
||||||
|
fun donationDialogVisit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ import com.shabinder.common.models.AllPlatforms
|
|||||||
import com.shabinder.common.models.Consumer
|
import com.shabinder.common.models.Consumer
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot
|
import com.shabinder.common.root.SpotiFlyerRoot
|
||||||
|
import com.shabinder.common.root.SpotiFlyerRoot.Analytics
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot.Child
|
import com.shabinder.common.root.SpotiFlyerRoot.Child
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
|
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
|
||||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||||
@ -49,7 +50,8 @@ 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
|
||||||
) : SpotiFlyerRoot, ComponentContext by componentContext {
|
) : SpotiFlyerRoot, ComponentContext by componentContext {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -72,7 +74,8 @@ internal class SpotiFlyerRootImpl(
|
|||||||
dependencies
|
dependencies
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = dependencies.actions.freeze()
|
actions = dependencies.actions.freeze(),
|
||||||
|
analytics = dependencies.analytics
|
||||||
) {
|
) {
|
||||||
instanceKeeper.ensureNeverFrozen()
|
instanceKeeper.ensureNeverFrozen()
|
||||||
methods.value = dependencies.actions.freeze()
|
methods.value = dependencies.actions.freeze()
|
||||||
@ -113,16 +116,23 @@ internal class SpotiFlyerRootImpl(
|
|||||||
|
|
||||||
private fun onMainOutput(output: SpotiFlyerMain.Output) =
|
private fun onMainOutput(output: SpotiFlyerMain.Output) =
|
||||||
when (output) {
|
when (output) {
|
||||||
is SpotiFlyerMain.Output.Search -> router.push(Configuration.List(link = output.link))
|
is SpotiFlyerMain.Output.Search -> {
|
||||||
|
router.push(Configuration.List(link = output.link))
|
||||||
|
analytics.listScreenVisit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListOutput(output: SpotiFlyerList.Output): Unit =
|
private fun onListOutput(output: SpotiFlyerList.Output): Unit =
|
||||||
when (output) {
|
when (output) {
|
||||||
is SpotiFlyerList.Output.Finished -> router.pop()
|
is SpotiFlyerList.Output.Finished -> {
|
||||||
|
router.pop()
|
||||||
|
analytics.homeScreenVisit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){
|
private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
|
analytics.appLaunchEvent()
|
||||||
/*Authenticate Spotify Client*/
|
/*Authenticate Spotify Client*/
|
||||||
spotifyProvider.authenticateSpotifyClient(override)
|
spotifyProvider.authenticateSpotifyClient(override)
|
||||||
}
|
}
|
||||||
@ -143,6 +153,9 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<
|
|||||||
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
|
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
|
||||||
override val mainOutput: Consumer<SpotiFlyerMain.Output> = output
|
override val mainOutput: Consumer<SpotiFlyerMain.Output> = output
|
||||||
override val dir: Dir = directories
|
override val dir: Dir = directories
|
||||||
|
override val mainAnalytics = object : SpotiFlyerMain.Analytics {
|
||||||
|
override fun donationDialogVisit() = analytics.donationDialogVisit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,5 +168,6 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out
|
|||||||
override val link: String = link
|
override val link: String = link
|
||||||
override val listOutput: Consumer<SpotiFlyerList.Output> = output
|
override val listOutput: Consumer<SpotiFlyerList.Output> = output
|
||||||
override val downloadProgressFlow = downloadProgressReport
|
override val downloadProgressFlow = downloadProgressReport
|
||||||
|
override val listAnalytics = object : SpotiFlyerList.Analytics {}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user