diff --git a/.gitignore b/.gitignore index 67314e1d..89aa95db 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ venv/ *.log *.prof *.pyc + +.DS_Store diff --git a/README.md b/README.md index f8c06287..f202df48 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,8 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for detailed instructions and developing Co ## Requirements - `python3` - `meson >= 0.40.0` as build system -- `gtk3 >= 3.22` -- `libhandy >= 1.0.0` -- `libdazzle >= 3.34.0` +- `gtk4 >= 4.10` +- `libadwaita >= 1.4.0` - `peewee >= 3.9.6` as object relation mapper - `mutagen` for meta tag management - `distro` @@ -165,6 +164,7 @@ To the contributors on GitHub: - paper42 - phpwutz - rapenne-s +- rdbende - thibaultamartin - umeboshi2 - worldofpeace @@ -277,9 +277,9 @@ The translators: To nedrichards for the Flatpak. ## Help me translate cozy! -Cozy is on Transifex, where anyone can contribute and translate. Can't find your language in the list? Let me know! +Cozy is on Transifex, where anyone can contribute and translate. Can't find your language in the list? Let me know! -If you like this project, consider supporting me on Patreon :) +If you like this project, consider supporting me on Patreon :) ---- [![Maintainability](https://api.codeclimate.com/v1/badges/fde8cbdff23033adaca2/maintainability)](https://codeclimate.com/github/geigi/cozy/maintainability) diff --git a/com.github.geigi.cozy.json b/com.github.geigi.cozy.json index dac90784..f8521006 100644 --- a/com.github.geigi.cozy.json +++ b/com.github.geigi.cozy.json @@ -1,7 +1,7 @@ { "app-id": "com.github.geigi.cozy", "runtime": "org.gnome.Platform", - "runtime-version": "42", + "runtime-version": "45", "sdk": "org.gnome.Sdk", "command": "com.github.geigi.cozy", "finish-args": [ @@ -130,37 +130,6 @@ } ] }, - { - "name": "libhandy", - "buildsystem": "meson", - "config-opts": [ - "-Dprofiling=false", - "-Dintrospection=enabled", - "-Dgtk_doc=false", - "-Dtests=false", - "-Dexamples=false", - "-Dvapi=false", - "-Dglade_catalog=disabled" - ], - "sources": [ - { - "type": "git", - "url": "https://gitlab.gnome.org/GNOME/libhandy", - "tag": "1.7.90" - } - ] - }, - { - "name": "libdazzle", - "buildsystem": "meson", - "sources": [ - { - "type": "git", - "url": "https://gitlab.gnome.org/GNOME/libdazzle.git", - "tag": "3.44.0" - } - ] - }, { "name": "cozy", "buildsystem": "meson", @@ -172,4 +141,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/cozy.doap b/cozy.doap index 38b2fd39..64b07bdd 100644 --- a/cozy.doap +++ b/cozy.doap @@ -8,8 +8,8 @@ Listen to audio books Python - GTK 3 - Libhandy + GTK 4 + Libadwaita diff --git a/cozy/app_controller.py b/cozy/app_controller.py index 4458cae9..ec1fbd53 100644 --- a/cozy/app_controller.py +++ b/cozy/app_controller.py @@ -21,7 +21,7 @@ from cozy.ui.app_view import AppView from cozy.ui.book_detail_view import BookDetailView from cozy.ui.headerbar import Headerbar -from cozy.ui.info_banner import InfoBanner +from cozy.ui.toaster import ToastNotifier from cozy.ui.library_view import LibraryView from cozy.ui.main_view import CozyUI from cozy.ui.media_controller import MediaController @@ -105,7 +105,7 @@ def configure_inject(self, binder): binder.bind_to_constructor(SleepTimerViewModel, lambda: SleepTimerViewModel()) binder.bind_to_constructor(GstPlayer, lambda: GstPlayer()) binder.bind_to_constructor(PowerManager, lambda: PowerManager()) - binder.bind_to_constructor(InfoBanner, lambda: InfoBanner()) + binder.bind_to_constructor(ToastNotifier, lambda: ToastNotifier()) binder.bind_to_constructor(AppViewModel, lambda: AppViewModel()) binder.bind_to_constructor(SettingsViewModel, lambda: SettingsViewModel()) @@ -125,9 +125,6 @@ def open_library(self): self.library_view_model.open_library() self.app_view_model.view = View.LIBRARY_FILTER - def navigate_back(self): - self.app_view_model.navigate_back() - def _connect_popovers(self): self.headerbar.search_button.set_popover(self.search_view.popover) @@ -140,8 +137,6 @@ def _on_open_view(self, event, data): self.open_book(data) elif event == OpenView.LIBRARY: self.open_library() - elif event == OpenView.BACK: - self.navigate_back() def _on_library_view_event(self, event: str, _): if event == "work-done": diff --git a/cozy/application.py b/cozy/application.py index dc57b7d2..33521fda 100644 --- a/cozy/application.py +++ b/cozy/application.py @@ -2,6 +2,7 @@ import locale import logging import os +import platform import sys import threading from pathlib import Path @@ -15,9 +16,7 @@ from cozy.ui.widgets.filter_list_box import FilterListBox from cozy.ui.widgets.seek_bar import SeekBar -gi.require_version('Handy', '1') - -from gi.repository import Gtk, GLib, Handy +from gi.repository import Gtk, GLib, Adw from cozy.app_controller import AppController from cozy.control.db import init_db @@ -57,7 +56,7 @@ def run_with_except_hook(*args2, **kwargs2): threading.Thread.__init__ = init -class Application(Gtk.Application): +class Application(Adw.Application): ui: CozyUI app_controller: AppController @@ -65,7 +64,7 @@ def __init__(self, localedir: str, pkgdatadir: str): self.localedir = localedir self.pkgdatadir = pkgdatadir - Gtk.Application.__init__(self, application_id='com.github.geigi.cozy') + super().__init__(application_id='com.github.geigi.cozy') self.init_custom_widgets() GLib.setenv("PULSE_PROP_media.role", "music", True) @@ -87,17 +86,12 @@ def __init__(self, localedir: str, pkgdatadir: str): def do_startup(self): log.info(distro.linux_distribution(full_distribution_name=False)) - log.info("Starting up cozy " + __version__) + log.info(f"Starting up cozy {__version__}") + log.info(f"libadwaita version: {Adw._version}") + self.ui = CozyUI(self.pkgdatadir, self, __version__) + Adw.Application.do_startup(self) init_db() - Gtk.Application.do_startup(self) - Handy.init() - try: - manager = Handy.StyleManager.get_default() - manager.set_color_scheme(Handy.ColorScheme.PREFER_LIGHT) - except: - log.info("Not setting libhandy style manager, version is too old.") - log.info("libhandy version: {}".format(Handy._version)) self.ui.startup() def do_activate(self): @@ -115,15 +109,17 @@ def do_activate(self): os.makedirs(path, exist_ok=True) self.add_window(self.ui.window) - mpris = MPRIS(self) - mpris._on_current_changed() + + if platform.system().lower() == "linux": + mpris = MPRIS(self) + mpris._on_current_changed() def handle_exception(self, exc_type, exc_value, exc_traceback): print("handle exception") try: reporter.exception("uncaught", exc_value, "\n".join(format_exception(exc_type, exc_value, exc_traceback))) - except: - pass + except Exception: + None self.old_except_hook(exc_type, exc_value, exc_traceback) @@ -131,7 +127,6 @@ def quit(self): self.app_controller.quit() super(Application, self).quit() - @staticmethod def init_custom_widgets(): FilterListBox() diff --git a/cozy/application_settings.py b/cozy/application_settings.py index 22d3c90e..8a4cbe62 100644 --- a/cozy/application_settings.py +++ b/cozy/application_settings.py @@ -115,7 +115,7 @@ def dark_mode(self) -> bool: @dark_mode.setter def dark_mode(self, new_value: bool): - self._settings.set_boolean("dark_mode", new_value) + self._settings.set_boolean("dark-mode", new_value) @property def window_width(self) -> int: diff --git a/cozy/architecture/event_sender.py b/cozy/architecture/event_sender.py index 31072a43..11c440f4 100644 --- a/cozy/architecture/event_sender.py +++ b/cozy/architecture/event_sender.py @@ -1,10 +1,7 @@ from typing import List, Callable import gi - -gi.require_version('Gdk', '3.0') - -from gi.repository import Gdk, GLib +from gi.repository import GLib class EventSender: @@ -14,15 +11,14 @@ def __init__(self): self._listeners = [] def emit_event(self, event, message=None): - if type(event) is tuple and not message: - message = event[1] - event = event[0] + if isinstance(event, tuple) and message is None: + event, message = event for function in self._listeners: function(event, message) def emit_event_main_thread(self, event: str, message=None): - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.emit_event, (event, message)) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self.emit_event, (event, message)) def add_listener(self, function: Callable[[str, object], None]): self._listeners.append(function) diff --git a/cozy/architecture/observable.py b/cozy/architecture/observable.py index 9fae8311..eb27831f 100644 --- a/cozy/architecture/observable.py +++ b/cozy/architecture/observable.py @@ -1,6 +1,6 @@ from typing import Callable -from gi.repository import Gdk, GLib +from gi.repository import GLib from cozy.report import reporter import logging @@ -47,7 +47,7 @@ def _notify(self, prop: str): reporter.exception("observable", e) def _notify_main_thread(self, prop: str): - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._notify, (prop)) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self._notify, (prop)) def _destroy_observers(self): self._observers = {} diff --git a/cozy/control/artwork_cache.py b/cozy/control/artwork_cache.py index 62ae184f..462e7ddd 100644 --- a/cozy/control/artwork_cache.py +++ b/cozy/control/artwork_cache.py @@ -2,7 +2,7 @@ import uuid import logging -from gi.repository import GdkPixbuf +from gi.repository import Gdk, GdkPixbuf from cozy.application_settings import ApplicationSettings from cozy.control.application_directories import get_cache_dir @@ -21,31 +21,25 @@ def __init__(self): _app_settings = inject.instance(ApplicationSettings) _app_settings.add_listener(self._on_app_setting_changed) - def get_cover_pixbuf(self, book, scale, size=0): + def get_cover_paintable(self, book, scale, size=0) -> Gdk.Texture | None: pixbuf = None size *= scale if size > 0: - # first try the cache + # First try the cache pixbuf = self._load_pixbuf_from_cache(book, size) - if pixbuf: - return pixbuf - else: - # then try the db or file + if not pixbuf: + # Then try the db or file pixbuf = self._load_cover_pixbuf(book) - if pixbuf: - # return original size if it is not greater than 0 - if not size > 0: - return pixbuf - - # create cached version - pixbuf = self._create_artwork_cache(book, pixbuf, size) - else: - pixbuf = None + if not pixbuf: + return None + elif size > 0: + # Resize and cache artwork if size is greater than 0 + pixbuf = self._create_artwork_cache(book, pixbuf, size) - return pixbuf + return Gdk.Texture.new_for_pixbuf(pixbuf) def delete_artwork_cache(self): """ @@ -240,3 +234,4 @@ def _load_pixbuf_from_file(self, book): def _on_app_setting_changed(self, event: str, data): if event == "prefer-external-cover": self.delete_artwork_cache() + diff --git a/cozy/control/db_updater.py b/cozy/control/db_updater.py index 0e3a49ff..bd973110 100644 --- a/cozy/control/db_updater.py +++ b/cozy/control/db_updater.py @@ -283,8 +283,7 @@ def update_db(): _restore_db(backup_dir_name) from cozy.ui.db_migration_failed_view import DBMigrationFailedView - dialog = DBMigrationFailedView() - dialog.show() + DBMigrationFailedView().present() exit(1) if version < 10: @@ -298,8 +297,7 @@ def update_db(): _restore_db(backup_dir_name) from cozy.ui.db_migration_failed_view import DBMigrationFailedView - dialog = DBMigrationFailedView() - dialog.show() + DBMigrationFailedView().present() exit(1) @@ -354,3 +352,4 @@ def _restore_db(backup_dir_name: str): if os.path.exists(wal_path_backup): log.info("Copying wal file") shutil.copyfile(wal_path_backup, wal_path) + diff --git a/cozy/control/filesystem_monitor.py b/cozy/control/filesystem_monitor.py index cdffc1f3..bc826f9c 100644 --- a/cozy/control/filesystem_monitor.py +++ b/cozy/control/filesystem_monitor.py @@ -48,9 +48,10 @@ def init_offline_mode(self): # go through all audiobook locations and test if they can be found in the mounts list for storage in self._settings.external_storage_locations: - online = False - if any(mount.get_root().get_path() in storage.path for mount in mounts): - online = True + online = any( + mount.get_root().get_path() and mount.get_root().get_path() in storage.path + for mount in mounts + ) self.external_storage.append(ExternalStorage(storage=storage, online=online)) def close(self): @@ -62,18 +63,14 @@ def close(self): def get_book_online(self, book: Book): try: - result = next( + return next( (storage.online for storage in self.external_storage if storage.storage.path in book.chapters[0].file), True) - return result except IndexError: return True def is_track_online(self, track): - """ - """ - result = next((storage.online for storage in self.external_storage if storage.storage.path in track.file), True) - return (result) + return next((storage.online for storage in self.external_storage if storage.storage.path in track.file), True) def get_offline_storages(self): return [i.storage.path for i in self.external_storage if not i.online] diff --git a/cozy/extensions/gtk_widget.py b/cozy/extensions/gtk_widget.py deleted file mode 100644 index 5fc474b5..00000000 --- a/cozy/extensions/gtk_widget.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging - -from gi.repository import Gtk, Gdk - -log = logging.getLogger("gtk_widget") - - -def set_hand_cursor(widget: Gtk.Widget): - try: - widget.props.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - except: - log.error("Broken mouse theme, failed to set cursor.") - - -def reset_cursor(widget: Gtk.Widget): - widget.props.window.set_cursor(None) diff --git a/cozy/media/files.py b/cozy/media/files.py index 12b84d53..f31efb69 100644 --- a/cozy/media/files.py +++ b/cozy/media/files.py @@ -10,7 +10,7 @@ from cozy.media.importer import Importer from cozy.model.settings import Settings from cozy.report import reporter -from cozy.ui.info_banner import InfoBanner +from cozy.ui.toaster import ToastNotifier log = logging.getLogger("files") @@ -18,7 +18,7 @@ class Files(EventSender): _settings = inject.attr(Settings) _importer = inject.attr(Importer) - _info_bar: InfoBanner = inject.attr(InfoBanner) + _toast: ToastNotifier = inject.attr(ToastNotifier) _file_count = 0 _file_progess = 0 @@ -29,23 +29,21 @@ def __init__(self): def copy(self, selection): log.info("Start of copying files") self.emit_event_main_thread("start-copy", None) - uris = selection.get_uris() + + paths = [f.get_path() for f in selection] storage_location = self._settings.default_location.path self._file_count = 0 self._file_progess = 0 - self._count_all_files(uris) - self._copy_all(uris, storage_location) + self._count_all_files(paths) + self._copy_all(paths, storage_location) log.info("Copying of files finished") self._importer.scan() def _copy_all(self, sources, destination: str): - for uri in sources: - parsed_path = urllib.parse.urlparse(uri) - path = urllib.parse.unquote(parsed_path.path) - + for path in sources: if os.path.isdir(path): self._copy_directory(path, destination) else: @@ -66,11 +64,11 @@ def _copy_file(self, source_path: str, dest_path: str): if e.code == Gio.IOErrorEnum.CANCELLED: pass elif e.code == Gio.IOErrorEnum.READ_ONLY: - self._info_bar.show(_("Cannot copy: Audiobook directory is read only")) + self._toast.show(_("Cannot copy: Audiobook directory is read only")) elif e.code == Gio.IOErrorEnum.NO_SPACE: - self._info_bar.show(_("Cannot copy: Disk is full")) + self._toast.show(_("Cannot copy: Disk is full")) elif e.code == Gio.IOErrorEnum.PERMISSION_DENIED: - self._info_bar.show(_("Cannot copy: Permission denied")) + self._toast.show(_("Cannot copy: Permission denied")) else: reporter.exception("files", e) @@ -86,7 +84,7 @@ def _copy_directory(self, path, destination): Path(destination_dir).mkdir(parents=True, exist_ok=True) except PermissionError as e: log.error(e) - self._info_bar.show(_("Cannot copy: Permission denied")) + self._toast.show(_("Cannot copy: Permission denied")) return for file in filenames: @@ -94,10 +92,8 @@ def _copy_directory(self, path, destination): file_copy_destination = os.path.join(destination, dirname, file) self._copy_file(source, file_copy_destination) - def _count_all_files(self, uris): - for uri in uris: - parsed_path = urllib.parse.urlparse(uri) - path = urllib.parse.unquote(parsed_path.path) + def _count_all_files(self, paths: list[str]) -> None: + for path in paths: if os.path.isdir(path): self._file_count += self._count_files_in_folder(path) else: diff --git a/cozy/media/gst_player.py b/cozy/media/gst_player.py index 22fcde55..27396c40 100644 --- a/cozy/media/gst_player.py +++ b/cozy/media/gst_player.py @@ -5,12 +5,9 @@ from enum import Enum, auto from typing import Optional -import gi - from cozy.architecture.event_sender import EventSender from cozy.report import reporter -gi.require_version('Gst', '1.0') from gi.repository import Gst log = logging.getLogger("gst_player") diff --git a/cozy/media/importer.py b/cozy/media/importer.py index e79de6d3..d5429372 100644 --- a/cozy/media/importer.py +++ b/cozy/media/importer.py @@ -18,7 +18,7 @@ from cozy.model.library import Library from cozy.model.settings import Settings from cozy.report import reporter -from cozy.ui.info_banner import InfoBanner +from cozy.ui.toaster import ToastNotifier log = logging.getLogger("importer") @@ -56,7 +56,7 @@ class Importer(EventSender): _settings = inject.attr(Settings) _library = inject.attr(Library) _database_importer = inject.attr(DatabaseImporter) - _info_bar: InfoBanner = inject.attr(InfoBanner) + _toast: ToastNotifier = inject.attr(ToastNotifier) def __init__(self): super().__init__() @@ -118,7 +118,7 @@ def _execute_import(self, files_to_scan: List[str]) -> (Set[str], Set[str]): log.error("Error while inserting new tracks to the database") reporter.exception("importer", e) log.error(traceback.format_exc()) - self._info_bar.show("{}: {}".format(_("Error while importing new files"), str(e.__class__))) + self._toast.show("{}: {}".format(_("Error while importing new files"), str(e.__class__))) if self._progress >= self._files_count: break diff --git a/cozy/media/media_detector.py b/cozy/media/media_detector.py index 6a3fb705..c5f3fb19 100644 --- a/cozy/media/media_detector.py +++ b/cozy/media/media_detector.py @@ -3,13 +3,9 @@ from cozy.architecture.event_sender import EventSender -import gi - from cozy.media.media_file import MediaFile from cozy.media.tag_reader import TagReader -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') from gi.repository import Gst, GstPbutils log = logging.getLogger("media_detector") diff --git a/cozy/media/player.py b/cozy/media/player.py index a7521633..7a12122e 100644 --- a/cozy/media/player.py +++ b/cozy/media/player.py @@ -18,7 +18,7 @@ from cozy.report import reporter from cozy.tools import IntervalTimer from cozy.ui.file_not_found_dialog import FileNotFoundDialog -from cozy.ui.info_banner import InfoBanner +from cozy.ui.toaster import ToastNotifier log = logging.getLogger("mediaplayer") @@ -30,7 +30,7 @@ class Player(EventSender): _library: Library = inject.attr(Library) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) _offline_cache: OfflineCache = inject.attr(OfflineCache) - _info_bar: InfoBanner = inject.attr(InfoBanner) + _toast: ToastNotifier = inject.attr(ToastNotifier) _importer: Importer = inject.attr(Importer) _gst_player: GstPlayer = inject.attr(GstPlayer) @@ -285,6 +285,7 @@ def _next_chapter(self): if not self._book: log.error("Cannot play next chapter because no book reference is stored.") reporter.error("player", "Cannot play next chapter because no book reference is stored.") + return index_current_chapter = self._book.chapters.index(self._book.current_chapter) @@ -327,14 +328,14 @@ def _on_gst_player_event(self, event: str, message): def _handle_gst_error(self, error: GLib.Error): if error.code != Gst.ResourceError.BUSY: - self._info_bar.show(error.message) + self._toast.show(error.message) if error.code == Gst.ResourceError.OPEN_READ or Gst.ResourceError.READ: self._stop_playback() def _handle_file_not_found(self): if self.loaded_chapter: - FileNotFoundDialog(self.loaded_chapter).show() + FileNotFoundDialog(self.loaded_chapter).present() self._stop_playback() else: log.warning("No chapter loaded, cannot display file not found dialog.") diff --git a/cozy/media/tag_reader.py b/cozy/media/tag_reader.py index 1f60886f..00425b48 100644 --- a/cozy/media/tag_reader.py +++ b/cozy/media/tag_reader.py @@ -2,12 +2,9 @@ from typing import List from urllib.parse import unquote, urlparse -import gi import mutagen from mutagen.mp4 import MP4 -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') from gi.repository import GstPbutils, Gst, GLib from cozy.media.chapter import Chapter diff --git a/cozy/model/book.py b/cozy/model/book.py index 226b9d36..9e4d5bc2 100644 --- a/cozy/model/book.py +++ b/cozy/model/book.py @@ -202,7 +202,6 @@ def remove(self): self.destroy_listeners() self._destroy_observers() - @timing def _fetch_chapters(self): tracks = TrackModel \ .select() \ diff --git a/cozy/report/report_to_loki.py b/cozy/report/report_to_loki.py index 0bee75d2..6b779065 100644 --- a/cozy/report/report_to_loki.py +++ b/cozy/report/report_to_loki.py @@ -13,10 +13,6 @@ from peewee import __version__ as PeeweeVersion from mutagen import version_string as MutagenVersion -import gi - -gi.require_version('Gtk', '3.0') - from gi.repository import Gtk URL = 'https://errors.cozy.sh:3100/api/prom/push' diff --git a/cozy/ui/app_view.py b/cozy/ui/app_view.py index ad2950a5..cc16e548 100644 --- a/cozy/ui/app_view.py +++ b/cozy/ui/app_view.py @@ -1,4 +1,4 @@ -from gi.repository import Gtk, Handy +from gi.repository import Gtk, Adw from cozy.ext import inject from cozy.view_model.app_view_model import AppViewModel @@ -9,9 +9,6 @@ PREPARING_LIBRARY = "import" BOOK_DETAIL = "book_overview" -LIBRARY_FILTER = "filter" -LIBRARY_BOOKS = "books" - class AppView: _view_model: AppViewModel = inject.attr(AppViewModel) @@ -27,12 +24,11 @@ def __init__(self, builder: Gtk.Builder): def _get_ui_elements(self): self._main_stack: Gtk.Stack = self._builder.get_object("main_stack") - self._library_leaflet: Handy.Leaflet = self._builder.get_object("library_leaflet") + self._navigation_view: Adw.NavigationView = self._builder.get_object("navigation_view") def _connect_ui_elements(self): self._main_stack.connect("notify::visible-child", self._update_view_model_view) - self._library_leaflet.connect("notify::folded", self._update_view_model_view) - self._library_leaflet.connect("notify::visible-child", self._update_view_model_view) + self._navigation_view.connect("notify::visible-page", self._update_view_model_view) def _connect_view_model(self): self._view_model.bind_to("view", self._on_view_changed) @@ -46,28 +42,17 @@ def _on_view_changed(self): self._main_stack.set_visible_child_name(PREPARING_LIBRARY) elif view == View.LIBRARY: self._main_stack.set_visible_child_name(LIBRARY) - elif view == View.LIBRARY_FILTER: - self._main_stack.set_visible_child_name(LIBRARY) - self._library_leaflet.set_visible_child_name(LIBRARY_FILTER) - elif view == View.LIBRARY_BOOKS: - self._main_stack.set_visible_child_name(LIBRARY) - self._library_leaflet.set_visible_child_name(LIBRARY_BOOKS) - def _update_view_model_view(self, _, __): + def _update_view_model_view(self, *_): page = self._main_stack.props.visible_child_name - library_folded = self._library_leaflet.props.folded - library_page = self._library_leaflet.props.visible_child_name - if page == EMPTY_STATE: + if page == LIBRARY: + if self._navigation_view.props.visible_page.props.tag == BOOK_DETAIL: + self._view_model.view = View.BOOK_DETAIL + else: + self._view_model.view = View.LIBRARY + elif page == EMPTY_STATE: self._view_model.view = View.EMPTY_STATE elif page == PREPARING_LIBRARY: self._view_model.view = View.PREPARING_LIBRARY - elif page == BOOK_DETAIL: - self._view_model.view = View.BOOK_DETAIL - elif page == LIBRARY: - if library_folded and library_page == LIBRARY_FILTER: - self._view_model.view = View.LIBRARY_FILTER - elif library_folded and library_page == LIBRARY_BOOKS: - self._view_model.view = View.LIBRARY_BOOKS - elif not library_folded: - self._view_model.view = View.LIBRARY + diff --git a/cozy/ui/book_detail_view.py b/cozy/ui/book_detail_view.py index 18ee1577..3a25271c 100644 --- a/cozy/ui/book_detail_view.py +++ b/cozy/ui/book_detail_view.py @@ -12,22 +12,21 @@ from cozy.report import reporter from cozy.ui.chapter_element import ChapterElement from cozy.ui.disk_element import DiskElement -from cozy.ui.widgets.album_art import AlbumArt from cozy.view_model.book_detail_view_model import BookDetailViewModel -gi.require_version('Gtk', '3.0') - -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Adw, GLib, Gtk log = logging.getLogger("BookDetailView") +ALBUM_ART_SIZE = 256 + + @Gtk.Template.from_resource('/com/github/geigi/cozy/book_detail.ui') -class BookDetailView(Gtk.EventBox): +class BookDetailView(Gtk.Box): __gtype_name__ = 'BookDetail' play_book_button: Gtk.Button = Gtk.Template.Child() - play_img: Gtk.Image = Gtk.Template.Child() book_label: Gtk.Label = Gtk.Template.Child() author_label: Gtk.Label = Gtk.Template.Child() @@ -45,6 +44,7 @@ class BookDetailView(Gtk.EventBox): download_image: Gtk.Image = Gtk.Template.Child() download_switch: Gtk.Switch = Gtk.Template.Child() + album_art: Gtk.Picture = Gtk.Template.Child() album_art_container: Gtk.Box = Gtk.Template.Child() unavailable_box: Gtk.Box = Gtk.Template.Child() @@ -63,15 +63,16 @@ class BookDetailView(Gtk.EventBox): def __init__(self, main_window_builder: Gtk.Builder): super().__init__() - self._main_stack: Gtk.Stack = main_window_builder.get_object("main_stack") - # self._toolbar_revealer: Gtk.Revealer = main_window_builder.get_object("toolbar_revealer") - self._main_stack.add_named(self, "book_overview") + self._navigation_view: Adw.NavigationView = main_window_builder.get_object("navigation_view") + self._book_details_container: Adw.ToolbarView = main_window_builder.get_object("book_details_container") + self._book_details_container.set_content(self) - if Gtk.get_minor_version() > 20: - self.book_overview_scroller.props.propagate_natural_height = True + headerbar = Adw.HeaderBar() + self.header_title = Adw.WindowTitle() + headerbar.set_title_widget(self.header_title) + self._book_details_container.add_top_bar(headerbar) - self.art = AlbumArt() - self.album_art_container.pack_start(self.art, True, True, 0) + self.book_overview_scroller.props.propagate_natural_height = True self._chapters_event: Event = Event() self._chapters_thread: Thread = None @@ -79,7 +80,6 @@ def __init__(self, main_window_builder: Gtk.Builder): self._connect_view_model() self._connect_widgets() - self._add_mouse_button_accel() def _connect_view_model(self): self._view_model.bind_to("book", self._on_book_changed) @@ -98,13 +98,6 @@ def _connect_view_model(self): def _connect_widgets(self): self.play_book_button.connect("clicked", self._play_book_clicked) self.download_switch.connect("state-set", self._download_switch_changed) - self.main_flow_box.connect("size-allocate", self._main_flow_box_size_changed) - - def _add_mouse_button_accel(self): - self.gesture = Gtk.GestureMultiPress(widget=self) - self.gesture.set_button(0) - self.gesture.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) - self.gesture.connect('pressed', self._on_mouse_event) def _on_book_changed(self): if not self._view_model.book: @@ -130,6 +123,9 @@ def _on_book_changed(self): self.book_label.set_text(book.name) self.author_label.set_text(book.author) + self.header_title.set_title(book.name) + self.header_title.set_subtitle(book.author) + self.last_played_label.set_text(self._view_model.last_played_text) self._set_cover_image(book) @@ -137,15 +133,19 @@ def _on_book_changed(self): self._display_external_section() self._set_progress() - def _open_book_overview(self): - self._main_stack.set_visible_child_name("book_overview") - #self._toolbar_revealer.set_reveal_child(False) + def _on_open(self): + if self._navigation_view.props.visible_page.props.tag == "book_overview": + self._navigation_view.pop_to_tag("book_overview") + else: + self._navigation_view.push_by_tag("book_overview") def _on_play_changed(self): playing = self._view_model.playing - play_button_img = "pause-symbolic" if playing else "play-symbolic" - self.play_img.set_from_icon_name(play_button_img, Gtk.IconSize.DND) + if playing: + self.play_book_button.set_icon_name("media-playback-pause-symbolic") + else: + self.play_book_button.set_icon_name("media-playback-start-symbolic") if self._current_selected_chapter: self._current_selected_chapter.set_playing(playing) @@ -209,9 +209,9 @@ def _schedule_chapters_rendering(self, book: Book, callback: Callable): return if multiple_disks and disk_number != chapter.disk: - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._add_disk, book.id, chapter) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self._add_disk, book.id, chapter) - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._add_chapter, book.id, chapter) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self._add_chapter, book.id, chapter) disk_number = chapter.disk @@ -219,7 +219,7 @@ def _schedule_chapters_rendering(self, book: Book, callback: Callable): self._chapters_event.wait() self._chapters_event.clear() - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, callback) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, callback) def _on_chapters_displayed(self): self.total_label.set_text(self._view_model.total_text) @@ -248,8 +248,7 @@ def _add_disk(self, book_id: int, chapter: Chapter): return disc_element = DiskElement(chapter.disk) - self.chapter_box.add(disc_element) - disc_element.show_all() + self.chapter_box.append(disc_element) self._chapters_event.set() def _add_chapter(self, book_id: int, chapter: Chapter): @@ -258,22 +257,22 @@ def _add_chapter(self, book_id: int, chapter: Chapter): chapter_element = ChapterElement(chapter) chapter_element.connect("play-pause-clicked", self._play_chapter_clicked) - self.chapter_box.add(chapter_element) - chapter_element.show_all() + self.chapter_box.append(chapter_element) self._chapters_event.set() def _schedule_chapters_clearing(self): - Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.chapter_box.remove_all_children) + GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self.chapter_box.remove_all_children) def _set_progress(self): self.remaining_label.set_text(self._view_model.remaining_text) self.book_progress_bar.set_fraction(self._view_model.progress_percent) def _set_cover_image(self, book: Book): - pixbuf = self._artwork_cache.get_cover_pixbuf(book, self.get_scale_factor(), 250) - if pixbuf: + paintable = self._artwork_cache.get_cover_paintable(book, self.get_scale_factor(), ALBUM_ART_SIZE) + if paintable: self.album_art_container.set_visible(True) - self.art.set_art(pixbuf) + self.album_art.set_paintable(paintable) + self.album_art.set_overflow(True) else: self.album_art_container.set_visible(False) @@ -291,19 +290,6 @@ def _prepare_chapters_job(self): def _download_switch_changed(self, _, state: bool): self._view_model.download_book(state) - def _main_flow_box_size_changed(self, _, __): - if self._is_chapter_box_wrapped(): - vertical_scroll_policy = Gtk.PolicyType.NEVER - else: - vertical_scroll_policy = Gtk.PolicyType.ALWAYS - - self.book_overview_scroller.set_policy(Gtk.PolicyType.NEVER, vertical_scroll_policy) - - def _is_chapter_box_wrapped(self): - x, _ = self.book_overview_scroller.translate_coordinates(self.main_flow_box, 0, 0) - - return x < 100 - def _set_book_download_status(self): if not self._view_model.is_book_external: return @@ -315,7 +301,7 @@ def _set_book_download_status(self): icon_name = "download-symbolic" text = _("Download") - self.download_image.set_from_icon_name(icon_name, Gtk.IconSize.LARGE_TOOLBAR) + self.download_image.set_from_icon_name(icon_name) self.download_label.set_text(text) def _play_chapter_clicked(self, _, chapter: Chapter): @@ -324,13 +310,3 @@ def _play_chapter_clicked(self, _, chapter: Chapter): def _play_book_clicked(self, _): self._view_model.play_book() - def _on_mouse_event(self, gesture: Gtk.GestureMultiPress, _, __, ___): - btn = gesture.get_current_button() - if btn == 8: - self._view_model.navigate_back() - return True - - return False - - def _on_open(self): - self._open_book_overview() \ No newline at end of file diff --git a/cozy/ui/chapter_element.py b/cozy/ui/chapter_element.py index de6318b8..10f20aae 100644 --- a/cozy/ui/chapter_element.py +++ b/cozy/ui/chapter_element.py @@ -5,10 +5,9 @@ @Gtk.Template.from_resource('/com/github/geigi/cozy/chapter_element.ui') -class ChapterElement(Gtk.EventBox): +class ChapterElement(Gtk.Box): __gtype_name__ = "ChapterElement" - icon_event_box: Gtk.EventBox = Gtk.Template.Child() icon_stack: Gtk.Stack = Gtk.Template.Child() number_label: Gtk.Label = Gtk.Template.Child() play_icon: Gtk.Image = Gtk.Template.Child() @@ -17,62 +16,36 @@ class ChapterElement(Gtk.EventBox): def __init__(self, chapter: Chapter): self.selected = False - self.chapter: Chapter = chapter + self.chapter = chapter super().__init__() - self.connect("enter-notify-event", self._on_enter_notify) - self.connect("leave-notify-event", self._on_leave_notify) - self.connect("button-press-event", self._on_button_press) - self.set_tooltip_text(_("Play this part")) - - # This box contains all content - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.HORIZONTAL) - self.box.set_spacing(3) - self.box.set_halign(Gtk.Align.FILL) - self.box.set_valign(Gtk.Align.CENTER) - - # These are the widgets that contain data - self.play_img = Gtk.Image() - no_label = Gtk.Label() - title_label = Gtk.Label() - dur_label = Gtk.Label() - - self.play_img.set_margin_right(5) - self.play_img.props.width_request = 16 - - if self.chapter.number > 0: - no_label.set_text(str(self.chapter.number)) - no_label.props.margin = 4 - no_label.set_margin_right(7) - no_label.set_margin_left(0) - no_label.set_size_request(30, -1) - no_label.set_xalign(1) + gesture = Gtk.GestureClick(button=Gdk.BUTTON_PRIMARY) + gesture.connect("released", self._on_button_press) + self.add_controller(gesture) + + motion = Gtk.EventControllerMotion() + motion.connect("enter", self._on_enter_notify) + motion.connect("leave", self._on_leave_notify) + self.add_controller(motion) self.number_label.set_text(str(self.chapter.number)) self.title_label.set_text(self.chapter.name) self.duration_label.set_text(seconds_to_str(self.chapter.length)) - self.icon_event_box.connect("enter-notify-event", self._on_enter_notify) - self.icon_event_box.connect("leave-notify-event", self._on_leave_notify) - - def _on_button_press(self, _, event): - if event.type == Gdk.EventType.BUTTON_PRESS and event.button != 1: - return False - + def _on_button_press(self, *_): self.emit("play-pause-clicked", self.chapter) - return True - def _on_enter_notify(self, _, __): + def _on_enter_notify(self, *_): + self.add_css_class("box_hover") + self.play_icon.add_css_class("box_hover") + if not self.selected: self.icon_stack.set_visible_child_name("play_icon") - self.get_style_context().add_class("box_hover") - self.play_icon.get_style_context().add_class("box_hover") - def _on_leave_notify(self, _, __): - self.get_style_context().remove_class("box_hover") - self.play_icon.get_style_context().remove_class("box_hover") + def _on_leave_notify(self, *_): + self.remove_css_class("box_hover") + self.play_icon.remove_css_class("box_hover") if not self.selected: self.icon_stack.set_visible_child_name("number") @@ -87,9 +60,9 @@ def deselect(self): def set_playing(self, playing): if playing: - self.play_icon.set_from_icon_name("pause-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + self.play_icon.set_from_icon_name("media-playback-pause-symbolic") else: - self.play_icon.set_from_icon_name("play-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + self.play_icon.set_from_icon_name("media-playback-start-symbolic") GObject.type_register(ChapterElement) diff --git a/cozy/ui/db_migration_failed_view.py b/cozy/ui/db_migration_failed_view.py index b1df22a7..f2e81593 100644 --- a/cozy/ui/db_migration_failed_view.py +++ b/cozy/ui/db_migration_failed_view.py @@ -1,22 +1,31 @@ import webbrowser import gi +from gi.repository import Adw -from cozy.ext import inject +EXPLANATION = _("During an update of the database an error occurred and Cozy will not be able to startup.\ + A backup of the database was created before the update and has been restored now.\ + Until this issue is resolved please use version 0.9.5 of Cozy.\ + You can help resolve this problem by reporting an issue on GitHub.") -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +class DBMigrationFailedView(Adw.MessageDialog): + def __init__(self): + super().__init__( + heading=_("Failed to Update Database"), + body=EXPLANATION, + default_response="help", + close_response="close", + modal=True, + ) -@Gtk.Template(resource_path='/com/github/geigi/cozy/db_migration_failed.ui') -class DBMigrationFailedView(Gtk.Dialog): - __gtype_name__ = 'DBMigrationFailedDialog' + self.add_response("close", _("Close Cozy")) + self.add_response("help", _("Receive help on GitHub")) + self.set_response_appearance("help", Adw.ResponseAppearance.SUGGESTED) - def __init__(self, **kwargs): - super().__init__(**kwargs) + self.connect("response", self.get_help) - def show(self): - response = self.run() - - if response == Gtk.ResponseType.OK: + def get_help(self, *_, response): + if response == "help": webbrowser.open("https://github.com/geigi/cozy/issues", new=2) + diff --git a/cozy/ui/delete_book_view.py b/cozy/ui/delete_book_view.py index aa2dfac6..33743767 100644 --- a/cozy/ui/delete_book_view.py +++ b/cozy/ui/delete_book_view.py @@ -1,26 +1,43 @@ -import gi +from gi.repository import Adw, Gtk from cozy.ext import inject +from cozy.control.artwork_cache import ArtworkCache -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk - - -@Gtk.Template(resource_path='/com/github/geigi/cozy/delete_book_dialog.ui') -class DeleteBookView(Gtk.Dialog): - __gtype_name__ = 'DeleteBookDialog' +class DeleteBookView(Adw.MessageDialog): main_window = inject.attr("MainWindow") + artwork_cache: ArtworkCache = inject.attr(ArtworkCache) + + def __init__(self, callback, book): + super().__init__( + heading=_("Delete Audiobook?"), + body=_("The audiobook will be removed from your disk and from Cozy's library."), + default_response="cancel", + close_response="cancel", + transient_for=self.main_window.window, + modal=True, + ) + + self.add_response("cancel", _("Cancel")) + self.add_response("delete", _("Remove Audiobook")) + self.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE) + + book_row = Adw.ActionRow( + title=book.name, + subtitle=book.author, + selectable=False, + use_markup=False, + ) + album_art = Gtk.Picture(margin_top=6, margin_bottom=6) + + paintable = self.artwork_cache.get_cover_paintable(book, 1, 48) + if paintable: + album_art.set_paintable(paintable) + book_row.add_prefix(album_art) + + list_box = Gtk.ListBox(margin_top=12, css_classes=["boxed-list"]) + list_box.append(book_row) + self.set_extra_child(list_box) + + self.connect("response", callback, book) - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.set_modal(self.main_window.window) - - def get_delete_book(self): - response = self.run() - - if response == Gtk.ResponseType.APPLY: - return True - else: - return False diff --git a/cozy/ui/disk_element.py b/cozy/ui/disk_element.py index 1b410a50..c183f0f9 100644 --- a/cozy/ui/disk_element.py +++ b/cozy/ui/disk_element.py @@ -1,30 +1,25 @@ from gi.repository import Gtk + class DiskElement(Gtk.Box): """ This class represents a small disk number header for the book overview track list. """ - disc_number = None - container = None def __init__(self, disc_number): super().__init__() - self.container = Gtk.Box() + self.add_css_class("dim-label") - self.disc_number = disc_number if disc_number > 1: - self.container.set_margin_top(18) + self.set_margin_top(18) + self.set_margin_bottom(3) + self.set_margin_start(6) + + image = Gtk.Image.new_from_icon_name("media-optical-cd-audio-symbolic") + self.append(image) - self.container.set_margin_bottom(3) - self.container.set_margin_left(5) - self.container.set_orientation(Gtk.Orientation.HORIZONTAL) - self.container.get_style_context().add_class("dim-label") + label = Gtk.Label(margin_start=5) + text = _("Disc") + " " + str(disc_number) # TODO: use formatted translation string here + label.set_markup(f"{text}") + self.append(label) - image = Gtk.Image.new_from_icon_name("media-optical-cd-audio-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - self.container.add(image) - label = Gtk.Label() - label.set_margin_left(5) - text = _("Disc") + " " + str(disc_number) - label.set_markup("" + text + "") - self.container.add(label) - self.add(self.container) diff --git a/cozy/ui/file_not_found_dialog.py b/cozy/ui/file_not_found_dialog.py index bf5d5534..4beba83d 100644 --- a/cozy/ui/file_not_found_dialog.py +++ b/cozy/ui/file_not_found_dialog.py @@ -1,67 +1,64 @@ -import os +from pathlib import Path -from gi.repository import Gtk +from gi.repository import Adw, Gio, GLib, Gtk -import cozy.ui from cozy.ext import inject from cozy.media.importer import Importer from cozy.model.chapter import Chapter -from cozy.model.library import Library -class FileNotFoundDialog(): +class FileNotFoundDialog(Adw.MessageDialog): + main_window = inject.attr("MainWindow") _importer: Importer = inject.attr(Importer) - _library: Library = inject.attr(Library) - def __init__(self, chapter: Chapter): self.missing_chapter = chapter - self.parent = cozy.ui.main_view.CozyUI() - self.builder = Gtk.Builder.new_from_resource( - "/com/github/geigi/cozy/file_not_found.ui") - self.dialog = self.builder.get_object("dialog") - self.dialog.set_modal(self.parent.window) - self.builder.get_object("file_label").set_markup( - "" + chapter.file + "") - - cancel_button = self.builder.get_object("cancel_button") - cancel_button.connect("clicked", self.close) - locate_button = self.builder.get_object("locate_button") - locate_button.connect("clicked", self.locate) - - def show(self): - self.dialog.show() - - def close(self, _): - self.dialog.destroy() - - def locate(self, __): - directory, filename = os.path.split(self.missing_chapter.file) - dialog = Gtk.FileChooserDialog("Please locate the file " + filename, self.parent.window, - Gtk.FileChooserAction.OPEN, - (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) - - filter = Gtk.FileFilter() - filter.add_pattern(filename) - filter.set_name(filename) - dialog.add_filter(filter) - path, file_extension = os.path.splitext(self.missing_chapter.file) - filter = Gtk.FileFilter() - filter.add_pattern("*" + file_extension) - filter.set_name(file_extension + " files") - dialog.add_filter(filter) - filter = Gtk.FileFilter() - filter.add_pattern("*") - filter.set_name(_("All files")) - dialog.add_filter(filter) - dialog.set_local_only(False) - - response = dialog.run() - if response == Gtk.ResponseType.OK: - new_location = dialog.get_filename() - self.missing_chapter.file = new_location - self._importer.scan() - self.dialog.destroy() - - dialog.destroy() + + super().__init__( + heading=_("File not found"), + body=_("This file could not be found. Do you want to locate it manually?"), + default_response="locate", + close_response="cancel", + transient_for=self.main_window.window, + modal=True, + ) + + self.add_response("cancel", _("Cancel")) + self.add_response("locate", _("Locate")) + self.set_response_appearance("locate", Adw.ResponseAppearance.SUGGESTED) + + label = Gtk.Label(label=chapter.file, margin_top=12) + label.add_css_class("monospace") + self.set_extra_child(label) + + self.connect("response", self._on_locate) + + def _on_locate(self, __, response): + if response == "locate": + file_dialog = Gtk.FileDialog(title=_("Locate Missing File")) + + extension = Path(self.missing_chapter.file).suffix[1:] + current_extension_filter = Gtk.FileFilter(name=_("{ext} files").format(ext=extension)) + current_extension_filter.add_suffix(extension) + + audio_files_filter = Gtk.FileFilter(name=_("Audio files")) + audio_files_filter.add_mime_type("audio/*") + + filters = Gio.ListStore.new(Gtk.FileFilter) + filters.append(current_extension_filter) + filters.append(audio_files_filter) + + file_dialog.set_filters(filters) + file_dialog.set_default_filter(current_extension_filter) + file_dialog.open(self.main_window.window, None, self._file_dialog_open_callback) + + def _file_dialog_open_callback(self, dialog, result): + try: + file = dialog.open_finish(result) + except GLib.GError: + pass + else: + if file is not None: + self.missing_chapter.file = file.get_path() + self._importer.scan() + diff --git a/cozy/ui/headerbar.py b/cozy/ui/headerbar.py index 4d306c88..f5a01e21 100644 --- a/cozy/ui/headerbar.py +++ b/cozy/ui/headerbar.py @@ -6,11 +6,7 @@ from cozy.ui.widgets.progress_popover import ProgressPopover from cozy.view_model.headerbar_view_model import HeaderbarViewModel, HeaderBarState -gi.require_version('Gtk', '3.0') -gi.require_version('Dazzle', '1.0') -from gi.repository import Gtk, Handy -from gi.repository.Handy import HeaderBar -from gi.repository.Dazzle import ProgressMenuButton +from gi.repository import Adw, Gtk log = logging.getLogger("Headerbar") @@ -18,35 +14,37 @@ @Gtk.Template.from_resource('/com/github/geigi/cozy/headerbar.ui') -class Headerbar(HeaderBar): +class Headerbar(Adw.Bin): __gtype_name__ = "Headerbar" + headerbar: Adw.HeaderBar = Gtk.Template.Child() + + show_sidebar_button: Gtk.ToggleButton = Gtk.Template.Child() search_button: Gtk.MenuButton = Gtk.Template.Child() menu_button: Gtk.MenuButton = Gtk.Template.Child() - progress_menu_button: ProgressMenuButton = Gtk.Template.Child() + progress_menu_button: Gtk.MenuButton = Gtk.Template.Child() + progress_spinner: Gtk.Spinner = Gtk.Template.Child() - back_button: Gtk.Button = Gtk.Template.Child() - category_toolbar: Handy.ViewSwitcherTitle = Gtk.Template.Child() + view_switcher: Adw.ViewSwitcher = Gtk.Template.Child() def __init__(self, main_window_builder: Gtk.Builder): super().__init__() - self._library_mobile_view_switcher: Handy.ViewSwitcherBar = main_window_builder.get_object( - "library_mobile_view_switcher") - self._library_mobile_revealer: Gtk.Revealer = main_window_builder.get_object("library_mobile_revealer") - self._header_container: Gtk.Box = main_window_builder.get_object("header_container") - self._header_container.pack_start(self, False, True, 0) + self.header_container: Adw.ToolbarView = main_window_builder.get_object("header_container") + self.header_container.add_top_bar(self) + + self.mobile_view_switcher: Adw.ViewSwitcherBar = main_window_builder.get_object("mobile_view_switcher") + self.split_view: Adw.OverlaySplitView = main_window_builder.get_object("split_view") - self._sort_stack: Gtk.Stack = main_window_builder.get_object("sort_stack") - self.category_toolbar.set_stack(self._sort_stack) - self._library_mobile_view_switcher.set_stack(self._sort_stack) + self.sort_stack: Adw.ViewStack = main_window_builder.get_object("sort_stack") + self.view_switcher.set_stack(self.sort_stack) + self.mobile_view_switcher.set_stack(self.sort_stack) self.progress_popover = ProgressPopover() self.progress_menu_button.set_popover(self.progress_popover) self._headerbar_view_model: HeaderbarViewModel = inject.instance(HeaderbarViewModel) - self._init_app_menu() self._connect_view_model() self._connect_widgets() @@ -54,53 +52,47 @@ def _connect_view_model(self): self._headerbar_view_model.bind_to("state", self._on_state_changed) self._headerbar_view_model.bind_to("work_progress", self._on_work_progress_changed) self._headerbar_view_model.bind_to("work_message", self._on_work_message_changed) - self._headerbar_view_model.bind_to("can_navigate_back", self._on_can_navigate_back_changed) - self._headerbar_view_model.bind_to("show_library_filter", self._on_show_library_filter_changed) self._headerbar_view_model.bind_to("lock_ui", self._on_lock_ui_changed) def _connect_widgets(self): - self.back_button.connect("clicked", self._back_clicked) - self.category_toolbar.connect("notify::title-visible", self._on_title_visible_changed) + self.split_view.connect("notify::show-sidebar", self._on_sidebar_toggle) + self.show_sidebar_button.connect("notify::active", self._on_sidebar_toggle) + self.mobile_view_switcher.connect("notify::reveal", self._on_mobile_view) + self.sort_stack.connect("notify::visible-child", self._on_sort_stack_changed) + + def _on_sort_stack_changed(self, widget, _): + page = widget.props.visible_child_name + + self.show_sidebar_button.set_visible(page != "recent") + + def _on_mobile_view(self, widget, _): + if widget.props.reveal: + self.headerbar.set_title_widget(Adw.WindowTitle(title="Cozy")) + else: + self.headerbar.set_title_widget(self.view_switcher) + + def _on_sidebar_toggle(self, widget, param): + show_sidebar = widget.get_property(param.name) - def _init_app_menu(self): - self.menu_builder = Gtk.Builder.new_from_resource("/com/github/geigi/cozy/titlebar_menu.ui") - menu = self.menu_builder.get_object("titlebar_menu") - self.menu_button.set_menu_model(menu) + if widget is self.show_sidebar_button: + self.split_view.set_show_sidebar(show_sidebar) + elif widget is self.split_view: + self.show_sidebar_button.set_active(show_sidebar) def _on_state_changed(self): if self._headerbar_view_model.state == HeaderBarState.PLAYING: - progress_visible = False - self.progress_menu_button.set_progress(0) + self.progress_menu_button.set_visible(False) + self.progress_popover.set_progress(0) + self.progress_spinner.stop() else: - progress_visible = True - - self.progress_menu_button.set_visible(progress_visible) + self.progress_menu_button.set_visible(True) + self.progress_spinner.start() def _on_work_progress_changed(self): - progress = self._headerbar_view_model.work_progress - self.progress_menu_button.set_progress(progress) - self.progress_popover.set_progress(progress) + self.progress_popover.set_progress(self._headerbar_view_model.work_progress) def _on_work_message_changed(self): self.progress_popover.set_message(self._headerbar_view_model.work_message) - def _on_can_navigate_back_changed(self): - self.back_button.set_visible(self._headerbar_view_model.can_navigate_back) - - def _on_show_library_filter_changed(self): - self.category_toolbar.set_visible(self._headerbar_view_model.show_library_filter) - self._reveal_mobile_library_filter(self.category_toolbar.get_title_visible()) - - def _back_clicked(self, _): - self._headerbar_view_model.navigate_back() - - def _on_title_visible_changed(self, widget, param): - visible = widget.get_property(param.name) - self._reveal_mobile_library_filter(visible) - - def _reveal_mobile_library_filter(self, reveal: bool): - reveal_child = reveal and self._headerbar_view_model.show_library_filter - self._library_mobile_revealer.set_reveal_child(reveal_child) - def _on_lock_ui_changed(self): self.search_button.set_sensitive(not self._headerbar_view_model.lock_ui) diff --git a/cozy/ui/import_failed_dialog.py b/cozy/ui/import_failed_dialog.py index cc741270..7b98fa86 100644 --- a/cozy/ui/import_failed_dialog.py +++ b/cozy/ui/import_failed_dialog.py @@ -1,35 +1,59 @@ -from gi.repository import Gtk +from gettext import gettext as _ -import cozy.ui +from gi.repository import Adw, Gtk +from cozy.ext import inject -class ImportFailedDialog(): + +HEADER = _("This can have multiple reasons:") +POSSIBILITIES = "\n • ".join(( # yes, it is a hack, because \t would be too wide + "", + _("The audio format is not supported"), + _("The path or filename contains non utf-8 characters"), + _("The file(s) are no valid audio files"), + _("The file(s) are corrupt"), +)) + +message = HEADER + POSSIBILITIES + + +class ImportFailedDialog(Adw.MessageDialog): """ Dialog that displays failed files on import. """ + main_window = inject.attr("MainWindow") + + def __init__(self, files: list[str]): + super().__init__( + heading=_("Some files could not be imported"), + default_response="cancel", + close_response="cancel", + transient_for=self.main_window.window, + modal=True, + ) + + self.add_response("cancel", _("Ok")) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18) + body_label = Gtk.Label(label=message) + + text_buffer = Gtk.TextBuffer( + text="\n".join(files).encode("utf-8", errors="replace").decode("utf-8") + ) + text_view = Gtk.TextView( + buffer=text_buffer, + editable=False, + cursor_visible=False, + css_classes=["card", "failed-import-card", "monospace"] + ) + + scroller = Gtk.ScrolledWindow( + max_content_height=200, + propagate_natural_height=True, + child=text_view + ) + + box.append(body_label) + box.append(scroller) + self.set_extra_child(box) - def __init__(self, files): - self.parent = cozy.ui.main_view.CozyUI() - self.builder = Gtk.Builder.new_from_resource( - "/com/github/geigi/cozy/import_failed.ui") - self.dialog = self.builder.get_object("dialog") - self.dialog.set_modal(self.parent.window) - self.text = self.builder.get_object("files_buffer") - - files_string = "\n".join(files) - self.text.set_text(files_string.encode("utf-8", "replace").decode("utf-8")) - - locate_button = self.builder.get_object("ok_button") - locate_button.connect("clicked", self.ok) - - def show(self): - """ - show this dialog - """ - self.dialog.show() - - def ok(self, button): - """ - Close this dialog and destroy it. - """ - self.dialog.destroy() diff --git a/cozy/ui/info_banner.py b/cozy/ui/info_banner.py deleted file mode 100644 index 5f3a986e..00000000 --- a/cozy/ui/info_banner.py +++ /dev/null @@ -1,23 +0,0 @@ -import gi - -from gi.repository import Gtk - -from cozy.ext import inject - - -class InfoBanner: - _builder: Gtk.Builder = inject.attr("MainWindowBuilder") - - def __init__(self): - super(InfoBanner, self).__init__() - - self._toast: Gtk.InfoBar = self._builder.get_object("error_info_bar") - self._label: Gtk.Label = self._builder.get_object("error_message_label") - self._toast.connect("response", self._on_response) - - def show(self, message: str): - self._label.set_text(message) - self._toast.set_revealed(True) - - def _on_response(self, _, __): - self._toast.set_revealed(False) \ No newline at end of file diff --git a/cozy/ui/library_view.py b/cozy/ui/library_view.py index 0695c398..3e530919 100644 --- a/cozy/ui/library_view.py +++ b/cozy/ui/library_view.py @@ -1,6 +1,7 @@ +import functools from typing import Optional -from gi.repository import Gtk, Handy +from gi.repository import Gtk, Adw from gi.repository.Gtk import Builder from cozy.ext import inject @@ -40,16 +41,16 @@ def __init__(self, builder: Builder): def _get_ui_elements(self): self._filter_stack: Gtk.Stack = self._builder.get_object("sort_stack") self._main_stack: Gtk.Stack = self._builder.get_object("main_stack") + self._navigation_view: Adw.NavigationView = self._builder.get_object("navigation_view") + self._split_view: Adw.OverlaySplitView = self._builder.get_object("split_view") self._book_box: Gtk.FlowBox = self._builder.get_object("book_box") self._filter_stack_revealer: Gtk.Revealer = self._builder.get_object("sort_stack_revealer") self._author_box: FilterListBox = self._builder.get_object("author_box") self._reader_box: FilterListBox = self._builder.get_object("reader_box") - self._library_leaflet: Handy.Leaflet = self._builder.get_object("library_leaflet") self._book_stack: Gtk.Stack = self._builder.get_object("book_stack") def _connect_ui_elements(self): self._filter_stack.connect("notify::visible-child", self._on_sort_stack_changed) - self._main_stack.connect("notify::visible-child", self._on_main_stack_changed) self._book_box.set_sort_func(self._view_model.display_book_sort) self._book_box.set_filter_func(self._view_model.display_book_filter) @@ -64,7 +65,6 @@ def _connect_ui_elements(self): def _connect_view_model(self): self._view_model.bind_to("library_view_mode", self._on_library_view_mode_changed) - self._view_model.bind_to("library_page", self._on_library_page_changed) self._view_model.bind_to("authors", self.populate_author) self._view_model.bind_to("readers", self.populate_reader) self._view_model.bind_to("books", self.populate_book_box) @@ -88,12 +88,6 @@ def _on_sort_stack_changed(self, widget, _): self._view_model.library_view_mode = view_mode - def _on_main_stack_changed(self, widget, _): - page = widget.props.visible_child_name - - if page != MAIN_BOOK_PAGE: - self._view_model.library_page = LibraryPage.NONE - def populate_book_box(self): self._book_box.remove_all_children() @@ -102,10 +96,7 @@ def populate_book_box(self): book_element.connect("play-pause-clicked", self._play_book_clicked) book_element.connect("open-book-overview", self._open_book_overview_clicked) book_element.connect("book-removed", self._on_book_removed) - book_element.show_all() - self._book_box.add(book_element) - - self._book_box.show_all() + self._book_box.append(book_element) def populate_author(self): self._author_box.populate(self._view_model.authors) @@ -139,20 +130,13 @@ def _on_library_view_mode_changed(self): self._main_stack.props.visible_child_name = main_view_page self._filter_stack.set_visible_child_name(visible_child_name) self._book_stack.set_visible_child_name(books_view_page) + self._navigation_view.pop_to_tag("main") if active_filter_box: self._apply_selected_filter(active_filter_box.get_selected_row()) self._invalidate_filters() - def _on_library_page_changed(self): - page = self._view_model.library_page - - if page == LibraryPage.FILTER: - self._library_leaflet.set_visible_child_name("filter") - elif page == LibraryPage.BOOKS: - self._library_leaflet.set_visible_child_name("books") - def _invalidate_filters(self): self._book_box.invalidate_filter() self._book_box.invalidate_sort() @@ -166,7 +150,6 @@ def _apply_selected_filter(self, row): def _on_filter_row_activated(self, _, row): self._apply_selected_filter(row) - self._view_model.library_page = LibraryPage.BOOKS return True @@ -181,17 +164,19 @@ def _play_book_clicked(self, _, book): def _open_book_overview_clicked(self, _, book): self._view_model.open_book_detail(book) - self._view_model.library_page = LibraryPage.NONE + return True def _on_book_removed(self, _, book): - delete_from_library = True - delete_files = False - if self._view_model.book_files_exist(book): - dialog = DeleteBookView() - delete_from_library = delete_files = dialog.get_delete_book() - dialog.destroy() + DeleteBookView(self._on_book_removed_clicked, book).present() + + def _on_book_removed_clicked(self, _, response, book): + if response != "delete": + return + + delete_from_library = True + delete_files = True # TODO: maybe an option to not delete the files if delete_files: self._view_model.delete_book_files(book) @@ -203,11 +188,14 @@ def _current_book_in_playback(self): if self._connected_book_element: self._connected_book_element.set_playing(False) - self._connected_book_element = next((book_element - for book_element - in self._book_box.get_children() - if book_element.book == self._view_model.current_book_in_playback), - None) + self._connected_book_element = None + + index = 0 + while book_element := self._book_box.get_child_at_index(index): + if book_element.book == self._view_model.current_book_in_playback: + self._connected_book_element = book_element + break + index += 1 def _playing(self): if self._connected_book_element: diff --git a/cozy/ui/list_box_row_with_data.py b/cozy/ui/list_box_row_with_data.py index fc1a227b..595a2e81 100644 --- a/cozy/ui/list_box_row_with_data.py +++ b/cozy/ui/list_box_row_with_data.py @@ -6,27 +6,23 @@ class ListBoxRowWithData(Gtk.ListBoxRow): This class represents a listboxitem for an author/reader. """ LABEL_MARGIN = 8 - ROW_MARGIN = 4 def __init__(self, data, bold=False, **properties): super().__init__(**properties) self.data = data - self.set_margin_top(self.ROW_MARGIN) - self.set_margin_bottom(self.ROW_MARGIN) - self.set_margin_start(self.ROW_MARGIN) - self.set_margin_end(self.ROW_MARGIN) + self.set_margin_bottom(3) - self.get_style_context().add_class("filter-list-box-row") + self.add_css_class("filter-list-box-row") - label: Gtk.Label = Gtk.Label.new(data) + label = Gtk.Label.new(data) if bold: label.set_markup("" + data + "") label.set_xalign(0.0) label.set_margin_top(self.LABEL_MARGIN) label.set_margin_bottom(self.LABEL_MARGIN) - label.set_margin_start(7) + label.set_margin_start(6) label.set_max_width_chars(30) label.set_ellipsize(Pango.EllipsizeMode.END) - self.add(label) + self.set_child(label) self.set_tooltip_text(data) diff --git a/cozy/ui/list_box_separator_row.py b/cozy/ui/list_box_separator_row.py index b0ff68bc..af1eb593 100644 --- a/cozy/ui/list_box_separator_row.py +++ b/cozy/ui/list_box_separator_row.py @@ -9,6 +9,6 @@ class ListBoxSeparatorRow(Gtk.ListBoxRow): def __init__(self): super().__init__() separator = Gtk.Separator() - self.add(separator) + self.set_child(separator) self.set_sensitive(False) self.props.selectable = False \ No newline at end of file diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index 22c7bf6e..e0b2e20a 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -3,7 +3,7 @@ import webbrowser from threading import Thread -from gi.repository import Gtk, Gio, Gdk, GLib +from gi.repository import Adw, Gtk, Gio, Gdk, GLib, GObject import cozy.control.filesystem_monitor as fs_monitor import cozy.ext.inject as inject @@ -62,7 +62,6 @@ def activate(self, library_view: LibraryView): def startup(self): self.__init_resources() - self.__init_css() def __init_resources(self): """ @@ -84,91 +83,48 @@ def __init_resources(self): self.window: Gtk.Window = self.window_builder.get_object("app_window") - def __init_css(self): - """ - Initialize the main css files and providers. - Add css classes to the default screen style context. - """ - main_cssProviderFile = Gio.File.new_for_uri( - "resource:///com/github/geigi/cozy/application.css") - main_cssProvider = Gtk.CssProvider() - main_cssProvider.load_from_file(main_cssProviderFile) - - if Gtk.get_minor_version() > 18: - log.debug("Fanciest design possible") - cssProviderFile = Gio.File.new_for_uri( - "resource:///com/github/geigi/cozy/application_default.css") - else: - log.debug("Using legacy css file") - cssProviderFile = Gio.File.new_for_uri( - "resource:///com/github/geigi/cozy/application_legacy.css") - cssProvider = Gtk.CssProvider() - cssProvider.load_from_file(cssProviderFile) - - # add the bordered css class to the default screen for the borders around album art - screen = Gdk.Screen.get_default() - styleContext = Gtk.StyleContext() - styleContext.add_provider_for_screen( - screen, main_cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) - styleContext.add_provider_for_screen( - screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) - styleContext.add_class("bordered") - def __init_window(self): """ - Add fields for all ui objects we need to access from code. - Initialize everything we can't do from glade like events and other stuff. + Add fields for all UI objects we need to access from code. + Initialize everything we can't do from the UI files like events and other stuff. """ - log.info("Initialize main window") + log.info("Initializing main window") self._restore_window_size() + self.window.set_title("Cozy") self.window.set_application(self.app) - self.window.show_all() - self.window.present() - self.window.connect("delete-event", self.on_close) - self.window.connect("drag_data_received", self.__on_drag_data_received) - self.window.connect("size-allocate", self._on_window_size_allocate) - self.window.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, - [Gtk.TargetEntry.new("text/uri-list", 0, 80)], Gdk.DragAction.COPY) - self.window.title = "Cozy" - - self.book_box = self.window_builder.get_object("book_box") + + self.window.connect("close-request", self.on_close) + self.window.connect("notify::default-width", self._on_window_size_allocate) + self.window.connect("notify::default-height", self._on_window_size_allocate) + + self._drop_target = Gtk.DropTarget() + self._drop_target.set_gtypes([Gdk.FileList]) + self._drop_target.set_actions(Gdk.DragAction.COPY) + self._drop_target.connect("enter", self._on_drag_enter) + self._drop_target.connect("leave", self._on_drag_leave) + self._drop_target.connect("drop", self._on_drag_data_received) + self.window.add_controller(self._drop_target) + self.main_stack: Gtk.Stack = self.window_builder.get_object("main_stack") + self.navigation_view: Adw.NavigationView = self.window_builder.get_object("navigation_view") + self.drop_revealer: Gtk.Revealer = self.window_builder.get_object("drop_revealer") - self.no_media_file_chooser = self.window_builder.get_object( - "no_media_file_chooser") - self.no_media_file_chooser.connect( - "clicked", self._open_audiobook_dir_selector) + self.no_media_file_chooser = self.window_builder.get_object("no_media_file_chooser") + self.no_media_file_chooser.connect("clicked", self._open_audiobook_dir_selector) - # get about dialog self.about_dialog = self.about_builder.get_object("about_dialog") self.about_dialog.set_modal(self.window) - self.about_dialog.connect("delete-event", self.hide_window) + self.about_dialog.connect("close-request", self.hide_window) self.about_dialog.set_version(self.version) - # shortcuts - self.accel = Gtk.AccelGroup() - - try: - about_close_button = self.about_builder.get_object( - "button_box").get_children()[2] - - if about_close_button: - about_close_button.connect( - "clicked", self.__about_close_clicked) - except Exception as e: - log.info("Not connecting about close button.") + self._preferences = PreferencesView() - self._preferences: PreferencesView = PreferencesView() + self.window.present() def __init_actions(self): """ Init all app actions. """ - self.accel = Gtk.AccelGroup() - - help_action = Gio.SimpleAction.new("help", None) - help_action.connect("activate", self.help) - self.app.add_action(help_action) about_action = Gio.SimpleAction.new("about", None) about_action.connect("activate", self.about) @@ -194,8 +150,9 @@ def __init_actions(self): self.app.add_action(self.play_pause_action) self.app.set_accels_for_action("app.play_pause", ["space"]) + # NavigationView.pop-on-escape doesn't work in some cases, so this is a hack back_action = Gio.SimpleAction.new("back", None) - back_action.connect("activate", self.back) + back_action.connect("activate", lambda *_: self.navigation_view.pop()) self.app.add_action(back_action) self.app.set_accels_for_action("app.back", ["Escape"]) @@ -215,12 +172,6 @@ def __init_components(self): def get_object(self, name): return self.window_builder.get_object(name) - def help(self, action, parameter): - """ - Show app help. - """ - webbrowser.open("https://github.com/geigi/cozy/issues", new=2) - def quit(self, action, parameter): """ Quit app. @@ -232,7 +183,25 @@ def about(self, action, parameter): """ Show about window. """ - self.about_dialog.show() + self.about_dialog.add_acknowledgement_section( + _("Patreon Supporters"), + ["Fred Warren", "Gabriel", "Hu Mann", "Josiah", "Oleksii Kriukov"] + ) + self.about_dialog.add_acknowledgement_section( + _("m4b chapter support in mutagen"), + ("mweinelt",), + ) + self.about_dialog.add_acknowledgement_section( + _("Open Source Projects"), + ("Lollypop music player https://gitlab.gnome.org/World/lollypop",), + ) + self.about_dialog.add_legal_section( + "python-inject", + "© 2010 Ivan Korobkov", + Gtk.License.APACHE_2_0 + ) + + self.about_dialog.present() def show_prefs(self, action, parameter): """ @@ -266,7 +235,7 @@ def block_ui_buttons(self, block, scan=False): if scan: self.scan_action.set_enabled(sensitive) self.hide_offline_action.set_enabled(sensitive) - except: + except GLib.GError: pass def switch_to_playing(self): @@ -274,15 +243,14 @@ def switch_to_playing(self): Switch the UI state back to playing. This enables all UI functionality for the user. """ - if self.main_stack.props.visible_child_name != "book_overview" and self.main_stack.props.visible_child_name != "no_media": - self.main_stack.props.visible_child_name = "main" + if self.navigation_view.props.visible_page != "book_overview" and self.main_stack.props.visible_child_name != "no_media": + self.navigation_view.pop_to_page("main") if self._player.loaded_book: self.block_ui_buttons(False, True) else: # we want to only block the player controls self.block_ui_buttons(False, True) self.block_ui_buttons(True, False) - self.window.props.window.set_cursor(None) self.emit_event_main_thread("working", False) def check_for_tracks(self): @@ -298,25 +266,17 @@ def _open_audiobook_dir_selector(self, __): if len(self._settings.storage_locations) > 0: path = self._settings.default_location.path - location_chooser: Gtk.FileChooserDialog = Gtk.FileChooserDialog(title=_("Set Audiobooks Directory"), - parent=self.window, - action=Gtk.FileChooserAction.SELECT_FOLDER) - location_chooser.add_buttons( - Gtk.STOCK_CANCEL, - Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, - Gtk.ResponseType.OK, - ) - location_chooser.set_select_multiple(False) - location_chooser.set_current_folder(path) - location_chooser.set_local_only(False) - response = location_chooser.run() - - if response == Gtk.ResponseType.OK: - audiobook_path = location_chooser.get_filename() - self._set_audiobook_path(audiobook_path) + location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) + location_chooser.select_folder(self.window, None, self._location_chooser_open_callback) - location_chooser.destroy() + def _location_chooser_open_callback(self, dialog, result): + try: + file = dialog.select_folder_finish(result) + except GLib.GError: + pass + else: + if file is not None: + self._set_audiobook_path(file.get_path()) def scan(self, _, __): thread = Thread(target=self._importer.scan, name="ScanMediaThread") @@ -326,9 +286,6 @@ def auto_import(self): if self.application_settings.autoscan: self.scan(None, None) - def back(self, action, parameter): - self.emit_event("open_view", OpenView.LIBRARY) - def __on_hide_offline(self, action, value): """ Show/Hide offline books action handler. @@ -336,14 +293,20 @@ def __on_hide_offline(self, action, value): action.set_state(value) self.application_settings.hide_offline = value.get_boolean() - def __on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp): - """ - We want to import the files that are dragged onto the window. - inspired by https://stackoverflow.com/questions/24094186/drag-and-drop-file-example-in-pygobject - """ - if target_type == 80: - thread = Thread(target=self._files.copy, args=[selection], name="DragDropImportThread") - thread.start() + def _on_drag_enter(self, *_): + self.drop_revealer.set_reveal_child(True) + self.main_stack.add_css_class("blurred") + return True + + def _on_drag_leave(self, *_): + self.drop_revealer.set_reveal_child(False) + self.main_stack.remove_css_class("blurred") + return True + + def _on_drag_data_received(self, widget, value, *_): + thread = Thread(target=self._files.copy, args=[value.get_files()], name="DnDImportThread") + thread.start() + return True def _set_audiobook_path(self, path): self._settings_view_model.add_first_storage_location(path) @@ -387,8 +350,9 @@ def _restore_window_size(self): else: self.window.unmaximize() - def _on_window_size_allocate(self, _, __): - width, height = self.window.get_size() + def _on_window_size_allocate(self, *_): + width, height = self.window.get_default_size() self.application_settings.window_width = width self.application_settings.window_height = height self.application_settings.window_maximize = self.window.is_maximized() + diff --git a/cozy/ui/media_controller.py b/cozy/ui/media_controller.py index cfb668bd..b5c9a506 100644 --- a/cozy/ui/media_controller.py +++ b/cozy/ui/media_controller.py @@ -2,21 +2,156 @@ import gi -from cozy.ui.media_controller_big import MediaControllerBig -from cozy.ui.media_controller_small import MediaControllerSmall +from cozy.control.artwork_cache import ArtworkCache +from cozy.db.book import Book +from cozy.ext import inject +from cozy.ui.widgets.playback_speed_popover import PlaybackSpeedPopover +from cozy.ui.widgets.seek_bar import SeekBar +from cozy.ui.widgets.sleep_timer import SleepTimer +from cozy.view_model.playback_control_view_model import PlaybackControlViewModel -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Handy +from gi.repository import Adw, Gtk, Gdk log = logging.getLogger("MediaController") +COVER_SIZE = 46 + + +@Gtk.Template.from_resource('/com/github/geigi/cozy/media_controller.ui') +class MediaController(Adw.BreakpointBin): + __gtype_name__ = "MediaController" + + seek_bar_container: Gtk.Box = Gtk.Template.Child() + + play_button: Gtk.Button = Gtk.Template.Child() + prev_button: Gtk.Button = Gtk.Template.Child() + next_button: Gtk.Button = Gtk.Template.Child() + volume_button: Gtk.ScaleButton = Gtk.Template.Child() + + cover_img: Gtk.Image = Gtk.Template.Child() + title_label: Gtk.Label = Gtk.Template.Child() + subtitle_label: Gtk.Label = Gtk.Template.Child() + + playback_speed_button: Gtk.MenuButton = Gtk.Template.Child() + timer_button: Gtk.MenuButton = Gtk.Template.Child() + + timer_image: Gtk.Image = Gtk.Template.Child() -class MediaController: def __init__(self, main_window_builder: Gtk.Builder): super().__init__() - self._media_control_squeezer: Handy.Squeezer = main_window_builder.get_object("media_control_squeezer") - self._media_controller_small: MediaControllerSmall = MediaControllerSmall() - self._media_controller_big: MediaControllerBig = MediaControllerBig() - self._media_control_squeezer.add(self._media_controller_big) - self._media_control_squeezer.add(self._media_controller_small) + media_control_box: Gtk.Box = main_window_builder.get_object("media_control_box") + media_control_box.append(self) + + self.seek_bar = SeekBar() + self.seek_bar_container.append(self.seek_bar) + + self.sleep_timer: SleepTimer = SleepTimer(self.timer_image) + self.playback_speed_button.set_popover(PlaybackSpeedPopover()) + self.timer_button.set_popover(self.sleep_timer) + + self._playback_control_view_model: PlaybackControlViewModel = inject.instance(PlaybackControlViewModel) + self._artwork_cache: ArtworkCache = inject.instance(ArtworkCache) + self._connect_view_model() + self._connect_widgets() + + self._on_book_changed() + self._on_lock_ui_changed() + self._on_length_changed() + self._on_position_changed() + self._on_volume_changed() + + def _connect_view_model(self): + self._playback_control_view_model.bind_to("book", self._on_book_changed) + self._playback_control_view_model.bind_to("playing", self._on_play_changed) + self._playback_control_view_model.bind_to("length", self._on_length_changed) + self._playback_control_view_model.bind_to("position", self._on_position_changed) + self._playback_control_view_model.bind_to("lock_ui", self._on_lock_ui_changed) + self._playback_control_view_model.bind_to("volume", self._on_volume_changed) + + def _connect_widgets(self): + self.play_button.connect("clicked", self._play_clicked) + self.prev_button.connect("clicked", self._rewind_clicked) + self.next_button.connect("clicked", self._forward_clicked) + self.volume_button.connect("value-changed", self._on_volume_button_changed) + self.seek_bar.connect("position-changed", self._on_seek_bar_position_changed) + + self._cover_img_gesture = Gtk.GestureClick() + self._cover_img_gesture.connect("pressed", self._cover_clicked) + self.cover_img.add_controller(self._cover_img_gesture) + + self.cover_img.set_cursor(Gdk.Cursor.new_from_name("pointer")) + + def _set_cover_image(self, book: Book): + paintable = self._artwork_cache.get_cover_paintable(book, self.get_scale_factor(), COVER_SIZE) + if paintable: + self.cover_img.set_from_paintable(paintable) + else: + self.cover_img.set_from_icon_name("book-open-variant-symbolic") + self.cover_img.props.pixel_size = COVER_SIZE + + def _on_book_changed(self) -> None: + book = self._playback_control_view_model.book + self._set_book(book) + self._show_media_information(bool(book)) + + def _show_media_information(self, visibility: bool) -> None: + self.title_label.set_visible(visibility) + self.subtitle_label.set_visible(visibility) + self.cover_img.set_visible(visibility) + self.seek_bar.visible = visibility + + def _set_book(self, book: Book) -> None: + if book is not None: + self._set_cover_image(book) + self.title_label.set_text(book.name) + self.title_label.set_tooltip_text(book.name) + self.subtitle_label.set_text(book.current_chapter.name) + self.subtitle_label.set_tooltip_text(book.current_chapter.name) + + def _on_play_changed(self): + if self._playback_control_view_model.playing: + self.play_button.set_icon_name("media-playback-pause-symbolic") + else: + self.play_button.set_icon_name("media-playback-start-symbolic") + + def _on_position_changed(self): + position = self._playback_control_view_model.position + if position is not None: + self.seek_bar.position = position + + def _on_length_changed(self): + length = self._playback_control_view_model.length + if length: + self.seek_bar.length = length + + def _on_lock_ui_changed(self): + sensitive = not self._playback_control_view_model.lock_ui + self.seek_bar.sensitive = sensitive + self.prev_button.set_sensitive(sensitive) + self.next_button.set_sensitive(sensitive) + self.play_button.set_sensitive(sensitive) + self.volume_button.set_sensitive(sensitive) + self.playback_speed_button.set_sensitive(sensitive) + self.timer_button.set_sensitive(sensitive) + + def _on_volume_changed(self): + self.volume_button.set_value(self._playback_control_view_model.volume) + + def _play_clicked(self, *_): + self._playback_control_view_model.play_pause() + + def _rewind_clicked(self, *_): + self._playback_control_view_model.rewind() + + def _forward_clicked(self, *_): + self._playback_control_view_model.forward() + + def _cover_clicked(self, *_): + self._playback_control_view_model.open_book_detail() + + def _on_volume_button_changed(self, _, volume): + self._playback_control_view_model.volume = volume + + def _on_seek_bar_position_changed(self, _, position): + self._playback_control_view_model.position = position diff --git a/cozy/ui/media_controller_big.py b/cozy/ui/media_controller_big.py deleted file mode 100644 index 89ef9e9a..00000000 --- a/cozy/ui/media_controller_big.py +++ /dev/null @@ -1,175 +0,0 @@ -import logging - -import gi - -from cozy.control.artwork_cache import ArtworkCache -from cozy.db.book import Book -from cozy.ext import inject -from cozy.ui.widgets.playback_speed_popover import PlaybackSpeedPopover -from cozy.ui.widgets.seek_bar import SeekBar -from cozy.ui.widgets.sleep_timer import SleepTimer -from cozy.view_model.playback_control_view_model import PlaybackControlViewModel - -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - -log = logging.getLogger("Headerbar") - -COVER_SIZE = 46 - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/media_controller_big.ui') -class MediaControllerBig(Gtk.Box): - __gtype_name__ = "MediaControllerBig" - - seek_bar_container: Gtk.Box = Gtk.Template.Child() - - play_button: Gtk.Button = Gtk.Template.Child() - prev_button: Gtk.Button = Gtk.Template.Child() - next_button: Gtk.Button = Gtk.Template.Child() - volume_button: Gtk.VolumeButton = Gtk.Template.Child() - - cover_img: Gtk.Image = Gtk.Template.Child() - cover_img_event_box: Gtk.EventBox = Gtk.Template.Child() - title_label: Gtk.Label = Gtk.Template.Child() - subtitle_label: Gtk.Label = Gtk.Template.Child() - - playback_speed_button: Gtk.MenuButton = Gtk.Template.Child() - timer_button: Gtk.MenuButton = Gtk.Template.Child() - - timer_image: Gtk.Image = Gtk.Template.Child() - play_img: Gtk.Image = Gtk.Template.Child() - - def __init__(self): - super().__init__() - - self.seek_bar = SeekBar() - self.seek_bar_container.add(self.seek_bar) - - self.sleep_timer: SleepTimer = SleepTimer(self.timer_image) - self.playback_speed_button.set_popover(PlaybackSpeedPopover()) - self.timer_button.set_popover(self.sleep_timer) - - self._playback_control_view_model: PlaybackControlViewModel = inject.instance(PlaybackControlViewModel) - self._artwork_cache: ArtworkCache = inject.instance(ArtworkCache) - self._connect_view_model() - self._connect_widgets() - - self._on_book_changed() - self._on_lock_ui_changed() - self._on_length_changed() - self._on_position_changed() - self._on_volume_changed() - - def _connect_view_model(self): - self._playback_control_view_model.bind_to("book", self._on_book_changed) - self._playback_control_view_model.bind_to("playing", self._on_play_changed) - self._playback_control_view_model.bind_to("length", self._on_length_changed) - self._playback_control_view_model.bind_to("position", self._on_position_changed) - self._playback_control_view_model.bind_to("lock_ui", self._on_lock_ui_changed) - self._playback_control_view_model.bind_to("volume", self._on_volume_changed) - - def _connect_widgets(self): - self.play_button.connect("clicked", self._play_clicked) - self.prev_button.connect("clicked", self._rewind_clicked) - self.next_button.connect("clicked", self._forward_clicked) - self.volume_button.connect("value-changed", self._on_volume_button_changed) - self.seek_bar.connect("position-changed", self._on_seek_bar_position_changed) - self.cover_img_event_box.connect("button-press-event", self._cover_clicked) - self.cover_img_event_box.connect("enter-notify-event", self._on_cover_enter_notify) - self.cover_img_event_box.connect("leave-notify-event", self._on_cover_leave_notify) - - def _set_cover_image(self, book: Book): - pixbuf = self._artwork_cache.get_cover_pixbuf(book, self.get_scale_factor(), COVER_SIZE) - if pixbuf: - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.get_scale_factor(), None) - self.cover_img.set_from_surface(surface) - else: - self.cover_img.set_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.DIALOG) - self.cover_img.props.pixel_size = COVER_SIZE - - def _on_book_changed(self): - book = self._playback_control_view_model.book - if book: - visibility = True - self._set_book() - else: - visibility = False - - self._show_media_information(visibility) - - def _show_media_information(self, visibility): - self.title_label.set_visible(visibility) - self.subtitle_label.set_visible(visibility) - self.cover_img.set_visible(visibility) - self.seek_bar.visible = visibility - - def _set_book(self): - book = self._playback_control_view_model.book - - self._set_cover_image(book) - self.title_label.set_text(book.name) - self.title_label.set_tooltip_text(book.name) - self.subtitle_label.set_text(book.current_chapter.name) - self.subtitle_label.set_tooltip_text(book.current_chapter.name) - - def _on_play_changed(self): - playing = self._playback_control_view_model.playing - - play_button_img = "pause-symbolic" if playing else "play-symbolic" - icon_size = 16 if playing else 20 - self.play_img.set_from_icon_name(play_button_img, Gtk.IconSize.LARGE_TOOLBAR) - self.play_img.set_pixel_size(icon_size) - - def _on_position_changed(self): - position = self._playback_control_view_model.position - if position is not None: - self.seek_bar.position = position - - def _on_length_changed(self): - length = self._playback_control_view_model.length - if length: - self.seek_bar.length = length - - def _on_lock_ui_changed(self): - sensitive = not self._playback_control_view_model.lock_ui - self.seek_bar.sensitive = sensitive - self.prev_button.set_sensitive(sensitive) - self.next_button.set_sensitive(sensitive) - self.play_button.set_sensitive(sensitive) - self.volume_button.set_sensitive(sensitive) - self.playback_speed_button.set_sensitive(sensitive) - self.timer_button.set_sensitive(sensitive) - - def _on_volume_changed(self): - self.volume_button.set_value(self._playback_control_view_model.volume) - - def _play_clicked(self, _): - self._playback_control_view_model.play_pause() - - def _rewind_clicked(self, _): - self._playback_control_view_model.rewind() - - def _forward_clicked(self, _): - self._playback_control_view_model.forward() - - def _back_clicked(self, _): - self._playback_control_view_model.navigate_back() - - def _cover_clicked(self, _, __): - self._playback_control_view_model.open_book_detail() - - def _on_cover_enter_notify(self, widget: Gtk.Widget, __): - try: - widget.props.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - except: - log.error("Broken mouse theme, failed to set cursor.") - - def _on_cover_leave_notify(self, widget: Gtk.Widget, __): - widget.props.window.set_cursor(None) - - def _on_volume_button_changed(self, _, volume): - self._playback_control_view_model.volume = volume - - def _on_seek_bar_position_changed(self, _, position): - self._playback_control_view_model.position = position diff --git a/cozy/ui/media_controller_small.py b/cozy/ui/media_controller_small.py deleted file mode 100644 index d375136c..00000000 --- a/cozy/ui/media_controller_small.py +++ /dev/null @@ -1,121 +0,0 @@ -import logging - -import gi - -from cozy.control.artwork_cache import ArtworkCache -from cozy.db.book import Book -from cozy.ext import inject -from cozy.ui.widgets.playback_speed_popover import PlaybackSpeedPopover -from cozy.view_model.playback_control_view_model import PlaybackControlViewModel - -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - -log = logging.getLogger("Headerbar") - -COVER_SIZE = 46 - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/media_controller_small.ui') -class MediaControllerSmall(Gtk.Box): - __gtype_name__ = "MediaControllerSmall" - - play_button: Gtk.Button = Gtk.Template.Child() - prev_button: Gtk.Button = Gtk.Template.Child() - next_button: Gtk.Button = Gtk.Template.Child() - - cover_img: Gtk.Image = Gtk.Template.Child() - cover_img_event_box: Gtk.EventBox = Gtk.Template.Child() - - playback_speed_button: Gtk.MenuButton = Gtk.Template.Child() - - play_img: Gtk.Image = Gtk.Template.Child() - - def __init__(self): - super().__init__() - - self.playback_speed_button.set_popover(PlaybackSpeedPopover()) - - self._playback_control_view_model: PlaybackControlViewModel = inject.instance(PlaybackControlViewModel) - self._artwork_cache: ArtworkCache = inject.instance(ArtworkCache) - self._connect_view_model() - self._connect_widgets() - - self._on_book_changed() - self._on_lock_ui_changed() - - def _connect_view_model(self): - self._playback_control_view_model.bind_to("book", self._on_book_changed) - self._playback_control_view_model.bind_to("playing", self._on_play_changed) - self._playback_control_view_model.bind_to("lock_ui", self._on_lock_ui_changed) - - def _connect_widgets(self): - self.play_button.connect("clicked", self._play_clicked) - self.prev_button.connect("clicked", self._rewind_clicked) - self.next_button.connect("clicked", self._forward_clicked) - self.cover_img_event_box.connect("button-press-event", self._cover_clicked) - self.cover_img_event_box.connect("enter-notify-event", self._on_cover_enter_notify) - self.cover_img_event_box.connect("leave-notify-event", self._on_cover_leave_notify) - - def _set_cover_image(self, book: Book): - pixbuf = self._artwork_cache.get_cover_pixbuf(book, self.get_scale_factor(), COVER_SIZE) - if pixbuf: - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.get_scale_factor(), None) - self.cover_img.set_from_surface(surface) - else: - self.cover_img.set_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.DIALOG) - self.cover_img.props.pixel_size = COVER_SIZE - - def _on_book_changed(self): - book = self._playback_control_view_model.book - if book: - visibility = True - self._set_book() - else: - visibility = False - - self._show_media_information(visibility) - - def _show_media_information(self, visibility): - self.cover_img.set_visible(visibility) - - def _set_book(self): - book = self._playback_control_view_model.book - - self._set_cover_image(book) - - def _on_play_changed(self): - playing = self._playback_control_view_model.playing - - play_button_img = "pause-symbolic" if playing else "play-symbolic" - icon_size = 16 if playing else 20 - self.play_img.set_from_icon_name(play_button_img, Gtk.IconSize.LARGE_TOOLBAR) - self.play_img.set_pixel_size(icon_size) - - def _on_lock_ui_changed(self): - sensitive = not self._playback_control_view_model.lock_ui - self.prev_button.set_sensitive(sensitive) - self.next_button.set_sensitive(sensitive) - self.play_button.set_sensitive(sensitive) - self.playback_speed_button.set_sensitive(sensitive) - - def _play_clicked(self, _): - self._playback_control_view_model.play_pause() - - def _rewind_clicked(self, _): - self._playback_control_view_model.rewind() - - def _forward_clicked(self, _): - self._playback_control_view_model.forward() - - def _cover_clicked(self, _, __): - self._playback_control_view_model.open_book_detail() - - def _on_cover_enter_notify(self, widget: Gtk.Widget, __): - try: - widget.props.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - except: - log.error("Broken mouse theme, failed to set cursor.") - - def _on_cover_leave_notify(self, widget: Gtk.Widget, __): - widget.props.window.set_cursor(None) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index c8bcbded..709488a1 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -1,27 +1,27 @@ from gi.repository import Gtk from cozy.view_model.settings_view_model import SettingsViewModel import gi -from gi.repository import Handy, Gio +from gi.repository import Adw, Gio from cozy.ext import inject from cozy.ui.widgets.error_reporting import ErrorReporting from cozy.ui.widgets.storage_list_box_row import StorageListBoxRow -gi.require_version('Gtk', '3.0') - @Gtk.Template.from_resource('/com/github/geigi/cozy/preferences.ui') -class PreferencesView(Handy.PreferencesWindow): +class PreferencesView(Adw.PreferencesWindow): __gtype_name__ = "PreferencesWindow" + main_window = inject.attr("MainWindow") + _glib_settings: Gio.Settings = inject.attr(Gio.Settings) _view_model: SettingsViewModel = inject.attr(SettingsViewModel) dark_mode_switch: Gtk.Switch = Gtk.Template.Child() swap_author_reader_switch: Gtk.Switch = Gtk.Template.Child() replay_switch: Gtk.Switch = Gtk.Template.Child() - sleep_timer_fadeout_switch: Gtk.Switch = Gtk.Template.Child() - sleep_timer_fadeout_row: Handy.ActionRow = Gtk.Template.Child() + sleep_timer_fadeout_switch: Adw.SwitchRow = Gtk.Template.Child() + fadeout_duration_spin_button: Adw.SpinRow = Gtk.Template.Child() artwork_prefer_external_switch: Gtk.Switch = Gtk.Template.Child() rewind_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() @@ -34,22 +34,22 @@ class PreferencesView(Handy.PreferencesWindow): external_storage_toggle_button: Gtk.ToggleButton = Gtk.Template.Child() default_storage_button: Gtk.ToggleButton = Gtk.Template.Child() - user_feedback_preference_row: Handy.PreferencesRow = Gtk.Template.Child() + user_feedback_preference_group: Adw.PreferencesRow = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) error_reporting = ErrorReporting() error_reporting.show_header(False) - self.user_feedback_preference_row.add(error_reporting) + self.user_feedback_preference_group.add(error_reporting) self._bind_settings() self._bind_view_model() - self.connect("delete-event", self._hide_window) + self.connect("close-request", self._hide_window) - self.sleep_timer_fadeout_switch.connect("state-set", self._on_sleep_fadeout_switch_changed) - self._on_sleep_fadeout_switch_changed(None, self.sleep_timer_fadeout_switch.get_active()) + self.sleep_timer_fadeout_switch.connect("notify::active", self._on_sleep_fadeout_switch_changed) + self.fadeout_duration_spin_button.set_sensitive(self.sleep_timer_fadeout_switch.props.active) self.storage_list_box.connect("row-selected", self._on_storage_box_changed) @@ -58,6 +58,8 @@ def __init__(self, **kwargs): self.external_button_handle_id = self.external_storage_toggle_button.connect("clicked", self._on_external_clicked) self.default_storage_button.connect("clicked", self._on_default_storage_clicked) + self.set_transient_for(self.main_window.window) + self._init_storage_box() def _bind_view_model(self): @@ -86,8 +88,9 @@ def _bind_settings(self): self._glib_settings.bind("prefer-external-cover", self.artwork_prefer_external_switch, "active", Gio.SettingsBindFlags.DEFAULT) - def _on_sleep_fadeout_switch_changed(self, _, state: bool): - self.sleep_timer_fadeout_row.set_sensitive(state) + def _on_sleep_fadeout_switch_changed(self, widget, param): + state = widget.get_property(param.name) + self.fadeout_duration_spin_button.set_sensitive(state) def _init_storage_box(self): self.storage_list_box.remove_all_children() @@ -95,7 +98,7 @@ def _init_storage_box(self): for storage in self._view_model.storage_locations: row = StorageListBoxRow(storage) row.connect("location-changed", self._on_storage_location_changed) - self.storage_list_box.add(row) + self.storage_list_box.append(row) def _on_add_storage_clicked(self, _): self._view_model.add_storage_location() @@ -143,8 +146,7 @@ def _on_storage_location_changed(self, widget, new_location): self._view_model.change_storage_location(widget.model, new_location) def _refresh_storage_rows(self): - for row in self.storage_list_box.get_children(): - row.refresh() + self._init_storage_box() self._on_storage_box_changed(None, self.storage_list_box.get_selected_row()) @@ -158,6 +160,6 @@ def _on_lock_ui_changed(self): self.default_storage_button.set_sensitive(sensitive) self._on_storage_box_changed(None, self.storage_list_box.get_selected_row()) - def _hide_window(self, _, __): + def _hide_window(self, *_): self.hide() return True diff --git a/cozy/ui/search_view.py b/cozy/ui/search_view.py index e6d206c8..9d20a8ee 100644 --- a/cozy/ui/search_view.py +++ b/cozy/ui/search_view.py @@ -4,11 +4,8 @@ from cozy.ext import inject from cozy.ui.widgets.search_results import BookSearchResult, ArtistSearchResult -import gi - from cozy.view_model.search_view_model import SearchViewModel -gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib @@ -42,11 +39,10 @@ def __init__(self): self.entry.connect("search-changed", self.__on_search_changed) - if Gtk.get_minor_version() > 20: - self.scroller.set_max_content_width(400) - self.scroller.set_max_content_height(600) - self.scroller.set_propagate_natural_height(True) - self.scroller.set_propagate_natural_width(True) + self.scroller.set_max_content_width(400) + self.scroller.set_max_content_height(600) + self.scroller.set_propagate_natural_height(True) + self.scroller.set_propagate_natural_width(True) self.search_thread = Thread(target=self.search, name="SearchThread") self.search_thread_stop = threading.Event() @@ -100,12 +96,6 @@ def search(self, user_search: str): main_context.invoke_full( GLib.PRIORITY_DEFAULT, self.stack.set_visible_child_name, "nothing") - def close(self, object=None): - if Gtk.get_minor_version() < 22: - self.popover.hide() - else: - self.popover.popdown() - def __on_search_changed(self, sender): self.search_thread_stop.set() @@ -148,7 +138,7 @@ def __on_search_changed(self, sender): def _on_search_open_changed(self): if self.view_model.search_open == False: - self.close() + self.popover.popdown() def __on_book_search_finished(self, books): if len(books) > 0: @@ -161,7 +151,7 @@ def __on_book_search_finished(self, books): return book_result = BookSearchResult(book, self.view_model.jump_to_book) - self.book_box.add(book_result) + self.book_box.append(book_result) def __on_author_search_finished(self, authors): if len(authors) > 0: @@ -174,7 +164,7 @@ def __on_author_search_finished(self, authors): return author_result = ArtistSearchResult(self.view_model.jump_to_author, author, True) - self.author_box.add(author_result) + self.author_box.append(author_result) def __on_reader_search_finished(self, readers): if len(readers) > 0: @@ -187,6 +177,6 @@ def __on_reader_search_finished(self, readers): return reader_result = ArtistSearchResult(self.view_model.jump_to_reader, reader, False) - self.reader_box.add(reader_result) + self.reader_box.append(reader_result) self.popover.set_size_request(-1, -1) diff --git a/cozy/ui/toaster.py b/cozy/ui/toaster.py new file mode 100644 index 00000000..e3c20065 --- /dev/null +++ b/cozy/ui/toaster.py @@ -0,0 +1,16 @@ +from gi.repository import Adw, Gtk + +from cozy.ext import inject + + +class ToastNotifier: + _builder: Gtk.Builder = inject.attr("MainWindowBuilder") + + def __init__(self) -> None: + super().__init__() + + self.overlay: Adw.ToastOverlay = self._builder.get_object("toast_overlay") + + def show(self, message: str) -> None: + self.overlay.add_toast(Adw.Toast(title=message, timeout=2)) + diff --git a/cozy/ui/warnings.py b/cozy/ui/warnings.py deleted file mode 100644 index 66f699f2..00000000 --- a/cozy/ui/warnings.py +++ /dev/null @@ -1,54 +0,0 @@ -from gettext import gettext - -import gi -import cozy.ext.inject as inject - -from cozy.control.filesystem_monitor import FilesystemMonitor - -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk - - -class Warnings(): - _fs_monitor: FilesystemMonitor = inject.attr("FilesystemMonitor") - - def __init__(self, button: Gtk.MenuButton): - self.button = button - - self.builder = Gtk.Builder.new_from_resource( - "/com/github/geigi/cozy/warning_popover.ui") - - self.popover = self.builder.get_object("warning_popover") - self.warning_container: Gtk.Box = self.builder.get_object("warning_container") - - self._fs_monitor.add_listener(self.__on_storage_changed) - - for storage in self._fs_monitor.get_offline_storages(): - self.append_text(gettext('{storage} is offline.').format(storage=storage)) - - self.__hide_show_button() - - def get_popover(self): - return self.popover - - def append_text(self, text): - label = Gtk.Label() - self.warning_container.add(label) - label.set_visible(True) - label.set_text(text) - - def __on_storage_changed(self, event, message): - if event == "storage-offline": - self.append_text(gettext('{storage} is offline.').format(storage=message)) - if event == "storage-online": - for label in self.warning_container.get_children(): - if message in label.get_text(): - self.warning_container.remove(label) - - self.__hide_show_button() - - def __hide_show_button(self): - if len(self.warning_container.get_children()) > 0: - self.button.set_visible(True) - else: - self.button.set_visible(False) \ No newline at end of file diff --git a/cozy/ui/widgets/ScrollWrapper.py b/cozy/ui/widgets/ScrollWrapper.py deleted file mode 100644 index 1deb1910..00000000 --- a/cozy/ui/widgets/ScrollWrapper.py +++ /dev/null @@ -1,20 +0,0 @@ -import gi - -gi.require_version('Gtk', '3.0') - -from gi.repository import Gtk - - -class ScrollWrapper(Gtk.ScrolledWindow): - def __init__(self, child: Gtk.Widget, **kwargs): - super().__init__(**kwargs) - self.show() - self.viewport = Gtk.Viewport() - self.viewport.show() - - self.add(self.viewport) - self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.set_margin_start(20) - self.set_margin_end(20) - - self.viewport.add(child) diff --git a/cozy/ui/widgets/album_art.py b/cozy/ui/widgets/album_art.py deleted file mode 100644 index 21d6c138..00000000 --- a/cozy/ui/widgets/album_art.py +++ /dev/null @@ -1,46 +0,0 @@ -import math - -import cairo -from gi.repository import Gtk, GdkPixbuf, Gdk - -RADIUS = 5 - - -class AlbumArt(Gtk.DrawingArea): - def __init__(self, **properties): - super().__init__(**properties) - - self.art = None - - self.set_visible(True) - self.connect("draw", self._on_draw) - - def set_art(self, art: GdkPixbuf): - self.art = art - scale = self.get_scale_factor() - self.set_size_request(art.get_width() / scale, art.get_height() / scale) - self.queue_draw() - - def _draw_rounded_path(self, context: cairo.Context): - degrees = math.pi / 180.0 - - context.new_sub_path() - context.arc(self.width - RADIUS, RADIUS, RADIUS, -90 * degrees, 0 * degrees) - context.arc(self.width - RADIUS, self.height - RADIUS, RADIUS, 0 * degrees, 90 * degrees) - context.arc(RADIUS, self.height - RADIUS, RADIUS, 90 * degrees, 180 * degrees) - context.arc(RADIUS, RADIUS, RADIUS, 180 * degrees, 270 * degrees) - context.close_path() - - def _on_draw(self, area: Gtk.DrawingArea, context: cairo.Context): - self.height = area.get_allocated_height() - self.width = area.get_allocated_width() - - self._draw_rounded_path(context) - - if self.art: - surface = Gdk.cairo_surface_create_from_pixbuf(self.art, self.get_scale_factor(), None) - context.set_source_surface(surface, 0, 0) - context.clip() - - context.paint() - return False diff --git a/cozy/ui/widgets/album_element.py b/cozy/ui/widgets/album_element.py index c403114b..11a84a1d 100644 --- a/cozy/ui/widgets/album_element.py +++ b/cozy/ui/widgets/album_element.py @@ -4,14 +4,13 @@ import cairo from cozy.control.artwork_cache import ArtworkCache -from cozy.extensions.gtk_widget import set_hand_cursor, reset_cursor from cozy.model.book import Book from cozy.ext import inject from gi.repository import Gtk, GObject, Gdk ALBUM_ART_SIZE = 200 -PLAY_BUTTON_ICON_SIZE = Gtk.IconSize.SMALL_TOOLBAR +PLAY_BUTTON_ICON_SIZE = Gtk.IconSize.NORMAL STROKE_WIDTH = 3 log = logging.getLogger("album_element") @@ -23,7 +22,6 @@ class AlbumElement(Gtk.Box): artwork_cache: ArtworkCache = inject.attr(ArtworkCache) - button_image: Gtk.Image = Gtk.Template.Child() album_art_image: Gtk.Image = Gtk.Template.Child() play_button: Gtk.Button = Gtk.Template.Child() progress_drawing_area: Gtk.DrawingArea = Gtk.Template.Child() @@ -35,44 +33,41 @@ def __init__(self, book: Book): super().__init__() self._book: Book = book - pixbuf = self.artwork_cache.get_cover_pixbuf(book, self.get_scale_factor(), ALBUM_ART_SIZE) + paintable = self.artwork_cache.get_cover_paintable(book, 1, ALBUM_ART_SIZE) - if pixbuf: - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.get_scale_factor(), None) - self.album_art_image.set_from_surface(surface) + if paintable: + self.album_art_image.set_from_paintable(paintable) + self.album_art_image.set_size_request(ALBUM_ART_SIZE, ALBUM_ART_SIZE) else: - self.album_art_image.set_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.DIALOG) + self.album_art_image.set_from_icon_name("book-open-variant-symbolic") self.album_art_image.props.pixel_size = ALBUM_ART_SIZE - self.set_size_request(ALBUM_ART_SIZE, ALBUM_ART_SIZE) - self.play_button.connect("button-release-event", self._on_play_button_press) + self.play_button.connect("clicked", self._on_play_button_press) - self.progress_drawing_area.connect("realize", lambda w: w.get_window().set_pass_through(True)) - self.progress_drawing_area.connect("draw", self._draw_progress) - self.album_art_drawing_area.connect("draw", self._draw_album_hover) - self.album_art_overlay_revealer.connect("enter-notify-event", self._on_revealer_enter_event) - self.play_button_revealer.connect("enter-notify-event", self._on_revealer_enter_event) + # TODO: Just use CSS + #self.progress_drawing_area.connect("realize", lambda w: w.get_window().set_pass_through(True)) + self.progress_drawing_area.set_draw_func(self._draw_progress) + #self.album_art_drawing_area.set_draw_func(self._draw_album_hover) def set_playing(self, playing: bool): if playing: - self.button_image.set_from_icon_name("pause-symbolic", PLAY_BUTTON_ICON_SIZE) + self.play_button.set_icon_name("media-playback-pause-symbolic") else: - self.button_image.set_from_icon_name("play-symbolic", PLAY_BUTTON_ICON_SIZE) + self.play_button.set_icon_name("media-playback-start-symbolic") def set_hover(self, hover: bool): self.album_art_overlay_revealer.set_reveal_child(hover) self.play_button_revealer.set_reveal_child(hover) - def _on_play_button_press(self, _, __): + def _on_play_button_press(self, _): self.emit("play-pause-clicked", self._book) - return True - def _draw_album_hover(self, area: Gtk.DrawingArea, context: cairo.Context): + def _draw_album_hover(self, area: Gtk.DrawingArea, context: cairo.Context, *_): context.rectangle(0, 0, area.get_allocated_width(), area.get_allocated_height()) context.set_source_rgba(1, 1, 1, 0.15) context.fill() - def _draw_progress(self, area: Gtk.DrawingArea, context: cairo.Context): + def _draw_progress(self, area: Gtk.DrawingArea, context: cairo.Context, *_): width = area.get_allocated_width() height = area.get_allocated_height() button_size = self.play_button.get_allocated_width() @@ -101,13 +96,6 @@ def draw_background(self, area: Gtk.DrawingArea, context: cairo.Context): def update_progress(self): self.progress_drawing_area.queue_draw() - def _on_revealer_enter_event(self, widget, _): - # Somehow the GTK Revealer steals the mouse events from the parent - # Maybe this is a bug in GTK but for now we have to handle hover in here as well - self.set_hover(True) - set_hand_cursor(widget) - return True - GObject.type_register(AlbumElement) GObject.signal_new('play-pause-clicked', AlbumElement, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, diff --git a/cozy/ui/widgets/book_element.py b/cozy/ui/widgets/book_element.py index 4ee2795b..8bb61fe5 100644 --- a/cozy/ui/widgets/book_element.py +++ b/cozy/ui/widgets/book_element.py @@ -1,14 +1,8 @@ -import os -import subprocess +from gi.repository import Gtk, GObject, Gdk, Gio -from gi.repository import Gtk, GObject, Gdk - -from cozy.extensions.gtk_widget import set_hand_cursor, reset_cursor from cozy.model.book import Book from cozy.ui.widgets.album_element import AlbumElement -MAX_LABEL_LENGTH = 60 - @Gtk.Template.from_resource('/com/github/geigi/cozy/book_element.ui') class BookElement(Gtk.FlowBoxChild): @@ -17,29 +11,51 @@ class BookElement(Gtk.FlowBoxChild): name_label: Gtk.Label = Gtk.Template.Child() author_label: Gtk.Label = Gtk.Template.Child() container_box: Gtk.Box = Gtk.Template.Child() - event_box: Gtk.Box = Gtk.Template.Child() def __init__(self, book: Book): - self.book: Book = book - super().__init__() + self.book = book + self.pressed = False + self.name_label.set_text(book.name) self.name_label.set_tooltip_text(book.name) self.author_label.set_text(book.author) self.author_label.set_tooltip_text(book.author) - self.art: AlbumElement = AlbumElement(self.book) - self.context_menu = None - self.pressed = False - - self.container_box.pack_start(self.art, False, True, 0) + self.art = AlbumElement(self.book) self.art.connect("play-pause-clicked", self._on_album_art_press_event) - self.event_box.connect("button-press-event", self._on_button_press_event) - self.event_box.connect("button-release-event", self._on_button_release_event) - self.event_box.connect("key-press-event", self._on_key_press_event) - self.event_box.connect("enter-notify-event", self._on_cover_enter_notify) - self.event_box.connect("leave-notify-event", self._on_cover_leave_notify) + + self.container_box.prepend(self.art) + self.set_cursor(Gdk.Cursor.new_from_name("pointer")) + + self._add_event_controllers() + + def _add_event_controllers(self): + primary_button_gesture = Gtk.GestureClick(button=Gdk.BUTTON_PRIMARY) + # primary_button_gesture.connect("pressed", self._select_item) + primary_button_gesture.connect("released", self._open_book_overview) + self.container_box.add_controller(primary_button_gesture) + + secondary_button_gesture = Gtk.GestureClick(button=Gdk.BUTTON_SECONDARY) + secondary_button_gesture.connect("released", self._show_context_menu) + self.container_box.add_controller(secondary_button_gesture) + + # FIXME: When clicking on an album's play button in the recents view, + # it jumps to the first position, and GtkGestureLongPress thinks it's + # a long press gesture, although it's just an unfinished long press + long_press_gesture = Gtk.GestureLongPress() + long_press_gesture.connect("pressed", self._show_context_menu) + self.container_box.add_controller(long_press_gesture) + + key_event_controller = Gtk.EventControllerKey() + key_event_controller.connect("key-pressed", self._on_key_press_event) + self.container_box.add_controller(key_event_controller) + + motion_event_controller = Gtk.EventControllerMotion() + motion_event_controller.connect("enter", self._on_cover_enter_notify) + motion_event_controller.connect("leave", self._on_cover_leave_notify) + self.container_box.add_controller(motion_event_controller) def set_playing(self, is_playing): self.art.set_playing(is_playing) @@ -48,82 +64,68 @@ def update_progress(self): self.art.update_progress() def _create_context_menu(self): - menu = Gtk.Menu() - read_item = Gtk.MenuItem(label=_("Mark as read")) - read_item.connect("button-press-event", self._mark_as_read) - - jump_item = Gtk.MenuItem(label=_("Open in file browser")) - jump_item.connect("button-press-event", self._jump_to_folder) - - rm_item = Gtk.MenuItem(label=_("Remove from library")) - rm_item.connect("button-press-event", self._remove_book) - - menu.append(read_item) - menu.append(jump_item) - menu.append(Gtk.SeparatorMenuItem()) - menu.append(rm_item) - menu.attach_to_widget(self) - menu.show_all() - return menu + menu_model = Gio.Menu() - def _remove_book(self, _, __): - if self.context_menu: - self.context_menu.popdown() + self.install_action("book_element.mark_as_read", None, self._mark_as_read) + menu_model.append(_("Mark as read"), "book_element.mark_as_read") + self.install_action("book_element.jump_to_folder", None, self._jump_to_folder) + menu_model.append(_("Open in file browser"), "book_element.jump_to_folder") + + self.install_action("book_element.remove_book", None, self._remove_book) + menu_model.append(_("Remove from library"), "book_element.remove_book") + + menu = Gtk.PopoverMenu(menu_model=menu_model, has_arrow=False) + menu.set_parent(self.art) + + return menu + + def _remove_book(self, *_): self.emit("book-removed", self.book) - def _mark_as_read(self, _, __): + def _mark_as_read(self, *_): self.book.position = -1 - def _jump_to_folder(self, _, __): + def _jump_to_folder(self, *_): """ Opens the folder containing this books files in the default file explorer. """ track = self.book.chapters[0] - path = os.path.dirname(track.file) - subprocess.Popen(['xdg-open', path]) - - def _on_button_press_event(self, _, event): - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - if self.context_menu is None: - self.context_menu = self._create_context_menu() - self.context_menu.popup( - None, None, None, None, event.button, event.time) - return True - elif event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1: - if super().get_sensitive(): - self.pressed = True - self.container_box.get_style_context().add_class("selected") - elif event.type == Gdk.EventType.KEY_PRESS and event.keyval == Gdk.KEY_Return: - if super().get_sensitive(): - self.emit("open-book-overview", self.book) - return True - - def _on_button_release_event(self, _, event): - if event.type == Gdk.EventType.BUTTON_RELEASE and event.button == 1 and self.pressed: - self.pressed = False - self.container_box.get_style_context().remove_class("selected") - if super().get_sensitive(): - self.emit("open-book-overview", self.book) - return True - - def _on_key_press_event(self, _, key): - if key.keyval == Gdk.KEY_Return and super().get_sensitive(): + + file_launcher = Gtk.FileLauncher(file=Gio.File.new_for_path(track.file)) + dummy_callback = lambda d, r: d.open_containing_folder_finish(r) + file_launcher.open_containing_folder(None, None, dummy_callback) + + def _show_context_menu(self, gesture: Gtk.Gesture, *_): + gesture.set_state(Gtk.EventSequenceState.CLAIMED) + + self._create_context_menu().popup() + + def _select_item(self, gesture: Gtk.Gesture, *_): + if super().get_sensitive(): + gesture.set_state(Gtk.EventSequenceState.CLAIMED) + self.pressed = True + self.container_box.add_css_class("selected") + + def _open_book_overview(self, gesture, *_): + gesture.set_state(Gtk.EventSequenceState.CLAIMED) + + self.pressed = False + self.container_box.remove_css_class("selected") + if super().get_sensitive(): self.emit("open-book-overview", self.book) - return True - def _on_cover_enter_notify(self, widget: Gtk.Widget, __): - set_hand_cursor(widget) + def _on_key_press_event(self, keyval, *_): + if keyval == Gdk.KEY_Return and super().get_sensitive(): + self.emit("open-book-overview", self.book) + def _on_cover_enter_notify(self, *_): self.art.set_hover(True) - return True - def _on_cover_leave_notify(self, widget: Gtk.Widget, __): - reset_cursor(widget) + def _on_cover_leave_notify(self, *_): self.art.set_hover(False) - return True - def _on_album_art_press_event(self, _, __): + def _on_album_art_press_event(self, *_): self.emit("play-pause-clicked", self.book) @@ -134,3 +136,4 @@ def _on_album_art_press_event(self, _, __): (GObject.TYPE_PYOBJECT,)) GObject.signal_new('book-removed', BookElement, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + diff --git a/cozy/ui/widgets/error_reporting.py b/cozy/ui/widgets/error_reporting.py index 20b0f768..36103270 100644 --- a/cozy/ui/widgets/error_reporting.py +++ b/cozy/ui/widgets/error_reporting.py @@ -5,7 +5,6 @@ from cozy.application_settings import ApplicationSettings from cozy.ext import inject -gi.require_version('Gtk', '3.0') from gi.repository import Gtk LEVELS = [ @@ -60,6 +59,7 @@ def show_header(self, show: bool): def _load_report_level(self): level = self.app_settings.report_level self.verbose_adjustment.set_value(level + 1) + self._update_ui_texts(level) def __init_scale(self): for i in range(1, 5): @@ -92,4 +92,5 @@ def _update_details(self, value): def _on_app_setting_changed(self, event, _): if event == "report-level": - self._load_report_level() \ No newline at end of file + self._load_report_level() + diff --git a/cozy/ui/widgets/filter_list_box.py b/cozy/ui/widgets/filter_list_box.py index 10a72724..74e65ae4 100644 --- a/cozy/ui/widgets/filter_list_box.py +++ b/cozy/ui/widgets/filter_list_box.py @@ -1,7 +1,5 @@ from typing import List -import gi -gi.require_version('Gtk', '3.0') from gi.repository import Gtk from cozy.ui.list_box_row_with_data import ListBoxRowWithData @@ -17,23 +15,22 @@ def __init__(self, **properties): def populate(self, elements: List[str]): self.remove_all_children() - all_row = ListBoxRowWithData(_("All"), False) + all_row = ListBoxRowWithData(_("All"), True) all_row.set_tooltip_text(_("Display all books")) - self.add(all_row) - self.add(ListBoxSeparatorRow()) + self.append(all_row) self.select_row(all_row) for element in elements: row = ListBoxRowWithData(element, False) - self.add(row) - - self.show_all() + self.append(row) def select_row_with_content(self, row_content: str): - for row in self.get_children(): - if not isinstance(row, ListBoxRowWithData): - continue + child = self.get_first_child() + while child: + next = child.get_next_sibling() - if row.data == row_content: - self.select_row(row) + if isinstance(child, ListBoxRowWithData) and child.data == row_content: + self.select_row(child) break + + child = next diff --git a/cozy/ui/widgets/list_box_extensions.py b/cozy/ui/widgets/list_box_extensions.py index 3d1d8ebf..82846727 100644 --- a/cozy/ui/widgets/list_box_extensions.py +++ b/cozy/ui/widgets/list_box_extensions.py @@ -3,15 +3,18 @@ def remove_all_children(self): """ - Removes all widgets from a gtk container. + Removes all widgets from a gtk widget. """ self.set_visible(False) - childs = self.get_children() - for element in childs: - self.remove(element) - element.destroy() + + child = self.get_first_child() + while child: + next = child.get_next_sibling() + self.remove(child) + child = next + self.set_visible(True) def extend_gtk_container(): - Gtk.Container.remove_all_children = remove_all_children + Gtk.Widget.remove_all_children = remove_all_children diff --git a/cozy/ui/widgets/playback_speed_popover.py b/cozy/ui/widgets/playback_speed_popover.py index 746bb185..2dd59847 100644 --- a/cozy/ui/widgets/playback_speed_popover.py +++ b/cozy/ui/widgets/playback_speed_popover.py @@ -1,9 +1,6 @@ -import gi - from cozy.ext import inject from cozy.view_model.playback_speed_view_model import PlaybackSpeedViewModel -gi.require_version('Gtk', '3.0') from gi.repository import Gtk diff --git a/cozy/ui/widgets/progress_popover.py b/cozy/ui/widgets/progress_popover.py index 2d90f4b8..8c5fe889 100644 --- a/cozy/ui/widgets/progress_popover.py +++ b/cozy/ui/widgets/progress_popover.py @@ -1,6 +1,3 @@ -import gi - -gi.require_version('Gtk', '3.0') from gi.repository import Gtk @@ -19,3 +16,4 @@ def set_message(self, message: str): def set_progress(self, progress: float): self.progress_bar.set_fraction(progress) + diff --git a/cozy/ui/widgets/search_results.py b/cozy/ui/widgets/search_results.py index 251f5d1c..ea1aa641 100644 --- a/cozy/ui/widgets/search_results.py +++ b/cozy/ui/widgets/search_results.py @@ -8,7 +8,7 @@ BOOK_ICON_SIZE = 40 -class SearchResult(Gtk.EventBox): +class SearchResult(Gtk.Box): """ This class is the base class for all search result GUI object. It features a GTK box that is highlighted when hovered. @@ -20,10 +20,14 @@ def __init__(self, on_click, on_click_data): self.on_click = on_click self.on_click_data = on_click_data - self.connect("enter-notify-event", self._on_enter_notify) - self.connect("leave-notify-event", self._on_leave_notify) - if on_click: - self.connect("button-press-event", self.__on_clicked) + self._motion_event = Gtk.EventControllerMotion() + self._motion_event.connect("enter", self._on_enter_notify) + self._motion_event.connect("leave", self._on_leave_notify) + self.add_controller(self._motion_event) + + self._primary_gesture = Gtk.GestureClick(button=Gdk.BUTTON_PRIMARY) + self._primary_gesture.connect("pressed", self.__on_clicked) + self.add_controller(self._primary_gesture) self.props.margin_top = 2 self.props.margin_bottom = 2 @@ -35,23 +39,22 @@ def __init__(self, on_click, on_click_data): self.box.set_halign(Gtk.Align.FILL) self.box.set_valign(Gtk.Align.CENTER) - def _on_enter_notify(self, widget, event): + def _on_enter_notify(self, widget, event, *_): """ On enter notify add css hover class - :param widget: as Gtk.EventBox + :param widget: as Gtk.Box :param event: as Gdk.Event """ - self.box.get_style_context().add_class("box_hover") + self.box.add_css_class("box_hover") - def _on_leave_notify(self, widget, event): + def _on_leave_notify(self, widget): """ On leave notify remove css hover class - :param widget: as Gtk.EventBox (can be None) - :param event: as Gdk.Event (can be None) + :param widget: as Gtk.Box (can be None) """ - self.box.get_style_context().remove_class("box_hover") + self.box.remove_css_class("box_hover") - def __on_clicked(self, widget, event): + def __on_clicked(self, widget, event, *_): self.on_click(self.on_click_data) @@ -75,17 +78,18 @@ def __init__(self, on_click, artist: str, is_author): title_label.set_text(tools.shorten_string(artist, MAX_BOOK_LENGTH)) self.set_tooltip_text(_("Jump to reader ") + artist) title_label.set_halign(Gtk.Align.START) - title_label.props.margin = 4 + title_label.props.margin_top = 4 + title_label.props.margin_bottom = 4 + title_label.props.margin_start = 4 + title_label.props.margin_end = 5 title_label.props.hexpand = True title_label.props.hexpand_set = True - title_label.set_margin_right(5) title_label.props.width_request = 100 title_label.props.xalign = 0.0 - title_label.set_line_wrap(True) + title_label.props.wrap = True - self.box.add(title_label) - self.add(self.box) - self.show_all() + self.box.append(title_label) + self.append(self.box) class BookSearchResult(SearchResult): @@ -100,27 +104,28 @@ def __init__(self, book: Book, on_click): self.set_tooltip_text(_("Play this book")) scale = self.get_scale_factor() - pixbuf = self._artwork_cache.get_cover_pixbuf(book, scale, BOOK_ICON_SIZE) - if pixbuf: - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, None) - img = Gtk.Image.new_from_surface(surface) + paintable = self._artwork_cache.get_cover_paintable(book, scale, BOOK_ICON_SIZE) + if paintable: + img = Gtk.Image.new_from_paintable(paintable) else: - img = Gtk.Image.new_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.MENU) + img = Gtk.Image.new_from_icon_name("book-open-variant-symbolic") img.props.pixel_size = BOOK_ICON_SIZE + img.set_size_request(BOOK_ICON_SIZE, BOOK_ICON_SIZE) title_label = Gtk.Label() title_label.set_text(tools.shorten_string(book.name, MAX_BOOK_LENGTH)) title_label.set_halign(Gtk.Align.START) - title_label.props.margin = 4 + title_label.props.margin_top = 4 + title_label.props.margin_bottom = 4 + title_label.props.margin_start = 4 + title_label.props.margin_end = 5 title_label.props.hexpand = True title_label.props.hexpand_set = True - title_label.set_margin_right(5) title_label.props.width_request = 100 title_label.props.xalign = 0.0 - title_label.set_line_wrap(True) + title_label.props.wrap = True - self.box.add(img) - self.box.add(title_label) - self.add(self.box) - self.show_all() + self.box.append(img) + self.box.append(title_label) + self.append(self.box) diff --git a/cozy/ui/widgets/seek_bar.py b/cozy/ui/widgets/seek_bar.py index 29795918..31c9e5c6 100644 --- a/cozy/ui/widgets/seek_bar.py +++ b/cozy/ui/widgets/seek_bar.py @@ -1,11 +1,7 @@ -import gi -from gi.repository import GObject, Gdk +from gi.repository import Gdk, GObject, Gtk from cozy.control.string_representation import seconds_to_str -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk - @Gtk.Template.from_resource('/com/github/geigi/cozy/seek_bar.ui') class SeekBar(Gtk.Box): @@ -14,7 +10,7 @@ class SeekBar(Gtk.Box): progress_scale: Gtk.Scale = Gtk.Template.Child() current_label: Gtk.Label = Gtk.Template.Child() remaining_label: Gtk.Label = Gtk.Template.Child() - remaining_event_box: Gtk.EventBox = Gtk.Template.Child() + remaining_event_box: Gtk.Box = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) @@ -22,9 +18,16 @@ def __init__(self, **kwargs): self._progress_scale_pressed = False self.progress_scale.connect("value-changed", self._on_progress_scale_changed) - self.progress_scale.connect("button-release-event", self._on_progress_scale_clicked) - self.progress_scale.connect("button-press-event", self._on_progress_scale_press) - self.progress_scale.connect("key-press-event", self._on_progress_key_pressed) + + self._progress_scale_gesture = Gtk.GestureClick() + self._progress_scale_gesture.connect("pressed", self._on_progress_scale_press) + self._progress_scale_gesture.connect("end", self._on_progress_scale_release) + self.progress_scale.add_controller(self._progress_scale_gesture) + + self._progress_scale_key = Gtk.EventControllerKey() + self._progress_scale_key.connect("key-pressed", self._on_progress_scale_press) + self._progress_scale_key.connect("key-released", self._on_progress_scale_release) + self.progress_scale.add_controller(self._progress_scale_key) @property def position(self) -> float: @@ -72,7 +75,7 @@ def _on_progress_scale_changed(self, _): self.current_label.set_markup("" + current_text + "") self.remaining_label.set_markup("-" + remaining_text + "") - def _on_progress_scale_clicked(self, _, __): + def _on_progress_scale_release(self, *_): self._progress_scale_pressed = False value = self.progress_scale.get_value() self.emit("position-changed", value) @@ -86,9 +89,8 @@ def _on_progress_key_pressed(self, _, event): self.position = min(self.position + 30, max_value) self.emit("position-changed", self.position) - def _on_progress_scale_press(self, _, __): + def _on_progress_scale_press(self, *_): self._progress_scale_pressed = True - return False diff --git a/cozy/ui/widgets/sleep_timer.py b/cozy/ui/widgets/sleep_timer.py index 4c49e79c..6d396db1 100644 --- a/cozy/ui/widgets/sleep_timer.py +++ b/cozy/ui/widgets/sleep_timer.py @@ -1,9 +1,6 @@ -import gi - from cozy.ext import inject from cozy.view_model.sleep_timer_view_model import SleepTimerViewModel, SystemPowerControl -gi.require_version('Gtk', '3.0') from gi.repository import Gtk @@ -21,8 +18,8 @@ class SleepTimer(Gtk.Popover): power_control_switch: Gtk.Switch = Gtk.Template.Child() power_control_options: Gtk.Box = Gtk.Template.Child() - system_shutdown_radiob: Gtk.RadioButton = Gtk.Template.Child() - system_suspend_radiob: Gtk.RadioButton = Gtk.Template.Child() + system_shutdown_radiob: Gtk.CheckButton = Gtk.Template.Child() + system_suspend_radiob: Gtk.CheckButton = Gtk.Template.Child() def __init__(self, timer_image: Gtk.Image): super().__init__() @@ -101,4 +98,5 @@ def _on_timer_enabled_changed(self): else: icon = "no-bed-symbolic" - self._timer_image.set_from_icon_name(icon, Gtk.IconSize.BUTTON) + self._timer_image.set_from_icon_name(icon) + diff --git a/cozy/ui/widgets/storage_list_box_row.py b/cozy/ui/widgets/storage_list_box_row.py index b630b07a..fcf97bd9 100644 --- a/cozy/ui/widgets/storage_list_box_row.py +++ b/cozy/ui/widgets/storage_list_box_row.py @@ -6,7 +6,7 @@ from cozy.ext import inject from cozy.model.library import Library from cozy.model.settings import Settings -from gi.repository import Gtk, GObject +from gi.repository import Gtk, GObject, Gio, GLib log = logging.getLogger("settings") @@ -15,6 +15,9 @@ class StorageListBoxRow(Gtk.ListBoxRow): """ This class represents a listboxitem for a storage location. """ + + main_window = inject.attr("MainWindow") + def __init__(self, model: Storage): self._model = model @@ -24,32 +27,29 @@ def __init__(self, model: Storage): box.set_spacing(3) box.set_halign(Gtk.Align.FILL) box.set_valign(Gtk.Align.CENTER) - box.set_margin_left(5) - box.set_margin_right(6) - box.set_margin_top(10) - box.set_margin_bottom(10) + box.set_margin_start(6) + box.set_margin_end(6) + box.set_margin_top(12) + box.set_margin_bottom(12) self.default_image = Gtk.Image() - self.default_image.set_from_icon_name("emblem-default-symbolic", Gtk.IconSize.LARGE_TOOLBAR) - self.default_image.set_margin_right(5) + self.default_image.set_from_icon_name("emblem-default-symbolic") + self.default_image.set_margin_end(5) self.type_image = Gtk.Image() self._set_drive_icon() - self.location_chooser = Gtk.FileChooserButton() - self.location_chooser.set_local_only(False) - self.location_chooser.set_action(Gtk.FileChooserAction.SELECT_FOLDER) - if self._model.path != "": - self.location_chooser.set_current_folder(self._model.path) - self.location_chooser.set_halign(Gtk.Align.FILL) - self.location_chooser.props.hexpand = True - self.location_chooser.connect("file-set", self.__on_folder_changed) - self.location_chooser.set_margin_right(6) - - box.add(self.type_image) - box.add(self.location_chooser) - box.add(self.default_image) - self.add(box) - self.show_all() + self.location_chooser = Gtk.Button() + self.location_label = Gtk.Label() + self.location_chooser.set_child(self.location_label) + self.location_chooser.set_margin_end(6) + self.location_chooser.connect("clicked", self._on_location_chooser_clicked) + + self.location_label.set_text(model.path) + + box.append(self.type_image) + box.append(self.location_chooser) + box.append(self.default_image) + self.set_child(box) self._set_default_icon() @property @@ -60,8 +60,7 @@ def refresh(self): self._set_drive_icon() self._set_default_icon() - def __on_folder_changed(self, widget): - new_path = self.location_chooser.get_file().get_path() + def __on_folder_changed(self, new_path): self.emit("location-changed", new_path) def _set_drive_icon(self): @@ -72,11 +71,30 @@ def _set_drive_icon(self): icon_name = "drive-harddisk-symbolic" self.type_image.set_tooltip_text(_("Internal drive")) - self.type_image.set_from_icon_name(icon_name, Gtk.IconSize.LARGE_TOOLBAR) - self.type_image.set_margin_right(5) + self.type_image.set_from_icon_name(icon_name) + self.type_image.set_margin_end(5) def _set_default_icon(self): self.default_image.set_visible(self._model.default) + def _on_location_chooser_clicked(self, *junk): + location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) + + if self._model.path != "": + folder = Gio.File.new_for_path(self._model.path) + location_chooser.set_initial_folder(folder) + + location_chooser.select_folder(self.main_window.window, None, self._location_chooser_open_callback) + + def _location_chooser_open_callback(self, dialog, result): + try: + file = dialog.select_folder_finish(result) + except GLib.GError: + pass + else: + if file is not None: + self.__on_folder_changed(file.get_path()) + GObject.signal_new('location-changed', StorageListBoxRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + diff --git a/cozy/ui/widgets/welcome.py b/cozy/ui/widgets/welcome.py index 5798fb3f..c2537653 100644 --- a/cozy/ui/widgets/welcome.py +++ b/cozy/ui/widgets/welcome.py @@ -1,11 +1,10 @@ import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Adw, Gtk @Gtk.Template.from_resource('/com/github/geigi/cozy/welcome.ui') -class Welcome(Gtk.Box): +class Welcome(Adw.Bin): __gtype_name__ = "Welcome" def __init__(self, **kwargs): diff --git a/cozy/ui/widgets/whats_new_importer.py b/cozy/ui/widgets/whats_new_importer.py index 7148f974..95113fd2 100644 --- a/cozy/ui/widgets/whats_new_importer.py +++ b/cozy/ui/widgets/whats_new_importer.py @@ -1,6 +1,3 @@ -import gi - -gi.require_version('Gtk', '3.0') from gi.repository import Gtk diff --git a/cozy/ui/widgets/whats_new_library.py b/cozy/ui/widgets/whats_new_library.py index ec548b1d..abea0bdd 100644 --- a/cozy/ui/widgets/whats_new_library.py +++ b/cozy/ui/widgets/whats_new_library.py @@ -1,6 +1,3 @@ -import gi - -gi.require_version('Gtk', '3.0') from gi.repository import Gtk diff --git a/cozy/ui/widgets/whats_new_m4b.py b/cozy/ui/widgets/whats_new_m4b.py index 0f72ec6d..50f4e1ee 100644 --- a/cozy/ui/widgets/whats_new_m4b.py +++ b/cozy/ui/widgets/whats_new_m4b.py @@ -1,6 +1,3 @@ -import gi - -gi.require_version('Gtk', '3.0') from gi.repository import Gtk diff --git a/cozy/ui/widgets/whats_new_m4b_chapter.py b/cozy/ui/widgets/whats_new_m4b_chapter.py index 23234fad..d0a33acf 100644 --- a/cozy/ui/widgets/whats_new_m4b_chapter.py +++ b/cozy/ui/widgets/whats_new_m4b_chapter.py @@ -1,6 +1,3 @@ -import gi - -gi.require_version('Gtk', '3.0') from gi.repository import Gtk diff --git a/cozy/ui/widgets/whats_new_window.py b/cozy/ui/widgets/whats_new_window.py index 1e80d1ef..0850609f 100644 --- a/cozy/ui/widgets/whats_new_window.py +++ b/cozy/ui/widgets/whats_new_window.py @@ -1,19 +1,16 @@ from typing import List from packaging import version -import gi - from cozy.application_settings import ApplicationSettings from cozy.ext import inject from cozy.ui.main_view import CozyUI from cozy.version import __version__ as CozyVersion -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Handy +from gi.repository import Gtk, Adw @Gtk.Template(resource_path='/com/github/geigi/cozy/whats_new.ui') -class WhatsNewWindow(Handy.Window): +class WhatsNewWindow(Adw.Window): __gtype_name__ = 'WhatsNew' content_stack: Gtk.Stack = Gtk.Template.Child() @@ -41,7 +38,7 @@ def __init__(self, **kwargs): self.set_default_size(800, 550) for widget in self.children: - self.content_stack.add(widget) + self.content_stack.add_child(widget) widget.set_visible(False) self.children[0].set_visible(True) diff --git a/cozy/view_model/app_view_model.py b/cozy/view_model/app_view_model.py index d98a8b64..c9d02d3a 100644 --- a/cozy/view_model/app_view_model.py +++ b/cozy/view_model/app_view_model.py @@ -20,8 +20,3 @@ def view(self, new_value: View): self._notify("view") self.emit_event_main_thread("view", self._view) - def navigate_back(self): - if self.view == View.BOOK_DETAIL: - self.view = View.LIBRARY_BOOKS - elif self.view == View.LIBRARY_BOOKS: - self.view = View.LIBRARY_FILTER diff --git a/cozy/view_model/book_detail_view_model.py b/cozy/view_model/book_detail_view_model.py index 9aa7d024..ede76aec 100644 --- a/cozy/view_model/book_detail_view_model.py +++ b/cozy/view_model/book_detail_view_model.py @@ -217,6 +217,3 @@ def _on_offline_cache_event(self, event, message): def _on_app_setting_changed(self, event, _): if event == "swap-author-reader": self._notify("book") - - def navigate_back(self): - self.emit_event(OpenView.BACK) diff --git a/cozy/view_model/headerbar_view_model.py b/cozy/view_model/headerbar_view_model.py index 564aea6e..dc20d622 100644 --- a/cozy/view_model/headerbar_view_model.py +++ b/cozy/view_model/headerbar_view_model.py @@ -52,23 +52,8 @@ def work_progress(self) -> float: def work_message(self) -> str: return self._work_message - @property - def can_navigate_back(self) -> bool: - return self._view == View.BOOK_DETAIL or \ - self._view == View.LIBRARY_BOOKS - - @property - def show_library_filter(self) -> bool: - return self._view == View.LIBRARY or \ - self._view == View.LIBRARY_BOOKS or \ - self._view == View.LIBRARY_FILTER or \ - self._view == View.BOOK_DETAIL or \ - self._view == View.NO_MEDIA - def set_view(self, value: View): self._view = value - self._notify("can_navigate_back") - self._notify("show_library_filter") self._notify("lock_ui") def _start_working(self, message: str): @@ -120,5 +105,3 @@ def _on_offline_cache_event(self, event: str, message): elif event == "finished": self._stop_working() - def navigate_back(self): - self.emit_event(OpenView.BACK) diff --git a/cozy/view_model/library_view_model.py b/cozy/view_model/library_view_model.py index 38263155..612724e8 100644 --- a/cozy/view_model/library_view_model.py +++ b/cozy/view_model/library_view_model.py @@ -50,7 +50,6 @@ def __init__(self): super(Observable, self).__init__() self._library_view_mode: LibraryViewMode = LibraryViewMode.CURRENT - self._library_page: LibraryPage = LibraryPage.NONE self._selected_filter: str = _("All") self._connect() @@ -77,15 +76,6 @@ def library_view_mode(self, value): self._notify("library_view_mode") self.emit_event(OpenView.LIBRARY, None) - @property - def library_page(self) -> LibraryPage: - return self._library_page - - @library_page.setter - def library_page(self, value: LibraryPage): - self._library_page = value - self._notify("library_page") - @property def selected_filter(self): return self._selected_filter @@ -211,8 +201,7 @@ def _on_importer_event(self, event, message): self._notify("books-filter") self._notify("library_view_mode") if event == "import-failed": - dialog = ImportFailedDialog(message) - dialog.show() + ImportFailedDialog(message).show() def _on_player_event(self, event, message): if event == "play": diff --git a/cozy/view_model/settings_view_model.py b/cozy/view_model/settings_view_model.py index 3301811e..28251f8a 100644 --- a/cozy/view_model/settings_view_model.py +++ b/cozy/view_model/settings_view_model.py @@ -13,7 +13,7 @@ from cozy.media.importer import Importer from cozy.model.settings import Settings from cozy.report import reporter -from gi.repository import Gtk +from gi.repository import Gtk, Adw @@ -34,6 +34,8 @@ def __init__(self): self._lock_ui: bool = False self._gtk_settings = Gtk.Settings.get_default() + self.style_manager = Adw.StyleManager.get_default() + self._set_dark_mode() self._app_settings.add_listener(self._on_app_setting_changed) @@ -119,8 +121,10 @@ def add_first_storage_location(self, path: str): self._notify("storage_locations") def _set_dark_mode(self): - prefer_dark_mode = self._app_settings.dark_mode - self._gtk_settings.set_property("gtk-application-prefer-dark-theme", prefer_dark_mode) + if self._app_settings.dark_mode: + self.style_manager.set_color_scheme(Adw.ColorScheme.PREFER_DARK) + else: + self.style_manager.set_color_scheme(Adw.ColorScheme.PREFER_LIGHT) def _on_app_setting_changed(self, event: str, data): if event == "dark-mode": diff --git a/data/icons/hicolor/scalable/actions/library-symbolic.svg b/data/icons/hicolor/scalable/actions/library-symbolic.svg new file mode 100644 index 00000000..4fdb7f87 --- /dev/null +++ b/data/icons/hicolor/scalable/actions/library-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/hicolor/scalable/actions/pause-symbolic.svg b/data/icons/hicolor/scalable/actions/pause-symbolic.svg deleted file mode 100644 index f2804257..00000000 --- a/data/icons/hicolor/scalable/actions/pause-symbolic.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/data/icons/hicolor/scalable/actions/play-symbolic.svg b/data/icons/hicolor/scalable/actions/play-symbolic.svg deleted file mode 100644 index d8ea5926..00000000 --- a/data/icons/hicolor/scalable/actions/play-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/data/ui/about.ui b/data/ui/about.ui index 4691fac4..468226d9 100644 --- a/data/ui/about.ui +++ b/data/ui/about.ui @@ -1,22 +1,14 @@ - - - - False - True - dialog - Cozy - 0.0.1 - Bundled dependencies: -python-inject by ivankorobkov - Apache-2.0 Licence - Thanks all supporters: https://www.patreon.com/geigi -Help translate Cozy: https://transifex.com/geigi/cozy + + + Cozy + com.github.geigi.cozy + gpl-3-0 + https://github.com/geigi/cozy/issues/new https://github.com/geigi/cozy - GitHub - geigi - -Contributions on GitHub: + geigi + geigi A6GibKm alyssais apandada1 @@ -40,23 +32,11 @@ oleg-krv paper42 phpwutz rapenne-s +rdbende thibaultamartin umeboshi2 -worldofpeace - -Thanks to mweinelt for implementing m4b chapter support in mutagen! - -And a thanks to all patreon supporters: -Fred Warren -Gabriel -Hu Mann -Josiah -Oleksii Kriukov - -And to other open source projects: -https://github.com/gnumdk/lollypop +worldofpeace Ainte -Ainte AndreBarata Andrey389 Asyx @@ -156,39 +136,6 @@ useruseruser1233211 vanhoxx vlabo xfgusta -yalexaner - - com.github.geigi.cozy - gpl-3-0 - - - - - - False - vertical - 2 - - - False - end - - - - - - - - - False - False - 0 - - - - - - - +yalexaner diff --git a/data/ui/album_element.ui b/data/ui/album_element.ui index 1b9b0046..5ed94ea6 100644 --- a/data/ui/album_element.ui +++ b/data/ui/album_element.ui @@ -1,150 +1,73 @@ - - - - True - False - play-symbolic - 2 - - + diff --git a/data/ui/application_default.css b/data/ui/application_default.css deleted file mode 100644 index f5851aa6..00000000 --- a/data/ui/application_default.css +++ /dev/null @@ -1,33 +0,0 @@ -.bold { - font-weight: bold; -} - -.semi-bold { - font-weight: 600; -} - -.bordered { - border: 1px solid shade(@theme_bg_color, 0.8); -} - -.h1 { - font-size: 230%; -} - -.h2 { - font-size: 200%; - font-weight: 200; -} - -.h3 { - font-size: 160%; -} - -.h5 { - font-size: 115%; -} - -.monospace { - font-family: monospace; -} - diff --git a/data/ui/application_legacy.css b/data/ui/application_legacy.css deleted file mode 100644 index 5c680ae3..00000000 --- a/data/ui/application_legacy.css +++ /dev/null @@ -1,12 +0,0 @@ -.bordered { - border: 2px solid shade(@theme_bg_color, 0.85); -} - -.h4 { - font-weight: lighter; - font-size: 110%; -} - -.h5 { - font-size: 115%; -} \ No newline at end of file diff --git a/data/ui/book_detail.ui b/data/ui/book_detail.ui index 0b6eb43a..df9a7838 100644 --- a/data/ui/book_detail.ui +++ b/data/ui/book_detail.ui @@ -1,569 +1,387 @@ - - - -