mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-12-22 12:47:54 +01:00
Major Module and Code Refactoring
This commit is contained in:
parent
04dbff4d7f
commit
59068c6c8b
@ -104,6 +104,8 @@ dependencies {
|
||||
implementation(project(":common:root"))
|
||||
implementation(project(":common:dependency-injection"))
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:core-components"))
|
||||
implementation(project(":common:providers"))
|
||||
|
||||
// Koin
|
||||
implementation(Koin.android)
|
||||
|
@ -42,7 +42,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
|
||||
import com.arkivanov.decompose.defaultComponentContext
|
||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||
import com.codekidlabs.storagechooser.R
|
||||
@ -51,14 +51,14 @@ import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsPadding
|
||||
import com.google.accompanist.insets.statusBarsHeight
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import com.shabinder.common.di.ConnectionLiveData
|
||||
import com.shabinder.common.di.Dir
|
||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||
import com.shabinder.common.di.analytics.AnalyticsManager
|
||||
import com.shabinder.common.core_components.ConnectionLiveData
|
||||
import com.shabinder.common.core_components.analytics.AnalyticsManager
|
||||
import com.shabinder.common.core_components.file_manager.FileManager
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.di.observeAsState
|
||||
import com.shabinder.common.di.preference.PreferenceManager
|
||||
import com.shabinder.common.models.*
|
||||
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
|
||||
import com.shabinder.common.providers.FetchPlatformQueryResult
|
||||
import com.shabinder.common.root.SpotiFlyerRoot
|
||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||
import com.shabinder.common.translations.Strings
|
||||
@ -82,16 +82,17 @@ import java.io.File
|
||||
@ExperimentalAnimationApi
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var root: SpotiFlyerRoot
|
||||
private val fetcher: FetchPlatformQueryResult by inject()
|
||||
private val dir: Dir by inject()
|
||||
private val fileManager: FileManager by inject()
|
||||
private val preferenceManager: PreferenceManager by inject()
|
||||
private val analyticsManager: AnalyticsManager by inject { parametersOf(this) }
|
||||
private lateinit var root: SpotiFlyerRoot
|
||||
private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks
|
||||
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
|
||||
private var permissionGranted = mutableStateOf(true)
|
||||
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
|
||||
|
||||
private val rootComponent = spotiFlyerRoot(defaultComponentContext())
|
||||
// private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance
|
||||
|
||||
// Variable for storing instance of our service class
|
||||
@ -114,7 +115,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
Box {
|
||||
root = SpotiFlyerRootContent(
|
||||
rememberRootComponent(::spotiFlyerRoot),
|
||||
rootComponent,
|
||||
Modifier.statusBarsPadding().navigationBarsPadding()
|
||||
)
|
||||
Spacer(
|
||||
@ -242,6 +243,7 @@ class MainActivity : ComponentActivity() {
|
||||
).show()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
permissionGranted.value = checkPermissions()
|
||||
@ -252,9 +254,9 @@ class MainActivity : ComponentActivity() {
|
||||
componentContext,
|
||||
dependencies = object : SpotiFlyerRoot.Dependencies {
|
||||
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
|
||||
override val database = this@MainActivity.dir.db
|
||||
override val database = this@MainActivity.fileManager.db
|
||||
override val fetchQuery = this@MainActivity.fetcher
|
||||
override val dir: Dir = this@MainActivity.dir
|
||||
override val fileManager: FileManager = this@MainActivity.fileManager
|
||||
override val preferenceManager = this@MainActivity.preferenceManager
|
||||
override val analyticsManager: AnalyticsManager = this@MainActivity.analyticsManager
|
||||
override val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
|
||||
@ -275,8 +277,10 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
override fun sendTracksToService(array: List<TrackDetails>) {
|
||||
if (foregroundService == null) initForegroundService()
|
||||
foregroundService?.downloadAllTracks(array)
|
||||
for (chunk in array.chunked(25)) {
|
||||
if (foregroundService == null) initForegroundService()
|
||||
foregroundService?.downloadAllTracks(array)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,7 +380,7 @@ class MainActivity : ComponentActivity() {
|
||||
// hell yeah :)
|
||||
preferenceManager.setDownloadDirectory(path)
|
||||
callBack(path)
|
||||
showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}"))
|
||||
showPopUpMessage(Strings.downloadDirectorySetTo("\n${fileManager.defaultDir()}"))
|
||||
} else {
|
||||
showPopUpMessage(Strings.noWriteAccess("\n$path "))
|
||||
}
|
||||
@ -386,6 +390,7 @@ class MainActivity : ComponentActivity() {
|
||||
chooser.show()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
@ -33,17 +33,17 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.di.Dir
|
||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||
import com.shabinder.common.di.R
|
||||
import com.shabinder.common.di.downloadFile
|
||||
import com.shabinder.common.di.utils.ParallelExecutor
|
||||
import com.shabinder.common.core_components.file_manager.FileManager
|
||||
import com.shabinder.common.core_components.file_manager.downloadFile
|
||||
import com.shabinder.common.core_components.parallel_executor.ParallelExecutor
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.models.event.coroutines.failure
|
||||
import com.shabinder.common.providers.FetchPlatformQueryResult
|
||||
import com.shabinder.common.translations.Strings
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.utils.autoclear.AutoClear
|
||||
import com.shabinder.spotiflyer.utils.autoclear.autoClear
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -60,7 +60,7 @@ class ForegroundService : LifecycleService() {
|
||||
val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1), lifecycleScope) }
|
||||
private val fetcher: FetchPlatformQueryResult by inject()
|
||||
private val logger: Kermit by inject()
|
||||
private val dir: Dir by inject()
|
||||
private val dir: FileManager by inject()
|
||||
|
||||
private var messageList = java.util.Collections.synchronizedList(MutableList(5) { emptyMessage })
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
@ -136,8 +136,8 @@ class ForegroundService : LifecycleService() {
|
||||
for (track in trackList) {
|
||||
trackStatusFlowMap[track.title] = DownloadStatus.Queued
|
||||
lifecycleScope.launch {
|
||||
downloadService.value.execute {
|
||||
fetcher.findMp3DownloadLink(track).fold(
|
||||
downloadService.value.executeSuspending {
|
||||
fetcher.findBestDownloadLink(track).fold(
|
||||
success = { url ->
|
||||
enqueueDownload(url, track)
|
||||
},
|
||||
|
@ -108,7 +108,7 @@ object JetBrains {
|
||||
|
||||
object Compose {
|
||||
// __LATEST_COMPOSE_RELEASE_VERSION__
|
||||
private const val VERSION = "1.0.0-alpha3"
|
||||
private const val VERSION = "1.0.0-alpha2"
|
||||
const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION"
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ kotlin {
|
||||
implementation(project(":common:main"))
|
||||
implementation(project(":common:list"))
|
||||
implementation(project(":common:preference"))
|
||||
implementation(project(":common:core-components"))
|
||||
implementation(project(":common:database"))
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:dependency-injection"))
|
||||
|
@ -11,8 +11,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.di.dispatcherIO
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
|
@ -2,7 +2,7 @@ package com.shabinder.common.uikit
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
|
||||
@Composable
|
||||
expect fun ImageLoad(
|
||||
|
@ -53,7 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.list.SpotiFlyerList
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
|
@ -78,7 +78,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.main.SpotiFlyerMain
|
||||
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
||||
import com.shabinder.common.models.DownloadRecord
|
||||
|
@ -2,17 +2,12 @@ package com.shabinder.common.uikit
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.di.dispatcherIO
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
@ -31,6 +26,11 @@ actual fun ImageLoad(
|
||||
}
|
||||
|
||||
Crossfade(pic) {
|
||||
if (it == null) Image(PlaceHolderImage(), desc, modifier, contentScale = ContentScale.Crop) else Image(it, desc, modifier, contentScale = ContentScale.Crop)
|
||||
if (it == null) Image(PlaceHolderImage(), desc, modifier, contentScale = ContentScale.Crop) else Image(
|
||||
it,
|
||||
desc,
|
||||
modifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
|
39
common/core-components/build.gradle.kts
Normal file
39
common/core-components/build.gradle.kts
Normal file
@ -0,0 +1,39 @@
|
||||
plugins {
|
||||
id("multiplatform-setup")
|
||||
id("multiplatform-setup-test")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:database"))
|
||||
implementation("org.jetbrains.kotlinx:atomicfu:0.16.2")
|
||||
api(MultiPlatformSettings.dep)
|
||||
implementation(MVIKotlin.rx)
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation(Extras.mp3agic)
|
||||
implementation(Extras.Android.countly)
|
||||
api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||
}
|
||||
}
|
||||
desktopMain {
|
||||
dependencies {
|
||||
implementation(Extras.mp3agic)
|
||||
implementation(Extras.Desktop.countly)
|
||||
implementation("com.github.kokorin.jaffree:jaffree:2021.08.16")
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation(npm("browser-id3-writer", "4.4.0"))
|
||||
implementation(npm("file-saver", "2.0.4"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
common/core-components/src/androidMain/AndroidManifest.xml
Normal file
20
common/core-components/src/androidMain/AndroidManifest.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ * Copyright (c) 2021 Shabinder Singh
|
||||
~ * This program is free software: you can redistribute it and/or modify
|
||||
~ * it under the terms of the GNU General Public License as published by
|
||||
~ * the Free Software Foundation, either version 3 of the License, or
|
||||
~ * (at your option) any later version.
|
||||
~ *
|
||||
~ * This program is distributed in the hope that it will be useful,
|
||||
~ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ * GNU General Public License for more details.
|
||||
~ *
|
||||
~ * You should have received a copy of the GNU General Public License
|
||||
~ * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest package="com.shabinder.common.core_components" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
</manifest>
|
@ -14,7 +14,7 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.CONNECTIVITY_SERVICE
|
||||
@ -24,6 +24,7 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
import android.net.NetworkRequest
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.shabinder.common.core_components.utils.isInternetAccessible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
@ -64,7 +64,7 @@ internal class AndroidAnalyticsManager(private val mainActivity: Activity) : Ana
|
||||
}
|
||||
}
|
||||
|
||||
actual fun analyticsModule() = module {
|
||||
internal actual fun analyticsModule() = module {
|
||||
factory { (mainActivity: Activity) ->
|
||||
AndroidAnalyticsManager(mainActivity)
|
||||
} bind AnalyticsManager::class
|
@ -14,56 +14,76 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components.file_manager
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Environment
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.mpatric.mp3agic.InvalidDataException
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.common.core_components.media_converter.MediaConverter
|
||||
import com.shabinder.common.core_components.media_converter.removeAllTags
|
||||
import com.shabinder.common.core_components.media_converter.setId3v1Tags
|
||||
import com.shabinder.common.core_components.media_converter.setId3v2TagsAndSaveFile
|
||||
import com.shabinder.common.core_components.parallel_executor.ParallelExecutor
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||
import com.shabinder.common.di.preference.PreferenceManager
|
||||
import com.shabinder.common.di.utils.ParallelExecutor
|
||||
import com.shabinder.common.di.getMemoryEfficientBitmap
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.models.event.coroutines.failure
|
||||
import com.shabinder.common.models.event.coroutines.map
|
||||
import com.shabinder.common.models.methods
|
||||
import com.shabinder.database.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
internal actual fun fileManagerModule() = module {
|
||||
single { AndroidFileManager(get(), get(), get(), get()) } bind FileManager::class
|
||||
}
|
||||
|
||||
/*
|
||||
* Ignore Deprecation
|
||||
* Deprecation is only a Suggestion P-)
|
||||
* `Deprecation is only a Suggestion P->`
|
||||
* */
|
||||
@Suppress("DEPRECATION")
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val preferenceManager: PreferenceManager,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) {
|
||||
class AndroidFileManager(
|
||||
override val logger: Kermit,
|
||||
override val preferenceManager: PreferenceManager,
|
||||
override val mediaConverter: MediaConverter,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase
|
||||
) : FileManager {
|
||||
@Suppress("DEPRECATION")
|
||||
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
||||
override fun fileSeparator(): String = File.separator
|
||||
|
||||
actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
|
||||
override fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
|
||||
|
||||
// fun call in order to always access Updated Value
|
||||
actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
|
||||
File.separator + "SpotiFlyer" + File.separator
|
||||
override fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
|
||||
File.separator + "SpotiFlyer" + File.separator
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
override fun isPresent(path: String): Boolean = File(path).exists()
|
||||
|
||||
actual fun createDirectory(dirPath: String) {
|
||||
override fun createDirectory(dirPath: String) {
|
||||
val yourAppDir = File(dirPath)
|
||||
|
||||
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
||||
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
||||
if (yourAppDir.mkdirs()) {
|
||||
logger.i { "$dirPath created" }
|
||||
} else {
|
||||
logger.e { "Unable to create Dir: $dirPath!" }
|
||||
}
|
||||
} else {
|
||||
@ -72,12 +92,12 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
||||
override suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
||||
File(imageCacheDir()).deleteRecursively()
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
actual suspend fun saveFileWithMetadata(
|
||||
override suspend fun saveFileWithMetadata(
|
||||
mp3ByteArray: ByteArray,
|
||||
trackDetails: TrackDetails,
|
||||
postProcess: (track: TrackDetails) -> Unit
|
||||
@ -94,60 +114,54 @@ actual class Dir actual constructor(
|
||||
// Write Bytes to Media File
|
||||
songFile.writeBytes(mp3ByteArray)
|
||||
|
||||
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||
".mp3" -> {
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
}
|
||||
".m4a" -> {
|
||||
/*FFmpeg.executeAsync(
|
||||
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
||||
){ _, returnCode ->
|
||||
when (returnCode) {
|
||||
Config.RETURN_CODE_SUCCESS -> {
|
||||
//FFMPEG task Completed
|
||||
logger.d{ "Async command execution completed successfully." }
|
||||
scope.launch {
|
||||
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
||||
}
|
||||
}
|
||||
Config.RETURN_CODE_CANCEL -> {
|
||||
logger.d{"Async command execution cancelled by user."}
|
||||
}
|
||||
else -> {
|
||||
logger.d { "Async command execution failed with rc=$returnCode" }
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
try {
|
||||
// Add Mp3 Tags and Add to Library
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
// Media File Isn't MP3 lets Convert It first
|
||||
if (e is InvalidDataException) {
|
||||
val convertedFilePath = songFile.absolutePath.substringBeforeLast('.') + ".temp.mp3"
|
||||
|
||||
val conversionResult = mediaConverter.convertAudioFile(
|
||||
inputFilePath = songFile.absolutePath,
|
||||
outputFilePath = convertedFilePath,
|
||||
)
|
||||
|
||||
conversionResult.map { outputFilePath ->
|
||||
Mp3File(File(outputFilePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
} catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
.setId3v2TagsAndSaveFile(trackDetails, trackDetails.outputFilePath)
|
||||
|
||||
addToLibrary(trackDetails.outputFilePath)
|
||||
}.failure {
|
||||
throw it
|
||||
}
|
||||
} else throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SuspendableEvent.success(trackDetails.outputFilePath)
|
||||
} catch (e: Throwable) {
|
||||
if (songFile.exists()) songFile.delete()
|
||||
logger.e { "${songFile.absolutePath} could not be created" }
|
||||
SuspendableEvent.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
|
||||
override fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
|
||||
|
||||
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture = withContext(dispatcherIO) {
|
||||
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture = withContext(dispatcherIO) {
|
||||
val cachePath = imageCacheDir() + getNameURL(url)
|
||||
Picture(image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage(url, reqWidth, reqHeight))?.asImageBitmap())
|
||||
Picture(
|
||||
image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage(
|
||||
url,
|
||||
reqWidth,
|
||||
reqHeight
|
||||
))?.asImageBitmap()
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadCachedImage(cachePath: String, reqWidth: Int, reqHeight: Int): Bitmap? {
|
||||
@ -160,7 +174,7 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
|
||||
override suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
|
||||
try {
|
||||
FileOutputStream(path).use { out ->
|
||||
(image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||
@ -183,7 +197,7 @@ actual class Dir actual constructor(
|
||||
// Get Memory Efficient Bitmap
|
||||
val bitmap: Bitmap? = getMemoryEfficientBitmap(input, reqWidth, reqHeight)
|
||||
|
||||
parallelExecutor.execute {
|
||||
parallelExecutor.executeSuspending {
|
||||
// Decode and Cache Full Sized Image in Background
|
||||
cacheImage(BitmapFactory.decodeByteArray(input, 0, input.size), imageCacheDir() + getNameURL(url))
|
||||
}
|
||||
@ -195,11 +209,11 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
/*
|
||||
* Parallel Executor with 4 concurrent operation at a time.
|
||||
* Parallel Executor with 2 concurrent operation at a time.
|
||||
* - We will use this to queue up operations and decode Full Sized Images
|
||||
* - Will Decode Only 4 at a time , to avoid going into `Out of Memory`
|
||||
* - Will Decode Only a small set of images at a time , to avoid going into `Out of Memory`
|
||||
* */
|
||||
private val parallelExecutor = ParallelExecutor(Dispatchers.IO)
|
||||
private val parallelExecutor = ParallelExecutor(Dispatchers.IO, 2)
|
||||
|
||||
actual val db: Database? = spotiFlyerDatabase.instance
|
||||
override val db: Database? = spotiFlyerDatabase.instance
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.shabinder.common.core_components.media_converter
|
||||
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.SpotiFlyerException
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
class AndroidMediaConverter : MediaConverter() {
|
||||
override suspend fun convertAudioFile(
|
||||
inputFilePath: String,
|
||||
outputFilePath: String,
|
||||
audioQuality: AudioQuality,
|
||||
progressCallbacks: (Long) -> Unit,
|
||||
) = executeSafelyInPool {
|
||||
val kbpsArg = if (audioQuality == AudioQuality.UNKNOWN) "" else "-b:a ${audioQuality.kbps}k"
|
||||
|
||||
val session = FFmpegKit.execute(
|
||||
"-i $inputFilePath -y $kbpsArg -acodec libmp3lame -vn $outputFilePath"
|
||||
)
|
||||
|
||||
when (session.returnCode.value) {
|
||||
ReturnCode.SUCCESS -> {
|
||||
//FFMPEG task Completed
|
||||
outputFilePath
|
||||
}
|
||||
ReturnCode.CANCEL -> {
|
||||
throw SpotiFlyerException.MP3ConversionFailed("FFmpeg Conversion Canceled for $inputFilePath")
|
||||
}
|
||||
else -> throw SpotiFlyerException.MP3ConversionFailed("FFmpeg Conversion Failed for $inputFilePath")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal actual fun mediaConverterModule() = module {
|
||||
single { AndroidMediaConverter() } bind MediaConverter::class
|
||||
}
|
@ -14,12 +14,13 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components.media_converter
|
||||
|
||||
import android.util.Log
|
||||
import com.mpatric.mp3agic.ID3v1Tag
|
||||
import com.mpatric.mp3agic.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.common.core_components.file_manager.downloadFile
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@ -48,7 +49,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, outputFilePath: String? = null) {
|
||||
val id3v2Tag = ID3v24Tag().apply {
|
||||
albumArtist = track.albumArtists.joinToString(", ")
|
||||
artist = track.artists.joinToString(", ")
|
||||
@ -71,7 +72,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
fis.close()
|
||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||
this.id3v2Tag = id3v2Tag
|
||||
saveFile(track.outputFilePath)
|
||||
saveFile(outputFilePath ?: track.outputFilePath)
|
||||
} catch (e: java.io.FileNotFoundException) {
|
||||
Log.e("Error", "Couldn't Write Cached Mp3 Album Art, Downloading And Trying Again, error: ${e.message}")
|
||||
try {
|
||||
@ -83,7 +84,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
is DownloadResult.Success -> {
|
||||
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||
this.id3v2Tag = id3v2Tag
|
||||
saveFile(track.outputFilePath)
|
||||
saveFile(outputFilePath ?: track.outputFilePath)
|
||||
}
|
||||
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
||||
}
|
||||
@ -96,9 +97,11 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
}
|
||||
|
||||
fun Mp3File.saveFile(filePath: String) {
|
||||
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||
val m4aFile = File(filePath)
|
||||
m4aFile.delete()
|
||||
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
|
||||
save(filePath.substringBeforeLast('.') + ".tagged.mp3")
|
||||
|
||||
val oldFile = File(filePath)
|
||||
oldFile.delete()
|
||||
|
||||
val newFile = File((filePath.substringBeforeLast('.') + ".tagged.mp3"))
|
||||
newFile.renameTo(File(filePath.substringBeforeLast('.') + ".mp3"))
|
||||
}
|
@ -20,9 +20,6 @@ import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual data class Picture(
|
||||
var image: ImageBitmap?
|
||||
)
|
||||
fun getMemoryEfficientBitmap(
|
||||
input: ByteArray,
|
||||
reqWidth: Int,
|
@ -0,0 +1,7 @@
|
||||
package com.shabinder.common.core_components.picture
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual data class Picture(
|
||||
var image: ImageBitmap?
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
package com.shabinder.common.core_components
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.shabinder.common.core_components.analytics.analyticsModule
|
||||
import com.shabinder.common.core_components.file_manager.fileManagerModule
|
||||
import com.shabinder.common.core_components.media_converter.mediaConverterModule
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.core_components.utils.createHttpClient
|
||||
import com.shabinder.common.database.getLogger
|
||||
import org.koin.dsl.module
|
||||
|
||||
fun coreComponentModules(enableLogging: Boolean) = listOf(
|
||||
commonModule(enableLogging),
|
||||
analyticsModule(),
|
||||
fileManagerModule(),
|
||||
mediaConverterModule()
|
||||
)
|
||||
|
||||
private fun commonModule(enableLogging: Boolean) = module {
|
||||
single { createHttpClient(enableLogging) }
|
||||
single { Settings() }
|
||||
single { Kermit(getLogger()) }
|
||||
single { PreferenceManager(get()) }
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
import org.koin.core.module.Module
|
||||
|
||||
@ -27,4 +27,4 @@ object COUNTLY_CONFIG {
|
||||
const val SERVER_URL = "https://counlty.shabinder.in"
|
||||
}
|
||||
|
||||
expect fun analyticsModule(): Module
|
||||
internal expect fun analyticsModule(): Module
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
sealed class AnalyticsEvent(private val eventName: String, private val extras: Map<String, Any> = emptyMap()): AnalyticsManager.Companion.AnalyticsAction() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
import com.shabinder.common.di.analytics.AnalyticsManager.Companion.AnalyticsAction
|
||||
import com.shabinder.common.core_components.analytics.AnalyticsManager.Companion.AnalyticsAction
|
||||
|
||||
sealed class AnalyticsView(private val viewName: String, private val extras: Map<String, Any> = emptyMap()) : AnalyticsAction() {
|
||||
override fun track(analyticsManager: AnalyticsManager) = analyticsManager.sendView(viewName,extras)
|
@ -14,44 +14,64 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components.file_manager
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||
import com.shabinder.common.di.preference.PreferenceManager
|
||||
import com.shabinder.common.di.utils.removeIllegalChars
|
||||
import com.shabinder.common.core_components.media_converter.MediaConverter
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.core_components.utils.createHttpClient
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.koin.core.module.Module
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
expect class Dir(
|
||||
logger: Kermit,
|
||||
preferenceManager: PreferenceManager,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) {
|
||||
internal expect fun fileManagerModule(): Module
|
||||
|
||||
interface FileManager {
|
||||
|
||||
val logger: Kermit
|
||||
val preferenceManager: PreferenceManager
|
||||
val mediaConverter: MediaConverter
|
||||
val db: Database?
|
||||
|
||||
fun isPresent(path: String): Boolean
|
||||
|
||||
fun fileSeparator(): String
|
||||
|
||||
fun defaultDir(): String
|
||||
|
||||
fun imageCacheDir(): String
|
||||
|
||||
fun createDirectory(dirPath: String)
|
||||
|
||||
suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage
|
||||
|
||||
suspend fun loadImage(url: String, reqWidth: Int = 150, reqHeight: Int = 150): Picture
|
||||
|
||||
suspend fun clearCache()
|
||||
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit = {})
|
||||
|
||||
suspend fun saveFileWithMetadata(
|
||||
mp3ByteArray: ByteArray,
|
||||
trackDetails: TrackDetails,
|
||||
postProcess: (track: TrackDetails) -> Unit = {}
|
||||
): SuspendableEvent<String, Throwable>
|
||||
|
||||
fun addToLibrary(path: String)
|
||||
}
|
||||
|
||||
/*
|
||||
* Call this function at startup!
|
||||
* */
|
||||
fun Dir.createDirectories() {
|
||||
fun FileManager.createDirectories() {
|
||||
try {
|
||||
createDirectory(defaultDir())
|
||||
createDirectory(imageCacheDir())
|
||||
@ -59,13 +79,21 @@ fun Dir.createDirectories() {
|
||||
createDirectory(defaultDir() + "Albums/")
|
||||
createDirectory(defaultDir() + "Playlists/")
|
||||
createDirectory(defaultDir() + "YT_Downloads/")
|
||||
} catch (e: Exception) {}
|
||||
} catch (ignored: Exception) {}
|
||||
}
|
||||
|
||||
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
||||
fun FileManager.finalOutputDir(
|
||||
itemName: String,
|
||||
type: String,
|
||||
subFolder: String,
|
||||
defaultDir: String,
|
||||
extension: String = ".mp3"
|
||||
): String =
|
||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
||||
removeIllegalChars(itemName) + extension
|
||||
if (subFolder.isEmpty()) "" else {
|
||||
removeIllegalChars(subFolder) + this.fileSeparator()
|
||||
} +
|
||||
removeIllegalChars(itemName) + extension
|
||||
/*DIR Specific Operation End*/
|
||||
|
||||
fun getNameURL(url: String): String {
|
@ -0,0 +1,28 @@
|
||||
package com.shabinder.common.core_components.media_converter
|
||||
|
||||
import com.shabinder.common.core_components.parallel_executor.ParallelExecutor
|
||||
import com.shabinder.common.core_components.parallel_executor.ParallelProcessor
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.dispatcherDefault
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import org.koin.core.module.Module
|
||||
|
||||
abstract class MediaConverter : ParallelProcessor {
|
||||
|
||||
/*
|
||||
* Operations Pool
|
||||
* */
|
||||
override val parallelExecutor = ParallelExecutor(dispatcherDefault)
|
||||
|
||||
/*
|
||||
* By Default AudioQuality Output will be equal to Input's Quality,i.e, Denoted by AudioQuality.UNKNOWN
|
||||
* */
|
||||
abstract suspend fun convertAudioFile(
|
||||
inputFilePath: String,
|
||||
outputFilePath: String,
|
||||
audioQuality: AudioQuality = AudioQuality.UNKNOWN,
|
||||
progressCallbacks: (Long) -> Unit = {},
|
||||
): SuspendableEvent<String, Throwable>
|
||||
}
|
||||
|
||||
internal expect fun mediaConverterModule(): Module
|
@ -14,40 +14,62 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di.utils
|
||||
package com.shabinder.common.core_components.parallel_executor
|
||||
|
||||
// Dependencies:
|
||||
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9-native-mt")
|
||||
// implementation("org.jetbrains.kotlinx:atomicfu:0.14.4")
|
||||
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
|
||||
|
||||
import com.shabinder.common.di.dispatcherIO
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ParallelExecutor(
|
||||
parentContext: CoroutineContext = dispatcherIO,
|
||||
) : Closeable {
|
||||
interface ParallelProcessor {
|
||||
|
||||
private val concurrentOperationLimit = atomic(4)
|
||||
private val coroutineContext = parentContext + Job()
|
||||
val parallelExecutor: ParallelExecutor
|
||||
|
||||
suspend fun <T> executeSafelyInPool(block: suspend () -> T): SuspendableEvent<T, Throwable> {
|
||||
return SuspendableEvent {
|
||||
parallelExecutor.executeSuspending(block)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> executeSafelyInPool(
|
||||
onComplete: suspend (result: SuspendableEvent<T, Throwable>) -> Unit = {},
|
||||
block: suspend () -> T
|
||||
): SuspendableEvent<T, Throwable> {
|
||||
return SuspendableEvent {
|
||||
parallelExecutor.executeSuspending(block)
|
||||
}.also { onComplete(it) }
|
||||
}
|
||||
|
||||
suspend fun stopAllTasks() {
|
||||
parallelExecutor.closeAndReInit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ParallelExecutor(
|
||||
private val context: CoroutineContext = dispatcherIO,
|
||||
concurrentOperationLimit: Int = 4
|
||||
) : Closeable, CoroutineScope {
|
||||
|
||||
private var service: Job = SupervisorJob()
|
||||
override val coroutineContext get() = context + service
|
||||
private var isClosed = atomic(false)
|
||||
private val killQueue = Channel<Unit>(Channel.UNLIMITED)
|
||||
private val operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
|
||||
private var killQueue = Channel<Unit>(Channel.UNLIMITED)
|
||||
private var operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
|
||||
private var concurrentOperationLimit = atomic(concurrentOperationLimit)
|
||||
|
||||
|
||||
init {
|
||||
startOrStopProcessors(expectedCount = concurrentOperationLimit.value, actualCount = 0)
|
||||
startOrStopProcessors(expectedCount = this.concurrentOperationLimit.value, actualCount = 0)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
@ -58,9 +80,22 @@ class ParallelExecutor(
|
||||
|
||||
killQueue.close(cause)
|
||||
operationQueue.close(cause)
|
||||
service.cancel(cause)
|
||||
coroutineContext.cancel(cause)
|
||||
}
|
||||
|
||||
fun closeAndReInit(newConcurrentOperationLimit: Int = 4) {
|
||||
// Close Everything
|
||||
close()
|
||||
|
||||
// ReInit everything
|
||||
service = SupervisorJob()
|
||||
isClosed = atomic(false)
|
||||
killQueue = Channel(Channel.UNLIMITED)
|
||||
operationQueue = Channel(Channel.RENDEZVOUS)
|
||||
concurrentOperationLimit = atomic(newConcurrentOperationLimit)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchProcessor() = launch {
|
||||
while (true) {
|
||||
val operation = select<Operation<*>?> {
|
||||
@ -72,7 +107,7 @@ class ParallelExecutor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <Result> execute(block: suspend () -> Result): Result =
|
||||
suspend fun <Result> executeSuspending(block: suspend () -> Result): Result =
|
||||
withContext(coroutineContext) {
|
||||
val operation = Operation(block)
|
||||
operationQueue.send(operation)
|
||||
@ -80,6 +115,15 @@ class ParallelExecutor(
|
||||
operation.result.await()
|
||||
}
|
||||
|
||||
fun <Result> execute(onComplete: (Result) -> Unit = {}, block: suspend () -> Result) {
|
||||
launch(coroutineContext) {
|
||||
val operation = Operation(block)
|
||||
operationQueue.send(operation)
|
||||
|
||||
onComplete(operation.result.await())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This launches all coroutines in advance even if they're never needed. Find a lazy way to do this.
|
||||
fun setConcurrentOperationLimit(limit: Int) {
|
||||
require(limit >= 1) { "'limit' must be greater than zero: $limit" }
|
||||
@ -89,6 +133,7 @@ class ParallelExecutor(
|
||||
}
|
||||
|
||||
private fun startOrStopProcessors(expectedCount: Int, actualCount: Int) {
|
||||
if (!service.isActive) service = SupervisorJob()
|
||||
if (expectedCount == actualCount)
|
||||
return
|
||||
|
||||
@ -100,9 +145,7 @@ class ParallelExecutor(
|
||||
change -= 1
|
||||
|
||||
if (change > 0)
|
||||
with(CoroutineScope(coroutineContext)) {
|
||||
repeat(change) { launchProcessor() }
|
||||
}
|
||||
repeat(change) { launchProcessor() }
|
||||
else
|
||||
repeat(-change) { killQueue.trySend(Unit).isSuccess }
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package com.shabinder.common.core_components.picture
|
||||
|
||||
expect class Picture
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.preference
|
||||
package com.shabinder.common.core_components.preference_manager
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.shabinder.common.models.AudioQuality
|
@ -0,0 +1,57 @@
|
||||
package com.shabinder.common.core_components.utils
|
||||
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import com.shabinder.common.utils.globalJson
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.features.json.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import io.ktor.client.features.logging.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.native.concurrent.SharedImmutable
|
||||
|
||||
suspend fun isInternetAccessible(): Boolean {
|
||||
return withContext(dispatcherIO) {
|
||||
try {
|
||||
ktorHttpClient.head<String>("https://open.spotify.com/")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
|
||||
// https://github.com/Kotlin/kotlinx.serialization/issues/1450
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(globalJson)
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
socketTimeoutMillis = 520_000
|
||||
requestTimeoutMillis = 360_000
|
||||
connectTimeoutMillis = 360_000
|
||||
}
|
||||
// WorkAround for Freezing
|
||||
// Use httpClient.getData / httpClient.postData Extensions
|
||||
/*install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
)
|
||||
}*/
|
||||
if (enableNetworkLogs) {
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*Client Active Throughout App's Lifetime*/
|
||||
@SharedImmutable
|
||||
val ktorHttpClient = HttpClient {}
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.utils
|
||||
package com.shabinder.common.core_components.utils
|
||||
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.decompose.value.ValueObserver
|
@ -1,6 +1,6 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
import com.shabinder.common.di.Dir
|
||||
import com.shabinder.common.core_components.file_manager.FileManager
|
||||
import ly.count.sdk.java.Config
|
||||
import ly.count.sdk.java.Config.DeviceIdStrategy
|
||||
import ly.count.sdk.java.Config.Feature
|
||||
@ -11,7 +11,7 @@ import org.koin.dsl.module
|
||||
import java.io.File
|
||||
|
||||
internal class DesktopAnalyticsManager(
|
||||
private val dir: Dir
|
||||
private val fileManager: FileManager
|
||||
) : AnalyticsManager {
|
||||
|
||||
init {
|
||||
@ -28,7 +28,7 @@ internal class DesktopAnalyticsManager(
|
||||
setRequiresConsent(true)
|
||||
}
|
||||
|
||||
Countly.init(File(dir.defaultDir()), config)
|
||||
Countly.init(File(fileManager.defaultDir()), config)
|
||||
|
||||
Countly.session().begin();
|
||||
}
|
@ -14,21 +14,33 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components.file_manager
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.mpatric.mp3agic.InvalidDataException
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.common.core_components.media_converter.MediaConverter
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.core_components.removeAllTags
|
||||
import com.shabinder.common.core_components.setId3v1Tags
|
||||
import com.shabinder.common.core_components.setId3v2TagsAndSaveFile
|
||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||
import com.shabinder.common.di.preference.PreferenceManager
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.dispatcherIO
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.models.event.coroutines.failure
|
||||
import com.shabinder.common.models.event.coroutines.map
|
||||
import com.shabinder.database.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.skija.Image
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
@ -38,33 +50,40 @@ import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val preferenceManager: PreferenceManager,
|
||||
actual internal fun fileManagerModule() = module {
|
||||
single { DesktopFileManager(get(), get(), get(), get()) } bind FileManager::class
|
||||
}
|
||||
|
||||
class DesktopFileManager(
|
||||
override val logger: Kermit,
|
||||
override val preferenceManager: PreferenceManager,
|
||||
override val mediaConverter: MediaConverter,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) {
|
||||
) : FileManager {
|
||||
|
||||
init {
|
||||
createDirectories()
|
||||
}
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
||||
override fun fileSeparator(): String = File.separator
|
||||
|
||||
actual fun imageCacheDir(): String = System.getProperty("user.home") +
|
||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||
override fun imageCacheDir(): String = System.getProperty("user.home") +
|
||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||
|
||||
private val defaultBaseDir = System.getProperty("user.home")
|
||||
|
||||
actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
override fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
override fun isPresent(path: String): Boolean = File(path).exists()
|
||||
|
||||
actual fun createDirectory(dirPath: String) {
|
||||
override fun createDirectory(dirPath: String) {
|
||||
val yourAppDir = File(dirPath)
|
||||
|
||||
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
||||
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
||||
if (yourAppDir.mkdirs()) {
|
||||
logger.i { "$dirPath created" }
|
||||
} else {
|
||||
logger.e { "Unable to create Dir: $dirPath!" }
|
||||
}
|
||||
} else {
|
||||
@ -72,11 +91,11 @@ actual class Dir actual constructor(
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun clearCache() {
|
||||
override suspend fun clearCache() {
|
||||
File(imageCacheDir()).deleteRecursively()
|
||||
}
|
||||
|
||||
actual suspend fun cacheImage(image: Any, path: String) {
|
||||
override suspend fun cacheImage(image: Any, path: String) {
|
||||
try {
|
||||
(image as? BufferedImage)?.let {
|
||||
ImageIO.write(it, "jpeg", File(path))
|
||||
@ -87,11 +106,11 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
actual suspend fun saveFileWithMetadata(
|
||||
override suspend fun saveFileWithMetadata(
|
||||
mp3ByteArray: ByteArray,
|
||||
trackDetails: TrackDetails,
|
||||
postProcess: (track: TrackDetails) -> Unit
|
||||
) {
|
||||
) = withContext(dispatcherIO) {
|
||||
val songFile = File(trackDetails.outputFilePath)
|
||||
try {
|
||||
/*
|
||||
@ -103,61 +122,46 @@ actual class Dir actual constructor(
|
||||
}
|
||||
|
||||
if (mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
|
||||
try {
|
||||
// Add Mp3 Tags and Add to Library
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
// Media File Isn't MP3 lets Convert It first
|
||||
if (e is InvalidDataException) {
|
||||
val convertedFilePath = songFile.absolutePath.substringBeforeLast('.') + ".temp.mp3"
|
||||
|
||||
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||
".mp3" -> {
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
}
|
||||
".m4a" -> {
|
||||
/*FFmpeg.executeAsync(
|
||||
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
||||
){ _, returnCode ->
|
||||
when (returnCode) {
|
||||
Config.RETURN_CODE_SUCCESS -> {
|
||||
//FFMPEG task Completed
|
||||
logger.d{ "Async command execution completed successfully." }
|
||||
scope.launch {
|
||||
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
||||
}
|
||||
}
|
||||
Config.RETURN_CODE_CANCEL -> {
|
||||
logger.d{"Async command execution cancelled by user."}
|
||||
}
|
||||
else -> {
|
||||
logger.d { "Async command execution failed with rc=$returnCode" }
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
Mp3File(File(songFile.absolutePath))
|
||||
val conversionResult = mediaConverter.convertAudioFile(
|
||||
inputFilePath = songFile.absolutePath,
|
||||
outputFilePath = convertedFilePath,
|
||||
)
|
||||
|
||||
conversionResult.map { outputFilePath ->
|
||||
Mp3File(File(outputFilePath))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(songFile.absolutePath)
|
||||
} catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
|
||||
.setId3v2TagsAndSaveFile(trackDetails, trackDetails.outputFilePath)
|
||||
|
||||
addToLibrary(trackDetails.outputFilePath)
|
||||
}.failure {
|
||||
throw it
|
||||
}
|
||||
} else throw e
|
||||
}
|
||||
SuspendableEvent.success(trackDetails.outputFilePath)
|
||||
} catch (e: Throwable) {
|
||||
if (songFile.exists()) songFile.delete()
|
||||
logger.e { "${songFile.absolutePath} could not be created" }
|
||||
SuspendableEvent.error(e)
|
||||
}
|
||||
}
|
||||
actual fun addToLibrary(path: String) {}
|
||||
|
||||
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||
override fun addToLibrary(path: String) {}
|
||||
|
||||
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||
val cachePath = imageCacheDir() + getNameURL(url)
|
||||
var picture: ImageBitmap? = loadCachedImage(cachePath, reqWidth, reqHeight)
|
||||
if (picture == null) picture = freshImage(url, reqWidth, reqHeight)
|
||||
@ -198,7 +202,7 @@ actual class Dir actual constructor(
|
||||
}
|
||||
}
|
||||
|
||||
actual val db: Database? = spotiFlyerDatabase.instance
|
||||
override val db: Database? = spotiFlyerDatabase.instance
|
||||
}
|
||||
|
||||
fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(
|
@ -0,0 +1,39 @@
|
||||
package com.shabinder.common.core_components.media_converter
|
||||
|
||||
import com.github.kokorin.jaffree.ffmpeg.FFmpeg
|
||||
import com.github.kokorin.jaffree.ffmpeg.UrlInput
|
||||
import com.github.kokorin.jaffree.ffmpeg.UrlOutput
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
class DesktopMediaConverter : MediaConverter() {
|
||||
|
||||
override suspend fun convertAudioFile(
|
||||
inputFilePath: String,
|
||||
outputFilePath: String,
|
||||
audioQuality: AudioQuality,
|
||||
progressCallbacks: (Long) -> Unit,
|
||||
) = executeSafelyInPool {
|
||||
FFmpeg.atPath().run {
|
||||
addInput(UrlInput.fromUrl(inputFilePath))
|
||||
setOverwriteOutput(true)
|
||||
if (audioQuality != AudioQuality.UNKNOWN) {
|
||||
addArguments("-b:a", "${audioQuality.kbps}k")
|
||||
}
|
||||
addArguments("-acodec", "libmp3lame")
|
||||
addArgument("-vn")
|
||||
addOutput(UrlOutput.toUrl(outputFilePath))
|
||||
setProgressListener {
|
||||
progressCallbacks(it.timeMillis)
|
||||
}
|
||||
execute()
|
||||
|
||||
return@run outputFilePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun mediaConverterModule() = module {
|
||||
single { DesktopMediaConverter() } bind MediaConverter::class
|
||||
}
|
@ -14,11 +14,12 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components
|
||||
|
||||
import com.mpatric.mp3agic.ID3v1Tag
|
||||
import com.mpatric.mp3agic.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.common.core_components.file_manager.downloadFile
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@ -47,16 +48,22 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, outputFilePath: String? = null) {
|
||||
val id3v2Tag = ID3v24Tag().apply {
|
||||
artist = track.artists.joinToString(",")
|
||||
albumArtist = track.albumArtists.joinToString(", ")
|
||||
artist = track.artists.joinToString(", ")
|
||||
title = track.title
|
||||
album = track.albumName
|
||||
year = track.year
|
||||
comment = "Genres:${track.comment}"
|
||||
lyrics = "Gonna Implement Soon"
|
||||
|
||||
genreDescription = "Genre: " + track.genre.joinToString(", ")
|
||||
comment = track.comment
|
||||
lyrics = track.lyrics ?: ""
|
||||
url = track.trackUrl
|
||||
if (track.trackNumber != null)
|
||||
this.track = track.trackNumber.toString()
|
||||
}
|
||||
try {
|
||||
val art = File(track.albumArtPath)
|
||||
@ -66,7 +73,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
fis.close()
|
||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||
this.id3v2Tag = id3v2Tag
|
||||
saveFile(track.outputFilePath)
|
||||
saveFile(outputFilePath ?: track.outputFilePath)
|
||||
} catch (e: java.io.FileNotFoundException) {
|
||||
try {
|
||||
// Image Still Not Downloaded!
|
||||
@ -77,21 +84,23 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||
is DownloadResult.Success -> {
|
||||
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||
this.id3v2Tag = id3v2Tag
|
||||
saveFile(track.outputFilePath)
|
||||
saveFile(outputFilePath ?: track.outputFilePath)
|
||||
}
|
||||
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Mp3File.saveFile(filePath: String) {
|
||||
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||
val m4aFile = File(filePath)
|
||||
m4aFile.delete()
|
||||
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
|
||||
save(filePath.substringBeforeLast('.') + ".tagged.mp3")
|
||||
|
||||
val oldFile = File(filePath)
|
||||
oldFile.delete()
|
||||
|
||||
val newFile = File((filePath.substringBeforeLast('.') + ".tagged.mp3"))
|
||||
newFile.renameTo(File(filePath.substringBeforeLast('.') + ".mp3"))
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.shabinder.common.core_components.picture
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual data class Picture(
|
||||
var image: ImageBitmap?
|
||||
)
|
@ -17,7 +17,7 @@
|
||||
@file:JsModule("file-saver")
|
||||
@file:JsNonModule
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components
|
||||
|
||||
import org.w3c.files.Blob
|
||||
|
@ -14,7 +14,7 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
package com.shabinder.common.core_components
|
||||
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.files.Blob
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.analytics
|
||||
package com.shabinder.common.core_components.analytics
|
||||
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.core_components.file_manager
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.core_components.DownloadProgressFlow
|
||||
import com.shabinder.common.core_components.ID3Writer
|
||||
import com.shabinder.common.core_components.allTracksStatus
|
||||
import com.shabinder.common.core_components.media_converter.MediaConverter
|
||||
import com.shabinder.common.core_components.picture.Picture
|
||||
import com.shabinder.common.core_components.preference_manager.PreferenceManager
|
||||
import com.shabinder.common.core_components.saveAs
|
||||
import com.shabinder.common.database.SpotiFlyerDatabase
|
||||
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.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
import com.shabinder.database.Database
|
||||
import kotlinext.js.Object
|
||||
import kotlinext.js.js
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.khronos.webgl.Int8Array
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import org.w3c.dom.ImageBitmap
|
||||
|
||||
|
||||
internal actual fun fileManagerModule() = module {
|
||||
single { WebFileManager(get(), get(), get(), get()) } bind FileManager::class
|
||||
}
|
||||
|
||||
class WebFileManager(
|
||||
override val logger: Kermit,
|
||||
override val preferenceManager: PreferenceManager,
|
||||
override val mediaConverter: MediaConverter,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) : FileManager {
|
||||
/*init {
|
||||
createDirectories()
|
||||
}*/
|
||||
|
||||
/*
|
||||
* TODO
|
||||
* */
|
||||
override fun fileSeparator(): String = "/"
|
||||
|
||||
override fun imageCacheDir(): String = "TODO" +
|
||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||
|
||||
override fun defaultDir(): String = "TODO" + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
|
||||
override fun isPresent(path: String): Boolean = false
|
||||
|
||||
override fun createDirectory(dirPath: String) {}
|
||||
|
||||
override suspend fun clearCache() {}
|
||||
|
||||
override suspend fun cacheImage(image: Any, path: String) {}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun saveFileWithMetadata(
|
||||
mp3ByteArray: ByteArray,
|
||||
trackDetails: TrackDetails,
|
||||
postProcess: (track: TrackDetails) -> Unit
|
||||
): SuspendableEvent<String, Throwable> {
|
||||
return SuspendableEvent {
|
||||
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
|
||||
val albumArt = downloadFile(corsApi + trackDetails.albumArtURL)
|
||||
albumArt.collect {
|
||||
when (it) {
|
||||
is DownloadResult.Success -> {
|
||||
logger.d { "Album Art Downloaded Success" }
|
||||
val albumArtObj = js {
|
||||
this["type"] = 3
|
||||
this["data"] = it.byteArray.toArrayBuffer()
|
||||
this["description"] = "Cover Art"
|
||||
}
|
||||
writeTagsAndSave(writer, albumArtObj as Object, trackDetails)
|
||||
}
|
||||
is DownloadResult.Error -> {
|
||||
logger.d { "Album Art Downloading Error" }
|
||||
writeTagsAndSave(writer, null, trackDetails)
|
||||
}
|
||||
is DownloadResult.Progress -> logger.d { "Album Art Downloading: ${it.progress}" }
|
||||
}
|
||||
}
|
||||
trackDetails.outputFilePath
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeTagsAndSave(writer: ID3Writer, albumArt: Object?, trackDetails: TrackDetails) {
|
||||
writer.apply {
|
||||
setFrame("TIT2", trackDetails.title)
|
||||
setFrame("TPE1", trackDetails.artists.toTypedArray())
|
||||
setFrame("TALB", trackDetails.albumName ?: "")
|
||||
try {
|
||||
trackDetails.year?.substring(0, 4)?.toInt()?.let { setFrame("TYER", it) }
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
setFrame("TPE2", trackDetails.artists.joinToString(","))
|
||||
setFrame("WOAS", trackDetails.source.toString())
|
||||
setFrame("TLEN", trackDetails.durationSec)
|
||||
albumArt?.let { setFrame("APIC", it) }
|
||||
}
|
||||
writer.addTag()
|
||||
allTracksStatus[trackDetails.title] = DownloadStatus.Downloaded
|
||||
DownloadProgressFlow.emit(allTracksStatus)
|
||||
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
|
||||
}
|
||||
|
||||
override fun addToLibrary(path: String) {}
|
||||
|
||||
override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||
return Picture(url)
|
||||
}
|
||||
|
||||
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
||||
|
||||
private suspend fun freshImage(url: String): ImageBitmap? = null
|
||||
|
||||
override val db: Database? = spotiFlyerDatabase.instance
|
||||
}
|
||||
|
||||
fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
||||
return this.unsafeCast<Int8Array>().buffer
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.shabinder.common.core_components.media_converter
|
||||
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.event.Event
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
class WebMediaConverter: MediaConverter() {
|
||||
override suspend fun convertAudioFile(
|
||||
inputFilePath: String,
|
||||
outputFilePath: String,
|
||||
audioQuality: AudioQuality,
|
||||
progressCallbacks: (Long) -> Unit
|
||||
): SuspendableEvent<String, Throwable> {
|
||||
// TODO("Not yet implemented")
|
||||
return SuspendableEvent.error(NotImplementedError())
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun mediaConverterModule() = module {
|
||||
single { WebMediaConverter() } bind MediaConverter::class
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.shabinder.common.core_components.picture
|
||||
|
||||
actual data class Picture(
|
||||
var imageUrl: String
|
||||
)
|
@ -57,8 +57,5 @@ kotlin {
|
||||
api(Internationalization.dep)
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.shabinder.common.models
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
// IO-Dispatcher
|
||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
@ -1,3 +0,0 @@
|
||||
package com.shabinder.common
|
||||
|
||||
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)
|
@ -5,7 +5,8 @@ enum class AudioQuality(val kbps: String) {
|
||||
KBPS160("160"),
|
||||
KBPS192("192"),
|
||||
KBPS256("256"),
|
||||
KBPS320("320");
|
||||
KBPS320("320"),
|
||||
UNKNOWN("-1");
|
||||
|
||||
companion object {
|
||||
fun getQuality(kbps: String): AudioQuality {
|
||||
@ -15,6 +16,7 @@ enum class AudioQuality(val kbps: String) {
|
||||
"192" -> KBPS192
|
||||
"256" -> KBPS256
|
||||
"320" -> KBPS320
|
||||
"-1" -> UNKNOWN
|
||||
else -> KBPS160 // Use 160 as baseline
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package com.shabinder.common.models
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
// IO-Dispatcher
|
||||
expect val dispatcherIO: CoroutineDispatcher
|
||||
|
||||
// Default-Dispatcher
|
||||
val dispatcherDefault: CoroutineDispatcher = Dispatchers.Default
|
@ -43,7 +43,9 @@ data class TrackDetails(
|
||||
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||
var outputFilePath: String, // UriString in Android
|
||||
var videoID: String? = null,
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
val outputMp3Path get() = outputFilePath.substringBeforeLast(".") + ".mp3"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class DownloadStatus : Parcelable {
|
||||
|
@ -9,7 +9,7 @@ sealed class SpotiFlyerException(override val message: String) : Exception(messa
|
||||
|
||||
data class MP3ConversionFailed(
|
||||
val extraInfo: String? = null,
|
||||
override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo"
|
||||
override val message: String = /*${Strings.mp3ConverterBusy()} */"CAUSE:$extraInfo"
|
||||
) : SpotiFlyerException(message)
|
||||
|
||||
data class UnknownReason(
|
||||
@ -28,13 +28,17 @@ sealed class SpotiFlyerException(override val message: String) : Exception(messa
|
||||
) : SpotiFlyerException(message)
|
||||
|
||||
data class DownloadLinkFetchFailed(
|
||||
val trackName: String,
|
||||
val jioSaavnError: Throwable,
|
||||
val ytMusicError: Throwable,
|
||||
override val message: String = "${Strings.noLinkFound()}: $trackName," +
|
||||
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
|
||||
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
|
||||
) : SpotiFlyerException(message)
|
||||
val errorTrace: String
|
||||
) : SpotiFlyerException(errorTrace) {
|
||||
constructor(
|
||||
trackName: String,
|
||||
jioSaavnError: Throwable,
|
||||
ytMusicError: Throwable,
|
||||
errorTrace: String = "${Strings.noLinkFound()}: $trackName," +
|
||||
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
|
||||
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
|
||||
): this(errorTrace)
|
||||
}
|
||||
|
||||
data class LinkInvalid(
|
||||
val link: String? = null,
|
||||
|
@ -155,6 +155,8 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable> : ReadOnlyPropert
|
||||
// Factory methods
|
||||
fun <E : Throwable> error(ex: E) = Failure<Nothing, E>(ex)
|
||||
|
||||
fun <V : Any> success(res: V) = Success<V, Throwable>(res)
|
||||
|
||||
inline fun <V : Any?> of(value: V?, crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent<V, Throwable> {
|
||||
return value?.let { Success<V, Nothing>(it) } ?: error(fail())
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package com.shabinder.common.utils
|
||||
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)
|
||||
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun buildString(track: TrackDetails, builderAction: StringBuilder.() -> Unit): String {
|
||||
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
|
||||
return StringBuilder().run {
|
||||
appendLine("Find Link for ${track.title} ${if (!track.videoID.isNullOrBlank()) "-> VideoID:" + track.videoID else ""}")
|
||||
apply(builderAction)
|
||||
}.toString()
|
||||
}
|
||||
|
||||
fun StringBuilder.appendPadded(data: Any?) {
|
||||
appendLine().append(data).appendLine()
|
||||
}
|
||||
|
||||
fun StringBuilder.appendPadded(header: Any?, data: Any?) {
|
||||
appendLine().append(header).appendLine(data).appendLine()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common.di.utils
|
||||
package com.shabinder.common.utils
|
||||
|
||||
/*
|
||||
* JSON UTILS
|
@ -1,20 +1,4 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di.utils
|
||||
package com.shabinder.common.utils
|
||||
|
||||
import io.github.shabinder.TargetPlatforms
|
||||
import io.github.shabinder.activePlatform
|
||||
@ -22,7 +6,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
|
||||
@ThreadLocal
|
||||
val json by lazy {
|
||||
val globalJson by lazy {
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
@ -0,0 +1,7 @@
|
||||
package com.shabinder.common.models
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
// IO-Dispatcher
|
||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
@ -0,0 +1,6 @@
|
||||
package com.shabinder.common.models
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
@ -1,26 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="40dp"
|
||||
android:height="38dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#516AEC" android:pathData="m296,288 l60,-60c7.73,-7.73 17.86,-11.6 28,-11.6 10.055,0 20.101,3.806 27.806,11.407 15.612,15.402 15.207,41.18 -0.3,56.687l-134.293,134.293c-11.716,11.716 -30.711,11.716 -42.426,0l-134.787,-134.787c-7.73,-7.73 -11.6,-17.86 -11.6,-28 0,-10.055 3.806,-20.101 11.407,-27.806 15.402,-15.612 41.18,-15.207 56.687,0.3l59.506,59.506v-232c0,-22.091 17.909,-40 40,-40 22.091,0 40,17.909 40,40z"/>
|
||||
<path android:fillColor="#EC7EBA" android:pathData="m411.51,284.49 l-134.3,134.3c-11.71,11.71 -30.71,11.71 -42.42,0l-12.74,-12.74c10.69,4.06 23.23,1.77 31.84,-6.84l134.29,-134.29c12.51,-12.51 15.19,-31.7 7.57,-46.74 5.86,1.81 11.39,5.03 16.06,9.63 15.61,15.4 15.2,41.18 -0.3,56.68z"/>
|
||||
<path android:fillColor="#EC7EBA" android:pathData="m251.88,27.72c-3.46,-3.46 -7.55,-6.29 -12.08,-8.3 4.95,-2.2 10.43,-3.42 16.2,-3.42 11.04,0 21.04,4.48 28.28,11.72s11.72,17.24 11.72,28.28v232l-15.329,15.329c-6.3,6.3 -17.071,1.838 -17.071,-7.071v-240.258c0,-11.04 -4.48,-21.04 -11.72,-28.28z"/>
|
||||
<path android:fillColor="#6A82FB" android:pathData="m496,512h-24c-8.836,0 -16,-7.164 -16,-16s7.164,-16 16,-16h24c8.836,0 16,7.164 16,16s-7.164,16 -16,16z"/>
|
||||
<path android:fillColor="#6A82FB" android:pathData="m40,512h-24c-8.836,0 -16,-7.164 -16,-16s7.164,-16 16,-16h24c8.836,0 16,7.164 16,16s-7.164,16 -16,16z"/>
|
||||
<path android:fillColor="#FC5C7D" android:pathData="m416,512h-320c-8.836,0 -16,-7.164 -16,-16s7.164,-16 16,-16h320c8.836,0 16,7.164 16,16s-7.164,16 -16,16z"/>
|
||||
<path android:fillColor="#4AFC5C7D" android:pathData="m256,443.552c-11.78,0 -23.56,-4.484 -32.527,-13.452l-134.786,-134.787c-10.503,-10.502 -16.287,-24.463 -16.287,-39.313 0,-14.708 5.688,-28.573 16.017,-39.042 10.31,-10.451 24.214,-16.233 39.151,-16.284h0.189c14.966,0 29.552,6.009 40.05,16.507l32.193,32.192v-193.373c0,-30.878 25.122,-56 56,-56s56,25.122 56,56v193.373l32.687,-32.687c10.501,-10.502 24.463,-16.286 39.313,-16.286 14.708,0 28.573,5.688 39.042,16.017 10.45,10.31 16.233,24.214 16.284,39.151 0.051,15.032 -5.966,29.698 -16.507,40.24l-134.292,134.292c-8.967,8.968 -20.747,13.452 -32.527,13.452zM127.761,232.673c-0.028,0 -0.056,0 -0.084,0 -6.349,0.021 -12.202,2.421 -16.479,6.758 -4.383,4.443 -6.797,10.327 -6.797,16.569 0,6.302 2.456,12.228 6.914,16.686l134.785,134.787c5.459,5.459 14.341,5.459 19.8,0l134.292,-134.292c4.556,-4.557 7.157,-10.937 7.134,-17.504 -0.021,-6.349 -2.421,-12.202 -6.758,-16.479 -4.443,-4.383 -10.327,-6.797 -16.569,-6.797 -6.302,0 -12.228,2.456 -16.687,6.914l-60,60c-4.575,4.577 -11.456,5.945 -17.437,3.469 -5.977,-2.478 -9.875,-8.313 -9.875,-14.784v-232c0,-13.234 -10.767,-24 -24,-24 -13.234,0 -24,10.766 -24,24v232c0,6.471 -3.898,12.306 -9.877,14.782 -5.978,2.476 -12.861,1.108 -17.437,-3.469l-59.506,-59.505c-4.536,-4.537 -10.882,-7.135 -17.419,-7.135z"/>
|
||||
</vector>
|
@ -1,26 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
|
||||
</vector>
|
@ -1,31 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="40dp" android:height="40dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="m512,256c0,141.387 -114.613,256 -256,256s-256,-114.613 -256,-256 114.613,-256 256,-256 256,114.613 256,256zM512,256">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="512" android:endY="256"
|
||||
android:startX="0" android:startY="256" android:type="linear">
|
||||
<item android:color="#748AFF" android:offset="0"/>
|
||||
<item android:color="#FF3C64" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#000" android:pathData="m256,56c-110.281,0 -200,89.719 -200,200s89.719,200 200,200 200,-89.719 200,-200 -89.719,-200 -200,-200zM256,426c-93.738,0 -170,-76.262 -170,-170s76.262,-170 170,-170 170,76.262 170,170 -76.262,170 -170,170zM256,426"/>
|
||||
<path android:fillColor="#000" android:pathData="m324.18,187.82c-5.859,-5.855 -15.355,-5.855 -21.215,0l-46.965,46.965 -46.965,-46.965c-5.859,-5.855 -15.355,-5.855 -21.215,0 -5.855,5.859 -5.855,15.355 0,21.215l46.965,46.965 -46.965,46.965c-5.855,5.859 -5.855,15.355 0,21.215 2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.605,-4.395l46.969,-46.965 46.965,46.965c2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.609,-4.395 5.855,-5.859 5.855,-15.355 0,-21.215l-46.965,-46.965 46.965,-46.965c5.855,-5.859 5.855,-15.355 0,-21.215zM324.18,187.82"/>
|
||||
</vector>
|
@ -1,32 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector android:height="42dp" android:viewportHeight="200"
|
||||
android:viewportWidth="200" android:width="42dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group>
|
||||
<clip-path android:pathData="M100,100m-100,0a100,100 0,1 1,200 0a100,100 0,1 1,-200 0"/>
|
||||
<path android:fillColor="#E62C28" android:pathData="M202.7,195.2c0,0.3 -0.1,0.8 -0.1,1.1c-1.5,3 -3.7,5.3 -6.9,6.4c-0.2,0 -0.4,0 -0.6,0.1c-0.8,-0.1 -1.6,-0.4 -2.3,-0.4c-61,0 -122.1,0 -183.1,0c-0.7,0 -1.4,0.3 -2.1,0.4c-0.3,0 -0.7,-0.1 -1,-0.1c-3,-1.4 -5.2,-3.6 -6.5,-6.6V6.7c1.3,-3.1 3.5,-5.3 6.6,-6.7C6.8,0 7,0 7.2,0c0.9,0.1 1.7,0.4 2.6,0.4c61,0 122.1,0 183.1,0c0.8,0 1.5,-0.2 2.3,-0.4c0.2,0 0.4,0 0.5,0c3.4,1.2 5.7,3.5 6.9,7c0,0.1 0,0.4 0,0.5c-0.1,0.7 -0.4,1.4 -0.4,2.1c0,61.2 0,122.3 0,183.5C202.3,193.8 202.6,194.5 202.7,195.2zM52.3,178.7c0.9,0 1.6,0 2.3,0c11.3,0 22.6,0.1 33.8,0c2.9,0 6,-0.3 8.8,-0.9c15.9,-3.4 26.8,-12.6 30.8,-28.7c1.8,-7.3 2.8,-14.9 4.1,-22.3c5.5,-31.1 11,-62.2 16.4,-93.2c0.5,-3.1 1.1,-6.1 1.6,-9.4c-1,0 -1.8,0 -2.5,0c-13.4,0 -26.8,-0.1 -40.2,0c-2.9,0 -6,0.3 -8.8,0.9C82,28.6 71.1,38.5 67.6,55.6c-2.1,10 -3.7,20.1 -5.4,30.1c-1.2,6.9 -2.6,13.9 -3.2,20.9c-0.9,10.1 2.7,18.4 11.9,23.6c3.9,2.2 8.2,3.7 12.7,3.9c7.7,0.3 15.4,0.3 23.1,0.4c0.7,0 1.4,0 2.2,0c-0.7,3.6 -1.3,6.8 -1.9,10c-1.6,8.3 -6.1,12.1 -14.5,12.1c-11.3,0 -22.6,0 -33.8,0c-0.8,0 -1.5,0 -2.3,0C54.9,164.1 53.7,171.2 52.3,178.7z"/>
|
||||
<path android:fillColor="#E94845" android:pathData="M195.2,0c-0.8,0.1 -1.5,0.4 -2.3,0.4c-61,0 -122.1,0 -183.1,0C9,0.4 8.3,0.1 7.5,0C70.1,0 132.6,0 195.2,0z"/>
|
||||
<path android:fillColor="#E94845" android:pathData="M202.7,195.2c-0.1,-0.7 -0.4,-1.4 -0.4,-2.1c0,-61.2 0,-122.3 0,-183.5c0,-0.7 0.2,-1.4 0.4,-2.1C202.7,70.1 202.7,132.6 202.7,195.2z"/>
|
||||
<path android:fillColor="#E94845" android:pathData="M7.5,202.7c0.7,-0.1 1.4,-0.4 2.1,-0.4c61,0 122.1,0 183.1,0c0.7,0 1.4,0.3 2.1,0.4C132.4,202.7 69.9,202.7 7.5,202.7z"/>
|
||||
<path android:fillColor="#FDFCFC" android:pathData="M202.7,7.1c-1.2,-3.5 -3.6,-5.9 -7.1,-7.1h7.1V7.1z"/>
|
||||
<path android:fillColor="#FDFCFC" android:pathData="M195.6,202.7c3.4,-1.2 5.7,-3.5 7.1,-6.7v6.7H195.6z"/>
|
||||
<path android:fillColor="#FDFCFC" android:pathData="M0,196c1.3,3.1 3.6,5.3 6.7,6.7H0V196z"/>
|
||||
<path android:fillColor="#FDFCFC" android:pathData="M6.7,0C3.6,1.4 1.3,3.6 0,6.7V0H6.7z"/>
|
||||
<path android:fillColor="#FDFCFC" android:pathData="M52.3,178.7c1.3,-7.4 2.6,-14.6 4,-22c0.8,0 1.6,0 2.3,0c11.3,0 22.6,0 33.8,0c8.4,0 12.9,-3.8 14.5,-12.1c0.6,-3.2 1.2,-6.5 1.9,-10c-0.8,0 -1.5,0 -2.2,0c-7.7,-0.1 -15.4,0 -23.1,-0.4c-4.5,-0.2 -8.8,-1.7 -12.7,-3.9c-9.2,-5.2 -12.8,-13.5 -11.9,-23.6c0.6,-7 2,-13.9 3.2,-20.9c1.7,-10.1 3.4,-20.2 5.4,-30.1C71.1,38.5 82,28.6 98.8,25c2.9,-0.6 5.9,-0.9 8.8,-0.9c13.4,-0.1 26.8,0 40.2,0c0.7,0 1.4,0 2.5,0c-0.6,3.3 -1.1,6.3 -1.6,9.4c-5.5,31.1 -11,62.2 -16.4,93.2c-1.3,7.5 -2.3,15 -4.1,22.3c-4,16.1 -14.9,25.3 -30.8,28.7c-2.9,0.6 -5.9,0.9 -8.8,0.9c-11.3,0.1 -22.6,0 -33.8,0C53.9,178.7 53.2,178.7 52.3,178.7zM112.7,112.4c1.2,-6.8 2.4,-13.4 3.5,-20c2.1,-12.1 4.3,-24.3 6.3,-36.4c0.8,-4.6 -1.4,-8 -5.9,-9.3c-1.3,-0.4 -2.7,-0.5 -4.1,-0.5c-3,-0.1 -5.9,0 -8.9,0c-8.2,0 -13.2,4.4 -14.5,12.4c-0.8,4.9 -1.7,9.7 -2.6,14.6c-1.7,9.6 -3.5,19.2 -5,28.8c-0.9,6 2.1,10 8.2,10.3C97.3,112.6 104.9,112.4 112.7,112.4z"/>
|
||||
<path android:fillColor="#E62D29" android:pathData="M112.7,112.4c-7.8,0 -15.3,0.3 -22.9,-0.1c-6.1,-0.3 -9.2,-4.3 -8.2,-10.3c1.5,-9.6 3.3,-19.2 5,-28.8c0.8,-4.9 1.8,-9.7 2.6,-14.6c1.3,-8 6.3,-12.4 14.5,-12.4c3,0 5.9,-0.1 8.9,0c1.4,0 2.8,0.2 4.1,0.5c4.5,1.3 6.7,4.7 5.9,9.3c-2.1,12.1 -4.2,24.3 -6.3,36.4C115,98.9 113.9,105.5 112.7,112.4z"/>
|
||||
</group>
|
||||
</vector>
|
@ -1,21 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="34dp"
|
||||
android:height="34dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#4EA4FF" android:pathData="M255.968,5.329C114.624,5.329 0,120.401 0,262.353c0,113.536 73.344,209.856 175.104,243.872c12.8,2.368 17.472,-5.568 17.472,-12.384c0,-6.112 -0.224,-22.272 -0.352,-43.712c-71.2,15.52 -86.24,-34.464 -86.24,-34.464c-11.616,-29.696 -28.416,-37.6 -28.416,-37.6c-23.264,-15.936 1.728,-15.616 1.728,-15.616c25.696,1.824 39.2,26.496 39.2,26.496c22.848,39.264 59.936,27.936 74.528,21.344c2.304,-16.608 8.928,-27.936 16.256,-34.368c-56.832,-6.496 -116.608,-28.544 -116.608,-127.008c0,-28.064 9.984,-51.008 26.368,-68.992c-2.656,-6.496 -11.424,-32.64 2.496,-68c0,0 21.504,-6.912 70.4,26.336c20.416,-5.696 42.304,-8.544 64.096,-8.64c21.728,0.128 43.648,2.944 64.096,8.672c48.864,-33.248 70.336,-26.336 70.336,-26.336c13.952,35.392 5.184,61.504 2.56,68c16.416,17.984 26.304,40.928 26.304,68.992c0,98.72 -59.84,120.448 -116.864,126.816c9.184,7.936 17.376,23.616 17.376,47.584c0,34.368 -0.32,62.08 -0.32,70.496c0,6.88 4.608,14.88 17.6,12.352C438.72,472.145 512,375.857 512,262.353C512,120.401 397.376,5.329 255.968,5.329z"/>
|
||||
</vector>
|
||||
|
@ -1,24 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp"
|
||||
android:height="25dp" android:viewportWidth="512.007" android:viewportHeight="512.007">
|
||||
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="m237.72,453.517c-204.315,-181.786 -197.402,-175.776 -197.402,-175.776 -25.999,-24.984 -40.318,-58.201 -40.318,-93.533 0,-46.48 24.63,-91.702 65.906,-115.47 3.589,-2.067 8.174,-0.833 10.242,2.757 2.067,3.589 0.833,8.175 -2.757,10.242 -36.017,20.74 -58.391,60.004 -58.391,102.471 0,31.212 12.683,60.588 35.711,82.717 0,0 -6.881,-5.996 196.979,175.386 2.292,2.039 5.242,3.161 8.309,3.161 3.066,0 6.018,-1.123 8.31,-3.162l61.917,-55.089c3.095,-2.753 7.835,-2.477 10.588,0.618s2.477,7.835 -0.618,10.588l-61.917,55.09c-10.431,9.281 -26.148,9.263 -36.559,0zM357.363,377.059c-2.067,0 -4.124,-0.849 -5.606,-2.515 -2.753,-3.095 -2.477,-7.835 0.618,-10.588l105.273,-93.665c21.815,-19.409 35.132,-44.369 38.513,-72.181 0.001,-0.006 0.001,-0.012 0.002,-0.018 7.637,-62.927 -37.915,-131.557 -116.038,-131.557 -54.879,0 -102.877,38.923 -114.129,92.55 -1.005,4.79 -5.116,8.135 -9.997,8.135s-8.991,-3.346 -9.996,-8.136c-11.252,-53.626 -59.25,-92.549 -114.128,-92.549 -9.633,0 -19.082,1.076 -28.084,3.198 -4.033,0.952 -8.07,-1.548 -9.021,-5.579 -0.951,-4.032 1.547,-8.07 5.579,-9.021 10.128,-2.388 20.735,-3.598 31.525,-3.598 55.699,0 105.463,35.109 124.125,87.792 18.71,-52.817 68.567,-87.792 124.125,-87.792 84.905,0 139.884,74.56 130.929,148.362 0,0.007 -0.001,0.015 -0.002,0.022 -3.829,31.494 -18.847,59.703 -43.433,81.578l-105.273,93.665c-1.429,1.272 -3.209,1.897 -4.982,1.897z"/>
|
||||
</vector>
|
@ -1,6 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="456"
|
||||
android:viewportWidth="456" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M61.179,282h-41.2c-6,0 -10.9,4.9 -10.9,10.9v152.2c0,6 4.9,10.9 10.9,10.9h41.2c6,0 10.9,-4.9 10.9,-10.9V292.9C72.079,286.9 67.179,282 61.179,282z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M443.179,294.4c-0.3,-0.4 -0.6,-0.8 -0.8,-1.2c-6.1,-6.8 -16.4,-7.7 -23.6,-2.1c-20,16.3 -49.2,39.8 -68.1,55.1c-16.7,13.3 -37.3,21 -58.7,21.8l-51.2,1.7c-9.4,0.3 -18.2,-4.5 -23,-12.6l-5.7,-9.7c-1.5,-2.5 -2.5,-5.3 -3.1,-8.2c-2.6,-13.9 6.5,-27.4 20.4,-30l52.9,-10c8.5,-1.7 14.3,-9.7 13.3,-18.3c-1,-8.2 -8,-14.3 -16.2,-14.4c-0.3,0 -0.7,0 -0.8,0c-0.4,0 -0.9,0 -1.3,0l-71.2,-2.9c-24.7,-0.9 -46,3.9 -71.2,16.1l-42.8,20.7v114l35.9,-6.6c0.1,-0.1 0.2,-0.1 0.3,-0.1c13.9,-2 23.1,-2.9 38.2,-2.2l107.5,5c33.7,1.4 66.8,-9.7 92.7,-31.3l74.4,-61.7C447.979,311.7 448.879,301.3 443.179,294.4z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M307.379,0c-61.2,0.1 -110.7,49.7 -110.8,110.8c0,0.1 0,0.1 0,0.1c0,61.2 49.7,110.8 110.9,110.8s110.8,-49.7 110.8,-110.9S368.579,0 307.379,0zM333.079,80h13.4c5.5,0 10,4.5 10,10s-4.5,10 -10,10h-13.4c-2,7.8 -6,14.9 -11.7,20.6c-7.5,7.5 -17.5,11.9 -28.1,12.5l37.7,35.7c4,3.8 4.2,10.1 0.4,14.1s-10.1,4.2 -14.1,0.4l-55.9,-52.9c-1.9,-1.9 -3.1,-4.5 -3.1,-7.2c-0.1,-5.6 4.4,-10.1 10,-10.2h22.4c6.2,0.1 12.2,-2.3 16.7,-6.7c1.8,-1.9 3.3,-4 4.6,-6.3h-43.7c-5.5,0 -10,-4.5 -10,-10s4.5,-10 10,-10h43.7c-3.7,-8 -11.9,-14 -21.3,-14h-22.4c-5.5,0 -10,-4.5 -10,-10s4.5,-10 10,-10h78.2c5.5,0 10,4.5 10,10s-4.5,10 -10,10h-19.2C330.079,70.3 331.979,75 333.079,80z"/>
|
||||
</vector>
|
@ -1,50 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="32dp" android:height="32dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="M352,0H160C71.648,0 0,71.648 0,160v192c0,88.352 71.648,160 160,160h192c88.352,0 160,-71.648 160,-160V160C512,71.648 440.352,0 352,0zM464,352c0,61.76 -50.24,112 -112,112H160c-61.76,0 -112,-50.24 -112,-112V160C48,98.24 98.24,48 160,48h192c61.76,0 112,50.24 112,112V352z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="465.1312" android:endY="46.8656"
|
||||
android:startX="46.8688" android:startY="465.1344" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M256,128c-70.688,0 -128,57.312 -128,128s57.312,128 128,128s128,-57.312 128,-128S326.688,128 256,128zM256,336c-44.096,0 -80,-35.904 -80,-80c0,-44.128 35.904,-80 80,-80s80,35.872 80,80C336,300.096 300.096,336 256,336z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="346.5072" android:endY="165.4928"
|
||||
android:startX="165.4928" android:startY="346.5072" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M393.6,118.4m-17.056,0a17.056,17.056 0,1 1,34.112 0a17.056,17.056 0,1 1,-34.112 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="405.6592" android:endY="106.3408"
|
||||
android:startX="381.5408" android:startY="130.4624" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
@ -1,8 +0,0 @@
|
||||
<vector android:height="42dp" android:viewportHeight="250"
|
||||
android:viewportWidth="488" android:width="81.984dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#fff" android:pathData="M483.73,36A53.1,53.1 0,0 0,452 4.28C438.49,0 425.94,0 400.84,0H325.16C300.07,0 287.52,0 274,4.28A53.08,53.08 0,0 0,242.28 36a76.64,76.64 0,0 0,-2 7.74,140.32 140.32,0 0,1 14,24.86c0.38,-9.57 1.27,-17.22 3.46,-24.14 4.68,-12.86 11.88,-20.06 24.74,-24.74C294.25,16 308.12,16 330,16h66c21.88,0 35.76,0 47.54,3.73 12.86,4.68 20,11.88 24.74,24.74C472,56.25 472,70.13 472,92v66c0,21.88 0,35.76 -3.72,47.53 -4.69,12.86 -11.88,20.06 -24.74,24.74C431.76,234 417.88,234 396,234H330c-21.89,0 -35.76,0 -47.54,-3.73 -12.86,-4.68 -20.06,-11.88 -24.74,-24.74 -2.19,-6.92 -3.09,-14.58 -3.46,-24.15a140.51,140.51 0,0 1,-14 24.85,77.18 77.18,0 0,0 2,7.77A53.08,53.08 0,0 0,274 245.73C287.52,250 300.07,250 325.16,250h75.68c25.1,0 37.65,0 51.16,-4.27A53.11,53.11 0,0 0,483.73 214C488,200.49 488,187.94 488,162.84V87.17C488,62.07 488,49.52 483.73,36Z"/>
|
||||
<path android:fillColor="#fff" android:pathData="M422,217L380.33,217c-1.76,0 -5.83,-2.79 -2.63,-6.67 21.36,-23 48,-30.93 73.4,-39.42 3.32,-1 3.91,2.51 3.91,3.48v8.68C455,202.61 441.57,217 422,217ZM343.73,212.69c-4,-29.73 -18.06,-80.79 -71,-118.55A3.78,3.78 0,0 1,271 90.63L271,66.36c0,-26.69 18,-33.31 26.37,-33.31a4.3,4.3 0,0 1,4.07 2.1c25.24,55 41,89.86 50.7,172.83 0.05,1.62 0.31,2.39 1.28,0 6.86,-15.07 39.35,-92 26.44,-170.68a3.64,3.64 0,0 1,3.5 -4.25L422,33.05c19.54,0 33,13.43 33,33.36L455,100.5a3.63,3.63 0,0 1,-2.07 3.36,180.12 180.12,0 0,0 -90.3,109.25c-0.79,2.21 -1.25,3.9 -3.71,3.9h-11.8C344.77,217 344.27,216.05 343.73,212.7ZM304.35,217c-20,0 -33.35,-12.37 -33.35,-33.93v-2.24c0,-0.9 0.71,-4.29 4.09,-3.63 20.24,6.23 41.92,12.52 57.77,33.49 1.82,2.56 0.23,6.3 -2.91,6.31Z"/>
|
||||
<path android:fillColor="#fff" android:pathData="M124.991,239.991a115,115 54.655,1 0,2.007 -229.991a115,115 54.655,1 0,-2.007 229.991z"/>
|
||||
<path android:fillColor="#2bc5b4" android:pathData="M180.77,114.59c-8.62,0 -15.61,7.39 -15.61,16.49s7,16.5 15.61,16.5 15.62,-7.38 15.62,-16.5S189.4,114.59 180.77,114.59Z"/>
|
||||
<path android:fillColor="#2bc5b4" android:pathData="M125,0A125,125 0,1 0,250 125,125 125,0 0,0 125,0ZM95.37,132.09c0,63.82 -101.74,35.68 -60.49,2.93 9.65,13.39 28.18,12.5 30.15,-0.72l0.37,-52.05c0.95,-13.32 26.85,-16 30,0ZM133.31,156.32a12.05,12.05 0,0 1,-12 12L116.1,168.32a12.05,12.05 0,0 1,-12 -12L104.1,106a12,12 0,0 1,12 -12h5.21a12,12 0,0 1,12 12ZM133.31,74.56a11.84,11.84 0,0 1,-11.79 11.79L115.9,86.35a11.84,11.84 0,0 1,-11.81 -11.79L104.09,71.65A11.83,11.83 0,0 1,115.9 59.86h5.62a11.82,11.82 0,0 1,11.79 11.79ZM180.77,169.9c-22,0 -39.82,-17.37 -39.82,-38.82s17.84,-38.81 39.82,-38.81 39.81,17.38 39.81,38.81S202.76,169.9 180.77,169.9Z"/>
|
||||
</vector>
|
@ -1,33 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="52dp" android:height="52dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="m140.008,423h-30c-11.047,0 -20,-8.953 -20,-20v-186c0,-11.047 8.953,-20 20,-20h30c11.047,0 20,8.953 20,20v186c0,11.047 -8.953,20 -20,20zM166.992,124.996c0,-22.629 -18.359,-40.996 -40.977,-40.996 -22.703,0 -41.016,18.367 -41.016,40.996 0,22.637 18.313,41.004 41.016,41.004 22.617,0 40.977,-18.367 40.977,-41.004zM422,403v-104.336c0,-60.668 -12.816,-105.664 -83.688,-105.664 -34.055,0 -56.914,17.031 -66.246,34.742h-0.066v-10.742c0,-11.047 -8.953,-20 -20,-20h-28c-11.047,0 -20,8.953 -20,20v186c0,11.047 8.953,20 20,20h28c11.047,0 20,-8.953 20,-20v-92.211c0,-29.387 7.48,-57.855 43.906,-57.855 35.93,0 37.094,33.605 37.094,59.723v90.344c0,11.047 8.953,20 20,20h29c11.047,0 20,-8.953 20,-20zM512,432c0,-11.047 -8.953,-20 -20,-20s-20,8.953 -20,20c0,22.055 -17.945,40 -40,40h-352c-22.055,0 -40,-17.945 -40,-40v-352c0,-22.055 17.945,-40 40,-40h352c22.055,0 40,17.945 40,40v251c0,11.047 8.953,20 20,20s20,-8.953 20,-20v-251c0,-44.113 -35.887,-80 -80,-80h-352c-44.113,0 -80,35.887 -80,80v352c0,44.113 35.887,80 80,80h352c44.113,0 80,-35.887 80,-80zM512,432">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="512" android:endY="256"
|
||||
android:startX="0" android:startY="256" android:type="linear">
|
||||
<item android:color="#FF00F2FE" android:offset="0"/>
|
||||
<item android:color="#FF03EFFE" android:offset="0.0208"/>
|
||||
<item android:color="#FF24D2FE" android:offset="0.2931"/>
|
||||
<item android:color="#FF3CBDFE" android:offset="0.5538"/>
|
||||
<item android:color="#FF4AB0FE" android:offset="0.7956"/>
|
||||
<item android:color="#FF4FACFE" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
@ -1,29 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="40dp"
|
||||
android:height="40dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#ff5d7d" android:fillType="evenOdd" android:pathData="m258.229,255.863c-11.191,-11.155 -29.503,-11.155 -40.693,0 -4.486,4.471 -11.053,4.007 -15.072,0 -11.191,-11.155 -29.503,-11.155 -40.693,0 -30.403,30.307 28.128,83.271 48.229,88.64 20.102,-5.369 78.632,-58.333 48.229,-88.64z"/>
|
||||
<path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="m258.229,255.863c30.403,30.307 -28.128,83.271 -48.23,88.64 -20.102,-5.369 -78.633,-58.334 -48.229,-88.64 11.191,-11.155 29.502,-11.155 40.693,0 4.02,4.007 10.587,4.471 15.072,0 11.191,-11.155 29.503,-11.155 40.694,0zM10,176c0,94.167 60,173.334 80,260h240c3.112,-13.487 7.193,-26.792 11.866,-40 4.742,-13.403 10.093,-26.707 15.66,-40 16.471,-39.33 34.83,-78.563 44.877,-119.994 3.154,-13.009 5.489,-26.235 6.689,-39.749 0.593,-6.679 0.908,-13.429 0.908,-20.257 0,-11 -9,-20 -20,-20 -120,0 -240,0 -360.001,0 -10.999,0 -19.999,9 -19.999,20z"/>
|
||||
<path android:fillColor="#ccf5fc" android:fillType="evenOdd" android:pathData="m402,356h-44.474c-5.567,13.293 -10.918,26.597 -15.66,40h60.134c55,0 99.999,-45 99.999,-100 0,-52.616 -41.185,-96.074 -92.908,-99.743 -1.2,13.514 -3.534,26.74 -6.69,39.749 32.818,0.218 59.599,27.129 59.599,59.994 0,33 -27,60 -60,60z"/>
|
||||
<path android:fillColor="#ccf5fc" android:fillType="evenOdd" android:pathData="m330,436h-240,-20c-11,0 -20,9 -20,20s9,20 20,20h280c11,0 20,-9 20,-20s-9,-20 -20,-20z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="m419.714,187.451c0.186,-3.793 0.286,-7.608 0.286,-11.451 0,-16.542 -13.458,-30 -30,-30h-360c-16.542,0 -30,13.458 -30,30 0,59.097 22.691,112.205 44.635,163.564 12.597,29.484 24.571,57.526 32.514,86.436h-7.149c-16.542,0 -30,13.458 -30,30s13.458,30 30,30h280c16.542,0 30,-13.458 30,-30s-13.458,-30 -30,-30h-7.147c1.842,-6.704 3.897,-13.362 6.13,-20h53.017c60.654,0 110,-49.346 110,-110 0,-53.968 -39.847,-99.962 -92.286,-108.549zM409.978,246.658c23.725,3.87 42.022,24.684 42.022,49.342 0,27.57 -22.43,50 -50,50h-29.383c0.912,-2.138 1.828,-4.282 2.747,-6.435 12.854,-30.084 25.958,-60.771 34.614,-92.907zM254.997,426c-5.523,0 -10,4.478 -10,10s4.477,10 10,10h95.003c5.514,0 10,4.486 10,10s-4.486,10 -10,10h-280c-5.514,0 -10,-4.486 -10,-10s4.486,-10 10,-10h94.997c5.523,0 10,-4.478 10,-10s-4.477,-10 -10,-10h-67.153c-8.334,-32.299 -21.78,-63.781 -34.817,-94.293 -21.153,-49.509 -43.027,-100.704 -43.027,-155.707 0,-5.514 4.486,-10 10,-10h360c5.514,0 10,4.486 10,10 0,55.003 -21.874,106.198 -43.027,155.707 -13.036,30.513 -26.486,61.997 -34.82,94.293zM402,386h-45.791c2.546,-6.646 5.221,-13.303 7.988,-20h37.803c38.599,0 70,-31.401 70,-70 0,-34.024 -24.884,-62.818 -57.401,-68.83 1.329,-6.513 2.447,-13.09 3.312,-19.739 42.202,7.615 74.089,44.901 74.089,88.569 0,49.626 -40.374,90 -90,90z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="m210.476,248.781c-0.2,0.201 -0.476,0.477 -0.953,0v0.001c-15.113,-15.066 -39.703,-15.066 -54.813,0 -10.553,10.519 -13.958,24.203 -9.847,39.572 8.313,31.073 45.551,61.27 62.555,65.811 0.845,0.226 1.713,0.339 2.581,0.339s1.735,-0.113 2.581,-0.339c17.004,-4.541 54.242,-34.736 62.556,-65.811 4.111,-15.369 0.706,-29.054 -9.846,-39.572 -15.113,-15.065 -39.702,-15.066 -54.814,-0.001zM255.815,283.185c-5.882,21.986 -33.302,45.229 -45.815,50.721 -12.513,-5.491 -39.933,-28.734 -45.814,-50.721 -2.249,-8.407 -0.773,-14.838 4.646,-20.239 3.663,-3.651 8.474,-5.478 13.286,-5.478s9.624,1.826 13.288,5.478v0.001c8.185,8.156 21.007,8.157 29.191,-0.001 7.326,-7.303 19.247,-7.303 26.574,0 5.417,5.401 6.892,11.832 4.644,20.239z"/>
|
||||
<path android:fillColor="#ccf5fc" android:pathData="m201.736,110.504c-3.034,4.615 -1.752,10.815 2.862,13.85 1.693,1.113 3.599,1.646 5.484,1.646 3.253,0 6.444,-1.586 8.365,-4.507 17.816,-27.099 6.822,-41.619 -0.453,-51.228 -6.372,-8.416 -9.882,-13.052 0.453,-28.771 3.034,-4.615 1.752,-10.815 -2.862,-13.85 -4.614,-3.034 -10.815,-1.753 -13.85,2.861 -18.093,27.52 -7.016,42.15 0.314,51.832 6.489,8.57 9.745,12.871 -0.313,28.167z"/>
|
||||
<path android:fillColor="#ccf5fc" android:pathData="m121.733,110.504c-3.034,4.615 -1.752,10.815 2.862,13.85 1.693,1.113 3.599,1.646 5.484,1.646 3.253,0 6.444,-1.586 8.365,-4.507 17.816,-27.099 6.823,-41.619 -0.452,-51.228 -6.373,-8.416 -9.882,-13.053 0.452,-28.771 3.034,-4.615 1.752,-10.815 -2.862,-13.85 -4.614,-3.034 -10.816,-1.753 -13.85,2.861 -18.093,27.52 -7.016,42.15 0.314,51.831 6.489,8.571 9.746,12.872 -0.313,28.168z"/>
|
||||
<path android:fillColor="#ccf5fc" android:pathData="m281.739,110.504c-3.034,4.615 -1.753,10.815 2.861,13.85 1.693,1.113 3.6,1.646 5.484,1.646 3.254,0 6.444,-1.585 8.365,-4.507 17.817,-27.099 6.823,-41.619 -0.452,-51.228 -6.372,-8.416 -9.882,-13.053 0.452,-28.771 3.034,-4.615 1.753,-10.815 -2.861,-13.85 -4.615,-3.034 -10.815,-1.751 -13.85,2.861 -18.094,27.52 -7.017,42.15 0.313,51.831 6.49,8.571 9.746,12.872 -0.312,28.168z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="m210,426h-0.007c-5.523,0 -9.996,4.478 -9.996,10s4.48,10 10.003,10 10,-4.478 10,-10 -4.477,-10 -10,-10z"/>
|
||||
</vector>
|
@ -1,28 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="300dp"
|
||||
android:height="300dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#A3787878" android:pathData="m256,80a48.054,48.054 0,0 1,48 48v32h12a19.991,19.991 0,0 0,3.524 -39.671,63.984 63.984,0 0,0 -127.048,0 19.991,19.991 0,0 0,3.524 39.671h12v-32a48.054,48.054 0,0 1,48 -48z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m48,152a24.027,24.027 0,0 0,24 -24v-74.234l42.53,-14.176 -5.06,-15.18 -48,16a8,8 0,0 0,-5.47 7.59v57.376a24,24 0,1 0,-8 46.624zM48,120a8,8 0,1 1,-8 8,8.009 8.009,0 0,1 8,-8z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m485.006,17.76a7.993,7.993 0,0 0,-6.741 -1.569l-72,16a8,8 0,0 0,-6.265 7.809v57.376a24,24 0,1 0,16 22.624v-73.583l56,-12.444v47.4a24,24 0,1 0,16 22.627v-80a8,8 0,0 0,-2.994 -6.24zM392,128a8,8 0,1 1,8 -8,8.009 8.009,0 0,1 -8,8zM464,112a8,8 0,1 1,8 -8,8.009 8.009,0 0,1 -8,8z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m48,456h416v40h-416z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m64,376a16,16 0,0 0,-16 16v7h48v-7a16,16 0,0 0,-16 -16z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m24,416h464v24h-464z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="M256,144m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m368,400 l16,-160h-256l16,160zM256,296a24,24 0,1 1,-24 24,24 24,0 0,1 24,-24z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m168,224h176a32,32 0,0 0,-32 -32h-112a32,32 0,0 0,-32 32z"/>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="64"
|
||||
android:viewportWidth="64" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#BBFFFFFF" android:fillType="evenOdd" android:pathData="M52.402,31.916c0,4.03 -1.17,7.895 -3.178,11.087l8.196,8.23c4.014,-5.375 6.523,-12.094 6.523,-19.318s-2.51,-13.942 -6.523,-19.318l-8.196,8.23c2.007,3.192 3.178,6.887 3.178,11.087z"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="evenOdd" android:pathData="M32.004,52.41c-11.207,0 -20.406,-9.24 -20.406,-20.493s9.2,-20.493 20.406,-20.493c4.182,0 7.86,1.176 11.04,3.36l8.196,-8.23C45.887,2.52 39.197,0 32.004,0 14.44,0 0.057,14.278 0.057,32.084S14.44,64 32.004,64c7.36,0 14.05,-2.52 19.403,-6.55l-8.196,-8.23c-3.178,2.016 -7.025,3.192 -11.207,3.192z"/>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="435.505"
|
||||
android:viewportWidth="435.505" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M403.496,101.917c-4.104,-5.073 -8.877,-9.705 -14.166,-13.839c0.707,13.117 -0.508,27.092 -3.668,41.884c-8.627,40.413 -29.256,74.754 -59.656,99.304c-30.375,24.533 -68.305,37.502 -109.686,37.502h-60.344l-19.533,91.512c-3.836,17.959 -19.943,30.99 -38.303,30.99H70.938l-4.898,22.484c-1.258,5.79 0.17,11.839 3.887,16.453c3.715,4.614 9.324,7.298 15.25,7.298h66.498c9.24,0 17.225,-6.459 19.152,-15.495L193.667,313h76.188c36.854,0 70.527,-11.464 97.384,-33.152c26.869,-21.697 45.129,-52.186 52.807,-88.162C427.822,155.309 422.253,125.106 403.496,101.917z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M117.292,354.191l22.84,-107.008h76.188c36.852,0 70.527,-11.465 97.383,-33.154c26.867,-21.697 45.129,-52.186 52.809,-88.161c7.773,-36.378 2.207,-66.58 -16.553,-89.769C331.952,13.832 301.17,0 269.633,0H103.639c-9.209,0 -17.174,6.417 -19.135,15.414L12.505,345.938c-1.26,5.789 0.168,11.838 3.887,16.453c3.713,4.613 9.32,7.296 15.248,7.296h66.5C107.38,369.687 115.36,363.229 117.292,354.191zM178.235,75.291h52.229c12.287,0 23.274,5.149 30.145,14.129c7.297,9.539 9.431,22.729 5.853,36.188c-0.047,0.171 -0.088,0.342 -0.131,0.516c-6.57,27.73 -33.892,50.291 -60.898,50.291h-50.05L178.235,75.291z"/>
|
||||
</vector>
|
@ -1,31 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
|
||||
android:height="42dp" android:viewportWidth="496" android:viewportHeight="496">
|
||||
<path android:fillColor="#6C9DFF" android:pathData="M248,92c-13.6,0 -24,-10.4 -24,-24V24c0,-13.6 10.4,-24 24,-24s24,10.4 24,24v44C272,80.8 261.6,92 248,92z"/>
|
||||
<path android:fillColor="#DA3B7A" android:pathData="M248,496c-13.6,0 -24,-10.4 -24,-24v-44c0,-13.6 10.4,-24 24,-24s24,10.4 24,24v44C272,485.6 261.6,496 248,496z"/>
|
||||
<path android:fillColor="#63BBFF" android:pathData="M157.6,116c-8,0 -16,-4 -20.8,-12l-21.6,-37.6c-6.4,-11.2 -2.4,-26.4 8.8,-32.8s26.4,-2.4 32.8,8.8L178.4,80c6.4,11.2 2.4,26.4 -8.8,32.8C166.4,114.4 161.6,116 157.6,116z"/>
|
||||
<path android:fillColor="#E542A9" android:pathData="M360,465.6c-8,0 -16,-4 -20.8,-12L317.6,416c-6.4,-11.2 -2.4,-26.4 8.8,-32.8c11.2,-6.4 26.4,-2.4 32.8,8.8l21.6,37.6c6.4,11.2 2.4,26.4 -8.8,32.8C368,464.8 364,465.6 360,465.6z"/>
|
||||
<path android:fillColor="#A1DCEC" android:pathData="M92,181.6c-4,0 -8,-0.8 -12,-3.2l-37.6,-21.6c-11.2,-6.4 -15.2,-21.6 -8.8,-32.8s21.6,-15.2 32.8,-8.8l37.6,21.6c11.2,6.4 15.2,21.6 8.8,32.8C108,177.6 100,181.6 92,181.6z"/>
|
||||
<path android:fillColor="#B135FF" android:pathData="M442.4,384c-4,0 -8,-0.8 -12,-3.2L392,359.2c-11.2,-6.4 -15.2,-21.6 -8.8,-32.8c6.4,-11.2 21.6,-15.2 32.8,-8.8l37.6,21.6c11.2,6.4 15.2,21.6 8.8,32.8C458.4,380 450.4,384 442.4,384z"/>
|
||||
<path android:fillColor="#F3FFFD" android:pathData="M68,272H24c-13.6,0 -24,-10.4 -24,-24s10.4,-24 24,-24h44c13.6,0 24,10.4 24,24S80.8,272 68,272z"/>
|
||||
<path android:fillColor="#9254C8" android:pathData="M472,272h-44c-13.6,0 -24,-10.4 -24,-24s10.4,-24 24,-24h44c13.6,0 24,10.4 24,24S485.6,272 472,272z"/>
|
||||
<path android:fillColor="#CE1CFF" android:pathData="M53.6,384c-8,0 -16,-4 -20.8,-12c-6.4,-11.2 -2.4,-26.4 8.8,-32.8l37.6,-21.6c11.2,-6.4 26.4,-2.4 32.8,8.8c6.4,11.2 2.4,26.4 -8.8,32.8l-37.6,21.6C62.4,383.2 58.4,384 53.6,384z"/>
|
||||
<path android:fillColor="#6953E5" android:pathData="M404,181.6c-8,0 -16,-4 -20.8,-12c-6.4,-11.2 -2.4,-26.4 8.8,-32.8l37.6,-21.6c11.2,-6.4 26.4,-2.4 32.8,8.8s2.4,26.4 -8.8,32.8L416,178.4C412,180.8 408,181.6 404,181.6z"/>
|
||||
<path android:fillColor="#DE339F" android:pathData="M136,465.6c-4,0 -8,-0.8 -12,-3.2c-11.2,-6.4 -15.2,-21.6 -8.8,-32.8l21.6,-37.6c6.4,-11.2 21.6,-15.2 32.8,-8.8c11.2,6.4 15.2,21.6 8.8,32.8l-21.6,37.6C152,461.6 144,465.6 136,465.6z"/>
|
||||
<path android:fillColor="#5681FF" android:pathData="M338.4,116c-4,0 -8,-0.8 -12,-3.2c-11.2,-6.4 -15.2,-21.6 -8.8,-32.8l21.6,-37.6c6.4,-11.2 21.6,-15.2 32.8,-8.8c11.2,6.4 15.2,21.6 8.8,32.8L359.2,104C354.4,111.2 346.4,116 338.4,116z"/>
|
||||
</vector>
|
@ -1,26 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM16.3,16.3c-0.39,0.39 -1.02,0.39 -1.41,0L12,13.41 9.11,16.3c-0.39,0.39 -1.02,0.39 -1.41,0 -0.39,-0.39 -0.39,-1.02 0,-1.41L10.59,12 7.7,9.11c-0.39,-0.39 -0.39,-1.02 0,-1.41 0.39,-0.39 1.02,-0.39 1.41,0L12,10.59l2.89,-2.89c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41L13.41,12l2.89,2.89c0.38,0.38 0.38,1.02 0,1.41z"/>
|
||||
</vector>
|
@ -1,22 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp"
|
||||
android:height="32dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#FF3C64" android:pathData="m304,232a24,24 0,0 1,-16.971 -40.971l160,-160a24,24 0,0 1,33.942 33.942l-160,160a23.926,23.926 0,0 1,-16.971 7.029z"/>
|
||||
<path android:fillColor="#FF3B63" android:pathData="m464,200a24,24 0,0 1,-24 -24v-104h-104a24,24 0,0 1,0 -48h128a24,24 0,0 1,24 24v128a24,24 0,0 1,-24 24z"/>
|
||||
<path android:fillColor="#CE1CFF" android:pathData="m464,488h-416a24,24 0,0 1,-24 -24v-416a24,24 0,0 1,24 -24h176a24,24 0,0 1,0 48h-152v368h368v-152a24,24 0,0 1,48 0v176a24,24 0,0 1,-24 24z"/>
|
||||
</vector>
|
@ -1,21 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
|
||||
android:height="42dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#A3787878" android:pathData="m511.739,103.734 l-257,50.947v233.725c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-182.682l197,-39.053v98.141c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c39.927,0 71.547,-34.762 67.073,-75h0.427zM217.239,482c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM444.239,422c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM481.739,199.682 L284.739,238.735v-59.416l197,-39.053z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m182.179,159.75h30c0,-31.002 4.415,-66.799 -24.144,-95.356 -8.968,-8.968 -17.455,-16.07 -24.942,-22.336 -19.798,-16.57 -27.832,-24.012 -27.832,-42.058h-30v221.406c-10.734,-7.199 -23.634,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-227.219c9.458,8.262 20.077,16.341 31.562,27.825 19.029,19.031 15.356,44.009 15.356,74.144zM67.761,315c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.823,37.5 -37.5,37.5z"/>
|
||||
</vector>
|
@ -1,47 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector android:height="150dp" android:viewportHeight="512"
|
||||
android:viewportWidth="512" android:width="150dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:pathData="M256,256m-256,0a256,256 0,1 1,512 0a256,256 0,1 1,-512 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="437.019" android:endY="74.981"
|
||||
android:startX="74.981" android:startY="437.019" android:type="linear">
|
||||
<item android:color="#FF736BFD" android:offset="0"/>
|
||||
<item android:color="#FFF54187" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#FF000000" android:pathData="M377,356.7c-68.9,-45.4 -155.6,-56.4 -257.6,-32.7c-20.5,4.8 -13.6,35.8 7.3,31.2C290.7,317 351.6,386 368.2,386C384,386 390.2,365.4 377,356.7z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M112.1,275.1C203.9,253.4 308.1,266 384,308c18.5,10.2 34,-17.8 15.5,-28c-82.7,-45.7 -195.6,-59.5 -294.7,-36C84.2,248.8 91.5,280 112.1,275.1L112.1,275.1z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M100,191.9c96.6,-29.6 232.2,-13.4 308.7,36.9c17.6,11.5 35.3,-15.1 17.6,-26.7c-84.9,-55.8 -229.2,-73.3 -335.6,-40.8C70.4,167.5 79.9,198.1 100,191.9L100,191.9z"/>
|
||||
<path android:pathData="M507.8,438.2c-1.6,97.2 -141.9,97.1 -143.5,0C365.9,341 506.2,341 507.8,438.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="384.197" android:endY="490.009"
|
||||
android:startX="487.832" android:startY="386.374" android:type="linear">
|
||||
<item android:color="#FF736BFD" android:offset="0"/>
|
||||
<item android:color="#FFF54187" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M486.8,456.8c-0.6,-2.4 -6.9,-1 -8.5,-1.4c11.5,-82 -82.4,-86.7 -87.1,-22.2c0.3,1.8 -1,6.7 2.2,6.6c0,0 8.6,0 8.6,0c3.1,0.1 2,-4.7 2.2,-6.6c0.1,-23.3 35,-23.3 35.2,0c0,0 0,6.9 0,6.9c-0.1,2.8 4.4,2.8 4.3,0c5,-35.2 -43.8,-40.1 -43.8,-4.7h-4.3c-1.6,-53.7 77.2,-55.9 78.4,-2.2c0,0 0,24.4 0,24.4c-0.1,2.9 3.8,2.1 5.6,2.2l-20.7,21l-20.7,-21c1.8,-0.1 5.6,0.7 5.6,-2.2c0,0 0,-8.8 0,-8.8c0,-2.8 -4.4,-2.8 -4.3,0c0,0 0,6.6 0,6.6c-2.2,0.2 -11.3,-1.3 -8,3.7c0,0 25.9,26.3 25.9,26.3c0.8,0.9 2.2,0.9 3.1,0C460.6,484.4 489.4,458.3 486.8,456.8z"
|
||||
android:strokeColor="#000" android:strokeWidth=".75"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M510,437.5c-1.7,96.2 -142.1,96.2 -143.8,0C367.9,341.3 508.4,341.3 510,437.5z"
|
||||
android:strokeColor="#000" android:strokeWidth="6"/>
|
||||
</vector>
|
@ -1,20 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
|
||||
android:height="42dp" android:viewportWidth="427.652" android:viewportHeight="427.652">
|
||||
<path android:fillColor="#00D95F" android:pathData="M213.826,0C95.733,0 0,95.733 0,213.826s95.733,213.826 213.826,213.826s213.826,-95.733 213.826,-213.826S331.919,0 213.826,0zM306.886,310.32c-2.719,4.652 -7.612,7.246 -12.638,7.247c-2.506,0 -5.044,-0.645 -7.364,-2c-38.425,-22.456 -82.815,-26.065 -113.295,-25.138c-33.763,1.027 -58.523,7.692 -58.769,7.76c-7.783,2.126 -15.826,-2.454 -17.961,-10.236c-2.134,-7.781 2.43,-15.819 10.209,-17.962c1.116,-0.307 27.76,-7.544 64.811,-8.766c21.824,-0.72 42.834,0.801 62.438,4.52c24.83,4.71 47.48,12.978 67.322,24.574C308.612,294.393 310.96,303.349 306.886,310.32zM334.07,253.861c-3.22,5.511 -9.016,8.583 -14.97,8.584c-2.968,0 -5.975,-0.763 -8.723,-2.369c-45.514,-26.6 -98.097,-30.873 -134.2,-29.776c-39.994,1.217 -69.323,9.112 -69.614,9.192c-9.217,2.515 -18.746,-2.906 -21.275,-12.124c-2.528,-9.218 2.879,-18.738 12.093,-21.277c1.322,-0.364 32.882,-8.937 76.77,-10.384c25.853,-0.852 50.739,0.949 73.96,5.354c29.412,5.58 56.241,15.373 79.744,29.108C336.115,234.995 338.897,245.603 334.07,253.861zM350.781,202.526c-3.641,0 -7.329,-0.936 -10.7,-2.906c-108.207,-63.238 -248.572,-25.643 -249.977,-25.255c-11.313,3.117 -23.008,-3.527 -26.124,-14.839c-3.117,-11.312 3.527,-23.008 14.839,-26.124c1.621,-0.447 40.333,-10.962 94.166,-12.737c31.713,-1.044 62.237,1.164 90.72,6.567c36.077,6.844 68.987,18.856 97.815,35.704c10.13,5.92 13.543,18.931 7.623,29.061C365.193,198.757 358.084,202.526 350.781,202.526z"/>
|
||||
</vector>
|
@ -1,30 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="40dp" android:height="40dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="m512,256c0,141.387 -114.613,256 -256,256s-256,-114.613 -256,-256 114.613,-256 256,-256 256,114.613 256,256zM512,256">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="512" android:endY="256"
|
||||
android:startX="0" android:startY="256" android:type="linear">
|
||||
<item android:color="#748AFF" android:offset="0"/>
|
||||
<item android:color="#FF3C64" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#000000" android:pathData="m175,395.246c-4.035,0 -7.902,-1.629 -10.727,-4.512l-81,-82.832c-5.789,-5.922 -5.684,-15.418 0.238,-21.211 5.922,-5.793 15.418,-5.688 21.211,0.238l70.273,71.859 232.277,-237.523c5.793,-5.922 15.289,-6.027 21.211,-0.234 5.926,5.789 6.031,15.289 0.238,21.211l-243,248.492c-2.82,2.883 -6.688,4.512 -10.723,4.512zM175,395.246"/>
|
||||
</vector>
|
@ -1,23 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector android:height="42dp" android:viewportHeight="500"
|
||||
android:viewportWidth="500" android:width="42dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#E11F23" android:pathData="M236.966,236.966m-236.966,0a236.966,236.966 0,1 1,473.932 0a236.966,236.966 0,1 1,-473.932 0"/>
|
||||
<path android:fillColor="#E11F23" android:pathData="M404.518,69.38c92.541,92.549 92.549,242.593 0,335.142c-92.541,92.541 -242.593,92.545 -335.142,0L404.518,69.38z"/>
|
||||
<path android:fillColor="#E11F23" android:pathData="M469.168,284.426L351.886,167.148l-138.322,15.749l-83.669,129.532l156.342,156.338C378.157,449.322 450.422,376.612 469.168,284.426z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M360.971,191.238c0,-19.865 -16.093,-35.966 -35.947,-35.966H156.372c-19.85,0 -35.94,16.105 -35.94,35.966v96.444c0,19.865 16.093,35.966 35.94,35.966h168.649c19.858,0 35.947,-16.105 35.947,-35.966v-96.444H360.971zM216.64,280.146v-90.584l68.695,45.294L216.64,280.146z"/>
|
||||
</vector>
|
@ -1,22 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector android:height="44dp" android:viewportHeight="192"
|
||||
android:viewportWidth="192" android:width="44dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF0000" android:pathData="M96,96m-88,0a88,88 0,1 1,176 0a88,88 0,1 1,-176 0"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M96,54.04c23.14,0 41.96,18.82 41.96,41.96S119.14,137.96 96,137.96S54.04,119.14 54.04,96S72.86,54.04 96,54.04M96,50c-25.41,0 -46,20.59 -46,46s20.59,46 46,46s46,-20.59 46,-46S121.41,50 96,50L96,50z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M80,119l39,-24l-39,-22z"/>
|
||||
</vector>
|
@ -1,21 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2021 Shabinder Singh
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
|
||||
android:height="42dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#A3787878" android:pathData="m511.739,103.734 l-257,50.947v233.725c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-182.682l197,-39.053v98.141c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c39.927,0 71.547,-34.762 67.073,-75h0.427zM217.239,482c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM444.239,422c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM481.739,199.682 L284.739,238.735v-59.416l197,-39.053z"/>
|
||||
<path android:fillColor="#A3787878" android:pathData="m182.179,159.75h30c0,-31.002 4.415,-66.799 -24.144,-95.356 -8.968,-8.968 -17.455,-16.07 -24.942,-22.336 -19.798,-16.57 -27.832,-24.012 -27.832,-42.058h-30v221.406c-10.734,-7.199 -23.634,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-227.219c9.458,8.262 20.077,16.341 31.562,27.825 19.029,19.031 15.356,44.009 15.356,74.144zM67.761,315c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.823,37.5 -37.5,37.5z"/>
|
||||
</vector>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -30,33 +30,8 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:database"))
|
||||
implementation("org.jetbrains.kotlinx:atomicfu:0.16.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
|
||||
api(MultiPlatformSettings.dep)
|
||||
implementation(Extras.youtubeDownloader)
|
||||
implementation(Extras.fuzzyWuzzy)
|
||||
implementation(MVIKotlin.rx)
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(Extras.mp3agic)
|
||||
implementation(Extras.Android.countly)
|
||||
// implementation(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||
}
|
||||
}
|
||||
desktopMain {
|
||||
dependencies {
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(Extras.mp3agic)
|
||||
implementation(Extras.Desktop.countly)
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation(npm("browser-id3-writer", "4.4.0"))
|
||||
implementation(npm("file-saver", "2.0.4"))
|
||||
implementation(project(":common:providers"))
|
||||
implementation(project(":common:core-components"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,82 +16,30 @@
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.shabinder.common.core_components.coreComponentModules
|
||||
import com.shabinder.common.database.databaseModule
|
||||
import com.shabinder.common.database.getLogger
|
||||
import com.shabinder.common.di.analytics.analyticsModule
|
||||
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.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import io.ktor.client.features.logging.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.shabinder.common.providers.providersModule
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.KoinAppDeclaration
|
||||
import org.koin.dsl.module
|
||||
import kotlin.native.concurrent.SharedImmutable
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
|
||||
fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
|
||||
startKoin {
|
||||
appDeclaration()
|
||||
|
||||
modules(
|
||||
commonModule(enableNetworkLogs = enableNetworkLogs),
|
||||
analyticsModule(),
|
||||
providersModule(),
|
||||
databaseModule()
|
||||
coreComponentModules(enableNetworkLogs),
|
||||
listOf(
|
||||
providersModule(enableNetworkLogs),
|
||||
databaseModule(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Called by IOS
|
||||
fun initKoin() = initKoin(enableNetworkLogs = false) { }
|
||||
|
||||
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()) }
|
||||
}
|
||||
|
||||
@ThreadLocal
|
||||
val globalJson = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
|
||||
// https://github.com/Kotlin/kotlinx.serialization/issues/1450
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(globalJson)
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
socketTimeoutMillis = 520_000
|
||||
requestTimeoutMillis = 360_000
|
||||
connectTimeoutMillis = 360_000
|
||||
}
|
||||
// WorkAround for Freezing
|
||||
// Use httpClient.getData / httpClient.postData Extensions
|
||||
/*install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
)
|
||||
}*/
|
||||
if (enableNetworkLogs) {
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*Client Active Throughout App's Lifetime*/
|
||||
@SharedImmutable
|
||||
val ktorHttpClient = HttpClient {}
|
||||
private fun KoinApplication.modules(vararg moduleLists: List<Module>): KoinApplication {
|
||||
return modules(moduleLists.toList().flatten())
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.di.preference.PreferenceManager
|
||||
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.providers.get
|
||||
import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.models.SpotiFlyerException
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.models.event.coroutines.flatMap
|
||||
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
|
||||
|
||||
class FetchPlatformQueryResult(
|
||||
private val gaanaProvider: GaanaProvider,
|
||||
private val spotifyProvider: SpotifyProvider,
|
||||
private val youtubeProvider: YoutubeProvider,
|
||||
private val saavnProvider: SaavnProvider,
|
||||
private val youtubeMusic: YoutubeMusic,
|
||||
private val youtubeMp3: YoutubeMp3,
|
||||
private val audioToMp3: AudioToMp3,
|
||||
val dir: Dir,
|
||||
val preferenceManager: PreferenceManager,
|
||||
val logger: Kermit
|
||||
) {
|
||||
private val db: DownloadRecordDatabaseQueries?
|
||||
get() = dir.db?.downloadRecordDatabaseQueries
|
||||
|
||||
suspend fun authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient()
|
||||
|
||||
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult, Throwable> {
|
||||
val result = when {
|
||||
// SPOTIFY
|
||||
link.contains("spotify", true) ->
|
||||
spotifyProvider.query(link)
|
||||
|
||||
// YOUTUBE
|
||||
link.contains("youtube.com", true) || link.contains("youtu.be", true) ->
|
||||
youtubeProvider.query(link)
|
||||
|
||||
// Jio Saavn
|
||||
link.contains("saavn", true) ->
|
||||
saavnProvider.query(link)
|
||||
|
||||
// GAANA
|
||||
link.contains("gaana", true) ->
|
||||
gaanaProvider.query(link)
|
||||
|
||||
else -> {
|
||||
SuspendableEvent.error(SpotiFlyerException.LinkInvalid(link))
|
||||
}
|
||||
}
|
||||
result.success {
|
||||
addToDatabaseAsync(
|
||||
link,
|
||||
it.copy() // Send a copy in order to not to freeze Result itself
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 1) Try Finding on JioSaavn (better quality upto 320KBPS)
|
||||
// 2) If Not found try finding on Youtube Music
|
||||
suspend fun findMp3DownloadLink(
|
||||
track: TrackDetails,
|
||||
preferredQuality: AudioQuality = preferenceManager.audioQuality
|
||||
): SuspendableEvent<String, Throwable> =
|
||||
if (track.videoID != null) {
|
||||
// We Already have VideoID
|
||||
when (track.source) {
|
||||
Source.JioSaavn -> {
|
||||
saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song ->
|
||||
song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findMp3Link(track, preferredQuality)
|
||||
}
|
||||
}
|
||||
Source.YouTube -> {
|
||||
youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull(), preferredQuality).flatMapError {
|
||||
logger.e("Yt1sMp3 Failed") { it.message ?: "couldn't fetch link for ${track.videoID} ,trying manual extraction" }
|
||||
youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink ->
|
||||
audioToMp3.convertToMp3(m4aLink)
|
||||
} ?: throw SpotiFlyerException.YoutubeLinkNotFound(track.videoID)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
/*We should never reach here for now*/
|
||||
findMp3Link(track, preferredQuality)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
findMp3Link(track, preferredQuality)
|
||||
}
|
||||
|
||||
private suspend fun findMp3Link(
|
||||
track: TrackDetails,
|
||||
preferredQuality: AudioQuality
|
||||
): SuspendableEvent<String, Throwable> {
|
||||
// Try Fetching Track from Jio Saavn
|
||||
return saavnProvider.findMp3SongDownloadURL(
|
||||
trackName = track.title,
|
||||
trackArtists = track.artists,
|
||||
preferredQuality = preferredQuality
|
||||
).flatMapError { saavnError ->
|
||||
logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
|
||||
// Saavn Failed, Lets Try Fetching Now From Youtube Music
|
||||
youtubeMusic.findMp3SongDownloadURLYT(track, preferredQuality).flatMapError { ytMusicError ->
|
||||
// If Both Failed Bubble the Exception Up with both StackTraces
|
||||
SuspendableEvent.error(
|
||||
SpotiFlyerException.DownloadLinkFetchFailed(
|
||||
trackName = track.title,
|
||||
ytMusicError = ytMusicError,
|
||||
jioSaavnError = saavnError
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
|
||||
GlobalScope.launch(dispatcherIO) {
|
||||
db?.add(
|
||||
result.folderType, result.title, link, result.coverUrl, result.trackList.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
expect class Picture
|
@ -1,16 +0,0 @@
|
||||
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(), get()) }
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di.providers
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.di.providers.requests.youtubeMp3.Yt1sMp3
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.corsApi
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import com.shabinder.common.models.event.coroutines.map
|
||||
import io.ktor.client.*
|
||||
|
||||
interface YoutubeMp3 : Yt1sMp3 {
|
||||
|
||||
companion object {
|
||||
operator fun invoke(
|
||||
client: HttpClient,
|
||||
logger: Kermit
|
||||
): YoutubeMp3 {
|
||||
return object : YoutubeMp3 {
|
||||
override val httpClient: HttpClient = client
|
||||
override val logger: Kermit = logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMp3DownloadLink(videoID: String, quality: AudioQuality): SuspendableEvent<String, Throwable> = getLinkFromYt1sMp3(videoID, quality).map {
|
||||
corsApi + it
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package com.shabinder.common.di.providers.requests.audioToMp3
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.models.AudioQuality
|
||||
import com.shabinder.common.models.SpotiFlyerException
|
||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
interface AudioToMp3 {
|
||||
|
||||
val client: HttpClient
|
||||
val logger: Kermit
|
||||
|
||||
companion object {
|
||||
operator fun invoke(
|
||||
client: HttpClient,
|
||||
logger: Kermit
|
||||
): AudioToMp3 {
|
||||
return object : AudioToMp3 {
|
||||
override val client: HttpClient = client
|
||||
override val logger: Kermit = logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun convertToMp3(
|
||||
URL: String,
|
||||
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
|
||||
): SuspendableEvent<String, Throwable> = SuspendableEvent {
|
||||
// Active Host ex - https://hostveryfast.onlineconverter.com/file/send
|
||||
// Convert Job Request ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
|
||||
var (activeHost, jobLink) = convertRequest(URL, audioQuality).value
|
||||
|
||||
// (jobStatus.contains("d")) == COMPLETION
|
||||
var jobStatus: String
|
||||
var retryCount = 40 // Set it to optimal level
|
||||
|
||||
do {
|
||||
jobStatus = try {
|
||||
client.get(
|
||||
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (e is ClientRequestException && e.response.status.value == 404) {
|
||||
// No Need to Retry, Host/Converter is Busy
|
||||
throw SpotiFlyerException.MP3ConversionFailed(e.message)
|
||||
}
|
||||
// Try Using New Host/Converter
|
||||
convertRequest(URL, audioQuality).value.also {
|
||||
activeHost = it.first
|
||||
jobLink = it.second
|
||||
}
|
||||
""
|
||||
}
|
||||
retryCount--
|
||||
logger.i("Job Status") { jobStatus }
|
||||
if (!jobStatus.contains("d")) delay(600) // Add Delay , to give Server Time to process audio
|
||||
} while (!jobStatus.contains("d", true) && retryCount > 0)
|
||||
|
||||
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
|
||||
}
|
||||
|
||||
/*
|
||||
* Response Link Ex : `https://www.onlineconverter.com/convert/11affb6d88d31861fe5bcd33da7b10a26c`
|
||||
* - to start the conversion
|
||||
* */
|
||||
private suspend fun convertRequest(
|
||||
URL: String,
|
||||
audioQuality: AudioQuality = AudioQuality.KBPS160,
|
||||
): SuspendableEvent<Pair<String, String>, Throwable> = SuspendableEvent {
|
||||
val activeHost by getHost()
|
||||
val convertJob = client.submitFormWithBinaryData<String>(
|
||||
url = activeHost,
|
||||
formData = formData {
|
||||
append("class", "audio")
|
||||
append("from", "audio")
|
||||
append("to", "mp3")
|
||||
append("source", "url")
|
||||
append("url", URL.replace("https:", "http:"))
|
||||
append("audio_quality", audioQuality.kbps)
|
||||
}
|
||||
) {
|
||||
headers {
|
||||
header("Host", activeHost.getHostDomain().also { logger.i("AudioToMp3 Host") { it } })
|
||||
header("Origin", "https://www.onlineconverter.com")
|
||||
header("Referer", "https://www.onlineconverter.com/")
|
||||
}
|
||||
}.run {
|
||||
// logger.d { this }
|
||||
dropLast(3) // last 3 are useless unicode char
|
||||
}
|
||||
|
||||
val job = client.get<HttpStatement>(convertJob) {
|
||||
headers {
|
||||
header("Host", "www.onlineconverter.com")
|
||||
}
|
||||
}.execute()
|
||||
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
|
||||
|
||||
Pair(activeHost, convertJob)
|
||||
}
|
||||
|
||||
// Active Host free to process conversion
|
||||
// ex - https://hostveryfast.onlineconverter.com/file/send
|
||||
private suspend fun getHost(): SuspendableEvent<String, Throwable> = SuspendableEvent {
|
||||
client.get<String>("https://www.onlineconverter.com/get/host") {
|
||||
headers {
|
||||
header("Host", "www.onlineconverter.com")
|
||||
}
|
||||
} // .also { logger.i("Active Host") { it } }
|
||||
}
|
||||
|
||||
// Extract full Domain from URL
|
||||
// ex - hostveryfast.onlineconverter.com
|
||||
private fun String.getHostDomain(): String {
|
||||
return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/")
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual data class Picture(
|
||||
var image: ImageBitmap?
|
||||
)
|
@ -1,123 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
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.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
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.khronos.webgl.Int8Array
|
||||
import org.w3c.dom.ImageBitmap
|
||||
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
private val preferenceManager: PreferenceManager,
|
||||
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||
) {
|
||||
/*init {
|
||||
createDirectories()
|
||||
}*/
|
||||
|
||||
/*
|
||||
* TODO
|
||||
* */
|
||||
actual fun fileSeparator(): String = "/"
|
||||
|
||||
actual fun imageCacheDir(): String = "TODO" +
|
||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||
|
||||
actual fun defaultDir(): String = "TODO" + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
|
||||
actual fun isPresent(path: String): Boolean = false
|
||||
|
||||
actual fun createDirectory(dirPath: String) {}
|
||||
|
||||
actual suspend fun clearCache() {}
|
||||
|
||||
actual suspend fun cacheImage(image: Any, path: String) {}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
actual suspend fun saveFileWithMetadata(
|
||||
mp3ByteArray: ByteArray,
|
||||
trackDetails: TrackDetails,
|
||||
postProcess: (track: TrackDetails) -> Unit
|
||||
) {
|
||||
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
|
||||
val albumArt = downloadFile(corsApi + trackDetails.albumArtURL)
|
||||
albumArt.collect {
|
||||
when (it) {
|
||||
is DownloadResult.Success -> {
|
||||
logger.d { "Album Art Downloaded Success" }
|
||||
val albumArtObj = js {
|
||||
this["type"] = 3
|
||||
this["data"] = it.byteArray.toArrayBuffer()
|
||||
this["description"] = "Cover Art"
|
||||
}
|
||||
writeTagsAndSave(writer, albumArtObj as Object, trackDetails)
|
||||
}
|
||||
is DownloadResult.Error -> {
|
||||
logger.d { "Album Art Downloading Error" }
|
||||
writeTagsAndSave(writer, null, trackDetails)
|
||||
}
|
||||
is DownloadResult.Progress -> logger.d { "Album Art Downloading: ${it.progress}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeTagsAndSave(writer: ID3Writer, albumArt: Object?, trackDetails: TrackDetails) {
|
||||
writer.apply {
|
||||
setFrame("TIT2", trackDetails.title)
|
||||
setFrame("TPE1", trackDetails.artists.toTypedArray())
|
||||
setFrame("TALB", trackDetails.albumName ?: "")
|
||||
try { trackDetails.year?.substring(0, 4)?.toInt()?.let { setFrame("TYER", it) } } catch (e: Exception) {}
|
||||
setFrame("TPE2", trackDetails.artists.joinToString(","))
|
||||
setFrame("WOAS", trackDetails.source.toString())
|
||||
setFrame("TLEN", trackDetails.durationSec)
|
||||
albumArt?.let { setFrame("APIC", it) }
|
||||
}
|
||||
writer.addTag()
|
||||
allTracksStatus[trackDetails.title] = DownloadStatus.Downloaded
|
||||
DownloadProgressFlow.emit(allTracksStatus)
|
||||
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
|
||||
}
|
||||
|
||||
actual fun addToLibrary(path: String) {}
|
||||
|
||||
actual suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture {
|
||||
return Picture(url)
|
||||
}
|
||||
|
||||
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
||||
|
||||
private suspend fun freshImage(url: String): ImageBitmap? = null
|
||||
|
||||
actual val db: Database? = spotiFlyerDatabase.instance
|
||||
}
|
||||
|
||||
fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
||||
return this.unsafeCast<Int8Array>().buffer
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
actual data class Picture(
|
||||
var imageUrl: String
|
||||
)
|
@ -28,6 +28,8 @@ kotlin {
|
||||
implementation(project(":common:dependency-injection"))
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:database"))
|
||||
implementation(project(":common:providers"))
|
||||
implementation(project(":common:core-components"))
|
||||
implementation(SqlDelight.coroutineExtensions)
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user