Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sleep timer redesign #973

Merged
merged 15 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cozy/app_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,4 @@ def _on_main_window_event(self, event: str, data):
self._on_open_view(data, None)

def quit(self):
self.sleep_timer_view_model.destroy()
self.player.destroy()
8 changes: 8 additions & 0 deletions cozy/control/time_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from gi.repository import Gst


def seconds_to_time(seconds: int) -> str:
return ns_to_time(seconds * Gst.SECOND)


def ns_to_time(
nanoseconds: int, max_length: int | None = None, include_seconds: bool = True
) -> str:
Expand Down Expand Up @@ -36,6 +40,10 @@ def ns_to_time(
return result


def min_to_human_readable(minutes: int) -> str:
return ns_to_human_readable(minutes * Gst.SECOND * 60)


def ns_to_human_readable(nanoseconds: int) -> str:
"""
Create a string with the following format:
Expand Down
18 changes: 9 additions & 9 deletions cozy/media/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,8 @@ def _fadeout_callback(self) -> None:
GLib.source_remove(self._fade_timeout)
self._fade_timeout = None

self.pause()
self._volume_fader.props.volume = 1.0

self.emit_event("fadeout-finished", None)

def fadeout(self, length: int) -> None:
if not self._is_player_loaded():
return
Expand Down Expand Up @@ -427,11 +424,10 @@ def play_pause(self):
log.error("Trying to play/pause although player is in STOP state.")
reporter.error("player", "Trying to play/pause although player is in STOP state.")

def pause(self, fadeout: bool = False):
if fadeout:
self._gst_player.fadeout(self._app_settings.sleep_timer_fadeout_duration)
return
def fadeout(self, duration: int):
self._gst_player.fadeout(duration)

def pause(self):
if self._gst_player.state == Gst.State.PLAYING:
self._gst_player.pause()

Expand Down Expand Up @@ -486,6 +482,7 @@ def volume_down(self):
self.volume = max(0, self.volume - 0.1)

def destroy(self):
self._stop_tick_thread()
self._gst_player.stop()

def _load_book(self, book: Book):
Expand Down Expand Up @@ -649,7 +646,10 @@ def _on_importer_event(self, event: str, message):

def _on_gst_player_event(self, event: str, message):
if event == "file-finished":
self._next_chapter()
if self._play_next_chapter:
self._next_chapter()
else:
self._stop_playback()
elif event == "resource-not-found":
self._handle_file_not_found()
elif event == "state" and message == Gst.State.PLAYING:
Expand Down Expand Up @@ -708,7 +708,7 @@ def _emit_tick(self):
log.info("Not emitting tick because no book/chapter is loaded.")
return

if self.position > self.loaded_chapter.end_position:
if self.position > self.loaded_chapter.end_position and self._play_next_chapter:
self._next_chapter()

try:
Expand Down
8 changes: 0 additions & 8 deletions cozy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,6 @@ def prefer_external_cover(self) -> bool:
def prefer_external_cover(self, new_value: bool):
self._settings.set_boolean("prefer-external-cover", new_value)

@property
def sleep_timer_fadeout(self) -> bool:
return self._settings.get_boolean("sleep-timer-fadeout")

@property
def sleep_timer_fadeout_duration(self) -> int:
return self._settings.get_int("sleep-timer-fadeout-duration")

@property
def timer(self) -> int:
return self._settings.get_int("timer")
Expand Down
4 changes: 2 additions & 2 deletions cozy/ui/media_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ def __init__(self, main_window_builder: Gtk.Builder):
self.seek_bar = SeekBar()
self.seek_bar_container.append(self.seek_bar)

self.sleep_timer: SleepTimer = SleepTimer(self.timer_image)
self.sleep_timer = SleepTimer(self.timer_button)
self.timer_button.connect("clicked", self.sleep_timer.present)
self.playback_speed_button.set_popover(PlaybackSpeedPopover())
self.timer_button.set_popover(self.sleep_timer)

self.volume_button.set_icons(
[
Expand Down
4 changes: 0 additions & 4 deletions cozy/ui/preferences_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ class PreferencesWindow(Adw.PreferencesDialog):

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()
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()

def __init__(self) -> None:
super().__init__()
Expand All @@ -47,8 +45,6 @@ def _bind_settings(self) -> None:
bind_settings("replay", self.replay_switch, "active")
bind_settings("rewind-duration", self.rewind_duration_adjustment, "value")
bind_settings("forward-duration", self.forward_duration_adjustment, "value")
bind_settings("sleep-timer-fadeout", self.sleep_timer_fadeout_switch, "enable-expansion")
bind_settings("sleep-timer-fadeout-duration", self.fadeout_duration_adjustment, "value")
bind_settings("prefer-external-cover", self.artwork_prefer_external_switch, "active")

def _on_lock_ui_changed(self) -> None:
Expand Down
187 changes: 120 additions & 67 deletions cozy/ui/widgets/sleep_timer.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,150 @@
from __future__ import annotations

import inject
from gi.repository import Gtk
from gi.repository import Adw, Gio, GLib, Gtk

from cozy.view_model.sleep_timer_view_model import SleepTimerViewModel, SystemPowerControl
from cozy.control.time_format import min_to_human_readable, seconds_to_time
from cozy.view_model.sleep_timer_view_model import SleepTimerViewModel


@Gtk.Template.from_resource('/com/github/geigi/cozy/ui/timer_popover.ui')
class SleepTimer(Gtk.Popover):
@Gtk.Template.from_resource("/com/github/geigi/cozy/ui/sleep_timer_dialog.ui")
class SleepTimer(Adw.Dialog):
__gtype_name__ = "SleepTimer"

_view_model = inject.attr(SleepTimerViewModel)

timer_scale: Gtk.Scale = Gtk.Template.Child()
timer_label: Gtk.Label = Gtk.Template.Child()
timer_grid: Gtk.Grid = Gtk.Template.Child()
min_label: Gtk.Label = Gtk.Template.Child()
chapter_switch: Gtk.Switch = Gtk.Template.Child()

power_control_switch: Gtk.Switch = Gtk.Template.Child()
power_control_options: Gtk.Box = Gtk.Template.Child()
system_shutdown_radiob: Gtk.CheckButton = Gtk.Template.Child()
system_suspend_radiob: Gtk.CheckButton = Gtk.Template.Child()
list: Adw.PreferencesGroup = Gtk.Template.Child()
stack: Gtk.Stack = Gtk.Template.Child()
set_timer_button: Gtk.Button = Gtk.Template.Child()
timer_state: Adw.StatusPage = Gtk.Template.Child()
toolbarview: Adw.ToolbarView = Gtk.Template.Child()
till_end_of_chapter_button_row: Adw.ButtonRow = Gtk.Template.Child()

def __init__(self, timer_image: Gtk.Image):
def __init__(self, parent_button: Gtk.Button):
super().__init__()
self._parent_button = parent_button

self._timer_image: Gtk.Image = timer_image
self.custom_adjustment = Gtk.Adjustment(lower=1, upper=300, value=20, step_increment=1)

self._init_timer_scale()
self._connect_widgets()
timer_action_group = Gio.SimpleActionGroup()
self.insert_action_group("timer", timer_action_group)

self._connect_view_model()
self.sleep_timer_action = Gio.SimpleAction(
name="selected", state=GLib.Variant("n", 0), parameter_type=GLib.VariantType("n")
)
timer_action_group.add_action(self.sleep_timer_action)
self.sleep_timer_action.connect("notify::state", self._on_timer_interval_selected)

first_row = self._create_timer_selection_row(5)
self.list.add(first_row)

for duration in (15, 30, 60, -2):
self.list.add(self._create_timer_selection_row(duration, first_row))

self._on_timer_scale_changed(self.timer_scale)
self.spin_row = self._create_spin_timer_row(first_row)
self.list.add(self.spin_row)
self.custom_adjustment.connect("value-changed", self._update_custom_interval_text)
self._update_custom_interval_text()

def _connect_widgets(self):
self.timer_scale.connect("value-changed", self._on_timer_scale_changed)
self.chapter_switch.connect("state-set", self._on_chapter_switch_changed)
self.power_control_switch.connect("state-set", self._on_power_options_switch_changed)
self.system_suspend_radiob.connect("toggled", self._on_system_action_radio_button_changed)
self.system_shutdown_radiob.connect("toggled", self._on_system_action_radio_button_changed)
self._connect_view_model()

def _connect_view_model(self):
self._view_model.bind_to("stop_after_chapter", self._on_stop_after_chapter_changed)
self._view_model.bind_to("remaining_seconds", self._on_remaining_seconds_changed)
self._view_model.bind_to("timer_enabled", self._on_timer_enabled_changed)

def _init_timer_scale(self):
for i in range(0, 121, 30):
self.timer_scale.add_mark(i, Gtk.PositionType.RIGHT, None)

def _on_timer_scale_changed(self, scale: Gtk.Scale):
value = scale.get_value()

if value > 0:
self.timer_label.set_visible(True)
self.min_label.set_text(_("min"))
text = str(int(value))
self.timer_label.set_text(text)
self._view_model.remaining_seconds = value * 60
else:
self.min_label.set_text(_("Off"))
self.timer_label.set_visible(False)
self._view_model.remaining_seconds = 0

def _on_chapter_switch_changed(self, _, state):
self.timer_grid.set_sensitive(not state)
self._view_model.stop_after_chapter = state
def _add_radio_button_to_timer_row(
self, row: Adw.ActionRow, action_target: int, group: Gtk.CheckButton | None
) -> None:
radio = Gtk.CheckButton(
css_classes=["selection-mode"],
group=group,
action_name="timer.selected",
action_target=GLib.Variant("n", action_target),
can_focus=False,
)
row.radio = radio
row.set_activatable_widget(radio)
row.add_prefix(radio)

def _create_timer_selection_row(
self, duration: int, group: Adw.ActionRow | None = None
) -> Adw.ActionRow:
title = _("End of Chapter") if duration == -2 else min_to_human_readable(duration)
row = Adw.ActionRow(title=title)
self._add_radio_button_to_timer_row(row, duration, group.radio if group else None)

return row

def _create_spin_timer_row(self, group: Adw.ActionRow) -> Adw.SpinRow:
spin_row = Adw.SpinRow(adjustment=self.custom_adjustment)
spin_row.add_css_class("sleep-timer")
self._add_radio_button_to_timer_row(spin_row, -1, group.radio)

spin_button = spin_row.get_first_child().get_last_child().get_last_child()
spin_button.set_halign(Gtk.Align.END)

spin_row.connect("input", lambda x, y: spin_row.activate() or 0)

return spin_row

def _update_custom_interval_text(self, *_) -> None:
self.spin_row.set_title(min_to_human_readable(self.custom_adjustment.get_value()))

def _on_timer_interval_selected(self, action, _):
value = action.get_state().unpack()
if value != 0:
self.set_timer_button.set_sensitive(True)

def _on_remaining_seconds_changed(self):
if self._view_model.remaining_seconds < 1:
value = 0
else:
value = int(self._view_model.remaining_seconds / 60) + 1

self.timer_scale.set_value(value)
self.timer_state.set_title(seconds_to_time(self._view_model.remaining_seconds))

def _on_power_options_switch_changed(self, _, state):
self.power_control_options.set_sensitive(state)
def _on_stop_after_chapter_changed(self):
self.till_end_of_chapter_button_row.set_visible(not self._view_model.stop_after_chapter)
if self._view_model.stop_after_chapter:
self.timer_state.set_title(_("Stopping After Current Chapter"))

if not state:
self._view_model.system_power_control = SystemPowerControl.OFF
def _on_timer_enabled_changed(self):
self.stack.set_visible_child_name(
"running" if self._view_model.timer_enabled else "uninitiated"
)
self.toolbarview.set_reveal_bottom_bars(not self._view_model.timer_enabled)
self._parent_button.set_icon_name(
"bed-symbolic" if self._view_model.timer_enabled else "no-bed-symbolic"
)

def present(self, *_) -> None:
super().present(inject.instance("MainWindow").window)

@Gtk.Template.Callback()
def close(self, *_):
super().close()

@Gtk.Template.Callback()
def set_timer(self, *_):
super().close()

value = self.sleep_timer_action.get_state().unpack()
if value == -1:
self._view_model.remaining_seconds = self.custom_adjustment.get_value() * 60
elif value == -2:
self._view_model.stop_after_chapter = True
else:
self._on_system_action_radio_button_changed(None)
self._view_model.remaining_seconds = value * 60

def _on_system_action_radio_button_changed(self, _):
if self.system_suspend_radiob.get_active():
self._view_model.system_power_control = SystemPowerControl.SUSPEND
@Gtk.Template.Callback()
def plus_5_minutes(self, *_):
if self._view_model.stop_after_chapter:
self._view_model.remaining_seconds = self._view_model.get_remaining_from_chapter() + 300
else:
self._view_model.system_power_control = SystemPowerControl.SHUTDOWN

def _on_stop_after_chapter_changed(self):
self.chapter_switch.set_active(self._view_model.stop_after_chapter)
self._view_model.remaining_seconds += 300

def _on_timer_enabled_changed(self):
self._timer_image.set_from_icon_name('bed-symbolic' if self._view_model.timer_enabled else 'no-bed-symbolic')
@Gtk.Template.Callback()
def till_end_of_chapter(self, *_):
self._view_model.stop_after_chapter = True

@Gtk.Template.Callback()
def cancel_timer(self, *_):
super().close()
self._view_model.remaining_seconds = 0
self._view_model.stop_after_chapter = False
Loading
Loading