Preference Screen Added

This commit is contained in:
shabinder 2021-07-11 01:44:27 +05:30
parent 34fcfe4d88
commit a44f4cc061
21 changed files with 337 additions and 54 deletions

View File

@ -300,7 +300,7 @@ class MainActivity : ComponentActivity() {
override fun showPopUpMessage(string: String, long: Boolean) = this@MainActivity.showPopUpMessage(string,long) override fun showPopUpMessage(string: String, long: Boolean) = this@MainActivity.showPopUpMessage(string,long)
override fun setDownloadDirectoryAction() = setUpOnPrefClickListener() override fun setDownloadDirectoryAction(callBack: (String) -> Unit) = setUpOnPrefClickListener(callBack)
override fun queryActiveTracks() = this@MainActivity.queryActiveTracks() override fun queryActiveTracks() = this@MainActivity.queryActiveTracks()
@ -400,7 +400,7 @@ class MainActivity : ComponentActivity() {
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun setUpOnPrefClickListener() { private fun setUpOnPrefClickListener(callBack : (String) -> Unit) {
// Initialize Builder // Initialize Builder
val chooser = StorageChooser.Builder() val chooser = StorageChooser.Builder()
.withActivity(this) .withActivity(this)
@ -421,6 +421,7 @@ class MainActivity : ComponentActivity() {
if (f.canWrite()) { if (f.canWrite()) {
// hell yeah :) // hell yeah :)
preferenceManager.setDownloadDirectory(path) preferenceManager.setDownloadDirectory(path)
callBack(dir.defaultDir())
showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}")) showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}"))
}else{ }else{
showPopUpMessage(Strings.noWriteAccess("\n$path ")) showPopUpMessage(Strings.noWriteAccess("\n$path "))

View File

@ -33,6 +33,7 @@ kotlin {
implementation(project(":common:root")) implementation(project(":common:root"))
implementation(project(":common:main")) implementation(project(":common:main"))
implementation(project(":common:list")) implementation(project(":common:list"))
implementation(project(":common:preference"))
implementation(project(":common:database")) implementation(project(":common:database"))
implementation(project(":common:data-models")) implementation(project(":common:data-models"))
implementation(project(":common:dependency-injection")) implementation(project(":common:dependency-injection"))

View File

@ -0,0 +1,201 @@
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.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
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.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.RadioButton
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Insights
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.SnippetFolder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.unit.dp
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.translations.Strings
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
import com.shabinder.common.uikit.configurations.colorAccent
import com.shabinder.common.uikit.configurations.colorOffWhite
@Composable
fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
val model by component.model.subscribeAsState()
val stateVertical = rememberScrollState(0)
Column(Modifier.fillMaxSize().padding(8.dp).verticalScroll(stateVertical)) {
Spacer(Modifier.padding(top = 16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, Color.Gray)
) {
Column(Modifier.padding(12.dp)) {
Text(
text = Strings.preferences(),
style = SpotiFlyerTypography.body1,
color = colorAccent
)
Spacer(modifier = Modifier.padding(top = 12.dp))
SettingsRow(
icon = rememberVectorPainter(Icons.Rounded.MusicNote),
title = "Preferred Audio Quality",
value = model.preferredQuality.kbps + "KBPS"
) { save ->
val audioQualities = AudioQuality.values()
audioQualities.forEach { quality ->
Row(
Modifier
.fillMaxWidth()
.selectable(
selected = (quality == model.preferredQuality),
onClick = {
component.setPreferredQuality(quality)
save()
}
)
.padding(horizontal = 16.dp,vertical = 2.dp)
) {
RadioButton(
selected = (quality == model.preferredQuality),
onClick = {
component.setPreferredQuality(quality)
save()
}
)
Text(
text = quality.kbps + " KBPS",
style = SpotiFlyerTypography.h6,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
Spacer(Modifier.padding(top = 12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(
onClick = { component.selectNewDownloadDirectory() }
)
) {
Icon(Icons.Rounded.SnippetFolder, Strings.setDownloadDirectory(), Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = Strings.setDownloadDirectory(),
style = SpotiFlyerTypography.h6
)
Text(
text = model.downloadPath,
style = SpotiFlyerTypography.subtitle2
)
}
}
Spacer(Modifier.padding(top = 12.dp))
Row(
modifier = Modifier.fillMaxWidth()
.clickable(
onClick = { component.toggleAnalytics(!model.isAnalyticsEnabled) }
),
verticalAlignment = Alignment.CenterVertically
) {
@Suppress("DuplicatedCode")
Icon(Icons.Rounded.Insights, Strings.analytics() + Strings.status(), Modifier.size(32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column(
Modifier.weight(1f)
) {
Text(
text = Strings.analytics(),
style = SpotiFlyerTypography.h6
)
Text(
text = Strings.analyticsDescription(),
style = SpotiFlyerTypography.subtitle2
)
}
Switch(
checked = model.isAnalyticsEnabled,
onCheckedChange = null,
colors = SwitchDefaults.colors(uncheckedThumbColor = colorOffWhite)
)
}
}
}
Spacer(modifier = Modifier.padding(top = 8.dp))
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SettingsRow(
icon: Painter,
title: String,
value:String,
editContent: @Composable ColumnScope.(() -> Unit) -> Unit
) {
var isEditMode by remember { mutableStateOf(false) }
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(
onClick = { isEditMode = !isEditMode }
).padding(vertical = 6.dp)
) {
Icon(icon, title, Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = title,
style = SpotiFlyerTypography.h6
)
Text(
text = value,
style = SpotiFlyerTypography.subtitle2
)
}
}
AnimatedVisibility(isEditMode) {
Column {
editContent {
isEditMode = false
}
}
}
}
}

View File

@ -136,8 +136,8 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.d
AppBar( AppBar(
backgroundColor = appBarColor, backgroundColor = appBarColor,
onBackPressed = callBacks::popBackToHomeScreen, onBackPressed = callBacks::popBackToHomeScreen,
setDownloadDirectory = callBacks::setDownloadDirectory, openPreferenceScreen = callBacks::openPreferenceScreen,
isBackButtonVisible = activeComponent.value.activeChild.instance is Child.List, isBackButtonVisible = activeComponent.value.activeChild.instance !is Child.Main,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.padding(top = topPadding)) Spacer(Modifier.padding(top = topPadding))
@ -148,6 +148,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.d
when (val child = it.instance) { when (val child = it.instance) {
is Child.Main -> SpotiFlyerMainContent(component = child.component) is Child.Main -> SpotiFlyerMainContent(component = child.component)
is Child.List -> SpotiFlyerListContent(component = child.component) is Child.List -> SpotiFlyerListContent(component = child.component)
is Child.Preference -> SpotiFlyerPreferenceContent(component = child.component)
} }
} }
} }
@ -158,7 +159,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.d
fun AppBar( fun AppBar(
backgroundColor: Color, backgroundColor: Color,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
setDownloadDirectory: () -> Unit, openPreferenceScreen: () -> Unit,
isBackButtonVisible: Boolean, isBackButtonVisible: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -189,7 +190,7 @@ fun AppBar(
}, },
actions = { actions = {
IconButton( IconButton(
onClick = { setDownloadDirectory() } onClick = { openPreferenceScreen() }
) { ) {
Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray) Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray)
} }

View File

@ -22,7 +22,7 @@ interface Actions {
fun showPopUpMessage(string: String, long: Boolean = false) fun showPopUpMessage(string: String, long: Boolean = false)
// Change Download Directory // Change Download Directory
fun setDownloadDirectoryAction() fun setDownloadDirectoryAction(callBack: (String) -> Unit)
/* /*
* Query Downloading Tracks * Query Downloading Tracks
@ -47,7 +47,7 @@ interface Actions {
private fun stubActions(): Actions = object : Actions { private fun stubActions(): Actions = object : Actions {
override val platformActions = StubPlatformActions override val platformActions = StubPlatformActions
override fun showPopUpMessage(string: String, long: Boolean) {} override fun showPopUpMessage(string: String, long: Boolean) {}
override fun setDownloadDirectoryAction() {} override fun setDownloadDirectoryAction(callBack: (String) -> Unit) {}
override fun queryActiveTracks() {} override fun queryActiveTracks() {}
override fun giveDonation() {} override fun giveDonation() {}
override fun shareApp() {} override fun shareApp() {}

View File

@ -4,7 +4,6 @@ enum class AudioQuality(val kbps: String) {
KBPS128("128"), KBPS128("128"),
KBPS160("160"), KBPS160("160"),
KBPS192("192"), KBPS192("192"),
KBPS224("224"),
KBPS256("256"), KBPS256("256"),
KBPS320("320"); KBPS320("320");
@ -14,7 +13,6 @@ enum class AudioQuality(val kbps: String) {
"128" -> KBPS128 "128" -> KBPS128
"160" -> KBPS160 "160" -> KBPS160
"192" -> KBPS192 "192" -> KBPS192
"224" -> KBPS224
"256" -> KBPS256 "256" -> KBPS256
"320" -> KBPS320 "320" -> KBPS320
else -> KBPS160 // Use 160 as baseline else -> KBPS160 // Use 160 as baseline

View File

@ -1,35 +1,40 @@
package com.shabinder.common.di.preference package com.shabinder.common.di.preference
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import com.shabinder.common.models.AudioQuality
class PreferenceManager(settings: Settings): Settings by settings { class PreferenceManager(settings: Settings): Settings by settings {
companion object { companion object {
const val DirKey = "downloadDir" const val DIR_KEY = "downloadDir"
const val AnalyticsKey = "analytics" const val ANALYTICS_KEY = "analytics"
const val FirstLaunch = "firstLaunch" const val FIRST_LAUNCH = "firstLaunch"
const val DonationInterval = "donationInterval" const val DONATION_INTERVAL = "donationInterval"
const val PREFERRED_AUDIO_QUALITY = "preferredAudioQuality"
} }
/* ANALYTICS */ /* ANALYTICS */
val isAnalyticsEnabled get() = getBooleanOrNull(AnalyticsKey) ?: false val isAnalyticsEnabled get() = getBooleanOrNull(ANALYTICS_KEY) ?: false
fun toggleAnalytics(enabled: Boolean) = putBoolean(AnalyticsKey, enabled) fun toggleAnalytics(enabled: Boolean) = putBoolean(ANALYTICS_KEY, enabled)
/* DOWNLOAD DIRECTORY */ /* DOWNLOAD DIRECTORY */
val downloadDir get() = getStringOrNull(DirKey) val downloadDir get() = getStringOrNull(DIR_KEY)
fun setDownloadDirectory(newBasePath: String) = putString(DirKey, newBasePath) fun setDownloadDirectory(newBasePath: String) = putString(DIR_KEY, newBasePath)
/* Preferred Audio Quality */
val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320")
fun setPreferredAudioQuality(quality: AudioQuality) = putString(PREFERRED_AUDIO_QUALITY,quality.kbps)
/* OFFSET FOR WHEN TO ASK FOR SUPPORT */ /* OFFSET FOR WHEN TO ASK FOR SUPPORT */
val getDonationOffset: Int get() = (getIntOrNull(DonationInterval) ?: 3).also { val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
// Min. Donation Asking Interval is `3` // Min. Donation Asking Interval is `3`
if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1) if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1)
} }
fun setDonationOffset(offset: Int = 5) = putInt(DonationInterval, offset) fun setDonationOffset(offset: Int = 5) = putInt(DONATION_INTERVAL, offset)
/* TO CHECK IF THIS IS APP's FIRST LAUNCH */ /* TO CHECK IF THIS IS APP's FIRST LAUNCH */
val isFirstLaunch get() = getBooleanOrNull(FirstLaunch) ?: true val isFirstLaunch get() = getBooleanOrNull(FIRST_LAUNCH) ?: true
fun firstLaunchDone() = putBoolean(FirstLaunch, false) fun firstLaunchDone() = putBoolean(FIRST_LAUNCH, false)
} }

View File

@ -18,6 +18,7 @@ package com.shabinder.common.main.integration
import co.touchlab.stately.ensureNeverFrozen import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.lifecycle.doOnResume
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture import com.shabinder.common.di.Picture
@ -39,6 +40,9 @@ internal class SpotiFlyerMainImpl(
init { init {
instanceKeeper.ensureNeverFrozen() instanceKeeper.ensureNeverFrozen()
lifecycle.doOnResume {
store.accept(Intent.ToggleAnalytics(preferenceManager.isAnalyticsEnabled))
}
} }
private val store = private val store =

View File

@ -71,12 +71,12 @@ internal class SpotiFlyerMainStoreProvider(
data class ItemsLoaded(val items: List<DownloadRecord>) : Result() data class ItemsLoaded(val items: List<DownloadRecord>) : Result()
data class CategoryChanged(val category: SpotiFlyerMain.HomeCategory) : Result() data class CategoryChanged(val category: SpotiFlyerMain.HomeCategory) : Result()
data class LinkChanged(val link: String) : Result() data class LinkChanged(val link: String) : Result()
data class ToggleAnalytics(val isEnabled: Boolean) : Result() data class AnalyticsToggled(val isEnabled: Boolean) : 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.ToggleAnalytics(preferenceManager.isAnalyticsEnabled)) dispatch(Result.AnalyticsToggled(preferenceManager.isAnalyticsEnabled))
updates?.collect { updates?.collect {
dispatch(Result.ItemsLoaded(it)) dispatch(Result.ItemsLoaded(it))
} }
@ -90,7 +90,7 @@ internal class SpotiFlyerMainStoreProvider(
is Intent.SetLink -> dispatch(Result.LinkChanged(link = intent.link)) is Intent.SetLink -> dispatch(Result.LinkChanged(link = intent.link))
is Intent.SelectCategory -> dispatch(Result.CategoryChanged(intent.category)) is Intent.SelectCategory -> dispatch(Result.CategoryChanged(intent.category))
is Intent.ToggleAnalytics -> { is Intent.ToggleAnalytics -> {
dispatch(Result.ToggleAnalytics(intent.enabled)) dispatch(Result.AnalyticsToggled(intent.enabled))
preferenceManager.toggleAnalytics(intent.enabled) preferenceManager.toggleAnalytics(intent.enabled)
} }
} }
@ -103,7 +103,7 @@ internal class SpotiFlyerMainStoreProvider(
is Result.ItemsLoaded -> copy(records = result.items) is Result.ItemsLoaded -> copy(records = result.items)
is Result.LinkChanged -> copy(link = result.link) is Result.LinkChanged -> copy(link = result.link)
is Result.CategoryChanged -> copy(selectedCategory = result.category) is Result.CategoryChanged -> copy(selectedCategory = result.category)
is Result.ToggleAnalytics -> copy(isAnalyticsEnabled = result.isEnabled) is Result.AnalyticsToggled -> copy(isAnalyticsEnabled = result.isEnabled)
} }
} }
} }

View File

@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.Picture import com.shabinder.common.di.Picture
import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.di.preference.PreferenceManager
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.preference.integration.SpotiFlyerPreferenceImpl import com.shabinder.common.preference.integration.SpotiFlyerPreferenceImpl
@ -34,7 +35,9 @@ interface SpotiFlyerPreference {
fun toggleAnalytics(enabled: Boolean) fun toggleAnalytics(enabled: Boolean)
fun setDownloadDirectory(newBasePath: String) fun selectNewDownloadDirectory()
fun setPreferredQuality(quality: AudioQuality)
suspend fun loadImage(url: String): Picture suspend fun loadImage(url: String): Picture
@ -43,6 +46,7 @@ interface SpotiFlyerPreference {
val storeFactory: StoreFactory val storeFactory: StoreFactory
val dir: Dir val dir: Dir
val preferenceManager: PreferenceManager val preferenceManager: PreferenceManager
val actions: Actions
val preferenceAnalytics: Analytics val preferenceAnalytics: Analytics
} }
@ -54,6 +58,7 @@ interface SpotiFlyerPreference {
data class State( data class State(
val preferredQuality: AudioQuality = AudioQuality.KBPS320, val preferredQuality: AudioQuality = AudioQuality.KBPS320,
val downloadPath: String = "",
val isAnalyticsEnabled: Boolean = false val isAnalyticsEnabled: Boolean = false
) )
} }

View File

@ -22,6 +22,7 @@ import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue import com.shabinder.common.di.utils.asValue
import com.shabinder.common.models.AudioQuality
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
@ -42,7 +43,9 @@ internal class SpotiFlyerPreferenceImpl(
instanceKeeper.getStore { instanceKeeper.getStore {
SpotiFlyerPreferenceStoreProvider( SpotiFlyerPreferenceStoreProvider(
storeFactory = storeFactory, storeFactory = storeFactory,
preferenceManager = preferenceManager preferenceManager = preferenceManager,
dir = dir,
actions = actions
).provide() ).provide()
} }
@ -59,8 +62,14 @@ internal class SpotiFlyerPreferenceImpl(
store.accept(Intent.ToggleAnalytics(enabled)) store.accept(Intent.ToggleAnalytics(enabled))
} }
override fun setDownloadDirectory(newBasePath: String) { override fun selectNewDownloadDirectory() {
preferenceManager.setDownloadDirectory(newBasePath) actions.setDownloadDirectoryAction {
store.accept(Intent.SetDownloadDirectory(dir.defaultDir()))
}
}
override fun setPreferredQuality(quality: AudioQuality) {
store.accept(Intent.SetPreferredAudioQuality(quality))
} }
override suspend fun loadImage(url: String): Picture { override suspend fun loadImage(url: String): Picture {

View File

@ -17,12 +17,15 @@
package com.shabinder.common.preference.store 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.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> {
sealed class Intent { sealed class Intent {
data class OpenPlatform(val platformID: String, val platformLink: String) : Intent() data class OpenPlatform(val platformID: String, val platformLink: String) : Intent()
data class ToggleAnalytics(val enabled: Boolean) : Intent() data class ToggleAnalytics(val enabled: Boolean) : Intent()
data class SetDownloadDirectory(val path: String) : Intent()
data class SetPreferredAudioQuality(val quality: AudioQuality) : Intent()
object GiveDonation : Intent() object GiveDonation : Intent()
object ShareApp : Intent() object ShareApp : Intent()
} }

View File

@ -21,14 +21,19 @@ import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.di.Dir
import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.AudioQuality
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
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
internal class SpotiFlyerPreferenceStoreProvider( internal class SpotiFlyerPreferenceStoreProvider(
private val storeFactory: StoreFactory, private val storeFactory: StoreFactory,
private val preferenceManager: PreferenceManager private val preferenceManager: PreferenceManager,
private val dir: Dir,
private val actions: Actions
) { ) {
fun provide(): SpotiFlyerPreferenceStore = fun provide(): SpotiFlyerPreferenceStore =
@ -43,12 +48,16 @@ internal class SpotiFlyerPreferenceStoreProvider(
) {} ) {}
private sealed class Result { private sealed class Result {
data class ToggleAnalytics(val isEnabled: Boolean) : Result() data class AnalyticsToggled(val isEnabled: Boolean) : Result()
data class DownloadPathSet(val path: String) : Result()
data class PreferredAudioQualityChanged(val quality: AudioQuality) : 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.ToggleAnalytics(preferenceManager.isAnalyticsEnabled)) dispatch(Result.AnalyticsToggled(preferenceManager.isAnalyticsEnabled))
dispatch(Result.PreferredAudioQualityChanged(preferenceManager.audioQuality))
dispatch(Result.DownloadPathSet(dir.defaultDir()))
} }
override suspend fun executeIntent(intent: Intent, getState: () -> State) { override suspend fun executeIntent(intent: Intent, getState: () -> State) {
@ -57,9 +66,17 @@ internal class SpotiFlyerPreferenceStoreProvider(
is Intent.GiveDonation -> methods.value.giveDonation() is Intent.GiveDonation -> methods.value.giveDonation()
is Intent.ShareApp -> methods.value.shareApp() is Intent.ShareApp -> methods.value.shareApp()
is Intent.ToggleAnalytics -> { is Intent.ToggleAnalytics -> {
dispatch(Result.ToggleAnalytics(intent.enabled)) dispatch(Result.AnalyticsToggled(intent.enabled))
preferenceManager.toggleAnalytics(intent.enabled) preferenceManager.toggleAnalytics(intent.enabled)
} }
is Intent.SetDownloadDirectory -> {
dispatch(Result.DownloadPathSet(intent.path))
preferenceManager.setDownloadDirectory(intent.path)
}
is Intent.SetPreferredAudioQuality -> {
dispatch(Result.PreferredAudioQualityChanged(intent.quality))
preferenceManager.setPreferredAudioQuality(intent.quality)
}
} }
} }
} }
@ -67,7 +84,9 @@ internal class SpotiFlyerPreferenceStoreProvider(
private object ReducerImpl : Reducer<State, Result> { private object ReducerImpl : Reducer<State, Result> {
override fun State.reduce(result: Result): State = override fun State.reduce(result: Result): State =
when (result) { when (result) {
is Result.ToggleAnalytics -> copy(isAnalyticsEnabled = result.isEnabled) is Result.AnalyticsToggled -> copy(isAnalyticsEnabled = result.isEnabled)
is Result.DownloadPathSet -> copy(downloadPath = result.path)
is Result.PreferredAudioQualityChanged -> copy(preferredQuality = result.quality)
} }
} }
} }

View File

@ -30,6 +30,7 @@ fun org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer.generateFramewor
export(project(":common:database")) export(project(":common:database"))
export(project(":common:main")) export(project(":common:main"))
export(project(":common:list")) export(project(":common:list"))
export(project(":common:preference"))
export(Decompose.decompose) export(Decompose.decompose)
export(MVIKotlin.mvikotlinMain) export(MVIKotlin.mvikotlinMain)
export(MVIKotlin.mvikotlinLogging) export(MVIKotlin.mvikotlinLogging)
@ -65,6 +66,7 @@ kotlin {
implementation(project(":common:database")) implementation(project(":common:database"))
implementation(project(":common:list")) implementation(project(":common:list"))
implementation(project(":common:main")) implementation(project(":common:main"))
implementation(project(":common:preference"))
implementation(SqlDelight.coroutineExtensions) implementation(SqlDelight.coroutineExtensions)
} }
} }
@ -79,6 +81,7 @@ kotlin {
api(project(":common:database")) api(project(":common:database"))
api(project(":common:list")) api(project(":common:list"))
api(project(":common:main")) api(project(":common:main"))
api(project(":common:preference"))
api(Decompose.decompose) api(Decompose.decompose)
api(MVIKotlin.mvikotlinMain) api(MVIKotlin.mvikotlinMain)
api(MVIKotlin.mvikotlinLogging) api(MVIKotlin.mvikotlinLogging)

View File

@ -27,6 +27,7 @@ import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.root.integration.SpotiFlyerRootImpl import com.shabinder.common.root.integration.SpotiFlyerRootImpl
@ -45,6 +46,7 @@ interface SpotiFlyerRoot {
sealed class Child { sealed class Child {
data class Main(val component: SpotiFlyerMain) : Child() data class Main(val component: SpotiFlyerMain) : Child()
data class List(val component: SpotiFlyerList) : Child() data class List(val component: SpotiFlyerList) : Child()
data class Preference(val component: SpotiFlyerPreference) : Child()
} }
interface Dependencies { interface Dependencies {

View File

@ -20,5 +20,5 @@ interface SpotiFlyerRootCallBacks {
fun searchLink(link: String) fun searchLink(link: String)
fun showToast(text: String) fun showToast(text: String)
fun popBackToHomeScreen() fun popBackToHomeScreen()
fun setDownloadDirectory() fun openPreferenceScreen()
} }

View File

@ -33,6 +33,7 @@ import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.Consumer import com.shabinder.common.models.Consumer
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import com.shabinder.common.preference.SpotiFlyerPreference
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Analytics
import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Child
@ -47,6 +48,7 @@ internal class SpotiFlyerRootImpl(
componentContext: ComponentContext, componentContext: ComponentContext,
private val main: (ComponentContext, output: Consumer<SpotiFlyerMain.Output>) -> SpotiFlyerMain, private val main: (ComponentContext, output: Consumer<SpotiFlyerMain.Output>) -> SpotiFlyerMain,
private val list: (ComponentContext, link: String, output: Consumer<SpotiFlyerList.Output>) -> SpotiFlyerList, private val list: (ComponentContext, link: String, output: Consumer<SpotiFlyerList.Output>) -> SpotiFlyerList,
private val preference: (ComponentContext, output: Consumer<SpotiFlyerPreference.Output>) -> SpotiFlyerPreference,
private val actions: Actions, private val actions: Actions,
private val analytics: Analytics private val analytics: Analytics
) : SpotiFlyerRoot, ComponentContext by componentContext { ) : SpotiFlyerRoot, ComponentContext by componentContext {
@ -57,19 +59,13 @@ internal class SpotiFlyerRootImpl(
) : this( ) : this(
componentContext = componentContext, componentContext = componentContext,
main = { childContext, output -> main = { childContext, output ->
spotiFlyerMain( spotiFlyerMain(childContext, output, dependencies)
childContext,
output,
dependencies
)
}, },
list = { childContext, link, output -> list = { childContext, link, output ->
spotiFlyerList( spotiFlyerList(childContext, link, output, dependencies)
childContext, },
link, preference = { childContext, output ->
output, spotiFlyerPreference(childContext, output, dependencies)
dependencies
)
}, },
actions = dependencies.actions.freeze(), actions = dependencies.actions.freeze(),
analytics = dependencies.analytics analytics = dependencies.analytics
@ -95,20 +91,25 @@ internal class SpotiFlyerRootImpl(
override val callBacks = object : SpotiFlyerRootCallBacks { override val callBacks = object : SpotiFlyerRootCallBacks {
override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link)) override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link))
override fun popBackToHomeScreen() { override fun popBackToHomeScreen() {
if (router.state.value.activeChild.instance is Child.List && router.state.value.backStack.isNotEmpty()) { if (router.state.value.activeChild.instance !is Child.Main && router.state.value.backStack.isNotEmpty()) {
router.popWhile { router.popWhile {
it !is Configuration.Main it !is Configuration.Main
} }
} }
} }
override fun openPreferenceScreen() {
router.push(Configuration.Preference)
}
override fun showToast(text: String) { toastState.value = text } override fun showToast(text: String) { toastState.value = text }
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
} }
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child = private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child =
when (configuration) { when (configuration) {
is Configuration.Main -> Child.Main(main(componentContext, Consumer(::onMainOutput))) is Configuration.Main -> Child.Main(main(componentContext, Consumer(::onMainOutput)))
is Configuration.List -> Child.List(list(componentContext, configuration.link, Consumer(::onListOutput))) is Configuration.List -> Child.List(list(componentContext, configuration.link, Consumer(::onListOutput)))
is Configuration.Preference -> Child.Preference(preference(componentContext, Consumer(::onPreferenceOutput)),)
} }
private fun onMainOutput(output: SpotiFlyerMain.Output) = private fun onMainOutput(output: SpotiFlyerMain.Output) =
@ -128,6 +129,15 @@ internal class SpotiFlyerRootImpl(
analytics.homeScreenVisit() analytics.homeScreenVisit()
} }
} }
private fun onPreferenceOutput(output: SpotiFlyerPreference.Output): Unit =
when (output) {
is SpotiFlyerPreference.Output.Finished -> {
if (router.state.value.activeChild.instance is Child.Preference && router.state.value.backStack.isNotEmpty()) {
router.pop()
}
Unit
}
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) { private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) {
@ -142,6 +152,9 @@ internal class SpotiFlyerRootImpl(
@Parcelize @Parcelize
object Main : Configuration() object Main : Configuration()
@Parcelize
object Preference : Configuration()
@Parcelize @Parcelize
data class List(val link: String) : Configuration() data class List(val link: String) : Configuration()
} }
@ -165,3 +178,12 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out
override val listAnalytics = object : SpotiFlyerList.Analytics, Analytics by analytics {} override val listAnalytics = object : SpotiFlyerList.Analytics, Analytics by analytics {}
} }
) )
private fun spotiFlyerPreference(componentContext: ComponentContext, output: Consumer<SpotiFlyerPreference.Output>, dependencies: Dependencies): SpotiFlyerPreference =
SpotiFlyerPreference(
componentContext = componentContext,
dependencies = object : SpotiFlyerPreference.Dependencies, Dependencies by dependencies {
override val prefOutput: Consumer<SpotiFlyerPreference.Output> = output
override val preferenceAnalytics = object : SpotiFlyerPreference.Analytics, Analytics by analytics {}
}
)

View File

@ -37,6 +37,7 @@ import com.shabinder.common.models.Actions
import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.translations.Strings
import com.shabinder.common.uikit.configurations.SpotiFlyerColors import com.shabinder.common.uikit.configurations.SpotiFlyerColors
import com.shabinder.common.uikit.configurations.SpotiFlyerShapes import com.shabinder.common.uikit.configurations.SpotiFlyerShapes
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
@ -106,7 +107,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
} }
} }
override fun setDownloadDirectoryAction() { override fun setDownloadDirectoryAction(callBack: (String) -> Unit) {
val fileChooser = JFileChooser().apply { val fileChooser = JFileChooser().apply {
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
} }
@ -115,9 +116,10 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
val directory = fileChooser.selectedFile val directory = fileChooser.selectedFile
if(directory.canWrite()){ if(directory.canWrite()){
preferenceManager.setDownloadDirectory(directory.absolutePath) preferenceManager.setDownloadDirectory(directory.absolutePath)
showPopUpMessage("Set New Download Directory:\n${directory.absolutePath}") callBack(dir.defaultDir())
showPopUpMessage("${Strings.setDownloadDirectory()} \n${dir.defaultDir()}")
} else { } else {
showPopUpMessage("Cant Write to Selected Directory!") showPopUpMessage(Strings.noWriteAccess("\n${directory.absolutePath} "))
} }
} }
else -> { else -> {

View File

@ -0,0 +1,7 @@
- YT Quality 128 -> 192 KBPS
- Bug Fixes
- Better Error Handling, and Bubbling upto the caller
- Error Dialog with error info added
- YT extraction fixed
- Retry on Error Logo Added
- Translations added for languages: Locales - de, en, es, fr, id, pt, ru, uk

View File

@ -1 +1 @@
Download All your songs from Spotify, Gaana, Youtube Music. Download All your songs from Spotify, Gaana, Jio Saavn, Youtube Music.

View File

@ -73,7 +73,7 @@ class App(props: AppProps): RComponent<AppProps, RState>(props) {
override fun copyToClipboard(text: String) {} override fun copyToClipboard(text: String) {}
override fun setDownloadDirectoryAction() {} override fun setDownloadDirectoryAction(callBack: (String) -> Unit) {}
override fun queryActiveTracks() {} override fun queryActiveTracks() {}