Explorar o código

Add tag-based search functionality

- Include tags in fuzzy search scoring alongside title, author, path
- Add "tag:tagname" prefix syntax for exact tag filtering
- Case-insensitive matching for all tag searches
- Substring matching within tags when using tag: prefix

Users can now search their library by tag names. Using "tag:fiction"
will show only books tagged with "fiction" or tags containing "fiction".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bernardo Magri hai 1 mes
pai
achega
3b9d74a172
Modificáronse 2 ficheiros con 40 adicións e 13 borrados
  1. 9 13
      PLAN.md
  2. 31 0
      src/BookShelf.cpp

+ 9 - 13
PLAN.md

@@ -73,21 +73,17 @@ Implemented:
 
 ### 2.1 Implement Tag-Based Search
 **Files**: `src/BookShelf.cpp`
-**Status**: Not Started
-**Location**: `src/BookShelf.cpp:67-76` (`book_score()` function)
+**Status**: COMPLETED
+**Location**: `src/BookShelf.cpp:67-107`
 
-Current behavior:
-- `book_score()` searches: title, author, filePath, id
-- Tags are NOT searched despite being loaded into Book objects
+Implemented:
+- [x] Tags are now included in fuzzy search scoring
+- [x] `tag:tagname` prefix syntax for exact tag filtering
+- [x] Case-insensitive tag matching
+- [x] Substring matching within tags for `tag:` syntax
 
-Tasks:
-- [ ] Add tag matching loop in `book_score()`:
-  ```cpp
-  for (const auto& tag : book.tags())
-    best = std::max(best, field_score(tag, pattern));
-  ```
-- [ ] Support `tag:tagname` search syntax for exact tag filtering
-- [ ] Consider tag chips as quick filters in search UI (Phase 3)
+Remaining (moved to Phase 3):
+- [ ] Consider tag chips as quick filters in search UI
 
 ### 2.2 Add Error Handling and User Feedback
 **Files**: `src/BibliothecaWindow.cpp`, `src/BookDetails.cpp`, `src/BookImport.cpp`

+ 31 - 0
src/BookShelf.cpp

@@ -64,14 +64,45 @@ double field_score(const std::string& field, const std::string& pattern) {
   return subsequence_score(lower, pattern);
 }
 
+// Check if pattern starts with "tag:" prefix for exact tag filtering
+bool has_tag_prefix(const std::string& pattern, std::string& tag_query) {
+  const std::string prefix = "tag:";
+  if (pattern.size() > prefix.size() &&
+      ascii_lower(pattern.substr(0, prefix.size())) == prefix) {
+    tag_query = ascii_lower(pattern.substr(prefix.size()));
+    return true;
+  }
+  return false;
+}
+
 double book_score(const Book& book, const std::string& pattern) {
   if (pattern.empty())
     return 0.0;
+
+  // Handle "tag:tagname" syntax for exact tag filtering
+  std::string tag_query;
+  if (has_tag_prefix(pattern, tag_query)) {
+    if (tag_query.empty())
+      return 0.0;
+    for (const auto& tag : book.tags()) {
+      auto lower_tag = ascii_lower(tag);
+      // Exact match or substring match within tag
+      if (lower_tag == tag_query || lower_tag.find(tag_query) != std::string::npos)
+        return 100.0; // High score for tag match
+    }
+    return -1.0; // No matching tag
+  }
+
+  // Regular fuzzy search across all fields
   double best = -1.0;
   best = std::max(best, field_score(book.title(), pattern));
   best = std::max(best, field_score(book.author(), pattern));
   best = std::max(best, field_score(book.filePath(), pattern));
   best = std::max(best, field_score(book.id(), pattern));
+  // Also search tags
+  for (const auto& tag : book.tags()) {
+    best = std::max(best, field_score(tag, pattern));
+  }
   return best;
 }