mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 10:24:31 +01:00
Migrating to MVI Arch
This commit is contained in:
parent
6e20cc3b0a
commit
a8f5941050
@ -66,7 +66,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(Koin.android)
|
implementation(Koin.android)
|
||||||
implementation(Koin.androidViewModel)
|
implementation(Koin.androidViewModel)
|
||||||
|
//DECOMPOSE
|
||||||
|
implementation(Decompose.decompose)
|
||||||
|
implementation(Decompose.extensionsCompose)
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
Versions.androidLifecycle.let{
|
Versions.androidLifecycle.let{
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it")
|
||||||
|
39
android/src/main/java/com/shabinder/android/App.kt
Normal file
39
android/src/main/java/com/shabinder/android/App.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,14 @@
|
|||||||
package com.shabinder.android
|
package com.shabinder.android
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.platform.setContent
|
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.ui.SpotiFlyerMain
|
||||||
import com.shabinder.common.youtube.YoutubeMusic
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -17,9 +16,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
SpotiFlyerMain()
|
SpotiFlyerMain()
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
122
android/src/main/java/com/shabinder/android/SharedViewModel.kt
Normal file
122
android/src/main/java/com/shabinder/android/SharedViewModel.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<TrackDetails>()
|
||||||
|
|
||||||
|
fun updateTrackList(list:List<TrackDetails>){
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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()) }
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ allprojects {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
maven(url = "https://dl.bintray.com/ekito/koin")
|
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://kotlin.bintray.com/kotlin-js-wrappers/")
|
||||||
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
flatDir {
|
flatDir {
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -63,6 +63,33 @@ object JetBrains {
|
|||||||
const val materialIcon = "androidx.compose.material:material-icons-extended:${Versions.composeVersion}"
|
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 {
|
object Ktor {
|
||||||
val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}"
|
val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}"
|
||||||
val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}"
|
val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}"
|
||||||
|
@ -7,8 +7,14 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose)
|
implementation(project(":common:dependency-injection"))
|
||||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<Model>
|
||||||
|
|
||||||
|
fun onDownloadRecordClicked(link: String)
|
||||||
|
|
||||||
|
fun onInputLinkChanged(link: String)
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
val storeFactory: StoreFactory
|
||||||
|
val database: Database
|
||||||
|
val mainOutput: Consumer<Output>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Model(
|
||||||
|
val record: List<DownloadRecord>,
|
||||||
|
val link: String
|
||||||
|
)
|
||||||
|
sealed class Output {
|
||||||
|
data class Searched(val link: String) : Output()
|
||||||
|
}
|
||||||
|
}
|
@ -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<SpotiFlyerMain.Model>
|
||||||
|
get() = TODO("Not yet implemented")
|
||||||
|
|
||||||
|
override fun onDownloadRecordClicked(link: String) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInputLinkChanged(link: String) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Intent, State, Nothing> {
|
||||||
|
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<DownloadRecord> = emptyList(),
|
||||||
|
val link: String = ""
|
||||||
|
)
|
||||||
|
}
|
@ -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<DownloadRecord>) : Result()
|
||||||
|
data class TextChanged(val text: String) : Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Unit, State, Result, Nothing>() {
|
||||||
|
override fun executeAction(action: Unit, getState: () -> State) {
|
||||||
|
val updates: Observable<List<DownloadRecord>> =
|
||||||
|
database.downloadRecordDatabaseQueries
|
||||||
|
.selectAll()
|
||||||
|
.asObservable(Query<com.shabinder.common.database.DownloadRecord>::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 -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<DownloadRecord>,
|
||||||
|
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<HomeCategory>,
|
||||||
|
selectCategory: (HomeCategory) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val selectedIndex =categories.indexOfFirst { it == selectedCategory }
|
||||||
|
val indicator = @Composable { tabPositions: List<TabPosition> ->
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<List<DownloadRecord>>(listOf())
|
||||||
|
|
||||||
|
fun getDownloadRecordList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
delay(100) //TEMP
|
||||||
|
downloadRecordList = sharedViewModel.databaseDAO.getRecord()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class HomeCategory {
|
||||||
|
About, History
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<PlatformQueryResult?>(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<TrackDetails>,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) }
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Palette.Swatch> {
|
||||||
|
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()
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
data class DownloadRecord(
|
data class DownloadRecord(
|
||||||
var id:Int = 0,
|
var id:Long = 0,
|
||||||
var type:String,
|
var type:String,
|
||||||
var name:String,
|
var name:String,
|
||||||
var link:String,
|
var link:String,
|
||||||
var coverUrl:String,
|
var coverUrl:String,
|
||||||
var totalFiles:Int = 1,
|
var totalFiles:Long = 1,
|
||||||
)
|
)
|
@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Token(
|
data class TokenData(
|
||||||
var access_token:String?,
|
var access_token:String?,
|
||||||
var token_type:String?,
|
var token_type:String?,
|
||||||
@SerialName("expires_in") var expiry:Long?
|
@SerialName("expires_in") var expiry:Long?
|
@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sqldelight {
|
sqldelight {
|
||||||
database("DownloadRecordDatabase") {
|
database("Database") {
|
||||||
packageName = "com.shabinder.database"
|
packageName = "com.shabinder.database"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,7 +14,8 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(Deps.Badoo.Reaktive.reaktive)
|
implementation(project(":common:data-models"))
|
||||||
|
implementation(Badoo.Reaktive.reaktive)
|
||||||
// SQL Delight
|
// SQL Delight
|
||||||
implementation(SqlDelight.runtime)
|
implementation(SqlDelight.runtime)
|
||||||
implementation(SqlDelight.coroutineExtensions)
|
implementation(SqlDelight.coroutineExtensions)
|
||||||
|
@ -3,14 +3,13 @@ package com.shabinder.common.database
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import co.touchlab.kermit.LogcatLogger
|
import co.touchlab.kermit.LogcatLogger
|
||||||
import co.touchlab.kermit.Logger
|
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.android.AndroidSqliteDriver
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
|
||||||
|
|
||||||
lateinit var appContext: Context
|
lateinit var appContext: Context
|
||||||
|
|
||||||
actual fun createDb(): DownloadRecordDatabase {
|
actual fun createDatabase(): Database {
|
||||||
val driver = AndroidSqliteDriver(DownloadRecordDatabase.Schema, appContext, "DownloadRecordDatabase.db")
|
val driver = AndroidSqliteDriver(Database.Schema, appContext, "Database.db")
|
||||||
return DownloadRecordDatabase(driver)
|
return Database(driver)
|
||||||
}
|
}
|
||||||
actual fun getLogger(): Logger = LogcatLogger()
|
actual fun getLogger(): Logger = LogcatLogger()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.shabinder.common.database
|
package com.shabinder.common.database
|
||||||
|
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.shabinder.database.Database
|
||||||
|
|
||||||
expect fun createDb() : DownloadRecordDatabase
|
expect fun createDatabase() : Database
|
||||||
expect fun getLogger(): Logger
|
expect fun getLogger(): Logger
|
@ -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 <T : Any, R> Query<T>.asObservable(execute: (Query<T>) -> R): Observable<R> =
|
||||||
|
asObservable()
|
||||||
|
.observeOn(ioScheduler)
|
||||||
|
.map(execute)
|
||||||
|
|
||||||
|
fun <T : Any> Query<T>.asObservable(): Observable<Query<T>> =
|
||||||
|
observable { emitter ->
|
||||||
|
val listener =
|
||||||
|
object : Query.Listener {
|
||||||
|
override fun queryResultsChanged() {
|
||||||
|
emitter.onNext(this@asObservable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.onNext(this@asObservable)
|
||||||
|
addListener(listener)
|
||||||
|
emitter.setCancellable { removeListener(listener) }
|
||||||
|
}
|
@ -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;
|
@ -2,15 +2,14 @@ package com.shabinder.common.database
|
|||||||
|
|
||||||
import co.touchlab.kermit.CommonLogger
|
import co.touchlab.kermit.CommonLogger
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
import com.shabinder.database.Database
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
|
||||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
actual fun createDb(): DownloadRecordDatabase {
|
actual fun createDatabase(): Database {
|
||||||
val databasePath = File(System.getProperty("java.io.tmpdir"), "DownloadRecordDatabase.db")
|
val databasePath = File(System.getProperty("java.io.tmpdir"), "Database.db")
|
||||||
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}")
|
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}")
|
||||||
.also { DownloadRecordDatabase.Schema.create(it) }
|
.also { Database.Schema.create(it) }
|
||||||
return DownloadRecordDatabase(driver)
|
return Database(driver)
|
||||||
}
|
}
|
||||||
actual fun getLogger(): Logger = CommonLogger()
|
actual fun getLogger(): Logger = CommonLogger()
|
@ -12,6 +12,7 @@ kotlin {
|
|||||||
implementation(project(":common:database"))
|
implementation(project(":common:database"))
|
||||||
implementation(project(":fuzzywuzzy:app"))
|
implementation(project(":fuzzywuzzy:app"))
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1")
|
||||||
implementation(Ktor.clientCore)
|
implementation(Ktor.clientCore)
|
||||||
implementation(Ktor.clientCio)
|
implementation(Ktor.clientCio)
|
||||||
implementation(Ktor.clientSerialization)
|
implementation(Ktor.clientSerialization)
|
||||||
|
@ -20,7 +20,8 @@ import co.touchlab.kermit.Kermit
|
|||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.spotify.Source
|
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 io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -28,7 +29,7 @@ import org.koin.core.KoinComponent
|
|||||||
|
|
||||||
actual class YoutubeProvider actual constructor(
|
actual class YoutubeProvider actual constructor(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val database: DownloadRecordDatabase,
|
private val database: Database,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
){
|
){
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
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.database.getLogger
|
||||||
import com.shabinder.common.providers.GaanaProvider
|
import com.shabinder.common.providers.GaanaProvider
|
||||||
import com.shabinder.common.providers.SpotifyProvider
|
import com.shabinder.common.providers.SpotifyProvider
|
||||||
@ -22,9 +22,10 @@ fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclarat
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun commonModule(enableNetworkLogs: Boolean) = module {
|
fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||||
single { Dir() }
|
single { Dir(get()) }
|
||||||
single { createDb() }
|
single { createDatabase() }
|
||||||
single { Kermit(getLogger()) }
|
single { Kermit(getLogger()) }
|
||||||
|
single { TokenStore(get(),get()) }
|
||||||
single { YoutubeMusic(get(),get()) }
|
single { YoutubeMusic(get(),get()) }
|
||||||
single { SpotifyProvider(get(),get(),get(),get()) }
|
single { SpotifyProvider(get(),get(),get(),get()) }
|
||||||
single { GaanaProvider(get(),get(),get(),get()) }
|
single { GaanaProvider(get(),get(),get(),get()) }
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
package com.shabinder.common
|
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 isPresent(path:String):Boolean
|
||||||
fun fileSeparator(): String
|
fun fileSeparator(): String
|
||||||
fun defaultDir(): String
|
fun defaultDir(): String
|
||||||
fun imageDir(): 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 =
|
fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
|
||||||
expect class YoutubeProvider(
|
expect class YoutubeProvider(
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
database: DownloadRecordDatabase,
|
database: Database,
|
||||||
logger: Kermit,
|
logger: Kermit,
|
||||||
dir: Dir
|
dir: Dir
|
||||||
) {
|
) {
|
||||||
|
@ -22,14 +22,14 @@ import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
|||||||
import com.shabinder.common.gaana.GaanaRequests
|
import com.shabinder.common.gaana.GaanaRequests
|
||||||
import com.shabinder.common.gaana.GaanaTrack
|
import com.shabinder.common.gaana.GaanaTrack
|
||||||
import com.shabinder.common.spotify.Source
|
import com.shabinder.common.spotify.Source
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class GaanaProvider(
|
class GaanaProvider(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
private val database: DownloadRecordDatabase,
|
private val database: Database,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
): GaanaRequests {
|
): GaanaRequests {
|
||||||
|
@ -20,14 +20,14 @@ import co.touchlab.kermit.Kermit
|
|||||||
import com.shabinder.common.*
|
import com.shabinder.common.*
|
||||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.spotify.*
|
import com.shabinder.common.spotify.*
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SpotifyProvider(
|
class SpotifyProvider(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
private val database: DownloadRecordDatabase,
|
private val database: Database,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
) :SpotifyRequests {
|
) :SpotifyRequests {
|
||||||
|
@ -10,7 +10,7 @@ import io.ktor.client.request.*
|
|||||||
import io.ktor.client.request.forms.*
|
import io.ktor.client.request.forms.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
|
||||||
suspend fun authenticateSpotify(): Token {
|
suspend fun authenticateSpotify(): TokenData {
|
||||||
return spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
|
return spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
|
||||||
body = FormDataContent(Parameters.build { append("grant_type","client_credentials") })
|
body = FormDataContent(Parameters.build { append("grant_type","client_credentials") })
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common.utils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removing Illegal Chars from File Name
|
* Removing Illegal Chars from File Name
|
||||||
@ -39,4 +39,4 @@ fun removeIllegalChars(fileName: String): String {
|
|||||||
name = name.replace(":".toRegex(), "")
|
name = name.replace(":".toRegex(), "")
|
||||||
name = name.replace("\\|".toRegex(), "")
|
name = name.replace("\\|".toRegex(), "")
|
||||||
return name
|
return name
|
||||||
}
|
}
|
@ -1,8 +1,9 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
actual open class Dir{
|
actual open class Dir actual constructor(private val logger: Kermit) {
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
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 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -20,14 +20,15 @@ import co.touchlab.kermit.Kermit
|
|||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.spotify.Source
|
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 io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
actual class YoutubeProvider actual constructor(
|
actual class YoutubeProvider actual constructor(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val database: DownloadRecordDatabase,
|
private val database: Database,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
){
|
){
|
||||||
|
@ -20,6 +20,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(project(":common:database"))
|
implementation(project(":common:database"))
|
||||||
|
implementation(project(":common:dependency-injection"))
|
||||||
implementation(project(":common:compose-ui"))
|
implementation(project(":common:compose-ui"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import androidx.compose.desktop.Window
|
import androidx.compose.desktop.Window
|
||||||
|
import com.shabinder.common.initKoin
|
||||||
|
|
||||||
|
private val koin = initKoin(enableNetworkLogs = true).koin
|
||||||
|
|
||||||
fun main() = Window {
|
fun main() = Window {
|
||||||
//TODO
|
//TODO
|
||||||
|
Loading…
Reference in New Issue
Block a user