diff --git a/android/build.gradle.kts b/android/build.gradle.kts index f37792e1..4a88c08c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -16,7 +16,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.compose.compose -import org.jetbrains.kotlin.kapt.cli.main plugins { id("com.android.application") @@ -79,6 +78,9 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } + packagingOptions { + exclude("META-INF/*") + } configurations { "implementation" { exclude(group = "androidx.compose.animation") @@ -92,7 +94,7 @@ android { dependencies { implementation(compose.material) implementation(compose.materialIconsExtended) - implementation(Androidx.androidxActivity) + implementation(deps.androidx.activity) // Project's SubModules implementation(project(":common:database")) @@ -103,43 +105,40 @@ dependencies { implementation(project(":common:core-components")) implementation(project(":common:providers")) - // Koin - implementation(Koin.android) - implementation(Koin.compose) + with(deps) { - // DECOMPOSE - implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) + // Koin + with(koin) { + implementation(androidx.compose) + implementation(android) + } - // MVI - implementation(MVIKotlin.mvikotlin) - implementation(MVIKotlin.mvikotlinMain) - implementation(MVIKotlin.mvikotlinLogging) - implementation(MVIKotlin.mvikotlinTimeTravel) + // DECOMPOSE + with(decompose) { + implementation(dep) + implementation(extensions.compose) + } - // Extras - with(Extras.Android) { - implementation(countly) - implementation(appUpdator) + implementation(countly.android) + implementation(android.app.notifier) + implementation(storage.chooser) + + with(bundles) { + implementation(ktor) + implementation(mviKotlin) + implementation(androidx.lifecycle) + implementation(accompanist.inset) + } + + // Test + testImplementation(junit) + androidTestImplementation(androidx.junit) + androidTestImplementation(androidx.expresso) + + // Desugar + coreLibraryDesugaring(androidx.desugar) + + // Debug + debugImplementation(leak.canary) } - - with(Versions.androidxLifecycle) { - implementation("androidx.lifecycle:lifecycle-service:$this") - implementation("androidx.lifecycle:lifecycle-common-java8:$this") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:$this") - } - - implementation(Extras.kermit) - // implementation("com.jakewharton.timber:timber:4.7.1") - implementation("dev.icerock.moko:parcelize:${Versions.mokoParcelize}") - implementation("com.github.shabinder:storage-chooser:2.0.4.45") - implementation("com.google.accompanist:accompanist-insets:0.16.1") - - // Test - testImplementation("junit:junit:4.13.2") - androidTestImplementation(Androidx.junit) - androidTestImplementation(Androidx.expresso) - - // Desugaring - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 847dc6dc..00bda4ce 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index b66853ec..d0eb2034 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -43,6 +43,7 @@ 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 io.ktor.client.HttpClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -62,6 +63,7 @@ class ForegroundService : LifecycleService() { private val fetcher: FetchPlatformQueryResult by inject() private val logger: Kermit by inject() private val dir: FileManager by inject() + private val httpClient: HttpClient by inject() private var messageList = java.util.Collections.synchronizedList(MutableList(5) { emptyMessage }) @@ -170,7 +172,7 @@ class ForegroundService : LifecycleService() { trackStatusFlowMap[track.title] = DownloadStatus.Downloading() // Enqueueing Download - downloadFile(url).collect { + httpClient.downloadFile(url).collect { when (it) { is DownloadResult.Error -> { logger.d(TAG) { it.message } diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt index fdd75530..06b015b2 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt @@ -15,12 +15,19 @@ fun cleanFiles(dir: File) { if (file.isDirectory) { cleanFiles(file) } else if (file.isFile) { - if (file.path.toString().substringAfterLast(".") != "mp3") { - Log.d("Files Cleaning", "Cleaning ${file.path}") + val filePath = file.path.toString() + if (filePath.substringAfterLast(".") != "mp3" || filePath.isTempFile()) { + Log.d("Files Cleaning", "Cleaning $filePath") file.delete() } } } } - } catch (e: Exception) { e.printStackTrace() } + } catch (e: Exception) { + e.printStackTrace() + } +} + +private fun String.isTempFile(): Boolean { + return substringBeforeLast(".").takeLast(5) == ".temp" } diff --git a/art/SpotiFlyer.png b/art/SpotiFlyer.png new file mode 100644 index 00000000..21c6153b Binary files /dev/null and b/art/SpotiFlyer.png differ diff --git a/build.gradle.kts b/build.gradle.kts index d775fc1f..c6ff0025 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,22 +38,32 @@ allprojects { tasks.withType>().configureEach { dependsOn(":common:data-models:generateI18n4kFiles") kotlinOptions { - if(this is org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions) { + if (this is org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions) { jvmTarget = "1.8" } freeCompilerArgs = (freeCompilerArgs + listOf("-Xopt-in=kotlin.RequiresOptIn")) } } - - afterEvaluate { - project.extensions.findByType()?.let { kmpExt -> - kmpExt.sourceSets.run { - all { - languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") - languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + configurations.all { + resolutionStrategy { + eachDependency { + if (requested.group == "org.jetbrains.kotlin") { + @Suppress("UnstableApiUsage") + useVersion(deps.kotlin.kotlinGradlePlugin.get().versionConstraint.requiredVersion) } - removeAll { it.name == "androidAndroidTestRelease" } } } } + afterEvaluate { + project.extensions.findByType() + ?.let { kmpExt -> + kmpExt.sourceSets.run { + all { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + } + removeAll { it.name == "androidAndroidTestRelease" } + } + } + } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 33d0caf4..7c635940 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -30,17 +30,20 @@ repositories { } dependencies { - implementation(Androidx.gradlePlugin) - implementation(JetBrains.Compose.gradlePlugin) - implementation(JetBrains.Kotlin.gradlePlugin) - implementation(JetBrains.Kotlin.serialization) - implementation(SqlDelight.gradlePlugin) - implementation(KTLint.gradlePlugin) - implementation(Internationalization.gradlePlugin) - implementation(Mosaic.gradlePlugin) + with(deps) { + implementation(androidx.gradle.plugin) + implementation(kotlin.compose.gradle) + implementation(ktlint.gradle) + implementation(mosaic.gradle) + implementation(kotlin.kotlinGradlePlugin) + implementation(sqldelight.gradle.plugin) + implementation(i18n4k.gradle.plugin) + implementation(kotlin.serialization) + } } kotlin { // Add Deps to compilation, so it will become available in main project sourceSets.getByName("main").kotlin.srcDir("buildSrc/src/main/kotlin") } + diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index f837ff4b..9de83bc6 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -14,11 +14,15 @@ * * along with this program. If not, see . */ -@file:Suppress("MayBeConstant", "SpellCheckingInspection") +@file:Suppress("MayBeConstant", "SpellCheckingInspection", "UnstableApiUsage") +import org.gradle.api.Project import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.kotlin.dsl.accessors.runtime.addDependencyTo +import org.gradle.kotlin.dsl.getByType object Versions { // App's Version (To be bumped at each update) @@ -26,44 +30,10 @@ object Versions { const val versionCode = 26 - // Kotlin - const val kotlinVersion = "1.5.21" - - const val coroutinesVersion = "1.5.1" - - // Code Formatting - const val ktLint = "10.1.0" - - // Console-App UI - const val mosaic = "0.1.0" - - // DI - const val koin = "3.1.2" - - // Logger - const val kermit = "0.1.9" - - const val mokoParcelize = "0.7.1" - - // Internet - const val ktor = "1.6.2" - - const val kotlinxSerialization = "1.2.2" - - // Database - const val sqlDelight = "1.5.1" - - const val sqliteJdbcDriver = "3.34.0" - const val slf4j = "1.7.31" - - // Internationalisation - const val i18n4k = "0.1.3" - // Android const val minSdkVersion = 21 - const val compileSdkVersion = 30 + const val compileSdkVersion = 31 const val targetSdkVersion = 29 - const val androidxLifecycle = "2.4.0-alpha03" } object HostOS { @@ -74,143 +44,46 @@ object HostOS { val isLinux = hostOs.startsWith("Linux", true) } -object MultiPlatformSettings { - const val dep = "com.russhwolf:multiplatform-settings-no-arg:0.7.7" -} +val Project.Deps: VersionCatalog get() = project.extensions.getByType().named("deps") -object KotlinJSWrappers { - private const val bomVersion = "0.0.1-pre.235-kotlin-1.5.21" - val bom = "org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:${bomVersion}" - const val kotlinReact = "org.jetbrains.kotlin-wrappers:kotlin-react" - const val kotlinReactDom = "org.jetbrains.kotlin-wrappers:kotlin-react-dom" - const val kotlinStyled = "org.jetbrains.kotlin-wrappers:kotlin-styled" -} +val VersionCatalog.ktorBundle get() = findBundle("ktor").get() +val VersionCatalog.statelyBundle get() = findBundle("stately").get() +val VersionCatalog.androidXLifecycleBundle get() = findBundle("androidx-lifecycle").get() +val VersionCatalog.androidXCommonBundle get() = findBundle("androidx-common").get() +val VersionCatalog.kotlinTestBundle get() = findBundle("kotlin-test").get() +val VersionCatalog.sqldelightBundle get() = findBundle("sqldelight").get() +val VersionCatalog.mviKotlinBundle get() = findBundle("mviKotlin").get() +val VersionCatalog.essentyBundle get() = findBundle("essenty").get() +val VersionCatalog.koinAndroidBundle get() = findBundle("koin-android").get() +val VersionCatalog.kotlinJSWrappers get() = findBundle("kotlin-js-wrappers").get() -object Koin { - val core = "io.insert-koin:koin-core:${Versions.koin}" - val test = "io.insert-koin:koin-test:${Versions.koin}" - val android = "io.insert-koin:koin-android:${Versions.koin}" - val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}" -} +val VersionCatalog.kotlinJunitTest get() = findDependency("kotlin-kotlinTestJunit").get() +val VersionCatalog.kotlinJSTest get() = findDependency("kotlin-kotlinTestJs").get() +val VersionCatalog.kermit get() = findDependency("kermit").get() +val VersionCatalog.decompose get() = findDependency("decompose-dep").get() +val VersionCatalog.decomposeComposeExt get() = findDependency("decompose-extensions-compose").get() +val VersionCatalog.jaffree get() = findDependency("jaffree").get() -object Androidx { - const val androidxActivity = "androidx.activity:activity-compose:1.3.1" - const val core = "androidx.core:core-ktx:1.6.0" - const val palette = "androidx.palette:palette-ktx:1.0.0" - const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutinesVersion}" +val VersionCatalog.ktlintGradle get() = findDependency("ktlint-gradle").get() +val VersionCatalog.androidGradle get() = findDependency("androidx-gradle-plugin").get() +val VersionCatalog.mosaicGradle get() = findDependency("mosaic-gradle").get() +val VersionCatalog.kotlinComposeGradle get() = findDependency("kotlin-compose-gradle").get() +val VersionCatalog.kotlinGradle get() = findDependency("kotlin-kotlinGradlePlugin").get() +val VersionCatalog.i18n4kGradle get() = findDependency("i18n4k-gradle-plugin").get() +val VersionCatalog.sqlDelightGradle get() = findDependency("sqldelight-gradle-plugin").get() +val VersionCatalog.kotlinSerializationPlugin get() = findDependency("kotlin-serialization").get() - const val junit = "androidx.test.ext:junit:1.1.2" - const val expresso = "androidx.test.espresso:espresso-core:3.3.0" +val VersionCatalog.koinCore get() = findDependency("koin-core").get() +val VersionCatalog.kotlinCoroutines get() = findDependency("kotlin-coroutines").get() +val VersionCatalog.kotlinxSerialization get() = findDependency("kotlinx-serialization-json").get() +val VersionCatalog.ktorClientIOS get() = findDependency("ktor-client-ios").get() +val VersionCatalog.ktorClientAndroid get() = findDependency("ktor-client-android").get() +val VersionCatalog.ktorClientApache get() = findDependency("ktor-client-apache").get() +val VersionCatalog.ktorClientJS get() = findDependency("ktor-client-js").get() +val VersionCatalog.ktorClientCIO get() = findDependency("ktor-client-cio").get() +val VersionCatalog.slf4j get() = findDependency("slf4j-simple").get() - const val gradlePlugin = "com.android.tools.build:gradle:7.0.1" -} - -object KTLint { - const val gradlePlugin = "org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}" -} - -object JetBrains { - object Kotlin { - const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt" - const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlinVersion}" - const val serialization = "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlinVersion}" - const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:${Versions.kotlinVersion}" - const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlinVersion}" - const val testAnnotationsCommon = - "org.jetbrains.kotlin:kotlin-test-annotations-common:${Versions.kotlinVersion}" - } - - object Compose { - // __LATEST_COMPOSE_RELEASE_VERSION__ - private const val VERSION = "1.0.0-alpha3" - const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION" - } -} - -object Mosaic { - const val gradlePlugin = "com.jakewharton.mosaic:mosaic-gradle-plugin:${Versions.mosaic}" -} - -object Decompose { - private const val VERSION = "0.3.1" - const val decompose = "com.arkivanov.decompose:decompose:$VERSION" - const val decomposeIosX64 = "com.arkivanov.decompose:decompose-iosx64:$VERSION" - const val decomposeIosArm64 = "com.arkivanov.decompose:decompose-iosarm64:$VERSION" - const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION" -} - -object MVIKotlin { - private const val VERSION = "2.0.4" - const val rx = "com.arkivanov.mvikotlin:rx:$VERSION" - const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION" - const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION" - const val coroutines = "com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:$VERSION" - const val keepers = "com.arkivanov.mvikotlin:keepers:$VERSION" - const val mvikotlinMainIosX64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosx64:$VERSION" - const val mvikotlinMainIosArm64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosarm64:$VERSION" - const val mvikotlinLogging = "com.arkivanov.mvikotlin:mvikotlin-logging:$VERSION" - const val mvikotlinTimeTravel = "com.arkivanov.mvikotlin:mvikotlin-timetravel:$VERSION" - const val mvikotlinExtensionsReaktive = "com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:$VERSION" -} - -object Ktor { - val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}" - val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}" - val clientLogging = "io.ktor:ktor-client-logging:${Versions.ktor}" - val clientSerialization = "io.ktor:ktor-client-serialization:${Versions.ktor}" - - val auth = "io.ktor:ktor-client-auth:${Versions.ktor}" - val clientAndroid = "io.ktor:ktor-client-android:${Versions.ktor}" - val clientCurl = "io.ktor:ktor-client-curl:${Versions.ktor}" - val clientApache = "io.ktor:ktor-client-apache:${Versions.ktor}" - val slf4j = "org.slf4j:slf4j-simple:${Versions.slf4j}" - val clientIos = "io.ktor:ktor-client-ios:${Versions.ktor}" - val clientCio = "io.ktor:ktor-client-cio:${Versions.ktor}" - val clientJs = "io.ktor:ktor-client-js:${Versions.ktor}" -} - -object Internationalization { - const val dep = "de.comahe.i18n4k:i18n4k-core:${Versions.i18n4k}" - const val gradlePlugin = "de.comahe.i18n4k:i18n4k-gradle-plugin:${Versions.i18n4k}" -} - -object Extras { - const val youtubeDownloader = "io.github.shabinder:youtube-api-dl:1.3" - const val fuzzyWuzzy = "io.github.shabinder:fuzzywuzzy:1.1" - const val mp3agic = "com.mpatric:mp3agic:0.9.0" - const val jaudioTagger = "com.github.Shabinder:JAudioTagger-Android:1.0" - const val kermit = "co.touchlab:kermit:${Versions.kermit}" - - object Android { - // Self Hosted Analytics & Crashlytics (FOSS) - val countly = "ly.count.android:sdk:20.11.8" - val appUpdator = "com.github.amitbd1508:AppUpdater:4.1.0" - } - - object Desktop { - val countly = "ly.count.sdk:java:20.11.0" - } -} - -object Serialization { - val json = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}" -} - -object SqlDelight { - val runtime = "com.squareup.sqldelight:runtime:${Versions.sqlDelight}" - val coroutineExtensions = "com.squareup.sqldelight:coroutines-extensions:${Versions.sqlDelight}" - - const val gradlePlugin = "com.squareup.sqldelight:gradle-plugin:${Versions.sqlDelight}" - const val androidDriver = "com.squareup.sqldelight:android-driver:${Versions.sqlDelight}" - const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:${Versions.sqlDelight}" - const val nativeDriver = "com.squareup.sqldelight:native-driver:${Versions.sqlDelight}" - val nativeDriverMacos = "com.squareup.sqldelight:native-driver-macosx64:${Versions.sqlDelight}" - val jdbcDriver = "org.xerial:sqlite-jdbc:${Versions.sqliteJdbcDriver}" -} - -fun DependencyHandler.`implementation`( - dependencyNotation: String, - dependencyConfiguration: ExternalModuleDependency.() -> Unit -): ExternalModuleDependency = addDependencyTo( - this, "implementation", dependencyNotation -) { dependencyConfiguration() } +val VersionCatalog.sqlDelightJDBC get() = findDependency("sqlite-jdbc-driver").get() +val VersionCatalog.sqlDelightNative get() = findDependency("sqldelight-native-driver").get() +val VersionCatalog.sqlDelightAndroid get() = findDependency("sqldelight-android-driver").get() +val VersionCatalog.sqlDelightDriver get() = findDependency("sqldelight-driver").get() diff --git a/buildSrc/deps.versions.toml b/buildSrc/deps.versions.toml new file mode 100644 index 00000000..a3507216 --- /dev/null +++ b/buildSrc/deps.versions.toml @@ -0,0 +1,135 @@ +[versions] +kotlin = "1.5.31" +androidCoroutines = "1.5.1" +ktLint = "10.1.0" +mosaic = "0.1.0" +koin = "3.1.2" +kermit = "0.1.9" +mokoParcelize = "0.7.1" +ktor = "1.6.3" +kotlinxSerialization = "1.3.0" +sqlDelight = "1.5.1" +sqliteJdbcDriver = "3.34.0" +slf4j = "1.7.31" +i18n4k = "0.1.3" +essenty = "0.1.3" +multiplatformSettings = "0.7.7" +decompose = "0.3.1" +mviKotlin = "2.0.4" +accompanist = "0.18.0" +statelyVersion = "1.1.10" +statelyIsoVersion = "1.2.0-nmm" +androidxLifecycle = "2.4.0-alpha03" + + +[libraries] +kotlin-kotlinGradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } +kotlin-kotlinTestCommon = { group = "org.jetbrains.kotlin", name = "kotlin-test-common", version.ref = "kotlin" } +kotlin-kotlinTestJs = { group = "org.jetbrains.kotlin", name = "kotlin-test-js", version.ref = "kotlin" } +kotlin-kotlinTestJunit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" } +kotlin-kotlinTestAnnotationsCommon = { group = "org.jetbrains.kotlin", name = "kotlin-test-annotations-common", version.ref = "kotlin" } +kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.5.2-native-mt" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +kotlinx-atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu", version = "0.16.3" } +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.2.1" } + +kotlin-compose-gradle = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version = "1.0.0-alpha4-build366" } +mosaic-gradle = { group = "com.jakewharton.mosaic", name = "mosaic-gradle-plugin", version.ref = "mosaic" } + +essenty-lifecycle = { group = "com.arkivanov.essenty", name = "lifecycle", version.ref = "essenty" } +essenty-instanceKeeper = { group = "com.arkivanov.essenty", name = "instance-keeper", version.ref = "essenty" } + +decompose-dep = { group = "com.arkivanov.decompose", name = "decompose", version.ref = "decompose" } +decompose-extensions-compose = { group = "com.arkivanov.decompose", name = "extensions-compose-jetbrains", version.ref = "decompose" } + +mviKotlin-dep = { group = "com.arkivanov.mvikotlin", name = "mvikotlin", version.ref = "mviKotlin" } +mviKotlin-rx = { group = "com.arkivanov.mvikotlin", name = "rx", version.ref = "mviKotlin" } +mviKotlin-main = { group = "com.arkivanov.mvikotlin", name = "mvikotlin-main", version.ref = "mviKotlin" } +mviKotlin-coroutines = { group = "com.arkivanov.mvikotlin", name = "mvikotlin-extensions-coroutines", version.ref = "mviKotlin" } +mviKotlin-keepers = { group = "com.arkivanov.mvikotlin", name = "keepers", version.ref = "mviKotlin" } +mviKotlin-logging = { group = "com.arkivanov.mvikotlin", name = "mvikotlin-logging", version.ref = "mviKotlin" } +mviKotlin-timetravel = { group = "com.arkivanov.mvikotlin", name = "mvikotlin-timetravel", version.ref = "mviKotlin" } +mviKotlin-extensions-reaktive = { group = "com.arkivanov.mvikotlin", name = "mvikotlin-extensions-reaktive", version.ref = "mviKotlin" } + +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-json = { group = "io.ktor", name = "ktor-client-json", version.ref = "ktor" } +ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } +ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" } +ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } +ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } +ktor-client-curl = { group = "io.ktor", name = "ktor-client-curl", version.ref = "ktor" } +ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache", version.ref = "ktor" } +ktor-client-ios = { group = "io.ktor", name = "ktor-client-ios", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } +ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +i18n4k-core = { group = "de.comahe.i18n4k", name = "i18n4k-core", version.ref = "i18n4k" } +i18n4k-gradle-plugin = { group = "de.comahe.i18n4k", name = "i18n4k-gradle-plugin", version.ref = "i18n4k" } + +youtube-downloader = { group = "io.github.shabinder", name = "youtube-api-dl", version = "1.3" } +fuzzy-wuzzy = { group = "io.github.shabinder", name = "fuzzywuzzy", version = "1.1" } +mp3agic = { group = "com.mpatric", name = "mp3agic", version = "0.9.0" } +kermit = { group = "co.touchlab", name = "kermit", version.ref = "kermit" } +storage-chooser = { group = "com.github.shabinder", name = "storage-chooser", version = "2.0.4.45" } +accompanist-inset = { group = "com.google.accompanist", name = "accompanist-insets", version.ref = "accompanist" } +android-app-notifier = { group = "com.github.amitbd1508", name = "AppUpdater", version = "4.1.0" } +moko-parcelize = { group = "dev.icerock.moko", name = "parcelize", version.ref = "mokoParcelize" } +jaffree = { group = "com.github.kokorin.jaffree", name = "jaffree", version = "2021.08.16" } +multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } + +countly-android = { group = "ly.count.android", name = "sdk", version = "20.11.8" } +countly-desktop = { group = "ly.count.sdk", name = "java", version = "20.11.0" } + +stately-common = { group = "co.touchlab", name = "stately-common", version.ref = "statelyVersion" } +stately-concurrency = { group = "co.touchlab", name = "stately-concurrency", version.ref = "statelyVersion" } +stately-isolate = { group = "co.touchlab", name = "stately-isolate", version.ref = "statelyIsoVersion" } +stately-iso-collections = { group = "co.touchlab", name = "stately-iso-collections", version.ref = "statelyIsoVersion" } + +sqldelight-runtime = { group = "com.squareup.sqldelight", name = "runtime", version.ref = "sqlDelight" } +sqldelight-coroutines-extension = { group = "com.squareup.sqldelight", name = "coroutines-extensions", version.ref = "sqlDelight" } +sqldelight-gradle-plugin = { group = "com.squareup.sqldelight", name = "gradle-plugin", version.ref = "sqlDelight" } +sqldelight-driver = { group = "com.squareup.sqldelight", name = "sqlite-driver", version.ref = "sqlDelight" } +sqldelight-android-driver = { group = "com.squareup.sqldelight", name = "android-driver", version.ref = "sqlDelight" } +sqldelight-native-driver = { group = "com.squareup.sqldelight", name = "native-driver", version.ref = "sqlDelight" } +sqlite-jdbc-driver = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqliteJdbcDriver" } + +koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } + +kotlin-js-wrappers-react = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-react", version = "17.0.2-pre.251-kotlin-1.5.31" } +kotlin-js-wrappers-reactDom = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-react-dom", version = "17.0.2-pre.251-kotlin-1.5.31" } +kotlin-js-wrappers-styled = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-styled", version = "5.3.1-pre.250-kotlin-1.5.31" } +kotlin-js-wrappers-ext = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-extensions", version = "1.0.1-pre.251-kotlin-1.5.31" } + +androidx-activity = { group = "androidx.activity", name = "activity-compose", version = "1.3.1" } +androidx-core = { group = "androidx.core", name = "core-ktx", version = "1.6.0" } +androidx-palette = { group = "androidx.palette", name = "palette-ktx", version = "1.0.0" } +androidx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "androidCoroutines" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version = "1.1.2" } +androidx-expresso = { group = "androidx.test.espresso", name = "espresso-core", version = "3.3.0" } +androidx-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version = "4.2.2" } +androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "androidxLifecycle" } +androidx-lifecycle-common = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +androidx-desugar = { group = "com.android.tools", name = "desugar_jdk_libs", version = "1.1.5" } +leak-canary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.7" } +junit = { group = "junit", name = "junit", version = "4.13.2" } + +ktlint-gradle = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktLint" } + +[bundles] +ktor = ["ktor-client-core","ktor-client-json","ktor-client-auth","ktor-client-logging","ktor-client-serialization"] +stately = ["stately-common","stately-concurrency","stately-isolate","stately-iso-collections"] +androidx-lifecycle = ["androidx-lifecycle-service","androidx-lifecycle-common","androidx-lifecycle-runtime"] +androidx-common = ["androidx-activity","androidx-core"] +kotlin-test = ["kotlin-kotlinTestCommon","kotlin-kotlinTestAnnotationsCommon"] +sqldelight = ["sqldelight-runtime","sqldelight-coroutines-extension","sqldelight-driver"] +mviKotlin = ["mviKotlin-dep","mviKotlin-main","mviKotlin-coroutines","mviKotlin-logging","mviKotlin-timetravel"] +kotlinCommon = ["kotlin-coroutines", "kotlin-serialization", "kotlinx-serialization-json", "kotlinx-atomicfu"] +essenty = ["essenty-lifecycle","essenty-instanceKeeper"] +koin-android = ["koin-androidx-compose","koin-android"] +kotlin-js-wrappers = ["kotlin-js-wrappers-react","kotlin-js-wrappers-reactDom","kotlin-js-wrappers-styled","kotlin-js-wrappers-ext"] \ No newline at end of file diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..72f011b1 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,12 @@ + +enableFeaturePreview("VERSION_CATALOGS") +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + versionCatalogs { + create("deps") { + from(files("deps.versions.toml")) + } + } +} + +rootProject.name = "spotiflyer-build" diff --git a/buildSrc/src/main/kotlin/compiler-args.gradle.kts b/buildSrc/src/main/kotlin/compiler-args.gradle.kts index 39907c63..34c5b8f5 100644 --- a/buildSrc/src/main/kotlin/compiler-args.gradle.kts +++ b/buildSrc/src/main/kotlin/compiler-args.gradle.kts @@ -6,10 +6,10 @@ kotlin { sourceSets { all { languageSettings.apply { - useExperimentalAnnotation("kotlin.RequiresOptIn") - useExperimentalAnnotation("kotlin.Experimental") - useExperimentalAnnotation("kotlin.time.ExperimentalTime") - useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + optIn("kotlin.RequiresOptIn") + optIn("kotlin.Experimental") + optIn("kotlin.time.ExperimentalTime") + optIn("kotlinx.serialization.ExperimentalSerializationApi") } } } diff --git a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts index 8b324515..34dea94a 100644 --- a/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts @@ -29,36 +29,23 @@ kotlin { sourceSets { all { languageSettings.apply { - useExperimentalAnnotation("androidx.compose.animation") + optIn("androidx.compose.animation") } } named("commonMain") { dependencies { - // Decompose - implementation(Decompose.decompose) - - // MVI - implementation(MVIKotlin.coroutines) - implementation(MVIKotlin.mvikotlin) - implementation(compose.ui) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) implementation(compose.animation) - - implementation(Extras.kermit) - implementation("dev.icerock.moko:parcelize:${Versions.mokoParcelize}") - implementation(JetBrains.Kotlin.coroutines) { - @Suppress("DEPRECATION") - isForce = true - } + implementation(Deps.kotlinCoroutines) + implementation(Deps.decompose) } } named("androidMain") { dependencies { - implementation(Androidx.androidxActivity) - implementation(Androidx.core) + implementation(Deps.androidXCommonBundle) } } named("desktopMain") { diff --git a/buildSrc/src/main/kotlin/multiplatform-setup-test.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-setup-test.gradle.kts index c4a4889f..8f2e8408 100644 --- a/buildSrc/src/main/kotlin/multiplatform-setup-test.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-setup-test.gradle.kts @@ -42,23 +42,24 @@ kotlin { sourceSets { named("commonTest") { dependencies { - implementation(JetBrains.Kotlin.testCommon) - implementation(JetBrains.Kotlin.testAnnotationsCommon) + implementation(Deps.kotlinTestBundle) } } named("androidTest") { dependencies { - implementation(JetBrains.Kotlin.testJunit) + implementation(Deps.kotlinJunitTest) } } named("desktopTest") { dependencies { - implementation(JetBrains.Kotlin.testJunit) + implementation(Deps.kotlinJunitTest) } } named("jsTest") { - dependencies {} + dependencies { + implementation(Deps.kotlinJSTest) + } } } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts index 43462d25..54b92e72 100644 --- a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts @@ -25,7 +25,7 @@ plugins { kotlin { /*IOS Target Can be only built on Mac*/ - if(HostOS.isMac){ + if (HostOS.isMac) { val sdkName: String? = System.getenv("SDK_NAME") val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos") if (isiOSDevice) { @@ -50,45 +50,25 @@ kotlin { sourceSets { named("commonMain") { dependencies { - // Decompose - implementation(Decompose.decompose) - - // MVI - implementation(MVIKotlin.coroutines) - implementation(MVIKotlin.mvikotlin) - - // Koin - implementation(Koin.core) - - implementation(Ktor.auth) - implementation(Ktor.clientJson) - implementation(Ktor.clientCore) - implementation(Ktor.clientLogging) - implementation(Ktor.clientSerialization) - - // Extras - implementation(Extras.kermit) - implementation(Serialization.json) - implementation("co.touchlab:stately-common:1.1.7") - implementation("dev.icerock.moko:parcelize:${Versions.mokoParcelize}") - implementation(JetBrains.Kotlin.coroutines) { - @Suppress("DEPRECATION") - isForce = true - } + implementation(Deps.ktorBundle) + implementation(Deps.kotlinxSerialization) + implementation(Deps.kotlinCoroutines) + implementation(Deps.mviKotlinBundle) + implementation(Deps.decompose) + implementation(Deps.koinCore) } } named("androidMain") { dependencies { - implementation(Androidx.androidxActivity) - implementation(Androidx.core) implementation(compose.runtime) implementation(compose.material) implementation(compose.foundation) implementation(compose.materialIconsExtended) - implementation(Decompose.extensionsCompose) - implementation(Ktor.clientAndroid) - implementation(Koin.android) + implementation(Deps.androidXCommonBundle) + implementation(Deps.decomposeComposeExt) + implementation(Deps.ktorClientAndroid) + implementation(Deps.koinAndroidBundle) } } @@ -99,27 +79,20 @@ kotlin { implementation(compose.material) implementation(compose.desktop.common) implementation(compose.materialIconsExtended) - implementation(Decompose.extensionsCompose) - implementation(Ktor.clientApache) - implementation(Ktor.slf4j) + implementation(Deps.decomposeComposeExt) + implementation(Deps.ktorClientApache) + implementation(Deps.slf4j) } } named("jsMain") { dependencies { - implementation(Ktor.clientJs) - - /*with(KotlinJSWrappers) { - implementation(enforcedPlatform(bom)) - implementation(kotlinReact) - implementation(kotlinReactDom) - implementation(kotlinStyled) - }*/ + implementation(Deps.ktorClientJS) } } - if(HostOS.isMac){ - named("iosMain"){ + if (HostOS.isMac) { + named("iosMain") { dependencies { - implementation(Ktor.clientIos) + implementation(Deps.ktorClientIOS) } } } diff --git a/common/compose/build.gradle.kts b/common/compose/build.gradle.kts index c47e194a..ed4df484 100644 --- a/common/compose/build.gradle.kts +++ b/common/compose/build.gradle.kts @@ -40,7 +40,7 @@ kotlin { implementation(project(":common:database")) implementation(project(":common:data-models")) implementation(project(":common:dependency-injection")) - implementation(Decompose.extensionsCompose) + implementation(deps.decompose.extensions.compose) } } } diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt index e56a099e..a9eb45ce 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt @@ -81,6 +81,9 @@ actual fun SpotifyLogo() = getCachedPainter(R.drawable.ic_spotify_logo) @Composable actual fun SaavnLogo() = getCachedPainter(R.drawable.ic_jio_saavn_logo) +@Composable +actual fun SoundCloudLogo() = getCachedPainter(R.drawable.ic_soundcloud) + @Composable actual fun GaanaLogo() = getCachedPainter(R.drawable.ic_gaana) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt index 6be14625..557bfb49 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/ExpectImages.kt @@ -58,6 +58,9 @@ expect fun SpotifyLogo(): Painter @Composable expect fun SaavnLogo(): Painter +@Composable +expect fun SoundCloudLogo(): Painter + @Composable expect fun YoutubeLogo(): Painter diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerMainUi.kt index 670106fb..eab0877b 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerMainUi.kt @@ -83,12 +83,14 @@ import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain.HomeCategory import com.shabinder.common.models.DownloadRecord import com.shabinder.common.models.Actions +import com.shabinder.common.models.spotify.Source import com.shabinder.common.translations.Strings import com.shabinder.common.uikit.GaanaLogo import com.shabinder.common.uikit.GithubLogo import com.shabinder.common.uikit.ImageLoad import com.shabinder.common.uikit.SaavnLogo import com.shabinder.common.uikit.ShareImage +import com.shabinder.common.uikit.SoundCloudLogo import com.shabinder.common.uikit.SpotifyLogo import com.shabinder.common.uikit.VerticalScrollbar import com.shabinder.common.uikit.YoutubeLogo @@ -319,6 +321,17 @@ fun AboutColumn( ) ) } + Spacer(modifier = Modifier.padding(top = 8.dp)) + Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) { + Icon( + SoundCloudLogo(), + "${Strings.open()} Sound Cloud", + tint = Color.Unspecified, + modifier = Modifier.clip(SpotiFlyerShapes.medium).clickable( + onClick = { Actions.instance.openPlatform("com.soundcloud.android", "https://soundcloud.com/") } + ) + ) + } } } Spacer(modifier = Modifier.padding(top = 8.dp)) diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt index 2c4c1a91..125a1763 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopImages.kt @@ -21,15 +21,19 @@ package com.shabinder.common.uikit import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.loadXmlImageVector -import androidx.compose.ui.res.vectorXmlResource +import androidx.compose.ui.res.useResource +import org.xml.sax.InputSource @Composable -internal actual fun imageVectorResource(id: T): ImageVector = - vectorXmlResource(id as String) +internal actual fun imageVectorResource(id: T): ImageVector { + val density = LocalDensity.current + return useResource(id as String) { + loadXmlImageVector(InputSource(it), density) + } +} @Composable actual fun DownloadImageTick() { @@ -82,6 +86,10 @@ actual fun SpotifyLogo() = actual fun SaavnLogo() = getCachedPainter("drawable/ic_jio_saavn_logo.xml") +@Composable +actual fun SoundCloudLogo() = + getCachedPainter("drawable/ic_soundcloud.xml") + @Composable actual fun YoutubeLogo() = getCachedPainter("drawable/ic_youtube.xml") diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopScrollBar.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopScrollBar.kt index 9088ac4b..aad2af95 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopScrollBar.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DesktopScrollBar.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.unit.dp actual val MARGIN_SCROLLBAR: Dp = 8.dp -actual typealias ScrollbarAdapter = androidx.compose.foundation.ScrollbarAdapter +actual typealias ScrollbarAdapter = ScrollbarAdapter @OptIn(ExperimentalFoundationApi::class) @Composable @@ -23,8 +23,6 @@ actual fun rememberScrollbarAdapter( ): ScrollbarAdapter = androidx.compose.foundation.rememberScrollbarAdapter( scrollState = scrollState, - itemCount = itemCount, - averageItemSize = averageItemSize ) @Composable diff --git a/common/core-components/build.gradle.kts b/common/core-components/build.gradle.kts index 9a618a93..92592eda 100644 --- a/common/core-components/build.gradle.kts +++ b/common/core-components/build.gradle.kts @@ -10,29 +10,37 @@ kotlin { dependencies { implementation(project(":common:data-models")) implementation(project(":common:database")) - api("org.jetbrains.kotlinx:atomicfu:0.16.2") - api(MultiPlatformSettings.dep) - implementation(MVIKotlin.rx) + with(deps) { + api(multiplatform.settings) + api(kotlinx.atomicfu) + implementation(mviKotlin.rx) + implementation(decompose.dep) + } } } androidMain { dependencies { - implementation(Extras.mp3agic) - implementation(Extras.Android.countly) + with(deps) { + implementation(mp3agic) + implementation(countly.android) + } implementation(project(":ffmpeg:android-ffmpeg")) } } desktopMain { dependencies { - implementation(Extras.mp3agic) - implementation(Extras.Desktop.countly) - implementation("com.github.kokorin.jaffree:jaffree:2021.08.16") + with(deps) { + implementation(mp3agic) + implementation(countly.desktop) + implementation(jaffree) + } } } jsMain { dependencies { implementation(npm("browser-id3-writer", "4.4.0")) implementation(npm("file-saver", "2.0.4")) + implementation(deps.kotlin.js.wrappers.ext) } } } diff --git a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt index 43d87554..1b30fc38 100644 --- a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt +++ b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt @@ -117,7 +117,7 @@ class AndroidFileManager( try { // Add Mp3 Tags and Add to Library - if(trackDetails.audioFormat != AudioFormat.MP3) + if (trackDetails.audioFormat != AudioFormat.MP3) throw InvalidDataException("Audio Format is ${trackDetails.audioFormat}, Needs Conversion!") Mp3File(File(songFile.absolutePath)) @@ -166,7 +166,7 @@ class AndroidFileManager( override suspend fun loadImage(url: String, reqWidth: Int, reqHeight: Int): Picture = withContext(dispatcherIO) { - val cachePath = imageCacheDir() + getNameURL(url) + val cachePath = getImageCachePath(url) Picture( image = (loadCachedImage(cachePath, reqWidth, reqHeight) ?: freshImage( url, @@ -214,7 +214,7 @@ class AndroidFileManager( // Decode and Cache Full Sized Image in Background cacheImage( BitmapFactory.decodeByteArray(input, 0, input.size), - imageCacheDir() + getNameURL(url) + getImageCachePath(url) ) } bitmap // return Memory Efficient Bitmap diff --git a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/file_manager/FileManager.kt b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/file_manager/FileManager.kt index 6aedf2eb..d27662e9 100644 --- a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/file_manager/FileManager.kt +++ b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/file_manager/FileManager.kt @@ -25,10 +25,14 @@ 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.common.utils.requireNotNull import com.shabinder.database.Database -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* +import io.ktor.client.HttpClient +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.statement.HttpStatement +import io.ktor.http.contentLength +import io.ktor.http.isSuccess import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow @@ -80,12 +84,13 @@ fun FileManager.createDirectories() { if (!defaultDir().contains("null${fileSeparator()}SpotiFlyer")) { createDirectory(defaultDir()) createDirectory(imageCacheDir()) - createDirectory(defaultDir() + "Tracks/") - createDirectory(defaultDir() + "Albums/") - createDirectory(defaultDir() + "Playlists/") - createDirectory(defaultDir() + "YT_Downloads/") + createDirectory(defaultDir() + "Tracks" + fileSeparator()) + createDirectory(defaultDir() + "Albums" + fileSeparator()) + createDirectory(defaultDir() + "Playlists" + fileSeparator()) + createDirectory(defaultDir() + "YT_Downloads" + fileSeparator()) } - } catch (ignored: Exception) { } + } catch (ignored: Exception) { + } } fun FileManager.finalOutputDir( @@ -100,24 +105,50 @@ fun FileManager.finalOutputDir( removeIllegalChars(subFolder) + this.fileSeparator() } + removeIllegalChars(itemName) + extension -/*DIR Specific Operation End*/ -fun getNameURL(url: String): String { - return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length) - .replace('/', '_') +fun FileManager.getImageCachePath( + url: String +): String = imageCacheDir() + getNameFromURL(url, isImage = true) + +/*DIR Specific Operation End*/ +private fun getNameFromURL(url: String, isImage: Boolean = false): String { + val startIndex = url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1 + + var fileName = if (startIndex != -1) + url.substring(startIndex).replace('/', '_') + else url.substringAfterLast("/") + + // Generify File Extensions + if (isImage) { + if (fileName.length - fileName.lastIndexOf(".") > 5) { + fileName += ".jpeg" + } else { + if (fileName.endsWith(".jpg")) + fileName = fileName.substringBeforeLast(".") + ".jpeg" + } + } + + return fileName } -suspend fun downloadFile(url: String): Flow { +suspend fun HttpClient.downloadFile(url: String) = downloadFile(url, this) + +suspend fun downloadFile(url: String, client: HttpClient? = null): Flow { return flow { - val client = createHttpClient() - val response = client.get(url).execute() - val data = ByteArray(response.contentLength()!!.toInt()) + val httpClient = client ?: createHttpClient() + val response = httpClient.get(url).execute() + // Not all requests return Content Length + val data = kotlin.runCatching { + ByteArray(response.contentLength().requireNotNull().toInt()) + }.getOrNull() ?: byteArrayOf() var offset = 0 do { // Set Length optimally, after how many kb you want a progress update, now it 0.25mb val currentRead = response.content.readAvailable(data, offset, 2_50_000) offset += currentRead - val progress = (offset * 100f / data.size).roundToInt() + val progress = data.size.takeIf { it != 0 }?.let { fileSize -> + (offset * 100f / fileSize).roundToInt() + } ?: 0 emit(DownloadResult.Progress(progress)) } while (currentRead > 0) if (response.status.isSuccess()) { @@ -125,7 +156,10 @@ suspend fun downloadFile(url: String): Flow { } else { emit(DownloadResult.Error("File not downloaded")) } - client.close() + + // Close Client if We Created One + if (client == null) + httpClient.close() }.catch { e -> e.printStackTrace() emit(DownloadResult.Error(e.message ?: "File not downloaded")) diff --git a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt index 1b7eb918..ec3e78fd 100644 --- a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt +++ b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt @@ -8,6 +8,8 @@ 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 io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.native.concurrent.SharedImmutable @@ -23,6 +25,18 @@ suspend fun isInternetAccessible(): Boolean { } } +// If Fails returns Input Url +suspend inline fun HttpClient.getFinalUrl( + url: String, + crossinline block: HttpRequestBuilder.() -> Unit = {} +): String { + return withContext(dispatcherIO) { + runCatching { + get(url,block).call.request.url.toString() + }.getOrNull() ?: url + } +} + fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { // https://github.com/Kotlin/kotlinx.serialization/issues/1450 install(JsonFeature) { diff --git a/common/core-components/src/desktopMain/kotlin/com.shabinder.common.core_components/file_manager/DesktopFileManager.kt b/common/core-components/src/desktopMain/kotlin/com.shabinder.common.core_components/file_manager/DesktopFileManager.kt index 0f7b11a9..6ef248dd 100644 --- a/common/core-components/src/desktopMain/kotlin/com.shabinder.common.core_components/file_manager/DesktopFileManager.kt +++ b/common/core-components/src/desktopMain/kotlin/com.shabinder.common.core_components/file_manager/DesktopFileManager.kt @@ -30,17 +30,20 @@ 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.models.Actions import com.shabinder.common.models.DownloadStatus 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.Actions import com.shabinder.database.Database -import kotlinx.coroutines.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow -import org.jetbrains.skija.Image +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.skia.Image import org.koin.dsl.bind import org.koin.dsl.module import java.awt.image.BufferedImage @@ -104,9 +107,11 @@ class DesktopFileManager( override suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) { try { + val file = File(path) + if(!file.parentFile.exists()) createDirectories() (image as? BufferedImage)?.let { - ImageIO.write(it, "jpeg", File(path)) - } + ImageIO.write(it, "jpeg", file) + } } catch (e: IOException) { e.printStackTrace() } @@ -165,7 +170,7 @@ class DesktopFileManager( } SuspendableEvent.success(trackDetails.outputFilePath) } catch (e: Throwable) { - if(e is JaffreeException) Actions.instance.showPopUpMessage("No FFmpeg found at path.") + if (e is JaffreeException) Actions.instance.showPopUpMessage("No FFmpeg found at path.") if (songFile.exists()) songFile.delete() logger.e { "${songFile.absolutePath} could not be created" } SuspendableEvent.error(e) @@ -175,8 +180,7 @@ class DesktopFileManager( 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) + var picture: ImageBitmap? = loadCachedImage(getImageCachePath(url), reqWidth, reqHeight) if (picture == null) picture = freshImage(url, reqWidth, reqHeight) return Picture(image = picture) } @@ -205,7 +209,7 @@ class DesktopFileManager( if (result != null) { GlobalScope.launch(Dispatchers.IO) { // TODO Refactor - cacheImage(result, imageCacheDir() + getNameURL(url)) + cacheImage(result, getImageCachePath(url)) } result.toImageBitmap() } else null diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts index 9b5d0f70..0db019de 100644 --- a/common/data-models/build.gradle.kts +++ b/common/data-models/build.gradle.kts @@ -25,9 +25,6 @@ plugins { id("de.comahe.i18n4k") } -val statelyVersion = "1.1.7" -val statelyIsoVersion = "1.1.7-a1" - i18n4k { inputDirectory = "../../translations" packageName = "com.shabinder.common.translations" @@ -50,11 +47,13 @@ kotlin { } commonMain { dependencies { - implementation("co.touchlab:stately-concurrency:$statelyVersion") - implementation("co.touchlab:stately-isolate:$statelyIsoVersion") - implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion") - implementation(Extras.youtubeDownloader) - api(Internationalization.dep) + with(deps) { + api(bundles.stately) + api(i18n4k.core) + api(kermit) + api(moko.parcelize) + implementation(youtube.downloader) + } } } } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt index 4267ca97..10cc35a0 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt @@ -44,7 +44,7 @@ data class TrackDetails( var audioQuality: AudioQuality = AudioQuality.KBPS192, var audioFormat: AudioFormat = AudioFormat.MP4, var outputFilePath: String, // UriString in Android - var videoID: String? = null, + var videoID: String? = null, // will be used for purposes like Downloadable Link || VideoID etc. based on Provider ) : Parcelable { val outputMp3Path get() = outputFilePath.substringBeforeLast(".") + ".mp3" } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index 960baf53..3415020d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -12,6 +12,11 @@ sealed class SpotiFlyerException(override val message: String) : Exception(messa override val message: String = /*${Strings.mp3ConverterBusy()} */"CAUSE:$extraInfo" ) : SpotiFlyerException(message) + data class GeoLocationBlocked( + val extraInfo: String? = null, + override val message: String = "This Content is not Accessible from your Location, try using a VPN! \nCAUSE:$extraInfo" + ) : SpotiFlyerException(message) + data class UnknownReason( val exception: Throwable? = null, override val message: String = Strings.unknownError() diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt index c4cfae65..6f2e7eb2 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt @@ -20,7 +20,7 @@ data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor( // val explicit_content: Int = 0, val has_lyrics: Boolean = false, val id: String, - val image: String, + val image: String = "", val label: String? = null, val label_url: String? = null, val language: String, diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Badges.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Badges.kt new file mode 100644 index 00000000..52d0f77d --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Badges.kt @@ -0,0 +1,13 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Badges( + val pro: Boolean = false, + @SerialName("pro_unlimited") + val proUnlimited: Boolean = false, + val verified: Boolean = false +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/CreatorSubscription.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/CreatorSubscription.kt new file mode 100644 index 00000000..2c0fd45a --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/CreatorSubscription.kt @@ -0,0 +1,10 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreatorSubscription( + val product: Product = Product() +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Format.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Format.kt new file mode 100644 index 00000000..13b054ac --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Format.kt @@ -0,0 +1,14 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Format( + @SerialName("mime_type") + val mimeType: String = "", + val protocol: String = "" +) { + val isProgressive get() = protocol == "progressive" +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Media.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Media.kt new file mode 100644 index 00000000..66ba4707 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Media.kt @@ -0,0 +1,9 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.Serializable + +@Serializable +data class Media( + val transcodings: List = emptyList() +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Product.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Product.kt new file mode 100644 index 00000000..b82c5cf9 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Product.kt @@ -0,0 +1,10 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Product( + val id: String = "" +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/PublisherMetadata.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/PublisherMetadata.kt new file mode 100644 index 00000000..d4b51b8a --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/PublisherMetadata.kt @@ -0,0 +1,24 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PublisherMetadata( + @SerialName("album_title") + val albumTitle: String = "", + val artist: String = "", + @SerialName("contains_music") + val containsMusic: Boolean = false, + val id: Int = 0, + val isrc: String = "", + val publisher: String = "", + @SerialName("release_title") + val releaseTitle: String = "", + @SerialName("upc_or_ean") + val upcOrEan: String = "", + val urn: String = "", + @SerialName("writer_composer") + val writerComposer: String = "" +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Transcoding.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Transcoding.kt new file mode 100644 index 00000000..0bd313de --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Transcoding.kt @@ -0,0 +1,22 @@ +package com.shabinder.common.models.soundcloud + + +import com.shabinder.common.models.AudioFormat +import kotlinx.serialization.Serializable + +@Serializable +data class Transcoding( + val duration: Int = 0, + val format: Format = Format(), + val preset: String = "", + val quality: String = "", //sq == 128kbps //hq == 256kbps + val snipped: Boolean = false, + val url: String = "" +) { + val audioFormat: AudioFormat = when { + preset.contains("mp3") -> AudioFormat.MP3 + preset.contains("aac") || preset.contains("m4a") -> AudioFormat.MP4 + preset.contains("flac") -> AudioFormat.FLAC + else -> AudioFormat.UNKNOWN + } +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/User.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/User.kt new file mode 100644 index 00000000..4ea07493 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/User.kt @@ -0,0 +1,38 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class User( + @SerialName("avatar_url") + val avatarUrl: String = "", + val badges: Badges = Badges(), + val city: String = "", + @SerialName("country_code") + val countryCode: String = "", + @SerialName("first_name") + val firstName: String = "", + @SerialName("followers_count") + val followersCount: Int = 0, + @SerialName("full_name") + val fullName: String = "", + val id: Int = 0, + val kind: String = "", + @SerialName("last_modified") + val lastModified: String = "", + @SerialName("last_name") + val lastName: String = "", + val permalink: String = "", + @SerialName("permalink_url") + val permalinkUrl: String = "", + @SerialName("station_permalink") + val stationPermalink: String = "", + @SerialName("station_urn") + val stationUrn: String = "", + val uri: String = "", + val urn: String = "", + val username: String = "", + val verified: Boolean = false +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visual.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visual.kt new file mode 100644 index 00000000..9e28aede --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visual.kt @@ -0,0 +1,14 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Visual( + @SerialName("entry_time") + val entryTime: Int = 0, + val urn: String = "", + @SerialName("visual_url") + val visualUrl: String = "" +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visuals.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visuals.kt new file mode 100644 index 00000000..57ef84d2 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/Visuals.kt @@ -0,0 +1,13 @@ +package com.shabinder.common.models.soundcloud + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Visuals( + val enabled: Boolean = false, + //val tracking: Any = Any(), + val urn: String = "", + val visuals: List = listOf() +) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt new file mode 100644 index 00000000..ddc9c128 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt @@ -0,0 +1,166 @@ +package com.shabinder.common.models.soundcloud.resolvemodel + +import com.shabinder.common.models.AudioFormat +import com.shabinder.common.models.soundcloud.Media +import com.shabinder.common.models.soundcloud.PublisherMetadata +import com.shabinder.common.models.soundcloud.User +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator + +@Serializable +@JsonClassDiscriminator("kind") +sealed class SoundCloudResolveResponseBase { + abstract val kind: String + + @SerialName("playlist") + @Serializable + data class SoundCloudResolveResponsePlaylist( + @SerialName("artwork_url") + val artworkUrl: String = "", + @SerialName("calculated_artwork_url") + val calculatedArtworkUrl: String = "", //t500x500, t120x120 // "https://i1.sndcdn.com/artworks-pjsabv9w0EXW3lBJ-nvjDYg-large.jpg" // https://i1.sndcdn.com/artworks-pjsabv9w0EXW3lBJ-nvjDYg-t500x500.jpg + @SerialName("created_at") + val createdAt: String = "", + val description: String = "", + @SerialName("display_date") + val displayDate: String = "", + val duration: Int = 0, + override val kind: String = "", + @SerialName("embeddable_by") + val embeddableBy: String = "", + val genre: String = "", + val id: Int = 0, + @SerialName("is_album") + val isAlbum: Boolean = false, + @SerialName("label_name") + val labelName: String = "", + @SerialName("last_modified") + val lastModified: String = "", + val license: String = "", + @SerialName("likes_count") + val likesCount: Int = 0, + @SerialName("managed_by_feeds") + val managedByFeeds: Boolean = false, + val permalink: String = "", + @SerialName("permalink_url") + val permalinkUrl: String = "", + val `public`: Boolean = false, + @SerialName("published_at") + val publishedAt: String = "", + @SerialName("purchase_title") + val purchaseTitle: String = "", + @SerialName("purchase_url") + val purchaseUrl: String = "", + @SerialName("release_date") + val releaseDate: String = "", + @SerialName("reposts_count") + val repostsCount: Int = 0, + @SerialName("secret_token") + val secretToken: String = "", + @SerialName("set_type") + val setType: String = "", + val sharing: String = "", + @SerialName("tag_list") + val tagList: String = "", + val title: String = "", //"Top 50: Hip-hop & Rap" + @SerialName("track_count") + val trackCount: Int = 0, + var tracks: List = emptyList(), + val uri: String = "", + val user: User = User(), + @SerialName("user_id") + val userId: Int = 0 + ) : SoundCloudResolveResponseBase() + + + @SerialName("track") + @Serializable + data class SoundCloudResolveResponseTrack( + @SerialName("artwork_url") + val artworkUrl: String = "", + val caption: String = "", + @SerialName("comment_count") + val commentCount: Int = 0, + val commentable: Boolean = false, + @SerialName("created_at") + val createdAt: String = "", + val description: String = "", + @SerialName("display_date") + val displayDate: String = "", + @SerialName("download_count") + val downloadCount: Int = 0, + val downloadable: Boolean = false, + val duration: Int = 0, + @SerialName("embeddable_by") + val embeddableBy: String = "", + @SerialName("full_duration") + val fullDuration: Int = 0, + val genre: String = "", + @SerialName("has_downloads_left") + val hasDownloadsLeft: Boolean = false, + val id: Int = 0, + override val kind: String = "", + @SerialName("label_name") + val labelName: String = "", + @SerialName("last_modified") + val lastModified: String = "", + val license: String = "", + @SerialName("likes_count") + val likesCount: Int = 0, + val media: Media = Media(), + @SerialName("monetization_model") + val monetizationModel: String = "", + val permalink: String = "", + @SerialName("permalink_url") + val permalinkUrl: String = "", + @SerialName("playback_count") + val playbackCount: Int = 0, + val policy: String = "", + val `public`: Boolean = false, + @SerialName("publisher_metadata") + val publisherMetadata: PublisherMetadata = PublisherMetadata(), + @SerialName("purchase_title") + val purchaseTitle: String = "", + @SerialName("purchase_url") + val purchaseUrl: String = "", + @SerialName("release_date") + val releaseDate: String = "", + @SerialName("reposts_count") + val repostsCount: Int = 0, + @SerialName("secret_token") + val secretToken: String = "", + val sharing: String = "", + val state: String = "", + @SerialName("station_permalink") + val stationPermalink: String = "", + @SerialName("station_urn") + val stationUrn: String = "", + val streamable: Boolean = false, + @SerialName("tag_list") + val tagList: String = "", + val title: String = "", + @SerialName("track_authorization") + val trackAuthorization: String = "", + @SerialName("track_format") + val trackFormat: String = "", + val uri: String = "", + val urn: String = "", + val user: User = User(), + @SerialName("user_id") + val userId: Int = 0, + val visuals: String = "", + @SerialName("waveform_url") + val waveformUrl: String = "" + ) : SoundCloudResolveResponseBase() { + fun getDownloadableLink(): Pair? { + return (media.transcodings.firstOrNull { + it.quality == "hq" && (it.format.isProgressive || it.url.contains("progressive")) + } ?: media.transcodings.firstOrNull { + it.quality == "sq" && (it.format.isProgressive || it.url.contains("progressive")) + })?.let { + it.url to it.audioFormat + } + } + } +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt index fbaa3f7d..b20fe1d6 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt @@ -20,5 +20,6 @@ enum class Source { Spotify, YouTube, Gaana, - JioSaavn + JioSaavn, + SoundCloud } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt index 1bbad05f..cb685184 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt @@ -8,6 +8,7 @@ val globalJson by lazy { Json { isLenient = true ignoreUnknownKeys = true + coerceInputValues = true } } diff --git a/common/data-models/src/main/res/drawable/ic_soundcloud.xml b/common/data-models/src/main/res/drawable/ic_soundcloud.xml new file mode 100644 index 00000000..faaf1945 --- /dev/null +++ b/common/data-models/src/main/res/drawable/ic_soundcloud.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/common/data-models/src/main/res/drawable/spotiflyer.png b/common/data-models/src/main/res/drawable/spotiflyer.png new file mode 100644 index 00000000..74aebd66 Binary files /dev/null and b/common/data-models/src/main/res/drawable/spotiflyer.png differ diff --git a/common/database/build.gradle.kts b/common/database/build.gradle.kts index 45fc19bb..c4d095d1 100644 --- a/common/database/build.gradle.kts +++ b/common/database/build.gradle.kts @@ -34,31 +34,32 @@ kotlin { implementation(project(":common:data-models")) // SQL Delight - implementation(SqlDelight.runtime) - implementation(SqlDelight.coroutineExtensions) - - // koin - implementation(Koin.test) + with(deps.sqldelight) { + implementation(runtime) + api(coroutines.extension) + } } } androidMain { dependencies { - implementation(SqlDelight.androidDriver) + implementation(deps.sqldelight.android.driver) } } desktopMain { dependencies { - implementation(SqlDelight.sqliteDriver) - implementation(SqlDelight.jdbcDriver) + with(deps) { + implementation(sqldelight.driver) + implementation(sqlite.jdbc.driver) + } } } if (HostOS.isMac) { val iosMain by getting { dependencies { - implementation(SqlDelight.nativeDriver) + implementation(deps.sqldelight.native.driver) } } } diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index 3ae71372..0a7234a2 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -14,8 +14,6 @@ * * along with this program. If not, see . */ -import org.jetbrains.compose.compose - plugins { id("android-setup") id("multiplatform-setup") diff --git a/common/list/build.gradle.kts b/common/list/build.gradle.kts index 0e9910be..60894115 100644 --- a/common/list/build.gradle.kts +++ b/common/list/build.gradle.kts @@ -30,7 +30,6 @@ kotlin { implementation(project(":common:database")) implementation(project(":common:providers")) implementation(project(":common:core-components")) - implementation(SqlDelight.coroutineExtensions) } } } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index a6162851..a8667fc3 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -174,12 +174,10 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen } } - private fun List.updateTracksStatuses(map: HashMap): List { - val titleList = this.map { it.title } - val updatedList = mutableListOf().also { it.addAll(this) } - - for (newTrack in map) { - titleList.indexOf(newTrack.key).let { position -> + private fun List.updateTracksStatuses(map: Map): List { + val updatedList = ArrayList(this) + LinkedHashMap(map).forEach { newTrack -> + indexOfFirst { it.title == newTrack.key }.let { position -> if (position != -1) { updatedList.getOrNull(position)?.copy( downloaded = newTrack.value, diff --git a/common/main/build.gradle.kts b/common/main/build.gradle.kts index 0e9910be..60894115 100644 --- a/common/main/build.gradle.kts +++ b/common/main/build.gradle.kts @@ -30,7 +30,6 @@ kotlin { implementation(project(":common:database")) implementation(project(":common:providers")) implementation(project(":common:core-components")) - implementation(SqlDelight.coroutineExtensions) } } } diff --git a/common/preference/build.gradle.kts b/common/preference/build.gradle.kts index c9bec637..67bbc40b 100644 --- a/common/preference/build.gradle.kts +++ b/common/preference/build.gradle.kts @@ -30,7 +30,6 @@ kotlin { implementation(project(":common:database")) implementation(project(":common:core-components")) implementation(project(":common:providers")) - implementation(SqlDelight.coroutineExtensions) } } } diff --git a/common/providers/build.gradle.kts b/common/providers/build.gradle.kts index b4309fba..79e289f8 100644 --- a/common/providers/build.gradle.kts +++ b/common/providers/build.gradle.kts @@ -12,23 +12,25 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(project(":common:data-models")) - implementation(project(":common:database")) - implementation(project(":common:core-components")) - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1") - implementation(Extras.youtubeDownloader) - implementation(Extras.fuzzyWuzzy) + with(deps) { + implementation(project(":common:data-models")) + implementation(project(":common:database")) + implementation(project(":common:core-components")) + implementation(youtube.downloader) + implementation(fuzzy.wuzzy) + implementation(kotlinx.datetime) + } } } androidMain { dependencies { - implementation(Extras.mp3agic) + implementation(deps.mp3agic) } } desktopMain { dependencies { - implementation(Extras.mp3agic) - implementation("com.github.kokorin.jaffree:jaffree:2021.08.16") + implementation(deps.mp3agic) + implementation(deps.jaffree) } } jsMain { diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/FetchPlatformQueryResult.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/FetchPlatformQueryResult.kt index c2bceb41..87658110 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/FetchPlatformQueryResult.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/FetchPlatformQueryResult.kt @@ -20,7 +20,12 @@ import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager import com.shabinder.common.core_components.preference_manager.PreferenceManager import com.shabinder.common.database.DownloadRecordDatabaseQueries -import com.shabinder.common.models.* +import com.shabinder.common.models.AudioFormat +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.dispatcherIO import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.flatMapError import com.shabinder.common.models.event.coroutines.onSuccess @@ -28,9 +33,9 @@ import com.shabinder.common.models.event.coroutines.success import com.shabinder.common.models.spotify.Source import com.shabinder.common.providers.gaana.GaanaProvider import com.shabinder.common.providers.saavn.SaavnProvider +import com.shabinder.common.providers.sound_cloud.SoundCloudProvider import com.shabinder.common.providers.spotify.SpotifyProvider import com.shabinder.common.providers.youtube.YoutubeProvider -import com.shabinder.common.providers.youtube.get import com.shabinder.common.providers.youtube_music.YoutubeMusic import com.shabinder.common.providers.youtube_to_mp3.requests.YoutubeMp3 import com.shabinder.common.utils.appendPadded @@ -45,6 +50,7 @@ class FetchPlatformQueryResult( private val spotifyProvider: SpotifyProvider, private val youtubeProvider: YoutubeProvider, private val saavnProvider: SaavnProvider, + private val soundCloudProvider: SoundCloudProvider, private val youtubeMusic: YoutubeMusic, private val youtubeMp3: YoutubeMp3, val fileManager: FileManager, @@ -66,7 +72,7 @@ class FetchPlatformQueryResult( link.contains("youtube.com", true) || link.contains("youtu.be", true) -> youtubeProvider.query(link) - // Jio Saavn + // JioSaavn link.contains("saavn", true) -> saavnProvider.query(link) @@ -74,6 +80,10 @@ class FetchPlatformQueryResult( link.contains("gaana", true) -> gaanaProvider.query(link) + // SoundCloud + link.contains("soundcloud", true) -> + soundCloudProvider.query(link) + else -> { SuspendableEvent.error(SpotiFlyerException.LinkInvalid(link)) } @@ -122,7 +132,7 @@ class FetchPlatformQueryResult( ytMp3Link.component2()?.stackTraceToString() ?: "couldn't fetch link for ${track.videoID} ,trying manual extraction" ) - appendLine("Trying Local Extraction") + //appendLine("Trying Local Extraction") null } else { audioFormat = AudioFormat.MP3 @@ -130,6 +140,20 @@ class FetchPlatformQueryResult( } } } + Source.SoundCloud -> { + audioFormat = track.audioFormat + soundCloudProvider.getDownloadURL(track).let { + if (it is SuspendableEvent.Failure || it.component1().isNullOrEmpty()) { + appendPadded( + "SoundCloud Provider Failed for ${track.title}:", + it.component2()?.stackTraceToString() + ?: "couldn't fetch link for ${track.trackUrl}" + ) + null + } else + it.component1() + } + } else -> { appendPadded( "Invalid Arguments", diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/ProvidersModule.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/ProvidersModule.kt index 44c67ea3..18e250a2 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/ProvidersModule.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/ProvidersModule.kt @@ -2,6 +2,7 @@ package com.shabinder.common.providers import com.shabinder.common.providers.gaana.GaanaProvider import com.shabinder.common.providers.saavn.SaavnProvider +import com.shabinder.common.providers.sound_cloud.SoundCloudProvider import com.shabinder.common.providers.spotify.SpotifyProvider import com.shabinder.common.providers.spotify.token_store.TokenStore import com.shabinder.common.providers.youtube.YoutubeProvider @@ -16,7 +17,8 @@ fun providersModule(enableNetworkLogs: Boolean) = module { single { GaanaProvider(get(), get(), get()) } single { SaavnProvider(get(), get(), get()) } single { YoutubeProvider(get(), get(), get()) } + single { SoundCloudProvider(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()) } + single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/GaanaProvider.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/GaanaProvider.kt index 622524cb..438365ec 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/GaanaProvider.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/GaanaProvider.kt @@ -19,6 +19,7 @@ package com.shabinder.common.providers.gaana import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager import com.shabinder.common.core_components.file_manager.finalOutputDir +import com.shabinder.common.core_components.file_manager.getImageCachePath import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.SpotiFlyerException @@ -124,8 +125,7 @@ class GaanaProvider( title = it.track_title, artists = it.artist.map { artist -> artist?.name.toString() }, durationSec = it.duration, - albumArtPath = fileManager.imageCacheDir() + (it.artworkLink.substringBeforeLast('/') - .substringAfterLast('/')) + ".jpeg", + albumArtPath = fileManager.getImageCachePath(it.artworkLink), albumName = it.album_title, genre = it.genre?.mapNotNull { genre -> genre?.name } ?: emptyList(), year = it.release_date, diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/requests/GaanaRequests.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/requests/GaanaRequests.kt index ab244d1e..933b06f4 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/requests/GaanaRequests.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/gaana/requests/GaanaRequests.kt @@ -25,11 +25,13 @@ import com.shabinder.common.models.gaana.GaanaSong import io.ktor.client.* import io.ktor.client.request.* -private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990" -private val BASE_URL get() = "${corsApi}https://api.gaana.com" - interface GaanaRequests { + companion object { + private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990" + private val BASE_URL get() = "${corsApi}https://api.gaana.com" + } + val httpClient: HttpClient /* diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/SaavnProvider.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/SaavnProvider.kt index 4c807462..a6c7e836 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/SaavnProvider.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/SaavnProvider.kt @@ -3,6 +3,7 @@ package com.shabinder.common.providers.saavn import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager import com.shabinder.common.core_components.file_manager.finalOutputDir +import com.shabinder.common.core_components.file_manager.getImageCachePath import com.shabinder.common.models.* import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.saavn.SaavnSong @@ -28,7 +29,7 @@ class SaavnProvider( ).apply { val pageLink = fullLink.substringAfter("saavn.com/").substringBefore("?") when { - pageLink.contains("/song/", true) -> { + pageLink.contains("song/", true) -> { getSong(fullLink).value.let { folderType = "Tracks" subFolder = "" @@ -37,7 +38,7 @@ class SaavnProvider( coverUrl = it.image.replace("http:", "https:") } } - pageLink.contains("/album/", true) -> { + pageLink.contains("album/", true) -> { getAlbum(fullLink).value.let { folderType = "Albums" subFolder = removeIllegalChars(it.title) @@ -46,7 +47,7 @@ class SaavnProvider( coverUrl = it.image.replace("http:", "https:") } } - pageLink.contains("/featured/", true) -> { // Playlist + pageLink.contains("featured/", true) -> { // Playlist getPlaylist(fullLink).value.let { folderType = "Playlists" subFolder = removeIllegalChars(it.listname) @@ -68,7 +69,7 @@ class SaavnProvider( artists = it.artistMap.keys.toMutableSet().apply { addAll(it.singers.split(",")) }.toList(), durationSec = it.duration.toInt(), albumName = it.album, - albumArtPath = fileManager.imageCacheDir() + (it.image.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg", + albumArtPath = fileManager.getImageCachePath(it.image), year = it.year, comment = it.copyright_text, trackUrl = it.perma_url, diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/requests/JioSaavnRequests.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/requests/JioSaavnRequests.kt index a9d6faec..ca914ab7 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/requests/JioSaavnRequests.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/saavn/requests/JioSaavnRequests.kt @@ -18,9 +18,16 @@ import io.github.shabinder.utils.getBoolean import io.github.shabinder.utils.getJsonArray import io.github.shabinder.utils.getJsonObject import io.github.shabinder.utils.getString -import io.ktor.client.* -import io.ktor.client.request.* -import kotlinx.serialization.json.* +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put import kotlin.collections.set interface JioSaavnRequests { @@ -32,9 +39,9 @@ interface JioSaavnRequests { trackName: String, trackArtists: List, preferredQuality: AudioQuality - ): SuspendableEvent, Throwable> = searchForSong(trackName).map { songs -> - val bestMatch = sortByBestMatch(songs, trackName, trackArtists).keys.firstOrNull() ?: - throw SpotiFlyerException.DownloadLinkFetchFailed("No SAAVN Match Found for $trackName") + ): SuspendableEvent, Throwable> = searchForSong(trackName).map { songs -> + val bestMatch = sortByBestMatch(songs, trackName, trackArtists).keys.firstOrNull() + ?: throw SpotiFlyerException.DownloadLinkFetchFailed("No SAAVN Match Found for $trackName") var audioQuality: AudioQuality = AudioQuality.KBPS160 val m4aLink: String by getSongFromID(bestMatch).map { song -> @@ -46,7 +53,7 @@ interface JioSaavnRequests { song.media_url.requireNotNull().replaceAfterLast("_", "${optimalQuality.kbps}.mp4") } - Pair(m4aLink,audioQuality) + Pair(m4aLink, audioQuality) } suspend fun searchForSong( @@ -235,8 +242,8 @@ interface JioSaavnRequests { for (result in tracks) { var hasCommonWord = false - val resultName = result.title.lowercase().replace("/", " ") - val trackNameWords = trackName.lowercase().split(" ") + val resultName = result.title.toLowerCase().replace("/", " ") + val trackNameWords = trackName.toLowerCase().split(" ") for (nameWord in trackNameWords) { if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true @@ -256,11 +263,11 @@ interface JioSaavnRequests { // String Containing All Artist Names from JioSaavn Search Result val artistListString = mutableSetOf().apply { result.more_info?.singers?.split(",")?.let { addAll(it) } - result.more_info?.primary_artists?.lowercase()?.split(",")?.let { addAll(it) } + result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) } }.joinToString(" , ") for (artist in trackArtists) { - if (FuzzySearch.partialRatio(artist.lowercase(), artistListString) > 85) + if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85) artistMatchNumber++ } diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/SoundCloudProvider.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/SoundCloudProvider.kt new file mode 100644 index 00000000..d74087d5 --- /dev/null +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/SoundCloudProvider.kt @@ -0,0 +1,115 @@ +package com.shabinder.common.providers.sound_cloud + +import co.touchlab.kermit.Kermit +import com.shabinder.common.core_components.file_manager.FileManager +import com.shabinder.common.core_components.file_manager.finalOutputDir +import com.shabinder.common.core_components.file_manager.getImageCachePath +import com.shabinder.common.models.AudioFormat +import com.shabinder.common.models.AudioQuality +import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist +import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack +import com.shabinder.common.models.spotify.Source +import com.shabinder.common.providers.sound_cloud.requests.SoundCloudRequests +import com.shabinder.common.providers.sound_cloud.requests.doAuthenticatedRequest +import com.shabinder.common.utils.requireNotNull +import io.github.shabinder.utils.getString +import io.ktor.client.HttpClient +import kotlinx.serialization.json.JsonObject + +class SoundCloudProvider( + private val logger: Kermit, + private val fileManager: FileManager, + override val httpClient: HttpClient, +) : SoundCloudRequests { + suspend fun query(fullURL: String) = SuspendableEvent { + PlatformQueryResult( + folderType = "", + subFolder = "", + title = "", + coverUrl = "", + trackList = listOf(), + Source.SoundCloud + ).apply { + when (val response = fetchResult(fullURL)) { + is SoundCloudResolveResponseTrack -> { + folderType = "Tracks" + subFolder = "" + trackList = listOf(response).toTrackDetailsList(folderType, subFolder) + coverUrl = response.artworkUrl + title = response.title + } + is SoundCloudResolveResponsePlaylist -> { + folderType = "Playlists" + subFolder = response.title + trackList = response.tracks.toTrackDetailsList(folderType, subFolder) + coverUrl = response.artworkUrl.ifBlank { response.calculatedArtworkUrl } + title = response.title + } + } + } + } + + suspend fun getDownloadURL(trackDetails: TrackDetails) = SuspendableEvent { + doAuthenticatedRequest(trackDetails.videoID.requireNotNull()).getString("url") + } + + + private fun List.toTrackDetailsList( + type: String, + subFolder: String + ): List = + map { + val downloadableInfo = it.getDownloadableLink() + TrackDetails( + title = it.title, + //trackNumber = it.track_number, + genre = listOf(it.genre), + artists = /*it.artists?.map { artist -> artist?.name.toString() } ?:*/ listOf(it.user.username.ifBlank { it.genre }), + albumArtists = /*it.album?.artists?.mapNotNull { artist -> artist?.name } ?:*/ emptyList(), + durationSec = (it.duration / 1000), + albumArtPath = fileManager.getImageCachePath(it.artworkUrl.formatArtworkUrl()), + albumName = "", //it.album?.name, + year = runCatching { it.displayDate.substring(0, 4) }.getOrNull(), + comment = it.caption, + trackUrl = it.permalinkUrl, + downloaded = it.updateStatusIfPresent(type, subFolder), + source = Source.SoundCloud, + albumArtURL = it.artworkUrl.formatArtworkUrl(), + outputFilePath = fileManager.finalOutputDir( + it.title, + type, + subFolder, + fileManager.defaultDir()/*,".m4a"*/ + ), + audioQuality = AudioQuality.KBPS128, + videoID = downloadableInfo?.first, + audioFormat = downloadableInfo?.second ?: AudioFormat.MP3 + ) + } + + private fun SoundCloudResolveResponseTrack.updateStatusIfPresent( + folderType: String, + subFolder: String + ): DownloadStatus { + return if (fileManager.isPresent( + fileManager.finalOutputDir( + title, + folderType, + subFolder, + fileManager.defaultDir() + ) + ) + ) { // Download Already Present!! + DownloadStatus.Downloaded + } else + DownloadStatus.NotDownloaded + } + + private fun String.formatArtworkUrl(): String { + return substringBeforeLast("-") + "-t500x500." + substringAfterLast(".") + } +} \ No newline at end of file diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt new file mode 100644 index 00000000..004e49b4 --- /dev/null +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt @@ -0,0 +1,138 @@ +package com.shabinder.common.providers.sound_cloud.requests + +import com.shabinder.common.core_components.utils.getFinalUrl +import com.shabinder.common.models.SpotiFlyerException +import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase +import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist +import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack +import io.ktor.client.HttpClient +import io.ktor.client.features.ClientRequestException +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.supervisorScope +import kotlinx.serialization.InternalSerializationApi + +interface SoundCloudRequests { + + val httpClient: HttpClient + + suspend fun fetchResult(url: String): SoundCloudResolveResponseBase { + @Suppress("NAME_SHADOWING") + var url = url + + // Fetch Full URL if Input is Shortened URL from App + if (url.contains("soundcloud.app")) + url = httpClient.getFinalUrl(url) + + return getResponseObj(url).run { + when (this) { + is SoundCloudResolveResponseTrack -> { + getTrack() + } + is SoundCloudResolveResponsePlaylist -> { + populatePlaylist() + } + else -> throw SpotiFlyerException.FeatureNotImplementedYet() + } + } + } + + @Suppress("NAME_SHADOWING") + suspend fun SoundCloudResolveResponseTrack.getTrack() = apply { + val track = populateTrackInfo() + + if (track.policy == "BLOCK") + throw SpotiFlyerException.GeoLocationBlocked(extraInfo = "Use VPN to access ${track.title}") + + if (!track.streamable) + throw SpotiFlyerException.LinkInvalid("\nSound Cloud Reports that ${track.title} is not streamable !\n") + + return track + } + + @Suppress("NAME_SHADOWING") + suspend fun SoundCloudResolveResponsePlaylist.populatePlaylist(): SoundCloudResolveResponsePlaylist = apply { + supervisorScope { + try { + tracks = tracks.map { + async { + runCatching { + it.populateTrackInfo() + }.getOrNull() ?: it + } + }.awaitAll() + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + + + private suspend fun SoundCloudResolveResponseTrack.populateTrackInfo(): SoundCloudResolveResponseTrack { + if (media.transcodings.isNotEmpty()) + return this + + val infoURL = URLS.TRACK_INFO.buildURL(id.toString()) + return httpClient.get(infoURL) { + parameter("client_id", CLIENT_ID) + } + } + + private suspend fun getResponseObj(url: String, clientID: String = CLIENT_ID): SoundCloudResolveResponseBase { + val itemURL = URLS.RESOLVE.buildURL(url) + val resp: SoundCloudResolveResponseBase = try { + httpClient.get(itemURL) { + parameter("client_id", clientID) + } + } catch (e: ClientRequestException) { + if (clientID != ALT_CLIENT_ID) + return getResponseObj(url, ALT_CLIENT_ID) + throw e + } + val tracksPresent = (resp is SoundCloudResolveResponsePlaylist && resp.tracks.isNotEmpty()) + + if (!tracksPresent && clientID != ALT_CLIENT_ID) + return getResponseObj(ALT_CLIENT_ID) + + return resp + } + + @Suppress("unused") + companion object { + private enum class URLS(val buildURL: (arg: String) -> String) { + RESOLVE({ "https://api-v2.soundcloud.com/resolve?url=$it}" }), + PLAYLIST_LIKED({ "https://api-v2.soundcloud.com/users/$it/playlists/liked_and_owned?limit=200" }), + FAVORITES({ "'https://api-v2.soundcloud.com/users/$it/track_likes?limit=200" }), + COMMENTED({ "https://api-v2.soundcloud.com/users/$it/comments" }), + TRACKS({ "https://api-v2.soundcloud.com/users/$it/tracks?limit=200" }), + ALL({ "https://api-v2.soundcloud.com/profile/soundcloud:users:$it?limit=200" }), + TRACK_INFO({ "https://api-v2.soundcloud.com/tracks/$it" }), + ORIGINAL_DOWNLOAD({ "https://api-v2.soundcloud.com/tracks/$it/download" }), + USER({ "https://api-v2.soundcloud.com/users/$it" }), + ME({ "https://api-v2.soundcloud.com/me?oauth_token=$it" }), + } + + const val CLIENT_ID = "a3e059563d7fd3372b49b37f00a00bcf" + const val ALT_CLIENT_ID = "2t9loNQH90kzJcsFCODdigxfp325aq4z" + } +} + +@OptIn(InternalSerializationApi::class) +suspend inline fun SoundCloudRequests.doAuthenticatedRequest(url: String): T { + var clientID: String = SoundCloudRequests.CLIENT_ID + return try { + httpClient.get(url) { + parameter("client_id", clientID) + } + } catch (e: ClientRequestException) { + if (clientID != SoundCloudRequests.ALT_CLIENT_ID) { + clientID = SoundCloudRequests.ALT_CLIENT_ID + return httpClient.get(url) { + parameter("client_id", clientID) + } + } + throw e + } +} \ No newline at end of file diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/SpotifyProvider.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/SpotifyProvider.kt index c7706153..1973e890 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/SpotifyProvider.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/SpotifyProvider.kt @@ -19,6 +19,7 @@ package com.shabinder.common.providers.spotify import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager import com.shabinder.common.core_components.file_manager.finalOutputDir +import com.shabinder.common.core_components.file_manager.getImageCachePath import com.shabinder.common.core_components.utils.createHttpClient import com.shabinder.common.models.* import com.shabinder.common.models.event.coroutines.SuspendableEvent @@ -79,7 +80,7 @@ class SpotifyProvider( if (type == "episode" || type == "show") { throw SpotiFlyerException.FeatureNotImplementedYet( - "Support for Spotify's ${type.uppercase()} isn't implemented yet" + "Support for Spotify's ${type.toUpperCase()} isn't implemented yet" ) } @@ -201,9 +202,7 @@ class SpotifyProvider( artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), albumArtists = it.album?.artists?.mapNotNull { artist -> artist?.name } ?: emptyList(), durationSec = (it.duration_ms / 1000).toInt(), - albumArtPath = fileManager.imageCacheDir() + (it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast( - '/' - ) + ".jpeg", + albumArtPath = fileManager.getImageCachePath(it.album?.images?.firstOrNull()?.url ?: ""), albumName = it.album?.name, year = it.album?.release_date, comment = "Genres:${it.album?.genres?.joinToString()}", diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube/YoutubeProvider.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube/YoutubeProvider.kt index 4fea8918..7e5714c6 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube/YoutubeProvider.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube/YoutubeProvider.kt @@ -19,6 +19,7 @@ package com.shabinder.common.providers.youtube import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager import com.shabinder.common.core_components.file_manager.finalOutputDir +import com.shabinder.common.core_components.file_manager.getImageCachePath import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.SpotiFlyerException @@ -30,7 +31,7 @@ import io.github.shabinder.YoutubeDownloader import io.github.shabinder.models.YoutubeVideo import io.github.shabinder.models.formats.Format import io.github.shabinder.models.quality.AudioQuality -import io.ktor.client.* +import io.ktor.client.HttpClient class YoutubeProvider( private val httpClient: HttpClient, @@ -108,13 +109,14 @@ class YoutubeProvider( title = name trackList = videos.map { + val imageURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg" TrackDetails( title = it.title ?: "N/A", artists = listOf(it.author ?: "N/A"), durationSec = it.lengthSeconds, - albumArtPath = fileManager.imageCacheDir() + it.videoId + ".jpeg", + albumArtPath = fileManager.getImageCachePath(imageURL), source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg", + albumArtURL = imageURL, downloaded = if (fileManager.isPresent( fileManager.finalOutputDir( itemName = it.title ?: "N/A", @@ -155,7 +157,7 @@ class YoutubeProvider( val video = ytDownloader.getVideo(searchId) coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" val detail = video.videoDetails - val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true) + val name = detail.title?.replace(detail.author?.toUpperCase() ?: "", "", true) ?: detail.title ?: "" // logger.i{ detail.toString() } trackList = listOf( @@ -163,9 +165,9 @@ class YoutubeProvider( title = name, artists = listOf(detail.author ?: "N/A"), durationSec = detail.lengthSeconds, - albumArtPath = fileManager.imageCacheDir() + "$searchId.jpeg", + albumArtPath = fileManager.getImageCachePath(coverUrl), source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", + albumArtURL = coverUrl, downloaded = if (fileManager.isPresent( fileManager.finalOutputDir( itemName = name, @@ -179,7 +181,12 @@ class YoutubeProvider( else { DownloadStatus.NotDownloaded }, - outputFilePath = fileManager.finalOutputDir(name, folderType, subFolder, fileManager.defaultDir()/*,".m4a"*/), + outputFilePath = fileManager.finalOutputDir( + name, + folderType, + subFolder, + fileManager.defaultDir()/*,".m4a"*/ + ), videoID = searchId ) ) diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube_music/YoutubeMusic.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube_music/YoutubeMusic.kt index 7ea1676d..a2c10e54 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube_music/YoutubeMusic.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/youtube_music/YoutubeMusic.kt @@ -18,19 +18,33 @@ package com.shabinder.common.providers.youtube_music import co.touchlab.kermit.Kermit import com.shabinder.common.core_components.file_manager.FileManager -import com.shabinder.common.models.* +import com.shabinder.common.models.AudioFormat +import com.shabinder.common.models.AudioQuality +import com.shabinder.common.models.SpotiFlyerException +import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.YoutubeTrack +import com.shabinder.common.models.corsApi 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.map import com.shabinder.common.providers.youtube.YoutubeProvider -import com.shabinder.common.providers.youtube.get import com.shabinder.common.providers.youtube_to_mp3.requests.YoutubeMp3 import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.serialization.json.* +import io.ktor.client.HttpClient +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import kotlin.collections.set import kotlin.math.absoluteValue @@ -50,7 +64,7 @@ class YoutubeMusic constructor( suspend fun findMp3SongDownloadURLYT( trackDetails: TrackDetails, preferredQuality: AudioQuality = fileManager.preferenceManager.audioQuality - ): SuspendableEvent, Throwable> { + ): SuspendableEvent, Throwable> { return getYTIDBestMatch(trackDetails).flatMap { videoID -> // As YT compress Audio hence there is no benefit of quality for more than 192 val optimalQuality = @@ -69,7 +83,7 @@ class YoutubeMusic constructor( } }*/.map { trackDetails.audioFormat = AudioFormat.MP3 - Pair(it,optimalQuality) + Pair(it, optimalQuality) } } } @@ -168,7 +182,7 @@ class YoutubeMusic constructor( ! 4 - Duration (hh:mm:ss) ! ! We blindly gather all the details we get our hands on, then - ! cherry pick the details we need based on their index numbers, + ! cherry-pick the details we need based on their index numbers, ! we do so only if their Type is 'Song' or 'Video */ @@ -180,7 +194,7 @@ class YoutubeMusic constructor( /* Filter Out dummies here itself ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one - ! sub-block, if not its a dummy, why does the YTM response contain dummies? + ! sub-block, if not it is a dummy, why does the YTM response contain dummies? ! I have no clue. We skip these. ! Remember that we appended the linkBlock to result, treating that like the @@ -189,7 +203,7 @@ class YoutubeMusic constructor( */ for (detailArray in result.subList(0, result.size - 1)) { for (detail in detailArray.jsonArray) { - if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue + if ((detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0) < 2) continue // if not a dummy, collect All Variables val details = @@ -262,8 +276,8 @@ class YoutubeMusic constructor( // most song results on youtube go by $artist - $songName or artist1/artist2 var hasCommonWord = false - val resultName = result.name?.lowercase()?.replace("-", " ")?.replace("/", " ") ?: "" - val trackNameWords = trackName.lowercase().split(" ") + val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: "" + val trackNameWords = trackName.toLowerCase().split(" ") for (nameWord in trackNameWords) { if (nameWord.isNotBlank() && FuzzySearch.partialRatio( @@ -287,8 +301,8 @@ class YoutubeMusic constructor( if (result.type == "Song") { for (artist in trackArtists) { if (FuzzySearch.ratio( - artist.lowercase(), - result.artist?.lowercase() ?: "" + artist.toLowerCase(), + result.artist?.toLowerCase() ?: "" ) > 85 ) artistMatchNumber++ @@ -296,8 +310,8 @@ class YoutubeMusic constructor( } else { // i.e. is a Video for (artist in trackArtists) { if (FuzzySearch.partialRatio( - artist.lowercase(), - result.name?.lowercase() ?: "" + artist.toLowerCase(), + result.name?.toLowerCase() ?: "" ) > 85 ) artistMatchNumber++ diff --git a/common/providers/src/commonTest/kotlin/com/shabinder/common/providers/TestSpotifyTrackMatching.kt b/common/providers/src/commonTest/kotlin/com/shabinder/common/providers/TestSpotifyTrackMatching.kt index 8767770a..a06d8471 100644 --- a/common/providers/src/commonTest/kotlin/com/shabinder/common/providers/TestSpotifyTrackMatching.kt +++ b/common/providers/src/commonTest/kotlin/com/shabinder/common/providers/TestSpotifyTrackMatching.kt @@ -1,10 +1,15 @@ package com.shabinder.common.providers +import com.shabinder.common.core_components.utils.createHttpClient +import com.shabinder.common.core_components.utils.getFinalUrl import com.shabinder.common.models.TrackDetails import com.shabinder.common.providers.utils.CommonUtils import com.shabinder.common.providers.utils.SpotifyUtils import com.shabinder.common.providers.utils.SpotifyUtils.toTrackDetailsList import io.github.shabinder.runBlocking +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import kotlinx.serialization.InternalSerializationApi import kotlin.test.Test class TestSpotifyTrackMatching { @@ -17,7 +22,15 @@ class TestSpotifyTrackMatching { private val spotifyToken: String? get() = null -// get() = "BQB41HqrLcrh5eRYaL97GvaH6tRe-1EktQ8VGTWUQuFnYVWBEoTcF7T_8ogqVn1GHl9HCcMiQ0HBT-ybC74" + + // get() = "BQB41HqrLcrh5eRYaL97GvaH6tRe-1EktQ8VGTWUQuFnYVWBEoTcF7T_8ogqVn1GHl9HCcMiQ0HBT-ybC74" + @OptIn(InternalSerializationApi::class) + @Test + fun testRandomThing() = runBlocking { + val res = createHttpClient().getFinalUrl("https://soundcloud.app.goo.gl/vrBzR") + println(res) + } + @Test fun matchVideo() = runBlocking { diff --git a/common/root/build.gradle.kts b/common/root/build.gradle.kts index 8687f531..1bbe64e2 100644 --- a/common/root/build.gradle.kts +++ b/common/root/build.gradle.kts @@ -33,9 +33,10 @@ fun org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer.generateFramewor export(project(":common:providers")) export(project(":common:list")) export(project(":common:preference")) - export(Decompose.decompose) - export(MVIKotlin.mvikotlinMain) - export(MVIKotlin.mvikotlinLogging) + with(deps) { + export(decompose.dep) + export(bundles.mviKotlin) + } } } @@ -71,7 +72,6 @@ kotlin { implementation(project(":common:providers")) implementation(project(":common:core-components")) implementation(project(":common:preference")) - implementation(SqlDelight.coroutineExtensions) } } } @@ -86,9 +86,10 @@ kotlin { api(project(":common:list")) api(project(":common:main")) api(project(":common:preference")) - api(Decompose.decompose) - api(MVIKotlin.mvikotlinMain) - api(MVIKotlin.mvikotlinLogging) + with(deps) { + api(decompose.dep) + api(bundles.mviKotlin) + } } } } @@ -100,8 +101,11 @@ val packForXcode by tasks.creating(Sync::class) { group = "build" val mode = System.getenv("CONFIGURATION") ?: "DEBUG" val targetName = "ios" - val framework = kotlin.targets.getByName(targetName) - .binaries.getFramework(mode) + val framework = + kotlin.targets.getByName( + targetName + ) + .binaries.getFramework(mode) inputs.property("mode", mode) dependsOn(framework.linkTask) val targetDir = File(buildDir, "xcode-frameworks") diff --git a/console-app/build.gradle.kts b/console-app/build.gradle.kts index c3da4280..da6958db 100644 --- a/console-app/build.gradle.kts +++ b/console-app/build.gradle.kts @@ -19,38 +19,39 @@ application { } dependencies { - implementation(Koin.core) - implementation(project(":common:database")) - implementation(project(":common:data-models")) - implementation(project(":common:dependency-injection")) - implementation(project(":common:root")) - implementation(project(":common:main")) - implementation(project(":common:list")) - implementation(project(":common:list")) + with(deps) { + implementation(Koin.core) + implementation(project(":common:database")) + implementation(project(":common:data-models")) + implementation(project(":common:dependency-injection")) + implementation(project(":common:root")) + implementation(project(":common:main")) + implementation(project(":common:list")) + implementation(project(":common:list")) - // Decompose - implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) + // Decompose + implementation(Decompose.decompose) + implementation(Decompose.extensionsCompose) - // MVI - implementation(MVIKotlin.mvikotlin) - implementation(MVIKotlin.mvikotlinMain) + // MVI + implementation(MVIKotlin.mvikotlin) + implementation(MVIKotlin.mvikotlinMain) - // Koin - implementation(Koin.core) + // Koin + implementation(Koin.core) - // Matomo - implementation("org.piwik.java.tracking:matomo-java-tracker:1.6") + // Matomo - implementation(Ktor.slf4j) - implementation(Ktor.clientCore) - implementation(Ktor.clientJson) - implementation(Ktor.clientApache) - implementation(Ktor.clientLogging) - implementation(Ktor.clientSerialization) - implementation(Serialization.json) - // testDeps - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.5.21") + implementation(Ktor.slf4j) + implementation(Ktor.clientCore) + implementation(Ktor.clientJson) + implementation(Ktor.clientApache) + implementation(Ktor.clientLogging) + implementation(Ktor.clientSerialization) + implementation(Serialization.json) + // testDeps + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.5.21") + } } tasks.withType().configureEach { kotlinOptions { diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 0ccbe2dc..b176fd86 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -32,9 +32,12 @@ kotlin { kotlinOptions.jvmTarget = "1.8" } } - + tasks.named("jvmProcessResources") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } sourceSets { val jvmMain by getting { + resources.srcDirs("../common/data-models/src/main/res") dependencies { implementation(compose.desktop.currentOs) implementation(project(":common:database")) @@ -44,19 +47,21 @@ kotlin { implementation(project(":common:compose")) implementation(project(":common:providers")) implementation(project(":common:root")) - implementation("com.github.kokorin.jaffree:jaffree:2021.08.16") - // Decompose - implementation(Decompose.decompose) - implementation(Decompose.extensionsCompose) + with(deps) { + implementation(jaffree) - // MVI - implementation(MVIKotlin.mvikotlin) - implementation(MVIKotlin.mvikotlinMain) - - // Koin - implementation(Koin.core) + with(decompose) { + implementation(dep) + implementation(extensions.compose) + } + with(mviKotlin) { + implementation(dep) + implementation(main) + } + implementation(koin.core) + } } } val jvmTest by getting diff --git a/desktop/src/jvmMain/resources/drawable/ic_arrow.xml b/desktop/src/jvmMain/resources/drawable/ic_arrow.xml deleted file mode 100644 index a426c154..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_arrow.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_download_arrow.xml b/desktop/src/jvmMain/resources/drawable/ic_download_arrow.xml deleted file mode 100644 index deadedca..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_download_arrow.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_error.xml b/desktop/src/jvmMain/resources/drawable/ic_error.xml deleted file mode 100644 index 9ebd8a0e..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_error.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_gaana.xml b/desktop/src/jvmMain/resources/drawable/ic_gaana.xml deleted file mode 100644 index 28d27c3c..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_gaana.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_github.xml b/desktop/src/jvmMain/resources/drawable/ic_github.xml deleted file mode 100644 index 0e14b28a..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_github.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_heart.xml b/desktop/src/jvmMain/resources/drawable/ic_heart.xml deleted file mode 100644 index 92f9beb0..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_heart.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_indian_rupee.xml b/desktop/src/jvmMain/resources/drawable/ic_indian_rupee.xml deleted file mode 100644 index 637c6b56..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_indian_rupee.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_instagram.xml b/desktop/src/jvmMain/resources/drawable/ic_instagram.xml deleted file mode 100644 index 1cd9bc2d..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_instagram.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml deleted file mode 100644 index 1a84ca9a..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_jio_saavn_logo.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_linkedin.xml b/desktop/src/jvmMain/resources/drawable/ic_linkedin.xml deleted file mode 100644 index 8b177562..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_linkedin.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_mug.xml b/desktop/src/jvmMain/resources/drawable/ic_mug.xml deleted file mode 100644 index c8260781..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_mug.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_musicplaceholder.xml b/desktop/src/jvmMain/resources/drawable/ic_musicplaceholder.xml deleted file mode 100644 index 7d304388..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_musicplaceholder.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml b/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml deleted file mode 100644 index b1ac9100..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_opencollective_icon.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_paypal_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_paypal_logo.xml deleted file mode 100644 index 933369b5..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_paypal_logo.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_refreshgradient.xml b/desktop/src/jvmMain/resources/drawable/ic_refreshgradient.xml deleted file mode 100644 index 47805ea8..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_refreshgradient.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_round_cancel_24.xml b/desktop/src/jvmMain/resources/drawable/ic_round_cancel_24.xml deleted file mode 100644 index a5eacb39..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_round_cancel_24.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_share_open.xml b/desktop/src/jvmMain/resources/drawable/ic_share_open.xml deleted file mode 100644 index dbd7c1a4..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_share_open.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_song_placeholder.xml b/desktop/src/jvmMain/resources/drawable/ic_song_placeholder.xml deleted file mode 100644 index 04a9c803..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_song_placeholder.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_spotiflyer_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_spotiflyer_logo.xml deleted file mode 100644 index 8712d43c..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_spotiflyer_logo.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_spotify_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_spotify_logo.xml deleted file mode 100644 index e773449a..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_spotify_logo.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_tick.xml b/desktop/src/jvmMain/resources/drawable/ic_tick.xml deleted file mode 100644 index 47903522..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_tick.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_youtube.xml b/desktop/src/jvmMain/resources/drawable/ic_youtube.xml deleted file mode 100644 index 728779f1..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_youtube.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/ic_youtube_music_logo.xml b/desktop/src/jvmMain/resources/drawable/ic_youtube_music_logo.xml deleted file mode 100644 index 7e723917..00000000 --- a/desktop/src/jvmMain/resources/drawable/ic_youtube_music_logo.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/music.xml b/desktop/src/jvmMain/resources/drawable/music.xml deleted file mode 100644 index 04a9c803..00000000 --- a/desktop/src/jvmMain/resources/drawable/music.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/desktop/src/jvmMain/resources/drawable/spotiflyer.png b/desktop/src/jvmMain/resources/drawable/spotiflyer.png index d4aba36e..74aebd66 100644 Binary files a/desktop/src/jvmMain/resources/drawable/spotiflyer.png and b/desktop/src/jvmMain/resources/drawable/spotiflyer.png differ diff --git a/desktop/src/jvmMain/resources/font/montserrat_light.ttf b/desktop/src/jvmMain/resources/font/montserrat_light.ttf deleted file mode 100644 index 990857de..00000000 Binary files a/desktop/src/jvmMain/resources/font/montserrat_light.ttf and /dev/null differ diff --git a/desktop/src/jvmMain/resources/font/montserrat_medium.ttf b/desktop/src/jvmMain/resources/font/montserrat_medium.ttf deleted file mode 100644 index 6e079f69..00000000 Binary files a/desktop/src/jvmMain/resources/font/montserrat_medium.ttf and /dev/null differ diff --git a/desktop/src/jvmMain/resources/font/montserrat_regular.ttf b/desktop/src/jvmMain/resources/font/montserrat_regular.ttf deleted file mode 100644 index 8d443d5d..00000000 Binary files a/desktop/src/jvmMain/resources/font/montserrat_regular.ttf and /dev/null differ diff --git a/desktop/src/jvmMain/resources/font/montserrat_semibold.ttf b/desktop/src/jvmMain/resources/font/montserrat_semibold.ttf deleted file mode 100644 index f8a43f2b..00000000 Binary files a/desktop/src/jvmMain/resources/font/montserrat_semibold.ttf and /dev/null differ diff --git a/desktop/src/jvmMain/resources/font/pristine_script.ttf b/desktop/src/jvmMain/resources/font/pristine_script.ttf deleted file mode 100644 index e8d3e494..00000000 Binary files a/desktop/src/jvmMain/resources/font/pristine_script.ttf and /dev/null differ diff --git a/ffmpeg/android-ffmpeg/build.gradle.kts b/ffmpeg/android-ffmpeg/build.gradle.kts index 24a62ad6..be58fa56 100644 --- a/ffmpeg/android-ffmpeg/build.gradle.kts +++ b/ffmpeg/android-ffmpeg/build.gradle.kts @@ -13,8 +13,6 @@ android { minSdk = Versions.minSdkVersion targetSdk = Versions.targetSdkVersion -// versionCode = Versions.versionCode -// versionName = Versions.versionName /*ndk { abiFilters.addAll(setOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a")) diff --git a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/CpuArchHelper.java b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/CpuArchHelper.java index a363885e..19e681cc 100644 --- a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/CpuArchHelper.java +++ b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/CpuArchHelper.java @@ -2,6 +2,7 @@ package nl.bravobit.ffmpeg; import android.os.Build; +@SuppressWarnings("deprecation") public class CpuArchHelper { public static final String X86_CPU = "x86"; public static final String X86_64_CPU = "x86_64"; diff --git a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFcommandExecuteAsyncTask.java b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFcommandExecuteAsyncTask.java index 7ec21b44..83f93c79 100644 --- a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFcommandExecuteAsyncTask.java +++ b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFcommandExecuteAsyncTask.java @@ -1,7 +1,6 @@ package nl.bravobit.ffmpeg; import android.os.AsyncTask; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -9,10 +8,12 @@ import java.io.OutputStream; import java.util.Map; import java.util.concurrent.TimeoutException; +@SuppressWarnings("deprecation") class FFcommandExecuteAsyncTask extends AsyncTask implements FFtask { private final String[] cmd; - private Map environment; + private final Map environment; + private final StringBuilder outputStringBuilder = new StringBuilder(); private final FFcommandExecuteResponseHandler ffmpegExecuteResponseHandler; private final ShellCommand shellCommand; private final long timeout; @@ -39,6 +40,7 @@ class FFcommandExecuteAsyncTask extends AsyncTask i @Override protected CommandResult doInBackground(Void... params) { + CommandResult ret = CommandResult.getDummyFailureResponse(); try { process = shellCommand.run(cmd, environment); if (process == null) { @@ -46,16 +48,19 @@ class FFcommandExecuteAsyncTask extends AsyncTask i } Log.d("Running publishing updates method"); checkAndUpdateProcess(); - return CommandResult.getOutputFromProcess(process); + ret = CommandResult.getOutputFromProcess(process); + outputStringBuilder.append(ret.output); } catch (TimeoutException e) { Log.e("FFmpeg binary timed out", e); - return new CommandResult(false, e.getMessage()); + ret = new CommandResult(false, e.getMessage()); + outputStringBuilder.append(ret.output); } catch (Exception e) { Log.e("Error running FFmpeg binary", e); } finally { Util.destroyProcess(process); } - return CommandResult.getDummyFailureResponse(); + output = outputStringBuilder.toString(); + return ret; } @Override @@ -68,7 +73,6 @@ class FFcommandExecuteAsyncTask extends AsyncTask i @Override protected void onPostExecute(CommandResult commandResult) { if (ffmpegExecuteResponseHandler != null) { - output += commandResult.output; if (commandResult.success) { ffmpegExecuteResponseHandler.onSuccess(output); } else { @@ -107,7 +111,7 @@ class FFcommandExecuteAsyncTask extends AsyncTask i return; } - output += line + "\n"; + outputStringBuilder.append(line); outputStringBuilder.append("\n"); publishProgress(line); } } catch (IOException e) { @@ -139,4 +143,4 @@ class FFcommandExecuteAsyncTask extends AsyncTask i e.printStackTrace(); } } -} +} \ No newline at end of file diff --git a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFprobe.java b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFprobe.java index 447229d6..76bf63b6 100644 --- a/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFprobe.java +++ b/ffmpeg/android-ffmpeg/src/main/java/nl/bravobit/ffmpeg/FFprobe.java @@ -6,6 +6,7 @@ import android.os.AsyncTask; import java.io.File; import java.util.Map; +@SuppressWarnings("deprecation") public class FFprobe implements FFbinaryInterface { private final FFbinaryContextProvider context; @@ -22,12 +23,7 @@ public class FFprobe implements FFbinaryInterface { public static FFprobe getInstance(final Context context) { if (instance == null) { - instance = new FFprobe(new FFbinaryContextProvider() { - @Override - public Context provide() { - return context; - } - }); + instance = new FFprobe(() -> context); } return instance; } diff --git a/gradle.properties b/gradle.properties index b2589535..61c33e1d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m +org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/maintenance-tasks/build.gradle.kts b/maintenance-tasks/build.gradle.kts index b0395302..440b0d2c 100644 --- a/maintenance-tasks/build.gradle.kts +++ b/maintenance-tasks/build.gradle.kts @@ -18,16 +18,15 @@ application { } dependencies { - implementation(Ktor.slf4j) - implementation(Ktor.clientCore) - implementation(Ktor.clientJson) - implementation(Ktor.clientApache) - implementation(Ktor.clientLogging) - implementation(Ktor.clientSerialization) - implementation(Serialization.json) + with(deps) { + implementation(slf4j.simple) + implementation(bundles.ktor) + implementation(ktor.client.apache) + implementation(kotlinx.serialization.json) - // testDeps - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.5.21") + // testDep + testImplementation(kotlin.kotlinTestJunit) + } } tasks.test { diff --git a/settings.gradle.kts b/settings.gradle.kts index ed7aeddd..755545b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,16 @@ * * along with this program. If not, see . */ +enableFeaturePreview("VERSION_CATALOGS") +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + versionCatalogs { + create("deps") { + from(files("buildSrc/deps.versions.toml")) + } + } +} + rootProject.name = "spotiflyer" include( @@ -32,5 +42,5 @@ include( ":desktop", ":web-app", //":console-app", - ":maintenance-tasks" + ":maintenance-tasks", ) diff --git a/web-app/build.gradle.kts b/web-app/build.gradle.kts index e13b2509..fb0a4424 100644 --- a/web-app/build.gradle.kts +++ b/web-app/build.gradle.kts @@ -21,20 +21,17 @@ plugins { group = "com.shabinder" version = "0.1" -repositories { - mavenCentral() - //maven(url = "https://dl.bintray.com/kotlin/kotlin-js-wrappers") -} - dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-js:1.5.21") - implementation(Koin.core) - implementation(Extras.kermit) - implementation(Decompose.decompose) - implementation(MVIKotlin.mvikotlin) - implementation(MVIKotlin.coroutines) - implementation(MVIKotlin.mvikotlinMain) - implementation(MVIKotlin.mvikotlinLogging) + with(deps) { + implementation(koin.core) + implementation(decompose.dep) + implementation(ktor.client.js) + with(bundles) { + implementation(mviKotlin) + implementation(ktor) + implementation(kotlin.js.wrappers) + } + } implementation(project(":common:root")) implementation(project(":common:main")) implementation(project(":common:list")) @@ -43,27 +40,7 @@ dependencies { implementation(project(":common:providers")) implementation(project(":common:core-components")) implementation(project(":common:dependency-injection")) - implementation("co.touchlab:stately-common:1.1.7") - implementation("dev.icerock.moko:parcelize:${Versions.mokoParcelize}") - // implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") - - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") { - // https://youtrack.jetbrains.com/issue/KTOR-2670 - @Suppress("DEPRECATION") - isForce = true - } - - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt") { - @Suppress("DEPRECATION") - isForce = true - } - - with(KotlinJSWrappers) { - implementation(enforcedPlatform(bom)) - implementation(kotlinReact) - implementation(kotlinReactDom) - implementation(kotlinStyled) - } + implementation("org.jetbrains.kotlin:kotlin-stdlib-js:${deps.kotlin.kotlinGradlePlugin.get().versionConstraint.requiredVersion}") } kotlin { @@ -85,4 +62,10 @@ kotlin { } binaries.executable() } + // WorkAround: https://youtrack.jetbrains.com/issue/KT-49124 + rootProject.plugins.withType { + rootProject.the().apply { + resolution("@webpack-cli/serve", "1.5.2") + } + } } \ No newline at end of file diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt index 0f2ee177..f8cacb7b 100644 --- a/web-app/src/main/kotlin/App.kt +++ b/web-app/src/main/kotlin/App.kt @@ -23,21 +23,25 @@ import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.shabinder.common.core_components.file_manager.DownloadProgressFlow import com.shabinder.common.core_components.preference_manager.PreferenceManager +import com.shabinder.common.di.ApplicationInit import com.shabinder.common.models.Actions import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.TrackDetails import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.database.Database import extras.renderableChild -import react.* +import react.PropsWithChildren +import react.RBuilder +import react.RComponent +import react.State import root.RootR -external interface AppProps : RProps { +external interface AppProps : PropsWithChildren { var dependencies: AppDependencies } @Suppress("FunctionName") -fun RBuilder.App(attrs: AppProps.() -> Unit): ReactElement { +fun RBuilder.App(attrs: AppProps.() -> Unit) { return child(App::class) { this.attrs(attrs) } @@ -46,7 +50,7 @@ fun RBuilder.App(attrs: AppProps.() -> Unit): ReactElement { @Suppress("EXPERIMENTAL_IS_NOT_ENABLED", "NON_EXPORTABLE_TYPE") @OptIn(ExperimentalJsExport::class) @JsExport -class App(props: AppProps) : RComponent(props) { +class App(props: AppProps) : RComponent(props) { private val lifecycle = LifecycleRegistry() private val ctx = DefaultComponentContext(lifecycle = lifecycle) @@ -62,6 +66,7 @@ class App(props: AppProps) : RComponent(props) { override val fetchQuery = dependencies.fetchPlatformQueryResult override val fileManager = dependencies.fileManager override val analyticsManager = dependencies.analyticsManager + override val appInit: ApplicationInit = dependencies.appInit override val preferenceManager: PreferenceManager = dependencies.preferenceManager override val database: Database? = fileManager.db override val downloadProgressFlow = DownloadProgressFlow diff --git a/web-app/src/main/kotlin/client.kt b/web-app/src/main/kotlin/client.kt index 1efce0ae..50ca5c69 100644 --- a/web-app/src/main/kotlin/client.kt +++ b/web-app/src/main/kotlin/client.kt @@ -18,6 +18,7 @@ import co.touchlab.kermit.Kermit 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.ApplicationInit import com.shabinder.common.di.initKoin import com.shabinder.common.providers.FetchPlatformQueryResult import kotlinx.browser.document @@ -42,6 +43,7 @@ object AppDependencies : KoinComponent { val fetchPlatformQueryResult: FetchPlatformQueryResult val preferenceManager: PreferenceManager val analyticsManager: AnalyticsManager + val appInit: ApplicationInit init { initKoin() fileManager = get() @@ -49,5 +51,6 @@ object AppDependencies : KoinComponent { fetchPlatformQueryResult = get() preferenceManager = get() analyticsManager = get() + appInit = get() } } \ No newline at end of file diff --git a/web-app/src/main/kotlin/extras/RenderableComponent.kt b/web-app/src/main/kotlin/extras/RenderableComponent.kt index cc3731a0..dc37157c 100644 --- a/web-app/src/main/kotlin/extras/RenderableComponent.kt +++ b/web-app/src/main/kotlin/extras/RenderableComponent.kt @@ -18,16 +18,16 @@ package extras import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.ValueObserver +import react.PropsWithChildren import react.RComponent -import react.RProps -import react.RState +import react.State import react.setState @Suppress("EXPERIMENTAL_IS_NOT_ENABLED", "NON_EXPORTABLE_TYPE") @OptIn(ExperimentalJsExport::class) @JsExport -abstract class RenderableComponent( +abstract class RenderableComponent( props: Props, initialState: S ) : RComponent, S>(props) { @@ -60,7 +60,6 @@ abstract class RenderableComponent( } - protected class Subscription( val value: Value, val observer: ValueObserver @@ -72,8 +71,8 @@ abstract class RenderableComponent( @JsExport class RStateWrapper( var model: T -) : RState +) : State -external interface Props : RProps { +external interface Props : PropsWithChildren { var component: T } \ No newline at end of file diff --git a/web-app/src/main/kotlin/home/IconList.kt b/web-app/src/main/kotlin/home/IconList.kt index 1b751793..af947314 100644 --- a/web-app/src/main/kotlin/home/IconList.kt +++ b/web-app/src/main/kotlin/home/IconList.kt @@ -17,39 +17,49 @@ package home import Styles -import kotlinx.css.* +import kotlinx.css.borderRadius +import kotlinx.css.height +import kotlinx.css.margin +import kotlinx.css.px +import kotlinx.css.width import kotlinx.html.id -import react.* +import react.PropsWithChildren +import react.RBuilder import react.dom.attrs -import styled.* +import react.functionComponent +import styled.css +import styled.styledA +import styled.styledDiv +import styled.styledForm +import styled.styledImg -external interface IconListProps : RProps { - var iconsAndPlatforms: Map - var isBadge:Boolean +external interface IconListProps : PropsWithChildren { + var iconsAndPlatforms: Map + var isBadge: Boolean } @Suppress("FunctionName") -fun RBuilder.IconList(handler:IconListProps.() -> Unit): ReactElement { - return child(iconList){ +fun RBuilder.IconList(handler: IconListProps.() -> Unit) { + return child(iconList) { attrs { handler() } } } -private val iconList = functionalComponent("IconList") { props -> +private val iconList = functionComponent("IconList") { props -> styledDiv { css { margin(18.px) - if(props.isBadge) { - classes = mutableListOf("info-banners") + if (props.isBadge) { + classes.add("info-banners") } - + Styles.makeRow + +Styles.makeRow } val firstElem = props.iconsAndPlatforms.keys.elementAt(1) - for((icon,platformLink) in props.iconsAndPlatforms){ - if(icon == firstElem && props.isBadge){ + for ((icon, platformLink) in props.iconsAndPlatforms) { + if (icon == firstElem && props.isBadge) { //
styledForm { attrs { @@ -57,13 +67,13 @@ private val iconList = functionalComponent("IconList") { props -> } } } - styledA(href = platformLink,target="_blank"){ + styledA(href = platformLink, target = "_blank") { styledImg { attrs { src = icon } css { - classes = mutableListOf("glow-button") + classes.add("glow-button") margin(8.px) if (!props.isBadge) { height = 42.px diff --git a/web-app/src/main/kotlin/home/Message.kt b/web-app/src/main/kotlin/home/Message.kt index 4896de97..04f31a45 100644 --- a/web-app/src/main/kotlin/home/Message.kt +++ b/web-app/src/main/kotlin/home/Message.kt @@ -18,34 +18,32 @@ package home import kotlinx.css.em import kotlinx.css.fontSize +import react.PropsWithChildren import react.RBuilder -import react.RProps -import react.ReactElement -import react.child -import react.functionalComponent +import react.functionComponent import styled.css import styled.styledDiv import styled.styledH1 -external interface MessageProps : RProps { +external interface MessageProps : PropsWithChildren { var text: String } @Suppress("FunctionName") -fun RBuilder.Message(handler:MessageProps.() -> Unit): ReactElement { - return child(message){ +fun RBuilder.Message(handler: MessageProps.() -> Unit) { + return child(message) { attrs { handler() } } } -private val message = functionalComponent("Message") { props-> +private val message = functionComponent("Message") { props -> styledDiv { styledH1 { - + props.text + +props.text css { - classes = mutableListOf("headingTitle") + classes.add("headingTitle") fontSize = 2.6.em } } diff --git a/web-app/src/main/kotlin/home/Searchbar.kt b/web-app/src/main/kotlin/home/Searchbar.kt index c611f1a3..30b9bdaa 100644 --- a/web-app/src/main/kotlin/home/Searchbar.kt +++ b/web-app/src/main/kotlin/home/Searchbar.kt @@ -22,32 +22,35 @@ import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onClickFunction import kotlinx.html.js.onKeyDownFunction import org.w3c.dom.HTMLInputElement +import react.PropsWithChildren import react.RBuilder -import react.RProps -import react.child import react.dom.attrs -import react.functionalComponent -import styled.* +import react.functionComponent +import styled.css +import styled.styledButton +import styled.styledDiv +import styled.styledImg +import styled.styledInput -external interface SearchbarProps : RProps { +external interface SearchbarProps : PropsWithChildren { var link: String - var search:(String)->Unit - var onLinkChange:(String)->Unit + var search: (String) -> Unit + var onLinkChange: (String) -> Unit } @Suppress("FunctionName") -fun RBuilder.SearchBar(handler:SearchbarProps.() -> Unit) = child(searchbar){ +fun RBuilder.SearchBar(handler: SearchbarProps.() -> Unit) = child(searchbar) { attrs { handler() } } -val searchbar = functionalComponent("SearchBar"){ props -> - styledDiv{ +val searchbar = functionComponent("SearchBar") { props -> + styledDiv { css { - classes = mutableListOf("searchBox") + classes.add("searchBox") } - styledInput(type = InputType.url){ + styledInput(type = InputType.url) { attrs { placeholder = "Search" onChangeFunction = { @@ -55,30 +58,30 @@ val searchbar = functionalComponent("SearchBar"){ props -> props.onLinkChange(target.value) } this.onKeyDownFunction = { - if(it.asDynamic().key == "Enter") { - if(props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms") + if (it.asDynamic().key == "Enter") { + if (props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms") else props.search(props.link) } } value = props.link } css { - classes = mutableListOf("searchInput") + classes.add("searchInput") } } styledButton { attrs { onClickFunction = { - if(props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms") + if (props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms") else props.search(props.link) } } css { - classes = mutableListOf("searchButton") + classes.add("searchButton") } styledImg(src = "search.svg") { css { - classes = mutableListOf("search-icon") + classes.add("search-icon") } } } diff --git a/web-app/src/main/kotlin/list/CircularProgressBar.kt b/web-app/src/main/kotlin/list/CircularProgressBar.kt index 165a072d..42c51c6a 100644 --- a/web-app/src/main/kotlin/list/CircularProgressBar.kt +++ b/web-app/src/main/kotlin/list/CircularProgressBar.kt @@ -23,42 +23,44 @@ import kotlinx.css.justifyContent import kotlinx.css.marginBottom import kotlinx.css.px import kotlinx.css.width +import react.PropsWithChildren import react.RBuilder -import react.RProps -import react.ReactElement -import react.child -import react.functionalComponent +import react.functionComponent import styled.css import styled.styledDiv import styled.styledSpan @Suppress("FunctionName") -fun RBuilder.CircularProgressBar(handler: CircularProgressBarProps.() -> Unit): ReactElement { - return child(circularProgressBar){ +fun RBuilder.CircularProgressBar(handler: CircularProgressBarProps.() -> Unit) { + return child(circularProgressBar) { attrs { handler() } } } -external interface CircularProgressBarProps : RProps { - var progress:Int +external interface CircularProgressBarProps : PropsWithChildren { + var progress: Int } -private val circularProgressBar = functionalComponent("Circular-Progress-Bar") { props-> +private val circularProgressBar = functionComponent("Circular-Progress-Bar") { props -> styledDiv { styledSpan { +"${props.progress}%" } - styledDiv{ + styledDiv { css { - classes = mutableListOf("left-half-clipper") + classes.add("left-half-clipper") } - styledDiv{ css { classes = mutableListOf("first50-bar") } } - styledDiv{ css { classes = mutableListOf("value-bar") } } + styledDiv { css { classes.add("first50-bar") } } + styledDiv { css { classes.add("value-bar") } } } - css{ + css { display = Display.flex justifyContent = JustifyContent.center - classes = mutableListOf("progress-circle","p${props.progress}").apply { if(props.progress>50) add("over50") } + classes.addAll( + mutableListOf( + "progress-circle", + "p${props.progress}" + ).apply { if (props.progress > 50) add("over50") }) width = 50.px marginBottom = 65.px } diff --git a/web-app/src/main/kotlin/list/CoverImage.kt b/web-app/src/main/kotlin/list/CoverImage.kt index da2d489b..419e1503 100644 --- a/web-app/src/main/kotlin/list/CoverImage.kt +++ b/web-app/src/main/kotlin/list/CoverImage.kt @@ -16,32 +16,45 @@ package list -import kotlinx.css.* +import kotlinx.css.Align +import kotlinx.css.Display +import kotlinx.css.FlexDirection +import kotlinx.css.TextAlign +import kotlinx.css.alignItems +import kotlinx.css.display +import kotlinx.css.flexDirection +import kotlinx.css.height +import kotlinx.css.marginTop +import kotlinx.css.px +import kotlinx.css.textAlign +import kotlinx.css.width import kotlinx.html.id -import react.* +import react.PropsWithChildren +import react.RBuilder import react.dom.attrs +import react.functionComponent import styled.css import styled.styledDiv import styled.styledH1 import styled.styledImg -external interface CoverImageProps : RProps { +external interface CoverImageProps : PropsWithChildren { var coverImageURL: String var coverName: String } @Suppress("FunctionName") -fun RBuilder.CoverImage(handler: CoverImageProps.() -> Unit): ReactElement { - return child(coverImage){ +fun RBuilder.CoverImage(handler: CoverImageProps.() -> Unit) { + return child(coverImage) { attrs { handler() } } } -private val coverImage = functionalComponent("CoverImage"){ props -> +private val coverImage = functionComponent("CoverImage") { props -> styledDiv { - styledImg(src= props.coverImageURL){ + styledImg(src = props.coverImageURL) { css { height = 220.px width = 220.px diff --git a/web-app/src/main/kotlin/list/DownloadAllButton.kt b/web-app/src/main/kotlin/list/DownloadAllButton.kt index 6e79af2f..2dd4a0e9 100644 --- a/web-app/src/main/kotlin/list/DownloadAllButton.kt +++ b/web-app/src/main/kotlin/list/DownloadAllButton.kt @@ -16,52 +16,65 @@ package list -import kotlinx.css.* +import kotlinx.css.Align +import kotlinx.css.Display +import kotlinx.css.JustifyContent +import kotlinx.css.WhiteSpace +import kotlinx.css.alignItems +import kotlinx.css.display +import kotlinx.css.fontSize +import kotlinx.css.height +import kotlinx.css.justifyContent +import kotlinx.css.px +import kotlinx.css.whiteSpace import kotlinx.html.id import kotlinx.html.js.onClickFunction -import react.* +import react.PropsWithChildren +import react.RBuilder import react.dom.attrs +import react.functionComponent +import react.useEffect +import react.useState import styled.css import styled.styledDiv import styled.styledH5 import styled.styledImg -external interface DownloadAllButtonProps : RProps { - var isActive:Boolean - var link : String - var downloadAll:()->Unit +external interface DownloadAllButtonProps : PropsWithChildren { + var isActive: Boolean + var link: String + var downloadAll: () -> Unit } @Suppress("FunctionName") -fun RBuilder.DownloadAllButton(handler: DownloadAllButtonProps.() -> Unit): ReactElement { - return child(downloadAllButton){ +fun RBuilder.DownloadAllButton(handler: DownloadAllButtonProps.() -> Unit) { + return child(downloadAllButton) { attrs { handler() } } } -private val downloadAllButton = functionalComponent("DownloadAllButton") { props-> +private val downloadAllButton = functionComponent("DownloadAllButton") { props -> - val (isClicked,setClicked) = useState(false) + val (isClicked, setClicked) = useState(false) - useEffect(mutableListOf(props.link)){ + useEffect(mutableListOf(props.link)) { setClicked(false) } - if(props.isActive){ - if(isClicked) { - styledDiv{ + if (props.isActive) { + if (isClicked) { + styledDiv { css { display = Display.flex alignItems = Align.center justifyContent = JustifyContent.center height = 52.px } - LoadingSpinner { } + LoadingSpinner { } } - } - else{ + } else { styledDiv { attrs { onClickFunction = { @@ -71,9 +84,9 @@ private val downloadAllButton = functionalComponent("Dow } styledDiv { - styledImg(src = "download.svg",alt = "Download All Button") { + styledImg(src = "download.svg", alt = "Download All Button") { css { - classes = mutableListOf("download-all-icon") + classes.add("download-all-icon") height = 32.px } } @@ -82,7 +95,7 @@ private val downloadAllButton = functionalComponent("Dow attrs { id = "download-all-text" } - + "Download All" + +"Download All" css { whiteSpace = WhiteSpace.nowrap fontSize = 15.px @@ -90,13 +103,13 @@ private val downloadAllButton = functionalComponent("Dow } css { - classes = mutableListOf("download-icon") + classes.add("download-icon") display = Display.flex alignItems = Align.center } } css { - classes = mutableListOf("download-button") + classes.add("download-button") display = Display.flex alignItems = Align.center } diff --git a/web-app/src/main/kotlin/list/DownloadButton.kt b/web-app/src/main/kotlin/list/DownloadButton.kt index 922105c8..0b0f03a8 100644 --- a/web-app/src/main/kotlin/list/DownloadButton.kt +++ b/web-app/src/main/kotlin/list/DownloadButton.kt @@ -17,31 +17,37 @@ package list import com.shabinder.common.models.DownloadStatus -import kotlinx.css.* +import kotlinx.css.borderRadius +import kotlinx.css.em +import kotlinx.css.margin +import kotlinx.css.px +import kotlinx.css.width import kotlinx.html.js.onClickFunction -import react.* +import react.PropsWithChildren +import react.RBuilder import react.dom.attrs +import react.functionComponent import styled.css import styled.styledDiv import styled.styledImg @Suppress("FunctionName") -fun RBuilder.DownloadButton(handler: DownloadButtonProps.() -> Unit): ReactElement { - return child(downloadButton){ +fun RBuilder.DownloadButton(handler: DownloadButtonProps.() -> Unit) { + return child(downloadButton) { attrs { handler() } } } -external interface DownloadButtonProps : RProps { - var onClick:()->Unit - var status :DownloadStatus +external interface DownloadButtonProps : PropsWithChildren { + var onClick: () -> Unit + var status: DownloadStatus } -private val downloadButton = functionalComponent("Circular-Progress-Bar") { props-> +private val downloadButton = functionComponent("Circular-Progress-Bar") { props -> styledDiv { - val src = when(props.status){ + val src = when (props.status) { is DownloadStatus.NotDownloaded -> "download-gradient.svg" is DownloadStatus.Downloaded -> "check.svg" is DownloadStatus.Failed -> "error.svg" @@ -59,7 +65,7 @@ private val downloadButton = functionalComponent("Circular- } } css { - classes = mutableListOf("glow-button") + classes.add("glow-button") borderRadius = 100.px } } diff --git a/web-app/src/main/kotlin/list/ListScreen.kt b/web-app/src/main/kotlin/list/ListScreen.kt index 4fb0a767..ab9f3a51 100644 --- a/web-app/src/main/kotlin/list/ListScreen.kt +++ b/web-app/src/main/kotlin/list/ListScreen.kt @@ -84,7 +84,7 @@ class ListScreen( } css { - classes = mutableListOf("list-screen") + classes.add("list-screen") display = Display.flex padding(8.px) flexDirection = FlexDirection.column diff --git a/web-app/src/main/kotlin/list/LoadingAnim.kt b/web-app/src/main/kotlin/list/LoadingAnim.kt index f618c280..9d738f94 100644 --- a/web-app/src/main/kotlin/list/LoadingAnim.kt +++ b/web-app/src/main/kotlin/list/LoadingAnim.kt @@ -24,43 +24,41 @@ import kotlinx.css.flexGrow import kotlinx.css.height import kotlinx.css.px import kotlinx.css.width +import react.PropsWithChildren import react.RBuilder -import react.RProps -import react.ReactElement -import react.child -import react.functionalComponent +import react.functionComponent import styled.css import styled.styledDiv @Suppress("FunctionName") -fun RBuilder.LoadingAnim(handler: RProps.() -> Unit): ReactElement { - return child(loadingAnim){ +fun RBuilder.LoadingAnim(handler: PropsWithChildren.() -> Unit) { + return child(loadingAnim) { attrs { handler() } } } -private val loadingAnim = functionalComponent("Loading Animation") { - styledDiv{ +private val loadingAnim = functionComponent("Loading Animation") { + styledDiv { css { flexGrow = 1.0 display = Display.flex alignItems = Align.center } styledDiv { - styledDiv { css { classes = mutableListOf("sk-cube sk-cube1") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube2") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube3") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube4") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube5") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube6") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube7") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube8") } } - styledDiv { css { classes = mutableListOf("sk-cube sk-cube9") } } + styledDiv { css { classes.add("sk-cube sk-cube1") } } + styledDiv { css { classes.add("sk-cube sk-cube2") } } + styledDiv { css { classes.add("sk-cube sk-cube3") } } + styledDiv { css { classes.add("sk-cube sk-cube4") } } + styledDiv { css { classes.add("sk-cube sk-cube5") } } + styledDiv { css { classes.add("sk-cube sk-cube6") } } + styledDiv { css { classes.add("sk-cube sk-cube7") } } + styledDiv { css { classes.add("sk-cube sk-cube8") } } + styledDiv { css { classes.add("sk-cube sk-cube9") } } css { - classes = mutableListOf("sk-cube-grid") + classes.add("sk-cube-grid") height = 60.px width = 60.px } diff --git a/web-app/src/main/kotlin/list/LoadingSpinner.kt b/web-app/src/main/kotlin/list/LoadingSpinner.kt index a03cb107..2bd9cece 100644 --- a/web-app/src/main/kotlin/list/LoadingSpinner.kt +++ b/web-app/src/main/kotlin/list/LoadingSpinner.kt @@ -19,31 +19,29 @@ package list import kotlinx.css.marginRight import kotlinx.css.px import kotlinx.css.width +import react.PropsWithChildren import react.RBuilder -import react.RProps -import react.ReactElement -import react.child -import react.functionalComponent +import react.functionComponent import styled.css import styled.styledDiv @Suppress("FunctionName") -fun RBuilder.LoadingSpinner(handler: RProps.() -> Unit): ReactElement { - return child(loadingSpinner){ +fun RBuilder.LoadingSpinner(handler: PropsWithChildren.() -> Unit) { + return child(loadingSpinner) { attrs { handler() } } } -private val loadingSpinner = functionalComponent("Loading-Spinner") { +private val loadingSpinner = functionComponent("Loading-Spinner") { styledDiv { - styledDiv{} - styledDiv{} - styledDiv{} - styledDiv{} - css{ - classes = mutableListOf("lds-ring") + styledDiv {} + styledDiv {} + styledDiv {} + styledDiv {} + css { + classes.add("lds-ring") width = 50.px marginRight = 8.px } diff --git a/web-app/src/main/kotlin/list/TrackItem.kt b/web-app/src/main/kotlin/list/TrackItem.kt index 1d66d6b3..df04c40a 100644 --- a/web-app/src/main/kotlin/list/TrackItem.kt +++ b/web-app/src/main/kotlin/list/TrackItem.kt @@ -18,30 +18,61 @@ package list import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails -import kotlinx.css.* +import kotlinx.css.Align +import kotlinx.css.Display +import kotlinx.css.FlexDirection +import kotlinx.css.Overflow +import kotlinx.css.TextAlign +import kotlinx.css.TextOverflow +import kotlinx.css.WhiteSpace +import kotlinx.css.alignItems +import kotlinx.css.display +import kotlinx.css.em +import kotlinx.css.flexDirection +import kotlinx.css.flexGrow +import kotlinx.css.fontSize +import kotlinx.css.height +import kotlinx.css.margin +import kotlinx.css.minWidth +import kotlinx.css.overflow +import kotlinx.css.padding +import kotlinx.css.paddingRight +import kotlinx.css.px +import kotlinx.css.textAlign +import kotlinx.css.textOverflow +import kotlinx.css.whiteSpace +import kotlinx.css.width import kotlinx.html.id -import react.* +import react.PropsWithChildren +import react.RBuilder import react.dom.attrs -import styled.* +import react.functionComponent +import react.useEffect +import react.useState +import styled.css +import styled.styledDiv +import styled.styledH3 +import styled.styledH4 +import styled.styledImg -external interface TrackItemProps : RProps { - var details:TrackDetails - var downloadTrack:(TrackDetails)->Unit +external interface TrackItemProps : PropsWithChildren { + var details: TrackDetails + var downloadTrack: (TrackDetails) -> Unit } @Suppress("FunctionName") -fun RBuilder.TrackItem(handler: TrackItemProps.() -> Unit): ReactElement { - return child(trackItem){ +fun RBuilder.TrackItem(handler: TrackItemProps.() -> Unit) { + return child(trackItem) { attrs { handler() } } } -private val trackItem = functionalComponent("Track-Item"){ props -> - val (downloadStatus,setDownloadStatus) = useState(props.details.downloaded) +private val trackItem = functionComponent("Track-Item") { props -> + val (downloadStatus, setDownloadStatus) = useState(props.details.downloaded) val details = props.details - useEffect(listOf(props.details)){ + useEffect(listOf(props.details)) { setDownloadStatus(props.details.downloaded) } styledDiv { @@ -63,14 +94,14 @@ private val trackItem = functionalComponent("Track-Item"){ props flexDirection = FlexDirection.column margin(8.px) } - styledDiv{ + styledDiv { css { height = 40.px alignItems = Align.center display = Display.flex } styledH3 { - + details.title + +details.title css { padding(8.px) fontSize = 1.3.em @@ -87,7 +118,7 @@ private val trackItem = functionalComponent("Track-Item"){ props display = Display.flex } styledH4 { - + details.artists.joinToString(",") + +details.artists.joinToString(",") css { flexGrow = 1.0 padding(8.px) @@ -109,12 +140,12 @@ private val trackItem = functionalComponent("Track-Item"){ props whiteSpace = WhiteSpace.nowrap overflow = Overflow.hidden } - + "${details.durationSec/60} min, ${details.durationSec%60} sec" + +"${details.durationSec / 60} min, ${details.durationSec % 60} sec" } } } - when(downloadStatus){ - is DownloadStatus.NotDownloaded ->{ + when (downloadStatus) { + is DownloadStatus.NotDownloaded -> { DownloadButton { onClick = { setDownloadStatus(DownloadStatus.Queued) @@ -152,7 +183,7 @@ private val trackItem = functionalComponent("Track-Item"){ props css { alignItems = Align.center - display =Display.flex + display = Display.flex paddingRight = 16.px } } diff --git a/web-app/src/main/kotlin/navbar/NavBar.kt b/web-app/src/main/kotlin/navbar/NavBar.kt index afd781c8..4903a57e 100644 --- a/web-app/src/main/kotlin/navbar/NavBar.kt +++ b/web-app/src/main/kotlin/navbar/NavBar.kt @@ -16,36 +16,55 @@ package navbar -import kotlinx.css.* +import kotlinx.css.Align +import kotlinx.css.Display +import kotlinx.css.LinearDimension +import kotlinx.css.alignItems +import kotlinx.css.display +import kotlinx.css.filter +import kotlinx.css.fontSize +import kotlinx.css.height +import kotlinx.css.margin +import kotlinx.css.marginLeft +import kotlinx.css.marginRight +import kotlinx.css.px +import kotlinx.css.width import kotlinx.html.id import kotlinx.html.js.onBlurFunction import kotlinx.html.js.onClickFunction -import react.* +import react.RBuilder +import react.RProps import react.dom.attrs -import styled.* +import react.functionComponent +import styled.css +import styled.styledA +import styled.styledDiv +import styled.styledH1 +import styled.styledImg +import styled.styledNav @Suppress("FunctionName") -fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{ - return child(navBar){ +fun RBuilder.NavBar(handler: NavBarProps.() -> Unit) { + return child(navBar) { attrs { handler() } } } -external interface NavBarProps:RProps{ +external interface NavBarProps : RProps { var isBackVisible: Boolean var popBackToHomeScreen: () -> Unit } -private val navBar = functionalComponent("NavBar") { props -> +private val navBar = functionComponent("NavBar") { props -> styledNav { css { +NavBarStyles.nav } - styledDiv{ + styledDiv { attrs { onClickFunction = { props.popBackToHomeScreen() @@ -54,22 +73,22 @@ private val navBar = functionalComponent("NavBar") { props -> props.popBackToHomeScreen() } } - styledImg(src = "left-arrow.svg",alt = "Back Arrow"){ + styledImg(src = "left-arrow.svg", alt = "Back Arrow") { css { height = 42.px width = 42.px - display = if(props.isBackVisible) Display.inline else Display.none + display = if (props.isBackVisible) Display.inline else Display.none filter = "invert(100)" marginRight = 12.px } } } - styledA(href = "https://shabinder.github.io/SpotiFlyer/",target="_blank") { + styledA(href = "https://shabinder.github.io/SpotiFlyer/", target = "_blank") { css { display = Display.flex alignItems = Align.center } - styledImg(src = "spotiflyer.svg",alt = "Logo") { + styledImg(src = "spotiflyer.svg", alt = "Logo") { css { height = 42.px width = 42.px @@ -80,7 +99,7 @@ private val navBar = functionalComponent("NavBar") { props -> attrs { id = "appName" } - css{ + css { fontSize = 46.px margin(horizontal = 14.px) } @@ -93,7 +112,7 @@ private val navBar = functionalComponent("NavBar") { props -> setCorsMode(corsProxy) }*/ - styledDiv{ + styledDiv { /*styledH4 { + "Extension" } @@ -125,8 +144,8 @@ private val navBar = functionalComponent("NavBar") { props -> } }*/ - styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){ - styledImg(src = "github.svg"){ + styledA(href = "https://github.com/Shabinder/SpotiFlyer/") { + styledImg(src = "github.svg") { css { height = 42.px width = 42.px diff --git a/web-app/src/main/kotlin/root/RootR.kt b/web-app/src/main/kotlin/root/RootR.kt index 5b4a76af..b7a1c4d5 100644 --- a/web-app/src/main/kotlin/root/RootR.kt +++ b/web-app/src/main/kotlin/root/RootR.kt @@ -26,7 +26,6 @@ import home.HomeScreen import list.ListScreen import navbar.NavBar import react.RBuilder -import react.RState class RootR(props: Props) : RenderableComponent( props = props, @@ -58,4 +57,4 @@ class RootR(props: Props) : RenderableComponent -) : RState +) : react.State