From c8a9e6653b708b91b8315a2d82441602512612c2 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 9 Feb 2026 14:43:27 -0500 Subject: [PATCH] feat: Add dedicated pinned bookmarks screen with paginated data loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../com/shaarit/data/local/dao/LinkDao.kt | 3 + .../data/repository/LinkRepositoryImpl.kt | 9 ++ .../domain/repository/LinkRepository.kt | 2 + .../shaarit/presentation/feed/FeedScreen.kt | 10 ++ .../com/shaarit/presentation/nav/NavGraph.kt | 13 ++ .../presentation/pinned/PinnedScreen.kt | 137 ++++++++++++++++++ .../presentation/pinned/PinnedViewModel.kt | 38 +++++ 7 files changed, 212 insertions(+) create mode 100644 app/src/main/java/com/shaarit/presentation/pinned/PinnedScreen.kt create mode 100644 app/src/main/java/com/shaarit/presentation/pinned/PinnedViewModel.kt diff --git a/app/src/main/java/com/shaarit/data/local/dao/LinkDao.kt b/app/src/main/java/com/shaarit/data/local/dao/LinkDao.kt index 838893e..a2440c5 100644 --- a/app/src/main/java/com/shaarit/data/local/dao/LinkDao.kt +++ b/app/src/main/java/com/shaarit/data/local/dao/LinkDao.kt @@ -149,6 +149,9 @@ interface LinkDao { @Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC") fun getPinnedLinks(): Flow> + @Query("SELECT * FROM links WHERE is_pinned = 1 ORDER BY created_at DESC") + fun getPinnedLinksPaged(): PagingSource + // ====== Sync ====== @Query("SELECT * FROM links WHERE sync_status = :status") diff --git a/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt b/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt index d50e302..5f4fe4f 100644 --- a/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt +++ b/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt @@ -513,6 +513,15 @@ constructor( pagingData.map { it.toDomainModel() } } } + + override fun getPinnedLinksStream(): Flow> { + 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 diff --git a/app/src/main/java/com/shaarit/domain/repository/LinkRepository.kt b/app/src/main/java/com/shaarit/domain/repository/LinkRepository.kt index 14f4192..d84519c 100644 --- a/app/src/main/java/com/shaarit/domain/repository/LinkRepository.kt +++ b/app/src/main/java/com/shaarit/domain/repository/LinkRepository.kt @@ -61,4 +61,6 @@ interface LinkRepository { suspend fun updateLinkClassification(id: Int, contentType: String?, siteName: String?, tagsToAdd: List? = null): Result fun getDeadLinksStream(): Flow> + + fun getPinnedLinksStream(): Flow> } diff --git a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt index ee08996..f3d59c4 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt @@ -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", diff --git a/app/src/main/java/com/shaarit/presentation/nav/NavGraph.kt b/app/src/main/java/com/shaarit/presentation/nav/NavGraph.kt index a97490e..3b7b71d 100644 --- a/app/src/main/java/com/shaarit/presentation/nav/NavGraph.kt +++ b/app/src/main/java/com/shaarit/presentation/nav/NavGraph.kt @@ -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)) + } + ) + } } } diff --git a/app/src/main/java/com/shaarit/presentation/pinned/PinnedScreen.kt b/app/src/main/java/com/shaarit/presentation/pinned/PinnedScreen.kt new file mode 100644 index 0000000..1874a5a --- /dev/null +++ b/app/src/main/java/com/shaarit/presentation/pinned/PinnedScreen.kt @@ -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) } + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/shaarit/presentation/pinned/PinnedViewModel.kt b/app/src/main/java/com/shaarit/presentation/pinned/PinnedViewModel.kt new file mode 100644 index 0000000..fa7bdab --- /dev/null +++ b/app/src/main/java/com/shaarit/presentation/pinned/PinnedViewModel.kt @@ -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> = + 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) + } + } +}