aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris Guyonvarch2025-12-29 17:35:51 +0100
committerJoris Guyonvarch2025-12-29 17:35:51 +0100
commit31d87c86ce2a5f2a8f8aa1d404e39a093e975da7 (patch)
tree3f5e7f20544c3c05137e58803ac24973346d458c
parent64be629f81dc30f865d84958fc3f68a6217e6482 (diff)
Allow filtering by genre
-rw-r--r--README.md1
-rw-r--r--src/application.py45
-rw-r--r--src/book_flow.py52
-rw-r--r--src/book_form.py6
-rw-r--r--src/filters.py63
-rw-r--r--src/main_window.py71
-rw-r--r--src/models.py14
-rw-r--r--src/picture_cache.py3
8 files changed, 157 insertions, 98 deletions
diff --git a/README.md b/README.md
index d6c9651..314b40b 100644
--- a/README.md
+++ b/README.md
@@ -22,4 +22,3 @@ pytest
- filters:
- textual search
https://stackoverflow.com/questions/55828169/how-to-filter-gtk-flowbox-children-with-gtk-entrysearch
- - select by genre
diff --git a/src/application.py b/src/application.py
index 46ab901..24c2a9a 100644
--- a/src/application.py
+++ b/src/application.py
@@ -2,51 +2,10 @@
# https://github.com/Taiko2k/GTK4PythonTutorial/blob/main/README.md#adding-your-custom-css-stylesheet
import gi
-gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
-from gi.repository import Gtk, Adw
+from gi.repository import Adw
-from src.book_flow import BookFlow
-from src.filters import Filters
-from src.book_form import BookForm
-import src.utils as utils
-
-class MainWindow(Gtk.ApplicationWindow):
- def __init__(self, resources, library, ereader, conn, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- utils.set_header_bar(self, 'Books')
-
- scrolled_window = Gtk.ScrolledWindow()
- self.set_child(scrolled_window)
-
- init_progress = 'Reading'
-
- add_book_button = Gtk.Button(label='Ajouter un livre')
- add_book_button.connect('clicked', lambda _: BookForm(self, resources, library, conn, self._filters.get_progress(), self._on_book_added).present())
-
- header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
- utils.set_margin(header, 20)
- self._filters = Filters(init_progress, self._update_book_flow_progress)
- self._filters.set_hexpand(True)
- header.append(self._filters)
- header.append(add_book_button)
-
- box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
- box.append(header)
- self._book_flow = BookFlow(self, resources, library, ereader, conn, init_progress, self._update_filters_progress)
- box.append(self._book_flow)
- scrolled_window.set_child(box)
-
- def _update_book_flow_progress(self, progress):
- self._book_flow.update_progress(progress)
-
- def _update_filters_progress(self, progress):
- self._filters.update_progress(progress)
-
- def _on_book_added(self, book_id, data):
- self._update_filters_progress(data['progress'])
- self._book_flow.add_book(book_id, data)
+from src.main_window import MainWindow
class Application(Adw.Application):
def __init__(self, resources, library, ereader, conn, **kwargs):
diff --git a/src/book_flow.py b/src/book_flow.py
index ff77616..37f4866 100644
--- a/src/book_flow.py
+++ b/src/book_flow.py
@@ -9,6 +9,7 @@ import subprocess
import src.utils as utils
import src.db as db
+import src.models as models
from src.book_detail import BookDetail
from src.book_delete import BookDelete
from src.book_form import BookForm
@@ -18,7 +19,7 @@ import src.book_files as book_files
class BookFlow(Gtk.FlowBox):
- def __init__(self, window, resources, library, ereader, conn, progress, update_filter_progress):
+ def __init__(self, window, resources, library, ereader, conn, books, progress, genre, msg):
Gtk.FlowBox.__init__(self)
self._window = window
@@ -26,29 +27,23 @@ class BookFlow(Gtk.FlowBox):
self._library = library
self._ereader = ereader
self._conn = conn
- self._progress = progress
- self._update_filter_progress = update_filter_progress
+ self._msg = msg
self._picture_cache = PictureCache()
- self._books = db.get_books(self._conn)
- self._reset_view()
+ self.reset(books, progress, genre)
- def add_book(self, book_id, data):
- self._books[book_id] = data
- self.update_progress(data['progress'])
- self.select_child(self._flow_box_children[book_id])
+ def select_book(self, book_id):
+ if book_id in self._flow_box_children:
+ self.select_child(self._flow_box_children[book_id])
- def update_progress(self, progress):
- self._progress = progress
- self._reset_view()
+ def reset(self, books, progress, genre, book_id=None):
+ if book_id:
+ self._picture_cache.invalidate(f'{self._library}/{book_id}/cover-min.png')
- # Private
-
- def _reset_view(self):
self.remove_all()
self._flow_box_children = {}
- for book_id, data in sorted(self._books.items(), key=book_sort):
- if data['progress'] == self._progress:
+ for book_id, data in sorted(books.items(), key=book_sort):
+ if self._is_selected(data, progress, genre):
picture = self._picture_cache.get(f'{self._library}/{book_id}/cover-min.png')
picture.set_can_shrink(False)
@@ -66,6 +61,17 @@ class BookFlow(Gtk.FlowBox):
self._flow_box_children[book_id] = flow_box_child
self.append(flow_box_child)
+ # Private
+
+ def _is_selected(self, data, progress, genre):
+ if data['progress'] != progress:
+ return False
+ if genre == models.all_genres:
+ return True
+ if genre == models.no_genre:
+ return len(data['genres']) == 0
+ return genre in data['genres']
+
def _on_left_click(self, gesture, n_press, x, y, book_id, data):
if n_press == 2:
self._open_detail(book_id, data)
@@ -103,23 +109,15 @@ class BookFlow(Gtk.FlowBox):
def _update_book(self, book_id, data):
book = {'id': book_id, 'data': data }
- BookForm(self._window, self._resources, self._library, self._conn, data['progress'], self._on_book_updated, book).present()
-
- def _on_book_updated(self, book_id, data):
- self._picture_cache.invalidate(f'{self._library}/{book_id}/cover-min.png')
- self._books[book_id] = data
- self.update_progress(data['progress']) # Note: this redraws everything, this is overkill in some cases
- self._update_filter_progress(data['progress'])
- self.select_child(self._flow_box_children[book_id])
+ BookForm(self._window, self._resources, self._library, self._conn, data['progress'], self._msg, book).present()
def _confirm_delete_book(self, book_id, data):
BookDelete(self._window, self._library, book_id, data, lambda: self._delete_book(book_id, data)).present()
def _delete_book(self, book_id, data):
- del self._books[book_id]
db.delete_book(self._conn, book_id)
shutil.rmtree(f'{self._library}/{book_id}', ignore_errors=True)
- self._reset_view()
+ self._msg(['book-deleted', book_id, data])
def _transfer_book(self, book_id, data, paths):
BookTransfer(self._window, self._ereader, book_id, data, paths).present()
diff --git a/src/book_form.py b/src/book_form.py
index cff4907..baabe58 100644
--- a/src/book_form.py
+++ b/src/book_form.py
@@ -13,13 +13,13 @@ import src.str_format as str_format
class BookForm(Gtk.Window):
- def __init__(self, parent_window, resources, library, conn, init_progress, on_book_saved, book = None):
+ def __init__(self, parent_window, resources, library, conn, init_progress, msg, book = None):
Gtk.Window.__init__(self)
self._book = book
self._library = library
self._conn = conn
- self._on_book_saved = on_book_saved
+ self._msg = msg
title = 'Modifier un livre' if book else 'Ajouter un livre'
utils.configure_dialog(self, parent_window, title, height=800)
@@ -125,7 +125,7 @@ class BookForm(Gtk.Window):
book_id = book_store.store(self._library, self._conn, data, cover, books, self._book, self._remove_book_sources.get_active())
if book_id:
self.close()
- self._on_book_saved(book_id, data)
+ self._msg(['book-saved', book_id, data])
def non_empty(xs):
return [x.strip() for x in xs if x.strip()]
diff --git a/src/filters.py b/src/filters.py
index f62eee1..e81c3cf 100644
--- a/src/filters.py
+++ b/src/filters.py
@@ -2,30 +2,47 @@ import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
-import src.utils as utils
import src.models as models
class Filters(Gtk.Box):
- def __init__(self, init_progress, on_progress_updated):
- Gtk.Box.__init__(self)
-
- self._progress = init_progress
- self._on_progress_updated = on_progress_updated
-
- self._dropdown = Gtk.DropDown.new_from_strings(models.all_progress)
- self._dropdown.set_selected(models.all_progress.index(init_progress))
- self._dropdown.connect('notify::selected-item', self._on_selected_item)
-
- self.append(self._dropdown)
-
- def get_progress(self):
- return self._progress
-
- def update_progress(self, progress):
- self._progress = progress
- self._dropdown.set_selected(models.all_progress.index(progress))
-
- def _on_selected_item(self, _dropdown, _data):
- self._progress = self._dropdown.get_selected_item().get_string()
- self._on_progress_updated(self._progress)
+ def __init__(self, init_books, progress, genres, genre, msg):
+ Gtk.Box.__init__(self, spacing=10)
+
+ self._msg = msg
+
+ self._progress_dropdown = Gtk.DropDown.new_from_strings(models.all_progress)
+ self._progress_dropdown.set_selected(models.all_progress.index(progress))
+ self._progress_dropdown.connect('notify::selected-item', self._on_selected_progress)
+ self.append(self._progress_dropdown)
+
+ self._genres_dropdown = self._get_genres_dropdown(genres, genre)
+ self.append(self._genres_dropdown)
+
+ def select_progress(self, progress):
+ self._progress_dropdown.set_selected(models.all_progress.index(progress))
+
+ # Hacky, this replaces the dropdown by a new one
+ def actualize_genres(self, genres, genre):
+ if self._last_installed_genres != genres:
+ new_dropdown = self._get_genres_dropdown(genres, genre)
+ self.insert_child_after(new_dropdown, self._genres_dropdown)
+ self.remove(self._genres_dropdown)
+ self._genres_dropdown = new_dropdown
+ else:
+ self._genres_dropdown.set_selected(genres.index(genre))
+
+ def _get_genres_dropdown(self, genres, genre):
+ self._last_installed_genres = genres
+ dropdown = Gtk.DropDown.new_from_strings(genres)
+ dropdown.set_selected(genres.index(genre))
+ dropdown.connect('notify::selected-item', self._on_selected_genre)
+ return dropdown
+
+ def _on_selected_progress(self, dropdown, _data):
+ progress = dropdown.get_selected_item().get_string()
+ self._msg(['progress-selected', progress])
+
+ def _on_selected_genre(self, dropdown, _data):
+ genre = dropdown.get_selected_item().get_string()
+ self._msg(['genre-selected', genre])
diff --git a/src/main_window.py b/src/main_window.py
new file mode 100644
index 0000000..420b4ea
--- /dev/null
+++ b/src/main_window.py
@@ -0,0 +1,71 @@
+# To add CSS
+# https://github.com/Taiko2k/GTK4PythonTutorial/blob/main/README.md#adding-your-custom-css-stylesheet
+
+import gi
+gi.require_version('Gtk', '4.0')
+from gi.repository import Gtk
+
+from src.book_flow import BookFlow
+from src.filters import Filters
+from src.book_form import BookForm
+import src.utils as utils
+import src.db as db
+import src.models as models
+
+class MainWindow(Gtk.ApplicationWindow):
+ def __init__(self, resources, library, ereader, conn, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ utils.set_header_bar(self, 'Books')
+
+ # State
+ self._books = db.get_books(conn)
+ self._progress = 'Reading'
+ self._genres = models.get_genres(self._books)
+ self._genre = models.all_genres
+
+ scrolled_window = Gtk.ScrolledWindow()
+ self.set_child(scrolled_window)
+
+ add_book_button = Gtk.Button(label='Ajouter un livre')
+ add_book_button.connect('clicked', lambda _: BookForm(self, resources, library, conn, self._progress, self._msg).present())
+
+ header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ utils.set_margin(header, 20)
+ self._filters = Filters(self._books, self._progress, self._genres, self._genre, self._msg)
+ self._filters.set_hexpand(True)
+ header.append(self._filters)
+ header.append(add_book_button)
+
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
+ box.append(header)
+ self._book_flow = BookFlow(self, resources, library, ereader, conn, self._books, self._progress, self._genre, self._msg)
+ box.append(self._book_flow)
+ scrolled_window.set_child(box)
+
+ def _msg(self, msg):
+ match msg:
+ case ['progress-selected', progress]:
+ self._progress = progress
+ self._book_flow.reset(self._books, self._progress, self._genre)
+ case ['genre-selected', genre]:
+ self._genre = genre
+ self._book_flow.reset(self._books, self._progress, self._genre)
+ case ['book-saved', book_id, data]:
+ self._books[book_id] = data
+ self._progress = data['progress']
+ self._filters.select_progress(data['progress'])
+ if not self._genre in data['genres']:
+ self._genre = models.all_genres
+ self._actualize_genres()
+ self._book_flow.reset(self._books, self._progress, self._genre, book_id)
+ self._book_flow.select_book(book_id)
+ case ['book-deleted', book_id, data]:
+ del self._books[book_id]
+ self._actualize_genres()
+ self._book_flow.reset(self._books, self._progress, self._genre)
+
+ def _actualize_genres(self):
+ self._genres = models.get_genres(self._books)
+ self._genre = self._genre if self._genre in self._genres else models.all_genres
+ self._filters.actualize_genres(self._genres, self._genre)
diff --git a/src/models.py b/src/models.py
index d271b62..0297551 100644
--- a/src/models.py
+++ b/src/models.py
@@ -1,2 +1,16 @@
all_progress = ['Unread', 'Reading', 'Read', 'Stopped']
langs = ['fr', 'en', 'de']
+
+# Genres
+
+all_genres = 'Tous les genres'
+no_genre = 'Aucun genre'
+
+def get_genres(books):
+ genres = {all_genres}
+ for data in books.values():
+ for genre in data['genres']:
+ genres.add(genre)
+ if len(data['genres']) == 0:
+ genres.add(no_genre)
+ return list(genres)
diff --git a/src/picture_cache.py b/src/picture_cache.py
index 8da136b..af4e1bb 100644
--- a/src/picture_cache.py
+++ b/src/picture_cache.py
@@ -22,4 +22,5 @@ class PictureCache:
def invalidate(self, path):
logger.debug('Invalidating: %s', path)
- del self._cache[path]
+ if path in self._cache:
+ del self._cache[path]