mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 01:04: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.androidViewModel)
|
||||
|
||||
//DECOMPOSE
|
||||
implementation(Decompose.decompose)
|
||||
implementation(Decompose.extensionsCompose)
|
||||
//Lifecycle
|
||||
Versions.androidLifecycle.let{
|
||||
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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.setContent
|
||||
import com.shabinder.common.spotify.authenticateSpotify
|
||||
import com.shabinder.android.di.appModule
|
||||
import com.shabinder.common.database.appContext
|
||||
import com.shabinder.common.initKoin
|
||||
import com.shabinder.common.ui.SpotiFlyerMain
|
||||
import com.shabinder.common.youtube.YoutubeMusic
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -17,9 +16,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setContent {
|
||||
val scope = rememberCoroutineScope()
|
||||
SpotiFlyerMain()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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()
|
||||
maven(url = "https://jitpack.io")
|
||||
maven(url = "https://dl.bintray.com/ekito/koin")
|
||||
maven(url = "https://kotlin.bintray.com/kotlinx/")
|
||||
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers/")
|
||||
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
flatDir {
|
||||
|
@ -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}"
|
||||
}
|
||||
}
|
||||
object Decompose {
|
||||
private const val VERSION = "0.1.7"
|
||||
const val decompose = "com.arkivanov.decompose:decompose:$VERSION"
|
||||
const val decomposeIosX64 = "com.arkivanov.decompose:decompose-iosx64:$VERSION"
|
||||
const val decomposeIosArm64 = "com.arkivanov.decompose:decompose-iosarm64:$VERSION"
|
||||
const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION"
|
||||
}
|
||||
object MVIKotlin {
|
||||
private const val VERSION = "2.0.0"
|
||||
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
|
||||
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION"
|
||||
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION"
|
||||
const val mvikotlinMainIosX64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosx64:$VERSION"
|
||||
const val mvikotlinMainIosArm64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosarm64:$VERSION"
|
||||
const val mvikotlinLogging = "com.arkivanov.mvikotlin:mvikotlin-logging:$VERSION"
|
||||
const val mvikotlinTimeTravel = "com.arkivanov.mvikotlin:mvikotlin-timetravel:$VERSION"
|
||||
const val mvikotlinExtensionsReaktive = "com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:$VERSION"
|
||||
}
|
||||
object Badoo {
|
||||
object Reaktive {
|
||||
private const val VERSION = "1.1.19"
|
||||
const val reaktive = "com.badoo.reaktive:reaktive:$VERSION"
|
||||
const val reaktiveTesting = "com.badoo.reaktive:reaktive-testing:$VERSION"
|
||||
const val utils = "com.badoo.reaktive:utils:$VERSION"
|
||||
const val coroutinesInterop = "com.badoo.reaktive:coroutines-interop:$VERSION"
|
||||
}
|
||||
}
|
||||
object Ktor {
|
||||
val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}"
|
||||
val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}"
|
||||
|
@ -7,8 +7,14 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(Deps.ArkIvanov.Decompose.decompose)
|
||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
|
||||
implementation(project(":common:dependency-injection"))
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:database"))
|
||||
implementation(MVIKotlin.mvikotlin)
|
||||
implementation(MVIKotlin.mvikotlinExtensionsReaktive)
|
||||
implementation(Badoo.Reaktive.reaktive)
|
||||
implementation(Decompose.decompose)
|
||||
implementation(Decompose.extensionsCompose)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
data class DownloadRecord(
|
||||
var id:Int = 0,
|
||||
var id:Long = 0,
|
||||
var type:String,
|
||||
var name:String,
|
||||
var link:String,
|
||||
var coverUrl:String,
|
||||
var totalFiles:Int = 1,
|
||||
var totalFiles:Long = 1,
|
||||
)
|
@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Token(
|
||||
data class TokenData(
|
||||
var access_token:String?,
|
||||
var token_type:String?,
|
||||
@SerialName("expires_in") var expiry:Long?
|
@ -5,7 +5,7 @@ plugins {
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
database("DownloadRecordDatabase") {
|
||||
database("Database") {
|
||||
packageName = "com.shabinder.database"
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,8 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(Deps.Badoo.Reaktive.reaktive)
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(Badoo.Reaktive.reaktive)
|
||||
// SQL Delight
|
||||
implementation(SqlDelight.runtime)
|
||||
implementation(SqlDelight.coroutineExtensions)
|
||||
|
@ -3,14 +3,13 @@ package com.shabinder.common.database
|
||||
import android.content.Context
|
||||
import co.touchlab.kermit.LogcatLogger
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.database.Database
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
|
||||
lateinit var appContext: Context
|
||||
|
||||
actual fun createDb(): DownloadRecordDatabase {
|
||||
val driver = AndroidSqliteDriver(DownloadRecordDatabase.Schema, appContext, "DownloadRecordDatabase.db")
|
||||
return DownloadRecordDatabase(driver)
|
||||
actual fun createDatabase(): Database {
|
||||
val driver = AndroidSqliteDriver(Database.Schema, appContext, "Database.db")
|
||||
return Database(driver)
|
||||
}
|
||||
actual fun getLogger(): Logger = LogcatLogger()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.shabinder.common.database
|
||||
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.shabinder.database.Database
|
||||
|
||||
expect fun createDb() : DownloadRecordDatabase
|
||||
expect fun createDatabase() : Database
|
||||
expect fun getLogger(): Logger
|
@ -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.Logger
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.shabinder.database.Database
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import java.io.File
|
||||
|
||||
actual fun createDb(): DownloadRecordDatabase {
|
||||
val databasePath = File(System.getProperty("java.io.tmpdir"), "DownloadRecordDatabase.db")
|
||||
actual fun createDatabase(): Database {
|
||||
val databasePath = File(System.getProperty("java.io.tmpdir"), "Database.db")
|
||||
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}")
|
||||
.also { DownloadRecordDatabase.Schema.create(it) }
|
||||
return DownloadRecordDatabase(driver)
|
||||
.also { Database.Schema.create(it) }
|
||||
return Database(driver)
|
||||
}
|
||||
actual fun getLogger(): Logger = CommonLogger()
|
@ -12,6 +12,7 @@ kotlin {
|
||||
implementation(project(":common:database"))
|
||||
implementation(project(":fuzzywuzzy:app"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1")
|
||||
implementation(Ktor.clientCore)
|
||||
implementation(Ktor.clientCio)
|
||||
implementation(Ktor.clientSerialization)
|
||||
|
@ -20,7 +20,8 @@ import co.touchlab.kermit.Kermit
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.spotify.Source
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -28,7 +29,7 @@ import org.koin.core.KoinComponent
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val database: DownloadRecordDatabase,
|
||||
private val database: Database,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
){
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.database.createDb
|
||||
import com.shabinder.common.database.createDatabase
|
||||
import com.shabinder.common.database.getLogger
|
||||
import com.shabinder.common.providers.GaanaProvider
|
||||
import com.shabinder.common.providers.SpotifyProvider
|
||||
@ -22,9 +22,10 @@ fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclarat
|
||||
}
|
||||
|
||||
fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||
single { Dir() }
|
||||
single { createDb() }
|
||||
single { Dir(get()) }
|
||||
single { createDatabase() }
|
||||
single { Kermit(getLogger()) }
|
||||
single { TokenStore(get(),get()) }
|
||||
single { YoutubeMusic(get(),get()) }
|
||||
single { SpotifyProvider(get(),get(),get(),get()) }
|
||||
single { GaanaProvider(get(),get(),get(),get()) }
|
||||
|
@ -1,12 +1,25 @@
|
||||
package com.shabinder.common
|
||||
|
||||
expect open class Dir() {
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
|
||||
expect open class Dir(
|
||||
logger: Kermit
|
||||
) {
|
||||
fun isPresent(path:String):Boolean
|
||||
fun fileSeparator(): String
|
||||
fun defaultDir(): String
|
||||
fun imageDir(): String
|
||||
fun createDirectory(dirPath:String)
|
||||
}
|
||||
fun Dir.createDirectories() {
|
||||
createDirectory(defaultDir())
|
||||
createDirectory(imageDir())
|
||||
createDirectory(defaultDir() + "Tracks/")
|
||||
createDirectory(defaultDir() + "Albums/")
|
||||
createDirectory(defaultDir() + "Playlists/")
|
||||
createDirectory(defaultDir() + "YT_Downloads/")
|
||||
}
|
||||
|
||||
fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
||||
|
@ -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
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.*
|
||||
|
||||
expect class YoutubeProvider(
|
||||
httpClient: HttpClient,
|
||||
database: DownloadRecordDatabase,
|
||||
database: Database,
|
||||
logger: Kermit,
|
||||
dir: Dir
|
||||
) {
|
||||
|
@ -22,14 +22,14 @@ import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.gaana.GaanaRequests
|
||||
import com.shabinder.common.gaana.GaanaTrack
|
||||
import com.shabinder.common.spotify.Source
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class GaanaProvider(
|
||||
override val httpClient: HttpClient,
|
||||
private val database: DownloadRecordDatabase,
|
||||
private val database: Database,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
): GaanaRequests {
|
||||
|
@ -20,14 +20,14 @@ import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.*
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.spotify.*
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SpotifyProvider(
|
||||
override val httpClient: HttpClient,
|
||||
private val database: DownloadRecordDatabase,
|
||||
private val database: Database,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
) :SpotifyRequests {
|
||||
|
@ -10,7 +10,7 @@ import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
|
||||
suspend fun authenticateSpotify(): Token {
|
||||
suspend fun authenticateSpotify(): TokenData {
|
||||
return spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
|
||||
body = FormDataContent(Parameters.build { append("grant_type","client_credentials") })
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.shabinder.common
|
||||
package com.shabinder.common.utils
|
||||
|
||||
/**
|
||||
* Removing Illegal Chars from File Name
|
||||
@ -39,4 +39,4 @@ fun removeIllegalChars(fileName: String): String {
|
||||
name = name.replace(":".toRegex(), "")
|
||||
name = name.replace("\\|".toRegex(), "")
|
||||
return name
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import java.io.File
|
||||
|
||||
actual open class Dir{
|
||||
actual open class Dir actual constructor(private val logger: Kermit) {
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
||||
|
||||
@ -14,4 +15,20 @@ actual open class Dir{
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
|
||||
actual fun createDirectory(dirPath:String){
|
||||
val yourAppDir = File(dirPath)
|
||||
|
||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||
{ // create empty directory
|
||||
if (yourAppDir.mkdirs())
|
||||
{logger.i{"$dirPath created"}}
|
||||
else
|
||||
{
|
||||
logger.e{"Unable to create Dir: $dirPath!"}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.i { "$dirPath already exists" }
|
||||
}
|
||||
}
|
||||
}
|
@ -20,14 +20,15 @@ import co.touchlab.kermit.Kermit
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.spotify.Source
|
||||
import com.shabinder.database.DownloadRecordDatabase
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
import com.shabinder.database.Database
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val database: DownloadRecordDatabase,
|
||||
private val database: Database,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
){
|
||||
|
@ -20,6 +20,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(project(":common:database"))
|
||||
implementation(project(":common:dependency-injection"))
|
||||
implementation(project(":common:compose-ui"))
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import androidx.compose.desktop.Window
|
||||
import com.shabinder.common.initKoin
|
||||
|
||||
private val koin = initKoin(enableNetworkLogs = true).koin
|
||||
|
||||
fun main() = Window {
|
||||
//TODO
|
||||
|
Loading…
Reference in New Issue
Block a user