From 3e865ee62249e072a37c25c281071de54fd41870 Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 12 Oct 2022 03:25:51 +0530 Subject: [PATCH] - User Configurable Spotify Creds Support. - Crash & Visibility Fix for SettingsIcon. - Changed default Creds, thanks to spotDL. --- .../uikit/screens/SpotiFlyerPreferenceUi.kt | 115 +++++++++++++++++- .../common/uikit/screens/SpotiFlyerRootUi.kt | 12 +- .../preference_manager/PreferenceManager.kt | 18 +++ .../models/spotify/SpotifyCredentials.kt | 9 ++ .../common/preference/SpotiFlyerPreference.kt | 6 +- .../integration/SpotiFlyerPreferenceImpl.kt | 5 + .../store/SpotiFlyerPreferenceStore.kt | 2 + .../SpotiFlyerPreferenceStoreProvider.kt | 9 ++ .../spotify/requests/SpotifyAuth.kt | 4 +- translations/Strings_en.properties | 8 ++ 10 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/SpotifyCredentials.kt diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerPreferenceUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerPreferenceUi.kt index 650d7864..22fce210 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerPreferenceUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerPreferenceUi.kt @@ -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 @@ -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 @@ -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) { @@ -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( @@ -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( @@ -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) @@ -166,6 +275,7 @@ fun SettingsRow( icon: Painter, title: String, value: String, + contentEnd: @Composable RowScope.() -> Unit = {}, editContent: @Composable ColumnScope.(() -> Unit) -> Unit ) { @@ -189,6 +299,7 @@ fun SettingsRow( style = SpotiFlyerTypography.subtitle2 ) } + contentEnd() } AnimatedVisibility(isEditMode) { Column { diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerRootUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerRootUi.kt index 32811760..849f9978 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerRootUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/screens/SpotiFlyerRootUi.kt @@ -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)) @@ -164,6 +165,7 @@ fun AppBar( onBackPressed: () -> Unit, openPreferenceScreen: () -> Unit, isBackButtonVisible: Boolean, + isSettingsIconVisible: Boolean, modifier: Modifier = Modifier ) { TopAppBar( @@ -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, diff --git a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/preference_manager/PreferenceManager.kt b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/preference_manager/PreferenceManager.kt index 9682d59a..6bf135bf 100644 --- a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/preference_manager/PreferenceManager.kt +++ b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/preference_manager/PreferenceManager.kt @@ -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, @@ -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 @@ -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` diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/SpotifyCredentials.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/SpotifyCredentials.kt new file mode 100644 index 00000000..b0c1f9f7 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/SpotifyCredentials.kt @@ -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", +) \ No newline at end of file diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt index 58957b36..4ae5b0a4 100644 --- a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt @@ -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 { @@ -40,6 +41,8 @@ interface SpotiFlyerPreference { fun setPreferredQuality(quality: AudioQuality) + fun updateSpotifyCredentials(credentials: SpotifyCredentials) + suspend fun loadImage(url: String): Picture interface Dependencies { @@ -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() ) } diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt index 19e59ee7..895e876c 100644 --- a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt @@ -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 @@ -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) diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt index b12056dc..dad0b189 100644 --- a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt @@ -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 { @@ -26,6 +27,7 @@ internal interface SpotiFlyerPreferenceStore : Store() { 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())) } @@ -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) + } } } } @@ -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) } } } diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/requests/SpotifyAuth.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/requests/SpotifyAuth.kt index 9ba55581..80c13972 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/requests/SpotifyAuth.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/spotify/requests/SpotifyAuth.kt @@ -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 @@ -45,8 +46,7 @@ suspend fun authenticateSpotify(): SuspendableEvent = Susp @SharedImmutable private val spotifyAuthClient by lazy { HttpClient { - val clientId = "694d8bf4f6ec420fa66ea7fb4c68f89d" - val clientSecret = "02ca2d4021a7452dae2328b47a6e8fe8" + val (clientId, clientSecret) = PreferenceManager.instance.spotifyCredentials install(Auth) { basic { diff --git a/translations/Strings_en.properties b/translations/Strings_en.properties index 5902e0f8..821d2757 100644 --- a/translations/Strings_en.properties +++ b/translations/Strings_en.properties @@ -40,6 +40,14 @@ checkInternetConnection = Please check your network connection. grantPermissions = Grant permissions requiredPermissions = Required 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. backgroundRunning = Background running. backgroundRunningReason = To download all songs in background without any system interruptions.