Migrating to MVI Arch

This commit is contained in:
shabinder 2021-01-31 17:03:41 +05:30
parent 6e20cc3b0a
commit a8f5941050
48 changed files with 1964 additions and 80 deletions

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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