diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dc6ef57d..0185725f 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -66,7 +66,9 @@ dependencies { implementation(Koin.android) implementation(Koin.androidViewModel) - + //DECOMPOSE + implementation(Decompose.decompose) + implementation(Decompose.extensionsCompose) //Lifecycle Versions.androidLifecycle.let{ implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it") diff --git a/android/src/main/java/com/shabinder/android/App.kt b/android/src/main/java/com/shabinder/android/App.kt new file mode 100644 index 00000000..7e2722eb --- /dev/null +++ b/android/src/main/java/com/shabinder/android/App.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.android + +import android.app.Application +import com.shabinder.android.di.appModule +import com.shabinder.common.database.appContext +import com.shabinder.common.initKoin +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.KoinComponent + +class App: Application(), KoinComponent { + override fun onCreate() { + super.onCreate() + + appContext = this + + initKoin { + androidLogger() + androidContext(this@App) + modules(appModule) + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/android/MainActivity.kt b/android/src/main/java/com/shabinder/android/MainActivity.kt index f07388b3..a9f4b513 100644 --- a/android/src/main/java/com/shabinder/android/MainActivity.kt +++ b/android/src/main/java/com/shabinder/android/MainActivity.kt @@ -1,15 +1,14 @@ package com.shabinder.android import android.os.Bundle -import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.setContent -import com.shabinder.common.spotify.authenticateSpotify +import com.shabinder.android.di.appModule +import com.shabinder.common.database.appContext +import com.shabinder.common.initKoin import com.shabinder.common.ui.SpotiFlyerMain -import com.shabinder.common.youtube.YoutubeMusic -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import org.koin.android.ext.koin.androidLogger class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -17,9 +16,7 @@ class MainActivity : AppCompatActivity() { setContent { val scope = rememberCoroutineScope() SpotiFlyerMain() - scope.launch(Dispatchers.IO) { - } } } } \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/android/SharedViewModel.kt b/android/src/main/java/com/shabinder/android/SharedViewModel.kt new file mode 100644 index 00000000..e0fa5edd --- /dev/null +++ b/android/src/main/java/com/shabinder/android/SharedViewModel.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.android + +import android.content.Intent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import co.touchlab.kermit.Kermit +import com.shabinder.common.DownloadStatus +import com.shabinder.common.TrackDetails +import com.shabinder.common.YoutubeProvider +import com.shabinder.common.providers.GaanaProvider +import com.shabinder.common.providers.SpotifyProvider +import com.shabinder.database.Database +import com.shabinder.spotiflyer.ui.colorPrimaryDark +import com.tonyodev.fetch2.Status + +class SharedViewModel( + val database: Database, + val logger: Kermit, + val spotifyProvider: SpotifyProvider, + val gaanaProvider : GaanaProvider, + val youtubeProvider: YoutubeProvider +) : ViewModel() { + var isAuthenticated by mutableStateOf(false) + private set + + fun authenticated(s:Boolean) { + isAuthenticated = s + } + + /* + * Nav Gives Error on YT links with ? sign + * */ + var link by mutableStateOf("") + private set + + fun updateLink(s:String) { + link = s + } + + + val trackList = mutableStateListOf() + + fun updateTrackList(list:List){ + trackList.clear() + trackList.addAll(list) + } + fun updateTrackStatus(position:Int, status: DownloadStatus){ + if(position != -1){ + val track = trackList[position].apply { downloaded = status } + trackList[position] = track + } + } + + fun updateTrackStatus(intent: Intent){ + val trackDetails = intent.getSerializableExtra("track") as TrackDetails? + trackDetails?.let { + val position: Int = + trackList.map { trackState -> trackState.title }.indexOf(it.title) + logger.d{"$position, ${intent.action} , ${it.title}"} + if (position != -1) { + trackList.getOrNull(position)?.let{ track -> + when (intent.action) { + Status.QUEUED.name -> { + track.downloaded = DownloadStatus.Queued + } + Status.FAILED.name -> { + track.downloaded = DownloadStatus.Failed + } + Status.DOWNLOADING.name -> { + track.downloaded = DownloadStatus.Downloading + } + "Progress" -> { + //Progress Update + track.progress = intent.getIntExtra("progress", 0) + track.downloaded = DownloadStatus.Downloading + } + "Converting" -> { + //Progress Update + track.downloaded = DownloadStatus.Converting + } + "track_download_completed" -> { + track.downloaded = DownloadStatus.Downloaded + } + } + trackList[position] = track + logger.d{"TrackListUpdated"} + } + } + } + } + + var gradientColor by mutableStateOf(Color.Transparent) + private set + + fun updateGradientColor(color: Color) { + gradientColor = color + } + + fun resetGradient() { + gradientColor = colorPrimaryDark + } +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/android/di/AppModule.kt b/android/src/main/java/com/shabinder/android/di/AppModule.kt new file mode 100644 index 00000000..4a5a2101 --- /dev/null +++ b/android/src/main/java/com/shabinder/android/di/AppModule.kt @@ -0,0 +1,9 @@ +package com.shabinder.android.di + +import com.shabinder.android.SharedViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val appModule = module { + viewModel { SharedViewModel(get(),get(),get(),get(),get()) } +} diff --git a/android/src/main/java/com/shabinder/android/navigation/ComposeNavigation.kt b/android/src/main/java/com/shabinder/android/navigation/ComposeNavigation.kt new file mode 100644 index 00000000..978defc0 --- /dev/null +++ b/android/src/main/java/com/shabinder/android/navigation/ComposeNavigation.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.android.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.* +import com.shabinder.spotiflyer.MainActivity +import com.shabinder.spotiflyer.providers.GaanaProvider +import com.shabinder.spotiflyer.providers.SpotifyProvider +import com.shabinder.spotiflyer.providers.YoutubeProvider +import com.shabinder.common.ui.home.Home +import com.shabinder.spotiflyer.ui.tracklist.TrackList +import com.shabinder.spotiflyer.utils.sharedViewModel + +@Composable +fun ComposeNavigation( + mainActivity: MainActivity, + navController: NavHostController, + spotifyProvider: SpotifyProvider, + gaanaProvider: GaanaProvider, + youtubeProvider: YoutubeProvider, + ) { + NavHost( + navController = navController, + startDestination = "home" + ) { + + //HomeScreen - Starting Point + composable("home") { + Home( + navController = navController, + mainActivity, + ) + } + + //Spotify Screen + //Argument `link` = Link of Track/Album/Playlist + composable( + "track_list/{link}", + arguments = listOf(navArgument("link") { type = NavType.StringType }) + ) { + TrackList( + fullLink = it.arguments?.getString("link") ?: "error", + navController = navController, + spotifyProvider, + gaanaProvider, + youtubeProvider + ) + } + } +} + +fun NavController.navigateToTrackList(link:String, singleInstance: Boolean = true, inclusive:Boolean = false) { + sharedViewModel.updateLink(link) + navigate("track_list/$link") { + launchSingleTop = singleInstance + popUpTo(route = "home") { + this.inclusive = inclusive + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f2b06387..b64b99d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ allprojects { mavenCentral() maven(url = "https://jitpack.io") maven(url = "https://dl.bintray.com/ekito/koin") + maven(url = "https://kotlin.bintray.com/kotlinx/") maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers/") maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev") flatDir { diff --git a/buildSrc/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/buildSrc/src/main/kotlin/Deps.kt deleted file mode 100644 index 40d5463d..00000000 --- a/buildSrc/buildSrc/src/main/kotlin/Deps.kt +++ /dev/null @@ -1,33 +0,0 @@ -object Deps { - object ArkIvanov { - object MVIKotlin { - private const val VERSION = "2.0.0" - 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 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 Decompose { - private const val VERSION = "0.1.6" - 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 Badoo { - object Reaktive { - private const val VERSION = "1.1.19" - const val reaktive = "com.badoo.reaktive:reaktive:$VERSION" - const val reaktiveTesting = "com.badoo.reaktive:reaktive-testing:$VERSION" - const val utils = "com.badoo.reaktive:utils:$VERSION" - const val coroutinesInterop = "com.badoo.reaktive:coroutines-interop:$VERSION" - } - } -} diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 5ee763ee..7c161a05 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -63,6 +63,33 @@ object JetBrains { const val materialIcon = "androidx.compose.material:material-icons-extended:${Versions.composeVersion}" } } +object Decompose { + private const val VERSION = "0.1.7" + 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.0" + 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 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 Badoo { + object Reaktive { + private const val VERSION = "1.1.19" + const val reaktive = "com.badoo.reaktive:reaktive:$VERSION" + const val reaktiveTesting = "com.badoo.reaktive:reaktive-testing:$VERSION" + const val utils = "com.badoo.reaktive:utils:$VERSION" + const val coroutinesInterop = "com.badoo.reaktive:coroutines-interop:$VERSION" + } +} object Ktor { val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}" val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}" diff --git a/common/compose-ui/build.gradle.kts b/common/compose-ui/build.gradle.kts index 660a114f..b138ff54 100644 --- a/common/compose-ui/build.gradle.kts +++ b/common/compose-ui/build.gradle.kts @@ -7,8 +7,14 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(Deps.ArkIvanov.Decompose.decompose) - implementation(Deps.ArkIvanov.Decompose.extensionsCompose) + implementation(project(":common:dependency-injection")) + implementation(project(":common:data-models")) + implementation(project(":common:database")) + implementation(MVIKotlin.mvikotlin) + implementation(MVIKotlin.mvikotlinExtensionsReaktive) + implementation(Badoo.Reaktive.reaktive) + implementation(Decompose.decompose) + implementation(Decompose.extensionsCompose) } } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt new file mode 100644 index 00000000..67cd2422 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -0,0 +1,30 @@ +package com.shabinder.common.main + +import com.arkivanov.decompose.value.Value +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.badoo.reaktive.base.Consumer +import com.shabinder.common.DownloadRecord +import com.shabinder.database.Database + +interface SpotiFlyerMain { + + val models: Value + + fun onDownloadRecordClicked(link: String) + + fun onInputLinkChanged(link: String) + + interface Dependencies { + val storeFactory: StoreFactory + val database: Database + val mainOutput: Consumer + } + + data class Model( + val record: List, + val link: String + ) + sealed class Output { + data class Searched(val link: String) : Output() + } +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt new file mode 100644 index 00000000..7ebccb41 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -0,0 +1,23 @@ +package com.shabinder.common.main.integration + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value +import com.shabinder.common.main.SpotiFlyerMain +import com.shabinder.common.main.SpotiFlyerMain.Dependencies + +internal class SpotiFlyerMainImpl( + componentContext: ComponentContext, + dependencies: Dependencies +): SpotiFlyerMain,ComponentContext by componentContext, Dependencies by dependencies { + override val models: Value + get() = TODO("Not yet implemented") + + override fun onDownloadRecordClicked(link: String) { + TODO("Not yet implemented") + } + + override fun onInputLinkChanged(link: String) { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStore.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStore.kt new file mode 100644 index 00000000..ee01d295 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStore.kt @@ -0,0 +1,18 @@ +package com.shabinder.common.main.store + +import com.arkivanov.mvikotlin.core.store.Store +import com.shabinder.common.DownloadRecord +import com.shabinder.common.main.store.SpotiFlyerMainStore.* + +internal interface SpotiFlyerMainStore: Store { + sealed class Intent { + data class OpenPlatform(val platformID:String,val platformLink:String):Intent() + object GiveDonation : Intent() + object ShareApp: Intent() + } + + data class State( + val records: List = emptyList(), + val link: String = "" + ) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt new file mode 100644 index 00000000..76b3a957 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt @@ -0,0 +1,52 @@ +package com.shabinder.common.main.store + +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.reaktive.ReaktiveExecutor +import com.badoo.reaktive.observable.Observable +import com.badoo.reaktive.observable.map +import com.badoo.reaktive.observable.mapIterable +import com.badoo.reaktive.observable.observeOn +import com.badoo.reaktive.scheduler.mainScheduler +import com.shabinder.common.DownloadRecord +import com.shabinder.common.database.asObservable +import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent +import com.shabinder.common.main.store.SpotiFlyerMainStore.State +import com.shabinder.database.Database +import com.squareup.sqldelight.Query + +internal class SpotiFlyerMainStoreProvider( + private val storeFactory: StoreFactory, + private val database: Database +) { + private sealed class Result { + data class ItemsLoaded(val items: List) : Result() + data class TextChanged(val text: String) : Result() + } + + private inner class ExecutorImpl : ReaktiveExecutor() { + override fun executeAction(action: Unit, getState: () -> State) { + val updates: Observable> = + database.downloadRecordDatabaseQueries + .selectAll() + .asObservable(Query::executeAsList) + .mapIterable { it.run { + DownloadRecord( + id, type, name, link, coverUrl, totalFiles + ) + } } + + + updates + .observeOn(mainScheduler) + .map(Result::ItemsLoaded) + .subscribeScoped(onNext = ::dispatch) + } + override fun executeIntent(intent: Intent, getState: () -> State) { + when (intent) {//TODO + is Intent.OpenPlatform -> {} + is Intent.GiveDonation -> {} + is Intent.ShareApp -> {} + } + } + } +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Color.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Color.kt new file mode 100644 index 00000000..c4c0149f --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Color.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui + +import androidx.compose.material.Colors +import androidx.compose.material.darkColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver + +val colorPrimary = Color(0xFFFC5C7D) +val colorPrimaryDark = Color(0xFFCE1CFF) +val colorAccent = Color(0xFF9AB3FF) +val colorRedError = Color(0xFFFF9494) +val colorSuccessGreen = Color(0xFF59C351) +val darkBackgroundColor = Color(0xFF000000) +val colorOffWhite = Color(0xFFE7E7E7) + +val SpotiFlyerColors = darkColors( + primary = colorPrimary, + onPrimary = Color.Black, + primaryVariant = colorPrimaryDark, + secondary = colorAccent, + onSecondary = Color.Black, + error = colorRedError, + onError = Color.Black, + surface = darkBackgroundColor, + background = darkBackgroundColor, + onSurface = Color.LightGray, + onBackground = Color.LightGray +) + +/** + * Return the fully opaque color that results from compositing [onSurface] atop [surface] with the + * given [alpha]. Useful for situations where semi-transparent colors are undesirable. + */ +@Composable +fun Colors.compositedOnSurface(alpha: Float): Color { + return onSurface.copy(alpha = alpha).compositeOver(surface) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Shape.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Shape.kt new file mode 100644 index 00000000..04bbfcf0 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Shape.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val SpotiFlyerShapes = Shapes( + small = RoundedCornerShape(percent = 50), + medium = RoundedCornerShape(size = 8.dp), + large = RoundedCornerShape(size = 0.dp) +) \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Theme.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Theme.kt new file mode 100644 index 00000000..6466bc67 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Theme.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui + +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun ComposeLearnTheme(content: @Composable() () -> Unit) { + MaterialTheme( + colors = SpotiFlyerColors, + typography = SpotiFlyerTypography, + shapes = SpotiFlyerShapes, + content = content + ) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Type.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Type.kt new file mode 100644 index 00000000..cd6042cd --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Type.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui + +import androidx.compose.material.Typography +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.font +import androidx.compose.ui.text.font.fontFamily +import androidx.compose.ui.unit.sp +import com.shabinder.spotiflyer.R + +private val Montserrat = fontFamily( + font(R.font.montserrat_light, FontWeight.Light), + font(R.font.montserrat_regular, FontWeight.Normal), + font(R.font.montserrat_medium, FontWeight.Medium), + font(R.font.montserrat_semibold, FontWeight.SemiBold), +) + +val pristineFont = fontFamily( + font(R.font.pristine_script, FontWeight.Bold) +) + +val SpotiFlyerTypography = Typography( + h1 = TextStyle( + fontFamily = Montserrat, + fontSize = 96.sp, + fontWeight = FontWeight.Light, + lineHeight = 117.sp, + letterSpacing = (-1.5).sp + ), + h2 = TextStyle( + fontFamily = Montserrat, + fontSize = 60.sp, + fontWeight = FontWeight.Light, + lineHeight = 73.sp, + letterSpacing = (-0.5).sp + ), + h3 = TextStyle( + fontFamily = Montserrat, + fontSize = 48.sp, + fontWeight = FontWeight.Normal, + lineHeight = 59.sp + ), + h4 = TextStyle( + fontFamily = Montserrat, + fontSize = 30.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 37.sp + ), + h5 = TextStyle( + fontFamily = Montserrat, + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 29.sp + ), + h6 = TextStyle( + fontFamily = Montserrat, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + lineHeight = 26.sp, + letterSpacing = 0.5.sp + + ), + subtitle1 = TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 20.sp, + letterSpacing = 0.5.sp + ), + subtitle2 = TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 17.sp, + letterSpacing = 0.1.sp + ), + body1 = TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.sp, + letterSpacing = 0.15.sp, + ), + body2 = TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + button = TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.25.sp + ), + caption = TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 0.sp + ), + overline = TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.sp + ) +) + +val appNameStyle = TextStyle( + fontFamily = pristineFont, + fontSize = 40.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 42.sp, + letterSpacing = (1.5).sp, + color = Color(0xFFECECEC) +) diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/Home.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/Home.kt new file mode 100644 index 00000000..6a2f7697 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/Home.kt @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.common.ui.home + +import android.content.Intent +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.material.AmbientTextStyle +import androidx.compose.material.Icon +import androidx.compose.material.TabDefaults.tabIndicatorOffset +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.rounded.CardGiftcard +import androidx.compose.material.icons.rounded.Flag +import androidx.compose.material.icons.rounded.InsertLink +import androidx.compose.material.icons.rounded.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.AmbientContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.viewModel +import androidx.core.net.toUri +import androidx.navigation.NavController +import com.razorpay.Checkout +import com.shabinder.spotiflyer.MainActivity +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.database.DownloadRecord +import com.shabinder.spotiflyer.navigation.navigateToTrackList +import com.shabinder.spotiflyer.ui.SpotiFlyerTypography +import com.shabinder.spotiflyer.ui.colorAccent +import com.shabinder.spotiflyer.ui.colorPrimary +import com.shabinder.spotiflyer.ui.home.HomeCategory +import com.shabinder.spotiflyer.ui.home.HomeViewModel +import com.shabinder.spotiflyer.utils.isOnline +import com.shabinder.spotiflyer.utils.openPlatform +import com.shabinder.spotiflyer.utils.sharedViewModel +import com.shabinder.spotiflyer.utils.showDialog +import dev.chrisbanes.accompanist.coil.CoilImage +import org.json.JSONObject + +@Composable +fun Home( + navController: NavController, + mainActivity: MainActivity, + modifier: Modifier = Modifier) { + val viewModel: HomeViewModel = viewModel() + + Column(modifier = modifier) { + + AuthenticationBanner(sharedViewModel.isAuthenticated,modifier) + + SearchPanel( + sharedViewModel.link, + sharedViewModel::updateLink, + navController, + modifier + ) + + HomeTabBar( + viewModel.selectedCategory, + HomeCategory.values(), + viewModel::selectCategory, + modifier + ) + + when(viewModel.selectedCategory){ + HomeCategory.About -> AboutColumn(mainActivity) + HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController) + } + } + //Update Download List + viewModel.getDownloadRecordList() + //reset Gradient + sharedViewModel.resetGradient() +} + + +@Composable +fun AboutColumn(mainActivity: MainActivity,modifier: Modifier = Modifier) { + val ctx = AmbientContext.current + ScrollableColumn(modifier.fillMaxSize(),contentPadding = PaddingValues(16.dp)) { + Card( + modifier = modifier.fillMaxWidth(), + border = BorderStroke(1.dp,Color.Gray) + ) { + Column(modifier.padding(12.dp)) { + Text( + text = stringResource(R.string.supported_platform), + style = SpotiFlyerTypography.body1, + color = colorAccent + ) + Spacer(modifier = Modifier.padding(top = 12.dp)) + Row(horizontalArrangement = Arrangement.Center,modifier = modifier.fillMaxWidth()) { + Icon( + imageVector = vectorResource(id = R.drawable.ic_spotify_logo), tint = Color.Unspecified, + modifier = Modifier.clickable( + onClick = { openPlatform("com.spotify.music","http://open.spotify.com",ctx) }) + ) + Spacer(modifier = modifier.padding(start = 16.dp)) + Icon(imageVector = vectorResource(id = R.drawable.ic_gaana ),tint = Color.Unspecified, + modifier = Modifier.clickable( + onClick = { openPlatform("com.gaana","http://gaana.com",ctx) }) + ) + Spacer(modifier = modifier.padding(start = 16.dp)) + Icon(imageVector = vectorResource(id = R.drawable.ic_youtube),tint = Color.Unspecified, + modifier = Modifier.clickable( + onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com",ctx) }) + ) + Spacer(modifier = modifier.padding(start = 12.dp)) + Icon(imageVector = vectorResource(id = R.drawable.ic_youtube_music_logo),tint = Color.Unspecified, + modifier = Modifier.clickable( + onClick = { openPlatform("com.google.android.apps.youtube.music","https://music.youtube.com/",ctx) }) + ) + } + } + } + Spacer(modifier = Modifier.padding(top = 8.dp)) + Card( + modifier = modifier.fillMaxWidth(), + border = BorderStroke(1.dp,Color.Gray) + ) { + Column(modifier.padding(12.dp)) { + Text( + text = stringResource(R.string.support_development), + style = SpotiFlyerTypography.body1, + color = colorAccent + ) + Spacer(modifier = Modifier.padding(top = 6.dp)) + Row(verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().clickable( + onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer",ctx) }) + .padding(vertical = 6.dp) + ) { + Icon(imageVector = vectorResource(id = R.drawable.ic_github ),tint = Color.LightGray) + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = stringResource(R.string.github), + style = SpotiFlyerTypography.h6 + ) + Text( + text = stringResource(R.string.github_star), + style = SpotiFlyerTypography.subtitle2 + ) + } + } + Row( + modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) + .clickable(onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer", ctx) }), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.Flag.copy(defaultHeight = 32.dp,defaultWidth = 32.dp)) + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = stringResource(R.string.translate), + style = SpotiFlyerTypography.h6 + ) + Text( + text = stringResource(R.string.help_us_translate), + style = SpotiFlyerTypography.subtitle2 + ) + } + } + Row( + modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) + .clickable(onClick = { startPayment(mainActivity) }), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.CardGiftcard.copy(defaultHeight = 32.dp,defaultWidth = 32.dp)) + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = stringResource(R.string.donate), + style = SpotiFlyerTypography.h6 + ) + Text( + text = stringResource(R.string.donate_subtitle), + style = SpotiFlyerTypography.subtitle2 + ) + } + } + Row( + modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) + .clickable(onClick = { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer") + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + ctx.startActivity(shareIntent) + }), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.Share.copy(defaultHeight = 32.dp,defaultWidth = 32.dp)) + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = stringResource(R.string.share), + style = SpotiFlyerTypography.h6 + ) + Text( + text = stringResource(R.string.share_subtitle), + style = SpotiFlyerTypography.subtitle2 + ) + } + } + } + } + } +} + +@Composable +fun HistoryColumn( + list: List, + navController: NavController +) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + content = { + items(list) { + DownloadRecordItem(item = it,navController = navController) + } + }, + modifier = Modifier.padding(top = 8.dp).fillMaxSize() + ) +} + +@Composable +fun DownloadRecordItem(item: DownloadRecord,navController: NavController) { + val ctx = AmbientContext.current + Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) { + val imgUri = item.coverUrl.toUri().buildUpon().scheme("https").build() + CoilImage( + data = imgUri, + //Loading Placeholder Makes Scrolling very stuttery +// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, + error = {Image(vectorResource(id = R.drawable.ic_musicplaceholder))}, + contentScale = ContentScale.Inside, +// fadeIn = true, + modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp) + ) + Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { + Text(item.name,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() + ){ + Text(item.type,fontSize = 13.sp) + Text("Tracks: ${item.totalFiles}",fontSize = 13.sp) + } + } + Image( + imageVector = vectorResource(id = R.drawable.ic_share_open), + modifier = Modifier.clickable(onClick = { + if(!isOnline(ctx)) showDialog("Check Your Internet Connection") + else navController.navigateToTrackList(item.link) + }) + ) + } +} + +private fun startPayment(mainActivity: MainActivity) { + /* + * You need to pass current activity in order to let Razorpay create CheckoutActivity + * */ + val co = Checkout().apply { + setKeyID("rzp_live_3ZQeoFYOxjmXye") + setImage(R.drawable.ic_launcher_foreground) + } + + try { + val preFill = JSONObject() + + val options = JSONObject().apply { + put("name","SpotiFlyer") + put("description","Thanks For the Donation!") + //You can omit the image option to fetch the image from dashboard + //put("image","https://github.com/Shabinder/SpotiFlyer/raw/master/app/SpotifyDownload.png") + put("currency","INR") + put("amount","4900") + put("prefill",preFill) + } + + co.open(mainActivity,options) + }catch (e: Exception){ + showDialog("Error in payment: "+ e.message) + e.printStackTrace() + } +} + + +@Composable +fun AuthenticationBanner(isAuthenticated: Boolean, modifier: Modifier) { + + if (!isAuthenticated) { + // TODO show a progress indicator or similar + } +} + +@Composable +fun HomeTabBar( + selectedCategory: HomeCategory, + categories: Array, + selectCategory: (HomeCategory) -> Unit, + modifier: Modifier = Modifier +) { + val selectedIndex =categories.indexOfFirst { it == selectedCategory } + val indicator = @Composable { tabPositions: List -> + HomeCategoryTabIndicator( + Modifier.tabIndicatorOffset(tabPositions[selectedIndex]) + ) + } + + TabRow( + selectedTabIndex = selectedIndex, + indicator = indicator, + modifier = modifier, + ) { + categories.forEachIndexed { index, category -> + Tab( + selected = index == selectedIndex, + onClick = { selectCategory(category) }, + text = { + Text( + text = when (category) { + HomeCategory.About -> stringResource(R.string.home_about) + HomeCategory.History -> stringResource(R.string.home_history) + }, + style = MaterialTheme.typography.body2 + ) + }, + icon = { + when (category) { + HomeCategory.About -> Icon(Icons.Outlined.Info) + HomeCategory.History -> Icon(Icons.Outlined.History) + } + } + ) + } + } +} + +@Composable +fun SearchPanel( + link:String, + updateLink:(s:String) -> Unit, + navController: NavController, + modifier: Modifier = Modifier +){ + val ctx = AmbientContext.current + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.padding(top = 16.dp) + ){ + TextField( + leadingIcon = { + Icon(Icons.Rounded.InsertLink,tint = Color.LightGray) + }, + label = {Text(text = "Paste Link Here...",color = Color.LightGray)}, + value = link, + onValueChange = { updateLink(it) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + modifier = Modifier.padding(12.dp).fillMaxWidth() + .border( + BorderStroke(2.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))), + RoundedCornerShape(30.dp) + ), + backgroundColor = Color.Black, + textStyle = AmbientTextStyle.current.merge(TextStyle(fontSize = 18.sp,color = Color.White)), + shape = RoundedCornerShape(size = 30.dp), + activeColor = Color.Transparent, + inactiveColor = Color.Transparent + ) + OutlinedButton( + modifier = Modifier.padding(12.dp).wrapContentWidth(), + onClick = { + if(link.isBlank()) showDialog("Enter A Link!") + else{ + if(!isOnline(ctx)) showDialog("Check Your Internet Connection") + else navController.navigateToTrackList(link) + } + }, + border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))) + ){ + Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp)) + } + } +} + + +@Composable +fun HomeCategoryTabIndicator( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colors.onSurface +) { + Spacer( + modifier.padding(horizontal = 24.dp) + .preferredHeight(4.dp) + .background(color, RoundedCornerShape(topLeftPercent = 100, topRightPercent = 100)) + ) +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/HomeViewModel.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/HomeViewModel.kt new file mode 100644 index 00000000..ef2d20ab --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/HomeViewModel.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.home + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.shabinder.spotiflyer.database.DownloadRecord +import com.shabinder.spotiflyer.utils.sharedViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HomeViewModel : ViewModel() { + + var selectedCategory by mutableStateOf(HomeCategory.About) + private set + + fun selectCategory(s:HomeCategory) { + selectedCategory = s + } + + var downloadRecordList by mutableStateOf>(listOf()) + + fun getDownloadRecordList() { + viewModelScope.launch { + withContext(Dispatchers.IO){ + delay(100) //TEMP + downloadRecordList = sharedViewModel.databaseDAO.getRecord() + } + } + } +} + +enum class HomeCategory { + About, History +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/MainScreen.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/MainScreen.kt new file mode 100644 index 00000000..8ba35d86 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/home/MainScreen.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.example.jetcaster.util.verticalGradientScrim +import com.shabinder.spotiflyer.MainActivity +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.SharedViewModel +import com.shabinder.spotiflyer.navigation.ComposeNavigation +import com.shabinder.spotiflyer.ui.appNameStyle +import dev.chrisbanes.accompanist.insets.statusBarsHeight + +@Composable +fun MainScreen( + modifier: Modifier, + mainActivity: MainActivity, + sharedViewModel: SharedViewModel, + navController: NavHostController, + topPadding: Dp = 0.dp +){ + val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f) + + Column( + modifier = modifier.fillMaxSize().verticalGradientScrim( + color = sharedViewModel.gradientColor.copy(alpha = 0.38f), + startYPercentage = 0.29f, + endYPercentage = 0f, + ) + ) { + // Draw a scrim over the status bar which matches the app bar + Spacer( + Modifier.background(appBarColor).fillMaxWidth() + .statusBarsHeight() + ) + AppBar( + backgroundColor = appBarColor, + modifier = Modifier.fillMaxWidth() + ) + //Space for Animation + Spacer(Modifier.padding(top = topPadding)) + ComposeNavigation( + mainActivity, + navController, + sharedViewModel.spotifyProvider, + sharedViewModel.gaanaProvider, + sharedViewModel.youtubeProvider + ) + } +} + +@Composable +fun AppBar( + backgroundColor: Color, + modifier: Modifier = Modifier +) { + TopAppBar( + backgroundColor = backgroundColor, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + imageVector = vectorResource(R.drawable.ic_spotiflyer_logo), + Modifier.preferredSize(32.dp) + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + text = "SpotiFlyer", + style = appNameStyle + ) + } + }, + /*actions = { + Providers(AmbientContentAlpha provides ContentAlpha.medium) { + IconButton( + onClick = { *//* TODO: Open Preferences*//* } + ) { + Icon(Icons.Filled.Settings, tint = Color.Gray) + } + } + },*/ + modifier = modifier, + elevation = 0.dp + ) +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/splash/Splash.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/splash/Splash.kt new file mode 100644 index 00000000..99d06f0b --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/splash/Splash.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.ui.SpotiFlyerTypography +import com.shabinder.spotiflyer.ui.colorAccent +import com.shabinder.spotiflyer.ui.colorPrimary +import kotlinx.coroutines.delay + +private const val SplashWaitTime: Long = 1100 + +@Composable +fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + // Adds composition consistency. Use the value when LaunchedEffect is first called + val currentOnTimeout by rememberUpdatedState(onTimeout) + + LaunchedEffect(Unit) { + delay(SplashWaitTime) + currentOnTimeout() + } + Image(imageVector = vectorResource(id = R.drawable.ic_spotiflyer_logo)) + MadeInIndia(Modifier.align(Alignment.BottomCenter)) + } +} + +@Composable +fun MadeInIndia( + modifier: Modifier = Modifier +){ + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.padding(8.dp) + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.made_with_love), + color = colorPrimary, + fontSize = 22.sp + ) + Spacer(modifier = Modifier.padding(start = 4.dp)) + Icon(vectorResource(id = R.drawable.ic_heart),tint = Color.Unspecified) + Spacer(modifier = Modifier.padding(start = 4.dp)) + Text( + text = stringResource(id = R.string.in_india), + color = colorPrimary, + fontSize = 22.sp + ) + } + Text( + "by: Shabinder Singh", + style = SpotiFlyerTypography.h6, + color = colorAccent, + fontSize = 14.sp + ) + } +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt new file mode 100644 index 00000000..694543ad --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/tracklist/TrackList.kt @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.tracklist + +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.AmbientContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.navigation.NavController +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.PlatformQueryResult +import com.shabinder.spotiflyer.models.TrackDetails +import com.shabinder.spotiflyer.providers.GaanaProvider +import com.shabinder.spotiflyer.providers.SpotifyProvider +import com.shabinder.spotiflyer.providers.YoutubeProvider +import com.shabinder.spotiflyer.ui.SpotiFlyerTypography +import com.shabinder.spotiflyer.ui.colorAccent +import com.shabinder.spotiflyer.ui.utils.calculateDominantColor +import com.shabinder.spotiflyer.utils.downloadTracks +import com.shabinder.spotiflyer.utils.sharedViewModel +import com.shabinder.spotiflyer.utils.showDialog +import com.shabinder.spotiflyer.worker.ForegroundService +import dev.chrisbanes.accompanist.coil.CoilImage +import kotlinx.coroutines.* + +/* +* UI for List of Tracks to be universally used. +**/ +@Composable +fun TrackList( + fullLink: String, + navController: NavController, + spotifyProvider: SpotifyProvider, + gaanaProvider: GaanaProvider, + youtubeProvider: YoutubeProvider, + modifier: Modifier = Modifier +){ + val context = AmbientContext.current + val coroutineScope = rememberCoroutineScope() + + var result by remember(fullLink) { mutableStateOf(null) } + + coroutineScope.launch(Dispatchers.Default) { + @Suppress("UnusedEquals")//Add Delay if result is not Initialized yet. + try{result == null}catch(e:java.lang.IllegalStateException){delay(100)} + if(result == null){ + result = when{ + /* + * Using SharedViewModel's Link as NAVIGATION's Arg is buggy for links. + * */ + //SPOTIFY + sharedViewModel.link.contains("spotify",true) -> + spotifyProvider.query(sharedViewModel.link) + + //YOUTUBE + sharedViewModel.link.contains("youtube.com",true) || sharedViewModel.link.contains("youtu.be",true) -> + youtubeProvider.query(sharedViewModel.link) + + //GAANA + sharedViewModel.link.contains("gaana",true) -> + gaanaProvider.query(sharedViewModel.link) + + else -> { + showDialog("Link is Not Valid") + null + } + } + } + withContext(Dispatchers.Main){ + //Error Occurred And Has Been Shown to User + if(result == null) navController.popBackStack() + } + } + + sharedViewModel.updateTrackList(result?.trackList ?: listOf()) + queryActiveTracks(context) + + result?.let{ + val ctx = AmbientContext.current + Box(modifier = modifier.fillMaxSize()){ + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + content = { + item { + CoverImage(it.title,it.coverUrl,coroutineScope) + } + itemsIndexed(sharedViewModel.trackList) { index, item -> + TrackCard( + track = item, + onDownload = { + downloadTracks(arrayListOf(item),ctx) + sharedViewModel.updateTrackStatus(index,DownloadStatus.Queued) + }, + ) + } + }, + modifier = Modifier.fillMaxSize(), + ) + DownloadAllButton( + onClick = { + val finalList = sharedViewModel.trackList.filter{it.downloaded == DownloadStatus.NotDownloaded} + if (finalList.isNullOrEmpty()) showDialog("All Songs are Processed") + else downloadTracks(finalList as ArrayList,ctx) + val list = sharedViewModel.trackList.map { + if(it.downloaded == DownloadStatus.NotDownloaded){ + it.downloaded = DownloadStatus.Queued + } + it + } + sharedViewModel.updateTrackList(list) + }, + modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) + ) + } + } +} + +@Composable +fun CoverImage( + title: String, + coverURL: String, + scope: CoroutineScope, + modifier: Modifier = Modifier, +) { + val ctx = AmbientContext.current + Column( + modifier.padding(vertical = 8.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val imgUri = coverURL.toUri().buildUpon().scheme("https").build() + CoilImage( + data = imgUri, + contentScale = ContentScale.Crop, + loading = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, + modifier = Modifier + .preferredWidth(210.dp) + .preferredHeight(230.dp) + .clip(MaterialTheme.shapes.medium) + ) + Text( + text = title, + style = SpotiFlyerTypography.h5, + maxLines = 2, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + //color = colorAccent, + ) + } + scope.launch { + updateGradient(coverURL, ctx) + } +} + +@Composable +fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + ExtendedFloatingActionButton( + text = { Text("Download All") }, + onClick = onClick, + icon = { Icon(imageVector = vectorResource(R.drawable.ic_download_arrow),tint = Color.Black) }, + backgroundColor = colorAccent, + modifier = modifier + ) +} + +@Composable +fun TrackCard( + track:TrackDetails, + onDownload:(TrackDetails)->Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + val imgUri = track.albumArtURL.toUri().buildUpon().scheme("https").build() + CoilImage( + data = imgUri, + //Loading Placeholder Makes Scrolling very stuttery +// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, + error = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, + contentScale = ContentScale.Inside, +// fadeIn = true, + modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp) + ) + Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { + Text(track.title,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() + ){ + Text("${track.artists.firstOrNull()}...",fontSize = 12.sp,maxLines = 1) + Text("${track.durationSec/60} min, ${track.durationSec%60} sec",fontSize = 12.sp,maxLines = 1,overflow = TextOverflow.Ellipsis) + } + } + when(track.downloaded){ + DownloadStatus.Downloaded -> { + Image(vectorResource(id = R.drawable.ic_tick)) + } + DownloadStatus.Queued -> { + CircularProgressIndicator() + } + DownloadStatus.Failed -> { + Image(vectorResource(id = R.drawable.ic_error)) + } + DownloadStatus.Downloading -> { + CircularProgressIndicator(progress = track.progress.toFloat()/100f) + } + DownloadStatus.Converting -> { + CircularProgressIndicator(progress = 100f,color = colorAccent) + } + DownloadStatus.NotDownloaded -> { + Image(vectorResource(id = R.drawable.ic_arrow), Modifier.clickable(onClick = { + onDownload(track) + })) + } + } + } +} +private fun queryActiveTracks(context:Context?) { + val serviceIntent = Intent(context, ForegroundService::class.java).apply { + action = "query" + } + context?.let { ContextCompat.startForegroundService(it, serviceIntent) } +} +suspend fun updateGradient(imageURL:String,ctx:Context){ + calculateDominantColor(imageURL,ctx)?.color + ?.let { sharedViewModel.updateGradientColor(it) } +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/Colors.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/Colors.kt new file mode 100644 index 00000000..aefb163c --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/Colors.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.utils + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.luminance +import kotlin.math.max +import kotlin.math.min + +fun Color.contrastAgainst(background: Color): Float { + val fg = if (alpha < 1f) compositeOver(background) else this + + val fgLuminance = fg.luminance() + 0.05f + val bgLuminance = background.luminance() + 0.05f + + return max(fgLuminance, bgLuminance) / min(fgLuminance, bgLuminance) +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/DynamicTheming.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/DynamicTheming.kt new file mode 100644 index 00000000..995c9cd3 --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/DynamicTheming.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.utils + +import android.content.Context +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import coil.Coil +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.size.Scale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Immutable +data class DominantColors(val color: Color, val onColor: Color) + + +suspend fun calculateDominantColor(url: String,ctx:Context): DominantColors? { + // we calculate the swatches in the image, and return the first valid color + return calculateSwatchesInImage(ctx, url) + // First we want to sort the list by the color's population + .sortedByDescending { swatch -> swatch.population } + // Then we want to find the first valid color + .firstOrNull { swatch -> Color(swatch.rgb).contrastAgainst(Color.Black) >= 3f } + // If we found a valid swatch, wrap it in a [DominantColors] + ?.let { swatch -> + DominantColors( + color = Color(swatch.rgb), + onColor = Color(swatch.bodyTextColor).copy(alpha = 1f) + ) + } +} + + +/** + * Fetches the given [imageUrl] with [Coil], then uses [Palette] to calculate the dominant color. + */ +suspend fun calculateSwatchesInImage( + context: Context, + imageUrl: String +): List { + val r = ImageRequest.Builder(context) + .data(imageUrl) + // We scale the image to cover 128px x 128px (i.e. min dimension == 128px) + .size(128).scale(Scale.FILL) + // Disable hardware bitmaps, since Palette uses Bitmap.getPixels() + .allowHardware(false) + .build() + + val bitmap = when (val result = Coil.execute(r)) { + is SuccessResult -> result.drawable.toBitmap() + else -> null + } + + return bitmap?.let { + withContext(Dispatchers.Default) { + val palette = Palette.Builder(bitmap) + // Disable any bitmap resizing in Palette. We've already loaded an appropriately + // sized bitmap through Coil + .resizeBitmapArea(0) + // Clear any built-in filters. We want the unfiltered dominant color + .clearFilters() + // We reduce the maximum color count down to 8 + .maximumColorCount(8) + .generate() + + palette.swatches + } + } ?: emptyList() +} diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/GradientScrim.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/GradientScrim.kt new file mode 100644 index 00000000..1ebc2f7f --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/utils/GradientScrim.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021 Shabinder Singh + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.example.jetcaster.util + +import androidx.annotation.FloatRange +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import kotlin.math.pow + +/** + * Draws a vertical gradient scrim in the foreground. + * + * @param color The color of the gradient scrim. + * @param startYPercentage The start y value, in percentage of the layout's height (0f to 1f) + * @param endYPercentage The end y value, in percentage of the layout's height (0f to 1f) + * @param decay The exponential decay to apply to the gradient. Defaults to `1.0f` which is + * a linear gradient. + * @param numStops The number of color stops to draw in the gradient. Higher numbers result in + * the higher visual quality at the cost of draw performance. Defaults to `16`. + */ +fun Modifier.verticalGradientScrim( + color: Color, + @FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f, + @FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f, + decay: Float = 1.0f, + numStops: Int = 16, + fixedHeight: Float? = null +): Modifier = composed { + val colors = remember(color, numStops) { + if (decay != 1f) { + // If we have a non-linear decay, we need to create the color gradient steps + // manually + val baseAlpha = color.alpha + List(numStops) { i -> + val x = i * 1f / (numStops - 1) + val opacity = x.pow(decay) + color.copy(alpha = baseAlpha * opacity) + } + } else { + // If we have a linear decay, we just create a simple list of start + end colors + listOf(color.copy(alpha = 0f), color) + } + } + + var height by remember { mutableStateOf(fixedHeight ?: 0f) } + val brush = remember(color, numStops, startYPercentage, endYPercentage, height) { + Brush.verticalGradient( + colors = colors, + startY = height * startYPercentage, + endY = height * endYPercentage + ) + } + + drawBehind { + height = fixedHeight ?: size.height +// log("Height",size.height.toString()) + drawRect(brush = brush) + } +} diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadRecord.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadRecord.kt index ef05fa7f..e8fe50ff 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadRecord.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadRecord.kt @@ -1,10 +1,10 @@ package com.shabinder.common data class DownloadRecord( - var id:Int = 0, + var id:Long = 0, var type:String, var name:String, var link:String, var coverUrl:String, - var totalFiles:Int = 1, + var totalFiles:Long = 1, ) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/Token.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/TokenData.kt similarity index 97% rename from common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/Token.kt rename to common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/TokenData.kt index fa57c3ab..19a0b25b 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/Token.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/spotify/TokenData.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Token( +data class TokenData( var access_token:String?, var token_type:String?, @SerialName("expires_in") var expiry:Long? diff --git a/common/database/build.gradle.kts b/common/database/build.gradle.kts index bd02bc2d..41c593ca 100644 --- a/common/database/build.gradle.kts +++ b/common/database/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } sqldelight { - database("DownloadRecordDatabase") { + database("Database") { packageName = "com.shabinder.database" } } @@ -14,7 +14,8 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(Deps.Badoo.Reaktive.reaktive) + implementation(project(":common:data-models")) + implementation(Badoo.Reaktive.reaktive) // SQL Delight implementation(SqlDelight.runtime) implementation(SqlDelight.coroutineExtensions) diff --git a/common/database/src/androidMain/kotlin/com/shabinder/common/database/Actual.kt b/common/database/src/androidMain/kotlin/com/shabinder/common/database/Actual.kt index 6efb33e8..563e4f2d 100644 --- a/common/database/src/androidMain/kotlin/com/shabinder/common/database/Actual.kt +++ b/common/database/src/androidMain/kotlin/com/shabinder/common/database/Actual.kt @@ -3,14 +3,13 @@ package com.shabinder.common.database import android.content.Context import co.touchlab.kermit.LogcatLogger import co.touchlab.kermit.Logger -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.database.Database import com.squareup.sqldelight.android.AndroidSqliteDriver -import com.squareup.sqldelight.db.SqlDriver lateinit var appContext: Context -actual fun createDb(): DownloadRecordDatabase { - val driver = AndroidSqliteDriver(DownloadRecordDatabase.Schema, appContext, "DownloadRecordDatabase.db") - return DownloadRecordDatabase(driver) +actual fun createDatabase(): Database { + val driver = AndroidSqliteDriver(Database.Schema, appContext, "Database.db") + return Database(driver) } actual fun getLogger(): Logger = LogcatLogger() diff --git a/common/database/src/commonMain/kotlin/com/shabinder/common/database/Expect.kt b/common/database/src/commonMain/kotlin/com/shabinder/common/database/Expect.kt index 18420ef8..563725a8 100644 --- a/common/database/src/commonMain/kotlin/com/shabinder/common/database/Expect.kt +++ b/common/database/src/commonMain/kotlin/com/shabinder/common/database/Expect.kt @@ -1,7 +1,7 @@ package com.shabinder.common.database -import com.shabinder.database.DownloadRecordDatabase import co.touchlab.kermit.Logger +import com.shabinder.database.Database -expect fun createDb() : DownloadRecordDatabase +expect fun createDatabase() : Database expect fun getLogger(): Logger \ No newline at end of file diff --git a/common/database/src/commonMain/kotlin/com/shabinder/common/database/ReaktiveExt.kt b/common/database/src/commonMain/kotlin/com/shabinder/common/database/ReaktiveExt.kt new file mode 100644 index 00000000..8a8b84ce --- /dev/null +++ b/common/database/src/commonMain/kotlin/com/shabinder/common/database/ReaktiveExt.kt @@ -0,0 +1,28 @@ +package com.shabinder.common.database + +import com.badoo.reaktive.base.setCancellable +import com.badoo.reaktive.observable.Observable +import com.badoo.reaktive.observable.map +import com.badoo.reaktive.observable.observable +import com.badoo.reaktive.observable.observeOn +import com.badoo.reaktive.scheduler.ioScheduler +import com.squareup.sqldelight.Query + +fun Query.asObservable(execute: (Query) -> R): Observable = + asObservable() + .observeOn(ioScheduler) + .map(execute) + +fun Query.asObservable(): Observable> = + observable { emitter -> + val listener = + object : Query.Listener { + override fun queryResultsChanged() { + emitter.onNext(this@asObservable) + } + } + + emitter.onNext(this@asObservable) + addListener(listener) + emitter.setCancellable { removeListener(listener) } + } diff --git a/common/database/src/commonMain/sqldelight/com/shabinder/common/database/TokenDB.sq b/common/database/src/commonMain/sqldelight/com/shabinder/common/database/TokenDB.sq new file mode 100644 index 00000000..f7f00145 --- /dev/null +++ b/common/database/src/commonMain/sqldelight/com/shabinder/common/database/TokenDB.sq @@ -0,0 +1,17 @@ +CREATE TABLE Token ( + index INTEGER NOT NULL DEFAULT 0 PRIMARY KEY ON CONFLICT REPLACE, + accessToken TEXT NOT NULL, + expiry INTEGER NOT NULL +); + +add: +INSERT OR REPLACE INTO Token (accessToken,expiry) +VALUES (?,?); + +select: +SELECT * +FROM Token +WHERE index = 0; + +clear: +DELETE FROM Token; \ No newline at end of file diff --git a/common/database/src/desktopMain/kotlin/com/shabinder/common/database/Actual.kt b/common/database/src/desktopMain/kotlin/com/shabinder/common/database/Actual.kt index b2942a1c..51d1ddfd 100644 --- a/common/database/src/desktopMain/kotlin/com/shabinder/common/database/Actual.kt +++ b/common/database/src/desktopMain/kotlin/com/shabinder/common/database/Actual.kt @@ -2,15 +2,14 @@ package com.shabinder.common.database import co.touchlab.kermit.CommonLogger import co.touchlab.kermit.Logger -import com.shabinder.database.DownloadRecordDatabase -import com.squareup.sqldelight.db.SqlDriver +import com.shabinder.database.Database import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver import java.io.File -actual fun createDb(): DownloadRecordDatabase { - val databasePath = File(System.getProperty("java.io.tmpdir"), "DownloadRecordDatabase.db") +actual fun createDatabase(): Database { + val databasePath = File(System.getProperty("java.io.tmpdir"), "Database.db") val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}") - .also { DownloadRecordDatabase.Schema.create(it) } - return DownloadRecordDatabase(driver) + .also { Database.Schema.create(it) } + return Database(driver) } actual fun getLogger(): Logger = CommonLogger() \ No newline at end of file diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index df38346a..d7664760 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -12,6 +12,7 @@ kotlin { implementation(project(":common:database")) implementation(project(":fuzzywuzzy:app")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1") implementation(Ktor.clientCore) implementation(Ktor.clientCio) implementation(Ktor.clientSerialization) diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt index 28a25489..e4eb0f11 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt @@ -20,7 +20,8 @@ import co.touchlab.kermit.Kermit import com.github.kiulian.downloader.YoutubeDownloader import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.spotify.Source -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.common.utils.removeIllegalChars +import com.shabinder.database.Database import io.ktor.client.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -28,7 +29,7 @@ import org.koin.core.KoinComponent actual class YoutubeProvider actual constructor( private val httpClient: HttpClient, - private val database: DownloadRecordDatabase, + private val database: Database, private val logger: Kermit, private val dir: Dir, ){ diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt index b10375b2..c2ff8317 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt @@ -1,7 +1,7 @@ package com.shabinder.common import co.touchlab.kermit.Kermit -import com.shabinder.common.database.createDb +import com.shabinder.common.database.createDatabase import com.shabinder.common.database.getLogger import com.shabinder.common.providers.GaanaProvider import com.shabinder.common.providers.SpotifyProvider @@ -22,9 +22,10 @@ fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclarat } fun commonModule(enableNetworkLogs: Boolean) = module { - single { Dir() } - single { createDb() } + single { Dir(get()) } + single { createDatabase() } single { Kermit(getLogger()) } + single { TokenStore(get(),get()) } single { YoutubeMusic(get(),get()) } single { SpotifyProvider(get(),get(),get(),get()) } single { GaanaProvider(get(),get(),get(),get()) } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt index fe10442d..4847ac08 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt @@ -1,12 +1,25 @@ package com.shabinder.common -expect open class Dir() { +import co.touchlab.kermit.Kermit +import com.shabinder.common.utils.removeIllegalChars + +expect open class Dir( + logger: Kermit +) { fun isPresent(path:String):Boolean fun fileSeparator(): String fun defaultDir(): String fun imageDir(): String + fun createDirectory(dirPath:String) +} +fun Dir.createDirectories() { + createDirectory(defaultDir()) + createDirectory(imageDir()) + createDirectory(defaultDir() + "Tracks/") + createDirectory(defaultDir() + "Albums/") + createDirectory(defaultDir() + "Playlists/") + createDirectory(defaultDir() + "YT_Downloads/") } - fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String = defaultDir + removeIllegalChars(type) + this.fileSeparator() + if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} + diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/TokenStore.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/TokenStore.kt new file mode 100644 index 00000000..570f0297 --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/TokenStore.kt @@ -0,0 +1,35 @@ +package com.shabinder.common + +import co.touchlab.kermit.Kermit +import com.shabinder.common.database.TokenDBQueries +import com.shabinder.common.spotify.TokenData +import com.shabinder.common.spotify.authenticateSpotify +import com.shabinder.database.Database +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock + +class TokenStore( + private val tokenDB: Database, + private val logger: Kermit, +) { + private val db: TokenDBQueries + get() = tokenDB.tokenDBQueries + + private suspend fun save(token: TokenData){ + if(!token.access_token.isNullOrBlank() && token.expiry != null) + db.add(token.access_token!!, token.expiry!! + Clock.System.now().epochSeconds) + } + + suspend fun getToken(): TokenData{ + var token:TokenData? = db.select().executeAsOneOrNull()?.let { + TokenData(it.accessToken,null,it.expiry) + } + if(Clock.System.now().epochSeconds > token?.expiry ?:0 || token == null){ + logger.d{"Requesting New Token"} + token = authenticateSpotify() + GlobalScope.launch { token.access_token?.let { save(token) } } + } + return token + } +} \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/YoutubeProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/YoutubeProvider.kt index 87fc5e7a..d31e4975 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/YoutubeProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/YoutubeProvider.kt @@ -1,12 +1,12 @@ package com.shabinder.common import co.touchlab.kermit.Kermit -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.database.Database import io.ktor.client.* expect class YoutubeProvider( httpClient: HttpClient, - database: DownloadRecordDatabase, + database: Database, logger: Kermit, dir: Dir ) { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt index 3f128672..f7677dab 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt @@ -22,14 +22,14 @@ import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.gaana.GaanaRequests import com.shabinder.common.gaana.GaanaTrack import com.shabinder.common.spotify.Source -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.database.Database import io.ktor.client.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class GaanaProvider( override val httpClient: HttpClient, - private val database: DownloadRecordDatabase, + private val database: Database, private val logger: Kermit, private val dir: Dir, ): GaanaRequests { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt index a8a17a06..246c368e 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt @@ -20,14 +20,14 @@ import co.touchlab.kermit.Kermit import com.shabinder.common.* import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.spotify.* -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.database.Database import io.ktor.client.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class SpotifyProvider( override val httpClient: HttpClient, - private val database: DownloadRecordDatabase, + private val database: Database, private val logger: Kermit, private val dir: Dir, ) :SpotifyRequests { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/spotify/SpotifyAuth.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/spotify/SpotifyAuth.kt index 220575bd..d15c0769 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/spotify/SpotifyAuth.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/spotify/SpotifyAuth.kt @@ -10,7 +10,7 @@ import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* -suspend fun authenticateSpotify(): Token { +suspend fun authenticateSpotify(): TokenData { return spotifyAuthClient.post("https://accounts.spotify.com/api/token"){ body = FormDataContent(Parameters.build { append("grant_type","client_credentials") }) } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Utils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt similarity index 96% rename from common/data-models/src/commonMain/kotlin/com/shabinder/common/Utils.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt index 2a6c2ad6..9379e6f5 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Utils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt @@ -1,4 +1,4 @@ -package com.shabinder.common +package com.shabinder.common.utils /** * Removing Illegal Chars from File Name @@ -39,4 +39,4 @@ fun removeIllegalChars(fileName: String): String { name = name.replace(":".toRegex(), "") name = name.replace("\\|".toRegex(), "") return name -} +} \ No newline at end of file diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt index 3c647fc6..be4a00fa 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/PlatformImpl.kt @@ -1,8 +1,9 @@ package com.shabinder.common +import co.touchlab.kermit.Kermit import java.io.File -actual open class Dir{ +actual open class Dir actual constructor(private val logger: Kermit) { actual fun fileSeparator(): String = File.separator @@ -14,4 +15,20 @@ actual open class Dir{ actual fun isPresent(path: String): Boolean = File(path).exists() + actual fun createDirectory(dirPath:String){ + val yourAppDir = File(dirPath) + + if(!yourAppDir.exists() && !yourAppDir.isDirectory) + { // create empty directory + if (yourAppDir.mkdirs()) + {logger.i{"$dirPath created"}} + else + { + logger.e{"Unable to create Dir: $dirPath!"} + } + } + else { + logger.i { "$dirPath already exists" } + } + } } \ No newline at end of file diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt index 3b163a02..25c9f740 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt @@ -20,14 +20,15 @@ import co.touchlab.kermit.Kermit import com.github.kiulian.downloader.YoutubeDownloader import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.spotify.Source -import com.shabinder.database.DownloadRecordDatabase +import com.shabinder.common.utils.removeIllegalChars +import com.shabinder.database.Database import io.ktor.client.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext actual class YoutubeProvider actual constructor( private val httpClient: HttpClient, - private val database: DownloadRecordDatabase, + private val database: Database, private val logger: Kermit, private val dir: Dir, ){ diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index dad3cc2f..a652a55f 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { dependencies { implementation(compose.desktop.currentOs) implementation(project(":common:database")) + implementation(project(":common:dependency-injection")) implementation(project(":common:compose-ui")) } } diff --git a/desktop/src/jvmMain/kotlin/main.kt b/desktop/src/jvmMain/kotlin/main.kt index 94149b33..c2187b9c 100644 --- a/desktop/src/jvmMain/kotlin/main.kt +++ b/desktop/src/jvmMain/kotlin/main.kt @@ -1,4 +1,7 @@ import androidx.compose.desktop.Window +import com.shabinder.common.initKoin + +private val koin = initKoin(enableNetworkLogs = true).koin fun main() = Window { //TODO