Przeglądaj źródła

adding documentation

Bernardo Magri 3 miesięcy temu
rodzic
commit
8e553142f6
9 zmienionych plików z 577 dodań i 0 usunięć
  1. 48 0
      docs/README.md
  2. 83 0
      docs/bibliotheca_window.md
  3. 75 0
      docs/bookimport.md
  4. 91 0
      docs/booklist.md
  5. 103 0
      docs/bookshelf.md
  6. 60 0
      docs/booktile.md
  7. 79 0
      docs/database_manager.md
  8. 16 0
      docs/main.md
  9. 22 0
      docs/sha256.md

+ 48 - 0
docs/README.md

@@ -0,0 +1,48 @@
+# Bibliotheca Architecture Overview
+
+This document links together the module overviews and describes how a book flows
+through the system.
+
+## High-level flow
+
+1. **Import** (`BookImport`, `Sha256`)
+   - Compute SHA-256 id for each selected file.
+   - Extract metadata and cover (EPUB via libzip/tinyxml2, PDF via poppler).
+2. **Persistence** (`DatabaseManager`)
+   - Ensure the SQLite schema exists.
+   - Insert/update/remove records using prepared statements.
+3. **Model** (`BookList`)
+   - Load books from the database into memory.
+   - Expose CRUD helpers with signal emission.
+   - Provide thread-safe enqueue helpers for background imports.
+4. **Presentation** (`BookShelf`, `BookTile`)
+   - Mirror the model into a `Gio::ListStore`.
+   - Render a grid of tiles, expose activation, and filter results.
+5. **Application shell** (`BibliothecaWindow`)
+   - Build the UI chrome (header, search bar, stack pages).
+   - React to imports, search queries, and user activation.
+
+## Module documentation
+
+- [BookImport](bookimport.md)
+- [DatabaseManager](database_manager.md)
+- [BookList](booklist.md)
+- [BookShelf](bookshelf.md)
+- [BookTile](booktile.md)
+- [BibliothecaWindow](bibliotheca_window.md)
+- [SHA-256 helper](sha256.md)
+- [Entry point](main.md)
+
+## External dependencies
+
+- **GTK 4 / gtkmm**: UI toolkit for widgets, signals, and layout.
+- **SQLite3**: persistence layer for book metadata.
+- **poppler-glib**: PDF metadata/covers.
+- **libzip + tinyxml2**: EPUB metadata extraction.
+- **OpenSSL (libcrypto)**: SHA-256 hashing.
+
+## Future work
+
+- Add ADRs (`docs/decisions/`) for larger architectural choices.
+- Document additional subsystems as they appear (e.g., reader view, tagging).
+- Add screenshots or diagrams for quick onboarding.

+ 83 - 0
docs/bibliotheca_window.md

@@ -0,0 +1,83 @@
+# BibliothecaWindow Module Overview
+
+`BibliothecaWindow` is the top-level GTK window that stitches together the data
+model (`BookList`) and the UI widgets (`BookShelf`, search bar, placeholders).
+It owns the application chrome and orchestrates imports, filtering, and state
+transitions between empty / shelf / no-results views.
+
+## Responsibilities
+
+- Build the header bar (add button, search toggle) and main layout (`Gtk::Box`).
+- Manage the search UI (`Gtk::SearchBar` + `Gtk::SearchEntry`) and pipe queries
+  to `BookShelf::setFilter()`.
+- Host a `Gtk::Stack` that swaps between:
+  - Empty library placeholder.
+  - Library grid (`BookShelf`).
+  - "No results" message when filtering yields nothing.
+- Handle "Add books" action: open a file dialog, import metadata/covers on a
+  worker thread, and enqueue `BookList::upsertMany()`.
+- Emit simple debug logging for activated books (placeholder behaviour for now).
+
+## Layout overview
+
+```
+Gtk::Window (BibliothecaWindow)
+ └── Gtk::Box m_mainBox (vertical)
+     ├── Gtk::SearchBar m_searchBar
+     │    └── Gtk::SearchEntry m_searchEntry
+     └── Gtk::Stack m_stack
+          ├── Gtk::Box m_placeholder        // empty library
+          ├── BookShelf (GridView)          // main shelf view
+          └── Gtk::Box m_noResults          // "no match" placeholder
+```
+
+The header bar carries the add (+) button and a search toggle button that
+activates the search bar.
+
+## Event flow
+
+1. **Startup**: constructor calls `buildHeaderBar()`, `buildPlaceholder()`,
+   creates the shelf, search widgets, and stack, then calls `updateVisibleView()`
+   to pick the correct page based on the current library.
+2. **Search**:
+   - Clicking the search toggle (or pressing Ctrl+F) toggles the `Gtk::SearchBar`.
+   - Typing updates `m_lastQuery`, calls `BookShelf::setFilter()`, and refreshes
+     the stack (showing “no results” if the filtered shelf is empty).
+   - Leaving the search mode clears the entry.
+3. **Importing books**:
+   - `onAddBookClicked()` opens `Gtk::FileDialog::open_multiple()`.
+   - Each selected file is processed on a background thread: compute SHA-256,
+     call `import_book_assets()`, merge metadata, collect into a vector.
+   - Once done, the vector is enqueued back onto the main loop via
+     `BookList::upsertMany()`, and the UI refreshes.
+4. **Activation**: `BookShelf::signalBookActivated()` connects to
+   `BibliothecaWindow::onBookActivated()`, which currently logs a placeholder
+   message (can be extended to open a details pane or reader).
+
+## Signals & slots
+
+BibliothecaWindow subscribes to the relevant `BookList` signals to call
+`updateVisibleView()` whenever the library changes. This keeps the stack in sync
+with imports, deletions, and resets without manual intervention.
+
+## Extension points
+
+- **Actions on activation**: replace the placeholder logging with navigation to
+  a details page or opening the file.
+- **Drag-and-drop import**: wire a drop controller onto `m_mainBox` that reuses
+  the existing import flow.
+- **Search enhancements**: add filters (e.g. tags, formats) or highlight matches
+  inside the grid by extending `BookShelf`.
+
+## Expected usage
+
+`BibliothecaWindow` is the root window instantiated in `main.cpp`. Other modules
+should construct it with an existing `BookList`:
+
+```cpp
+auto window = Gtk::make_managed<BibliothecaWindow>(bookList);
+app->add_window(*window);
+```
+
+The window takes ownership of the `BookShelf` widget and manages presentation
+according to the current state of the library and active search query.

+ 75 - 0
docs/bookimport.md

@@ -0,0 +1,75 @@
+# BookImport Module Overview
+
+`BookImport` contains the format-specific logic for extracting metadata and
+covers from incoming files. It supports EPUB and PDF sources out of the box and
+returns a lightweight `ImportResult` that the rest of the pipeline uses to build
+`Book` instances.
+
+## Responsibilities
+
+- Ensure the covers cache directory exists (`~/.local/share/bibliotheca/covers`).
+- Extract title/author metadata from EPUB and PDF files.
+- Render or extract cover art, saving a PNG (or original asset) into the covers
+  directory.
+- Expose a single `import_book_assets(path, bookIdHex)` function that dispatches
+  to the correct handler based on file extension.
+
+## Structure
+
+```
+BookImport.cpp
+ ├── ensure_covers_dir()          // filesystem helper
+ ├── import_epub(...)             // libzip + tinyxml2
+ ├── import_pdf(...)              // poppler-glib + cairo
+ └── import_book_assets(...)      // public dispatcher
+```
+
+## ImportResult
+
+```cpp
+struct ImportResult {
+  std::string title;
+  std::string author;
+  std::string coverPngPath; // empty if no cover extracted
+};
+```
+
+If a format fails to provide metadata or a cover, the corresponding fields are
+left empty; the caller (usually `BibliothecaWindow`) merges these with defaults
+(e.g., falls back to the filename for the title).
+
+## EPUB pipeline
+
+1. Open the `.epub` as a ZIP archive via libzip.
+2. Parse `META-INF/container.xml` to locate the OPF package.
+3. Read the OPF document with TinyXML2, extracting `<dc:title>`, `<dc:creator>`,
+   and the cover manifest entry (`cover-image` property or meta `name="cover"`).
+4. If a cover asset exists, copy it into the covers directory (preserving the
+   original extension); otherwise leave `coverPngPath` empty.
+
+## PDF pipeline
+
+1. Open the document with Poppler (`poppler_document_new_from_file`).
+2. Pull title/author metadata using Poppler's getters.
+3. Render the first page to an ARGB32 Cairo surface scaled to ~1000px tall.
+4. Write the rendered surface to `${coversDir}/${bookId}.png`.
+
+Errors (e.g., corrupt files) throw `std::runtime_error`. Callers typically catch
+these during batch imports, log the failure, and skip the problematic file.
+
+## Extension points
+
+- **Additional formats**: add new `is_xyz()` helpers and `import_xyz()` methods,
+  then extend `import_book_assets()` to dispatch accordingly.
+- **Metadata enrichment**: augment the result with series information, tags, or
+  the table of contents if formats expose them.
+- **Cover sizing**: adjust the Cairo scale if you want smaller thumbnails.
+
+## Expected usage
+
+`BibliothecaWindow` computes a SHA-256 id for each selected file, calls
+`import_book_assets()` in a worker thread, and combines the `ImportResult` with
+fallback metadata before enqueuing `BookList::upsertMany()`.
+
+Because cover files live in a shared directory addressed by book id, repeated
+imports overwrite previous covers, ensuring consistency across sessions.

+ 91 - 0
docs/booklist.md

@@ -0,0 +1,91 @@
+# BookList Module Overview
+
+`BookList` is the in-memory model of the library. It mirrors the contents of
+SQLite (via `DatabaseManager`), exposes thread-safe APIs to mutate the list, and
+emits granular signals so UI layers (e.g. `BookShelf`) can stay synchronized
+without polling.
+
+## Responsibilities
+
+- Load the full set of books from the database into a `std::vector<Book>`.
+- Provide CRUD helpers (`upsert`, `removeById`, `upsertMany`) that keep both the
+  vector and the database in sync.
+- Broadcast change notifications (`signalBookAdded`, `signalBookUpdated`, etc.).
+- Offer `_enqueue` variants that marshal work onto the GTK main loop so helper
+  threads can safely update the model.
+
+## Structure
+
+```
+BookList
+ ├── DatabaseManager& m_database   // owned externally
+ ├── std::vector<Book> m_books     // authoritative in-memory copy
+ ├── std::mutex m_mutex            // guards access to m_books
+ └── Signals (sigc::signal<...>)   // fine-grained notifications
+```
+
+## Data flow
+
+1. **Initial load**: `loadAll()` grabs a copy of all rows via the database
+   manager, swaps them into `m_books`, and emits `signalReset()` followed by
+   `signalBulkChanged()` so views can rebuild.
+2. **Insert/Update**: `upsert()` writes the book to SQLite, then updates (or
+   appends) the matching entry in `m_books`. Depending on whether the book was
+   new, it emits `signalBookAdded()` or `signalBookUpdated()`.
+3. **Removal**: `removeById()` deletes the record from the database; if the book
+   existed in `m_books` it also erases it and emits `signalBookRemoved()`.
+4. **Bulk updates**: `upsertMany()` writes each book to the database, swaps the
+   vector entries, and finally emits `signalBulkChanged()` (useful after a batch
+   import).
+
+## Thread-safety helpers
+
+- `_enqueue` variants (`upsertEnqueue`, `removeByIdEnqueue`, `loadAllEnqueue`)
+  capture the arguments and schedule the corresponding method via
+  `Glib::signal_idle().connect_once`. This allows worker threads (e.g. import
+  jobs) to request updates without touching `Gtk` APIs off the main thread.
+- Internally, `std::mutex m_mutex` protects the vector so multiple calls don’t
+  race even if they come from different threads.
+
+## Signals
+
+| Signal                | Emitted when …                                |
+|-----------------------|-----------------------------------------------|
+| `signalBookAdded`     | A new book (id not previously seen) is added. |
+| `signalBookUpdated`   | An existing book’s metadata changes.          |
+| `signalBookRemoved`   | A book is erased by id.                       |
+| `signalReset`         | The vector is cleared / reloaded wholesale.   |
+| `signalBulkChanged`   | Large batch operations mutate the list.       |
+
+Consumers (e.g. `BookShelf`) typically connect to these to update UI widgets.
+
+## Extension points
+
+- **Metadata fields**: When new columns are added to `Book`, extend the CRUD
+  methods and signal payloads accordingly.
+- **Sorting / ordering**: `m_books` preserves insertion order; consumers are free
+  to sort if needed (e.g. `BookShelf`’s fuzzy filter reorders matches).
+- **Persistence layer**: swapping the backend (e.g. to a REST API) would mean
+  replacing `DatabaseManager` calls but the public `BookList` API could remain
+  stable.
+
+## Expected usage
+
+Typical lifecycle inside the app:
+
+```cpp
+BookList m_bookList(database);
+m_bookList.loadAll();
+m_bookList.signalBookAdded().connect(...);
+```
+
+Worker thread example:
+
+```cpp
+std::thread([&list, book] {
+  list.upsertEnqueue(book);  // safe from any thread
+}).detach();
+```
+
+UI code listens to the signals to stay synchronized, while background import
+jobs call the `_enqueue` helpers to avoid GTK threading violations.

+ 103 - 0
docs/bookshelf.md

@@ -0,0 +1,103 @@
+# BookShelf Module Overview
+
+The `BookShelf` module is the bridge between the data model (`BookList`) and the
+visual grid of book tiles. It packages a full GTK widget tree that can be
+embedded into any window: the widget takes care of subscribing to model
+updates, populating the `Gtk::GridView`, handling selection state, and exposing
+activation + filtering hooks back to the caller.
+
+## Responsibilities
+
+- Mirror the in-memory book collection into a `Gio::ListStore` suitable for a
+  `Gtk::GridView`.
+- Build and bind `BookTile` widgets on demand using a `Gtk::SignalListItemFactory`.
+- Provide user interaction glue:
+  - Emits `signalBookActivated()` when a tile is clicked.
+  - Tracks selection state via `Gtk::SingleSelection`.
+  - Clears selection when the user clicks outside the grid or moves the mouse
+    into empty space.
+- Implement a lightweight fuzzy filter so the grid only shows matches for the
+  active query.
+
+## Structure
+
+```
+BookShelf
+ ├── Gio::ListStore<BookObject>   // backing data model
+ ├── Gtk::SingleSelection         // selection controller passed to the grid
+ ├── Gtk::GridView                // renders BookTile cells
+ ├── Gtk::SignalListItemFactory   // setup / bind / unbind callbacks
+ ├── BookObject                   // small wrapper storing Book for the model
+ └── Filtering helpers            // scoring & refresh logic
+```
+
+### BookObject wrapper
+
+`Gio::ListStore` expects items that derive from `Glib::Object`. `BookObject`
+wraps a `Book` so we can put books inside the GTK list store while still
+using value semantics when refreshing the grid.
+
+## Data flow
+
+1. Construction:
+   - `BookShelf` creates the list store, selection model, signal factory, and
+     sets up the GridView.
+   - Background gestures/controllers are added so clicking empty space clears
+     the selection.
+
+2. Synchronisation:
+   - `reload()`, `addTile()`, `updateTile()`, and `removeTile()` all forward to
+     `refreshVisible()`, which rebuilds the list store from `BookList` (using
+     the current filter, if any).
+   - `BookList` signals are connected to these entry points in the constructor
+     so UI stays in sync with database changes.
+
+3. Rendering:
+   - `onFactorySetup()` creates a `BookTile` and stores it in each `Gtk::ListItem`.
+   - `onFactoryBind()` pulls the corresponding `BookObject` from the store and
+     feeds it into the tile via `BookTile::setBook()`.
+   - `onFactoryUnbind()` resets the tile to a blank Book so stale metadata isn’t
+     shown when the item is recycled.
+
+4. Interaction:
+   - When the grid emits `signal_activate(position)`, the selection model is
+     updated and `signalBookActivated()` is forwarded to the outside world.
+   - Background gestures reset the selection so keyboard focus isn’t stuck.
+
+## Filtering algorithm
+
+`setFilter(query)` stores a lowercase copy of the query and calls
+`refreshVisible()`. The refresh routine:
+
+1. Clears the current selection and list store.
+2. If the query is empty, re-populates the grid with every book.
+3. Otherwise evaluates a fuzzy score for each book using:
+   - substring match (higher score for earlier matches), and
+   - subsequence match (reward tightly clustered characters).
+4. Keeps only books with a non-negative score, sorts descending by score, and
+   repopulates the store so the grid shows the ranked results.
+
+The helper functions `ascii_lower`, `substring_score`, `subsequence_score`, and
+`book_score` live inside the module to keep the fuzzy logic confined.
+
+## Extension points
+
+- **Custom tile content**: tweak `BookTile` (e.g., add badges or context menus);
+  the factory setup already exposes the widget instance.
+- **Filtering strategy**: adjust scoring or add new fields (tags, notes) inside
+  `book_score()`.
+- **Selection behaviour**: change the background event controllers if you want
+  multi-select or persistent selection.
+
+## Expected usage
+
+The module is designed to be embedded like so:
+
+```cpp
+m_shelf = std::make_unique<BookShelf>(m_bookList, 180);
+m_shelf->signalBookActivated().connect(...);
+container.append(*m_shelf);
+```
+
+Call `setFilter(query)` from your search UI, and `reload()` if you ever perform
+bulk operations that bypass the standard BookList signals.

+ 60 - 0
docs/booktile.md

@@ -0,0 +1,60 @@
+# BookTile Module Overview
+
+`BookTile` is the compact widget that displays a single book inside the shelf
+grid. Each tile shows the cover art (if available) and a wrapped title label,
+and emits a signal when the user clicks it.
+
+## Responsibilities
+
+- Render a thumbnail using `Gtk::Picture`, loading a `Gdk::Texture` from the
+  `Book` when needed.
+- Display a short title (falling back to the file name if the book has no title).
+- Provide a click gesture (`Gtk::GestureClick`) that triggers
+  `signalActivated()` for the parent view to handle.
+- Respect layout hints (size request, alignment) so `BookShelf` can pack multiple
+  tiles per row seamlessly.
+
+## Structure
+
+```
+BookTile : Gtk::Box (vertical)
+ ├── Gtk::Picture m_cover
+ └── Gtk::Label   m_title
+```
+
+Gestures and styling are applied to the box itself so the grid behind it can
+control selection appearance.
+
+## Key methods
+
+- `BookTile::setBook(const Book&)`: updates internal state, refreshes the cover
+  texture and title.
+- `BookTile::rebuild()`: helper that (re)loads the cover if missing and updates
+  the label text.
+- `signalActivated()`: forward this signal to react when the tile is clicked.
+
+## Styling expectations
+
+`BookShelf` adds CSS classes (`book-tile`) and wraps each tile inside a
+`GridView` child. To keep a consistent grid:
+
+- The constructor calls `set_size_request(m_coverSize + 16, -1)` so each tile has
+  a predictable width for column calculations.
+- Margins and spacing are kept small; the grid’s CSS handles selection borders.
+
+## Extension points
+
+- Add badges/metadata: append extra widgets (e.g., author label, progress bar)
+  to the box.
+- Context menus: attach a `Gtk::GestureClick` with button 3 or a `Gtk::Popover`.
+- Custom selection visuals: override the CSS class or expose a property that the
+  grid can toggle.
+
+## Expected usage
+
+`BookShelf` creates `BookTile` instances inside `onFactorySetup()`, binds them to
+`BookObject` data during `onFactoryBind()`, and listens to `signalActivated()` to
+emit its own `signalBookActivated()`.
+
+If other modules need to render books (e.g., a details sidebar), they can reuse
+`BookTile` directly or subclass it with additional controls.

+ 79 - 0
docs/database_manager.md

@@ -0,0 +1,79 @@
+# DatabaseManager Module Overview
+
+`DatabaseManager` is the low-level persistence layer for Bibliotheca. It wraps a
+SQLite connection, owns prepared statements, and provides a narrow set of CRUD
+methods that the higher-level `BookList` uses to keep the database in sync.
+
+## Responsibilities
+
+- Open / close the SQLite database (`sqlite3_open` / `sqlite3_close`).
+- Create the schema on first run (`ensure_schema`).
+- Expose prepared insert/update/delete/select helpers for `Book` records.
+- Provide a busy handler so concurrent access backs off politely.
+- Guard SQLite calls with a mutex so callers can safely use the manager across
+  threads.
+
+## Structure
+
+```
+DatabaseManager
+ ├── std::string db_path_   // path to the .sqlite file
+ ├── sqlite3* db_           // raw SQLite handle (owned)
+ └── std::mutex mtx_        // serialises public API calls
+```
+
+All statements are prepared lazily by `prepare_statements()` and reused for each
+operation.
+
+## Schema
+
+`ensure_schema()` creates a single table named `books`:
+
+```sql
+CREATE TABLE IF NOT EXISTS books (
+  id TEXT PRIMARY KEY,
+  title TEXT,
+  author TEXT,
+  file_path TEXT,
+  cover_path TEXT
+);
+```
+
+Additional indices or columns should be added here, keeping in mind that the
+manager is the canonical place to evolve storage.
+
+## Public API
+
+| Method                | Description                                   |
+|-----------------------|-----------------------------------------------|
+| `ensure_schema()`     | Creates tables if they do not exist.          |
+| `upsert_book(book)`   | INSERT OR REPLACE using the book’s id.        |
+| `remove_book(id)`     | Deletes the row; returns false if not present.|
+| `get_book(id)`        | Loads a single book (optional result).        |
+| `load_all_books()`    | Returns a vector of all books.                |
+
+Internally each method locks `mtx_`, executes the prepared statement, and
+translates SQLite rows into `Book` objects.
+
+## Busy handling & thread safety
+
+SQLite is file-based and can momentarily return `SQLITE_BUSY` if another thread
+or process holds a lock. The static `busy_handler` callback retries with an
+exponential backoff. Because all public methods take the mutex, only one thread
+issues SQL commands at a time, simplifying concurrency.
+
+## Extension points
+
+- **Schema migrations**: add ALTER TABLE logic inside `ensure_schema()` and keep
+  the `Book` serialization/deserialization in sync.
+- **Additional queries**: expose specialised getters (e.g., search by author) if
+  higher layers start to need them.
+- **Alternative backends**: swapping SQLite for another storage engine would
+  mean re-implementing this module while keeping `BookList` untouched.
+
+## Expected usage
+
+`BookList` owns a single `DatabaseManager` instance, calls `ensure_schema()` on
+startup, and uses the CRUD helpers whenever a book changes. Other modules should
+not talk to SQLite directly; instead they should route all persistence through
+this manager to keep logic centralised.

+ 16 - 0
docs/main.md

@@ -0,0 +1,16 @@
+# Application Entry Point (`main.cpp`)
+
+`main.cpp` bootstraps GTK, initialises the data model, and launches the main
+window. This module is intentionally thin—most logic lives in dedicated classes.
+
+## Workflow
+
+1. Initialise GTK via `Gtk::Application::create()`.
+2. Construct `DatabaseManager`, call `ensure_schema()`.
+3. Instantiate `BookList` with the database manager and load the existing books.
+4. Create `BibliothecaWindow` (passing the `BookList` reference) and add it to
+the application.
+5. Enter the GTK main loop via `app->run(window)`.
+
+The entry point is also the place to install global actions/startup handlers if
+you expand the app (e.g., register keyboard shortcuts or file-open hooks).

+ 22 - 0
docs/sha256.md

@@ -0,0 +1,22 @@
+# Sha256 Utility Overview
+
+`Sha256.cpp` provides a small helper to compute the SHA-256 digest of a file.
+The function is used during import to assign a stable, unique identifier for
+books; this id becomes the primary key in SQLite and the filename for cached
+covers.
+
+## Responsibilities
+
+- Stream a file from disk.
+- Feed the bytes through OpenSSL’s SHA-256 implementation.
+- Return the digest as a lowercase hexadecimal string.
+
+## Expected usage
+
+```cpp
+std::string digest = sha256_file(path);
+```
+
+The caller is responsible for handling I/O errors (the helper throws if the file
+cannot be read). Since the digest is deterministic, re-importing the same file
+produces the same id, allowing updates to merge cleanly.