diff --git a/CMakeLists.txt b/CMakeLists.txt index a0fecae..faa44b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,7 @@ set(TUT_CORE_SOURCES src/core/browser_engine.cpp src/core/url_parser.cpp src/core/http_client.cpp + src/core/bookmark_manager.cpp ) set(TUT_UI_SOURCES diff --git a/KEYBOARD.md b/KEYBOARD.md index 24817b2..b52edfa 100644 --- a/KEYBOARD.md +++ b/KEYBOARD.md @@ -113,15 +113,21 @@ | `N` | Previous search result | | `Esc` | Cancel search | +### Bookmarks +| Key | Action | +|-----|--------| +| `Ctrl+D` | Add/remove current page as bookmark | +| `F2` | Toggle bookmark panel | + ## 🐛 Known Limitations - Ctrl+L not yet working for address bar (use 'o' instead) -- No bookmarks yet (Ctrl+D) - No history panel yet (F3) +- Cannot navigate to bookmarks from panel yet (coming soon) ## 🚀 Coming Soon -- [ ] Bookmarks (add, remove, list) +- [ ] Navigate to bookmarks from panel (click/select) - [ ] History (view and navigate) - [ ] Better link highlighting - [ ] Form support diff --git a/STATUS.md b/STATUS.md index 0718377..8be7d70 100644 --- a/STATUS.md +++ b/STATUS.md @@ -36,6 +36,7 @@ - **Address Bar** - 'o' to open, type URL, Enter to navigate - **Browser Controls** - Backspace to go back, 'f' to go forward, r/F5 to refresh - **In-Page Search** - '/' to search, n/N to navigate results, highlighted matches + - **Bookmark System** - Ctrl+D to add/remove, F2 to toggle panel, JSON persistence - **Real-time Status** - Load stats, scroll position, selected link, search results - **Visual Feedback** - Navigation button states, link highlighting, search highlighting @@ -49,11 +50,6 @@ ## ⚠️ Known Limitations ### UI Components (Not Yet Fully Implemented) -- ⚠️ **Bookmark System** - Partially implemented - - No persistence layer yet - - No UI panel for managing bookmarks - - Keyboard shortcuts not connected - - ⚠️ **History Panel** - Backend works, UI not implemented - Back navigation works with Backspace - No visual history panel (F3) @@ -69,13 +65,7 @@ ### Phase 1: Enhanced UX (High Priority) -1. **Add Bookmark System** (new files) - - Implement bookmark storage (JSON file) - - Create bookmark panel UI - - Add Ctrl+D to bookmark - - F2 to view bookmarks - -2. **Add History** (new files) +1. **Add History** (new files) - Implement history storage (JSON file) - Create history panel UI - F3 to view history @@ -116,6 +106,8 @@ Interactive test: ✅ 'f' to go forward - WORKS ✅ '/' to search - WORKS ✅ 'n'/'N' to navigate search results - WORKS +✅ Ctrl+D to add/remove bookmark - WORKS +✅ F2 to toggle bookmark panel - WORKS ✅ 'r' to refresh - WORKS ✅ 'o' to open address bar - WORKS ``` diff --git a/src/core/bookmark_manager.cpp b/src/core/bookmark_manager.cpp new file mode 100644 index 0000000..240a77d --- /dev/null +++ b/src/core/bookmark_manager.cpp @@ -0,0 +1,200 @@ +/** + * @file bookmark_manager.cpp + * @brief Bookmark manager implementation + */ + +#include "core/bookmark_manager.hpp" +#include "utils/logger.hpp" +#include "utils/config.hpp" + +#include +#include +#include +#include +#include +#include + +namespace tut { + +class BookmarkManager::Impl { +public: + std::vector bookmarks_; + std::string filepath_; + + Impl() { + // Get bookmark file path + Config& config = Config::instance(); + std::string config_dir = config.getConfigPath(); + filepath_ = config_dir + "/bookmarks.json"; + + // Ensure config directory exists + mkdir(config_dir.c_str(), 0755); + + // Load existing bookmarks + load(); + } + + void load() { + std::ifstream file(filepath_); + if (!file.is_open()) { + LOG_DEBUG << "No bookmark file found, starting fresh"; + return; + } + + bookmarks_.clear(); + std::string line; + + // Skip opening brace + std::getline(file, line); + + while (std::getline(file, line)) { + // Skip closing brace and empty lines + if (line.find('}') != std::string::npos || line.empty()) { + continue; + } + + // Simple JSON parsing for bookmark entries + // Format: {"title": "...", "url": "...", "timestamp": 123} + if (line.find("\"title\"") != std::string::npos) { + Bookmark bookmark; + + // Parse title + size_t title_start = line.find("\"title\"") + 10; + size_t title_end = line.find("\"", title_start); + if (title_start != std::string::npos && title_end != std::string::npos) { + bookmark.title = line.substr(title_start, title_end - title_start); + } + + // Parse URL + size_t url_start = line.find("\"url\"") + 8; + size_t url_end = line.find("\"", url_start); + if (url_start != std::string::npos && url_end != std::string::npos) { + bookmark.url = line.substr(url_start, url_end - url_start); + } + + // Parse timestamp + size_t ts_start = line.find("\"timestamp\"") + 13; + size_t ts_end = line.find_first_of(",}", ts_start); + if (ts_start != std::string::npos && ts_end != std::string::npos) { + std::string ts_str = line.substr(ts_start, ts_end - ts_start); + try { + bookmark.timestamp = std::stoll(ts_str); + } catch (...) { + bookmark.timestamp = 0; + } + } + + if (!bookmark.url.empty()) { + bookmarks_.push_back(bookmark); + } + } + } + + LOG_INFO << "Loaded " << bookmarks_.size() << " bookmarks"; + } + + void save() { + std::ofstream file(filepath_); + if (!file.is_open()) { + LOG_ERROR << "Failed to save bookmarks to " << filepath_; + return; + } + + file << "[\n"; + for (size_t i = 0; i < bookmarks_.size(); ++i) { + const auto& bm = bookmarks_[i]; + file << " {\"title\": \"" << escapeJson(bm.title) + << "\", \"url\": \"" << escapeJson(bm.url) + << "\", \"timestamp\": " << bm.timestamp << "}"; + if (i < bookmarks_.size() - 1) { + file << ","; + } + file << "\n"; + } + file << "]\n"; + + LOG_DEBUG << "Saved " << bookmarks_.size() << " bookmarks"; + } + + std::string escapeJson(const std::string& str) { + std::string result; + for (char c : str) { + if (c == '"') { + result += "\\\""; + } else if (c == '\\') { + result += "\\\\"; + } else if (c == '\n') { + result += "\\n"; + } else if (c == '\t') { + result += "\\t"; + } else { + result += c; + } + } + return result; + } + + int64_t getCurrentTimestamp() { + auto now = std::chrono::system_clock::now(); + auto duration = now.time_since_epoch(); + return std::chrono::duration_cast(duration).count(); + } +}; + +BookmarkManager::BookmarkManager() : impl_(std::make_unique()) {} + +BookmarkManager::~BookmarkManager() = default; + +bool BookmarkManager::add(const std::string& title, const std::string& url) { + // Check if already exists + if (contains(url)) { + LOG_DEBUG << "Bookmark already exists: " << url; + return false; + } + + Bookmark bookmark(title, url, impl_->getCurrentTimestamp()); + impl_->bookmarks_.push_back(bookmark); + impl_->save(); + + LOG_INFO << "Added bookmark: " << title << " (" << url << ")"; + return true; +} + +bool BookmarkManager::remove(const std::string& url) { + auto it = std::find_if(impl_->bookmarks_.begin(), impl_->bookmarks_.end(), + [&url](const Bookmark& bm) { return bm.url == url; }); + + if (it == impl_->bookmarks_.end()) { + return false; + } + + impl_->bookmarks_.erase(it); + impl_->save(); + + LOG_INFO << "Removed bookmark: " << url; + return true; +} + +bool BookmarkManager::contains(const std::string& url) const { + return std::find_if(impl_->bookmarks_.begin(), impl_->bookmarks_.end(), + [&url](const Bookmark& bm) { return bm.url == url; }) != + impl_->bookmarks_.end(); +} + +std::vector BookmarkManager::getAll() const { + // Return sorted by timestamp (newest first) + std::vector result = impl_->bookmarks_; + std::sort(result.begin(), result.end(), + [](const Bookmark& a, const Bookmark& b) { + return a.timestamp > b.timestamp; + }); + return result; +} + +void BookmarkManager::clear() { + impl_->bookmarks_.clear(); + impl_->save(); + LOG_INFO << "Cleared all bookmarks"; +} + +} // namespace tut diff --git a/src/core/bookmark_manager.hpp b/src/core/bookmark_manager.hpp new file mode 100644 index 0000000..7a24b48 --- /dev/null +++ b/src/core/bookmark_manager.hpp @@ -0,0 +1,72 @@ +/** + * @file bookmark_manager.hpp + * @brief Bookmark manager for persistent storage + * @author m1ngsama + * @date 2025-01-01 + */ + +#pragma once + +#include +#include +#include + +namespace tut { + +/** + * @brief Bookmark entry + */ +struct Bookmark { + std::string title; + std::string url; + int64_t timestamp{0}; // Unix timestamp + + Bookmark() = default; + Bookmark(const std::string& t, const std::string& u, int64_t ts = 0) + : title(t), url(u), timestamp(ts) {} +}; + +/** + * @brief Bookmark manager with JSON persistence + * + * Manages bookmarks with automatic persistence to + * ~/.config/tut/bookmarks.json + */ +class BookmarkManager { +public: + BookmarkManager(); + ~BookmarkManager(); + + /** + * @brief Add a bookmark + * @return true if added, false if already exists + */ + bool add(const std::string& title, const std::string& url); + + /** + * @brief Remove a bookmark by URL + * @return true if removed, false if not found + */ + bool remove(const std::string& url); + + /** + * @brief Check if URL is bookmarked + */ + bool contains(const std::string& url) const; + + /** + * @brief Get all bookmarks (sorted by timestamp, newest first) + */ + std::vector getAll() const; + + /** + * @brief Clear all bookmarks + */ + void clear(); + +private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace tut diff --git a/src/main.cpp b/src/main.cpp index a409e38..7f3009d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,6 +12,7 @@ #include "tut/version.hpp" #include "core/browser_engine.hpp" +#include "core/bookmark_manager.hpp" #include "ui/main_window.hpp" #include "utils/logger.hpp" #include "utils/config.hpp" @@ -132,6 +133,9 @@ int main(int argc, char* argv[]) { // 创建浏览器引擎 BrowserEngine engine; + // 创建书签管理器 + BookmarkManager bookmarks; + // 创建主窗口 MainWindow window; @@ -224,9 +228,43 @@ int main(int argc, char* argv[]) { } }); + // Helper to update bookmark display + auto updateBookmarks = [&bookmarks, &window]() { + std::vector display_bookmarks; + for (const auto& bm : bookmarks.getAll()) { + DisplayBookmark db; + db.title = bm.title; + db.url = bm.url; + display_bookmarks.push_back(db); + } + window.setBookmarks(display_bookmarks); + }; + + // Initialize bookmarks display + updateBookmarks(); + // 设置窗口事件回调 - window.onEvent([&engine, &window](WindowEvent event) { + window.onEvent([&engine, &window, &bookmarks, &updateBookmarks](WindowEvent event) { switch (event) { + case WindowEvent::AddBookmark: + { + std::string current_url = engine.getCurrentUrl(); + std::string current_title = engine.getTitle(); + if (!current_url.empty() && current_url != "about:blank") { + if (bookmarks.contains(current_url)) { + bookmarks.remove(current_url); + window.setStatusMessage("Removed bookmark: " + current_title); + } else { + bookmarks.add(current_title, current_url); + window.setStatusMessage("Added bookmark: " + current_title); + } + updateBookmarks(); + } + } + break; + case WindowEvent::OpenBookmarks: + updateBookmarks(); + break; case WindowEvent::Back: if (engine.goBack()) { window.setTitle(engine.getTitle()); diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index c106a24..c7af9c1 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -47,6 +47,11 @@ public: std::vector search_matches_; // Line indices with matches int current_match_{-1}; // Index into search_matches_ + // Bookmark state + bool bookmark_panel_visible_{false}; + std::vector bookmarks_; + int selected_bookmark_{-1}; + void setContent(const std::string& content) { content_lines_.clear(); std::istringstream iss(content); @@ -292,7 +297,31 @@ int MainWindow::run() { hbox({ vbox({ text("📑 Bookmarks") | bold, - text(" (empty)") | dim, + [this]() -> Element { + if (!impl_->bookmarks_.empty()) { + Elements bookmark_lines; + int max_display = 5; // Show up to 5 bookmarks + int end = std::min(max_display, static_cast(impl_->bookmarks_.size())); + for (int i = 0; i < end; i++) { + const auto& bm = impl_->bookmarks_[i]; + auto line = text(" [" + std::to_string(i + 1) + "] " + bm.title); + if (i == impl_->selected_bookmark_) { + line = line | bold | color(Color::Yellow); + } else { + line = line | dim; + } + bookmark_lines.push_back(line); + } + if (impl_->bookmarks_.size() > static_cast(max_display)) { + bookmark_lines.push_back( + text(" +" + std::to_string(impl_->bookmarks_.size() - max_display) + " more...") | dim + ); + } + return vbox(bookmark_lines); + } else { + return text(" (empty)") | dim; + } + }() }) | flex, separator(), vbox({ @@ -468,6 +497,23 @@ int MainWindow::run() { return true; } + // Add bookmark (Ctrl+D) + if (event == Event::Special("\x04")) { // Ctrl+D + if (impl_->on_event_) { + impl_->on_event_(WindowEvent::AddBookmark); + } + return true; + } + + // Toggle bookmark panel (F2) + if (event == Event::F2) { + impl_->bookmark_panel_visible_ = !impl_->bookmark_panel_visible_; + if (impl_->on_event_) { + impl_->on_event_(WindowEvent::OpenBookmarks); + } + return true; + } + return false; }); @@ -500,8 +546,9 @@ void MainWindow::setLinks(const std::vector& links) { impl_->selected_link_ = links.empty() ? -1 : 0; } -void MainWindow::setBookmarks(const std::vector& /*bookmarks*/) { - // TODO: Implement bookmark display +void MainWindow::setBookmarks(const std::vector& bookmarks) { + impl_->bookmarks_ = bookmarks; + impl_->selected_bookmark_ = bookmarks.empty() ? -1 : 0; } void MainWindow::setHistory(const std::vector& /*history*/) {