From f1aed5526f1f28247a4442253789c5d8330c62ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 02:24:40 +0000 Subject: [PATCH 1/2] Initial plan From 458777a9f742d4219007a33a219238f998aca60d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 02:33:41 +0000 Subject: [PATCH 2/2] Add pagination support to links list Co-authored-by: yogeshpaliyal <9381846+yogeshpaliyal@users.noreply.github.com> --- app/src/main/assets/index.html | 238 ++++++++++++++---- .../deepr/server/LocalServerRepositoryImpl.kt | 59 ++++- .../com/yogeshpaliyal/deepr/Deepr.sq | 54 ++++ 3 files changed, 295 insertions(+), 56 deletions(-) diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 0710420..3e71b29 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -237,6 +237,35 @@

Loading links...

Please wait while we fetch your saved links.

+ + + @@ -258,6 +287,10 @@

Loading links...

let allLinks = []; let availableTags = []; // Now contains {id, name, count} objects let selectedTags = []; // Now contains {id, name} objects + let currentPage = 1; + let itemsPerPage = 20; + let totalPages = 1; + let totalItems = 0; // Toast notification system function showToast(message, type = 'success') { @@ -302,6 +335,7 @@

Loading links...

const response = await fetch('/api/tags'); const tags = await response.json(); availableTags = tags; // tags now have {id, name, count} format + populateTagFilter(); } catch (error) { console.error('Error loading tags:', error); availableTags = []; @@ -403,40 +437,21 @@

Loading links...

// Filter and display links function filterAndDisplayLinks() { - const searchInput = document.getElementById('searchInput'); - const tagFilter = document.getElementById('tagFilter'); - const sortBy = document.getElementById('sortBy'); - const sortOrder = document.getElementById('sortOrder'); + currentPage = 1; // Reset to first page when filtering + loadLinks(); + } + + // Display links (without filtering, since that's done server-side now) + function displayLinks() { const container = document.getElementById('linksContainer'); const linkCount = document.getElementById('linkCount'); - // Apply filters - const filteredLinks = allLinks.filter(link => { - const searchTerm = searchInput.value.toLowerCase(); - const matchesSearch = link.name.toLowerCase().includes(searchTerm) || - link.link.toLowerCase().includes(searchTerm); - const matchesTag = tagFilter.value === '' || link.tags.includes(tagFilter.value); - return matchesSearch && matchesTag; - }); - - // Apply sorting - const sortedLinks = filteredLinks.sort((a, b) => { - const order = sortOrder.dataset.order === 'desc' ? -1 : 1; - if (sortBy.value === 'name') { - return order * a.name.localeCompare(b.name); - } else if (sortBy.value === 'openedCount') { - return order * (a.openedCount - b.openedCount); - } else { // createdAt - return order * (new Date(a.createdAt) - new Date(b.createdAt)); - } - }); - // Update link count - linkCount.textContent = `${sortedLinks.length} of ${allLinks.length} ${sortedLinks.length === 1 ? 'link' : 'links'}`; + linkCount.textContent = `${totalItems} ${totalItems === 1 ? 'link' : 'links'}`; // Display results - if (sortedLinks.length === 0) { - const isEmpty = allLinks.length === 0; + if (allLinks.length === 0) { + const isEmpty = totalItems === 0; container.innerHTML = `
@@ -457,7 +472,7 @@

container.innerHTML = `
- ${sortedLinks.map((link, index) => ` + ${allLinks.map((link, index) => `
@@ -517,23 +532,121 @@

// Populate tag filter dropdown function populateTagFilter() { const tagFilter = document.getElementById('tagFilter'); - const allTags = [...new Set(allLinks.flatMap(link => link.tags))].sort(); - + tagFilter.innerHTML = ` - - ${allTags.map(tag => { - const count = allLinks.filter(link => link.tags.includes(tag)).length; - return ``; + + ${availableTags.map(tag => { + return ``; }).join('')} `; } // Filter by specific tag (called when tag is clicked) - function filterByTag(tag) { - const tagFilter = document.getElementById('tagFilter'); - tagFilter.value = tag; - filterAndDisplayLinks(); - showToast(`Filtered by tag: ${tag}`, 'info'); + function filterByTag(tagName) { + const tag = availableTags.find(t => t.name === tagName); + if (tag) { + const tagFilter = document.getElementById('tagFilter'); + tagFilter.value = tag.id; + filterAndDisplayLinks(); + showToast(`Filtered by tag: ${tagName}`, 'info'); + } + } + + // Pagination functions + function updatePaginationControls() { + const paginationContainer = document.getElementById('paginationContainer'); + const prevButton = document.getElementById('prevPage'); + const nextButton = document.getElementById('nextPage'); + const pageNumbers = document.getElementById('pageNumbers'); + const paginationInfo = document.getElementById('paginationInfo'); + + // Show/hide pagination based on whether there are multiple pages + if (totalPages > 1) { + paginationContainer.classList.remove('hidden'); + } else { + paginationContainer.classList.add('hidden'); + return; + } + + // Update prev/next buttons + prevButton.disabled = currentPage === 1; + nextButton.disabled = currentPage === totalPages; + + // Generate page numbers + const pages = []; + const maxVisiblePages = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); + let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); + + // Adjust start if we're near the end + if (endPage - startPage < maxVisiblePages - 1) { + startPage = Math.max(1, endPage - maxVisiblePages + 1); + } + + // Add first page if not visible + if (startPage > 1) { + pages.push(1); + if (startPage > 2) { + pages.push('...'); + } + } + + // Add visible pages + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + // Add last page if not visible + if (endPage < totalPages) { + if (endPage < totalPages - 1) { + pages.push('...'); + } + pages.push(totalPages); + } + + pageNumbers.innerHTML = pages.map(page => { + if (page === '...') { + return `...`; + } + const isActive = page === currentPage; + return ` + + `; + }).join(''); + + // Update pagination info + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + paginationInfo.textContent = `Showing ${startItem}-${endItem} of ${totalItems}`; + } + + function goToPage(page) { + if (page >= 1 && page <= totalPages && page !== currentPage) { + currentPage = page; + loadLinks(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } + + function nextPage() { + if (currentPage < totalPages) { + goToPage(currentPage + 1); + } + } + + function prevPage() { + if (currentPage > 1) { + goToPage(currentPage - 1); + } } // Clear all filters @@ -591,12 +704,38 @@

async function loadLinks() { try { - const response = await fetch('/api/links'); - const links = await response.json(); + const searchInput = document.getElementById('searchInput'); + const tagFilter = document.getElementById('tagFilter'); + const sortBy = document.getElementById('sortBy'); + const sortOrder = document.getElementById('sortOrder'); + + const searchQuery = searchInput.value || ''; + const tagId = tagFilter.value || ''; + const sortField = sortBy.value || 'createdAt'; + const sortOrderValue = sortOrder.dataset.order === 'desc' ? 'DESC' : 'ASC'; + + const params = new URLSearchParams({ + page: currentPage.toString(), + limit: itemsPerPage.toString(), + search: searchQuery, + sortField: sortField, + sortOrder: sortOrderValue + }); + + if (tagId) { + params.append('tagId', tagId); + } + + const response = await fetch(`/api/links?${params.toString()}`); + const data = await response.json(); - allLinks = links; - populateTagFilter(); - filterAndDisplayLinks(); + allLinks = data.links; + totalPages = data.totalPages; + totalItems = data.total; + currentPage = data.page; + + displayLinks(); + updatePaginationControls(); } catch (error) { console.error('Error loading links:', error); @@ -714,6 +853,10 @@

Error loading links

document.getElementById('sortOrder').addEventListener('click', toggleSortOrder); document.getElementById('clearFilters').addEventListener('click', clearAllFilters); + // Pagination controls + document.getElementById('prevPage').addEventListener('click', prevPage); + document.getElementById('nextPage').addEventListener('click', nextPage); + // Tag input functionality const tagInput = document.getElementById('linkTags'); const debouncedShowSuggestions = debounce((input) => showSuggestions(input), 200); @@ -740,7 +883,10 @@

Error loading links

// Form and refresh document.getElementById('addLinkForm').addEventListener('submit', addLink); - document.getElementById('refreshBtn').addEventListener('click', loadLinks); + document.getElementById('refreshBtn').addEventListener('click', () => { + currentPage = 1; + loadLinks(); + }); // Initialize updateSortOrderIcon(); diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt index 216f596..ab22dcc 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt @@ -97,18 +97,38 @@ class LocalServerRepositoryImpl( get("/api/links") { try { + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 + val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 20 + val searchQuery = call.request.queryParameters["search"] ?: "" + val tagId = call.request.queryParameters["tagId"]?.toLongOrNull() + val sortOrder = call.request.queryParameters["sortOrder"] ?: "DESC" + val sortField = call.request.queryParameters["sortField"] ?: "createdAt" + + val offset = (page - 1) * limit + val links = deeprQueries - .getLinksAndTags( - "", - "", - "", - null, - "DESC", - "createdAt", - "DESC", - "createdAt", + .getLinksAndTagsWithPagination( + searchQuery, + searchQuery, + tagId?.toString() ?: "", + tagId, + sortOrder, + sortField, + sortOrder, + sortField, + limit.toLong(), + offset.toLong(), ).executeAsList() + + val total = deeprQueries + .countLinksAndTags( + searchQuery, + searchQuery, + tagId?.toString() ?: "", + tagId, + ).executeAsOne().toInt() + val response = links.map { link -> LinkResponse( @@ -120,7 +140,17 @@ class LocalServerRepositoryImpl( tags = link.tagsNames?.split(", ")?.filter { it.isNotEmpty() } ?: emptyList(), ) } - call.respond(HttpStatusCode.OK, response) + + val totalPages = (total + limit - 1) / limit + val paginatedResponse = PaginatedLinksResponse( + links = response, + page = page, + limit = limit, + total = total, + totalPages = totalPages + ) + + call.respond(HttpStatusCode.OK, paginatedResponse) } catch (e: Exception) { Log.e("LocalServer", "Error getting links", e) call.respond(HttpStatusCode.InternalServerError, ErrorResponse("Error getting links: ${e.message}")) @@ -265,6 +295,15 @@ data class LinkResponse( val tags: List, ) +@Serializable +data class PaginatedLinksResponse( + val links: List, + val page: Int, + val limit: Int, + val total: Int, + val totalPages: Int, +) + @Serializable data class TagData( val id: Long, diff --git a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq index 3602e4b..0170bab 100644 --- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq +++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq @@ -65,6 +65,60 @@ ORDER BY END END DESC; +getLinksAndTagsWithPagination: +SELECT + Deepr.id AS id, + Deepr.link, + Deepr.name, + Deepr.createdAt, + Deepr.openedCount, + GROUP_CONCAT(Tags.name, ', ') AS tagsNames, + GROUP_CONCAT(Tags.id, ', ') AS tagsIds +FROM + Deepr + LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId + LEFT JOIN Tags ON LinkTags.tagId = Tags.id +WHERE + (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%') + AND ( + ? = '' OR Tags.id = ? + ) +GROUP BY + Deepr.id +ORDER BY + CASE WHEN ? = 'ASC' THEN + CASE ? + WHEN 'createdAt' THEN Deepr.createdAt + WHEN 'openedCount' THEN Deepr.openedCount + WHEN 'name' THEN Deepr.name + WHEN 'link' THEN Deepr.link + ELSE Deepr.createdAt + END + END ASC, + CASE WHEN ? = 'DESC' THEN + CASE ? + WHEN 'createdAt' THEN Deepr.createdAt + WHEN 'openedCount' THEN Deepr.openedCount + WHEN 'name' THEN Deepr.name + WHEN 'link' THEN Deepr.link + ELSE Deepr.createdAt + END + END DESC +LIMIT ? OFFSET ?; + +countLinksAndTags: +SELECT + COUNT(DISTINCT Deepr.id) AS total +FROM + Deepr + LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId + LEFT JOIN Tags ON LinkTags.tagId = Tags.id +WHERE + (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%') + AND ( + ? = '' OR Tags.id = ? + ); + listDeeprAsc: SELECT * FROM Deepr ORDER BY createdAt ASC;