diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 40af8faa..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Unit Testing - -on: [push, pull_request] - -jobs: - test: - - runs-on: ubuntu-latest - container: - image: ghcr.io/geigi/cozy-ci:main - - steps: - - uses: actions/checkout@v4 - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --builtins="_" - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --builtins="_" - - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..d9d9b846 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,16 @@ +name: Checks + +on: + push: + branches: + - "main" + pull_request: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + with: + args: --exit-zero \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..3bd89da0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests + +on: + push: + branches: + - "main" + - "master" + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + container: + image: ghcr.io/geigi/cozy-ci:main + + steps: + - uses: actions/checkout@v4 + + - name: Run pytest + run: pytest diff --git a/cozy/app_controller.py b/cozy/app_controller.py index 97f58a82..2062a27e 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): @@ -75,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) @@ -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 @@ -149,10 +151,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/model/settings.py b/cozy/model/settings.py index 1739530e..61d50193 100644 --- a/cozy/model/settings.py +++ b/cozy/model/settings.py @@ -30,15 +30,19 @@ def last_played_book(self) -> Book | None: 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 @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: @@ -67,11 +71,11 @@ def external_storage_locations(self) -> list[Storage]: return [storage for storage in self._storages if storage.external] - def invalidate(self): - self._storages = [] + def invalidate(self) -> None: + self._storages.clear() - def _load_all_storage_locations(self): - self._storages = [] + def _load_all_storage_locations(self) -> None: + self.invalidate() for storage_db_obj in StorageModel.select(StorageModel.id): try: @@ -79,9 +83,9 @@ def _load_all_storage_locations(self): except InvalidPath: log.error("Invalid path found in database, skipping: %s", 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): default_storage_present = any(storage.default for storage in self._storages) if not default_storage_present and self._storages: 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/headerbar.py b/cozy/ui/headerbar.py index 6dfb3d8f..48c8efa2 100644 --- a/cozy/ui/headerbar.py +++ b/cozy/ui/headerbar.py @@ -1,6 +1,12 @@ import logging -from gi.repository import Adw, Gtk +import gi + +from cozy.ext import inject +from cozy.ui.widgets.progress_popover import ProgressPopover +from cozy.view_model.headerbar_view_model import HeaderbarViewModel, HeaderBarState + +from gi.repository import Adw, Gtk, GObject from cozy.ext import inject from cozy.ui.widgets.progress_popover import ProgressPopover @@ -52,6 +58,8 @@ def __init__(self, main_window_builder: Gtk.Builder): self._connect_view_model() self._connect_widgets() + self._set_show_sidebar_button_visible() + def _connect_view_model(self): self._headerbar_view_model.bind_to("state", self._on_state_changed) self._headerbar_view_model.bind_to("work_progress", self._on_work_progress_changed) @@ -62,20 +70,41 @@ def _connect_widgets(self): self.split_view.connect("notify::show-sidebar", self._on_sidebar_toggle) self.show_sidebar_button.connect("notify::active", self._on_sidebar_toggle) self.mobile_view_switcher.connect("notify::reveal", self._on_mobile_view) - self.sort_stack.connect("notify::visible-child", self._on_sort_stack_changed) + self.sort_stack.connect("notify::visible-child", self._set_show_sidebar_button_visible) + + self.mobile_view_switcher.bind_property( + "reveal", self.split_view, "collapsed", GObject.BindingFlags.SYNC_CREATE + ) - def _on_sort_stack_changed(self, widget, _): - page = widget.props.visible_child_name + def _on_mobile(self) -> bool: + return self.mobile_view_switcher.props.reveal + + def _set_show_sidebar_button_visible(self, *_): + page = self.sort_stack.props.visible_child_name + + if not self._on_mobile(): + self.show_sidebar_button.set_visible(False) + self.split_view.set_collapsed(False) + self.split_view.set_show_sidebar(page != "recent") + return + + if self.mobile_view_switcher.props.reveal: + self.show_sidebar_button.set_visible(page != "recent") + else: + self.show_sidebar_button.set_visible(False) - self.show_sidebar_button.set_visible(page != "recent") self.search_button.set_active(False) def _on_mobile_view(self, widget, _): + page = self.sort_stack.props.visible_child_name + if widget.props.reveal: self.headerbar.set_title_widget(Adw.WindowTitle(title="Cozy")) else: self.headerbar.set_title_widget(self.view_switcher) + self._set_show_sidebar_button_visible() + def _on_sidebar_toggle(self, widget, param): show_sidebar = widget.get_property(param.name) diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index e66d93ea..63460131 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -17,6 +17,8 @@ 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.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 from cozy.view_model.settings_view_model import SettingsViewModel @@ -37,7 +39,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__() @@ -243,9 +245,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): """ @@ -303,7 +305,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/media_controller.py b/cozy/ui/media_controller.py index b5c9a506..fe3e3e05 100644 --- a/cozy/ui/media_controller.py +++ b/cozy/ui/media_controller.py @@ -40,8 +40,8 @@ class MediaController(Adw.BreakpointBin): def __init__(self, main_window_builder: Gtk.Builder): super().__init__() - media_control_box: Gtk.Box = main_window_builder.get_object("media_control_box") - media_control_box.append(self) + self.container_bar: Gtk.Revealer = main_window_builder.get_object("media_control_box") + self.container_bar.set_child(self) self.seek_bar = SeekBar() self.seek_bar_container.append(self.seek_bar) @@ -127,13 +127,7 @@ def _on_length_changed(self): def _on_lock_ui_changed(self): sensitive = not self._playback_control_view_model.lock_ui - self.seek_bar.sensitive = sensitive - self.prev_button.set_sensitive(sensitive) - self.next_button.set_sensitive(sensitive) - self.play_button.set_sensitive(sensitive) - self.volume_button.set_sensitive(sensitive) - self.playback_speed_button.set_sensitive(sensitive) - self.timer_button.set_sensitive(sensitive) + self.container_bar.set_reveal_child(sensitive) def _on_volume_changed(self): self.volume_button.set_value(self._playback_control_view_model.volume) diff --git a/cozy/ui/preferences_view.py b/cozy/ui/preferences_view.py index 3fa2ca0b..decca8ff 100644 --- a/cozy/ui/preferences_view.py +++ b/cozy/ui/preferences_view.py @@ -1,158 +1,95 @@ +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.storage_list_box_row import StorageListBoxRow + +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" - main_window = inject.attr("MainWindow") - _glib_settings: Gio.Settings = inject.attr(Gio.Settings) _view_model: SettingsViewModel = inject.attr(SettingsViewModel) - dark_mode_switch: Gtk.Switch = Gtk.Template.Child() - swap_author_reader_switch: Gtk.Switch = Gtk.Template.Child() - replay_switch: Gtk.Switch = Gtk.Template.Child() + storages_page: Adw.PreferencesPage = Gtk.Template.Child() + user_feedback_preference_group: Adw.PreferencesGroup = 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() - 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) + def __init__(self, **kwargs: Any) -> None: + main_window = inject.instance("MainWindow") + super().__init__(transient_for=main_window.window, **kwargs) error_reporting = ErrorReporting() error_reporting.show_header(False) self.user_feedback_preference_group.add(error_reporting) + self.storage_locations_view = StorageLocations() + self.storages_page.add(self.storage_locations_view) + + self._view_model.bind_to("lock_ui", self._on_lock_ui_changed) self._bind_settings() - self._bind_view_model() - - 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) - - 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, "active", - 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_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.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()) + 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, + ) + + def _on_lock_ui_changed(self) -> None: + self.storage_locations_view.set_sensitive(not self._view_model.lock_ui) diff --git a/cozy/ui/widgets/seek_bar.py b/cozy/ui/widgets/seek_bar.py index 31c9e5c6..f901d510 100644 --- a/cozy/ui/widgets/seek_bar.py +++ b/cozy/ui/widgets/seek_bar.py @@ -19,15 +19,24 @@ def __init__(self, **kwargs): self.progress_scale.connect("value-changed", self._on_progress_scale_changed) - self._progress_scale_gesture = Gtk.GestureClick() - self._progress_scale_gesture.connect("pressed", self._on_progress_scale_press) - self._progress_scale_gesture.connect("end", self._on_progress_scale_release) - self.progress_scale.add_controller(self._progress_scale_gesture) - - self._progress_scale_key = Gtk.EventControllerKey() - self._progress_scale_key.connect("key-pressed", self._on_progress_scale_press) - self._progress_scale_key.connect("key-released", self._on_progress_scale_release) - self.progress_scale.add_controller(self._progress_scale_key) + # HACK: Using a GtkGestureClick here is not possible, as GtkRange's internal + # gesture controller claims the button press event, and thus the released signal doesn't get emitted. + # Therefore we get its internal GtkGestureClick, and add our handlers to that. + # Hacky workaround from: https://gitlab.gnome.org/GNOME/gtk/-/issues/4939 + # Ideally GtkRange would forward these signals, so we wouldn't need this hack + # TODO: Add these signals to Gtk and make a MR? + for controller in self.progress_scale.observe_controllers(): + if isinstance(controller, Gtk.GestureClick): + click_gesture = controller + break + + click_gesture.set_button(0) # Enable all mouse buttons + click_gesture.connect("pressed", self._on_progress_scale_press) + click_gesture.connect("released", self._on_progress_scale_release) + + keyboard_controller = Gtk.EventControllerKey() + keyboard_controller.connect("key-pressed", self._on_progress_key_pressed) + self.progress_scale.add_controller(keyboard_controller) @property def position(self) -> float: @@ -91,7 +100,6 @@ def _on_progress_key_pressed(self, _, event): def _on_progress_scale_press(self, *_): self._progress_scale_pressed = True - return False GObject.signal_new('position-changed', SeekBar, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, 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..2cf21ab4 --- /dev/null +++ b/cozy/ui/widgets/storages.py @@ -0,0 +1,168 @@ +from typing import Callable + +from gi.repository import Adw, Gio, GLib, GObject, Gtk + +from cozy.ext import inject +from cozy.model.storage import Storage +from cozy.view_model.storages_view_model import StoragesViewModel + + +def ask_storage_location(callback: Callable[[str], None], initial_folder: str | None = None): + location_chooser = Gtk.FileDialog(title=_("Set Audiobooks Directory")) + + if initial_folder: + gfile = Gio.File.new_for_path(initial_folder) + location_chooser.set_initial_folder(gfile) + + 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, *_) -> None: + 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) + + def _on_menu_opened(self, *_) -> None: + self.emit("menu-opened") + + 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")) + else: + self.icon.set_from_icon_name("folder-open-symbolic") + self.icon.set_tooltip_text(_("Internal drive")) + + 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, ()) + + +@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() + storage_menu: Gio.Menu = Gtk.Template.Child() + + def __init__(self) -> None: + super().__init__() + + 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() + + def _create_actions(self) -> None: + 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 _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() + + 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) + + self.storage_locations_list.append(self.new_storage_button) + + def _remove_storage_location(self, *_) -> None: + self._view_model.remove(self._view_model.selected_storage) + + def _set_default_storage_location(self, *_) -> None: + self._view_model.set_default(self._view_model.selected_storage) + + 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, *_) -> None: + ask_storage_location(self._view_model.add_storage_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) -> 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.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/headerbar_view_model.py b/cozy/view_model/headerbar_view_model.py index f2ec365c..d160220a 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): diff --git a/cozy/view_model/library_view_model.py b/cozy/view_model/library_view_model.py index 180ac467..2639ca8e 100644 --- a/cozy/view_model/library_view_model.py +++ b/cozy/view_model/library_view_model.py @@ -18,7 +18,7 @@ from cozy.report import reporter from cozy.ui.widgets.book_element import BookElement from cozy.ui.import_failed_dialog import ImportFailedDialog -from cozy.view_model.settings_view_model import SettingsViewModel +from cozy.view_model.storages_view_model import StoragesViewModel log = logging.getLogger("library_view_model") @@ -35,7 +35,7 @@ class LibraryViewModel(Observable, EventSender): _model = inject.attr(Library) _importer: Importer = inject.attr(Importer) _player: Player = inject.attr(Player) - _settings: SettingsViewModel = inject.attr(SettingsViewModel) + _storages: StoragesViewModel = inject.attr(StoragesViewModel) def __init__(self): super().__init__() @@ -52,7 +52,7 @@ def _connect(self): self._importer.add_listener(self._on_importer_event) self._player.add_listener(self._on_player_event) self._model.add_listener(self._on_model_event) - self._settings.add_listener(self._on_settings_event) + self._storages.add_listener(self._on_storages_event) @property def books(self): @@ -91,8 +91,7 @@ def authors(self): authors = { book.author - for book - in self._model.books + for book in self._model.books if is_book_online(book) or show_offline_books or book.downloaded } @@ -105,8 +104,7 @@ def readers(self): readers = { book.reader - for book - in self._model.books + for book in self._model.books if is_book_online(book) or show_offline_books or book.downloaded } @@ -209,24 +207,17 @@ def _on_player_event(self, event, message): elif event in {"position", "book-finished"}: self._notify("book-progress") - def _on_settings_event(self, event: str, message): + def _on_storages_event(self, event: str, message): if event == "storage-removed": - self._on_external_storage_removed(message) - - def _on_external_storage_removed(self, storage: Storage): - books = self.books.copy() - for book in books: - chapters_to_remove = [c for c in book.chapters if c.file.startswith(str(storage.path))] - - for chapter in chapters_to_remove: - chapter.delete() - - self._notify("authors") - self._notify("readers") - self._notify("books") - self._notify("books-filter") - self._notify("current_book_in_playback") - self._notify("playing") + for property in ( + "authors", + "readers", + "books", + "books-filter", + "current_book_in_playback", + "playing", + ): + self._notify(property) def _on_model_event(self, event: str, message): if event == "rebase-finished": diff --git a/cozy/view_model/settings_view_model.py b/cozy/view_model/settings_view_model.py index 716aa524..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 %s is not possible", 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..9316c012 --- /dev/null +++ b/cozy/view_model/storages_view_model.py @@ -0,0 +1,129 @@ +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.ext import inject +from cozy.media.importer import Importer +from cozy.model.library import Library +from cozy.model.settings import Settings +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) + _model: Settings = inject.attr(Settings) + _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) + _db = inject.attr(SqliteDatabase) + _fs_monitor = inject.attr(FilesystemMonitor) + + def __init__(self) -> None: + super().__init__() + super(Observable, self).__init__() + + self._selected_storage = None + + 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) -> None: + self.emit_event("storage-changed", model) + log.info("Audiobook location changed, rebasing the location in Cozy.") + 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._notify("storage_locations") + + self._scan_new_storage(model) + + def add_first_storage_location(self, path: str) -> None: + storage = self.storages[0] + storage.path = path + storage.external = self._fs_monitor.is_external(path) + assert storage.default + + 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() + + storage_path = str(model.path) + for book in self._library.books: + chapters_to_remove = [ + c for c in book.chapters if c.file.startswith(storage_path) + ] + + for chapter in chapters_to_remove: + chapter.delete() + + self.emit_event("storage-removed", model) + self._notify("storage_locations") + + def set_default(self, model: Storage) -> None: + 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) -> None: + 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/com.github.geigi.cozy.appdata.xml b/data/com.github.geigi.cozy.appdata.xml index 6e8f6e18..2aaf4dbe 100644 --- a/data/com.github.geigi.cozy.appdata.xml +++ b/data/com.github.geigi.cozy.appdata.xml @@ -1,18 +1,22 @@ com.github.geigi.cozy + Cozy CC0 GPL-3.0+ - Cozy + com.github.geigi.cozy.desktop + com.github.geigi.cozy + + Julian Geywitz + + Julian Geywitz + + cozy@geigi.de Listen to audio books -

- Do you like audio books? Then lets get cozy! -

-

- Cozy is a audio book player. Here are some of the features: -

-
- - com.github.geigi.cozy - - com.github.geigi.cozy.desktop - com.github.geigi.cozy + + https://cozy.sh + https://github.com/geigi/cozy/issues + https://matrix.to/#/#cozy:gnome.org + https://www.patreon.com/geigi + https://github.com/geigi/cozy/ + https://www.transifex.com/geigi/cozy/ https://raw.githubusercontent.com/geigi/cozy/img/img/screenshot1.png @@ -44,27 +50,26 @@ https://raw.githubusercontent.com/geigi/cozy/img/img/screenshot4.png - - Julian Geywitz - - Julian Geywitz - - https://cozy.sh - https://github.com/geigi/cozy/issues - https://matrix.to/#/#cozy:gnome.org?via=matrix.org&via=gnome.org - https://www.patreon.com/geigi - https://github.com/geigi/cozy/ - https://www.transifex.com/geigi/cozy/ - cozy@geigi.de - + +

+ After almost two years, a new version of Cozy is finally here! This release brings an updated user interface along with numerous bug fixes and improved performance. +

+

+ The user interface has been ported to GTK4 and Libadwaita. Thus, Cozy benefits from the new style sheet, automatic dark mode, and utilizes the latest and greatest UI elements throughout the application. Many thanks to Benedek Dévényi and grahamvh! +

+

Other changes include:

    -
  • Update to GTK 4 and libadwaita (thank you rdbende and grahamvh!)
  • +
  • Improved mobile support
  • +
  • Smaller visual refinements to match the state of the art of GNOME apps
  • +
  • Dozens of bug fixes and performance improvements
  • +
  • Significant cleanup and improvements to the codebase
  • +
  • As always, updated translations thanks to all translators!
- +
  • Support for GTK style manager (thanks A6GibKm)
  • @@ -73,7 +78,7 @@
- +

This release features a redesigned preference window. All settings can now be searched. Good news for mobile users too: the redesign should work a lot better on mobile devices now. @@ -88,7 +93,7 @@ - +

A small bugfix release which makes Cozy more reliable. @@ -101,7 +106,7 @@ - +

A small bugfix release which makes Cozy more reliable. @@ -117,7 +122,7 @@ - +

A small bugfix release which makes Cozy more reliable. @@ -133,7 +138,7 @@ - +

This release features a redesigned library with responsiveness in mind. @@ -150,7 +155,7 @@ - +

Performance improvements for the book detail view and some bugfixes. @@ -165,7 +170,7 @@ - +

A small bugfix release which makes Cozy more reliable. @@ -180,7 +185,7 @@ - +

A small bugfix release which makes Cozy more reliable. @@ -195,7 +200,7 @@ - +

This release features chapter support for m4b files. @@ -209,7 +214,7 @@ - +

This release features chapter support for m4b files. @@ -224,11 +229,22 @@ - - + + com.github.geigi.cozy + + + pointing + keyboard + touch + + + 360 + + + #ffa348 + #ffa348 + - #FB6542 - #FFFFFF 2 pk_live_XJocNlICBfLqHpdZXN0LxlyV00xrhZTbDe diff --git a/data/ui/book_element.ui b/data/ui/book_element.ui index 402d96c7..ad157685 100644 --- a/data/ui/book_element.ui +++ b/data/ui/book_element.ui @@ -8,10 +8,6 @@ true Open book overview - 6 - 6 - 12 - 12 vertical 1 start diff --git a/data/ui/gresource.xml b/data/ui/gresource.xml index 5049a867..8d9ea192 100644 --- a/data/ui/gresource.xml +++ b/data/ui/gresource.xml @@ -15,6 +15,8 @@ progress_popover.ui search_page.ui seek_bar.ui + storage_locations.ui + storage_row.ui timer_popover.ui welcome.ui whats_new.ui diff --git a/data/ui/main_window.ui b/data/ui/main_window.ui index 35f0024c..ea2695b6 100644 --- a/data/ui/main_window.ui +++ b/data/ui/main_window.ui @@ -156,15 +156,15 @@ minimum - 6 - 6 - 6 - 6 + 18 + 18 + 18 + 18 true - true + start true - 6 - 3 + 18 + 18 1 10 none @@ -224,11 +224,7 @@ vertical - - - + diff --git a/data/ui/media_controller.ui b/data/ui/media_controller.ui index fac4dd60..46318de5 100644 --- a/data/ui/media_controller.ui +++ b/data/ui/media_controller.ui @@ -106,7 +106,6 @@ 6 - false true true Rewind @@ -126,7 +125,6 @@ 42 42 - false true true Start playback @@ -145,7 +143,6 @@ - false true true Forward @@ -200,7 +197,6 @@ audio-volume-medium-symbolic - false true true Playback speed @@ -212,7 +208,6 @@ audio-volume-medium-symbolic - false true true Sleep timer diff --git a/data/ui/preferences.ui b/data/ui/preferences.ui index 3df26ebf..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 + + @@ -97,7 +99,7 @@ - + harddisk-symbolic Storage @@ -111,134 +113,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 - - - check-plain-symbolic - - - - - - - - - - - - - - diff --git a/data/ui/seek_bar.ui b/data/ui/seek_bar.ui index 6873bf27..2254819d 100644 --- a/data/ui/seek_bar.ui +++ b/data/ui/seek_bar.ui @@ -26,7 +26,6 @@ 150 - false true Jump to position in current chapter center diff --git a/data/ui/storage_locations.ui b/data/ui/storage_locations.ui new file mode 100644 index 00000000..afb6dce7 --- /dev/null +++ b/data/ui/storage_locations.ui @@ -0,0 +1,34 @@ + + + + + +
+ + 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 @@ + + + + + diff --git a/pyproject.toml b/pyproject.toml index cd6386ac..10591275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,4 +5,10 @@ skip-magic-trailing-comma = true [tool.isort] line_length = 100 profile = "black" -multi_line_output = 3 \ No newline at end of file +multi_line_output = 3 + +[tool.ruff] +line-length = 100 +builtins = ["_"] +output-format = "github" +extend-select = ["B", "SIM", "PIE", "C4", "INT", "LOG"] 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