1
0
mirror of https://github.com/aquatix/digimarks.git synced 2025-12-06 22:05:09 +01:00

Support for filtering on tags

This commit is contained in:
2025-05-12 16:13:28 +02:00
parent 1836eedfe8
commit 70979b3350
3 changed files with 60 additions and 11 deletions

View File

@@ -17,7 +17,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import AnyUrl, DirectoryPath, FilePath from pydantic import AnyUrl, DirectoryPath, FilePath, computed_field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from sqlmodel import AutoString, Field, Session, SQLModel, create_engine, desc, select from sqlmodel import AutoString, Field, Session, SQLModel, create_engine, desc, select
@@ -240,6 +240,15 @@ class Bookmark(SQLModel, table=True):
status: int = Field(default=Visibility.VISIBLE) status: int = Field(default=Visibility.VISIBLE)
@computed_field
@property
def tag_list(self) -> list:
"""The tags but as a proper list."""
if not self.tags:
# Not tags, return empty list instead of [''] that split returns in that case
return []
return self.tags.split(',')
async def set_title_from_source(self, request: Request) -> str: async def set_title_from_source(self, request: Request) -> str:
"""Request the title by requesting the source url.""" """Request the title by requesting the source url."""
try: try:

View File

@@ -7,7 +7,6 @@ document.addEventListener('alpine:init', () => {
cache: Alpine.$persist({}).as('cache'), cache: Alpine.$persist({}).as('cache'),
bookmarks: [], bookmarks: [],
tags: [],
/* Bookmark that is being edited, used to fill the form etc */ /* Bookmark that is being edited, used to fill the form etc */
bookmark_to_edit: null, bookmark_to_edit: null,
@@ -26,6 +25,10 @@ document.addEventListener('alpine:init', () => {
/* Search filter */ /* Search filter */
search: '', search: '',
/* Show bookmarks with this tag/these tags */
tags_filter: [],
/* Hide bookmarks with these tags */
tags_to_hide: Alpine.$persist([]).as('tags_to_hide'),
/* Sort on ~ */ /* Sort on ~ */
sort_title_asc: Alpine.$persist(false).as('sort_title_asc'), sort_title_asc: Alpine.$persist(false).as('sort_title_asc'),
@@ -61,9 +64,10 @@ document.addEventListener('alpine:init', () => {
}, },
async loadCache() { async loadCache() {
/* Load bookmarks and tags from cache */
if (this.userKey in this.cache) { if (this.userKey in this.cache) {
console.log('Loading bookmarks from cache for user "' + this.userKey + '"'); console.log('Loading bookmarks from cache for user "' + this.userKey + '"');
this.bookmarks = this.cache[this.userKey]['bookmarks'] || []; this.filterBookmarksByTags();
} }
}, },
async getBookmarks() { async getBookmarks() {
@@ -89,34 +93,65 @@ document.addEventListener('alpine:init', () => {
} }
console.log('Fetching latest bookmarks from backend for user "' + this.userKey + '"...'); console.log('Fetching latest bookmarks from backend for user "' + this.userKey + '"...');
/* At the moment, request 'a lot' bookmarks; likely all of them in one go; paging tbd if needed */
let response = await fetch('/api/v1/' + this.userKey + '/bookmarks/?limit=10000'); let response = await fetch('/api/v1/' + this.userKey + '/bookmarks/?limit=10000');
let result = await response.json();
this.bookmarks = result;
/* Cache the bookmarks to Local Storage */ /* Cache the bookmarks to Local Storage */
this.cache[this.userKey]['bookmarks'] = result; this.cache[this.userKey]['bookmarks'] = await response.json();
let tags_response = await fetch('/api/v1/' + this.userKey + '/tags/'); let tags_response = await fetch('/api/v1/' + this.userKey + '/tags/');
this.cache[this.userKey]['tags'] = await tags_response.json(); this.cache[this.userKey]['tags'] = await tags_response.json();
/* Filter bookmarks by (blacklisted) tags */
await this.filterBookmarksByTags();
this.loading = false; this.loading = false;
}, },
hasTag(tagList, filterList) {
/* Looks for the items in filterList and returns True when one appears on the tagList */
if (tagList === undefined) {
return false;
}
for (let tag in filterList) {
if (tagList.includes(tag))
return true;
}
return false;
},
filterBookmarksByTags() {
/* Filter away bookmarks with a tag on the 'blacklist' */
/* First make a shallow copy of all bookmarks */
let prefiltered_bookmarks = [...this.cache[this.userKey]['bookmarks']] || [];
if (this.tags_to_hide.length > 0) {
console.log('Filtering away bookmarks containing blacklisted tags');
this.bookmarks = prefiltered_bookmarks.filter(
i => !this.hasTag(i.tag_list, this.tags_to_hide)
)
} else {
this.bookmarks = prefiltered_bookmarks;
}
},
get filteredBookmarks() { get filteredBookmarks() {
// return this.cache[this.userKey]['bookmarks'].filter( /* Get the bookmarks, optionally filtered by search text or tag black-/whitelists */
// i => i.title.includes(this.search)
// ) /* Use 'bookmarks' and not the cache, as it can already be pre-filtered */
/* Use 'bookmarks' as it can already be pre-filtered */ if (this.search === '') {
/* No need to filter, quickly return the set */
return this.bookmarks;
}
return this.bookmarks.filter( return this.bookmarks.filter(
i => i.title.match(new RegExp(this.search, "i")) i => i.title.match(new RegExp(this.search, "i"))
) )
}, },
get filteredTags() { get filteredTags() {
/* Search in the list of all tags */
return this.cache[this.userKey].tags.filter( return this.cache[this.userKey].tags.filter(
i => i.match(new RegExp(this.search, "i")) i => i.match(new RegExp(this.search, "i"))
) )
}, },
async sortAlphabetically(order = 'asc') { async sortAlphabetically(order = 'asc') {
/* Sort the bookmarks (reverse) alphabetically, based on 'asc' or 'desc' */
this.loading = true; this.loading = true;
this.sort_created_asc = false; this.sort_created_asc = false;
this.sort_created_desc = false; this.sort_created_desc = false;
@@ -132,6 +167,7 @@ document.addEventListener('alpine:init', () => {
this.loading = false; this.loading = false;
}, },
async sortCreated(order = 'asc') { async sortCreated(order = 'asc') {
/* Sort the bookmarks (reverse) chronologically, based on 'asc' or 'desc' */
this.loading = true; this.loading = true;
this.sort_created_asc = false; this.sort_created_asc = false;
this.sort_created_desc = false; this.sort_created_desc = false;
@@ -148,22 +184,26 @@ document.addEventListener('alpine:init', () => {
}, },
async toggleTagPage() { async toggleTagPage() {
/* Show or hide the tag page instead of the bookmarks */
console.log('Toggle tag page'); console.log('Toggle tag page');
this.show_bookmarks = !this.show_bookmarks; this.show_bookmarks = !this.show_bookmarks;
this.show_tags = !this.show_bookmarks; this.show_tags = !this.show_bookmarks;
}, },
async toggleListOrGrid() { async toggleListOrGrid() {
/* Toggle between 'list' or 'grid' (cards) view */
this.show_bookmarks_list = !this.show_bookmarks_list; this.show_bookmarks_list = !this.show_bookmarks_list;
this.show_bookmarks_cards = !this.show_bookmarks_list; this.show_bookmarks_cards = !this.show_bookmarks_list;
}, },
async startAddingBookmark() { async startAddingBookmark() {
/* Open 'add bookmark' page */
console.log('Start adding bookmark'); console.log('Start adding bookmark');
this.bookmark_to_edit = { this.bookmark_to_edit = {
'url': '' 'url': ''
} }
}, },
async addBookmark() { async addBookmark() {
/* Post new bookmark to the backend */
// //
} }
}) })

View File

@@ -21,7 +21,7 @@
<li> <li>
<button @click="$store.digimarks.loopToNextTheme()" class="theme-toggle">theme</button> <button @click="$store.digimarks.loopToNextTheme()" class="theme-toggle">theme</button>
</li> </li>
<li><input x-model="$store.digimarks.search" placeholder="Search..."></li> <li><input x-model="$store.digimarks.search" placeholder="Search/filter..."></li>
<li x-show="$store.digimarks.loading"><i class="fa-solid fa-rotate-right fa-spin"></i></li> <li x-show="$store.digimarks.loading"><i class="fa-solid fa-rotate-right fa-spin"></i></li>
</ul> </ul>
</nav> </nav>