From cff4575bb34b4a19918a7e30110de9f30007e572 Mon Sep 17 00:00:00 2001 From: rdbende Date: Mon, 4 Dec 2023 19:47:26 +0100 Subject: [PATCH 01/28] Fix translations once and for all --- cozy/application.py | 27 ++++----------------------- main.py | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/cozy/application.py b/cozy/application.py index 33521fda..59996a49 100644 --- a/cozy/application.py +++ b/cozy/application.py @@ -1,5 +1,3 @@ -import gettext -import locale import logging import os import platform @@ -9,24 +7,18 @@ from traceback import format_exception import distro - -import gi - -from cozy.db.storage import Storage -from cozy.ui.widgets.filter_list_box import FilterListBox -from cozy.ui.widgets.seek_bar import SeekBar - -from gi.repository import Gtk, GLib, Adw +from gi.repository import Adw, GLib from cozy.app_controller import AppController from cozy.control.db import init_db from cozy.control.mpris import MPRIS from cozy.db.settings import Settings +from cozy.db.storage import Storage from cozy.report import reporter from cozy.ui.main_view import CozyUI +from cozy.ui.widgets.filter_list_box import FilterListBox from cozy.version import __version__ - log = logging.getLogger("application") @@ -60,8 +52,7 @@ class Application(Adw.Application): ui: CozyUI app_controller: AppController - def __init__(self, localedir: str, pkgdatadir: str): - self.localedir = localedir + def __init__(self, pkgdatadir: str): self.pkgdatadir = pkgdatadir super().__init__(application_id='com.github.geigi.cozy') @@ -74,16 +65,6 @@ def __init__(self, localedir: str, pkgdatadir: str): sys.excepthook = self.handle_exception setup_thread_excepthook() - # We need to call `locale.*textdomain` to get the strings in UI files translated - locale.bindtextdomain('com.github.geigi.cozy', localedir) - locale.textdomain('com.github.geigi.cozy') - - # But also `gettext.*textdomain`, to make `_("foo")` in Python work as well - gettext.bindtextdomain('com.github.geigi.cozy', localedir) - gettext.textdomain('com.github.geigi.cozy') - - gettext.install('com.github.geigi.cozy', localedir) - def do_startup(self): log.info(distro.linux_distribution(full_distribution_name=False)) log.info(f"Starting up cozy {__version__}") diff --git a/main.py b/main.py index 766540a6..00afe960 100755 --- a/main.py +++ b/main.py @@ -19,11 +19,14 @@ import argparse import code +import gettext +import locale import logging import os import signal import sys import traceback + import gi gi.require_version('Gtk', '4.0') @@ -32,17 +35,26 @@ gi.require_version('Gst', '1.0') gi.require_version('GstPbutils', '1.0') +from gi.repository import Gio, GLib + pkgdatadir = '@DATA_DIR@' localedir = '@LOCALE_DIR@' -from gi.repository import Gio +# We need to call `locale.*textdomain` to get the strings in UI files translated +locale.bindtextdomain('com.github.geigi.cozy', localedir) +locale.textdomain('com.github.geigi.cozy') + +# But also `gettext.*textdomain`, to make `_("foo")` in Python work as well +gettext.bindtextdomain('com.github.geigi.cozy', localedir) +gettext.textdomain('com.github.geigi.cozy') -# gresource must be registered before importing any Gtk.Template annotated classes +gettext.install('com.github.geigi.cozy', localedir) + + +# gresource must be registered before importing any Gtk.Template annotated classes resource = Gio.Resource.load(os.path.join(pkgdatadir, 'com.github.geigi.cozy.ui.gresource')) resource._register() -from gi.repository import GLib - old_except_hook = None log = logging.getLogger("main") @@ -93,7 +105,7 @@ def main(): listen() - application = Application(localedir, pkgdatadir) + application = Application(pkgdatadir) try: # Handle the debug option seperatly without the Glib stuff From 4d003513b62e3329d2d7e1095cb825d3e4a579c5 Mon Sep 17 00:00:00 2001 From: rdbende Date: Mon, 4 Dec 2023 16:34:04 +0100 Subject: [PATCH 02/28] Remove obsolete CSS style classes --- data/ui/style.css | 87 ++++++++++++++--------------------------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/data/ui/style.css b/data/ui/style.css index 89fbd3fc..f5635405 100644 --- a/data/ui/style.css +++ b/data/ui/style.css @@ -1,54 +1,27 @@ -.white { - color: white; -} - .box_hover { - color: @theme_selected_fg_color; - background: @theme_selected_bg_color; -} - -.selected { - color: @theme_selected_fg_color; + color: @theme_selected_fg_color; + background: @theme_selected_bg_color; } .no_frame { - border-style: none; -} - -.sort_switcher_element { - font-size: 110%; -} - -.sort_switcher_element > .radio { - min-width: 100px; -} - -.no_padding { - padding: 0px; + border-style: none; } .unavailable_box { - background-color: #C01C28; - border-radius: 25px; - padding: 3px; - padding-right: 10px; -} - -.unavailable_image { - color: white; -} - -.unavailable_label { - color: white; + background-color: @red_4; + border-radius: 25px; + padding: 3px; + padding-right: 10px; + color: white; } .book_card { - padding: 1rem; + padding: 1rem; } .selected { - color: @theme_selected_fg_color; - background-color: @theme_selected_bg_color; + color: @theme_selected_fg_color; + background-color: @theme_selected_bg_color; } .book_detail_art { @@ -56,35 +29,35 @@ } .chapter_element { - border-radius: 0.5rem; + border-radius: 0.5rem; } .book_play_button { - background: rgba(50, 50, 50, 1.0); - border: none; - color: white; - box-shadow: none; - transition: none; - -gtk-icon-shadow: none; + background: rgba(50, 50, 50, 1.0); + border: none; + color: white; + box-shadow: none; + transition: none; + -gtk-icon-shadow: none; } .book_play_button:hover, .book_play_button:active, .play_button:hover, .play_button:active { - border: none; - box-shadow: none; - -gtk-icon-shadow: none; + border: none; + box-shadow: none; + -gtk-icon-shadow: none; } .filter-list-box-row { - border-radius: 0.5rem; + border-radius: 0.5rem; } .play_button { - background-color: @theme_fg_color; - color: @theme_bg_color; + background-color: @theme_fg_color; + color: @theme_bg_color; } .player_bar { - background: shade(@theme_bg_color, 0.95) 0%; + background: shade(@theme_bg_color, 0.95) 0%; } .bold { @@ -95,16 +68,8 @@ font-weight: 600; } -.bordered { - border: 1px solid shade(@theme_bg_color, 0.8); -} - -.monospace { - font-family: monospace; -} - .transparent_bg { - background: transparent; + background: transparent; } .failed-import-card { From 59fa533aef61b07b8a7b1186731b319c17aa5e58 Mon Sep 17 00:00:00 2001 From: rdbende Date: Mon, 4 Dec 2023 21:40:44 +0100 Subject: [PATCH 03/28] Excepthook workaround is no longer needed --- cozy/application.py | 44 ++++++++++---------------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/cozy/application.py b/cozy/application.py index 59996a49..fee6d702 100644 --- a/cozy/application.py +++ b/cozy/application.py @@ -22,32 +22,6 @@ log = logging.getLogger("application") -def setup_thread_excepthook(): - """ - Workaround for `sys.excepthook` thread bug from: - http://bugs.python.org/issue1230540 - - Call once from the main thread before creating any threads. - """ - - init_original = threading.Thread.__init__ - - def init(self, *args, **kwargs): - - init_original(self, *args, **kwargs) - run_original = self.run - - def run_with_except_hook(*args2, **kwargs2): - try: - run_original(*args2, **kwargs2) - except Exception: - sys.excepthook(*sys.exc_info()) - - self.run = run_with_except_hook - - threading.Thread.__init__ = init - - class Application(Adw.Application): ui: CozyUI app_controller: AppController @@ -61,9 +35,7 @@ def __init__(self, pkgdatadir: str): GLib.setenv("PULSE_PROP_media.role", "music", True) GLib.set_application_name("Cozy") - self.old_except_hook = sys.excepthook - sys.excepthook = self.handle_exception - setup_thread_excepthook() + threading.excepthook = self.handle_exception def do_startup(self): log.info(distro.linux_distribution(full_distribution_name=False)) @@ -95,14 +67,18 @@ def do_activate(self): mpris = MPRIS(self) mpris._on_current_changed() - def handle_exception(self, exc_type, exc_value, exc_traceback): + def handle_exception(self, _): print("handle exception") + + exc_type, exc_value, exc_traceback = sys.exc_info() + + if exc_type is SystemExit: + return + try: reporter.exception("uncaught", exc_value, "\n".join(format_exception(exc_type, exc_value, exc_traceback))) - except Exception: - None - - self.old_except_hook(exc_type, exc_value, exc_traceback) + finally: + sys.excepthook(exc_type, exc_value, exc_traceback) def quit(self): self.app_controller.quit() From dca5cefdc86c31e4f0d6d4087c0c4cab5a02887d Mon Sep 17 00:00:00 2001 From: suve Date: Wed, 29 Mar 2023 12:22:58 +0200 Subject: [PATCH 04/28] Fix crash on startup This commit fixes the program crashing because of an uncaught exception. The bug was triggered by app_settings.last_launched_version being unset - which would always be the case when launching the program for the first time! The bug could also be triggered by manually changing the setting to an invalid value - e.g. by running: $ gsettings set com.github.geigi.cozy last-launched-version 'ayy lmao' --- cozy/ui/widgets/whats_new_window.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cozy/ui/widgets/whats_new_window.py b/cozy/ui/widgets/whats_new_window.py index 0850609f..4a1e8ca7 100644 --- a/cozy/ui/widgets/whats_new_window.py +++ b/cozy/ui/widgets/whats_new_window.py @@ -48,12 +48,16 @@ def __init__(self, **kwargs): def _fill_window(self): self.children = [] - last_launched_version = version.parse(self.app_settings.last_launched_version) - - if type(last_launched_version) is version.LegacyVersion: + try: + last_launched_version = version.parse(self.app_settings.last_launched_version) + except version.InvalidVersion: self._fill_welcome() else: - self._fill_whats_new(last_launched_version) + if type(last_launched_version) is version.LegacyVersion: + self._fill_welcome() + else: + self._fill_whats_new(last_launched_version) + def _fill_welcome(self): from cozy.ui.widgets.welcome import Welcome From da349eb6577dcab01cf61d424bccfbc2d46ada0d Mon Sep 17 00:00:00 2001 From: suve Date: Sun, 28 May 2023 12:09:02 +0200 Subject: [PATCH 05/28] Remove usage of version.LegacyVersion The "packaging" module has deprecated its LegacyVersion identifier back in 2020, finally removing it in 2021. This causes cozy to crash when running against new version of said module, with the following error: > AttributeError: module 'packaging.version' > has no attribute 'LegacyVersion' --- cozy/ui/widgets/whats_new_window.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cozy/ui/widgets/whats_new_window.py b/cozy/ui/widgets/whats_new_window.py index 4a1e8ca7..ceba1bb0 100644 --- a/cozy/ui/widgets/whats_new_window.py +++ b/cozy/ui/widgets/whats_new_window.py @@ -53,11 +53,7 @@ def _fill_window(self): except version.InvalidVersion: self._fill_welcome() else: - if type(last_launched_version) is version.LegacyVersion: - self._fill_welcome() - else: - self._fill_whats_new(last_launched_version) - + self._fill_whats_new(last_launched_version) def _fill_welcome(self): from cozy.ui.widgets.welcome import Welcome From 0075c47649cfe1b47a04e4ad99f13b9f84453f08 Mon Sep 17 00:00:00 2001 From: rdbende Date: Sat, 9 Dec 2023 20:29:17 +0100 Subject: [PATCH 06/28] Refactor MPRIS interface --- cozy/control/mpris.py | 649 +++++++++++++++++++++--------------------- cozy/media/player.py | 5 +- 2 files changed, 323 insertions(+), 331 deletions(-) diff --git a/cozy/control/mpris.py b/cozy/control/mpris.py index 00bd2d88..42e0746d 100644 --- a/cozy/control/mpris.py +++ b/cozy/control/mpris.py @@ -5,67 +5,95 @@ # copyright (c) 2013 Arnel A. Borja # copyright (c) 2013 Vadim Rutkovsky # copyright (c) 2017 Julian Geywitz -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# copyright (c) 2023 Benedek Dévényi + import logging -from gi.repository import Gio, GLib, Gtk +import re +import time +from dataclasses import dataclass -from random import randint +from gi.repository import Gio, GLib -import cozy.ui from cozy.application_settings import ApplicationSettings from cozy.control.artwork_cache import ArtworkCache from cozy.ext import inject -from cozy.media.player import Player +from cozy.media.player import NS_TO_SEC, US_TO_SEC, Player from cozy.model.book import Book from cozy.report import reporter -log = logging.getLogger("offline_cache") +log = logging.getLogger("mpris") + +CamelCasePattern = re.compile(r"(? str: + return CamelCasePattern.sub("_", name).lower() + + +@dataclass(kw_only=True, frozen=True, slots=True) +class Metadata: + track_id: str + track_number: int + title: str + album: str + artist: list[str] + length: int + url: str + artwork_uri: str + + def to_dict(self) -> dict[str, GLib.Variant]: + data = {} + data["mpris:trackid"] = GLib.Variant("o", self.track_id) + data["xesam:trackNumber"] = GLib.Variant("i", self.track_number) + data["xesam:title"] = GLib.Variant("s", self.title) + data["xesam:album"] = GLib.Variant("s", self.album) + data["xesam:artist"] = GLib.Variant("as", self.artist) + data["mpris:length"] = GLib.Variant("x", self.length) + data["xesam:url"] = GLib.Variant("s", self.url) + if self.artwork_uri: + data["mpris:artUrl"] = GLib.Variant("s", "file://" + self.artwork_uri) -class UnsupportedProperty(Exception): - pass + return data + + @staticmethod + def no_track() -> dict[str, GLib.Variant]: + no_track_path = GLib.Variant("o", "/org/mpris/MediaPlayer2/TrackList/NoTrack") + return {"mpris:trackid": no_track_path} class Server: - def __init__(self, con, path): - method_outargs = {} - method_inargs = {} - for interface in Gio.DBusNodeInfo.new_for_xml(self.__doc__).interfaces: + def __init__(self, connection: Gio.DBusConnection, path: str) -> None: + self.method_outargs = {} + self.method_inargs = {} + for interface in Gio.DBusNodeInfo.new_for_xml(self.__doc__).interfaces: for method in interface.methods: - method_outargs[method.name] = "(" + "".join( - [arg.signature for arg in method.out_args]) + ")" - method_inargs[method.name] = tuple( - arg.signature for arg in method.in_args) + self.method_inargs[method.name] = tuple( + arg.signature for arg in method.in_args + ) + out_sig = [arg.signature for arg in method.out_args] + self.method_outargs[method.name] = "(" + "".join(out_sig) + ")" try: - con.register_object(object_path=path, - interface_info=interface, - method_call_closure=self.on_method_call) - except: - log.error("MPRIS is already connected from another cozy process.") - - self.method_inargs = method_inargs - self.method_outargs = method_outargs - - def on_method_call(self, - connection, - sender, - object_path, - interface_name, - method_name, - parameters, - invocation): - + connection.register_object( + object_path=path, + interface_info=interface, + method_call_closure=self.on_method_call, + ) + except Exception: + log.error("MPRIS is already connected from another Cozy process.") + + def on_method_call( + self, + connection: Gio.DBusConnection, + sender: str, + object_path: str, + interface_name: str, + method_name: str, + parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation, + ) -> None: args = list(parameters.unpack()) for i, sig in enumerate(self.method_inargs[method_name]): if sig == "h": @@ -73,350 +101,311 @@ def on_method_call(self, fd_list = msg.get_unix_fd_list() args[i] = fd_list.get(args[i]) - out_args = None + snake_method = to_snake_case(method_name) try: - result = getattr(self, method_name)(*args) - - # out_args is atleast (signature1). + result = getattr(self, snake_method)(*args) + except AttributeError: + invocation.return_dbus_error( + "{}.Error.NotSupported".format(interface_name), "Unsupported property" + ) + except Exception as e: + log.error(e) + reporter.exception("mpris", e) + reporter.error( + "mpris", + "MPRIS method call failed with method name: {}".format(method_name), + ) + invocation.return_dbus_error( + "{}.Error.Failed".format(interface_name), "Internal exception occurred" + ) + else: + # out_args is at least (signature1). # We therefore always wrap the result as a tuple. - # Refer to https://bugzilla.gnome.org/show_bug.cgi?id=765603 + # Reference: + # https://bugzilla.gnome.org/show_bug.cgi?id=765603 result = (result,) out_args = self.method_outargs[method_name] - if out_args and out_args != "()" and result[0]: + if out_args != "()" and result[0]: variant = GLib.Variant(out_args, result) invocation.return_value(variant) else: invocation.return_value(None) - except UnsupportedProperty: - invocation.return_dbus_error("{}.Error.NotSupported".format(interface_name), "Unsupported property") - except Exception as e: - log.error(e) - reporter.exception("mpris", e) - reporter.error("mpris", "MPRIS method call failed with method name: {}".format(method_name)) - if out_args: - reporter.error("mpris", "MPRIS method call failed with out_args: {}".format(out_args)) - invocation.return_dbus_error("{}.Error.Failed".format(interface_name), "Internal exception occurred") class MPRIS(Server): """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ - __MPRIS_IFACE = "org.mpris.MediaPlayer2" - __MPRIS_PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" - __MPRIS_RATINGS_IFACE = "org.mpris.MediaPlayer2.ExtensionSetRatings" - __MPRIS_COZY = "org.mpris.MediaPlayer2.Cozy" - __MPRIS_PATH = "/org/mpris/MediaPlayer2" + + MEDIA_PLAYER2_INTERFACE = "org.mpris.MediaPlayer2" + MEDIA_PLAYER2_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" + _player: Player = inject.attr(Player) _artwork_cache: ArtworkCache = inject.attr(ArtworkCache) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) - def __init__(self, app): - self.__app = app - self.__ui = cozy.ui.main_view.CozyUI() - self.__rating = None - self.__cozy_id = 0 - self.__metadata = {"mpris:trackid": GLib.Variant( - "o", - "/org/mpris/MediaPlayer2/TrackList/NoTrack")} - self.__track_id = self.__get_media_id(0) - self.__bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) - Gio.bus_own_name_on_connection(self.__bus, - self.__MPRIS_COZY, - Gio.BusNameOwnerFlags.NONE, - None, - None) - Server.__init__(self, self.__bus, self.__MPRIS_PATH) + def __init__(self, app) -> None: + self._bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + Gio.bus_own_name_on_connection( + self._bus, + "org.mpris.MediaPlayer2.Cozy", + Gio.BusNameOwnerFlags.NONE, + None, + None, + ) + super().__init__(self._bus, "/org/mpris/MediaPlayer2") + + self._application = app + self._metadata = self._get_new_metadata() self._player.add_listener(self._on_player_changed) self._app_settings.add_listener(self._on_app_setting_changed) - def Raise(self): - try: - self.__app.ui.window.present_with_time(Gtk.get_current_event_time()) - except Exception as e: - reporter.exception("mpris", e) + def introspect(self): + return self.__doc__ - def Quit(self): - self.__app.quit() + def quit(self): + self._application.quit() - def Next(self): + def next(self): self._player.forward() - def Previous(self): + def previous(self): self._player.rewind() - def Pause(self): - self._player.pause() - - def PlayPause(self): + def play(self): self._player.play_pause() - def Stop(self): - self._player.destroy() + def pause(self): + self._player.pause() - def Play(self): + def play_pause(self): self._player.play_pause() - def SetPosition(self, track_id, position): - self._player.position = position * 10**3 + def stop(self): + self._player.destroy() - def Seek(self, offset): - self._player.position = self._player.position + offset * 10**3 + def set_position(self, track_id: str, position: int): + self._player.position = position / US_TO_SEC - def Seeked(self, position): - self.__bus.emit_signal( - None, - self.__MPRIS_PATH, - "org.freedesktop.DBus.Properties", - "Seeked", - GLib.Variant.new_tuple(GLib.Variant("x", position))) + def seek(self, offset: int): + self._player.position = self._player.position / NS_TO_SEC + offset / US_TO_SEC - def Get(self, interface, property_name): - if property_name in ["CanQuit", "CanRaise", "CanSeek", - "CanControl", "HasRatingsExtension"]: + def get(self, interface: str, property_name: str) -> GLib.Variant: + if property_name in {"CanQuit", "CanControl"}: return GLib.Variant("b", True) - elif property_name == "HasTrackList": + elif property_name in {"CanRaise", "HasTrackList"}: return GLib.Variant("b", False) - elif property_name == "Identity": - return GLib.Variant("s", "Cozy") - elif property_name == "DesktopEntry": - return GLib.Variant("s", "com.github.geigi.cozy") - elif property_name == "SupportedUriSchemes": - return GLib.Variant("as", ["file"]) - elif property_name == "SupportedMimeTypes": - return GLib.Variant("as", ["application/ogg", - "audio/x-vorbis+ogg", - "audio/x-flac", - "audio/mpeg"]) - elif property_name == "PlaybackStatus": - return GLib.Variant("s", self.__get_status()) - elif property_name == "Metadata": - return GLib.Variant("a{sv}", self.__metadata) - elif property_name == "Position": - return GLib.Variant( - "x", - round(self._player.position * 10**-3)) - elif property_name in ["CanGoNext", "CanGoPrevious", - "CanPlay", "CanPause"]: + elif property_name in { + "CanGoNext", + "CanGoPrevious", + "CanPlay", + "CanPause", + "CanSeek", + }: return GLib.Variant("b", self._player.loaded_book is not None) - elif property_name == "Volume": - return GLib.Variant("d", self._player.volume) + elif property_name in {"SupportedUriSchemes", "SupportedMimeTypes"}: + return GLib.Variant("as", []) + + # Might raise an AttributeError. We handle that in Server.on_method_call + return getattr(self, to_snake_case(property_name)) + + def get_all(self, interface): + if interface == self.MEDIA_PLAYER2_INTERFACE: + properties = ( + "CanQuit", + "CanRaise", + "HasTrackList", + "Identity", + "DesktopEntry", + "SupportedUriSchemes", + "SupportedMimeTypes", + ) + elif interface == self.MEDIA_PLAYER2_PLAYER_INTERFACE: + properties = ( + "PlaybackStatus", + "Metadata", + "Position", + "CanGoNext", + "CanGoPrevious", + "CanPlay", + "CanPause", + "CanSeek", + "CanControl", + "Volume", + ) + + return {property: self.get(interface, property) for property in properties} + + def properties_changed(self, iface_name, changed_props, invalidated_props): + self._bus.emit_signal( + None, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + GLib.Variant.new_tuple( + GLib.Variant("s", iface_name), + GLib.Variant("a{sv}", changed_props), + GLib.Variant("as", invalidated_props), + ), + ) + + @property + def desktop_entry(self): + return GLib.Variant("s", "com.github.geigi.cozy") + + @property + def identity(self): + return GLib.Variant("s", "Cozy") + + @property + def playback_status(self): + if self._player.playing: + return GLib.Variant("s", "Playing") + elif not self._player.loaded_book: + return GLib.Variant("s", "Stopped") else: - reporter.warning("mpris", "MPRIS required an unknown information: {}".format(property_name)) - raise UnsupportedProperty - - def GetAll(self, interface): - ret = {} - if interface == self.__MPRIS_IFACE: - for property_name in ["CanQuit", - "CanRaise", - "HasTrackList", - "Identity", - "DesktopEntry", - "SupportedUriSchemes", - "SupportedMimeTypes"]: - ret[property_name] = self.Get(interface, property_name) - elif interface == self.__MPRIS_PLAYER_IFACE: - for property_name in ["PlaybackStatus", - "Metadata", - "Position", - "CanGoNext", - "CanGoPrevious", - "CanPlay", - "CanPause", - "CanSeek", - "CanControl"]: - ret[property_name] = self.Get(interface, property_name) - elif interface == self.__MPRIS_RATINGS_IFACE: - ret["HasRatingsExtension"] = GLib.Variant("b", False) - return ret - - def Set(self, interface, property_name, new_value): - if property_name == "Volume": - self._player.volume = new_value - - def PropertiesChanged(self, interface_name, changed_properties, - invalidated_properties): - self.__bus.emit_signal(None, - self.__MPRIS_PATH, - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - GLib.Variant.new_tuple( - GLib.Variant("s", interface_name), - GLib.Variant("a{sv}", changed_properties), - GLib.Variant("as", invalidated_properties))) - - def Introspect(self): - return self.__doc__ + return GLib.Variant("s", "Paused") + + @property + def metadata(self): + return GLib.Variant("a{sv}", self._metadata) - ####################### - # PRIVATE # - ####################### + @property + def position(self): + return GLib.Variant("x", round(self._player.position / 1e3)) - def __get_media_id(self, track_id): + @property + def volume(self): + return GLib.Variant("d", self._player.volume) + + def _get_track_id(self) -> float: """ - TrackId's must be unique even up to - the point that if you repeat a song - it must have a different TrackId. + Track IDs must be unique even up to the point that if a song + is repeated in a playlist it must have a different TrackId. """ - track_id = track_id + randint(10000000, 90000000) - return GLib.Variant("o", "/com/github/geigi/cozy/TrackId/%s" % track_id) + return time.time() * 1e10 % 1e10 - def __get_status(self): - if self._player.playing: - return "Playing" - elif not self._player.loaded_book: - return "Stopped" - else: - return "Paused" - - def _on_player_changed(self, event, message): + def _get_new_metadata(self, book: Book | None = None) -> dict[str, GLib.Variant]: + if book is None: + return Metadata.no_track() + + track_path_template = "/com/github/geigi/cozy/TrackId/{id:.0f}" + uri_template = "file://{path}" + + metadata = Metadata( + track_id=track_path_template.format(id=self._get_track_id()), + track_number=book.current_chapter.number, + title=book.current_chapter.name, + album=book.name, + artist=[book.author], + length=book.current_chapter.length * US_TO_SEC, + url=uri_template.format(path=book.current_chapter.file), + artwork_uri=self._artwork_cache.get_album_art_path(book, 256), + ) + return metadata.to_dict() + + def _on_player_changed(self, event: str, _) -> None: if event == "chapter-changed": self._on_current_changed() elif event == "play": - self.__on_status_changed("Playing") + self._on_status_changed("Playing") elif event == "pause": - self.__on_status_changed("Paused") + self._on_status_changed("Paused") elif event == "stop": - self.__on_status_changed("Stopped") + self._on_status_changed("Stopped") - def _on_app_setting_changed(self, event, _): + def _on_app_setting_changed(self, event: str, _): if event == "swap-author-reader": self._on_current_changed() - def __update_metadata(self, book: Book): - # if track is None: - # track = get_current_track() - if book is None: - self.__metadata = {"mpris:trackid": GLib.Variant( - "o", - "/org/mpris/MediaPlayer2/TrackList/NoTrack")} - else: - self.__metadata["mpris:trackid"] = self.__track_id - track_number = book.current_chapter.number - - self.__metadata["xesam:trackNumber"] = GLib.Variant("i", - track_number) - self.__metadata["xesam:title"] = GLib.Variant( - "s", - book.current_chapter.name) - self.__metadata["xesam:album"] = GLib.Variant( - "s", - book.name) - self.__metadata["xesam:artist"] = GLib.Variant( - "as", - [book.author]) - self.__metadata["mpris:length"] = GLib.Variant( - "x", - book.current_chapter.length * 1000 * 1000) - self.__metadata["xesam:url"] = GLib.Variant( - "s", - "file:///" + book.current_chapter.file) - - path = self._artwork_cache.get_album_art_path(book, 180) - if path: - self.__metadata["mpris:artUrl"] = GLib.Variant( - "s", - "file://" + path) - - def __on_seeked(self, player, position): - self.Seeked(position * (1000 * 1000)) - - def _on_current_changed(self): + def _on_current_changed(self) -> None: if not self._player.loaded_book: return - current_track_id = self._player.loaded_chapter.id - if current_track_id and current_track_id >= 0: - self.__cozy_id = current_track_id - else: - self.__cozy_id = 0 - self.__track_id = self.__get_media_id(self.__cozy_id) - self.__rating = None - self.__update_metadata(self._player.loaded_book) - properties = {"Metadata": GLib.Variant("a{sv}", self.__metadata), - "CanPlay": GLib.Variant("b", True), - "CanPause": GLib.Variant("b", True), - "CanGoNext": GLib.Variant("b", True), - "CanGoPrevious": GLib.Variant("b", True)} - try: - self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) - except Exception as e: - print("MPRIS::__on_current_changed(): %s" % e) + self._metadata = self._get_new_metadata(self._player.loaded_book) + + properties = { + "Metadata": GLib.Variant("a{sv}", self._metadata), + "CanPlay": GLib.Variant("b", True), + "CanPause": GLib.Variant("b", True), + "CanGoNext": GLib.Variant("b", True), + "CanGoPrevious": GLib.Variant("b", True), + } + + self.properties_changed(self.MEDIA_PLAYER2_PLAYER_INTERFACE, properties, []) - def __on_status_changed(self, status, data=None): + def _on_status_changed(self, status: str) -> None: properties = {"PlaybackStatus": GLib.Variant("s", status)} - self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) + self.properties_changed(self.MEDIA_PLAYER2_PLAYER_INTERFACE, properties, []) diff --git a/cozy/media/player.py b/cozy/media/player.py index 7a12122e..5f8683c3 100644 --- a/cozy/media/player.py +++ b/cozy/media/player.py @@ -22,6 +22,7 @@ log = logging.getLogger("mediaplayer") +US_TO_SEC = 10 ** 6 NS_TO_SEC = 10 ** 9 REWIND_SECONDS = 30 @@ -80,7 +81,9 @@ def position(self) -> int: @position.setter def position(self, new_value: int): - self._gst_player.position = self.loaded_chapter.start_position + (new_value * NS_TO_SEC) + # FIXME: setter expects seconds, but getter returns nanoseconds + if self.loaded_chapter is not None: + self._gst_player.position = max(self.loaded_chapter.start_position + (new_value * NS_TO_SEC), 0) @property def volume(self) -> float: From 1be8d73c740cd6dfca2a7192c8bc509de806cb14 Mon Sep 17 00:00:00 2001 From: rdbende Date: Sat, 9 Dec 2023 20:29:43 +0100 Subject: [PATCH 07/28] Fix bug with stopping playback through MPRIS --- cozy/media/player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cozy/media/player.py b/cozy/media/player.py index 5f8683c3..e018a4b0 100644 --- a/cozy/media/player.py +++ b/cozy/media/player.py @@ -175,8 +175,7 @@ def forward(self): def destroy(self): self._gst_player.dispose() - - self._stop_tick_thread() + self._stop_playback() if self._fadeout_thread: self._fadeout_thread.stop() From dcc5aebf4ee31cc2911d040f6c692646d01976f9 Mon Sep 17 00:00:00 2001 From: rdbende Date: Mon, 18 Dec 2023 16:39:41 +0100 Subject: [PATCH 08/28] Huhh --- cozy/app_controller.py | 2 + cozy/model/settings.py | 17 +-- cozy/model/storage.py | 14 +-- cozy/ui/main_view.py | 6 +- cozy/ui/preferences_view.py | 95 ++------------ cozy/ui/widgets/storage_list_box_row.py | 100 --------------- cozy/ui/widgets/storages.py | 161 ++++++++++++++++++++++++ cozy/view_model/settings_view_model.py | 88 +------------ cozy/view_model/storages_view_model.py | 119 ++++++++++++++++++ data/ui/gresource.xml | 2 + data/ui/preferences.ui | 130 +------------------ data/ui/storage_locations.ui | 54 ++++++++ data/ui/storage_row.ui | 32 +++++ 13 files changed, 399 insertions(+), 421 deletions(-) delete mode 100644 cozy/ui/widgets/storage_list_box_row.py create mode 100644 cozy/ui/widgets/storages.py create mode 100644 cozy/view_model/storages_view_model.py create mode 100644 data/ui/storage_locations.ui create mode 100644 data/ui/storage_row.ui diff --git a/cozy/app_controller.py b/cozy/app_controller.py index ec1fbd53..003f53db 100644 --- a/cozy/app_controller.py +++ b/cozy/app_controller.py @@ -37,6 +37,7 @@ from cozy.view_model.search_view_model import SearchViewModel from cozy.view_model.settings_view_model import SettingsViewModel from cozy.view_model.sleep_timer_view_model import SleepTimerViewModel +from cozy.view_model.storages_view_model import StoragesViewModel class AppController(metaclass=Singleton): @@ -108,6 +109,7 @@ def configure_inject(self, binder): binder.bind_to_constructor(ToastNotifier, lambda: ToastNotifier()) binder.bind_to_constructor(AppViewModel, lambda: AppViewModel()) binder.bind_to_constructor(SettingsViewModel, lambda: SettingsViewModel()) + binder.bind_to_constructor(StoragesViewModel, lambda: StoragesViewModel()) def open_author(self, author: str): self.library_view_model.library_view_mode = LibraryViewMode.AUTHOR diff --git a/cozy/model/settings.py b/cozy/model/settings.py index 432415bb..c56dd913 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -16,7 +16,7 @@ class Settings: - _storages: List[Storage] = [] + _storages: list[Storage] = [] _db = cache = inject.attr(SqliteDatabase) def __init__(self): @@ -49,10 +49,7 @@ def last_played_book(self, new_value): @property def default_location(self): - return next(location - for location - in self.storage_locations - if location.default) + return next(location for location in self.storage_locations if location.default) @property def storage_locations(self): @@ -69,11 +66,9 @@ def external_storage_locations(self): return [storage for storage in self._storages if storage.external] def invalidate(self): - self._storages = [] + self._storages.clear() def _load_all_storage_locations(self): - self._storages = [] - for storage_db_obj in StorageModel.select(StorageModel.id): try: self._storages.append(Storage(self._db, storage_db_obj.id)) @@ -83,9 +78,5 @@ def _load_all_storage_locations(self): self._ensure_default_storage_present() def _ensure_default_storage_present(self): - default_storage_present = any(storage.default - for storage - in self._storages) - - if not default_storage_present and len(self._storages) > 0: + if self._storages and not any(storage.default for storage in self._storages): self._storages[0].default = True diff --git a/cozy/model/storage.py b/cozy/model/storage.py index cd5a8fc9..b09a3309 100644 --- a/cozy/model/storage.py +++ b/cozy/model/storage.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from peewee import SqliteDatabase @@ -17,8 +17,8 @@ def __init__(self, db: SqliteDatabase, db_id: int): self._get_db_object() @staticmethod - def new(db: SqliteDatabase): - db_obj = StorageModel.create(path="") + def new(db: SqliteDatabase, path: str): + db_obj = StorageModel.create(path=path) return Storage(db, db_obj.id) def _get_db_object(self): @@ -33,11 +33,11 @@ def path(self): return self._db_object.path @path.setter - def path(self, new_path: str): - if not os.path.isabs(new_path): + def path(self, path: str): + if not Path(path).is_absolute(): raise InvalidPath - self._db_object.path = new_path + self._db_object.path = path self._db_object.save(only=self._db_object.dirty_fields) @property @@ -68,4 +68,4 @@ def external(self, new_external: bool): self._db_object.save(only=self._db_object.dirty_fields) def delete(self): - self._db_object.delete_instance(recursive=True, delete_nullable=False) \ No newline at end of file + self._db_object.delete_instance(recursive=True, delete_nullable=False) diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index e0b2e20a..39801517 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -17,7 +17,7 @@ from cozy.media.importer import Importer, ScanStatus from cozy.media.player import Player from cozy.model.settings import Settings as SettingsModel -from cozy.view_model.settings_view_model import SettingsViewModel +from cozy.view_model.storages_view_model import StoragesViewModel from cozy.open_view import OpenView from cozy.ui.library_view import LibraryView from cozy.ui.preferences_view import PreferencesView @@ -38,7 +38,7 @@ class CozyUI(EventSender, metaclass=Singleton): _settings: SettingsModel = inject.attr(SettingsModel) _files: Files = inject.attr(Files) _player: Player = inject.attr(Player) - _settings_view_model: SettingsViewModel = inject.attr(SettingsViewModel) + _storages_view_model: StoragesViewModel = inject.attr(StoragesViewModel) def __init__(self, pkgdatadir, app, version): super().__init__() @@ -309,7 +309,7 @@ def _on_drag_data_received(self, widget, value, *_): return True def _set_audiobook_path(self, path): - self._settings_view_model.add_first_storage_location(path) + self._storages_view_model.add_first_storage_location(path) self.main_stack.props.visible_child_name = "import" self.scan(None, None) self.fs_monitor.init_offline_mode() diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index 709488a1..a00c1266 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -1,11 +1,11 @@ from gi.repository import Gtk from cozy.view_model.settings_view_model import SettingsViewModel -import gi from gi.repository import Adw, Gio +from typing import Callable from cozy.ext import inject from cozy.ui.widgets.error_reporting import ErrorReporting -from cozy.ui.widgets.storage_list_box_row import StorageListBoxRow +from cozy.ui.widgets.storages import StorageLocations @Gtk.Template.from_resource('/com/github/geigi/cozy/preferences.ui') @@ -17,6 +17,8 @@ class PreferencesView(Adw.PreferencesWindow): _glib_settings: Gio.Settings = inject.attr(Gio.Settings) _view_model: SettingsViewModel = inject.attr(SettingsViewModel) + storages_page: Adw.PreferencesPage = Gtk.Template.Child() + dark_mode_switch: Gtk.Switch = Gtk.Template.Child() swap_author_reader_switch: Gtk.Switch = Gtk.Template.Child() replay_switch: Gtk.Switch = Gtk.Template.Child() @@ -28,44 +30,25 @@ class PreferencesView(Adw.PreferencesWindow): forward_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() fadeout_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() - storage_list_box: Gtk.ListBox = Gtk.Template.Child() - add_storage_button: Gtk.Button = Gtk.Template.Child() - remove_storage_button: Gtk.Button = Gtk.Template.Child() - external_storage_toggle_button: Gtk.ToggleButton = Gtk.Template.Child() - default_storage_button: Gtk.ToggleButton = Gtk.Template.Child() - user_feedback_preference_group: Adw.PreferencesRow = Gtk.Template.Child() def __init__(self, **kwargs): - super().__init__(**kwargs) + super().__init__(transient_for=self.main_window.window, **kwargs) error_reporting = ErrorReporting() error_reporting.show_header(False) self.user_feedback_preference_group.add(error_reporting) + storage_locations = StorageLocations() + self.storages_page.add(storage_locations) + self._bind_settings() - self._bind_view_model() self.connect("close-request", self._hide_window) 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) - - self.add_storage_button.connect("clicked", self._on_add_storage_clicked) - self.remove_storage_button.connect("clicked", self._on_remove_storage_clicked) - 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): - self._view_model.bind_to("storage_locations", self._init_storage_box) - self._view_model.bind_to("storage_attributes", self._refresh_storage_rows) - def _bind_settings(self): self._glib_settings.bind("dark-mode", self.dark_mode_switch, "active", Gio.SettingsBindFlags.DEFAULT) @@ -91,74 +74,16 @@ def _bind_settings(self): 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() - - for storage in self._view_model.storage_locations: - row = StorageListBoxRow(storage) - row.connect("location-changed", self._on_storage_location_changed) - self.storage_list_box.append(row) - - def _on_add_storage_clicked(self, _): - self._view_model.add_storage_location() - - def _on_remove_storage_clicked(self, _): - row = self.storage_list_box.get_selected_row() - self._view_model.remove_storage_location(row.model) - - def _on_default_storage_clicked(self, _): - row = self.storage_list_box.get_selected_row() - self._view_model.set_default_storage(row.model) - self._on_storage_box_changed(None, row) - - def _on_storage_box_changed(self, _, row): - row = self.storage_list_box.get_selected_row() - if row is None: - sensitive = False - default_sensitive = False - remove_sensitive = False - else: - sensitive = True - remove_sensitive = True - if row.model.default or not row.model.path: - default_sensitive = remove_sensitive = False - else: - default_sensitive = True - - if not row.model.path: - remove_sensitive = True - - self.external_storage_toggle_button.handler_block(self.external_button_handle_id) - self.external_storage_toggle_button.set_active(row.model.external) - self.external_storage_toggle_button.handler_unblock(self.external_button_handle_id) - - self.remove_storage_button.set_sensitive(remove_sensitive) - self.external_storage_toggle_button.set_sensitive(sensitive) - self.default_storage_button.set_sensitive(default_sensitive) - - def _on_external_clicked(self, _): - external = self.external_storage_toggle_button.get_active() - row = self.storage_list_box.get_selected_row() - self._view_model.set_storage_external(row.model, external) - - def _on_storage_location_changed(self, widget, new_location): - self._view_model.change_storage_location(widget.model, new_location) - - def _refresh_storage_rows(self): - self._init_storage_box() - - self._on_storage_box_changed(None, self.storage_list_box.get_selected_row()) def _on_lock_ui_changed(self): sensitive = not self._view_model.lock_ui - self.storage_list_box.set_sensitive(sensitive) + self.storage_locations_list.set_sensitive(sensitive) self.add_storage_button.set_sensitive(sensitive) self.remove_storage_button.set_sensitive(sensitive) self.external_storage_toggle_button.set_sensitive(sensitive) self.default_storage_button.set_sensitive(sensitive) - self._on_storage_box_changed(None, self.storage_list_box.get_selected_row()) + self._on_storage_box_changed(None, self.storage_locations_list.get_selected_row()) def _hide_window(self, *_): self.hide() diff --git a/cozy/ui/widgets/storage_list_box_row.py b/cozy/ui/widgets/storage_list_box_row.py deleted file mode 100644 index fcf97bd9..00000000 --- a/cozy/ui/widgets/storage_list_box_row.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -from threading import Thread - -from cozy.control.filesystem_monitor import FilesystemMonitor -from cozy.model.storage import Storage -from cozy.ext import inject -from cozy.model.library import Library -from cozy.model.settings import Settings -from gi.repository import Gtk, GObject, Gio, GLib - -log = logging.getLogger("settings") - - -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 - - super(Gtk.ListBoxRow, self).__init__() - box = Gtk.Box() - box.set_orientation(Gtk.Orientation.HORIZONTAL) - box.set_spacing(3) - box.set_halign(Gtk.Align.FILL) - box.set_valign(Gtk.Align.CENTER) - 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") - self.default_image.set_margin_end(5) - - self.type_image = Gtk.Image() - self._set_drive_icon() - 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 - def model(self) -> Storage: - return self._model - - def refresh(self): - self._set_drive_icon() - self._set_default_icon() - - def __on_folder_changed(self, new_path): - self.emit("location-changed", new_path) - - def _set_drive_icon(self): - if self._model.external: - icon_name = "network-server-symbolic" - self.type_image.set_tooltip_text(_("External drive")) - else: - icon_name = "drive-harddisk-symbolic" - self.type_image.set_tooltip_text(_("Internal drive")) - - 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/storages.py b/cozy/ui/widgets/storages.py new file mode 100644 index 00000000..0f6e59c0 --- /dev/null +++ b/cozy/ui/widgets/storages.py @@ -0,0 +1,161 @@ +import logging +from threading import Thread +from typing import Callable + +from cozy.control.filesystem_monitor import FilesystemMonitor +from cozy.model.storage import Storage +from cozy.ext import inject +from cozy.model.library import Library +from cozy.model.settings import Settings +from gi.repository import Gtk, GObject, Gio, GLib, Adw +from cozy.view_model.storages_view_model import StoragesViewModel + +log = logging.getLogger("settings") + + +def ask_storage_location(callback: Callable[[str], None], *junk): + location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) + + # if path: + # folder = Gio.File.new_for_path(path) + # location_chooser.set_initial_folder(folder) + + def finish_callback(dialog, result): + try: + file = dialog.select_folder_finish(result) + except GLib.GError: + pass + else: + if file is not None: + callback(file.get_path()) + + location_chooser.select_folder(inject.instance("MainWindow").window, None, finish_callback) + + +@Gtk.Template.from_resource("/com/github/geigi/cozy/storage_row.ui") +class StorageRow(Adw.ActionRow): + __gtype_name__ = "StorageRow" + + icon: Gtk.Image = Gtk.Template.Child() + default_icon: Gtk.Image = Gtk.Template.Child() + menu_button: Gtk.MenuButton = Gtk.Template.Child() + + def __init__(self, model: Storage, menu_model: Gio.Menu) -> None: + self._model = model + + super().__init__(title=model.path) + self.connect("activated", self.ask_for_new_location) + + self.menu_button.set_menu_model(menu_model) + self.menu_button.connect("notify::active", self._on_menu_opened) + + self._set_default_icon() + self._set_drive_icon() + + @property + def model(self) -> Storage: + return self._model + + def ask_for_new_location(self, *_): + ask_storage_location(self._on_folder_changed) + + def _on_folder_changed(self, new_path): + self.emit("location-changed", new_path) + + def _on_menu_opened(self, *_): + self.emit("menu-opened") + + def _set_drive_icon(self): + if self._model.external: + self.icon.set_from_icon_name("network-server-symbolic") + self.icon.set_tooltip_text(_("External drive")) + else: + self.icon.set_from_icon_name("folder-open-symbolic") + self.icon.set_tooltip_text(_("Internal drive")) + + def _set_default_icon(self): + self.default_icon.set_visible(self._model.default) + + +GObject.signal_new('location-changed', StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, + (GObject.TYPE_PYOBJECT,)) +GObject.signal_new('menu-opened', StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, ()) + + +@Gtk.Template.from_resource("/com/github/geigi/cozy/storage_locations.ui") +class StorageLocations(Adw.PreferencesGroup): + __gtype_name__ = "StorageLocations" + + _view_model: StoragesViewModel = inject.attr(StoragesViewModel) + + storage_locations_list: Gtk.ListBox = Gtk.Template.Child() + new_storage_button: Adw.ActionRow = Gtk.Template.Child() + storage_menu: Gio.Menu = Gtk.Template.Child() + + def __init__(self) -> None: + super().__init__() + + self.new_storage_button.connect("activated", self._on_new_storage_clicked) + + self._view_model.bind_to("storage_locations", self._reload_storage_list) + self._view_model.bind_to("storage_attributes", self._reload_storage_list) + + self._create_actions() + + self._reload_storage_list() + + def _create_actions(self): + self.action_group = Gio.SimpleActionGroup.new() + self.insert_action_group("storage", self.action_group) + + self.set_external_action = Gio.SimpleAction.new_stateful( + "mark-external", + None, + GLib.Variant.new_boolean(False), + ) + self.set_external_signal_handler = self.set_external_action.connect( + "notify::state", self._mark_storage_location_external + ) + self.action_group.add_action(self.set_external_action) + + self.remove_action = Gio.SimpleAction.new("remove", None) + self.remove_action.connect("activate", self._remove_storage_location) + self.action_group.add_action(self.remove_action) + + self.make_default_action = Gio.SimpleAction.new("make-default", None) + self.make_default_action.connect("activate", self._set_default_storage_location) + self.action_group.add_action(self.make_default_action) + + def _reload_storage_list(self): + self.storage_locations_list.remove_all() + + for storage in self._view_model.storages: + row = StorageRow(storage, menu_model=self.storage_menu) + row.connect("location-changed", self._on_storage_location_changed) + row.connect("menu-opened", self._on_storage_menu_opened) + self.storage_locations_list.append(row) + + def _remove_storage_location(self, *_): + self._view_model.remove(self._view_model.selected_storage) + + def _set_default_storage_location(self, *_): + self._view_model.set_default(self._view_model.selected_storage) + + def _mark_storage_location_external(self, action, value): + value = action.get_property(value.name) + self._view_model.set_external(self._view_model.selected_storage, value) + + def _on_new_storage_clicked(self, *junk): + ask_storage_location(self._view_model.add_storage_location) + + def _on_storage_location_changed(self, widget, new_location): + self._view_model.change_storage_location(widget.model, new_location) + + def _on_storage_menu_opened(self, widget: StorageRow): + with self.set_external_action.handler_block(self.set_external_signal_handler): + self.set_external_action.props.state = GLib.Variant.new_boolean(widget.model.external) + + self.remove_action.props.enabled = not widget.model.default and len(self._view_model.storages) > 1 + self.make_default_action.props.enabled = widget.model is not self._view_model.default + self._view_model.selected_storage = widget.model + diff --git a/cozy/view_model/settings_view_model.py b/cozy/view_model/settings_view_model.py index 28251f8a..f5333529 100644 --- a/cozy/view_model/settings_view_model.py +++ b/cozy/view_model/settings_view_model.py @@ -1,31 +1,21 @@ import logging -from threading import Thread -from typing import List -from peewee import SqliteDatabase +from gi.repository import Adw, Gtk + from cozy.application_settings import ApplicationSettings from cozy.architecture.event_sender import EventSender from cozy.architecture.observable import Observable -from cozy.control.filesystem_monitor import FilesystemMonitor -from cozy.model.library import Library -from cozy.model.storage import Storage from cozy.ext import inject from cozy.media.importer import Importer from cozy.model.settings import Settings -from cozy.report import reporter -from gi.repository import Gtk, Adw - - log = logging.getLogger("settings_view_model") + class SettingsViewModel(Observable, EventSender): - _library: Library = inject.attr(Library) _importer: Importer = inject.attr(Importer) _model: Settings = inject.attr(Settings) _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) - _db = inject.attr(SqliteDatabase) - _fs_monitor = inject.attr(FilesystemMonitor) def __init__(self): super().__init__() @@ -33,7 +23,6 @@ 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() @@ -42,84 +31,15 @@ def __init__(self): if self._model.first_start: self._importer.scan() - @property - def storage_locations(self) -> List[Storage]: - return self._model.storage_locations - @property def lock_ui(self) -> bool: return self._lock_ui - + @lock_ui.setter def lock_ui(self, new_value: bool): self._lock_ui = new_value self._notify("lock_ui") - def add_storage_location(self): - Storage.new(self._db) - self._model.invalidate() - self._notify("storage_locations") - - def remove_storage_location(self, model: Storage): - if model.default: - log.error("deleting the default storage location {} is not possible".format(model.path)) - reporter.error("settings_view_model", "deleting the default storage location is not possible") - return - - model.delete() - self._model.invalidate() - self._notify("storage_locations") - self.emit_event("storage-removed", model) - - def set_storage_external(self, model: Storage, external: bool): - model.external = external - - if external: - self.emit_event("external-storage-added", model) - else: - self.emit_event("external-storage-removed", model) - - self._notify("storage_attributes") - - def set_default_storage(self, model: Storage): - if model.default: - return - - for storage in self._model.storage_locations: - storage.default = False - - model.default = True - - self._notify("storage_attributes") - - def change_storage_location(self, model: Storage, new_path: str): - old_path = model.path - model.path = new_path - model.external = self._fs_monitor.is_external(new_path) - - if old_path == "": - self.emit_event("storage-added", model) - log.info("New audiobook location added. Starting import scan.") - thread = Thread(target=self._importer.scan, name="ImportThread") - thread.start() - else: - self.emit_event("storage-changed", model) - log.info("Audio book location changed, rebasing the location in Cozy.") - thread = Thread(target=self._library.rebase_path, args=(old_path, new_path), name="RebaseStorageLocationThread") - thread.start() - - self._notify("storage_attributes") - - def add_first_storage_location(self, path: str): - storage = self._model.storage_locations[0] - - storage.path = path - storage.default = True - storage.external = self._fs_monitor.is_external(path) - - self._model.invalidate() - self._notify("storage_locations") - def _set_dark_mode(self): if self._app_settings.dark_mode: self.style_manager.set_color_scheme(Adw.ColorScheme.PREFER_DARK) diff --git a/cozy/view_model/storages_view_model.py b/cozy/view_model/storages_view_model.py new file mode 100644 index 00000000..34229c33 --- /dev/null +++ b/cozy/view_model/storages_view_model.py @@ -0,0 +1,119 @@ +import logging +from threading import Thread + +from peewee import SqliteDatabase +from cozy.application_settings import ApplicationSettings +from cozy.architecture.event_sender import EventSender +from cozy.architecture.observable import Observable +from cozy.control.filesystem_monitor import FilesystemMonitor +from cozy.model.library import Library +from cozy.model.storage import Storage +from cozy.ext import inject +from cozy.media.importer import Importer +from cozy.model.settings import Settings +from cozy.report import reporter +from gi.repository import Gtk, Adw + + + +log = logging.getLogger("storages_view_model") + +class StoragesViewModel(Observable, EventSender): + _library: Library = inject.attr(Library) + _importer: Importer = inject.attr(Importer) + _model: Settings = inject.attr(Settings) + _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) + _db = inject.attr(SqliteDatabase) + _fs_monitor = inject.attr(FilesystemMonitor) + + def __init__(self): + super().__init__() + super(Observable, self).__init__() + + self._selected_storage = None + + def _scan_new_storage(self, model: Storage): + self.emit_event("storage-added", model) + log.info("New audiobook location added. Starting import scan.") + thread = Thread(target=self._importer.scan, name="ImportThread") + thread.start() + + def _rebase_storage_location(self, model: Storage, old_path: str): + self.emit_event("storage-changed", model) + log.info("Audio book location changed, rebasing the location in Cozy.") + thread = Thread(target=self._library.rebase_path, args=(old_path, model.path), name="RebaseStorageLocationThread") + thread.start() + + def add_storage_location(self, path: str) -> None: + model = Storage.new(self._db, path) + + self._model.invalidate() + self._scan_new_storage(model) + self._notify("storage_locations") + + def add_first_storage_location(self, path: str): + storage = self.storages[0] + + storage.path = path + storage.default = True + storage.external = self._fs_monitor.is_external(path) + + self._model.invalidate() + self._notify("storage_locations") + + def change_storage_location(self, model: Storage, new_path: str) -> None: + old_path = model.path + model.path = new_path + model.external = self._fs_monitor.is_external(new_path) + + self._rebase_storage_location(model, old_path) + self._notify("storage_attributes") + + @property + def storages(self) -> list[Storage]: + return self._model.storage_locations + + @property + def default(self) -> Storage | None: + for item in self.storages: + if item.default: + return item + + @property + def selected_storage(self) -> Storage | None: + return self._selected_storage + + @selected_storage.setter + def selected_storage(self, value) -> None: + self._selected_storage = value + + def remove(self, model: Storage) -> None: + if model.default: + return + + model.delete() + self._model.invalidate() + self.emit_event("storage-removed", model) + + self._notify("storage_locations") + + def set_default(self, model: Storage): + if model.default: + return + + for storage in self.storages: + storage.default = False + + model.default = True + + self._notify("storage_attributes") + + def set_external(self, model: Storage, external: bool): + model.external = external + + if external: + self.emit_event("external-storage-added", model) + else: + self.emit_event("external-storage-removed", model) + + self._notify("storage_attributes") diff --git a/data/ui/gresource.xml b/data/ui/gresource.xml index 05828d20..fbb091fe 100644 --- a/data/ui/gresource.xml +++ b/data/ui/gresource.xml @@ -16,6 +16,8 @@ progress_popover.ui search_popover.ui seek_bar.ui + storage_locations.ui + storage_row.ui timer_popover.ui welcome.ui whats_new.ui diff --git a/data/ui/preferences.ui b/data/ui/preferences.ui index 20045d83..10235bbc 100644 --- a/data/ui/preferences.ui +++ b/data/ui/preferences.ui @@ -97,7 +97,7 @@ - + harddisk-symbolic Storage @@ -111,134 +111,6 @@ - - - Storage locations - - - Storage locations - - - 13 - 13 - 13 - 13 - true - vertical - - - 250 - - - true - never - - - - - 1 - - - - - - - - - - - - - - 24 - - - - 24 - 24 - Add location - - - list-add-symbolic - - - - - - - - 24 - 24 - false - Remove location - - - list-remove-symbolic - - - - - - - - vertical - True - - - - - - External drive - 24 - 24 - false - Toggle this storage location to be internal/external. - - - network-server-symbolic - - - - - - - - Set as default - 24 - 24 - false - Set as default storage location for new audiobooks - - - checkmark-symbolic - - - - - - - - - - - - - - diff --git a/data/ui/storage_locations.ui b/data/ui/storage_locations.ui new file mode 100644 index 00000000..a4922c55 --- /dev/null +++ b/data/ui/storage_locations.ui @@ -0,0 +1,54 @@ + + + + + +
+ + External drive + storage.mark-external + +
+
+ + Set as default + storage.make-default + + + Remove + storage.remove + +
+
+
+ diff --git a/data/ui/storage_row.ui b/data/ui/storage_row.ui new file mode 100644 index 00000000..0669bcf9 --- /dev/null +++ b/data/ui/storage_row.ui @@ -0,0 +1,32 @@ + + + + + From fcc0fe88f80dcff087e43daa90c1c91d0e9837be Mon Sep 17 00:00:00 2001 From: rdbende Date: Wed, 20 Dec 2023 18:44:11 +0100 Subject: [PATCH 09/28] Did I delete the ("working", True) event emission? --- cozy/ui/preferences_view.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index a00c1266..d5e7f7b5 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -18,20 +18,19 @@ class PreferencesView(Adw.PreferencesWindow): _view_model: SettingsViewModel = inject.attr(SettingsViewModel) storages_page: Adw.PreferencesPage = Gtk.Template.Child() + user_feedback_preference_group: Adw.PreferencesGroup = Gtk.Template.Child() - dark_mode_switch: Gtk.Switch = Gtk.Template.Child() - swap_author_reader_switch: Gtk.Switch = Gtk.Template.Child() - replay_switch: Gtk.Switch = Gtk.Template.Child() + dark_mode_switch: Adw.SwitchRow = Gtk.Template.Child() + swap_author_reader_switch: Adw.SwitchRow = Gtk.Template.Child() + replay_switch: Adw.SwitchRow = 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() + artwork_prefer_external_switch: Adw.SwitchRow = Gtk.Template.Child() rewind_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() forward_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() fadeout_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() - user_feedback_preference_group: Adw.PreferencesRow = Gtk.Template.Child() - def __init__(self, **kwargs): super().__init__(transient_for=self.main_window.window, **kwargs) @@ -39,15 +38,14 @@ def __init__(self, **kwargs): error_reporting.show_header(False) self.user_feedback_preference_group.add(error_reporting) - storage_locations = StorageLocations() - self.storages_page.add(storage_locations) + self.storage_locations_view = StorageLocations() + self.storages_page.add(self.storage_locations_view) self._bind_settings() - self.connect("close-request", self._hide_window) + self._view_model.bind_to("lock_ui", self._on_lock_ui_changed) - 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.connect("close-request", self._hide_window) def _bind_settings(self): self._glib_settings.bind("dark-mode", self.dark_mode_switch, "active", @@ -71,19 +69,10 @@ 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, widget, param): - state = widget.get_property(param.name) - self.fadeout_duration_spin_button.set_sensitive(state) - def _on_lock_ui_changed(self): sensitive = not self._view_model.lock_ui - self.storage_locations_list.set_sensitive(sensitive) - self.add_storage_button.set_sensitive(sensitive) - self.remove_storage_button.set_sensitive(sensitive) - self.external_storage_toggle_button.set_sensitive(sensitive) - self.default_storage_button.set_sensitive(sensitive) - self._on_storage_box_changed(None, self.storage_locations_list.get_selected_row()) + self.storage_locations_view.set_sensitive(sensitive) def _hide_window(self, *_): self.hide() From 9693858a960b85b7c2b3b5b7ff59de73ea33030e Mon Sep 17 00:00:00 2001 From: rdbende Date: Wed, 20 Dec 2023 19:00:48 +0100 Subject: [PATCH 10/28] Use AdwExpanderRow --- cozy/ui/preferences_view.py | 3 +-- data/ui/preferences.ui | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index d5e7f7b5..bf587d71 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -24,7 +24,6 @@ class PreferencesView(Adw.PreferencesWindow): swap_author_reader_switch: Adw.SwitchRow = Gtk.Template.Child() replay_switch: Adw.SwitchRow = 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: Adw.SwitchRow = Gtk.Template.Child() rewind_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() @@ -60,7 +59,7 @@ def _bind_settings(self): self._glib_settings.bind("forward-duration", self.forward_duration_adjustment, "value", Gio.SettingsBindFlags.DEFAULT) - self._glib_settings.bind("sleep-timer-fadeout", self.sleep_timer_fadeout_switch, "active", + self._glib_settings.bind("sleep-timer-fadeout", self.sleep_timer_fadeout_switch, "enable-expansion", Gio.SettingsBindFlags.DEFAULT) self._glib_settings.bind("sleep-timer-fadeout-duration", self.fadeout_duration_adjustment, diff --git a/data/ui/preferences.ui b/data/ui/preferences.ui index 10235bbc..a38b3219 100644 --- a/data/ui/preferences.ui +++ b/data/ui/preferences.ui @@ -79,17 +79,19 @@ Sleep Timer - + Fadeout - - - - - Fadeout duration - true - fadeout_duration_adjustment - true - true + true + true + + + Fadeout duration + true + fadeout_duration_adjustment + true + true + + From 5e7a13bd0bacebc3c574f42bb2e5e70a3c8c542c Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 15:52:45 +0100 Subject: [PATCH 11/28] Wrong order of functions. Yikes! --- cozy/view_model/storages_view_model.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cozy/view_model/storages_view_model.py b/cozy/view_model/storages_view_model.py index 34229c33..22aeee3a 100644 --- a/cozy/view_model/storages_view_model.py +++ b/cozy/view_model/storages_view_model.py @@ -35,25 +35,28 @@ def __init__(self): def _scan_new_storage(self, model: Storage): self.emit_event("storage-added", model) log.info("New audiobook location added. Starting import scan.") - thread = Thread(target=self._importer.scan, name="ImportThread") - thread.start() + Thread(target=self._importer.scan, name="ImportThread").start() def _rebase_storage_location(self, model: Storage, old_path: str): self.emit_event("storage-changed", model) log.info("Audio book location changed, rebasing the location in Cozy.") - thread = Thread(target=self._library.rebase_path, args=(old_path, model.path), name="RebaseStorageLocationThread") - thread.start() + Thread( + target=self._library.rebase_path, + args=(old_path, model.path), + name="RebaseStorageLocationThread" + ).start() def add_storage_location(self, path: str) -> None: model = Storage.new(self._db, path) + model.external = self._fs_monitor.is_external(path) self._model.invalidate() - self._scan_new_storage(model) self._notify("storage_locations") + self._scan_new_storage(model) + def add_first_storage_location(self, path: str): storage = self.storages[0] - storage.path = path storage.default = True storage.external = self._fs_monitor.is_external(path) From 242bd2401f33d7c9e3b646a2157d482d13afef5b Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 16:03:22 +0100 Subject: [PATCH 12/28] Add missing type annotations and format the code --- cozy/ui/preferences_view.py | 89 +++++++++++++++++--------- cozy/ui/widgets/storages.py | 69 ++++++++++++-------- cozy/view_model/storages_view_model.py | 24 ++++--- 3 files changed, 112 insertions(+), 70 deletions(-) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index bf587d71..ce871893 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -1,14 +1,14 @@ -from gi.repository import Gtk -from cozy.view_model.settings_view_model import SettingsViewModel -from gi.repository import Adw, Gio -from typing import Callable +from typing import Any + +from gi.repository import Adw, Gio, Gtk from cozy.ext import inject from cozy.ui.widgets.error_reporting import ErrorReporting from cozy.ui.widgets.storages import StorageLocations +from cozy.view_model.settings_view_model import SettingsViewModel -@Gtk.Template.from_resource('/com/github/geigi/cozy/preferences.ui') +@Gtk.Template.from_resource("/com/github/geigi/cozy/preferences.ui") class PreferencesView(Adw.PreferencesWindow): __gtype_name__ = "PreferencesWindow" @@ -30,7 +30,7 @@ class PreferencesView(Adw.PreferencesWindow): forward_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() fadeout_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(transient_for=self.main_window.window, **kwargs) error_reporting = ErrorReporting() @@ -46,33 +46,62 @@ def __init__(self, **kwargs): self.connect("close-request", self._hide_window) - def _bind_settings(self): - self._glib_settings.bind("dark-mode", self.dark_mode_switch, "active", - Gio.SettingsBindFlags.DEFAULT) - - self._glib_settings.bind("swap-author-reader", self.swap_author_reader_switch, "active", - Gio.SettingsBindFlags.DEFAULT) - - self._glib_settings.bind("replay", self.replay_switch, "active", Gio.SettingsBindFlags.DEFAULT) - self._glib_settings.bind("rewind-duration", self.rewind_duration_adjustment, "value", - Gio.SettingsBindFlags.DEFAULT) - self._glib_settings.bind("forward-duration", self.forward_duration_adjustment, "value", - Gio.SettingsBindFlags.DEFAULT) - - self._glib_settings.bind("sleep-timer-fadeout", self.sleep_timer_fadeout_switch, "enable-expansion", - Gio.SettingsBindFlags.DEFAULT) - - self._glib_settings.bind("sleep-timer-fadeout-duration", self.fadeout_duration_adjustment, - "value", Gio.SettingsBindFlags.DEFAULT) - - self._glib_settings.bind("prefer-external-cover", self.artwork_prefer_external_switch, "active", - Gio.SettingsBindFlags.DEFAULT) - - def _on_lock_ui_changed(self): + def _bind_settings(self) -> None: + self._glib_settings.bind( + "dark-mode", self.dark_mode_switch, "active", Gio.SettingsBindFlags.DEFAULT + ) + + self._glib_settings.bind( + "swap-author-reader", + self.swap_author_reader_switch, + "active", + Gio.SettingsBindFlags.DEFAULT, + ) + + self._glib_settings.bind( + "replay", self.replay_switch, "active", Gio.SettingsBindFlags.DEFAULT + ) + self._glib_settings.bind( + "rewind-duration", + self.rewind_duration_adjustment, + "value", + Gio.SettingsBindFlags.DEFAULT, + ) + self._glib_settings.bind( + "forward-duration", + self.forward_duration_adjustment, + "value", + Gio.SettingsBindFlags.DEFAULT, + ) + + self._glib_settings.bind( + "sleep-timer-fadeout", + self.sleep_timer_fadeout_switch, + "enable-expansion", + Gio.SettingsBindFlags.DEFAULT, + ) + + self._glib_settings.bind( + "sleep-timer-fadeout-duration", + self.fadeout_duration_adjustment, + "value", + Gio.SettingsBindFlags.DEFAULT, + ) + + self._glib_settings.bind( + "prefer-external-cover", + self.artwork_prefer_external_switch, + "active", + Gio.SettingsBindFlags.DEFAULT, + ) + + self.storage_locations_view.set_sensitive(False) + + def _on_lock_ui_changed(self) -> None: sensitive = not self._view_model.lock_ui self.storage_locations_view.set_sensitive(sensitive) - + def _hide_window(self, *_): self.hide() return True diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index 0f6e59c0..55d5280c 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -1,13 +1,10 @@ import logging -from threading import Thread from typing import Callable -from cozy.control.filesystem_monitor import FilesystemMonitor -from cozy.model.storage import Storage +from gi.repository import Adw, Gio, GLib, GObject, Gtk + from cozy.ext import inject -from cozy.model.library import Library -from cozy.model.settings import Settings -from gi.repository import Gtk, GObject, Gio, GLib, Adw +from cozy.model.storage import Storage from cozy.view_model.storages_view_model import StoragesViewModel log = logging.getLogger("settings") @@ -29,7 +26,9 @@ def finish_callback(dialog, result): if file is not None: callback(file.get_path()) - location_chooser.select_folder(inject.instance("MainWindow").window, None, finish_callback) + location_chooser.select_folder( + inject.instance("MainWindow").window, None, finish_callback + ) @Gtk.Template.from_resource("/com/github/geigi/cozy/storage_row.ui") @@ -56,16 +55,16 @@ def __init__(self, model: Storage, menu_model: Gio.Menu) -> None: def model(self) -> Storage: return self._model - def ask_for_new_location(self, *_): + def ask_for_new_location(self, *_) -> None: ask_storage_location(self._on_folder_changed) - def _on_folder_changed(self, new_path): + def _on_folder_changed(self, new_path: str) -> None: self.emit("location-changed", new_path) - def _on_menu_opened(self, *_): + def _on_menu_opened(self, *_) -> None: self.emit("menu-opened") - def _set_drive_icon(self): + def _set_drive_icon(self) -> None: if self._model.external: self.icon.set_from_icon_name("network-server-symbolic") self.icon.set_tooltip_text(_("External drive")) @@ -73,13 +72,20 @@ def _set_drive_icon(self): self.icon.set_from_icon_name("folder-open-symbolic") self.icon.set_tooltip_text(_("Internal drive")) - def _set_default_icon(self): + def _set_default_icon(self) -> None: self.default_icon.set_visible(self._model.default) -GObject.signal_new('location-changed', StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, - (GObject.TYPE_PYOBJECT,)) -GObject.signal_new('menu-opened', StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, ()) +GObject.signal_new( + "location-changed", + StorageRow, + GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, + (GObject.TYPE_PYOBJECT,), +) +GObject.signal_new( + "menu-opened", StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, () +) @Gtk.Template.from_resource("/com/github/geigi/cozy/storage_locations.ui") @@ -104,7 +110,7 @@ def __init__(self) -> None: self._reload_storage_list() - def _create_actions(self): + def _create_actions(self) -> None: self.action_group = Gio.SimpleActionGroup.new() self.insert_action_group("storage", self.action_group) @@ -126,7 +132,7 @@ def _create_actions(self): self.make_default_action.connect("activate", self._set_default_storage_location) self.action_group.add_action(self.make_default_action) - def _reload_storage_list(self): + def _reload_storage_list(self) -> None: self.storage_locations_list.remove_all() for storage in self._view_model.storages: @@ -135,27 +141,36 @@ def _reload_storage_list(self): row.connect("menu-opened", self._on_storage_menu_opened) self.storage_locations_list.append(row) - def _remove_storage_location(self, *_): + def _remove_storage_location(self, *_) -> None: self._view_model.remove(self._view_model.selected_storage) - def _set_default_storage_location(self, *_): + def _set_default_storage_location(self, *_) -> None: self._view_model.set_default(self._view_model.selected_storage) - def _mark_storage_location_external(self, action, value): + def _mark_storage_location_external( + self, action: Gio.SimpleAction, value: GObject.ParamSpec + ) -> None: value = action.get_property(value.name) self._view_model.set_external(self._view_model.selected_storage, value) - def _on_new_storage_clicked(self, *junk): + def _on_new_storage_clicked(self, *junk) -> None: ask_storage_location(self._view_model.add_storage_location) - def _on_storage_location_changed(self, widget, new_location): + def _on_storage_location_changed( + self, widget: StorageRow, new_location: str + ) -> None: self._view_model.change_storage_location(widget.model, new_location) - def _on_storage_menu_opened(self, widget: StorageRow): + def _on_storage_menu_opened(self, widget: StorageRow) -> None: with self.set_external_action.handler_block(self.set_external_signal_handler): - self.set_external_action.props.state = GLib.Variant.new_boolean(widget.model.external) + self.set_external_action.props.state = GLib.Variant.new_boolean( + widget.model.external + ) - self.remove_action.props.enabled = not widget.model.default and len(self._view_model.storages) > 1 - self.make_default_action.props.enabled = widget.model is not self._view_model.default + self.remove_action.props.enabled = ( + not widget.model.default and len(self._view_model.storages) > 1 + ) + self.make_default_action.props.enabled = ( + widget.model is not self._view_model.default + ) self._view_model.selected_storage = widget.model - diff --git a/cozy/view_model/storages_view_model.py b/cozy/view_model/storages_view_model.py index 22aeee3a..c9463fe3 100644 --- a/cozy/view_model/storages_view_model.py +++ b/cozy/view_model/storages_view_model.py @@ -2,22 +2,20 @@ from threading import Thread from peewee import SqliteDatabase + from cozy.application_settings import ApplicationSettings from cozy.architecture.event_sender import EventSender from cozy.architecture.observable import Observable from cozy.control.filesystem_monitor import FilesystemMonitor -from cozy.model.library import Library -from cozy.model.storage import Storage from cozy.ext import inject from cozy.media.importer import Importer +from cozy.model.library import Library from cozy.model.settings import Settings -from cozy.report import reporter -from gi.repository import Gtk, Adw - - +from cozy.model.storage import Storage log = logging.getLogger("storages_view_model") + class StoragesViewModel(Observable, EventSender): _library: Library = inject.attr(Library) _importer: Importer = inject.attr(Importer) @@ -26,24 +24,24 @@ class StoragesViewModel(Observable, EventSender): _db = inject.attr(SqliteDatabase) _fs_monitor = inject.attr(FilesystemMonitor) - def __init__(self): + def __init__(self) -> None: super().__init__() super(Observable, self).__init__() self._selected_storage = None - def _scan_new_storage(self, model: Storage): + def _scan_new_storage(self, model: Storage) -> None: self.emit_event("storage-added", model) log.info("New audiobook location added. Starting import scan.") Thread(target=self._importer.scan, name="ImportThread").start() - def _rebase_storage_location(self, model: Storage, old_path: str): + def _rebase_storage_location(self, model: Storage, old_path: str) -> None: self.emit_event("storage-changed", model) log.info("Audio book location changed, rebasing the location in Cozy.") Thread( target=self._library.rebase_path, args=(old_path, model.path), - name="RebaseStorageLocationThread" + name="RebaseStorageLocationThread", ).start() def add_storage_location(self, path: str) -> None: @@ -55,7 +53,7 @@ def add_storage_location(self, path: str) -> None: self._scan_new_storage(model) - def add_first_storage_location(self, path: str): + def add_first_storage_location(self, path: str) -> None: storage = self.storages[0] storage.path = path storage.default = True @@ -100,7 +98,7 @@ def remove(self, model: Storage) -> None: self._notify("storage_locations") - def set_default(self, model: Storage): + def set_default(self, model: Storage) -> None: if model.default: return @@ -111,7 +109,7 @@ def set_default(self, model: Storage): self._notify("storage_attributes") - def set_external(self, model: Storage, external: bool): + def set_external(self, model: Storage, external: bool) -> None: model.external = external if external: From df9dcef7e27b06a6ce2921688e6ca6cb7c495cda Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 16:03:44 +0100 Subject: [PATCH 13/28] This was a test lol --- cozy/ui/preferences_view.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index ce871893..6fd02e57 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -95,8 +95,6 @@ def _bind_settings(self) -> None: Gio.SettingsBindFlags.DEFAULT, ) - self.storage_locations_view.set_sensitive(False) - def _on_lock_ui_changed(self) -> None: sensitive = not self._view_model.lock_ui From fa9eac4218faa3f4298c4c3595c0087661352f80 Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 16:09:16 +0100 Subject: [PATCH 14/28] New storage button as part of the same list --- cozy/ui/widgets/storages.py | 13 ++++++++++--- data/ui/storage_locations.ui | 20 -------------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index 55d5280c..4fb70068 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -95,18 +95,16 @@ class StorageLocations(Adw.PreferencesGroup): _view_model: StoragesViewModel = inject.attr(StoragesViewModel) storage_locations_list: Gtk.ListBox = Gtk.Template.Child() - new_storage_button: Adw.ActionRow = Gtk.Template.Child() storage_menu: Gio.Menu = Gtk.Template.Child() def __init__(self) -> None: super().__init__() - self.new_storage_button.connect("activated", self._on_new_storage_clicked) - self._view_model.bind_to("storage_locations", self._reload_storage_list) self._view_model.bind_to("storage_attributes", self._reload_storage_list) self._create_actions() + self.new_storage_button = self._create_new_storage_button() self._reload_storage_list() @@ -132,6 +130,13 @@ def _create_actions(self) -> None: self.make_default_action.connect("activate", self._set_default_storage_location) self.action_group.add_action(self.make_default_action) + def _create_new_storage_button(self) -> Adw.ActionRow: + icon = Gtk.Image(icon_name="list-add-symbolic", margin_top=18, margin_bottom=18) + row = Adw.ActionRow(selectable=False, activatable=True) + row.connect("activated", self._on_new_storage_clicked) + row.set_child(icon) + return row + def _reload_storage_list(self) -> None: self.storage_locations_list.remove_all() @@ -141,6 +146,8 @@ def _reload_storage_list(self) -> None: row.connect("menu-opened", self._on_storage_menu_opened) self.storage_locations_list.append(row) + self.storage_locations_list.append(self.new_storage_button) + def _remove_storage_location(self, *_) -> None: self._view_model.remove(self._view_model.selected_storage) diff --git a/data/ui/storage_locations.ui b/data/ui/storage_locations.ui index a4922c55..afb6dce7 100644 --- a/data/ui/storage_locations.ui +++ b/data/ui/storage_locations.ui @@ -11,26 +11,6 @@
- - - - - false - true - - - list-add-symbolic - 18 - 18 - - - - - - -
From ae2def337c387d6d614f140f44624c5710d12503 Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 16:40:13 +0100 Subject: [PATCH 15/28] I didn't think about the tests before removing that unnecessary code --- cozy/model/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cozy/model/settings.py b/cozy/model/settings.py index c56dd913..6d5a2fc3 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -69,6 +69,8 @@ def invalidate(self): self._storages.clear() def _load_all_storage_locations(self): + self.invalidate() + for storage_db_obj in StorageModel.select(StorageModel.id): try: self._storages.append(Storage(self._db, storage_db_obj.id)) From d5fda9e288ffac06c3fcabe1d679b3378c4c8342 Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 19:01:04 +0100 Subject: [PATCH 16/28] Set initial folder when changing location --- cozy/ui/widgets/storages.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index 4fb70068..abed1da6 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -10,12 +10,12 @@ log = logging.getLogger("settings") -def ask_storage_location(callback: Callable[[str], None], *junk): +def ask_storage_location(callback: Callable[[str], None], *junk, initial_folder: str | None = None): location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) - # if path: - # folder = Gio.File.new_for_path(path) - # location_chooser.set_initial_folder(folder) + if initial_folder: + gfile = Gio.File.new_for_path(initial_folder) + location_chooser.set_initial_folder(gfile) def finish_callback(dialog, result): try: @@ -56,7 +56,7 @@ def model(self) -> Storage: return self._model def ask_for_new_location(self, *_) -> None: - ask_storage_location(self._on_folder_changed) + ask_storage_location(self._on_folder_changed, initial_folder=self._model.path) def _on_folder_changed(self, new_path: str) -> None: self.emit("location-changed", new_path) From 96f50905f30e1a2c715a86456659e22ec858dcb2 Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 19:17:18 +0100 Subject: [PATCH 17/28] Nah, this shouldn't be entirely public --- cozy/ui/widgets/storages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index abed1da6..12a8a745 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -117,7 +117,7 @@ def _create_actions(self) -> None: None, GLib.Variant.new_boolean(False), ) - self.set_external_signal_handler = self.set_external_action.connect( + self._set_external_signal_handler = self.set_external_action.connect( "notify::state", self._mark_storage_location_external ) self.action_group.add_action(self.set_external_action) @@ -169,7 +169,7 @@ def _on_storage_location_changed( self._view_model.change_storage_location(widget.model, new_location) def _on_storage_menu_opened(self, widget: StorageRow) -> None: - with self.set_external_action.handler_block(self.set_external_signal_handler): + with self.set_external_action.handler_block(self._set_external_signal_handler): self.set_external_action.props.state = GLib.Variant.new_boolean( widget.model.external ) From 3f1fb744ae6ece5430e8de71c0951fa6ad2793cb Mon Sep 17 00:00:00 2001 From: rdbende Date: Thu, 21 Dec 2023 23:34:17 +0100 Subject: [PATCH 18/28] Disable storage list while work is being done --- cozy/app_controller.py | 6 ++++-- cozy/ui/main_view.py | 2 +- cozy/view_model/headerbar_view_model.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cozy/app_controller.py b/cozy/app_controller.py index 003f53db..b504eb2d 100644 --- a/cozy/app_controller.py +++ b/cozy/app_controller.py @@ -76,7 +76,7 @@ def __init__(self, gtk_app, main_window_builder, main_window): self.library_view_model.add_listener(self._on_open_view) self.library_view_model.add_listener(self._on_library_view_event) self.playback_control_view_model.add_listener(self._on_open_view) - self.headerbar_view_model.add_listener(self._on_open_view) + self.headerbar_view_model.add_listener(self._on_working_event) self.app_view_model.add_listener(self._on_app_view_event) self.main_window.add_listener(self._on_main_window_event) @@ -148,10 +148,12 @@ def _on_app_view_event(self, event: str, data): if event == "view": self.headerbar_view_model.set_view(data) - def _on_main_window_event(self, event: str, data): + def _on_working_event(self, event: str, data) -> None: if event == "working": self.book_detail_view_model.lock_ui = data self.settings_view_model.lock_ui = data + + def _on_main_window_event(self, event: str, data): if event == "open_view": self._on_open_view(data, None) diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index 39801517..2fdf606e 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -249,9 +249,9 @@ def switch_to_playing(self): self.block_ui_buttons(False, True) else: # we want to only block the player controls + # TODO: rework. this is messy self.block_ui_buttons(False, True) self.block_ui_buttons(True, False) - self.emit_event_main_thread("working", False) def check_for_tracks(self): """ diff --git a/cozy/view_model/headerbar_view_model.py b/cozy/view_model/headerbar_view_model.py index dc20d622..9b5e2223 100644 --- a/cozy/view_model/headerbar_view_model.py +++ b/cozy/view_model/headerbar_view_model.py @@ -63,10 +63,12 @@ def _start_working(self, message: str): self._notify("work_message") self._notify("work_progress") self._notify("state") + self.emit_event_main_thread("working", True) def _stop_working(self): self._state = HeaderBarState.PLAYING self._notify("state") + self.emit_event_main_thread("working", False) def _on_importer_event(self, event: str, message): if event == "scan-progress" and isinstance(message, float): From 79dd5e54a7fb368dc4834a34367ac71108c52a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedek=20D=C3=A9v=C3=A9nyi?= Date: Fri, 22 Dec 2023 01:05:42 +0100 Subject: [PATCH 19/28] Update cozy/view_model/storages_view_model.py Co-authored-by: Naglis Jonaitis <827324+naglis@users.noreply.github.com> --- cozy/view_model/storages_view_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cozy/view_model/storages_view_model.py b/cozy/view_model/storages_view_model.py index c9463fe3..0ae64381 100644 --- a/cozy/view_model/storages_view_model.py +++ b/cozy/view_model/storages_view_model.py @@ -37,7 +37,7 @@ def _scan_new_storage(self, model: Storage) -> None: def _rebase_storage_location(self, model: Storage, old_path: str) -> None: self.emit_event("storage-changed", model) - log.info("Audio book location changed, rebasing the location in Cozy.") + log.info("Audiobook location changed, rebasing the location in Cozy.") Thread( target=self._library.rebase_path, args=(old_path, model.path), From 3c8bef4d285e5bafeb86f22ae3519991f1a49b55 Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 12:54:52 +0100 Subject: [PATCH 20/28] Remove unused logging, format code with proper line-length --- cozy/model/settings.py | 14 ++++++++++---- cozy/ui/widgets/storages.py | 27 ++++++--------------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/cozy/model/settings.py b/cozy/model/settings.py index 6d5a2fc3..41ea8e0d 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -31,9 +31,13 @@ def last_played_book(self) -> Optional[Book]: try: return self._db_object.last_played_book except peewee.DoesNotExist: - log.warning("last_played_book references an non existent object. Setting last_played_book to None.") - reporter.warning("settings_model", - "last_played_book references an non existent object. Setting last_played_book to None.") + log.warning( + "last_played_book references an non existent object. Setting last_played_book to None." + ) + reporter.warning( + "settings_model", + "last_played_book references an non existent object. Setting last_played_book to None.", + ) self.last_played_book = None return None @@ -75,7 +79,9 @@ def _load_all_storage_locations(self): try: self._storages.append(Storage(self._db, storage_db_obj.id)) except InvalidPath: - log.error("Invalid path found in database, skipping: {}".format(storage_db_obj.path)) + log.error( + "Invalid path found in database, skipping: {}".format(storage_db_obj.path) + ) self._ensure_default_storage_present() diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index 12a8a745..aba283e0 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -1,4 +1,3 @@ -import logging from typing import Callable from gi.repository import Adw, Gio, GLib, GObject, Gtk @@ -7,8 +6,6 @@ from cozy.model.storage import Storage from cozy.view_model.storages_view_model import StoragesViewModel -log = logging.getLogger("settings") - def ask_storage_location(callback: Callable[[str], None], *junk, initial_folder: str | None = None): location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) @@ -26,9 +23,7 @@ def finish_callback(dialog, result): if file is not None: callback(file.get_path()) - location_chooser.select_folder( - inject.instance("MainWindow").window, None, finish_callback - ) + location_chooser.select_folder(inject.instance("MainWindow").window, None, finish_callback) @Gtk.Template.from_resource("/com/github/geigi/cozy/storage_row.ui") @@ -83,9 +78,7 @@ def _set_default_icon(self) -> None: GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,), ) -GObject.signal_new( - "menu-opened", StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, () -) +GObject.signal_new("menu-opened", StorageRow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, ()) @Gtk.Template.from_resource("/com/github/geigi/cozy/storage_locations.ui") @@ -113,9 +106,7 @@ def _create_actions(self) -> None: self.insert_action_group("storage", self.action_group) self.set_external_action = Gio.SimpleAction.new_stateful( - "mark-external", - None, - GLib.Variant.new_boolean(False), + "mark-external", None, GLib.Variant.new_boolean(False) ) self._set_external_signal_handler = self.set_external_action.connect( "notify::state", self._mark_storage_location_external @@ -163,21 +154,15 @@ def _mark_storage_location_external( def _on_new_storage_clicked(self, *junk) -> None: ask_storage_location(self._view_model.add_storage_location) - def _on_storage_location_changed( - self, widget: StorageRow, new_location: str - ) -> None: + def _on_storage_location_changed(self, widget: StorageRow, new_location: str) -> None: self._view_model.change_storage_location(widget.model, new_location) def _on_storage_menu_opened(self, widget: StorageRow) -> None: with self.set_external_action.handler_block(self._set_external_signal_handler): - self.set_external_action.props.state = GLib.Variant.new_boolean( - widget.model.external - ) + self.set_external_action.props.state = GLib.Variant.new_boolean(widget.model.external) self.remove_action.props.enabled = ( not widget.model.default and len(self._view_model.storages) > 1 ) - self.make_default_action.props.enabled = ( - widget.model is not self._view_model.default - ) + self.make_default_action.props.enabled = widget.model is not self._view_model.default self._view_model.selected_storage = widget.model From 7cead39ba2d33aab84bc79ccc49675f472c02d0c Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 13:04:50 +0100 Subject: [PATCH 21/28] It should already be the default --- cozy/view_model/storages_view_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cozy/view_model/storages_view_model.py b/cozy/view_model/storages_view_model.py index 0ae64381..c564d6f4 100644 --- a/cozy/view_model/storages_view_model.py +++ b/cozy/view_model/storages_view_model.py @@ -56,8 +56,8 @@ def add_storage_location(self, path: str) -> None: def add_first_storage_location(self, path: str) -> None: storage = self.storages[0] storage.path = path - storage.default = True storage.external = self._fs_monitor.is_external(path) + assert storage.default self._model.invalidate() self._notify("storage_locations") From 9217a546ae7732dd2d2be30d3018ddd7fefeac1c Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 13:07:23 +0100 Subject: [PATCH 22/28] Better function name --- cozy/model/settings.py | 4 ++-- test/cozy/model/test_settings.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cozy/model/settings.py b/cozy/model/settings.py index 41ea8e0d..c1018da3 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -83,8 +83,8 @@ def _load_all_storage_locations(self): "Invalid path found in database, skipping: {}".format(storage_db_obj.path) ) - self._ensure_default_storage_present() + self._ensure_default_storage_is_present() - def _ensure_default_storage_present(self): + def _ensure_default_storage_is_present(self): if self._storages and not any(storage.default for storage in self._storages): self._storages[0].default = True diff --git a/test/cozy/model/test_settings.py b/test/cozy/model/test_settings.py index 6bba3875..8cf0639b 100644 --- a/test/cozy/model/test_settings.py +++ b/test/cozy/model/test_settings.py @@ -86,7 +86,7 @@ def test_fetching_non_existent_last_played_book_sets_it_to_none(peewee_database) assert SettingsModel.get().last_played_book is None -def test_ensure_default_storage_present_adds_default_if_not_present(peewee_database): +def test_ensure_default_storage_is_present_adds_default_if_not_present(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage @@ -94,17 +94,17 @@ def test_ensure_default_storage_present_adds_default_if_not_present(peewee_datab settings = Settings() settings._load_all_storage_locations() - settings._ensure_default_storage_present() + settings._ensure_default_storage_is_present() assert Storage.get(1).default assert not Storage.get(2).default -def test_ensure_default_storage_present_does_nothing_if_default_is_present(peewee_database): +def test_ensure_default_storage_is_present_does_nothing_if_default_is_present(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage settings = Settings() settings._load_all_storage_locations() - settings._ensure_default_storage_present() + settings._ensure_default_storage_is_present() assert not Storage.get(1).default assert Storage.get(2).default From e8fb0a717ca45657d14eeabe2c468b93e54a6208 Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 13:12:30 +0100 Subject: [PATCH 23/28] Add type annotations, and an assert --- cozy/model/settings.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cozy/model/settings.py b/cozy/model/settings.py index c1018da3..a1fd3dac 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -1,5 +1,4 @@ import logging -from typing import List, Optional import peewee @@ -27,7 +26,7 @@ def first_start(self) -> bool: return self._db_object.first_start @property - def last_played_book(self) -> Optional[Book]: + def last_played_book(self) -> Book | None: try: return self._db_object.last_played_book except peewee.DoesNotExist: @@ -43,7 +42,7 @@ def last_played_book(self) -> Optional[Book]: return None @last_played_book.setter - def last_played_book(self, new_value): + def last_played_book(self, new_value) -> None: if new_value: self._db_object.last_played_book = new_value._db_object else: @@ -52,27 +51,28 @@ def last_played_book(self, new_value): self._db_object.save(only=self._db_object.dirty_fields) @property - def default_location(self): + def default_location(self) -> Storage: return next(location for location in self.storage_locations if location.default) @property - def storage_locations(self): + def storage_locations(self) -> list[Storage]: if not self._storages: self._load_all_storage_locations() + assert self._storages return self._storages @property - def external_storage_locations(self): + def external_storage_locations(self) -> list[Storage]: if not self._storages: self._load_all_storage_locations() return [storage for storage in self._storages if storage.external] - def invalidate(self): + def invalidate(self) -> None: self._storages.clear() - def _load_all_storage_locations(self): + def _load_all_storage_locations(self) -> None: self.invalidate() for storage_db_obj in StorageModel.select(StorageModel.id): @@ -85,6 +85,6 @@ def _load_all_storage_locations(self): self._ensure_default_storage_is_present() - def _ensure_default_storage_is_present(self): + def _ensure_default_storage_is_present(self) -> None: if self._storages and not any(storage.default for storage in self._storages): self._storages[0].default = True From 9f87b45101e9ede4a5125a022b2f01717e42bc89 Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 13:15:22 +0100 Subject: [PATCH 24/28] Turns out that *junk was not even necessary anymore --- cozy/ui/widgets/storages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cozy/ui/widgets/storages.py b/cozy/ui/widgets/storages.py index aba283e0..2cf21ab4 100644 --- a/cozy/ui/widgets/storages.py +++ b/cozy/ui/widgets/storages.py @@ -7,7 +7,7 @@ from cozy.view_model.storages_view_model import StoragesViewModel -def ask_storage_location(callback: Callable[[str], None], *junk, initial_folder: str | None = None): +def ask_storage_location(callback: Callable[[str], None], initial_folder: str | None = None): location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) if initial_folder: @@ -151,7 +151,7 @@ def _mark_storage_location_external( value = action.get_property(value.name) self._view_model.set_external(self._view_model.selected_storage, value) - def _on_new_storage_clicked(self, *junk) -> None: + def _on_new_storage_clicked(self, *_) -> None: ask_storage_location(self._view_model.add_storage_location) def _on_storage_location_changed(self, widget: StorageRow, new_location: str) -> None: From d31b0ee55fac869b61af0f57b52f0fe7d17d01ad Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 13:52:23 +0100 Subject: [PATCH 25/28] This wasn't a good idea --- cozy/model/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cozy/model/settings.py b/cozy/model/settings.py index a1fd3dac..8afa96dc 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -59,7 +59,6 @@ def storage_locations(self) -> list[Storage]: if not self._storages: self._load_all_storage_locations() - assert self._storages return self._storages @property From a342844a512b8ac5eb390c5d4805891e2586fc69 Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 15:15:15 +0100 Subject: [PATCH 26/28] Remove what's new popups, make the initial folder selector better --- cozy/app_controller.py | 3 - cozy/ui/app_view.py | 2 +- cozy/ui/library_view.py | 4 +- cozy/ui/main_view.py | 28 ++----- cozy/ui/widgets/welcome.py | 11 --- cozy/ui/widgets/whats_new_importer.py | 9 --- cozy/ui/widgets/whats_new_library.py | 11 --- cozy/ui/widgets/whats_new_m4b.py | 12 --- cozy/ui/widgets/whats_new_m4b_chapter.py | 12 --- cozy/ui/widgets/whats_new_window.py | 93 ------------------------ data/ui/first_import_button.ui | 26 +++++++ data/ui/gresource.xml | 7 +- data/ui/main_window.ui | 24 ++---- data/ui/welcome.ui | 13 ---- data/ui/whats_new.ui | 52 ------------- data/ui/whats_new_importer.ui | 75 ------------------- data/ui/whats_new_library.ui | 75 ------------------- data/ui/whats_new_m4b.ui | 75 ------------------- data/ui/whats_new_m4b_chapter.ui | 75 ------------------- 19 files changed, 41 insertions(+), 566 deletions(-) delete mode 100644 cozy/ui/widgets/welcome.py delete mode 100644 cozy/ui/widgets/whats_new_importer.py delete mode 100644 cozy/ui/widgets/whats_new_library.py delete mode 100644 cozy/ui/widgets/whats_new_m4b.py delete mode 100644 cozy/ui/widgets/whats_new_m4b_chapter.py delete mode 100644 cozy/ui/widgets/whats_new_window.py create mode 100644 data/ui/first_import_button.ui delete mode 100644 data/ui/welcome.ui delete mode 100644 data/ui/whats_new.ui delete mode 100644 data/ui/whats_new_importer.ui delete mode 100644 data/ui/whats_new_library.ui delete mode 100644 data/ui/whats_new_m4b.ui delete mode 100644 data/ui/whats_new_m4b_chapter.ui diff --git a/cozy/app_controller.py b/cozy/app_controller.py index b504eb2d..28ff2ab4 100644 --- a/cozy/app_controller.py +++ b/cozy/app_controller.py @@ -26,7 +26,6 @@ from cozy.ui.main_view import CozyUI from cozy.ui.media_controller import MediaController from cozy.ui.search_view import SearchView -from cozy.ui.widgets.whats_new_window import WhatsNewWindow from cozy.view import View from cozy.view_model.app_view_model import AppViewModel from cozy.view_model.book_detail_view_model import BookDetailViewModel @@ -50,8 +49,6 @@ def __init__(self, gtk_app, main_window_builder, main_window): reporter.info("main", "startup") - self.whats_new_window: WhatsNewWindow = WhatsNewWindow() - self.library_view: LibraryView = LibraryView(main_window_builder) self.app_view: AppView = AppView(main_window_builder) self.search_view: SearchView = SearchView() diff --git a/cozy/ui/app_view.py b/cozy/ui/app_view.py index cc16e548..09d5ca89 100644 --- a/cozy/ui/app_view.py +++ b/cozy/ui/app_view.py @@ -5,7 +5,7 @@ from cozy.view import View LIBRARY = "main" -EMPTY_STATE = "no_media" +EMPTY_STATE = "welcome" PREPARING_LIBRARY = "import" BOOK_DETAIL = "book_overview" diff --git a/cozy/ui/library_view.py b/cozy/ui/library_view.py index 3e530919..ee779ada 100644 --- a/cozy/ui/library_view.py +++ b/cozy/ui/library_view.py @@ -14,7 +14,7 @@ AUTHOR_PAGE = "author" RECENT_PAGE = "recent" MAIN_BOOK_PAGE = "main" -NO_MEDIA_PAGE = "no_media" +WELCOME_PAGE = "welcome" NO_RECENT_PAGE = "no_recent" BOOKS_PAGE = "books" @@ -113,7 +113,7 @@ def _on_library_view_mode_changed(self): books_view_page = BOOKS_PAGE if len(self._view_model.books) < 1: - main_view_page = NO_MEDIA_PAGE + main_view_page = WELCOME_PAGE visible_child_name = RECENT_PAGE elif view_mode == LibraryViewMode.CURRENT: visible_child_name = RECENT_PAGE diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index 2fdf606e..53b13223 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -21,6 +21,7 @@ from cozy.open_view import OpenView from cozy.ui.library_view import LibraryView from cozy.ui.preferences_view import PreferencesView +from cozy.ui.widgets.first_import_button import FirstImportButton log = logging.getLogger("ui") @@ -109,9 +110,6 @@ def __init_window(self): 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.about_dialog = self.about_builder.get_object("about_dialog") self.about_dialog.set_modal(self.window) self.about_dialog.connect("close-request", self.hide_window) @@ -164,6 +162,10 @@ def __init_actions(self): self.app.add_action(self.hide_offline_action) def __init_components(self): + path = self._settings.default_location.path if self._settings.storage_locations else None + import_button = FirstImportButton(self._set_audiobook_path, path) + self.get_object("welcome_status_page").set_child(import_button) + if not self._player.loaded_book: self.block_ui_buttons(True) @@ -243,7 +245,7 @@ def switch_to_playing(self): Switch the UI state back to playing. This enables all UI functionality for the user. """ - if self.navigation_view.props.visible_page != "book_overview" and self.main_stack.props.visible_child_name != "no_media": + if self.navigation_view.props.visible_page != "book_overview" and self.main_stack.props.visible_child_name != "welcome": self.navigation_view.pop_to_page("main") if self._player.loaded_book: self.block_ui_buttons(False, True) @@ -261,23 +263,6 @@ def check_for_tracks(self): if books().count() < 1: self.block_ui_buttons(True) - def _open_audiobook_dir_selector(self, __): - path = "" - if len(self._settings.storage_locations) > 0: - path = self._settings.default_location.path - - location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) - location_chooser.select_folder(self.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._set_audiobook_path(file.get_path()) - def scan(self, _, __): thread = Thread(target=self._importer.scan, name="ScanMediaThread") thread.start() @@ -310,7 +295,6 @@ def _on_drag_data_received(self, widget, value, *_): def _set_audiobook_path(self, path): self._storages_view_model.add_first_storage_location(path) - self.main_stack.props.visible_child_name = "import" self.scan(None, None) self.fs_monitor.init_offline_mode() diff --git a/cozy/ui/widgets/welcome.py b/cozy/ui/widgets/welcome.py deleted file mode 100644 index c2537653..00000000 --- a/cozy/ui/widgets/welcome.py +++ /dev/null @@ -1,11 +0,0 @@ -import gi - -from gi.repository import Adw, Gtk - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/welcome.ui') -class Welcome(Adw.Bin): - __gtype_name__ = "Welcome" - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/cozy/ui/widgets/whats_new_importer.py b/cozy/ui/widgets/whats_new_importer.py deleted file mode 100644 index 95113fd2..00000000 --- a/cozy/ui/widgets/whats_new_importer.py +++ /dev/null @@ -1,9 +0,0 @@ -from gi.repository import Gtk - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/whats_new_importer.ui') -class WhatsNewImporter(Gtk.Box): - __gtype_name__ = "WhatsNewImporter" - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/cozy/ui/widgets/whats_new_library.py b/cozy/ui/widgets/whats_new_library.py deleted file mode 100644 index abea0bdd..00000000 --- a/cozy/ui/widgets/whats_new_library.py +++ /dev/null @@ -1,11 +0,0 @@ -from gi.repository import Gtk - - -INTRODUCED = "0.9" - -@Gtk.Template.from_resource('/com/github/geigi/cozy/whats_new_library.ui') -class WhatsNewLibrary(Gtk.Box): - __gtype_name__ = "WhatsNewLibrary" - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/cozy/ui/widgets/whats_new_m4b.py b/cozy/ui/widgets/whats_new_m4b.py deleted file mode 100644 index 50f4e1ee..00000000 --- a/cozy/ui/widgets/whats_new_m4b.py +++ /dev/null @@ -1,12 +0,0 @@ -from gi.repository import Gtk - - -INTRODUCED = "0.7.2" - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/whats_new_m4b.ui') -class WhatsNewM4B(Gtk.Box): - __gtype_name__ = "WhatsNewM4B" - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/cozy/ui/widgets/whats_new_m4b_chapter.py b/cozy/ui/widgets/whats_new_m4b_chapter.py deleted file mode 100644 index d0a33acf..00000000 --- a/cozy/ui/widgets/whats_new_m4b_chapter.py +++ /dev/null @@ -1,12 +0,0 @@ -from gi.repository import Gtk - - -INTRODUCED = "1.0.0" - - -@Gtk.Template.from_resource('/com/github/geigi/cozy/whats_new_m4b_chapter.ui') -class WhatsNewM4BChapter(Gtk.Box): - __gtype_name__ = "WhatsNewM4BChapter" - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/cozy/ui/widgets/whats_new_window.py b/cozy/ui/widgets/whats_new_window.py deleted file mode 100644 index ceba1bb0..00000000 --- a/cozy/ui/widgets/whats_new_window.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import List -from packaging import version - -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 - -from gi.repository import Gtk, Adw - - -@Gtk.Template(resource_path='/com/github/geigi/cozy/whats_new.ui') -class WhatsNewWindow(Adw.Window): - __gtype_name__ = 'WhatsNew' - - content_stack: Gtk.Stack = Gtk.Template.Child() - continue_button: Gtk.Button = Gtk.Template.Child() - children: List[Gtk.Widget] - - main_window: CozyUI = inject.attr("MainWindow") - app_settings: ApplicationSettings = inject.attr(ApplicationSettings) - - page = 0 - - def __init__(self, **kwargs): - if self.app_settings.last_launched_version == CozyVersion: - return - - super().__init__(**kwargs) - - self.set_modal(self.main_window.window) - - self._fill_window() - if len(self.children) < 1: - self.end() - return - - self.set_default_size(800, 550) - - for widget in self.children: - self.content_stack.add_child(widget) - widget.set_visible(False) - - self.children[0].set_visible(True) - self.continue_button.connect("clicked", self.__on_continue_clicked) - self.show() - - def _fill_window(self): - self.children = [] - - try: - last_launched_version = version.parse(self.app_settings.last_launched_version) - except version.InvalidVersion: - self._fill_welcome() - else: - self._fill_whats_new(last_launched_version) - - def _fill_welcome(self): - from cozy.ui.widgets.welcome import Welcome - from cozy.ui.widgets.error_reporting import ErrorReporting - self.children.append(Welcome()) - self.children.append(ErrorReporting()) - - def _fill_whats_new(self, last_launched_version: version.Version): - from cozy.ui.widgets.whats_new_m4b_chapter import INTRODUCED - if last_launched_version < version.parse(INTRODUCED): - from cozy.ui.widgets.whats_new_m4b_chapter import WhatsNewM4BChapter - self.children.append(WhatsNewM4BChapter()) - - from cozy.ui.widgets.whats_new_library import INTRODUCED - if last_launched_version < version.parse(INTRODUCED): - from cozy.ui.widgets.whats_new_library import WhatsNewLibrary - self.children.append(WhatsNewLibrary()) - - from cozy.ui.widgets.whats_new_m4b import INTRODUCED - if last_launched_version < version.parse(INTRODUCED): - from cozy.ui.widgets.whats_new_m4b import WhatsNewM4B - self.children.append(WhatsNewM4B()) - - def __on_continue_clicked(self, widget): - if len(self.children) == self.page + 1: - self.end() - return - - self.children[self.page].set_visible(False) - self.page += 1 - self.content_stack.set_visible_child(self.children[self.page]) - self.children[self.page].set_visible(True) - - def end(self): - self.app_settings.last_launched_version = CozyVersion - self.close() - self.destroy() diff --git a/data/ui/first_import_button.ui b/data/ui/first_import_button.ui new file mode 100644 index 00000000..ff26a8b3 --- /dev/null +++ b/data/ui/first_import_button.ui @@ -0,0 +1,26 @@ + + + + + diff --git a/data/ui/gresource.xml b/data/ui/gresource.xml index fbb091fe..ad42a176 100644 --- a/data/ui/gresource.xml +++ b/data/ui/gresource.xml @@ -8,6 +8,7 @@ book_element.ui chapter_element.ui error_reporting.ui + first_import_button.ui headerbar.ui main_window.ui media_controller.ui @@ -19,11 +20,5 @@ storage_locations.ui storage_row.ui timer_popover.ui - welcome.ui - whats_new.ui - whats_new_importer.ui - whats_new_library.ui - whats_new_m4b.ui - whats_new_m4b_chapter.ui diff --git a/data/ui/main_window.ui b/data/ui/main_window.ui index b6ad6048..4a7c3f15 100644 --- a/data/ui/main_window.ui +++ b/data/ui/main_window.ui @@ -261,7 +261,7 @@ - no_media + welcome @@ -270,24 +270,10 @@ - - com.github.geigi.cozy - Let's get cozy - Select a folder, or drag audiobooks here to import them - - - center - - - Select folder - - - - - + + com.github.geigi.cozy + Let's get cozy + Select a folder, or drag audiobooks here to add them to your library diff --git a/data/ui/welcome.ui b/data/ui/welcome.ui deleted file mode 100644 index 532cde76..00000000 --- a/data/ui/welcome.ui +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/data/ui/whats_new.ui b/data/ui/whats_new.ui deleted file mode 100644 index dda8ca65..00000000 --- a/data/ui/whats_new.ui +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/data/ui/whats_new_importer.ui b/data/ui/whats_new_importer.ui deleted file mode 100644 index 5dfd8471..00000000 --- a/data/ui/whats_new_importer.ui +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/data/ui/whats_new_library.ui b/data/ui/whats_new_library.ui deleted file mode 100644 index 3d699a37..00000000 --- a/data/ui/whats_new_library.ui +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/data/ui/whats_new_m4b.ui b/data/ui/whats_new_m4b.ui deleted file mode 100644 index c97c53b4..00000000 --- a/data/ui/whats_new_m4b.ui +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/data/ui/whats_new_m4b_chapter.ui b/data/ui/whats_new_m4b_chapter.ui deleted file mode 100644 index 7d382344..00000000 --- a/data/ui/whats_new_m4b_chapter.ui +++ /dev/null @@ -1,75 +0,0 @@ - - - - - From 5734e662453ff54537ffd30d5a671e2c4b15e51b Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 15:16:07 +0100 Subject: [PATCH 27/28] Add missing file --- cozy/ui/widgets/first_import_button.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 cozy/ui/widgets/first_import_button.py diff --git a/cozy/ui/widgets/first_import_button.py b/cozy/ui/widgets/first_import_button.py new file mode 100644 index 00000000..bfeabffd --- /dev/null +++ b/cozy/ui/widgets/first_import_button.py @@ -0,0 +1,28 @@ +from gi.repository import Adw, Gtk, GObject + +from .storages import ask_storage_location + +from typing import Callable + + +@Gtk.Template.from_resource('/com/github/geigi/cozy/first_import_button.ui') +class FirstImportButton(Gtk.Button): + __gtype_name__ = "FirstImportButton" + + stack: Gtk.Stack = Gtk.Template.Child() + label: Adw.ButtonContent = Gtk.Template.Child() + spinner: Gtk.Spinner = Gtk.Template.Child() + + def __init__(self, callback: Callable[[str], None], initial_folder: str) -> None: + super().__init__() + + self._callback = callback + self._initial_folder = initial_folder + + self.connect("clicked", self._on_clicked) + + def _on_clicked(self, *_) -> None: + ask_storage_location(self._callback, self._initial_folder) + self.set_sensitive(False) + self.spinner.set_spinning(True) + self.stack.set_visible_child(self.spinner) From 254da193478279d641945787c4dba4ff24be24a8 Mon Sep 17 00:00:00 2001 From: rdbende Date: Fri, 22 Dec 2023 15:23:21 +0100 Subject: [PATCH 28/28] Remove dependency on Python packaging module --- README.md | 1 - com.github.geigi.cozy.json | 19 ------------------- requirements.txt | 1 - 3 files changed, 21 deletions(-) diff --git a/README.md b/README.md index f202df48..9da89caa 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for detailed instructions and developing Co - `distro` - `requests` - `pytz` -- `packaging` - `gi-cairo` - `gst-1.0` - `file` diff --git a/com.github.geigi.cozy.json b/com.github.geigi.cozy.json index f8521006..e7423997 100644 --- a/com.github.geigi.cozy.json +++ b/com.github.geigi.cozy.json @@ -49,25 +49,6 @@ } ] }, - { - "name": "python3-packaging", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"packaging\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/05/8e/8de486cbd03baba4deef4142bd643a3e7bbe954a784dc1bb17142572d127/packaging-21.3-py3-none-any.whl", - "sha256": "ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/6c/10/a7d0fa5baea8fe7b50f448ab742f26f52b80bfca85ac2be9d35cdd9a3246/pyparsing-3.0.9-py3-none-any.whl", - "sha256": "5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - } - ] - }, { "name": "python3-peewee", "buildsystem": "simple", diff --git a/requirements.txt b/requirements.txt index 08b6b5e2..23408ea8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ distro mutagen -packaging peewee>=3.9.6 pytz requests