Skip to content

Commit

Permalink
Sleep timer redesign (#973)
Browse files Browse the repository at this point in the history
* First sketchy redesign of sleep timer

* Clean up sleep timer view model, fix quirks

* Remove togglable sleep timer fadeout preference

Instead of this being an optional thing, let's make it an actual
feature, and use it always

* Format changed files

* Move timer row constructors to functions, clean up

* Show timer button now that it works on mobile

* Add remaining time when adding +5 minutes to chapter end

* Reuse tick from player for sleep timer

This makes everything much simpler, and also makes it sure, that the two
counters are synchronized visually

* 20 hours for sleep timer is way too much. Reduce it to 5

* Format changed files

* Adjustments in UI file

* Looks like we were doing some unnecessary work

* Another bits of obsolete code

* Fix a Gtk warning

* Make the spin row a bit nicer to use with hacks
  • Loading branch information
rdbende authored Jan 8, 2025
1 parent 6cda5f3 commit 8450008
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 322 deletions.
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

0 comments on commit 8450008

Please sign in to comment.