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 AboutLibraries : NavKey, Parcelable
|
||||||
|
|
||||||
|
@Parcelize @Serializable data object TagFiltering : NavKey, Parcelable
|
||||||
|
|
||||||
enum class AppDestinations(
|
enum class AppDestinations(
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
val label: String,
|
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.Saved
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Search
|
import dev.msfjarvis.claw.android.ui.navigation.Search
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Settings
|
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.ui.navigation.User
|
||||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
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.urllauncher.UrlLauncher
|
||||||
import dev.msfjarvis.claw.common.user.UserProfile
|
import dev.msfjarvis.claw.common.user.UserProfile
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
@ -266,6 +268,7 @@ fun LobstersPostsScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
entry<TagFiltering> { TagList(contentPadding = contentPadding) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ dependencies {
|
||||||
implementation(libs.androidx.compose.material.icons.extended)
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.compose.runtime)
|
implementation(libs.androidx.compose.runtime)
|
||||||
|
implementation(libs.androidx.compose.runtime.saveable)
|
||||||
implementation(libs.androidx.compose.ui.text)
|
implementation(libs.androidx.compose.ui.text)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.coil3.compose)
|
implementation(libs.coil3.compose)
|
||||||
|
|
|
@ -43,6 +43,7 @@ constructor(
|
||||||
@ForScope(ApplicationScope::class) context: Context,
|
@ForScope(ApplicationScope::class) context: Context,
|
||||||
) : AndroidViewModel(context as Application) {
|
) : AndroidViewModel(context as Application) {
|
||||||
var postDetails by mutableStateOf<NetworkState>(NetworkState.Loading)
|
var postDetails by mutableStateOf<NetworkState>(NetworkState.Loading)
|
||||||
|
private set
|
||||||
|
|
||||||
suspend fun loadPostDetails(postId: String) {
|
suspend fun loadPostDetails(postId: String) {
|
||||||
if (postDetails is NetworkState.Error) {
|
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.napier)
|
||||||
api(libs.okhttp.core)
|
api(libs.okhttp.core)
|
||||||
api(libs.retrofit)
|
api(libs.retrofit)
|
||||||
|
api(libs.androidx.datastore)
|
||||||
|
|
||||||
implementation(platform(libs.okhttp.bom))
|
implementation(platform(libs.okhttp.bom))
|
||||||
implementation(libs.kotlinx.serialization.core)
|
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"
|
coil3 = "3.2.0"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
dagger = "2.56.2"
|
dagger = "2.56.2"
|
||||||
|
datastore = "1.2.0-alpha02"
|
||||||
eithernet = "2.0.0"
|
eithernet = "2.0.0"
|
||||||
glance = "1.0.0"
|
glance = "1.0.0"
|
||||||
haze = "1.6.3"
|
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 = { module = "androidx.compose.material3:material3" }
|
||||||
androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" }
|
androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||||
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
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 = { module = "androidx.compose.ui:ui" }
|
||||||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
||||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
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-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||||
androidx-core = "androidx.core:core:1.16.0"
|
androidx-core = "androidx.core:core:1.16.0"
|
||||||
androidx-core-splashscreen = "androidx.core:core-splashscreen:1.2.0-beta02"
|
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-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||||
androidx-lint-gradle = "androidx.lint:lint-gradle:1.0.0-alpha05"
|
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" }
|
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