mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 19:57:04 +05:30
common: add TagList
This commit is contained in:
parent
a0f044786f
commit
a746734ad8
10 changed files with 248 additions and 0 deletions
|
@ -36,6 +36,8 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Parcelize @Serializable data object AboutLibraries : NavKey, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object TagFiltering : NavKey, Parcelable
|
||||
|
||||
enum class AppDestinations(
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
|
|
|
@ -64,9 +64,11 @@ import dev.msfjarvis.claw.android.ui.navigation.Newest
|
|||
import dev.msfjarvis.claw.android.ui.navigation.Saved
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Search
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Settings
|
||||
import dev.msfjarvis.claw.android.ui.navigation.TagFiltering
|
||||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.tags.TagList
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
@ -266,6 +268,7 @@ fun LobstersPostsScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
entry<TagFiltering> { TagList(contentPadding = contentPadding) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ dependencies {
|
|||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.runtime.saveable)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil3.compose)
|
||||
|
|
|
@ -43,6 +43,7 @@ constructor(
|
|||
@ForScope(ApplicationScope::class) context: Context,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
var postDetails by mutableStateOf<NetworkState>(NetworkState.Loading)
|
||||
private set
|
||||
|
||||
suspend fun loadPostDetails(postId: String) {
|
||||
if (postDetails is NetworkState.Error) {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.tags
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class TagFilterRepository @Inject constructor(private val preferences: DataStore<Preferences>) {
|
||||
private val tagsKey = stringSetPreferencesKey("tags")
|
||||
|
||||
fun getSavedTags(): Flow<Set<String>> {
|
||||
return preferences.data.map { prefs -> prefs[tagsKey] ?: emptySet() }
|
||||
}
|
||||
|
||||
suspend fun saveTags(tags: Set<String>) {
|
||||
preferences.edit { prefs -> prefs[tagsKey] = tags }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.tags
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.toError
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.model.Tag
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ContributesViewModel
|
||||
class TagFilterViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val tagFilterRepository: TagFilterRepository,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
|
||||
var filteredTags by mutableStateOf<ImmutableList<String>>(persistentListOf())
|
||||
private set
|
||||
|
||||
var allTags by mutableStateOf<NetworkState>(NetworkState.Loading)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
tagFilterRepository.getSavedTags().collectLatest { filteredTags = it.toImmutableList() }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
allTags =
|
||||
runSuspendCatching<ImmutableList<Tag>> {
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getTags()) {
|
||||
is Success -> result.value.toImmutableList()
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> throw result.toError()
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
success = { details -> NetworkState.Success(details) },
|
||||
failure = { NetworkState.Error(error = it, description = "Failed to load comments") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTags(tags: Set<String>) {
|
||||
viewModelScope.launch { tagFilterRepository.saveTags(tags) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.tags
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.model.Tag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun TagList(
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TagFilterViewModel = injectedViewModel(key = "tag_filter"),
|
||||
) {
|
||||
val allTagsState = viewModel.allTags
|
||||
val filteredTags = viewModel.filteredTags
|
||||
|
||||
LazyColumn(modifier = modifier.fillMaxWidth().padding(contentPadding)) {
|
||||
when (allTagsState) {
|
||||
is NetworkState.Loading -> {
|
||||
item { Text("Loading tags...") }
|
||||
}
|
||||
is NetworkState.Error -> {
|
||||
item { Text("Failed to load tags") }
|
||||
}
|
||||
is NetworkState.Success<*> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val allTags = (allTagsState as NetworkState.Success<ImmutableList<Tag>>).data
|
||||
items(allTags) { tag ->
|
||||
val isSelected = filteredTags.contains(tag.tag)
|
||||
ListItem(
|
||||
headlineContent = { Text(tag.tag) },
|
||||
supportingContent = { Text(tag.description) },
|
||||
trailingContent = {
|
||||
Button(
|
||||
onClick = {
|
||||
val updatedTags =
|
||||
if (isSelected) {
|
||||
filteredTags - tag.tag
|
||||
} else {
|
||||
filteredTags + tag.tag
|
||||
}
|
||||
viewModel.saveTags(updatedTags.toSet())
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Remove else Icons.Default.Add,
|
||||
contentDescription = if (isSelected) "Remove" else "Add",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ dependencies {
|
|||
api(libs.napier)
|
||||
api(libs.okhttp.core)
|
||||
api(libs.retrofit)
|
||||
api(libs.androidx.datastore)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.core.persistence
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.PreferencesFileSerializer
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoSet
|
||||
import io.github.aakira.napier.Napier
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object PreferencesStoreModule {
|
||||
@Provides
|
||||
fun providePreferencesDataStore(
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
migrations: Set<@JvmSuppressWildcards DataMigration<@JvmSuppressWildcards Preferences>>,
|
||||
): DataStore<@JvmSuppressWildcards Preferences> {
|
||||
return DataStoreFactory.create(
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler {
|
||||
Napier.d(throwable = it) {
|
||||
"Preferences data store corruption detected, returning empty preferences."
|
||||
}
|
||||
emptyPreferences()
|
||||
},
|
||||
migrations = migrations.toList(),
|
||||
produceFile = { context.preferencesDataStoreFile("claw_preferences") },
|
||||
serializer = PreferencesFileSerializer,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun firstMigration(): DataMigration<Preferences> =
|
||||
object : DataMigration<Preferences> {
|
||||
override suspend fun shouldMigrate(currentData: Preferences): Boolean = false
|
||||
|
||||
override suspend fun migrate(currentData: Preferences): Preferences = currentData
|
||||
|
||||
override suspend fun cleanUp() {}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ benchmark = "1.4.0-beta02"
|
|||
coil3 = "3.2.0"
|
||||
coroutines = "1.10.2"
|
||||
dagger = "2.56.2"
|
||||
datastore = "1.2.0-alpha02"
|
||||
eithernet = "2.0.0"
|
||||
glance = "1.0.0"
|
||||
haze = "1.6.3"
|
||||
|
@ -42,6 +43,7 @@ androidx-compose-material-icons-extended = { module = "androidx.compose.material
|
|||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||
androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" }
|
||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
|
@ -50,6 +52,7 @@ androidx-compose-ui-unit = { module = "androidx.compose.ui:ui-unit" }
|
|||
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||
androidx-core = "androidx.core:core:1.16.0"
|
||||
androidx-core-splashscreen = "androidx.core:core-splashscreen:1.2.0-beta02"
|
||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-lint-gradle = "androidx.lint:lint-gradle:1.0.0-alpha05"
|
||||
androidx-material3-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "navigation3-material" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue