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")
|
||||
fun getPinnedLinks(): Flow<List<LinkEntity>>
|
||||
|
||||
@Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC")
|
||||
fun getPinnedLinksPaged(): PagingSource<Int, LinkEntity>
|
||||
|
||||
// ====== Sync ======
|
||||
|
||||
@Query("SELECT * FROM links WHERE sync_status = :status")
|
||||
|
||||
@ -514,6 +514,15 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
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? {
|
||||
if (errorBody.isNullOrBlank()) return null
|
||||
return try {
|
||||
|
||||
@ -61,4 +61,6 @@ interface LinkRepository {
|
||||
suspend fun updateLinkClassification(id: Int, contentType: String?, siteName: String?, tagsToAdd: List<String>? = null): Result<Unit>
|
||||
|
||||
fun getDeadLinksStream(): Flow<PagingData<ShaarliLink>>
|
||||
|
||||
fun getPinnedLinksStream(): Flow<PagingData<ShaarliLink>>
|
||||
}
|
||||
|
||||
@ -277,6 +277,7 @@ fun FeedScreen(
|
||||
onNavigateToRandom: () -> Unit = {},
|
||||
onNavigateToHelp: () -> Unit = {},
|
||||
onNavigateToDeadLinks: () -> Unit = {},
|
||||
onNavigateToPinned: () -> Unit = {},
|
||||
initialTagFilter: String? = null,
|
||||
initialCollectionId: Long? = null,
|
||||
viewModel: FeedViewModel = hiltViewModel()
|
||||
@ -405,6 +406,15 @@ fun FeedScreen(
|
||||
}
|
||||
)
|
||||
|
||||
DrawerNavigationItem(
|
||||
icon = Icons.Default.PushPin,
|
||||
label = "Épinglés",
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
onNavigateToPinned()
|
||||
}
|
||||
)
|
||||
|
||||
DrawerNavigationItem(
|
||||
icon = Icons.Default.StickyNote2,
|
||||
label = "Notes",
|
||||
|
||||
@ -43,6 +43,7 @@ sealed class Screen(val route: String) {
|
||||
object Settings : Screen("settings")
|
||||
object Help : Screen("help")
|
||||
object DeadLinks : Screen("dead_links")
|
||||
object Pinned : Screen("pinned")
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -128,6 +129,7 @@ fun AppNavGraph(
|
||||
onNavigateToRandom = { },
|
||||
onNavigateToHelp = { navController.navigate(Screen.Help.route) },
|
||||
onNavigateToDeadLinks = { navController.navigate(Screen.DeadLinks.route) },
|
||||
onNavigateToPinned = { navController.navigate(Screen.Pinned.route) },
|
||||
initialTagFilter = tag,
|
||||
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