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:
bors[bot] 2021-03-23 12:18:47 +00:00 committed by GitHub
commit 4a40192647
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 238 additions and 0 deletions

View file

@ -33,6 +33,7 @@ dependencies {
implementation(compose.ui)
implementation(Dependencies.AndroidX.appCompat)
implementation(Dependencies.AndroidX.browser)
implementation(Dependencies.AndroidX.datastore)
implementation(Dependencies.AndroidX.Compose.activity)
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
implementation(Dependencies.AndroidX.Compose.navigation)

View file

@ -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())
}
}

View file

@ -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()
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -54,6 +54,12 @@ fun LobstersApp() {
}
Scaffold(
topBar = {
LobstersTopAppBar(
currentDestination,
viewModel::toggleSortOrder,
)
},
bottomBar = {
LobstersBottomNav(
currentDestination,
@ -78,6 +84,7 @@ fun LobstersApp() {
posts = savedPosts,
saveAction = viewModel::toggleSave,
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()),
sortReversed = viewModel.getSortOrder(),
)
}
}

View file

@ -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() } },
)
}
}
)
}

View file

@ -10,6 +10,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.util.IconResource
import dev.msfjarvis.lobsters.util.asZonedDateTime
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SavedPosts(
posts: List<SavedPost>,
sortReversed: Flow<Boolean>,
modifier: Modifier = Modifier,
saveAction: (SavedPost) -> Unit,
) {
val listState = rememberLazyListState()
val urlLauncher = LocalUrlLauncher.current
val sortOrder by sortReversed.collectAsState(false)
if (posts.isEmpty()) {
Column(
@ -55,6 +60,8 @@ fun SavedPosts(
stickyHeader {
MonthHeader(month = month)
}
@Suppress("NAME_SHADOWING")
val posts = if (sortOrder) posts.reversed() else posts
items(posts) { item ->
LobstersItem(
post = item,

View file

@ -7,9 +7,11 @@ import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
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.repo.LobstersRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
@ -19,6 +21,7 @@ import kotlinx.coroutines.launch
@HiltViewModel
class LobstersViewModel @Inject constructor(
private val lobstersRepository: LobstersRepository,
private val clawPreferences: ClawPreferences,
) : ViewModel() {
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
val savedPosts = _savedPosts.asStateFlow()
@ -35,6 +38,14 @@ class LobstersViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
fun getSortOrder(): Flow<Boolean> {
return clawPreferences.sortingOrder
}
suspend fun toggleSortOrder() {
clawPreferences.toggleSortingOrder()
}
fun reloadPosts() {
pagingSource?.invalidate()
}

View 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>

View file

@ -10,4 +10,5 @@
<string name="remove_from_saved_posts">Remove from saved posts</string>
<string name="refresh_posts_content_description">Refresh posts</string>
<string name="open_comments">Open comments</string>
<string name="change_sorting_order">Change sort order</string>
</resources>

View file

@ -34,6 +34,7 @@ object Dependencies {
const val appCompat = "androidx.appcompat:appcompat:1.3.0-beta01"
const val browser = "androidx.browser:browser:1.3.0"
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 {