@@ 12,10 12,77 @@ from melon.widgets.iconbutton import IconButton
from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference
from melon.utils import pass_me
+def clamp(lower, value, upper):
+ return max(lower, min(value, upper))
+
+class OverlayDispay(Gtk.Box):
+ def __init__(self, icon, text, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_halign(Gtk.Align.CENTER)
+ self.set_valign(Gtk.Align.CENTER)
+ self.add_css_class("osd")
+ self.add_css_class("toolbar")
+ self._icon = Gtk.Image()
+ self.append(self._icon)
+ self._label = Gtk.Label()
+ self.append(self._label)
+ self.set_icon(icon)
+ self.set_text(text)
+ self.show()
+ def show(self):
+ self.set_opacity(1.0)
+ def hide(self):
+ self.set_opacity(0.0)
+ def set_icon(self, icon:str):
+ self._icon.set_from_icon_name(icon)
+ def set_text(self, text:str):
+ self._label.set_label(text)
+
+class BrightnessDisplay(OverlayDispay):
+ def __init__(self, value:float, *args, **kwargs):
+ initial_icon = self.get_icon_for(value)
+ initial_text = self.get_text_for(value)
+ super().__init__(initial_icon, initial_text, *args, **kwargs)
+ def update(self, value:float):
+ icon = self.get_icon_for(value)
+ text = self.get_text_for(value)
+ self.set_icon(icon)
+ self.set_text(text)
+ def get_icon_for(self, value:float) -> str:
+ if value < 0.4:
+ return "night-light-symbolic"
+ if value < 0.8:
+ return "weather-clear-symbolic"
+ return "display-brightness-symbolic"
+ def get_text_for(self, value:float) -> str:
+ return f"{int(value * 100)}%"
+
+class VolumeDisplay(OverlayDispay):
+ def __init__(self, value:float, *args, **kwargs):
+ initial_icon = self.get_icon_for(value)
+ initial_text = self.get_text_for(value)
+ super().__init__(initial_icon, initial_text, *args, **kwargs)
+ def update(self, value:float):
+ icon = self.get_icon_for(value)
+ text = self.get_text_for(value)
+ self.set_icon(icon)
+ self.set_text(text)
+ def get_icon_for(self, value:float) -> str:
+ if value == 0.0:
+ return "audio-volume-muted-symbolic"
+ if value < 0.4:
+ return "audio-volume-low-symbolic"
+ if value < 0.8:
+ return "audio-volume-medium-symbolic"
+ return "audio-volume-high-symbolic"
+ def get_text_for(self, value:float) -> str:
+ return f"{int(value * 100)}%"
+
class VideoPlayer(Gtk.Overlay):
- # TODO: consider adding volume + brightness control
- # place volume + brightness here so they are static
- # and the values are hopefully remembered for the app lifetime
+ volume = 1.0
+ # inverse opacity of overlay
+ brightness = 1.0
+
def __init__(self, streams: list[Stream], *args, **kwargs):
super().__init__(*args, **kwargs)
self.streams = streams
@@ 61,6 128,23 @@ class VideoPlayer(Gtk.Overlay):
# Set the paintable on the picture
self.picture.set_paintable(paintable)
+ self.brightness_overlay = Gtk.Box()
+ # make full size
+ self.brightness_overlay.set_hexpand(True)
+ self.brightness_overlay.set_vexpand(True)
+ # add background color (this is semi-opaque, so it won't ever completely hide the video)
+ self.brightness_overlay.add_css_class("osd")
+ overlay.add_overlay(self.brightness_overlay)
+
+ self.brightness_display = BrightnessDisplay(self.brightness)
+ overlay.add_overlay(self.brightness_display)
+ self.brightness_display.hide()
+ self.volume_display = VolumeDisplay(self.volume)
+ overlay.add_overlay(self.volume_display)
+ self.volume_display.hide()
+ self.change_brightness(self.brightness)
+ self.change_volume(self.volume)
+
self.controls = Gtk.Box()
overlay.add_overlay(self.controls)
# used for flat button styles
@@ 121,11 205,14 @@ class VideoPlayer(Gtk.Overlay):
self.add_controller(self.hover_ctr)
self.hover_ctr.connect("enter", self._mouse_enter)
self.hover_ctr.connect("leave", self._mouse_leave)
- # listen to click (for mobile support)
- # functions as a toggle
- self.click_ctr = Gtk.GestureClick()
- self.picture.add_controller(self.click_ctr)
- self.click_ctr.connect("pressed", self._onclick)
+ # swipe handler
+ self.swipe_ctr = Gtk.GestureSwipe()
+ # only called for valid swipe input, while it is happening
+ self.swipe_ctr.connect("update", self._update_swipe)
+ # also calls swipe for clicks (and on swipe end)
+ # so we use this to handle the onclick event to toggle the controls (mobile)
+ self.swipe_ctr.connect("swipe", self._end_swipe)
+ self.add_controller(self.swipe_ctr)
def connect_update(self, callback=None):
self.update_callback = callback
@@ 143,20 230,79 @@ class VideoPlayer(Gtk.Overlay):
def _schedule_hide_controls(self, timeout=2):
GLib.timeout_add(timeout*1000, self._hide_controls)
+ def _update_swipe(self, e, s):
+ bounds = self.swipe_ctr.get_bounding_box_center()
+ if not bounds[0]:
+ return
+ width = self.picture.get_width()
+ left_bound = width*(2/5)
+ right_bound = width - left_bound
+
+ vel = self.swipe_ctr.get_velocity()
+ if not vel[0]:
+ return
+
+ # randomly selected values to make it feel responsive
+ fact = vel[2]/10000
+ fact = clamp(-0.2, fact, 0.2)
+
+ if bounds[1] >= right_bound:
+ # volume control
+ # update the value before showing the overlay
+ # invert the value so it goes in the right direction
+ self.change_volume(self.volume - fact)
+ # make sure the display is shown
+ self.volume_display.show()
+ elif bounds[1] <= left_bound:
+ # brightness controll
+ # update the value before showing the overlay
+ # invert the value so it goes in the right direction
+ self.change_brightness(self.brightness - fact)
+ self.brightness_display.show()
+ else:
+ # mixed controls
+ # hide all
+ self.brightness_display.hide()
+ self.volume_display.hide()
+
+ def change_volume(self, vol):
+ self.volume = clamp(0.0, vol, 1.0)
+ self.volume_display.update(self.volume)
+ # TODO: update player volume
+ self.source.set_property("volume", self.volume)
+ # TODO: update volume button value (once implemented)
+
+ def change_brightness(self, bright):
+ self.brightness = clamp(0.0, bright, 1.0)
+ self.brightness_display.update(self.brightness)
+ self.brightness_overlay.set_opacity(1.0 - self.brightness)
+
+ def _end_swipe(self, e, vx, vy):
+ # no swipe movement
+ # just a click
+ if vx==0.0 and vy == 0.0:
+ self._onclick()
+ return
+ # hide overlay
+ # always hide both overlays just to make sure
+ self.brightness_display.hide()
+ self.volume_display.hide()
+
def _mouse_enter(self, w, x, y):
self._show_controls()
def _mouse_leave(self, w):
self._schedule_hide_controls()
- def _onclick(self, w, n, x, y):
- if n == 1:
- # single click toggles focus
- # because floats are a thing we can't use ==
- # however given that we switch between 0 and 1
- # we can use 0.9 or 0.1 as breakpoints
- if self.controls.get_opacity() >= 0.9:
- self._hide_controls(force=True)
- else:
- self._show_controls()
+
+ offset = 50
+ def _onclick(self):
+ # single click toggles focus
+ # because floats are a thing we can't use ==
+ # however given that we switch between 0 and 1
+ # we can use 0.9 or 0.1 as breakpoints
+ if self.controls.get_opacity() >= 0.9:
+ self._hide_controls(force=True)
+ else:
+ self._show_controls()
def _update_quality_list(self):
# TODO consider making this scrollable