Jetpack DataStore, Dependency Injection.

This commit is contained in:
shabinder 2021-01-02 00:36:39 +05:30
parent df5f7dd2c5
commit 5c0ec20e1b
20 changed files with 367 additions and 416 deletions

View File

@ -134,6 +134,9 @@ dependencies {
implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version"
implementation "dev.chrisbanes.accompanist:accompanist-insets:$coil_version"
//DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
//Extras
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1'

View File

@ -41,10 +41,13 @@
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<data android:mimeType="text/plain" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<!-- Add your API key here -->

View File

@ -28,8 +28,9 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.example.jetcaster.util.verticalGradientScrim
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.navigation.ComposeNavigation
import com.shabinder.spotiflyer.navigation.navigateToPlatform
import com.shabinder.spotiflyer.navigation.navigateToTrackList
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.ui.ComposeLearnTheme
@ -40,7 +41,10 @@ import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.accompanist.insets.ProvideWindowInsets
import dev.chrisbanes.accompanist.insets.statusBarsHeight
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
@ -69,14 +73,12 @@ class MainActivity : AppCompatActivity() {
ComposeLearnTheme {
Providers(AmbientContentColor provides colorOffWhite) {
ProvideWindowInsets {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.6f)
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.7f)
navController = rememberNavController()
val gradientColor by sharedViewModel.gradientColor.collectAsState()
Column(
modifier = Modifier.fillMaxSize().verticalGradientScrim(
color = gradientColor.copy(alpha = 0.38f),
color = sharedViewModel.gradientColor.copy(alpha = 0.38f),
startYPercentage = 1f,
endYPercentage = 0f,
fixedHeight = 700f,
@ -101,7 +103,6 @@ class MainActivity : AppCompatActivity() {
}
private fun initialize() {
authenticateSpotify()
requestStoragePermission()
disableDozeMode()
//checkIfLatestVersion()
@ -147,50 +148,12 @@ class MainActivity : AppCompatActivity() {
}
}
/**
* Adding my own Spotify Web Api Requests!
* */
private fun implementSpotifyService(token: String) {
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient.addInterceptor(Interceptor { chain ->
val request: Request =
chain.request().newBuilder().addHeader(
"Authorization",
"Bearer $token"
).build()
chain.proceed(request)
}).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder().run{
baseUrl("https://api.spotify.com/v1/")
client(httpClient.build())
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}
sharedViewModel.spotifyService.value = retrofit.create(SpotifyService::class.java)
}
fun authenticateSpotify() {
if(sharedViewModel.spotifyService.value == null){
sharedViewModel.viewModelScope.launch {
log("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken()
token.value?.let {
showDialog1("Success: Spotify Token Acquired")
implementSpotifyService(it.access_token)
}
log("Spotify Token", token.value.toString())
}
}
}
private fun handleIntentFromExternalActivity(intent: Intent? = getIntent()) {
if (intent?.action == Intent.ACTION_SEND) {
if ("text/plain" == intent.type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
log("Intent Received", it)
navController.navigateToPlatform(it)
navController.navigateToTrackList(it)
}
}
}

View File

@ -17,41 +17,42 @@
package com.shabinder.spotiflyer
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.ui.colorPrimary
import com.shabinder.spotiflyer.ui.colorPrimaryDark
import com.shabinder.spotiflyer.ui.home.HomeCategory
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ActivityRetainedScoped
class SharedViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO,
val spotifyService: SpotifyService,
val gaanaInterface : GaanaInterface,
val ytDownloader: YoutubeDownloader
) : ViewModel() {
var spotifyService = MutableStateFlow<SpotifyService?>(null)
var isAuthenticated by mutableStateOf(false)
private set
private val _gradientColor = MutableStateFlow(colorPrimaryDark)
val gradientColor : StateFlow<Color>
get() = _gradientColor
fun authenticated(s:Boolean) {
isAuthenticated = s
}
var gradientColor by mutableStateOf(colorPrimaryDark)
private set
fun updateGradientColor(color: Color) {
_gradientColor.value = color
gradientColor = color
}
fun resetGradient() {
_gradientColor.value = colorPrimaryDark
gradientColor = colorPrimaryDark
}
}

View File

@ -1,9 +1,12 @@
package com.shabinder.spotiflyer.models
import com.shabinder.spotiflyer.models.spotify.Source
data class PlatformQueryResult(
var folderType: String,
var subFolder: String,
var title: String,
var coverUrl: String,
var trackList: List<TrackDetails>
var trackList: List<TrackDetails>,
var source: Source
)

View File

@ -18,11 +18,12 @@
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import com.squareup.moshi.Json
import kotlinx.parcelize.Parcelize
@Parcelize
data class Token(
var access_token:String,
var token_type:String,
var expires_in:Int
var access_token:String?,
var token_type:String?,
@Json(name = "expires_in") var expiry:Long?
): Parcelable

View File

@ -7,12 +7,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.*
import androidx.navigation.compose.popUpTo
import com.shabinder.spotiflyer.ui.home.Home
import com.shabinder.spotiflyer.ui.platforms.gaana.Gaana
import com.shabinder.spotiflyer.ui.platforms.spotify.Spotify
import com.shabinder.spotiflyer.ui.platforms.youtube.Youtube
import com.shabinder.spotiflyer.utils.mainActivity
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import com.shabinder.spotiflyer.ui.tracklist.TrackList
@Composable
fun ComposeNavigation(navController: NavHostController) {
@ -29,34 +24,10 @@ fun ComposeNavigation(navController: NavHostController) {
//Spotify Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"spotify/{link}",
"track_list/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Spotify(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Gaana Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"gaana/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Gaana(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Youtube Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"youtube/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Youtube(
TrackList(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController
)
@ -64,34 +35,10 @@ fun ComposeNavigation(navController: NavHostController) {
}
}
fun NavController.navigateToPlatform(link:String){
when{
//SPOTIFY
link.contains("spotify",true) -> {
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
mainActivity.authenticateSpotify()
}
this.navigateAndPopUpToHome("spotify/$link")
}
//YOUTUBE
link.contains("youtube.com",true) || link.contains("youtu.be",true) -> {
this.navigateAndPopUpToHome("youtube/$link")
}
//GAANA
link.contains("gaana",true) -> {
this.navigateAndPopUpToHome("gaana/$link")
}
else -> showDialog("Link is Not Valid")
}
}
fun NavController.navigateAndPopUpToHome(route:String, inclusive:Boolean = false,singleInstance:Boolean = true){
this.navigate(route){
fun NavController.navigateToTrackList(link:String, singleInstance: Boolean = true, inclusive:Boolean = false) {
navigate("track_list/$link") {
launchSingleTop = singleInstance
popUpTo(route = "home"){
popUpTo(route = "home") {
this.inclusive = inclusive
}
}

View File

@ -0,0 +1,40 @@
package com.shabinder.spotiflyer.networking
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.utils.TokenStore
import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.showDialog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
/**
* Interceptor which adds authorization token in header.
*/
@Singleton
class SpotifyAuthInterceptor @Inject constructor(
private val tokenStore: TokenStore,
) : Interceptor {
/*
* Local Copy for Token
* Live Throughout Session
* */
private var token by mutableStateOf<Token?>(null)
override fun intercept(chain: Interceptor.Chain): Response {
if(token?.expiry?:0 < System.currentTimeMillis()/1000){
//Token Expired time to fetch New One
runBlocking { token = tokenStore.getToken() }
log("Spotify Auth",token.toString())
}
val authRequest = chain.request().newBuilder().
addHeader("Authorization", "Bearer ${token?.access_token}").build()
return chain.proceed(authRequest)
}
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.platforms.gaana
package com.shabinder.spotiflyer.providers
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
@ -25,15 +25,39 @@ import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
private const val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
suspend fun queryGaana(
fullLink: String,
):PlatformQueryResult?{
//Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")
val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
log("Gaana Fragment", "$type : $link")
//Error
if (type == "Error" || link == "Error"){
showDialog("Please Check Your Link!")
return null
}
return gaanaSearch(
type,
link,
sharedViewModel.gaanaInterface,
sharedViewModel.databaseDAO,
)
}
suspend fun gaanaSearch(
type:String,
link:String,
@ -46,6 +70,7 @@ suspend fun gaanaSearch(
title = link,
coverUrl = gaanaPlaceholderImageUrl,
trackList = listOf(),
Source.Gaana
)
with(result) {
when (type) {

View File

@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.platforms.spotify
package com.shabinder.spotiflyer.providers
import androidx.annotation.WorkerThread
import androidx.compose.runtime.Composable
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
@ -34,6 +35,40 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
suspend fun querySpotify(fullLink: String):PlatformQueryResult?{
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
if (!spotifyLink.contains("open.spotify")) {
//Very Rare instance
spotifyLink = resolveLink(spotifyLink, sharedViewModel.gaanaInterface)
}
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
log("Spotify Fragment", "$type : $link")
if (type == "Error" || link == "Error") {
showDialog("Please Check Your Link!")
return null
}
if (type == "episode" || type == "show") {
//TODO Implementation
showDialog("Implementing Soon, Stay Tuned!")
return null
}
return spotifySearch(
type,
link,
sharedViewModel.spotifyService,
sharedViewModel.databaseDAO
)
}
suspend fun spotifySearch(
type:String,
link: String,
@ -46,6 +81,7 @@ suspend fun spotifySearch(
title = "",
coverUrl = "",
trackList = listOf(),
Source.Spotify
)
with(result) {
when (type) {
@ -192,6 +228,10 @@ suspend fun spotifySearch(
return result
}
/*
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
@WorkerThread
fun resolveLink(
url:String,

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.platforms.youtube
package com.shabinder.spotiflyer.providers
import android.annotation.SuppressLint
import com.github.kiulian.downloader.YoutubeDownloader
@ -37,6 +37,43 @@ import java.io.File
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */
private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com"
/*
* Sending a Result as null = Some Error Occurred!
* */
suspend fun queryYoutube(fullLink: String): PlatformQueryResult?{
val link = fullLink.removePrefix("https://").removePrefix("http://")
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
return getYTPlaylist(
playlistId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error")
}
if(link.contains(sampleDomain2,true) ){
searchId = link.substringAfterLast("/","error")
}
return if(searchId != "error") {
getYTTrack(
searchId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
null
}
}
}
suspend fun getYTPlaylist(
searchId: String,
ytDownloader: YoutubeDownloader,
@ -48,6 +85,7 @@ suspend fun getYTPlaylist(
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
)
with(result) {
try {
@ -127,6 +165,7 @@ suspend fun getYTTrack(
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
)
with(result) {
try {

View File

@ -15,8 +15,6 @@ import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.viewinterop.viewModel
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -34,14 +32,13 @@ import androidx.core.net.toUri
import androidx.navigation.NavController
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.navigation.navigateToPlatform
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.utils.openPlatform
import com.shabinder.spotiflyer.utils.sharedViewModel
import dev.chrisbanes.accompanist.glide.GlideImage
import kotlinx.coroutines.flow.StateFlow
@Composable
fun Home(navController: NavController, modifier: Modifier = Modifier) {
@ -49,26 +46,23 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
val link by viewModel.link.collectAsState()
AuthenticationBanner(viewModel,modifier)
AuthenticationBanner(sharedViewModel.isAuthenticated,modifier)
SearchPanel(
link,
viewModel.link,
viewModel::updateLink,
navController,
modifier
)
val selectedCategory by viewModel.selectedCategory.collectAsState()
HomeTabBar(
selectedCategory,
viewModel.selectedCategory,
HomeCategory.values(),
viewModel::selectCategory,
modifier
)
when(selectedCategory){
when(viewModel.selectedCategory){
HomeCategory.About -> AboutColumn()
HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController)
}
@ -212,11 +206,9 @@ fun AboutColumn(modifier: Modifier = Modifier) {
@Composable
fun HistoryColumn(
downloadRecordList: StateFlow<List<DownloadRecord>>,
list: List<DownloadRecord>,
navController: NavController
) {
val list by downloadRecordList.collectAsState()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
@ -254,16 +246,15 @@ fun DownloadRecordItem(item: DownloadRecord,navController: NavController) {
}
Image(
imageVector = vectorResource(id = R.drawable.ic_share_open),
modifier = Modifier.clickable(onClick = { navController.navigateToPlatform(item.link) })
modifier = Modifier.clickable(onClick = { navController.navigateToTrackList(item.link) })
)
}
}
@Composable
fun AuthenticationBanner(viewModel: HomeViewModel, modifier: Modifier) {
val authenticationStatus by viewModel.isAuthenticating.collectAsState()
fun AuthenticationBanner(isAuthenticated: Boolean, modifier: Modifier) {
if (authenticationStatus) {
if (!isAuthenticated) {
// TODO show a progress indicator or similar
}
}
@ -321,7 +312,7 @@ fun SearchPanel(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(top = 16.dp,)
modifier = modifier.padding(top = 16.dp)
){
TextField(
leadingIcon = {
@ -346,7 +337,7 @@ fun SearchPanel(
OutlinedButton(
modifier = Modifier.padding(12.dp).wrapContentWidth(),
onClick = {
navController.navigateToPlatform(link)
navController.navigateToTrackList(link)
},
border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent)))
){
@ -354,6 +345,8 @@ fun SearchPanel(
}
}
}
@Composable
fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier,

View File

@ -2,56 +2,40 @@ package com.shabinder.spotiflyer.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.utils.sharedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class HomeViewModel : ViewModel() {
private val _link = MutableStateFlow("")
val link:StateFlow<String>
get() = _link
var link by mutableStateOf("")
private set
fun updateLink(s:String) {
_link.value = s
link = s
}
private val _isAuthenticating = MutableStateFlow(true)
val isAuthenticating:StateFlow<Boolean>
get() = _isAuthenticating
fun authenticated(s:Boolean) {
_isAuthenticating.value = s
}
private val _selectedCategory = MutableStateFlow(HomeCategory.About)
val selectedCategory :StateFlow<HomeCategory>
get() = _selectedCategory
var selectedCategory by mutableStateOf(HomeCategory.About)
private set
fun selectCategory(s:HomeCategory) {
_selectedCategory.value = s
selectedCategory = s
}
private val _downloadRecordList = MutableStateFlow<List<DownloadRecord>>(listOf())
val downloadRecordList: StateFlow<List<DownloadRecord>>
get() = _downloadRecordList
var downloadRecordList by mutableStateOf<List<DownloadRecord>>(listOf())
fun getDownloadRecordList() {
viewModelScope.launch {
withContext(Dispatchers.IO){
_downloadRecordList.value = sharedViewModel.databaseDAO.getRecord().toMutableList()
downloadRecordList = sharedViewModel.databaseDAO.getRecord()
}
}
}
init {
getDownloadRecordList()
}
}
enum class HomeCategory {

View File

@ -1,55 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.gaana
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch
@Composable
fun Gaana(
fullLink: String,
navController: NavController
) {
val source = Source.Gaana
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
//Coroutine Scope Active till this Composable is Active
val coroutineScope = rememberCoroutineScope()
//Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")
val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
log("Gaana Fragment", "$type : $link")
//Error
if (type == "Error" || link == "Error"){
showDialog("Please Check Your Link!")
navController.popBackStack()
}
coroutineScope.launch {
result = gaanaSearch(
type,
link,
sharedViewModel.gaanaInterface,
sharedViewModel.databaseDAO,
)
}
result?.let {
TrackList(
result = it,
source = source
)
}
}

View File

@ -1,79 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.spotify
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun Spotify(fullLink: String, navController: NavController,) {
val source: Source = Source.Spotify
val coroutineScope = rememberCoroutineScope()
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
log("Spotify Fragment Link", spotifyLink)
coroutineScope.launch(Dispatchers.Default) {
/*
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
if (!spotifyLink.contains("open.spotify")) {
val resolvedLink = resolveLink(spotifyLink, sharedViewModel.gaanaInterface)
log("Spotify Resolved Link", resolvedLink)
spotifyLink = resolvedLink
}
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
log("Spotify Fragment", "$type : $link")
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
if (isOnline()) mainActivity.authenticateSpotify()
}
if (type == "Error" || link == "Error") {
showDialog("Please Check Your Link!")
navController.popBackStack()
}
if (type == "episode" || type == "show") {
//TODO Implementation
showDialog("Implementing Soon, Stay Tuned!")
} else {
if (sharedViewModel.spotifyService.value == null){
//Authentication Still Pending
// TODO Better Implementation
showDialog("Authentication Failed")
navController.popBackStack()
}else{
result = spotifySearch(
type,
link,
sharedViewModel.spotifyService.value!!,
sharedViewModel.databaseDAO
)
}
}
}
result?.let {
log("Spotify",it.trackList.size.toString())
TrackList(
result = it,
source = source
)
}
}

View File

@ -1,64 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.youtube
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import kotlinx.coroutines.launch
private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com"
@Composable
fun Youtube(fullLink: String, navController: NavController,) {
val source = Source.YouTube
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
//Coroutine Scope Active till this Composable is Active
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
val link = fullLink.removePrefix("https://").removePrefix("http://")
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
getYTPlaylist(
playlistId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error")
}
if(link.contains(sampleDomain2,true) ){
searchId = link.substringAfterLast("/","error")
}
if(searchId != "error") {
result = getYTTrack(
searchId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
navController.popBackStack()
}
}
}
result?.let {
TrackList(
result = it,
source = source
)
}
}

View File

@ -8,6 +8,10 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -19,14 +23,18 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.navigation.NavController
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent
import com.shabinder.spotiflyer.providers.queryGaana
import com.shabinder.spotiflyer.providers.querySpotify
import com.shabinder.spotiflyer.providers.queryYoutube
import com.shabinder.spotiflyer.ui.utils.calculateDominantColor
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import dev.chrisbanes.accompanist.coil.CoilImage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -36,28 +44,56 @@ import kotlinx.coroutines.launch
**/
@Composable
fun TrackList(
result: PlatformQueryResult,
source: Source,
fullLink: String,
navController: NavController,
modifier: Modifier = Modifier
){
val coroutineScope = rememberCoroutineScope()
Box(modifier = modifier.fillMaxSize()){
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
item {
CoverImage(result.title,result.coverUrl,coroutineScope)
var result by remember(fullLink) { mutableStateOf<PlatformQueryResult?>(null) }
coroutineScope.launch {
if(result == null){
result = when{
//SPOTIFY
fullLink.contains("spotify",true) -> querySpotify(fullLink)
//YOUTUBE
fullLink.contains("youtube.com",true) || fullLink.contains("youtu.be",true) -> queryYoutube(fullLink)
//GAANA
fullLink.contains("gaana",true) -> queryGaana(fullLink)
else -> {
showDialog("Link is Not Valid")
null
}
items(result.trackList) {
TrackCard(track = it)
}
},
modifier = Modifier.fillMaxSize(),
)
DownloadAllButton(
onClick = {},
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
)
}
}
//Error Occurred And Has Been Shown to User
if(result == null) navController.popBackStack()
}
result?.let{
Box(modifier = modifier.fillMaxSize()){
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
item {
CoverImage(it.title,it.coverUrl,coroutineScope)
}
items(it.trackList) {
TrackCard(track = it)
}
},
modifier = Modifier.fillMaxSize(),
)
DownloadAllButton(
onClick = {},
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
)
}
}
}
@ -87,7 +123,9 @@ fun CoverImage(
//color = colorAccent,
)
}
scope.launch { updateGradient(coverURL) }
scope.launch {
updateGradient(coverURL)
}
}
@Composable

View File

@ -26,7 +26,8 @@ const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
log("Network Requesting",chain.request().url.toString())
log("Network Requesting Debug",chain.request().url.toString())
return if (!isOnline()){
//No Internet Connection
showDialog()

View File

@ -3,15 +3,11 @@ package com.shabinder.spotiflyer.utils
import android.content.Context
import android.os.Environment
import android.util.Base64
import androidx.lifecycle.ViewModelProvider
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.*
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
@ -26,6 +22,7 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@ -59,15 +56,26 @@ object Provider {
@Provides
@Singleton
fun getMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
fun getTokenStore(
@ApplicationContext appContext: Context,
spotifyServiceTokenRequest: SpotifyServiceTokenRequest):TokenStore = TokenStore(appContext,spotifyServiceTokenRequest)
@Provides
@Singleton
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
fun getSpotifyService(authInterceptor: SpotifyAuthInterceptor,okHttpClient: OkHttpClient.Builder,moshi: Moshi) :SpotifyService{
val retrofit = Retrofit.Builder().run{
baseUrl("https://api.spotify.com/v1/")
client(okHttpClient.addInterceptor(authInterceptor).build())
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}
return retrofit.create(SpotifyService::class.java)
}
@Provides
@Singleton
fun getSpotifyTokenInterface(moshi: Moshi,networkInterceptor: NetworkInterceptor): SpotifyServiceTokenRequest {
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val request: Request =
@ -82,7 +90,7 @@ object Provider {
}"
).build()
chain.proceed(request)
}).addInterceptor(NetworkInterceptor())
}).addInterceptor(networkInterceptor)
val retrofit = Retrofit.Builder()
.baseUrl("https://accounts.spotify.com/")
@ -94,18 +102,10 @@ object Provider {
@Provides
@Singleton
fun okHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(NetworkInterceptor())
.build()
}
@Provides
@Singleton
fun getGaanaInterface(moshi: Moshi, okHttpClient: OkHttpClient): GaanaInterface {
fun getGaanaInterface(moshi: Moshi, okHttpClient: OkHttpClient.Builder): GaanaInterface {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.gaana.com/")
.client(okHttpClient)
.client(okHttpClient.build())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(GaanaInterface::class.java)
@ -122,4 +122,25 @@ object Provider {
return retrofit.create(YoutubeMusicApi::class.java)
}
@Provides
@Singleton
fun getNetworkInterceptor():NetworkInterceptor = NetworkInterceptor()
@Provides
@Singleton
fun okHttpClient(networkInterceptor: NetworkInterceptor): OkHttpClient.Builder {
return OkHttpClient.Builder()
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
.addInterceptor(networkInterceptor)
}
@Provides
@Singleton
fun getMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
}

View File

@ -0,0 +1,47 @@
package com.shabinder.spotiflyer.utils
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.createDataStore
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class TokenStore (
context: Context,
private val spotifyServiceTokenRequest: SpotifyServiceTokenRequest
) {
private val dataStore = context.createDataStore(
name = "settings"
)
private val token = preferencesKey<String>("token")
private val tokenExpiry = preferencesKey<Long>("expiry")
suspend fun saveToken(tokenKey:String,time:Long){
dataStore.edit {
it[token] = tokenKey
it[tokenExpiry] = (System.currentTimeMillis()/1000) + time
}
}
suspend fun getToken(): Token?{
var token = dataStore.data.map {
Token(it[token],null,it[tokenExpiry])
}.firstOrNull()
if(System.currentTimeMillis()/1000 > token?.expiry?:0){
token = spotifyServiceTokenRequest.getToken().value
log("Spotify Token","Requesting New Token")
GlobalScope.launch { token?.access_token?.let { saveToken(it,token.expiry ?: 0) } }
}
return token
}
}