diff options
| author | Joris Guyonvarch | 2025-12-26 18:41:26 +0100 |
|---|---|---|
| committer | Joris Guyonvarch | 2025-12-27 20:41:44 +0100 |
| commit | a110c200e86d2325af07167531fac0f61d9681a0 (patch) | |
| tree | 90e843f915a2e153ba735849afd83710d90560bf /src/ui | |
| parent | a26d92ad5055fa057647158eb79511e7b1841162 (diff) | |
Switch to GUI to manage the library
Allow to regroup the CLI and the view into one unique tool.
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/__init__.py | 0 | ||||
| -rw-r--r-- | src/ui/book_entries.py | 95 | ||||
| -rw-r--r-- | src/ui/cover_entry.py | 91 | ||||
| -rw-r--r-- | src/ui/entry_list.py | 42 |
4 files changed, 228 insertions, 0 deletions
diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/ui/__init__.py diff --git a/src/ui/book_entries.py b/src/ui/book_entries.py new file mode 100644 index 0000000..edd9457 --- /dev/null +++ b/src/ui/book_entries.py @@ -0,0 +1,95 @@ +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, Gio, GLib +import pathlib +import os +import subprocess + +import src.utils as utils + +class BookEntries(Gtk.Box): + + def __init__(self, window): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=10) + self._window = window + + self._books = {} # Dict {path: name} + + header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + header.append(get_label('Books')) + add_button = Gtk.Button(label='+') + add_button.connect('clicked', lambda _: self._open_dialog()) + header.append(add_button) + self.append(header) + + self._entries = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.append(self._entries) + + self._setup_filedialog() + + def get_books(self): + return self._books + + def _setup_filedialog(self): + self._filedialog = Gtk.FileDialog.new() + self._filedialog.set_title('Select a book') + + f = Gtk.FileFilter() + f.set_name('Book files') + f.add_mime_type('application/epub+zip') + f.add_mime_type('application/vnd.amazon.ebook') + + filters = Gio.ListStore.new(Gtk.FileFilter) + filters.append(f) + + self._filedialog.set_filters(filters) + self._filedialog.set_default_filter(f) + + def _open_dialog(self): + self._filedialog.open(self._window, None, self._open_dialog_callback) + + def _open_dialog_callback(self, dialog, result): + try: + file = dialog.open_finish(result) + if file is not None: + # https://github.com/GNOME/glib/blob/main/gio/glocalfile.c + path = pathlib.Path(file.get_path()) + self.add_book(path) + except GLib.Error as error: + print(f'Error opening file: {error.message}') + + def add_book(self, path): + name = os.path.splitext(os.path.basename(path))[0] + self._books[path] = name + + line = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + entry = Gtk.Entry() + entry.set_text(name) + entry.set_hexpand(True) + entry.connect('changed', self._update_book, path) + line.append(entry) + line.append(open_book_button(path)) + remove_button = Gtk.Button(label='-') + remove_button.connect('clicked', lambda _: self._remove_book(line, path)) + line.append(remove_button) + self._entries.append(line) + + def _update_book(self, entry, path): + self._books[path] = entry.get_text() + + def _remove_book(self, line, path): + del self._books[path] + self._entries.remove(line) + +def get_label(text): + label = Gtk.Label() + label.set_text(text) + return label + +def open_book_button(path): + open_button = Gtk.Button(label='Ouvrir') + open_button.connect('clicked', lambda _: open_book(path)) + return open_button + +def open_book(path): + subprocess.run(['xdg-open', path]) diff --git a/src/ui/cover_entry.py b/src/ui/cover_entry.py new file mode 100644 index 0000000..8f73a01 --- /dev/null +++ b/src/ui/cover_entry.py @@ -0,0 +1,91 @@ +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +import numpy +from PIL import Image +import requests +import io +import threading +import time +import logging + +import src.utils as utils + +logger = logging.getLogger(__name__) + +class CoverEntry(Gtk.Box): + + def __init__(self, parent_window, resources, filename=None): + Gtk.Box.__init__(self) + + self._parent_window = parent_window + + self._image = Image.open(filename or f'{resources}/blank-cover.png') + self._picture = Gtk.Picture.new_for_pixbuf(utils.image_to_pixbuf(self._image)) + self.append(self._picture) + + gesture_click = Gtk.GestureClick() + gesture_click.connect('released', self._on_open_dialog) + self._picture.add_controller(gesture_click) + + def get_image(self): + return self._image + + def _on_open_dialog(self, gesture, n_press, x, y): + self._dialog = Gtk.Window() + utils.configure_dialog(self._dialog, self._parent_window, 'Couverture', height=None) + + self._dialog_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + self._dialog.set_child(self._dialog_box) + utils.set_margin(self._dialog_box, 20) + self._error_message = None + + self._dialog_box.append(utils.label('URL')) + + self._url_entry = Gtk.Entry() + self._dialog_box.append(self._url_entry) + + self._append_download_button() + + self._dialog.present() + + def _on_download_cover(self): + if(self._url_entry.get_text()): + thread = threading.Thread(target=self._download_cover_daemon) + thread.daemon = True + thread.start() + self._dialog_spinner = Gtk.Spinner() + self._dialog_spinner.start() + self._dialog_box.append(self._dialog_spinner) + self._dialog_box.remove(self._dialog_button) + if self._error_message: + self._dialog_box.remove(self._error_message) + + def _download_cover_daemon(self): + try: + self._image = download_image(self._url_entry.get_text()) + self._picture.set_pixbuf(utils.image_to_pixbuf(self._image)) + GLib.idle_add(self._complete_download) + except Exception as e: + logger.error('Failed downloading cover %s: %s', self._url_entry, e) + self._dialog_spinner.stop() + self._dialog_box.remove(self._dialog_spinner) + self._error_message = utils.label(f'{e}') + self._dialog_box.append(self._error_message) + self._append_download_button() + + def _append_download_button(self): + self._dialog_button = Gtk.Button(label='Télécharger') + self._dialog_button.connect('clicked', lambda _: self._on_download_cover()) + self._dialog_box.append(self._dialog_button) + + def _complete_download(self): + self._dialog.close() + +def download_image(url): + response = requests.get(url, headers={ 'User-Agent': 'python-script' }) + image = Image.open(io.BytesIO(response.content)) + width, height = image.size + if width > 400: + image = image.resize((400, int(400 * height / width)), Image.LANCZOS) + return image diff --git a/src/ui/entry_list.py b/src/ui/entry_list.py new file mode 100644 index 0000000..c0c355d --- /dev/null +++ b/src/ui/entry_list.py @@ -0,0 +1,42 @@ +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +import src.utils as utils + +class EntryList(Gtk.Box): + + def __init__(self, name): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=10) + + header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + header.append(get_label(name)) + add_button = Gtk.Button(label='+') + add_button.connect('clicked', lambda _: self.add_entry()) + header.append(add_button) + self.append(header) + + self._entries = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.append(self._entries) + + def get_entry_texts(self): + res = [] + for entry in self._entries: + res.append(entry.get_first_child().get_buffer().get_text()) + return res + + def add_entry(self, value=''): + line = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + entry = Gtk.Entry() + entry.set_text(value) + entry.set_hexpand(True) + line.append(entry) + remove_button = Gtk.Button(label='-') + remove_button.connect('clicked', lambda _: self._entries.remove(line)) + line.append(remove_button) + self._entries.append(line) + +def get_label(text): + label = Gtk.Label() + label.set_text(text) + return label |
