Major Module and Code Refactoring

This commit is contained in:
shabinder 2021-08-23 22:46:20 +05:30
parent 04dbff4d7f
commit 59068c6c8b
149 changed files with 1521 additions and 1905 deletions

View File

@ -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)

View File

@ -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,10 +277,12 @@ class MainActivity : ComponentActivity() {
}
override fun sendTracksToService(array: List<TrackDetails>) {
for (chunk in array.chunked(25)) {
if (foregroundService == null) initForegroundService()
foregroundService?.downloadAllTracks(array)
}
}
}
override fun showPopUpMessage(string: String, long: Boolean) =
this@MainActivity.showPopUpMessage(string, long)
@ -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)

View File

@ -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)
},

View File

@ -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"
}
}

View File

@ -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"))

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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
)
}
}

View 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"))
}
}
}
}

View 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>

View File

@ -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

View File

@ -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

View File

@ -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) +
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 {
// Add Mp3 Tags and Add to Library
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
} catch (e: Exception) { e.printStackTrace() }
}
}
} 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, 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) = 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
}

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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,

View File

@ -0,0 +1,7 @@
package com.shabinder.common.core_components.picture
import androidx.compose.ui.graphics.ImageBitmap
actual data class Picture(
var image: ImageBitmap?
)

View File

@ -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()) }
}

View File

@ -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

View File

@ -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() {

View File

@ -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)

View File

@ -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,12 +79,20 @@ 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() } +
if (subFolder.isEmpty()) "" else {
removeIllegalChars(subFolder) + this.fileSeparator()
} +
removeIllegalChars(itemName) + extension
/*DIR Specific Operation End*/

View File

@ -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

View File

@ -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() }
}
else
repeat(-change) { killQueue.trySend(Unit).isSuccess }
}

View File

@ -0,0 +1,3 @@
package com.shabinder.common.core_components.picture
expect class Picture

View File

@ -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

View File

@ -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 {}

View File

@ -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

View File

@ -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();
}

View File

@ -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") +
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() +
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)
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 {
// Add Mp3 Tags and Add to Library
Mp3File(File(songFile.absolutePath))
.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()
// 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, 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(

View File

@ -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
}

View File

@ -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"))
}

View File

@ -0,0 +1,7 @@
package com.shabinder.common.core_components.picture
import androidx.compose.ui.graphics.ImageBitmap
actual data class Picture(
var image: ImageBitmap?
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package com.shabinder.common.core_components.picture
actual data class Picture(
var imageUrl: String
)

View File

@ -57,8 +57,5 @@ kotlin {
api(Internationalization.dep)
}
}
androidMain {
dependencies {}
}
}
}

View File

@ -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

View File

@ -1,3 +0,0 @@
package com.shabinder.common
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)

View File

@ -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
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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," +
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 "
) : SpotiFlyerException(message)
): this(errorTrace)
}
data class LinkInvalid(
val link: String? = null,

View File

@ -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())
}

View File

@ -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()
}

View File

@ -1,4 +1,4 @@
package com.shabinder.common.di.utils
package com.shabinder.common.utils
/*
* JSON UTILS

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
package com.shabinder.common.models
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"))
}
}
}

View File

@ -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()) }
private fun KoinApplication.modules(vararg moduleLists: List<Module>): KoinApplication {
return modules(moduleLists.toList().flatten())
}
@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 {}

View File

@ -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()
)
}
}
}

View File

@ -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

View File

@ -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()) }
}

View File

@ -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
}
}

View File

@ -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("/")
}
}

View File

@ -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?
)

View File

@ -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
}

View File

@ -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
)

View File

@ -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