Add pull to refresh on entry list view
This commit is contained in:
parent
2d02416101
commit
ed0a7d813f
11 changed files with 83 additions and 49 deletions
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
|
@ -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" }
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue