feat: Add dedicated pinned bookmarks screen with paginated data loading
- Add getPinnedLinksPaged() query to LinkDao for paginated pinned link retrieval - Implement getPinnedLinksStream() in LinkRepository with PagingConfig for efficient data loading - Create PinnedScreen navigation route and composable in NavGraph - Add "Épinglés" (Pinned) drawer navigation item with PushPin icon in FeedScreen - Wire navigation callbacks from FeedScreen through NavGraph to PinnedScreen
This commit is contained in:
parent
98f2ef2e7e
commit
c8a9e6653b
@ -149,6 +149,9 @@ interface LinkDao {
|
|||||||
@Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC")
|
@Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC")
|
||||||
fun getPinnedLinks(): Flow<List<LinkEntity>>
|
fun getPinnedLinks(): Flow<List<LinkEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC")
|
||||||
|
fun getPinnedLinksPaged(): PagingSource<Int, LinkEntity>
|
||||||
|
|
||||||
// ====== Sync ======
|
// ====== Sync ======
|
||||||
|
|
||||||
@Query("SELECT * FROM links WHERE sync_status = :status")
|
@Query("SELECT * FROM links WHERE sync_status = :status")
|
||||||
|
|||||||
@ -513,6 +513,15 @@ constructor(
|
|||||||
pagingData.map { it.toDomainModel() }
|
pagingData.map { it.toDomainModel() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPinnedLinksStream(): Flow<PagingData<ShaarliLink>> {
|
||||||
|
return Pager(
|
||||||
|
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||||
|
pagingSourceFactory = { linkDao.getPinnedLinksPaged() }
|
||||||
|
).flow.map { pagingData ->
|
||||||
|
pagingData.map { it.toDomainModel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseExistingLink(errorBody: String?): LinkDto? {
|
private fun parseExistingLink(errorBody: String?): LinkDto? {
|
||||||
if (errorBody.isNullOrBlank()) return null
|
if (errorBody.isNullOrBlank()) return null
|
||||||
|
|||||||
@ -61,4 +61,6 @@ interface LinkRepository {
|
|||||||
suspend fun updateLinkClassification(id: Int, contentType: String?, siteName: String?, tagsToAdd: List<String>? = null): Result<Unit>
|
suspend fun updateLinkClassification(id: Int, contentType: String?, siteName: String?, tagsToAdd: List<String>? = null): Result<Unit>
|
||||||
|
|
||||||
fun getDeadLinksStream(): Flow<PagingData<ShaarliLink>>
|
fun getDeadLinksStream(): Flow<PagingData<ShaarliLink>>
|
||||||
|
|
||||||
|
fun getPinnedLinksStream(): Flow<PagingData<ShaarliLink>>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -277,6 +277,7 @@ fun FeedScreen(
|
|||||||
onNavigateToRandom: () -> Unit = {},
|
onNavigateToRandom: () -> Unit = {},
|
||||||
onNavigateToHelp: () -> Unit = {},
|
onNavigateToHelp: () -> Unit = {},
|
||||||
onNavigateToDeadLinks: () -> Unit = {},
|
onNavigateToDeadLinks: () -> Unit = {},
|
||||||
|
onNavigateToPinned: () -> Unit = {},
|
||||||
initialTagFilter: String? = null,
|
initialTagFilter: String? = null,
|
||||||
initialCollectionId: Long? = null,
|
initialCollectionId: Long? = null,
|
||||||
viewModel: FeedViewModel = hiltViewModel()
|
viewModel: FeedViewModel = hiltViewModel()
|
||||||
@ -405,6 +406,15 @@ fun FeedScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DrawerNavigationItem(
|
||||||
|
icon = Icons.Default.PushPin,
|
||||||
|
label = "Épinglés",
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
onNavigateToPinned()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
DrawerNavigationItem(
|
DrawerNavigationItem(
|
||||||
icon = Icons.Default.StickyNote2,
|
icon = Icons.Default.StickyNote2,
|
||||||
label = "Notes",
|
label = "Notes",
|
||||||
|
|||||||
@ -43,6 +43,7 @@ sealed class Screen(val route: String) {
|
|||||||
object Settings : Screen("settings")
|
object Settings : Screen("settings")
|
||||||
object Help : Screen("help")
|
object Help : Screen("help")
|
||||||
object DeadLinks : Screen("dead_links")
|
object DeadLinks : Screen("dead_links")
|
||||||
|
object Pinned : Screen("pinned")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -128,6 +129,7 @@ fun AppNavGraph(
|
|||||||
onNavigateToRandom = { },
|
onNavigateToRandom = { },
|
||||||
onNavigateToHelp = { navController.navigate(Screen.Help.route) },
|
onNavigateToHelp = { navController.navigate(Screen.Help.route) },
|
||||||
onNavigateToDeadLinks = { navController.navigate(Screen.DeadLinks.route) },
|
onNavigateToDeadLinks = { navController.navigate(Screen.DeadLinks.route) },
|
||||||
|
onNavigateToPinned = { navController.navigate(Screen.Pinned.route) },
|
||||||
initialTagFilter = tag,
|
initialTagFilter = tag,
|
||||||
initialCollectionId = collectionId
|
initialCollectionId = collectionId
|
||||||
)
|
)
|
||||||
@ -270,5 +272,16 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Screen.Pinned.route
|
||||||
|
) {
|
||||||
|
com.shaarit.presentation.pinned.PinnedScreen(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onNavigateToEdit = { linkId ->
|
||||||
|
navController.navigate(Screen.Edit.createRoute(linkId))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
package com.shaarit.presentation.pinned
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.shaarit.presentation.feed.ListViewItem
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PinnedScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToEdit: (Int) -> Unit,
|
||||||
|
viewModel: PinnedViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val pagingItems = viewModel.pagedPinnedLinks.collectAsLazyPagingItems()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.surface)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
"Épinglés",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Retour",
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.9f),
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (pagingItems.loadState.refresh is LoadState.Loading && pagingItems.itemCount == 0) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
} else if (pagingItems.itemCount == 0) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PushPin,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
"Aucun bookmark épinglé",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Épinglez vos bookmarks favoris avec le bouton 📌",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(count = pagingItems.itemCount) { index ->
|
||||||
|
val link = pagingItems[index]
|
||||||
|
if (link != null) {
|
||||||
|
ListViewItem(
|
||||||
|
link = link,
|
||||||
|
onItemClick = {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link.url))
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
onLinkClick = { url ->
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
onEditClick = onNavigateToEdit,
|
||||||
|
onDeleteClick = { viewModel.deleteLink(link.id) },
|
||||||
|
onTagClick = { },
|
||||||
|
onViewClick = { },
|
||||||
|
onTogglePin = { id -> viewModel.togglePin(id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.shaarit.presentation.pinned
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import com.shaarit.data.local.dao.LinkDao
|
||||||
|
import com.shaarit.domain.model.ShaarliLink
|
||||||
|
import com.shaarit.domain.repository.LinkRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class PinnedViewModel @Inject constructor(
|
||||||
|
private val linkRepository: LinkRepository,
|
||||||
|
private val linkDao: LinkDao
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val pagedPinnedLinks: Flow<PagingData<ShaarliLink>> =
|
||||||
|
linkRepository.getPinnedLinksStream()
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
fun togglePin(id: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
linkDao.getLinkById(id)?.let { link ->
|
||||||
|
linkDao.updatePinStatus(id, !link.isPinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteLink(id: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
linkRepository.deleteLink(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user