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 c9202233..b3487578 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:
@@ -47,11 +51,8 @@ def last_played_book(self, new_value):
self._db_object.save(only=self._db_object.dirty_fields)
@property
- def default_location(self) -> bool:
- for location in self.storage_locations:
- if location.default:
- return True
- return False
+ def default_location(self) -> Storage:
+ return next(location for location in self.storage_locations if location.default)
@property
def storage_locations(self) -> list[Storage]:
@@ -67,11 +68,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 +80,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/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/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/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/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/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 @@
@@ -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/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 @@
+
+
+
+
+ Storage locations
+
+
+ 18
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ false
+ true
+
+
+
+
+
+ false
+ 6
+ 6
+ emblem-default-symbolic
+
+
+
+
+
+
+
+
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