~comcloudway/melon

1b6397fd8edc044b5883e671fd6f2808d8c3c835 — Jakob Meier 6 months ago 03c49f9
add volume & brightness control using slide gestures

(these are currently not saved)
1 files changed, 164 insertions(+), 18 deletions(-)

M melon/widgets/player.py
M melon/widgets/player.py => melon/widgets/player.py +164 -18
@@ 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