- User Configurable Spotify Creds Support.

- Crash & Visibility Fix for SettingsIcon.
- Changed default Creds, thanks to spotDL.
This commit is contained in:
shabinder 2022-10-12 03:25:51 +05:30
parent 76ef76e522
commit 3e865ee622
10 changed files with 179 additions and 9 deletions

View File

@ -3,15 +3,19 @@ package com.shabinder.common.uikit.screens
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -21,8 +25,12 @@ import androidx.compose.material.RadioButton
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text 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.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Insights 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.MusicNote
import androidx.compose.material.icons.rounded.SnippetFolder import androidx.compose.material.icons.rounded.SnippetFolder
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -35,14 +43,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter 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.dp
import androidx.compose.ui.unit.sp
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState 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.AudioQuality
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.SpotiFlyerPreference import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.translations.Strings 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.SpotiFlyerTypography
import com.shabinder.common.uikit.configurations.colorAccent import com.shabinder.common.uikit.configurations.colorAccent
import com.shabinder.common.uikit.configurations.colorOffWhite import com.shabinder.common.uikit.configurations.colorOffWhite
import com.shabinder.common.uikit.configurations.colorPrimary
@Composable @Composable
fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) { fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
@ -110,7 +124,12 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
onClick = { component.selectNewDownloadDirectory() } 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)) Spacer(modifier = Modifier.padding(start = 16.dp))
Column { Column {
Text( Text(
@ -126,6 +145,92 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
Spacer(Modifier.padding(top = 12.dp)) 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( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.clickable( .clickable(
@ -134,7 +239,11 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@Suppress("DuplicatedCode") @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)) Spacer(modifier = Modifier.padding(start = 16.dp))
Column( Column(
Modifier.weight(1f) Modifier.weight(1f)
@ -166,6 +275,7 @@ fun SettingsRow(
icon: Painter, icon: Painter,
title: String, title: String,
value: String, value: String,
contentEnd: @Composable RowScope.() -> Unit = {},
editContent: @Composable ColumnScope.(() -> Unit) -> Unit editContent: @Composable ColumnScope.(() -> Unit) -> Unit
) { ) {
@ -189,6 +299,7 @@ fun SettingsRow(
style = SpotiFlyerTypography.subtitle2 style = SpotiFlyerTypography.subtitle2
) )
} }
contentEnd()
} }
AnimatedVisibility(isEditMode) { AnimatedVisibility(isEditMode) {
Column { Column {

View File

@ -141,6 +141,7 @@ fun MainScreen(
onBackPressed = callBacks::popBackToHomeScreen, onBackPressed = callBacks::popBackToHomeScreen,
openPreferenceScreen = callBacks::openPreferenceScreen, openPreferenceScreen = callBacks::openPreferenceScreen,
isBackButtonVisible = activeComponent.value.activeChild.instance !is Child.Main, isBackButtonVisible = activeComponent.value.activeChild.instance !is Child.Main,
isSettingsIconVisible = activeComponent.value.activeChild.instance is Child.Main,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.padding(top = topPadding)) Spacer(Modifier.padding(top = topPadding))
@ -164,6 +165,7 @@ fun AppBar(
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
openPreferenceScreen: () -> Unit, openPreferenceScreen: () -> Unit,
isBackButtonVisible: Boolean, isBackButtonVisible: Boolean,
isSettingsIconVisible: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
TopAppBar( TopAppBar(
@ -193,10 +195,12 @@ fun AppBar(
} }
}, },
actions = { actions = {
IconButton( AnimatedVisibility(isSettingsIconVisible) {
onClick = { openPreferenceScreen() } IconButton(
) { onClick = { openPreferenceScreen() }
Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray) ) {
Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray)
}
} }
}, },
modifier = modifier, modifier = modifier,

View File

@ -1,8 +1,13 @@
package com.shabinder.common.core_components.preference_manager package com.shabinder.common.core_components.preference_manager
import co.touchlab.stately.annotation.Throws
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import com.shabinder.common.core_components.analytics.AnalyticsManager import com.shabinder.common.core_components.analytics.AnalyticsManager
import com.shabinder.common.models.AudioQuality 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( class PreferenceManager(
settings: Settings, settings: Settings,
@ -14,6 +19,14 @@ class PreferenceManager(
const val FIRST_LAUNCH = "firstLaunch" const val FIRST_LAUNCH = "firstLaunch"
const val DONATION_INTERVAL = "donationInterval" const val DONATION_INTERVAL = "donationInterval"
const val PREFERRED_AUDIO_QUALITY = "preferredAudioQuality" 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 lateinit var analyticsManager: AnalyticsManager
@ -35,6 +48,11 @@ class PreferenceManager(
val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320") val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320")
fun setPreferredAudioQuality(quality: AudioQuality) = putString(PREFERRED_AUDIO_QUALITY, quality.kbps) 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 */ /* OFFSET FOR WHEN TO ASK FOR SUPPORT */
val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also { val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
// Min. Donation Asking Interval is `3` // Min. Donation Asking Interval is `3`

View File

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

View File

@ -26,6 +26,7 @@ import com.shabinder.common.core_components.preference_manager.PreferenceManager
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.AudioQuality import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.Consumer import com.shabinder.common.models.Consumer
import com.shabinder.common.models.spotify.SpotifyCredentials
import com.shabinder.common.preference.integration.SpotiFlyerPreferenceImpl import com.shabinder.common.preference.integration.SpotiFlyerPreferenceImpl
interface SpotiFlyerPreference { interface SpotiFlyerPreference {
@ -40,6 +41,8 @@ interface SpotiFlyerPreference {
fun setPreferredQuality(quality: AudioQuality) fun setPreferredQuality(quality: AudioQuality)
fun updateSpotifyCredentials(credentials: SpotifyCredentials)
suspend fun loadImage(url: String): Picture suspend fun loadImage(url: String): Picture
interface Dependencies { interface Dependencies {
@ -61,7 +64,8 @@ interface SpotiFlyerPreference {
data class State( data class State(
val preferredQuality: AudioQuality = AudioQuality.KBPS320, val preferredQuality: AudioQuality = AudioQuality.KBPS320,
val downloadPath: String = "", val downloadPath: String = "",
val isAnalyticsEnabled: Boolean = false val isAnalyticsEnabled: Boolean = false,
val spotifyCredentials: SpotifyCredentials = SpotifyCredentials()
) )
} }

View File

@ -23,6 +23,7 @@ import com.shabinder.common.caching.Cache
import com.shabinder.common.core_components.picture.Picture import com.shabinder.common.core_components.picture.Picture
import com.shabinder.common.core_components.utils.asValue import com.shabinder.common.core_components.utils.asValue
import com.shabinder.common.models.AudioQuality 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
import com.shabinder.common.preference.SpotiFlyerPreference.Dependencies import com.shabinder.common.preference.SpotiFlyerPreference.Dependencies
import com.shabinder.common.preference.SpotiFlyerPreference.State import com.shabinder.common.preference.SpotiFlyerPreference.State
@ -67,6 +68,10 @@ internal class SpotiFlyerPreferenceImpl(
store.accept(Intent.SetPreferredAudioQuality(quality)) store.accept(Intent.SetPreferredAudioQuality(quality))
} }
override fun updateSpotifyCredentials(credentials: SpotifyCredentials) {
store.accept(Intent.UpdateSpotifyCredentials(credentials))
}
override suspend fun loadImage(url: String): Picture { override suspend fun loadImage(url: String): Picture {
return cache.get(url) { return cache.get(url) {
fileManager.loadImage(url, 150, 150) fileManager.loadImage(url, 150, 150)

View File

@ -18,6 +18,7 @@ package com.shabinder.common.preference.store
import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.Store
import com.shabinder.common.models.AudioQuality 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
internal interface SpotiFlyerPreferenceStore : Store<SpotiFlyerPreferenceStore.Intent, SpotiFlyerPreference.State, Nothing> { internal interface SpotiFlyerPreferenceStore : Store<SpotiFlyerPreferenceStore.Intent, SpotiFlyerPreference.State, Nothing> {
@ -26,6 +27,7 @@ internal interface SpotiFlyerPreferenceStore : Store<SpotiFlyerPreferenceStore.I
data class ToggleAnalytics(val enabled: Boolean) : Intent() data class ToggleAnalytics(val enabled: Boolean) : Intent()
data class SetDownloadDirectory(val path: String) : Intent() data class SetDownloadDirectory(val path: String) : Intent()
data class SetPreferredAudioQuality(val quality: AudioQuality) : Intent() data class SetPreferredAudioQuality(val quality: AudioQuality) : Intent()
data class UpdateSpotifyCredentials(val credentials: SpotifyCredentials) : Intent()
object GiveDonation : Intent() object GiveDonation : Intent()
object ShareApp : Intent() object ShareApp : Intent()
} }

View File

@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.models.AudioQuality import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.Actions 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
import com.shabinder.common.preference.SpotiFlyerPreference.State import com.shabinder.common.preference.SpotiFlyerPreference.State
import com.shabinder.common.preference.store.SpotiFlyerPreferenceStore.Intent import com.shabinder.common.preference.store.SpotiFlyerPreferenceStore.Intent
@ -45,12 +46,14 @@ internal class SpotiFlyerPreferenceStoreProvider(
data class AnalyticsToggled(val isEnabled: Boolean) : Result() data class AnalyticsToggled(val isEnabled: Boolean) : Result()
data class DownloadPathSet(val path: String) : Result() data class DownloadPathSet(val path: String) : Result()
data class PreferredAudioQualityChanged(val quality: AudioQuality) : 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>() { private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
override suspend fun executeAction(action: Unit, getState: () -> State) { override suspend fun executeAction(action: Unit, getState: () -> State) {
dispatch(Result.AnalyticsToggled(preferenceManager.isAnalyticsEnabled)) dispatch(Result.AnalyticsToggled(preferenceManager.isAnalyticsEnabled))
dispatch(Result.PreferredAudioQualityChanged(preferenceManager.audioQuality)) dispatch(Result.PreferredAudioQualityChanged(preferenceManager.audioQuality))
dispatch(Result.SpotifyCredentialsUpdated(preferenceManager.spotifyCredentials))
dispatch(Result.DownloadPathSet(fileManager.defaultDir())) dispatch(Result.DownloadPathSet(fileManager.defaultDir()))
} }
@ -71,6 +74,11 @@ internal class SpotiFlyerPreferenceStoreProvider(
dispatch(Result.PreferredAudioQualityChanged(intent.quality)) dispatch(Result.PreferredAudioQualityChanged(intent.quality))
preferenceManager.setPreferredAudioQuality(intent.quality) preferenceManager.setPreferredAudioQuality(intent.quality)
} }
is Intent.UpdateSpotifyCredentials -> {
dispatch(Result.SpotifyCredentialsUpdated(intent.credentials))
preferenceManager.setSpotifyCredentials(intent.credentials)
}
} }
} }
} }
@ -81,6 +89,7 @@ internal class SpotiFlyerPreferenceStoreProvider(
is Result.AnalyticsToggled -> copy(isAnalyticsEnabled = result.isEnabled) is Result.AnalyticsToggled -> copy(isAnalyticsEnabled = result.isEnabled)
is Result.DownloadPathSet -> copy(downloadPath = result.path) is Result.DownloadPathSet -> copy(downloadPath = result.path)
is Result.PreferredAudioQualityChanged -> copy(preferredQuality = result.quality) is Result.PreferredAudioQualityChanged -> copy(preferredQuality = result.quality)
is Result.SpotifyCredentialsUpdated -> copy(spotifyCredentials = result.spotifyCredentials)
} }
} }
} }

View File

@ -16,6 +16,7 @@
package com.shabinder.common.providers.spotify.requests 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.SpotiFlyerException
import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
@ -45,8 +46,7 @@ suspend fun authenticateSpotify(): SuspendableEvent<TokenData, Throwable> = Susp
@SharedImmutable @SharedImmutable
private val spotifyAuthClient by lazy { private val spotifyAuthClient by lazy {
HttpClient { HttpClient {
val clientId = "694d8bf4f6ec420fa66ea7fb4c68f89d" val (clientId, clientSecret) = PreferenceManager.instance.spotifyCredentials
val clientSecret = "02ca2d4021a7452dae2328b47a6e8fe8"
install(Auth) { install(Auth) {
basic { basic {

View File

@ -40,6 +40,14 @@ checkInternetConnection = Please check your network connection.
grantPermissions = Grant permissions grantPermissions = Grant permissions
requiredPermissions = Required permissions: requiredPermissions = Required permissions:
storagePermission = Storage permissions. storagePermission = Storage permissions.
spotifyCreds = Spotify credentials.
clientID = Client ID
clientSecret = Client Secret
defaultString = Default
userSet = UserSet
save = Save
reset = Reset
requestAppRestart = You need to restart the app for changes to take effect
storagePermissionReason = To download your favourite songs to this device. storagePermissionReason = To download your favourite songs to this device.
backgroundRunning = Background running. backgroundRunning = Background running.
backgroundRunningReason = To download all songs in background without any system interruptions. backgroundRunningReason = To download all songs in background without any system interruptions.