mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-18 03:17:03 +05:30
Merge #170
170: Add ability to change saved posts sorting order r=msfjarvis a=msfjarvis Fixes #167 bors r+ Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
commit
4a40192647
16 changed files with 238 additions and 0 deletions
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
implementation(Dependencies.AndroidX.appCompat)
|
implementation(Dependencies.AndroidX.appCompat)
|
||||||
implementation(Dependencies.AndroidX.browser)
|
implementation(Dependencies.AndroidX.browser)
|
||||||
|
implementation(Dependencies.AndroidX.datastore)
|
||||||
implementation(Dependencies.AndroidX.Compose.activity)
|
implementation(Dependencies.AndroidX.Compose.activity)
|
||||||
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
|
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
|
||||||
implementation(Dependencies.AndroidX.Compose.navigation)
|
implementation(Dependencies.AndroidX.Compose.navigation)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 6 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
|
@ -0,0 +1,74 @@
|
||||||
|
package dev.msfjarvis.lobsters.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
|
import androidx.compose.ui.test.captureToImage
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import com.karumi.shot.ScreenshotTest
|
||||||
|
import dev.msfjarvis.lobsters.ui.DarkTestTheme
|
||||||
|
import dev.msfjarvis.lobsters.ui.LightTestTheme
|
||||||
|
import dev.msfjarvis.lobsters.ui.navigation.Destination
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LobstersTopBarTest : ScreenshotTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showsRefreshIconWhenOnHottestPostsScreen_DarkTheme() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
DarkTestTheme {
|
||||||
|
LobstersTopAppBar(
|
||||||
|
currentDestination = Destination.Hottest,
|
||||||
|
toggleSortingOrder = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showsRefreshIconWhenOnHottestPostsScreen_LightTheme() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LightTestTheme {
|
||||||
|
LobstersTopAppBar(
|
||||||
|
currentDestination = Destination.Hottest,
|
||||||
|
toggleSortingOrder = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun doesNotShowRefreshIconWhenOnSavedPostsScreen_DarkTheme() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
DarkTestTheme {
|
||||||
|
LobstersTopAppBar(
|
||||||
|
currentDestination = Destination.Saved,
|
||||||
|
toggleSortingOrder = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun doesNotShowRefreshIconWhenOnSavedPostsScreen_LightTheme() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LightTestTheme {
|
||||||
|
LobstersTopAppBar(
|
||||||
|
currentDestination = Destination.Saved,
|
||||||
|
toggleSortingOrder = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.msfjarvis.lobsters.data.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class ClawPreferences @Inject constructor(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
) {
|
||||||
|
private val sortKey = booleanPreferencesKey("post_sorting_order")
|
||||||
|
|
||||||
|
val sortingOrder: Flow<Boolean>
|
||||||
|
get() = dataStore.data.map { preferences -> preferences[sortKey] ?: false }
|
||||||
|
|
||||||
|
suspend fun toggleSortingOrder() {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
preferences[sortKey] = (preferences[sortKey] ?: false).not()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package dev.msfjarvis.lobsters.injection
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class PreferenceStoreFileNameQualifier
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DataStoreModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideDataStore(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
@PreferenceStoreFileNameQualifier fileName: String,
|
||||||
|
): DataStore<Preferences> {
|
||||||
|
return PreferenceDataStoreFactory.create { context.preferencesDataStoreFile(fileName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@PreferenceStoreFileNameQualifier
|
||||||
|
fun provideDataStoreFilename(): String {
|
||||||
|
return "Claw_preferences"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dev.msfjarvis.lobsters.injection
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dev.msfjarvis.lobsters.data.preferences.ClawPreferences
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object PreferenceModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideClawPreferences(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
): ClawPreferences {
|
||||||
|
return ClawPreferences(dataStore)
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,12 @@ fun LobstersApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
LobstersTopAppBar(
|
||||||
|
currentDestination,
|
||||||
|
viewModel::toggleSortOrder,
|
||||||
|
)
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
LobstersBottomNav(
|
LobstersBottomNav(
|
||||||
currentDestination,
|
currentDestination,
|
||||||
|
@ -78,6 +84,7 @@ fun LobstersApp() {
|
||||||
posts = savedPosts,
|
posts = savedPosts,
|
||||||
saveAction = viewModel::toggleSave,
|
saveAction = viewModel::toggleSave,
|
||||||
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()),
|
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()),
|
||||||
|
sortReversed = viewModel.getSortOrder(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package dev.msfjarvis.lobsters.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.msfjarvis.lobsters.R
|
||||||
|
import dev.msfjarvis.lobsters.ui.navigation.Destination
|
||||||
|
import dev.msfjarvis.lobsters.util.IconResource
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LobstersTopAppBar(
|
||||||
|
currentDestination: Destination,
|
||||||
|
toggleSortingOrder: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.app_name),
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (currentDestination == Destination.Saved) {
|
||||||
|
IconResource(
|
||||||
|
resourceId = R.drawable.ic_sort_24px,
|
||||||
|
contentDescription = stringResource(id = R.string.change_sorting_order),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
|
.clickable { scope.launch { toggleSortingOrder() } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -20,16 +22,19 @@ import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||||
import dev.msfjarvis.lobsters.util.IconResource
|
import dev.msfjarvis.lobsters.util.IconResource
|
||||||
import dev.msfjarvis.lobsters.util.asZonedDateTime
|
import dev.msfjarvis.lobsters.util.asZonedDateTime
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SavedPosts(
|
fun SavedPosts(
|
||||||
posts: List<SavedPost>,
|
posts: List<SavedPost>,
|
||||||
|
sortReversed: Flow<Boolean>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
saveAction: (SavedPost) -> Unit,
|
saveAction: (SavedPost) -> Unit,
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val urlLauncher = LocalUrlLauncher.current
|
val urlLauncher = LocalUrlLauncher.current
|
||||||
|
val sortOrder by sortReversed.collectAsState(false)
|
||||||
|
|
||||||
if (posts.isEmpty()) {
|
if (posts.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -55,6 +60,8 @@ fun SavedPosts(
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
MonthHeader(month = month)
|
MonthHeader(month = month)
|
||||||
}
|
}
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val posts = if (sortOrder) posts.reversed() else posts
|
||||||
items(posts) { item ->
|
items(posts) { item ->
|
||||||
LobstersItem(
|
LobstersItem(
|
||||||
post = item,
|
post = item,
|
||||||
|
|
|
@ -7,9 +7,11 @@ import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||||
|
import dev.msfjarvis.lobsters.data.preferences.ClawPreferences
|
||||||
import dev.msfjarvis.lobsters.data.remote.LobstersPagingSource
|
import dev.msfjarvis.lobsters.data.remote.LobstersPagingSource
|
||||||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
@ -19,6 +21,7 @@ import kotlinx.coroutines.launch
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LobstersViewModel @Inject constructor(
|
class LobstersViewModel @Inject constructor(
|
||||||
private val lobstersRepository: LobstersRepository,
|
private val lobstersRepository: LobstersRepository,
|
||||||
|
private val clawPreferences: ClawPreferences,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
|
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
|
||||||
val savedPosts = _savedPosts.asStateFlow()
|
val savedPosts = _savedPosts.asStateFlow()
|
||||||
|
@ -35,6 +38,14 @@ class LobstersViewModel @Inject constructor(
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSortOrder(): Flow<Boolean> {
|
||||||
|
return clawPreferences.sortingOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toggleSortOrder() {
|
||||||
|
clawPreferences.toggleSortingOrder()
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadPosts() {
|
fun reloadPosts() {
|
||||||
pagingSource?.invalidate()
|
pagingSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
9
app/src/main/res/drawable/ic_sort_24px.xml
Normal file
9
app/src/main/res/drawable/ic_sort_24px.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M4,18h4c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L4,16c-0.55,0 -1,0.45 -1,1s0.45,1 1,1zM3,7c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L4,6c-0.55,0 -1,0.45 -1,1zM4,13h10c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L4,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1z"/>
|
||||||
|
</vector>
|
|
@ -10,4 +10,5 @@
|
||||||
<string name="remove_from_saved_posts">Remove from saved posts</string>
|
<string name="remove_from_saved_posts">Remove from saved posts</string>
|
||||||
<string name="refresh_posts_content_description">Refresh posts</string>
|
<string name="refresh_posts_content_description">Refresh posts</string>
|
||||||
<string name="open_comments">Open comments</string>
|
<string name="open_comments">Open comments</string>
|
||||||
|
<string name="change_sorting_order">Change sort order</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -34,6 +34,7 @@ object Dependencies {
|
||||||
const val appCompat = "androidx.appcompat:appcompat:1.3.0-beta01"
|
const val appCompat = "androidx.appcompat:appcompat:1.3.0-beta01"
|
||||||
const val browser = "androidx.browser:browser:1.3.0"
|
const val browser = "androidx.browser:browser:1.3.0"
|
||||||
const val coreLibraryDesugaring = "com.android.tools:desugar_jdk_libs:1.0.10"
|
const val coreLibraryDesugaring = "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||||
|
const val datastore = "androidx.datastore:datastore-preferences:1.0.0-alpha08"
|
||||||
|
|
||||||
object Compose {
|
object Compose {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue