mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-11 17:28:05 +02:00
feat: implement Deep Forest dark mode, swipe gestures, PWA support, and PostgreSQL Full-Text Search
This commit is contained in:
1 parent
3e468d8885
commit
7e802df2a7
18 files changed
+4800
-128
No files matched your search
@@ -42,4 +42,5 @@ type ArticleRepository interface {
|
||||
ToggleFavorite(id uuid.UUID) error
|
||||
GetFavorites(userID uuid.UUID, limit, offset int) ([]*Article, error)
|
||||
CountUnread(feedID uuid.UUID) (int, error)
|
||||
Search(userID uuid.UUID, query string, limit, offset int) ([]*Article, error)
|
||||
}
|
||||
@@ -355,3 +355,36 @@ func (h *ArticleHandler) GetFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
respondJSON(w, http.StatusOK, articles)
|
||||
}
|
||||
|
||||
// Search handles GET /api/v1/articles/search
|
||||
func (h *ArticleHandler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := h.getUserFromRequest(r)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
respondJSON(w, http.StatusOK, []*domain.Article{})
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
articles, err := h.articleRepo.Search(userID, query, limit, offset)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to search articles")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, articles)
|
||||
}
|
||||
@@ -411,6 +411,91 @@ func (r *ArticleRepository) scanArticles(rows pgx.Rows) ([]*domain.Article, erro
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
// Search performs a full-text search on articles for a specific user.
|
||||
func (r *ArticleRepository) Search(userID uuid.UUID, query string, limit, offset int) ([]*domain.Article, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Use plainto_tsquery or websearch_to_tsquery for natural language search
|
||||
sql := `
|
||||
SELECT a.id, a.feed_id, a.guid, a.title, a.url, a.content, a.summary, a.author,
|
||||
a.image_url, a.published_at, a.is_read, a.is_favorite, a.read_at, a.created_at,
|
||||
f.title as feed_title,
|
||||
ts_rank_cd(a.tsv, websearch_to_tsquery('french', $2)) as rank
|
||||
FROM articles a
|
||||
JOIN feeds f ON f.id = a.feed_id
|
||||
WHERE f.user_id = $1 AND a.tsv @@ websearch_to_tsquery('french', $2)
|
||||
ORDER BY rank DESC, a.published_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`
|
||||
|
||||
rows, err := r.pool.Query(ctx, sql, userID, query, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("searching articles: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanArticlesWithRank(rows)
|
||||
}
|
||||
|
||||
// scanArticlesWithRank scans multiple article rows with their rank.
|
||||
func (r *ArticleRepository) scanArticlesWithRank(rows pgx.Rows) ([]*domain.Article, error) {
|
||||
var articles []*domain.Article
|
||||
for rows.Next() {
|
||||
var article domain.Article
|
||||
var url, content, summary, author, imageURL, feedTitle *string
|
||||
var publishedAt, readAt *time.Time
|
||||
var rank float32
|
||||
|
||||
err := rows.Scan(
|
||||
&article.ID,
|
||||
&article.FeedID,
|
||||
&article.GUID,
|
||||
&article.Title,
|
||||
&url,
|
||||
&content,
|
||||
&summary,
|
||||
&author,
|
||||
&imageURL,
|
||||
&publishedAt,
|
||||
&article.IsRead,
|
||||
&article.IsFavorite,
|
||||
&readAt,
|
||||
&article.CreatedAt,
|
||||
&feedTitle,
|
||||
&rank,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning article with rank: %w", err)
|
||||
}
|
||||
|
||||
if url != nil {
|
||||
article.URL = *url
|
||||
}
|
||||
if content != nil {
|
||||
article.Content = *content
|
||||
}
|
||||
if summary != nil {
|
||||
article.Summary = *summary
|
||||
}
|
||||
if author != nil {
|
||||
article.Author = *author
|
||||
}
|
||||
if imageURL != nil {
|
||||
article.ImageURL = *imageURL
|
||||
}
|
||||
if feedTitle != nil {
|
||||
article.FeedTitle = *feedTitle
|
||||
}
|
||||
article.PublishedAt = publishedAt
|
||||
article.ReadAt = readAt
|
||||
|
||||
articles = append(articles, &article)
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
// nullString returns nil if string is empty.
|
||||
func nullString(s string) *string {
|
||||
if s == "" {
|
||||
|
||||
Reference in new issue
Block a user