diff --git a/cozy/media/player.py b/cozy/media/player.py index 5bec8fb4..3b3bfac4 100644 --- a/cozy/media/player.py +++ b/cozy/media/player.py @@ -455,6 +455,12 @@ def forward(self): if state == Gst.State.PLAYING: self._gst_player.play() + def volume_up(self): + self.volume = min(1.0, self.volume + 0.1) + + def volume_down(self): + self.volume = max(0, self.volume - 0.1) + def destroy(self): self._gst_player.stop() @@ -583,6 +589,30 @@ def _next_chapter(self): chapter.position = chapter.start_position self.play_pause_chapter(self._book, chapter) + def _previous_chapter(self): + if not self._book: + log.error("Cannot play previous chapter because no book reference is stored.") + reporter.error( + "player", "Cannot play previous chapter because no book reference is stored." + ) + return + + index_current_chapter = self._book.chapters.index(self._book.current_chapter) + self._book.current_chapter.position = self._book.current_chapter.start_position + + if index_current_chapter - 1 < 0: + log.info("Book reached start, cannot rewind further.") + chapter = self._book.chapters[0] + chapter.position = chapter.start_position + + self._load_chapter(chapter) + self.pause() + self._emit_tick() + else: + chapter = self._book.chapters[index_current_chapter - 1] + chapter.position = chapter.start_position + self.play_pause_chapter(self._book, chapter) + def _on_importer_event(self, event: str, message): if event == "scan" and message == ScanStatus.SUCCESS: log.info("Reloading current book") diff --git a/cozy/ui/about_window.py b/cozy/ui/about_window.py index 09da27d9..bc6be071 100644 --- a/cozy/ui/about_window.py +++ b/cozy/ui/about_window.py @@ -22,6 +22,8 @@ def __init__(self, version: str) -> None: self.set_extra_credits() + self.connect = self._window.connect + def get_contributors(self) -> list[str]: authors_file = Gio.resources_lookup_data( "/com/github/geigi/cozy/appdata/authors.list", Gio.ResourceLookupFlags.NONE diff --git a/cozy/ui/main_view.py b/cozy/ui/main_view.py index c926b688..eed7196c 100644 --- a/cozy/ui/main_view.py +++ b/cozy/ui/main_view.py @@ -20,6 +20,8 @@ from cozy.ui.library_view import LibraryView from cozy.ui.preferences_window import PreferencesWindow from cozy.ui.widgets.first_import_button import FirstImportButton +from cozy.view_model.playback_control_view_model import PlaybackControlViewModel +from cozy.view_model.playback_speed_view_model import PlaybackSpeedViewModel from cozy.view_model.storages_view_model import StoragesViewModel log = logging.getLogger("ui") @@ -36,6 +38,8 @@ class CozyUI(EventSender, metaclass=Singleton): _files: Files = inject.attr(Files) _player: Player = inject.attr(Player) _storages_view_model: StoragesViewModel = inject.attr(StoragesViewModel) + _playback_control_view_model: PlaybackControlViewModel = inject.attr(PlaybackControlViewModel) + _playback_speed_view_model: PlaybackSpeedViewModel = inject.attr(PlaybackSpeedViewModel) _library_view: LibraryView @@ -44,6 +48,8 @@ def __init__(self, app, version): self.app = app self.version = version + self._actions_to_disable = [] + def activate(self, library_view: LibraryView): self.__init_window() self.__init_actions() @@ -55,7 +61,9 @@ def activate(self, library_view: LibraryView): self.check_for_tracks() def startup(self): - self.window_builder = Gtk.Builder.new_from_resource("/com/github/geigi/cozy/ui/main_window.ui") + self.window_builder = Gtk.Builder.new_from_resource( + "/com/github/geigi/cozy/ui/main_window.ui" + ) self.window: Adw.ApplicationWindow = self.window_builder.get_object("app_window") def __init_window(self): @@ -90,15 +98,17 @@ def __init_actions(self): """ Init all app actions. """ - self.create_action("about", self.show_about_window, ["F1"]) + self.create_action("about", self.show_about_window, ["F1"], global_shorcut=True) self.create_action("reset_book", self.reset_book) self.create_action("remove_book", self.remove_book) + self.create_action("mark_book_as_read", self.mark_book_as_read) self.create_action("jump_to_book_folder", self.jump_to_book_folder) - self.create_action("prefs", self.show_preferences_window, ["comma"]) - self.create_action("quit", self.quit, ["q", "w"]) + + self.create_action("prefs", self.show_preferences_window, ["comma"], global_shorcut=True) + self.create_action("quit", self.quit, ["q", "w"], global_shorcut=True) + self.scan_action = self.create_action("scan", self.scan) - self.play_pause_action = self.create_action("play_pause", self.play_pause, ["space"]) self.hide_offline_action = Gio.SimpleAction.new_stateful( "hide_offline", None, GLib.Variant.new_boolean(self.application_settings.hide_offline) @@ -106,6 +116,10 @@ def __init_actions(self): self.hide_offline_action.connect("change-state", self.__on_hide_offline) self.app.add_action(self.hide_offline_action) + def set_hotkeys_enabled(self, enabled: bool) -> None: + for action in self._actions_to_disable: + action.set_enabled(enabled) + def __init_components(self): path = self._settings.default_location.path if self._settings.storage_locations else None self.import_button = FirstImportButton(self._set_audiobook_path, path) @@ -121,6 +135,8 @@ def create_action( name: str, callback: Callable[[Gio.SimpleAction, None], None], shortcuts: list[str] | None = None, + *, + global_shorcut: bool = False, ) -> Gio.SimpleAction: action = Gio.SimpleAction.new(name, None) action.connect("activate", callback) @@ -129,6 +145,9 @@ def create_action( if shortcuts: self.app.set_accels_for_action(f"app.{name}", shortcuts) + if not global_shorcut: + self._actions_to_disable.append(action) + return action def refresh_library_filters(self): @@ -161,14 +180,21 @@ def quit(self, action, parameter): self.on_close(None) self.app.quit() + def _dialog_close_callback(self, dialog): + dialog.disconnect_by_func(self._dialog_close_callback) + self.set_hotkeys_enabled(True) + def show_about_window(self, *_): - AboutWindow(self.version).present(self.window) + self.set_hotkeys_enabled(False) + about = AboutWindow(self.version) + about.connect("closed", self._dialog_close_callback) + about.present(self.window) def show_preferences_window(self, *_): - PreferencesWindow().present(self.window) - - def play_pause(self, *_): - self._player.play_pause() + self.set_hotkeys_enabled(False) + prefs = PreferencesWindow() + prefs.connect("closed", self._dialog_close_callback) + prefs.present(self.window) def block_ui_buttons(self, block, scan=False): """ @@ -177,7 +203,6 @@ def block_ui_buttons(self, block, scan=False): """ sensitive = not block try: - self.play_pause_action.set_enabled(sensitive) if scan: self.scan_action.set_enabled(sensitive) self.hide_offline_action.set_enabled(sensitive) @@ -189,7 +214,10 @@ 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 != "welcome": + if ( + self.navigation_view.props.visible_page != "book_overview" + and self.main_stack.props.visible_child_name != "welcome" + ): self.navigation_view.pop_to_tag("main") if self._player.loaded_book: @@ -289,4 +317,3 @@ def _save_window_size(self, *_): self.application_settings.window_width = width self.application_settings.window_height = height self.application_settings.window_maximize = self.window.is_maximized() - diff --git a/cozy/ui/media_controller.py b/cozy/ui/media_controller.py index b609d60d..c4b93170 100644 --- a/cozy/ui/media_controller.py +++ b/cozy/ui/media_controller.py @@ -9,6 +9,7 @@ from cozy.ui.widgets.seek_bar import SeekBar from cozy.ui.widgets.sleep_timer import SleepTimer from cozy.view_model.playback_control_view_model import PlaybackControlViewModel +from cozy.view_model.playback_speed_view_model import PlaybackSpeedViewModel log = logging.getLogger("MediaController") @@ -57,9 +58,8 @@ def __init__(self, main_window_builder: Gtk.Builder): ] ) - self._playback_control_view_model: PlaybackControlViewModel = inject.instance( - PlaybackControlViewModel - ) + self._playback_control_view_model = inject.instance(PlaybackControlViewModel) + self._playback_speed_view_model = inject.instance(PlaybackSpeedViewModel) self._artwork_cache: ArtworkCache = inject.instance(ArtworkCache) self._connect_view_model() self._connect_widgets() @@ -69,6 +69,7 @@ def __init__(self, main_window_builder: Gtk.Builder): self._on_length_changed() self._on_position_changed() self._on_volume_changed() + self._setup_shortcuts() def _connect_view_model(self): self._playback_control_view_model.bind_to("book", self._on_book_changed) @@ -103,6 +104,22 @@ def _set_cover_image(self, book: Book): self.cover_img.set_from_icon_name("book-open-variant-symbolic") self.cover_img.props.pixel_size = COVER_SIZE + @inject.param("main_window", "MainWindow") + def _setup_shortcuts(self, main_window): + main_window.create_action("play_pause", self._play_clicked, ["space"]) + main_window.create_action("seek_rewind", self._rewind_clicked, ["Left"]) + main_window.create_action("seek_forward", self._forward_clicked, ["Right"]) + + main_window.create_action("volume_up", self._volume_up, ["Up"]) + main_window.create_action("volume_down", self._volume_down, ["Down"]) + + main_window.create_action("speed_up", self._speed_up, ["plus", "KP_Add", "Up"]) + main_window.create_action("speed_down", self._speed_down, ["minus", "KP_Subtract", "Down"]) + main_window.create_action("speed_reset", self._speed_reset, ["equal"]) + + main_window.create_action("prev_chapter", self._prev_chapter, ["Page_Down", "Left"]) + main_window.create_action("next_chapter", self._next_chapter, ["Page_Up", "Right"]) + def _on_book_changed(self) -> None: book = self._playback_control_view_model.book self._set_book(book) @@ -145,6 +162,27 @@ def _on_lock_ui_changed(self): def _on_volume_changed(self): self.volume_button.set_value(self._playback_control_view_model.volume) + def _volume_up(self, *_): + self._playback_control_view_model.volume_up() + + def _volume_down(self, *_): + self._playback_control_view_model.volume_down() + + def _speed_up(self, *_): + self._playback_speed_view_model.speed_up() + + def _speed_down(self, *_): + self._playback_speed_view_model.speed_down() + + def _speed_reset(self, *_): + self._playback_speed_view_model.speed_reset() + + def _next_chapter(self, *_): + self._playback_control_view_model.next_chapter() + + def _prev_chapter(self, *_): + self._playback_control_view_model.previous_chapter() + def _play_clicked(self, *_): self._playback_control_view_model.play_pause() diff --git a/cozy/ui/search_view.py b/cozy/ui/search_view.py index eba2d72d..d47731b8 100644 --- a/cozy/ui/search_view.py +++ b/cozy/ui/search_view.py @@ -54,12 +54,12 @@ def __init__(self, main_window_builder: Gtk.Builder, headerbar: Headerbar) -> No def open(self, *_) -> None: self.library_stack.set_visible_child(self) self.search_bar.set_search_mode(True) - self.main_view.play_pause_action.set_enabled(False) + self.main_view.set_hotkeys_enabled(False) def close(self) -> None: self.library_stack.set_visible_child(self.split_view) self.search_bar.set_search_mode(False) - self.main_view.play_pause_action.set_enabled(True) + self.main_view.set_hotkeys_enabled(True) def on_state_changed(self, widget: Gtk.Widget, param) -> None: if widget.get_property(param.name): diff --git a/cozy/ui/widgets/book_card.py b/cozy/ui/widgets/book_card.py index cfcb75ca..92fc0438 100644 --- a/cozy/ui/widgets/book_card.py +++ b/cozy/ui/widgets/book_card.py @@ -105,12 +105,8 @@ def _install_event_controllers(self): long_press_gesture = Gtk.GestureLongPress() long_press_gesture.connect("pressed", self._on_long_tap) - key_event_controller = Gtk.EventControllerKey() - key_event_controller.connect("key-pressed", self._on_key_press_event) - self.add_controller(hover_controller) self.add_controller(long_press_gesture) - self.add_controller(key_event_controller) def set_playing(self, is_playing): self.play_button.set_playing(is_playing) @@ -160,7 +156,3 @@ def _on_long_tap(self, gesture: Gtk.Gesture, *_): device = gesture.get_device() if device and device.get_source() == Gdk.InputSource.TOUCHSCREEN: self.menu_button.emit("activate") - - def _on_key_press_event(self, controller, keyval, *_): - if keyval == Gdk.KEY_Return: - self.emit("open-book-overview", self.book) diff --git a/cozy/ui/widgets/seek_bar.py b/cozy/ui/widgets/seek_bar.py index 18303b68..78a48376 100644 --- a/cozy/ui/widgets/seek_bar.py +++ b/cozy/ui/widgets/seek_bar.py @@ -1,4 +1,4 @@ -from gi.repository import Gdk, GObject, Gtk +from gi.repository import GObject, Gtk from cozy.control.time_format import ns_to_time @@ -37,10 +37,6 @@ def __init__(self, **kwargs): 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) - @GObject.Signal(arg_types=(object,)) def position_changed(self, *_): ... @@ -90,11 +86,5 @@ def _on_progress_scale_release(self, *_): value = self.progress_scale.get_value() self.emit("position-changed", value) - def _on_progress_key_pressed(self, _, event, *__): - if event in {Gdk.KEY_Up, Gdk.KEY_Left}: - self.emit("rewind") - elif event in {Gdk.KEY_Down, Gdk.KEY_Right}: - self.emit("forward") - def _on_progress_scale_press(self, *_): self._progress_scale_pressed = True diff --git a/cozy/view_model/playback_control_view_model.py b/cozy/view_model/playback_control_view_model.py index fcb76a6e..027094e3 100644 --- a/cozy/view_model/playback_control_view_model.py +++ b/cozy/view_model/playback_control_view_model.py @@ -94,9 +94,25 @@ def play_pause(self): def rewind(self): self._player.rewind() + self._player._emit_tick() def forward(self): self._player.forward() + self._player._emit_tick() + + def next_chapter(self): + self._player._next_chapter() + + def previous_chapter(self): + self._player._previous_chapter() + + def volume_up(self): + self._player.volume_up() + self._notify("volume") + + def volume_down(self): + self._player.volume_down() + self._notify("volume") def open_book_detail(self): if self.book: diff --git a/cozy/view_model/playback_speed_view_model.py b/cozy/view_model/playback_speed_view_model.py index adf4b3dd..cc8d4484 100644 --- a/cozy/view_model/playback_speed_view_model.py +++ b/cozy/view_model/playback_speed_view_model.py @@ -34,3 +34,15 @@ def _on_player_event(self, event: str, message): if event == "chapter-changed" and message: self._book = message self._notify("playback_speed") + + def speed_up(self): + self.playback_speed = min(self.playback_speed + 0.1, 3.5) + self._notify("playback_speed") + + def speed_down(self): + self.playback_speed = max(self.playback_speed - 0.1, 0.5) + self._notify("playback_speed") + + def speed_reset(self): + self.playback_speed = 1.0 + self._notify("playback_speed") diff --git a/data/ui/headerbar.blp b/data/ui/headerbar.blp index afc21dd5..ee9bb718 100644 --- a/data/ui/headerbar.blp +++ b/data/ui/headerbar.blp @@ -22,6 +22,7 @@ template $Headerbar: Box { tooltip-text: _("Options"); menu-model: primary_menu; icon-name: 'open-menu-symbolic'; + primary: true; accessibility { label: _("Open the options popover");