Add pull to refresh on entry list view

This commit is contained in:
William Brawner 2022-09-13 21:22:30 -06:00
parent 2d02416101
commit ed0a7d813f
11 changed files with 83 additions and 49 deletions

View file

@ -24,6 +24,8 @@
<entry key="../../../../layout/compose-model-1663094305778.xml" value="0.1" /> <entry key="../../../../layout/compose-model-1663094305778.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1663094326622.xml" value="0.1" /> <entry key="../../../../layout/compose-model-1663094326622.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1663094326625.xml" value="1.0" /> <entry key="../../../../layout/compose-model-1663094326625.xml" value="1.0" />
<entry key="../../../../layout/compose-model-1663101323996.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1663188478804.xml" value="0.47846283783783783" />
</map> </map>
</option> </option>
</component> </component>

View file

@ -57,6 +57,7 @@ dependencies {
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
implementation(libs.accompanist.swiperefresh)
implementation(libs.bundles.compose) implementation(libs.bundles.compose)
implementation(libs.lifecycle) implementation(libs.lifecycle)
implementation(libs.hilt.android.core) implementation(libs.hilt.android.core)

View file

@ -6,15 +6,10 @@ import com.wbrawner.nanoflux.network.repository.CategoryRepository
import com.wbrawner.nanoflux.network.repository.EntryRepository import com.wbrawner.nanoflux.network.repository.EntryRepository
import com.wbrawner.nanoflux.network.repository.FeedRepository import com.wbrawner.nanoflux.network.repository.FeedRepository
import com.wbrawner.nanoflux.network.repository.IconRepository import com.wbrawner.nanoflux.network.repository.IconRepository
import com.wbrawner.nanoflux.storage.model.Entry
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
import com.wbrawner.nanoflux.syncAll import com.wbrawner.nanoflux.syncAll
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -28,11 +23,9 @@ class MainViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
init { init {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) { if (entryRepository.getCount() == 0L) {
if (entryRepository.getCount() == 0L) { syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
}
} }
} }
} }

View file

@ -37,4 +37,12 @@ class UnreadViewModel @Inject constructor(
// TODO: Get Base URL // TODO: Get Base URL
fun getShareUrl(entry: Entry) = "baseUrl/${entry.shareCode}" fun getShareUrl(entry: Entry) = "baseUrl/${entry.shareCode}"
fun refresh() = viewModelScope.launch(Dispatchers.IO) {
_loading.emit(true)
feedRepository.getAll(fetch = true)
categoryRepository.getAll(fetch = true)
entryRepository.getAll(fetch = true)
_loading.emit(false)
}
} }

View file

@ -20,6 +20,8 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.wbrawner.nanoflux.NanofluxApp import com.wbrawner.nanoflux.NanofluxApp
import com.wbrawner.nanoflux.storage.model.* import com.wbrawner.nanoflux.storage.model.*
import java.util.* import java.util.*
@ -32,20 +34,26 @@ fun EntryList(
onFeedClicked: (feed: Feed) -> Unit, onFeedClicked: (feed: Feed) -> Unit,
onToggleReadClicked: (entry: Entry) -> Unit, onToggleReadClicked: (entry: Entry) -> Unit,
onStarClicked: (entry: Entry) -> Unit, onStarClicked: (entry: Entry) -> Unit,
onExternalLinkClicked: (entry: Entry) -> Unit onExternalLinkClicked: @Composable (entry: Entry) -> Unit,
isRefreshing: Boolean,
onRefresh: () -> Unit
) { ) {
// TODO: Add pull to refresh SwipeRefresh(
LazyColumn { state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
items(entries) { entry -> onRefresh = onRefresh
EntryListItem( ) {
entry.entry, LazyColumn {
entry.feed, items(entries) { entry ->
onEntryItemClicked, EntryListItem(
onFeedClicked, entry.entry,
onToggleReadClicked, entry.feed,
onStarClicked, onEntryItemClicked,
onExternalLinkClicked onFeedClicked,
) onToggleReadClicked,
onStarClicked,
onExternalLinkClicked
)
}
} }
} }
} }
@ -58,7 +66,7 @@ fun EntryListItem(
onFeedClicked: (feed: Feed) -> Unit, onFeedClicked: (feed: Feed) -> Unit,
onToggleReadClicked: (entry: Entry) -> Unit, onToggleReadClicked: (entry: Entry) -> Unit,
onStarClicked: (entry: Entry) -> Unit, onStarClicked: (entry: Entry) -> Unit,
onExternalLinkClicked: (entry: Entry) -> Unit onExternalLinkClicked: @Composable (entry: Entry) -> Unit
) { ) {
// val swipeState = rememberSwipeableState(initialValue = entry.status) // val swipeState = rememberSwipeableState(initialValue = entry.status)
Column( Column(
@ -121,13 +129,15 @@ fun EntryListItem(
} }
val context = LocalContext.current val context = LocalContext.current
IconButton(onClick = { IconButton(onClick = {
context.startActivity(Intent(Intent.ACTION_SEND).apply { val intent = Intent(Intent.ACTION_SEND).apply {
// TODO: Get base url from viewmodel or something // TODO: Get base url from viewmodel or something
type = "text/plain"
putExtra( putExtra(
Intent.EXTRA_TEXT, Intent.EXTRA_TEXT,
"https://wbrawner.com/entry/share/${entry.shareCode}" "https://wbrawner.com/entry/share/${entry.shareCode}"
) )
}) }
context.startActivity(Intent.createChooser(intent, null))
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Share, imageVector = Icons.Default.Share,

View file

@ -109,6 +109,12 @@ fun MainScaffold(
composable("unread") { composable("unread") {
UnreadScreen(navController, snackbarHostState, hiltViewModel()) UnreadScreen(navController, snackbarHostState, hiltViewModel())
} }
composable("starred") {
UnreadScreen(navController, snackbarHostState, hiltViewModel())
}
composable("history") {
UnreadScreen(navController, snackbarHostState, hiltViewModel())
}
composable( composable(
"entries/{entryId}", "entries/{entryId}",
arguments = listOf(navArgument("entryId") { type = NavType.LongType }) arguments = listOf(navArgument("entryId") { type = NavType.LongType })

View file

@ -1,7 +1,10 @@
package com.wbrawner.nanoflux.ui package com.wbrawner.nanoflux.ui
import android.content.Intent
import android.net.Uri
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.wbrawner.nanoflux.data.viewmodel.UnreadViewModel import com.wbrawner.nanoflux.data.viewmodel.UnreadViewModel
@ -17,30 +20,30 @@ fun UnreadScreen(
val errorMessage by unreadViewModel.errorMessage.collectAsState() val errorMessage by unreadViewModel.errorMessage.collectAsState()
val entries by unreadViewModel.entries.collectAsState(emptyList()) val entries by unreadViewModel.entries.collectAsState(emptyList())
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
if (loading) {
CircularProgressIndicator()
}
errorMessage?.let { errorMessage?.let {
coroutineScope.launch { coroutineScope.launch {
when (snackbarHostState.showSnackbar(it, "Retry")) { when (snackbarHostState.showSnackbar(it, "Retry")) {
// SnackbarResult.ActionPerformed -> unreadViewModel.loadUnread() SnackbarResult.ActionPerformed -> unreadViewModel.refresh()
else -> unreadViewModel.dismissError() else -> unreadViewModel.dismissError()
} }
} }
} }
if (entries.isEmpty()) { EntryList(
Text("TODO: No entries") entries = entries,
} else { onEntryItemClicked = {
EntryList( navController.navigate("entries/${it.id}")
entries = entries, },
onEntryItemClicked = { onFeedClicked = {
navController.navigate("entries/${it.id}") navController.navigate("feeds/${it.id}")
}, },
onFeedClicked = { onToggleReadClicked = { /*TODO*/ },
navController.navigate("feeds/${it.id}") onStarClicked = { /*TODO*/ },
}, onExternalLinkClicked = {
onToggleReadClicked = { /*TODO*/ }, LocalContext.current.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
onStarClicked = { /*TODO*/ }) { },
isRefreshing = loading,
onRefresh = {
unreadViewModel.refresh()
} }
} )
} }

View file

@ -18,6 +18,7 @@ versionName = "1.0"
work = "2.7.1" work = "2.7.1"
[libraries] [libraries]
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version = "0.26.3-beta"}
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" } material = { module = "com.google.android.material:material", version.ref = "material" }

View file

@ -14,11 +14,11 @@ class EntryRepository @Inject constructor(
private val entryDao: EntryDao, private val entryDao: EntryDao,
private val logger: Timber.Tree private val logger: Timber.Tree
) { ) {
fun observeUnread(): Flow<List<EntryAndFeed>> = entryDao.observeAll() fun observeUnread(): Flow<List<EntryAndFeed>> = entryDao.observeUnread()
fun getCount(): Long = entryDao.getCount() fun getCount(): Long = entryDao.getCount()
suspend fun getAll(fetch: Boolean = false, afterId: Long = 0): List<EntryAndFeed> { suspend fun getAll(fetch: Boolean = false, afterId: Long? = null): List<EntryAndFeed> {
if (fetch) { if (fetch) {
getEntries { page -> getEntries { page ->
apiService.getEntries( apiService.getEntries(
@ -77,3 +77,9 @@ class EntryRepository @Inject constructor(
} }
} }
} }
enum class EntryStatus {
UNREAD,
STARRED,
HISTORY
}

View file

@ -11,11 +11,15 @@ interface EntryDao {
fun getCount(): Long fun getCount(): Long
@Transaction @Transaction
@Query("SELECT * FROM Entry ORDER BY createdAt DESC") @Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
fun observeAll(): Flow<List<EntryAndFeed>> fun observeAll(): Flow<List<EntryAndFeed>>
@Transaction @Transaction
@Query("SELECT * FROM Entry ORDER BY createdAt DESC") @Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY publishedAt DESC")
fun observeUnread(): Flow<List<EntryAndFeed>>
@Transaction
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
fun getAll(): List<EntryAndFeed> fun getAll(): List<EntryAndFeed>
@Transaction @Transaction

View file

@ -7,7 +7,7 @@ import com.wbrawner.nanoflux.storage.model.FeedCategoryIcon
@Dao @Dao
interface FeedDao { interface FeedDao {
@Transaction @Transaction
@Query("SELECT * FROM Feed") @Query("SELECT * FROM Feed ORDER BY title ASC")
fun getAll(): List<FeedCategoryIcon> fun getAll(): List<FeedCategoryIcon>
@Transaction @Transaction