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:
@@ -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:
|
||||||
|
|||||||
@@ -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 */
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user