aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorJoris Guyonvarch2025-12-26 18:41:26 +0100
committerJoris Guyonvarch2025-12-27 20:41:44 +0100
commita110c200e86d2325af07167531fac0f61d9681a0 (patch)
tree90e843f915a2e153ba735849afd83710d90560bf /src/ui
parenta26d92ad5055fa057647158eb79511e7b1841162 (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__.py0
-rw-r--r--src/ui/book_entries.py95
-rw-r--r--src/ui/cover_entry.py91
-rw-r--r--src/ui/entry_list.py42
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