Skip to content

Commit

Permalink
- User Configurable Spotify Creds Support.
Browse files Browse the repository at this point in the history
- Crash & Visibility Fix for SettingsIcon.
- Changed default Creds, thanks to spotDL.
  • Loading branch information
Shabinder committed Oct 11, 2022
1 parent 76ef76e commit 3e865ee
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ package com.shabinder.common.uikit.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
Expand All @@ -21,8 +25,12 @@ import androidx.compose.material.RadioButton
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Insights
import androidx.compose.material.icons.rounded.ManageAccounts
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.SnippetFolder
import androidx.compose.runtime.Composable
Expand All @@ -35,14 +43,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import com.shabinder.common.models.Actions
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.translations.Strings
import com.shabinder.common.uikit.configurations.SpotiFlyerShapes
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
import com.shabinder.common.uikit.configurations.colorAccent
import com.shabinder.common.uikit.configurations.colorOffWhite
import com.shabinder.common.uikit.configurations.colorPrimary

@Composable
fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
Expand Down Expand Up @@ -110,7 +124,12 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
onClick = { component.selectNewDownloadDirectory() }
)
) {
Icon(Icons.Rounded.SnippetFolder, Strings.setDownloadDirectory(), Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
Icon(
Icons.Rounded.SnippetFolder,
Strings.setDownloadDirectory(),
Modifier.size(32.dp),
tint = Color(0xFFCCCCCC)
)
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
Expand All @@ -126,6 +145,92 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {

Spacer(Modifier.padding(top = 12.dp))

SettingsRow(
icon = rememberVectorPainter(Icons.Rounded.ManageAccounts),
title = Strings.spotifyCreds(),
value = if (model.spotifyCredentials == SpotifyCredentials()) Strings.defaultString() else Strings.userSet(),
contentEnd = {
Spacer(Modifier.weight(1f))
Icon(
Icons.Rounded.Edit,
"Edit",
Modifier.padding(end = 8.dp).size(24.dp),
tint = Color(0xFFCCCCCC)
)
}
) { save ->
Spacer(Modifier.padding(top = 8.dp))

var clientID by remember { mutableStateOf(model.spotifyCredentials.clientID) }
var clientSecret by remember { mutableStateOf(model.spotifyCredentials.clientSecret) }
TextField(
value = clientID,
onValueChange = { clientID = it.trim() },
label = { Text(Strings.clientID()) }
)
Spacer(Modifier.padding(vertical = 4.dp))

TextField(
value = clientSecret,
onValueChange = { clientSecret = it.trim() },
label = { Text(Strings.clientSecret()) }
)

Spacer(Modifier.padding(vertical = 4.dp))

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
TextButton(
onClick = {
component.updateSpotifyCredentials(
SpotifyCredentials(
clientID,
clientSecret
)
)
Actions.instance.showPopUpMessage(Strings.requestAppRestart())
save()
},
Modifier.padding(bottom = 8.dp, start = 8.dp, end = 8.dp).wrapContentWidth()
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
.padding(horizontal = 4.dp),
shape = SpotiFlyerShapes.small
) {
Text(
Strings.save(),
color = Color.Black,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}

TextButton(
onClick = {
component.updateSpotifyCredentials(
SpotifyCredentials()
)
Actions.instance.showPopUpMessage(Strings.requestAppRestart())
save()
},
Modifier.padding(bottom = 8.dp, start = 8.dp, end = 8.dp).wrapContentWidth()
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
.padding(horizontal = 4.dp),
shape = SpotiFlyerShapes.small
) {
Text(
Strings.reset(),
color = Color.Black,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
}
}

Spacer(Modifier.padding(top = 4.dp))

Row(
modifier = Modifier.fillMaxWidth()
.clickable(
Expand All @@ -134,7 +239,11 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
verticalAlignment = Alignment.CenterVertically
) {
@Suppress("DuplicatedCode")
Icon(Icons.Rounded.Insights, Strings.analytics() + Strings.status(), Modifier.size(32.dp))
Icon(
Icons.Rounded.Insights,
Strings.analytics() + Strings.status(),
Modifier.size(32.dp)
)
Spacer(modifier = Modifier.padding(start = 16.dp))
Column(
Modifier.weight(1f)
Expand Down Expand Up @@ -166,6 +275,7 @@ fun SettingsRow(
icon: Painter,
title: String,
value: String,
contentEnd: @Composable RowScope.() -> Unit = {},
editContent: @Composable ColumnScope.(() -> Unit) -> Unit
) {

Expand All @@ -189,6 +299,7 @@ fun SettingsRow(
style = SpotiFlyerTypography.subtitle2
)
}
contentEnd()
}
AnimatedVisibility(isEditMode) {
Column {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ fun MainScreen(
onBackPressed = callBacks::popBackToHomeScreen,
openPreferenceScreen = callBacks::openPreferenceScreen,
isBackButtonVisible = activeComponent.value.activeChild.instance !is Child.Main,
isSettingsIconVisible = activeComponent.value.activeChild.instance is Child.Main,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(top = topPadding))
Expand All @@ -164,6 +165,7 @@ fun AppBar(
onBackPressed: () -> Unit,
openPreferenceScreen: () -> Unit,
isBackButtonVisible: Boolean,
isSettingsIconVisible: Boolean,
modifier: Modifier = Modifier
) {
TopAppBar(
Expand Down Expand Up @@ -193,10 +195,12 @@ fun AppBar(
}
},
actions = {
IconButton(
onClick = { openPreferenceScreen() }
) {
Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray)
AnimatedVisibility(isSettingsIconVisible) {
IconButton(
onClick = { openPreferenceScreen() }
) {
Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray)
}
}
},
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.shabinder.common.core_components.preference_manager

import co.touchlab.stately.annotation.Throws
import com.russhwolf.settings.Settings
import com.shabinder.common.core_components.analytics.AnalyticsManager
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.spotify.SpotifyCredentials
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlin.native.concurrent.ThreadLocal

class PreferenceManager(
settings: Settings,
Expand All @@ -14,6 +19,14 @@ class PreferenceManager(
const val FIRST_LAUNCH = "firstLaunch"
const val DONATION_INTERVAL = "donationInterval"
const val PREFERRED_AUDIO_QUALITY = "preferredAudioQuality"

@Suppress("VARIABLE_IN_SINGLETON_WITHOUT_THREAD_LOCAL")
lateinit var instance: PreferenceManager
private set
}

init {
instance = this
}

lateinit var analyticsManager: AnalyticsManager
Expand All @@ -35,6 +48,11 @@ class PreferenceManager(
val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320")
fun setPreferredAudioQuality(quality: AudioQuality) = putString(PREFERRED_AUDIO_QUALITY, quality.kbps)

val spotifyCredentials: SpotifyCredentials get() = getStringOrNull("spotifyCredentials")?.let {
Json.decodeFromString(it)
} ?: SpotifyCredentials()
fun setSpotifyCredentials(credentials: SpotifyCredentials) = putString("spotifyCredentials", Json.encodeToString(SpotifyCredentials.serializer(), credentials))

/* OFFSET FOR WHEN TO ASK FOR SUPPORT */
val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
// Min. Donation Asking Interval is `3`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shabinder.common.models.spotify

import kotlinx.serialization.Serializable

@Serializable
data class SpotifyCredentials(
val clientID: String = "5f573c9620494bae87890c0f08a60293",
val clientSecret: String = "212476d9b0f3472eaa762d90b19b0ba8",
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.shabinder.common.core_components.preference_manager.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.Consumer
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.integration.SpotiFlyerPreferenceImpl

interface SpotiFlyerPreference {
Expand All @@ -40,6 +41,8 @@ interface SpotiFlyerPreference {

fun setPreferredQuality(quality: AudioQuality)

fun updateSpotifyCredentials(credentials: SpotifyCredentials)

suspend fun loadImage(url: String): Picture

interface Dependencies {
Expand All @@ -61,7 +64,8 @@ interface SpotiFlyerPreference {
data class State(
val preferredQuality: AudioQuality = AudioQuality.KBPS320,
val downloadPath: String = "",
val isAnalyticsEnabled: Boolean = false
val isAnalyticsEnabled: Boolean = false,
val spotifyCredentials: SpotifyCredentials = SpotifyCredentials()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.shabinder.common.caching.Cache
import com.shabinder.common.core_components.picture.Picture
import com.shabinder.common.core_components.utils.asValue
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.preference.SpotiFlyerPreference.Dependencies
import com.shabinder.common.preference.SpotiFlyerPreference.State
Expand Down Expand Up @@ -67,6 +68,10 @@ internal class SpotiFlyerPreferenceImpl(
store.accept(Intent.SetPreferredAudioQuality(quality))
}

override fun updateSpotifyCredentials(credentials: SpotifyCredentials) {
store.accept(Intent.UpdateSpotifyCredentials(credentials))
}

override suspend fun loadImage(url: String): Picture {
return cache.get(url) {
fileManager.loadImage(url, 150, 150)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.shabinder.common.preference.store

import com.arkivanov.mvikotlin.core.store.Store
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.SpotiFlyerPreference

internal interface SpotiFlyerPreferenceStore : Store<SpotiFlyerPreferenceStore.Intent, SpotiFlyerPreference.State, Nothing> {
Expand All @@ -26,6 +27,7 @@ internal interface SpotiFlyerPreferenceStore : Store<SpotiFlyerPreferenceStore.I
data class ToggleAnalytics(val enabled: Boolean) : Intent()
data class SetDownloadDirectory(val path: String) : Intent()
data class SetPreferredAudioQuality(val quality: AudioQuality) : Intent()
data class UpdateSpotifyCredentials(val credentials: SpotifyCredentials) : Intent()
object GiveDonation : Intent()
object ShareApp : Intent()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.Actions
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.preference.SpotiFlyerPreference.State
import com.shabinder.common.preference.store.SpotiFlyerPreferenceStore.Intent
Expand All @@ -45,12 +46,14 @@ internal class SpotiFlyerPreferenceStoreProvider(
data class AnalyticsToggled(val isEnabled: Boolean) : Result()
data class DownloadPathSet(val path: String) : Result()
data class PreferredAudioQualityChanged(val quality: AudioQuality) : Result()
data class SpotifyCredentialsUpdated(val spotifyCredentials: SpotifyCredentials) : Result()
}

private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
override suspend fun executeAction(action: Unit, getState: () -> State) {
dispatch(Result.AnalyticsToggled(preferenceManager.isAnalyticsEnabled))
dispatch(Result.PreferredAudioQualityChanged(preferenceManager.audioQuality))
dispatch(Result.SpotifyCredentialsUpdated(preferenceManager.spotifyCredentials))
dispatch(Result.DownloadPathSet(fileManager.defaultDir()))
}

Expand All @@ -71,6 +74,11 @@ internal class SpotiFlyerPreferenceStoreProvider(
dispatch(Result.PreferredAudioQualityChanged(intent.quality))
preferenceManager.setPreferredAudioQuality(intent.quality)
}

is Intent.UpdateSpotifyCredentials -> {
dispatch(Result.SpotifyCredentialsUpdated(intent.credentials))
preferenceManager.setSpotifyCredentials(intent.credentials)
}
}
}
}
Expand All @@ -81,6 +89,7 @@ internal class SpotiFlyerPreferenceStoreProvider(
is Result.AnalyticsToggled -> copy(isAnalyticsEnabled = result.isEnabled)
is Result.DownloadPathSet -> copy(downloadPath = result.path)
is Result.PreferredAudioQualityChanged -> copy(preferredQuality = result.quality)
is Result.SpotifyCredentialsUpdated -> copy(spotifyCredentials = result.spotifyCredentials)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.shabinder.common.providers.spotify.requests

import com.shabinder.common.core_components.preference_manager.PreferenceManager
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.Actions
Expand Down Expand Up @@ -45,8 +46,7 @@ suspend fun authenticateSpotify(): SuspendableEvent<TokenData, Throwable> = Susp
@SharedImmutable
private val spotifyAuthClient by lazy {
HttpClient {
val clientId = "694d8bf4f6ec420fa66ea7fb4c68f89d"
val clientSecret = "02ca2d4021a7452dae2328b47a6e8fe8"
val (clientId, clientSecret) = PreferenceManager.instance.spotifyCredentials

install(Auth) {
basic {
Expand Down
Loading

1 comment on commit 3e865ee

@ameliasmiths
Copy link

@ameliasmiths ameliasmiths commented on 3e865ee Dec 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is very easy to download Spotify music for offline listening with Spotify premium. All you need to do is switch on Download on a playlist you want to listen to offline. You can download up to 10,000 songs on each of up to 5 different devices. However, Spotify Downloader free users are not allowed to download Spotify music for offline listening.

Is there any Spotify downloader that can download Spotify music with Spotify free account? The answer is yes. Here we will introduce you free tools for you to download Spotify music to mp3 with either free or premium account.

Please sign in to comment.