summaryrefslogtreecommitdiffstats
path: root/.config/mpv/scripts/uosc_shared/elements
diff options
context:
space:
mode:
Diffstat (limited to '.config/mpv/scripts/uosc_shared/elements')
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua37
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Button.lua90
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Controls.lua329
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Curtain.lua35
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/CycleButton.lua64
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Element.lua149
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Elements.lua158
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Menu.lua783
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua80
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Speed.lua183
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Timeline.lua394
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/TopBar.lua182
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/Volume.lua240
-rw-r--r--.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua33
14 files changed, 0 insertions, 2757 deletions
diff --git a/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua
deleted file mode 100644
index e2aa071..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua
+++ /dev/null
@@ -1,37 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@class BufferingIndicator : Element
-local BufferingIndicator = class(Element)
-
-function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end
-function BufferingIndicator:init()
- Element.init(self, 'buffer_indicator')
- self.ignores_menu = true
- self.enabled = false
-end
-
-function BufferingIndicator:decide_enabled()
- local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100
- local player = state.core_idle and not state.eof_reached
- if self.enabled then
- if not player or (state.pause and not cache) then self.enabled = false end
- elseif player and cache and state.uncached_ranges then self.enabled = true end
-end
-
-function BufferingIndicator:on_prop_pause() self:decide_enabled() end
-function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end
-function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end
-function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end
-function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end
-function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end
-
-function BufferingIndicator:render()
- local ass = assdraw.ass_new()
- ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3})
- local size = round(30 + math.min(display.width, display.height) / 10)
- local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8
- ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity})
- return ass
-end
-
-return BufferingIndicator
diff --git a/.config/mpv/scripts/uosc_shared/elements/Button.lua b/.config/mpv/scripts/uosc_shared/elements/Button.lua
deleted file mode 100644
index 8d0f725..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Button.lua
+++ /dev/null
@@ -1,90 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
-
----@class Button : Element
-local Button = class(Element)
-
----@param id string
----@param props ButtonProps
-function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end
----@param id string
----@param props ButtonProps
-function Button:init(id, props)
- self.icon = props.icon
- self.active = props.active
- self.tooltip = props.tooltip
- self.badge = props.badge
- self.foreground = props.foreground or fg
- self.background = props.background or bg
- ---@type fun()
- self.on_click = props.on_click
- Element.init(self, id, props)
-end
-
-function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
-function Button:on_mbtn_left_down()
- -- Don't accept clicks while hidden.
- if self:get_visibility() <= 0 then return end
- -- We delay the callback to next tick, otherwise we are risking race
- -- conditions as we are in the middle of event dispatching.
- -- For example, handler might add a menu to the end of the element stack, and that
- -- than picks up this click even we are in right now, and instantly closes itself.
- mp.add_timeout(0.01, self.on_click)
-end
-
-function Button:render()
- local visibility = self:get_visibility()
- if visibility <= 0 then return end
-
- local ass = assdraw.ass_new()
- local is_hover = self.proximity_raw == 0
- local is_hover_or_active = is_hover or self.active
- local foreground = self.active and self.background or self.foreground
- local background = self.active and self.foreground or self.background
-
- -- Background
- if is_hover_or_active then
- ass:rect(self.ax, self.ay, self.bx, self.by, {
- color = self.active and background or foreground, radius = 2,
- opacity = visibility * (self.active and 1 or 0.3),
- })
- end
-
- -- Tooltip on hover
- if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end
-
-
- -- Badge
- local icon_clip
- if self.badge then
- local badge_font_size = self.font_size * 0.6
- local badge_opts = {size = badge_font_size, color = background, opacity = visibility}
- local badge_width = text_width(self.badge, badge_opts)
- local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93)
- local bx, by = self.bx - 1, self.by - 1
- ass:rect(bx - width, by - height, bx, by, {
- color = foreground, radius = 2, opacity = visibility,
- border = self.active and 0 or 1, border_color = background,
- })
- ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts)
-
- local clip_border = math.max(self.font_size / 20, 1)
- local clip_path = assdraw.ass_new()
- clip_path:round_rect_cw(
- math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3
- )
- icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')'
- end
-
- -- Icon
- local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2)
- ass:icon(x, y, self.font_size, self.icon, {
- color = foreground, border = self.active and 0 or options.text_border, border_color = background,
- opacity = visibility, clip = icon_clip,
- })
-
- return ass
-end
-
-return Button
diff --git a/.config/mpv/scripts/uosc_shared/elements/Controls.lua b/.config/mpv/scripts/uosc_shared/elements/Controls.lua
deleted file mode 100644
index 9a6be72..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Controls.lua
+++ /dev/null
@@ -1,329 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-local Button = require('uosc_shared/elements/Button')
-local CycleButton = require('uosc_shared/elements/CycleButton')
-local Speed = require('uosc_shared/elements/Speed')
-
--- `scale` - `options.controls_size` scale factor.
--- `ratio` - Width/height ratio of a static or dynamic element.
--- `ratio_min` Min ratio for 'dynamic' sized element.
----@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table<string, boolean>}
-
----@class Controls : Element
-local Controls = class(Element)
-
-function Controls:new() return Class.new(self) --[[@as Controls]] end
-function Controls:init()
- Element.init(self, 'controls')
- ---@type ControlItem[] All control elements serialized from `options.controls`.
- self.controls = {}
- ---@type ControlItem[] Only controls that match current dispositions.
- self.layout = {}
-
- -- Serialize control elements
- local shorthands = {
- menu = 'command:menu:script-binding uosc/menu-blurred?Menu',
- subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles',
- audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio',
- ['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
- video = 'command:theaters:script-binding uosc/video#video>1?Video',
- playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
- chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters',
- ['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions',
- ['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality',
- ['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
- ['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files',
- prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
- next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
- first = 'command:first_page:script-binding uosc/first?First',
- last = 'command:last_page:script-binding uosc/last?Last',
- ['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist',
- ['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file',
- shuffle = 'toggle:shuffle:shuffle?Shuffle',
- fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen',
- }
-
- -- Parse out disposition/config pairs
- local items = {}
- local in_disposition = false
- local current_item = nil
- for c in options.controls:gmatch('.') do
- if not current_item then current_item = {disposition = '', config = ''} end
- if c == '<' and #current_item.config == 0 then in_disposition = true
- elseif c == '>' and #current_item.config == 0 then in_disposition = false
- elseif c == ',' and not in_disposition then
- items[#items + 1] = current_item
- current_item = nil
- else
- local prop = in_disposition and 'disposition' or 'config'
- current_item[prop] = current_item[prop] .. c
- end
- end
- items[#items + 1] = current_item
-
- -- Create controls
- self.controls = {}
- for i, item in ipairs(items) do
- local config = shorthands[item.config] and shorthands[item.config] or item.config
- local config_tooltip = split(config, ' *%? *')
- local tooltip = config_tooltip[2]
- config = shorthands[config_tooltip[1]]
- and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1]
- local config_badge = split(config, ' *# *')
- config = config_badge[1]
- local badge = config_badge[2]
- local parts = split(config, ' *: *')
- local kind, params = parts[1], itable_slice(parts, 2)
-
- -- Serialize dispositions
- local dispositions = {}
- for _, definition in ipairs(split(item.disposition, ' *, *')) do
- if #definition > 0 then
- local value = definition:sub(1, 1) ~= '!'
- local name = not value and definition:sub(2) or definition
- local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name
- dispositions[prop] = value
- end
- end
-
- -- Convert toggles into cycles
- if kind == 'toggle' then
- kind = 'cycle'
- params[#params + 1] = 'no/yes!'
- end
-
- -- Create a control element
- local control = {dispositions = dispositions, kind = kind}
-
- if kind == 'space' then
- control.sizing = 'space'
- elseif kind == 'gap' then
- table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0})
- elseif kind == 'command' then
- if #params ~= 2 then
- mp.error(string.format(
- 'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/')
- ))
- else
- local element = Button:new('control_' .. i, {
- icon = params[1],
- anchor_id = 'controls',
- on_click = function() mp.command(params[2]) end,
- tooltip = tooltip,
- count_prop = 'sub',
- })
- table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
- if badge then self:register_badge_updater(badge, element) end
- end
- elseif kind == 'cycle' then
- if #params ~= 3 then
- mp.error(string.format(
- 'cycle button needs 3 parameters, %d received: %s',
- #params, table.concat(params, '/')
- ))
- else
- local state_configs = split(params[3], ' */ *')
- local states = {}
-
- for _, state_config in ipairs(state_configs) do
- local active = false
- if state_config:sub(-1) == '!' then
- active = true
- state_config = state_config:sub(1, -2)
- end
- local state_params = split(state_config, ' *= *')
- local value, icon = state_params[1], state_params[2] or params[1]
- states[#states + 1] = {value = value, icon = icon, active = active}
- end
-
- local element = CycleButton:new('control_' .. i, {
- prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip,
- })
- table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
- if badge then self:register_badge_updater(badge, element) end
- end
- elseif kind == 'speed' then
- if not Elements.speed then
- local element = Speed:new({anchor_id = 'controls'})
- table_assign(control, {
- element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2,
- })
- else
- msg.error('there can only be 1 speed slider')
- end
- else
- msg.error('unknown element kind "' .. kind .. '"')
- break
- end
-
- self.controls[#self.controls + 1] = control
- end
-
- self:reflow()
-end
-
-function Controls:reflow()
- -- Populate the layout only with items that match current disposition
- self.layout = {}
- for _, control in ipairs(self.controls) do
- local matches = true
- for prop, value in pairs(control.dispositions) do
- if state[prop] ~= value then
- matches = false
- break
- end
- end
- if control.element then control.element.enabled = matches end
- if matches then self.layout[#self.layout + 1] = control end
- end
-
- self:update_dimensions()
- Elements:trigger('controls_reflow')
-end
-
----@param badge string
----@param element Element An element that supports `badge` property.
-function Controls:register_badge_updater(badge, element)
- local prop_and_limit = split(badge, ' *> *')
- local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1)
- local observable_name, serializer, is_external_prop = prop, nil, false
-
- if itable_index_of({'sub', 'audio', 'video'}, prop) then
- observable_name = 'track-list'
- serializer = function(value)
- local count = 0
- for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end
- return count
- end
- else
- local parts = split(prop, '@')
- -- Support both new `prop@owner` and old `@prop` syntaxes
- if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end
- serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end
- end
-
- local function handler(_, value)
- local new_value = serializer(value) --[[@as nil|string|integer]]
- local value_number = tonumber(new_value)
- if value_number then new_value = value_number > limit and value_number or nil end
- element.badge = new_value
- request_render()
- end
-
- if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
- else mp.observe_property(observable_name, 'native', handler) end
-end
-
-function Controls:get_visibility()
- return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered()
- and -1 or Element.get_visibility(self)
-end
-
-function Controls:update_dimensions()
- local window_border = Elements.window_border.size
- local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size
- local spacing = options.controls_spacing
- local margin = options.controls_margin
-
- -- Disable when not enough space
- local available_space = display.height - Elements.window_border.size * 2
- if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
- if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end
- self.enabled = available_space > size + 10
-
- -- Reset hide/enabled flags
- for c, control in ipairs(self.layout) do
- control.hide = false
- if control.element then control.element.enabled = self.enabled end
- end
-
- if not self.enabled then return end
-
- -- Container
- self.bx = display.width - window_border - margin
- self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin
- self.ax, self.ay = window_border + margin, self.by - size
-
- -- Controls
- local available_width = self.bx - self.ax
- local statics_width = (#self.layout - 1) * spacing
- local min_content_width = statics_width
- local max_dynamics_width, dynamic_units, spaces = 0, 0, 0
-
- -- Calculate statics_width, min_content_width, and count spaces
- for c, control in ipairs(self.layout) do
- if control.sizing == 'space' then
- spaces = spaces + 1
- elseif control.sizing == 'static' then
- local width = size * control.scale * control.ratio
- statics_width = statics_width + width
- min_content_width = min_content_width + width
- elseif control.sizing == 'dynamic' then
- min_content_width = min_content_width + size * control.scale * control.ratio_min
- max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio
- dynamic_units = dynamic_units + control.scale * control.ratio
- end
- end
-
- -- Hide & disable elements in the middle until we fit into available width
- if min_content_width > available_width then
- local i = math.ceil(#self.layout / 2 + 0.1)
- for a = 0, #self.layout - 1, 1 do
- i = i + (a * (a % 2 == 0 and 1 or -1))
- local control = self.layout[i]
-
- if control.kind ~= 'gap' and control.kind ~= 'space' then
- control.hide = true
- if control.element then control.element.enabled = false end
- if control.sizing == 'static' then
- local width = size * control.scale * control.ratio
- min_content_width = min_content_width - width - spacing
- statics_width = statics_width - width - spacing
- elseif control.sizing == 'dynamic' then
- min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing
- max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio
- dynamic_units = dynamic_units - control.scale * control.ratio
- end
-
- if min_content_width < available_width then break end
- end
- end
- end
-
- -- Lay out the elements
- local current_x = self.ax
- local width_for_dynamics = available_width - statics_width
- local space_width = (width_for_dynamics - max_dynamics_width) / spaces
-
- for c, control in ipairs(self.layout) do
- if not control.hide then
- local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio
- local width, height = 0, 0
-
- if sizing == 'space' then
- if space_width > 0 then width = space_width end
- elseif sizing == 'static' then
- height = size * scale
- width = height * ratio
- elseif sizing == 'dynamic' then
- height = size * scale
- width = max_dynamics_width < width_for_dynamics
- and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units)
- end
-
- local bx = current_x + width
- if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end
- current_x = bx + spacing
- end
- end
-
- Elements:update_proximities()
- request_render()
-end
-
-function Controls:on_dispositions() self:reflow() end
-function Controls:on_display() self:update_dimensions() end
-function Controls:on_prop_border() self:update_dimensions() end
-function Controls:on_prop_fullormaxed() self:update_dimensions() end
-function Controls:on_timeline_enabled() self:update_dimensions() end
-
-return Controls
diff --git a/.config/mpv/scripts/uosc_shared/elements/Curtain.lua b/.config/mpv/scripts/uosc_shared/elements/Curtain.lua
deleted file mode 100644
index 99b9f14..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Curtain.lua
+++ /dev/null
@@ -1,35 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@class Curtain : Element
-local Curtain = class(Element)
-
-function Curtain:new() return Class.new(self) --[[@as Curtain]] end
-function Curtain:init()
- Element.init(self, 'curtain', {ignores_menu = true})
- self.opacity = 0
- ---@type string[]
- self.dependents = {}
-end
-
----@param id string
-function Curtain:register(id)
- self.dependents[#self.dependents + 1] = id
- if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end
-end
-
----@param id string
-function Curtain:unregister(id)
- self.dependents = itable_filter(self.dependents, function(item) return item ~= id end)
- if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end
-end
-
-function Curtain:render()
- if self.opacity == 0 or options.curtain_opacity == 0 then return end
- local ass = assdraw.ass_new()
- ass:rect(0, 0, display.width, display.height, {
- color = '000000', opacity = options.curtain_opacity * self.opacity,
- })
- return ass
-end
-
-return Curtain
diff --git a/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua b/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua
deleted file mode 100644
index 7f1c02f..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua
+++ /dev/null
@@ -1,64 +0,0 @@
-local Button = require('uosc_shared/elements/Button')
-
----@alias CycleState {value: any; icon: string; active?: boolean}
----@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
-
----@class CycleButton : Button
-local CycleButton = class(Button)
-
----@param id string
----@param props CycleButtonProps
-function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end
----@param id string
----@param props CycleButtonProps
-function CycleButton:init(id, props)
- local is_state_prop = itable_index_of({'shuffle'}, props.prop)
- self.prop = props.prop
- self.states = props.states
-
- Button.init(self, id, props)
-
- self.icon = self.states[1].icon
- self.active = self.states[1].active
- self.current_state_index = 1
- self.on_click = function()
- local new_state = self.states[self.current_state_index + 1] or self.states[1]
- local new_value = new_state.value
- if self.owner then
- mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value)
- elseif is_state_prop then
- if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end
- set_state(self.prop, new_value)
- else
- mp.set_property(self.prop, new_value)
- end
- end
-
- self.handle_change = function(name, value)
- if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end
- local index = itable_find(self.states, function(state) return state.value == value end)
- self.current_state_index = index or 1
- self.icon = self.states[self.current_state_index].icon
- self.active = self.states[self.current_state_index].active
- request_render()
- end
-
- local prop_parts = split(self.prop, '@')
- if #prop_parts == 2 then -- External prop with a script owner
- self.prop, self.owner = prop_parts[1], prop_parts[2]
- self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end
- self.handle_change(self.prop, external[self.prop])
- elseif is_state_prop then -- uosc's state props
- self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end
- self.handle_change(self.prop, state[self.prop])
- else
- mp.observe_property(self.prop, 'string', self.handle_change)
- end
-end
-
-function CycleButton:destroy()
- Button.destroy(self)
- mp.unobserve_property(self.handle_change)
-end
-
-return CycleButton
diff --git a/.config/mpv/scripts/uosc_shared/elements/Element.lua b/.config/mpv/scripts/uosc_shared/elements/Element.lua
deleted file mode 100644
index 718782d..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Element.lua
+++ /dev/null
@@ -1,149 +0,0 @@
----@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}
-
--- Base class all elements inherit from.
----@class Element : Class
-local Element = class()
-
----@param id string
----@param props? ElementProps
-function Element:init(id, props)
- self.id = id
- -- `false` means element won't be rendered, or receive events
- self.enabled = true
- -- Element coordinates
- self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0
- -- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range.
- self.proximity = 0
- -- Raw proximity in pixels.
- self.proximity_raw = infinity
- ---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility.
- self.min_visibility = 0
- ---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations
- self.forced_visibility = nil
- ---@type boolean Render this element even when menu is open.
- self.ignores_menu = false
- ---@type nil|string ID of an element from which this one should inherit visibility.
- self.anchor_id = nil
-
- if props then table_assign(self, props) end
-
- -- Flash timer
- self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
- local getTo = function() return self.proximity end
- self:tween_property('forced_visibility', 1, getTo, function()
- self.forced_visibility = nil
- end)
- end)
- self._flash_out_timer:kill()
-
- Elements:add(self)
-end
-
-function Element:destroy()
- self.destroyed = true
- Elements:remove(self)
-end
-
----@param ax number
----@param ay number
----@param bx number
----@param by number
-function Element:set_coordinates(ax, ay, bx, by)
- self.ax, self.ay, self.bx, self.by = ax, ay, bx, by
- Elements:update_proximities()
- self:maybe('on_coordinates')
-end
-
-function Element:update_proximity()
- if cursor.hidden then
- self.proximity_raw = infinity
- self.proximity = 0
- else
- local range = options.proximity_out - options.proximity_in
- self.proximity_raw = get_point_to_rectangle_proximity(cursor, self)
- self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range)
- end
-end
-
--- Decide elements visibility based on proximity and various other factors
-function Element:get_visibility()
- -- Hide when menu is open, unless this is a menu
- ---@diagnostic disable-next-line: undefined-global
- if not self.ignores_menu and Menu and Menu:is_open() then return 0 end
-
- -- Persistency
- local persist = config[self.id .. '_persistency']
- if persist and (
- (persist.audio and state.is_audio)
- or (persist.paused and state.pause)
- or (persist.video and state.is_video)
- or (persist.image and state.is_image)
- or (persist.idle and state.is_idle)
- ) then return 1 end
-
- -- Forced visibility
- if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end
-
- -- Anchor inheritance
- -- If anchor returns -1, it means all attached elements should force hide.
- local anchor = self.anchor_id and Elements[self.anchor_id]
- local anchor_visibility = anchor and anchor:get_visibility() or 0
-
- return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility)
-end
-
--- Call method if it exists
-function Element:maybe(name, ...)
- if self[name] then return self[name](self, ...) end
-end
-
--- Attach a tweening animation to this element
----@param from number
----@param to number|fun():number
----@param setter fun(value: number)
----@param factor_or_callback? number|fun()
----@param callback? fun() Called either on animation end, or when animation is killed.
-function Element:tween(from, to, setter, factor_or_callback, callback)
- self:tween_stop()
- self._kill_tween = self.enabled and tween(
- from, to, setter, factor_or_callback,
- function()
- self._kill_tween = nil
- if callback then callback() end
- end
- )
-end
-
-function Element:is_tweening() return self and self._kill_tween end
-function Element:tween_stop() self:maybe('_kill_tween') end
-
--- Animate an element property between 2 values.
----@param prop string
----@param from number
----@param to number|fun():number
----@param factor_or_callback? number|fun()
----@param callback? fun() Called either on animation end, or when animation is killed.
-function Element:tween_property(prop, from, to, factor_or_callback, callback)
- self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback)
-end
-
----@param name string
-function Element:trigger(name, ...)
- local result = self:maybe('on_' .. name, ...)
- request_render()
- return result
-end
-
--- Briefly flashes the element for `options.flash_duration` milliseconds.
--- Useful to visualize changes of volume and timeline when changed via hotkeys.
-function Element:flash()
- if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
- self:tween_stop()
- self.forced_visibility = 1
- request_render()
- self._flash_out_timer:kill()
- self._flash_out_timer:resume()
- end
-end
-
-return Element
diff --git a/.config/mpv/scripts/uosc_shared/elements/Elements.lua b/.config/mpv/scripts/uosc_shared/elements/Elements.lua
deleted file mode 100644
index fe2cd6b..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Elements.lua
+++ /dev/null
@@ -1,158 +0,0 @@
-local Elements = {itable = {}}
-
----@param element Element
-function Elements:add(element)
- if not element.id then
- msg.error('attempt to add element without "id" property')
- return
- end
-
- if self:has(element.id) then Elements:remove(element.id) end
-
- self.itable[#self.itable + 1] = element
- self[element.id] = element
-
- request_render()
-end
-
-function Elements:remove(idOrElement)
- if not idOrElement then return end
- local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement
- local element = Elements[id]
- if element then
- if not element.destroyed then element:destroy() end
- element.enabled = false
- self.itable = itable_remove(self.itable, self[id])
- self[id] = nil
- request_render()
- end
-end
-
-function Elements:update_proximities()
- local capture_mbtn_left = false
- local capture_wheel = false
- local menu_only = Elements.menu ~= nil
- local mouse_leave_elements = {}
- local mouse_enter_elements = {}
-
- -- Calculates proximities and opacities for defined elements
- for _, element in self:ipairs() do
- if element.enabled then
- local previous_proximity_raw = element.proximity_raw
-
- -- If menu is open, all other elements have to be disabled
- if menu_only then
- if element.ignores_menu then
- capture_mbtn_left = true
- capture_wheel = true
- element:update_proximity()
- else
- element.proximity_raw = infinity
- element.proximity = 0
- end
- else
- element:update_proximity()
- end
-
- -- Element has global forced key listeners
- if element.on_global_mbtn_left_down then capture_mbtn_left = true end
- if element.on_global_wheel_up or element.on_global_wheel_down then capture_wheel = true end
-
- if element.proximity_raw == 0 then
- -- Element has local forced key listeners
- if element.on_mbtn_left_down then capture_mbtn_left = true end
- if element.on_wheel_up or element.on_wheel_up then capture_wheel = true end
-
- -- Mouse entered element area
- if previous_proximity_raw ~= 0 then
- mouse_enter_elements[#mouse_enter_elements + 1] = element
- end
- else
- -- Mouse left element area
- if previous_proximity_raw == 0 then
- mouse_leave_elements[#mouse_leave_elements + 1] = element
- end
- end
- end
- end
-
- -- Enable key group captures requested by elements
- mp[capture_mbtn_left and 'enable_key_bindings' or 'disable_key_bindings']('mbtn_left')
- mp[capture_wheel and 'enable_key_bindings' or 'disable_key_bindings']('wheel')
-
- -- Trigger `mouse_leave` and `mouse_enter` events
- for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end
- for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end
-end
-
--- Toggles passed elements' min visibilities between 0 and 1.
----@param ids string[] IDs of elements to peek.
-function Elements:toggle(ids)
- local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id].min_visibility ~= 1 end)
- self:set_min_visibility(has_invisible and 1 or 0, ids)
-end
-
--- Set (animate) elements' min visibilities to passed value.
----@param visibility number 0-1 floating point.
----@param ids string[] IDs of elements to peek.
-function Elements:set_min_visibility(visibility, ids)
- for _, id in ipairs(ids) do
- local element = Elements[id]
- if element then element:tween_property('min_visibility', element.min_visibility, visibility) end
- end
-end
-
--- Flash passed elements.
----@param ids string[] IDs of elements to peek.
-function Elements:flash(ids)
- local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end)
- for _, element in ipairs(elements) do element:flash() end
-end
-
----@param name string Event name.
-function Elements:trigger(name, ...)
- for _, element in self:ipairs() do element:trigger(name, ...) end
-end
-
--- Trigger two events, `name` and `global_name`, depending on element-cursor proximity.
--- Disabled elements don't receive these events.
----@param name string Event name.
-function Elements:proximity_trigger(name, ...)
- local stop_normal, stop_global = false, false
- for i = #self.itable, 1, -1 do
- local element = self.itable[i]
- if element.enabled then
- if element.proximity_raw == 0 then
- if element:trigger(name, ...) == 'stop_propagation' then break end
- end
- if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end
- end
- end
-end
-
-function Elements:has(id) return self[id] ~= nil end
-function Elements:ipairs() return ipairs(self.itable) end
-
----@param name string Event name.
-function Elements:create_proximity_dispatcher(name)
- return function(...) self:proximity_trigger(name, ...) end
-end
-
-mp.set_key_bindings({
- {
- 'mbtn_left',
- Elements:create_proximity_dispatcher('mbtn_left_up'),
- function(...)
- update_mouse_pos(nil, mp.get_property_native('mouse-pos'), true)
- Elements:proximity_trigger('mbtn_left_down', ...)
- end,
- },
- {'mbtn_left_dbl', 'ignore'},
-}, 'mbtn_left', 'force')
-
-mp.set_key_bindings({
- {'wheel_up', Elements:create_proximity_dispatcher('wheel_up')},
- {'wheel_down', Elements:create_proximity_dispatcher('wheel_down')},
-}, 'wheel', 'force')
-
-return Elements
diff --git a/.config/mpv/scripts/uosc_shared/elements/Menu.lua b/.config/mpv/scripts/uosc_shared/elements/Menu.lua
deleted file mode 100644
index aad5016..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Menu.lua
+++ /dev/null
@@ -1,783 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
--- Menu data structure accepted by `Menu:open(menu)`.
----@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;}
----@alias MenuDataItem MenuDataValue|MenuData
----@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;}
----@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun()}
-
--- Internal data structure created from `Menu`.
----@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling}
----@alias MenuStackItem MenuStackValue|MenuStack
----@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number}
----@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean}
-
----@class Menu : Element
-local Menu = class(Element)
-
----@param data MenuData
----@param callback fun(value: any)
----@param opts? MenuOptions
-function Menu:open(data, callback, opts)
- local open_menu = self:is_open()
- if open_menu then
- open_menu.is_being_replaced = true
- open_menu:close(true)
- end
- return Menu:new(data, callback, opts)
-end
-
----@param menu_type? string
----@return Menu|nil
-function Menu:is_open(menu_type)
- return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil
-end
-
----@param immediate? boolean Close immediately without fadeout animation.
----@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed.
----@overload fun(callback: fun())
-function Menu:close(immediate, callback)
- if type(immediate) ~= 'boolean' then callback = immediate end
-
- local menu = self == Menu and Elements.menu or self
-
- if menu and not menu.destroyed then
- if menu.is_closing then
- menu:tween_stop()
- return
- end
-
- local function close()
- Elements:remove('menu')
- menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {}
- menu:disable_key_bindings()
- Elements:update_proximities()
- if callback then callback() end
- request_render()
- end
-
- menu.is_closing = true
-
- if immediate then close()
- else menu:fadeout(close) end
- end
-end
-
----@param data MenuData
----@param callback fun(value: any)
----@param opts? MenuOptions
----@return Menu
-function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end
----@param data MenuData
----@param callback fun(value: any)
----@param opts? MenuOptions
-function Menu:init(data, callback, opts)
- Element.init(self, 'menu', {ignores_menu = true})
-
- -----@type fun()
- self.callback = callback
- self.opts = opts or {}
- self.offset_x = 0 -- Used for submenu transition animation.
- self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items
- self.item_height = nil
- self.item_spacing = 1
- self.item_padding = nil
- self.font_size = nil
- self.font_size_hint = nil
- self.scroll_step = nil -- Item height + item spacing.
- self.scroll_height = nil -- Items + spacings - container height.
- self.opacity = 0 -- Used to fade in/out.
- self.type = data.type
- ---@type MenuStack Root MenuStack.
- self.root = nil
- ---@type MenuStack Current MenuStack.
- self.current = nil
- ---@type MenuStack[] All menus in a flat array.
- self.all = nil
- ---@type table<string, MenuStack> Map of submenus by their ids, such as `'Tools > Aspect ratio'`.
- self.by_id = {}
- self.key_bindings = {}
- self.is_being_replaced = false
- self.is_closing, self.is_closed = false, false
- ---@type {y: integer, time: number}[]
- self.drag_data = nil
- self.is_dragging = false
-
- self:update(data)
-
- if self.mouse_nav then
- if self.current then self.current.selected_index = nil end
- else
- for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end
- end
-
- self:tween_property('opacity', 0, 1)
- self:enable_key_bindings()
- Elements.curtain:register('menu')
- if self.opts.on_open then self.opts.on_open() end
-end
-
-function Menu:destroy()
- Element.destroy(self)
- self:disable_key_bindings()
- self.is_closed = true
- if not self.is_being_replaced then Elements.curtain:unregister('menu') end
- if self.opts.on_close then self.opts.on_close() end
-end
-
----@param data MenuData
-function Menu:update(data)
- self.type = data.type
-
- local new_root = {is_root = true}
- local new_all = {}
- local new_by_id = {}
- local menus_to_serialize = {{new_root, data}}
- local old_current_id = self.current and self.current.id
-
- table_assign(new_root, data, {'title', 'hint', 'keep_open'})
-
- local i = 0
- while i < #menus_to_serialize do
- i = i + 1
- local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2]
- local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id
- if not menu.is_root then
- menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i)
- end
- menu.icon = 'chevron_right'
-
- -- Update items
- local first_active_index = nil
- menu.items = {}
-
- for i, item_data in ipairs(menu_data.items or {}) do
- if item_data.active and not first_active_index then first_active_index = i end
-
- local item = {}
- table_assign(item, item_data, {
- 'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator',
- })
- if item.keep_open == nil then item.keep_open = menu.keep_open end
-
- -- Submenu
- if item_data.items then
- item.parent_menu = menu
- menus_to_serialize[#menus_to_serialize + 1] = {item, item_data}
- end
-
- menu.items[i] = item
- end
-
- if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end
-
- -- Retain old state
- local old_menu = self.by_id[menu.is_root and '__root__' or menu.id]
- if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end
-
- new_all[#new_all + 1] = menu
- new_by_id[menu.is_root and '__root__' or menu.id] = menu
- end
-
- self.root, self.all, self.by_id = new_root, new_all, new_by_id
- self.current = self.by_id[old_current_id] or self.root
-
- self:update_content_dimensions()
- self:reset_navigation()
-end
-
----@param items MenuDataItem[]
-function Menu:update_items(items)
- local data = table_shallow_copy(self.root)
- data.items = items
- self:update(data)
-end
-
-function Menu:update_content_dimensions()
- self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height
- self.font_size = round(self.item_height * 0.48 * options.font_scale)
- self.font_size_hint = self.font_size - 1
- self.item_padding = round((self.item_height - self.font_size) * 0.6)
- self.scroll_step = self.item_height + self.item_spacing
-
- local title_opts = {size = self.font_size, italic = false, bold = false}
- local hint_opts = {size = self.font_size_hint}
-
- for _, menu in ipairs(self.all) do
- -- Estimate width of a widest item
- local max_width = 0
- for _, item in ipairs(menu.items) do
- local icon_width = item.icon and self.font_size or 0
- item.title_width = text_width(item.title, title_opts)
- item.hint_width = text_width(item.hint, hint_opts)
- local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0)
- + (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0)
- local estimated_width = item.title_width + item.hint_width + icon_width
- + (self.item_padding * spacings_in_item)
- if estimated_width > max_width then max_width = estimated_width end
- end
-
- -- Also check menu title
- title_opts.bold, title_opts.italic = true, false
- local menu_title_width = text_width(menu.title, title_opts)
- if menu_title_width > max_width then max_width = menu_title_width end
-
- menu.max_width = max_width
- end
-
- self:update_dimensions()
-end
-
-function Menu:update_dimensions()
- -- Coordinates and sizes are of the scrollable area to make
- -- consuming values in rendering and collisions easier. Title drawn above this, so
- -- we need to account for that in max_height and ay position.
- local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width
-
- for _, menu in ipairs(self.all) do
- menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9))
- local title_height = (menu.is_root and menu.title) and self.scroll_step or 0
- local max_height = round((display.height - title_height) * 0.9)
- local content_height = self.scroll_step * #menu.items
- menu.height = math.min(content_height - self.item_spacing, max_height)
- menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5))
- menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0)
- menu.scroll_y = menu.scroll_y or 0
- self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits
- end
-
- local ax = round((display.width - self.current.width) / 2) + self.offset_x
- self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height)
-end
-
-function Menu:reset_navigation()
- local menu = self.current
-
- -- Reset indexes and scroll
- self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits
- if self.mouse_nav then
- self:select_item_below_cursor()
- else
- self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil)
- end
-
- -- Walk up the parent menu chain and activate items that lead to current menu
- local parent = menu.parent_menu
- while parent do
- parent.selected_index = itable_index_of(parent.items, menu)
- menu, parent = parent, parent.parent_menu
- end
-
- request_render()
-end
-
-function Menu:set_offset_x(offset)
- local delta = offset - self.offset_x
- self.offset_x = offset
- self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by)
-end
-
-function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end
-
-function Menu:get_item_index_below_cursor()
- local menu = self.current
- if #menu.items < 1 or self.proximity_raw > 0 then return nil end
- return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items))
-end
-
-function Menu:get_first_active_index(menu)
- menu = menu or self.current
- for index, item in ipairs(self.current.items) do
- if item.active then return index end
- end
-end
-
----@param pos? number
----@param menu? MenuStack
-function Menu:set_scroll_to(pos, menu)
- menu = menu or self.current
- menu.scroll_y = clamp(0, pos or 0, menu.scroll_height)
- request_render()
-end
-
----@param delta? number
----@param menu? MenuStack
-function Menu:set_scroll_by(delta, menu)
- menu = menu or self.current
- self:set_scroll_to(menu.scroll_y + delta, menu)
-end
-
----@param pos? number
----@param menu? MenuStack
----@param fling_options? table
-function Menu:scroll_to(pos, menu, fling_options)
- menu = menu or self.current
- menu.fling = {
- y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y),
- time = mp.get_time(), duration = 0.1, easing = ease_out_sext,
- }
- if fling_options then table_assign(menu.fling, fling_options) end
- request_render()
-end
-
----@param delta? number
----@param menu? MenuStack
----@param fling_options? Fling
-function Menu:scroll_by(delta, menu, fling_options)
- menu = menu or self.current
- self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options)
-end
-
----@param index? integer
----@param menu? MenuStack
----@param immediate? boolean
-function Menu:scroll_to_index(index, menu, immediate)
- menu = menu or self.current
- if (index and index >= 1 and index <= #menu.items) then
- local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2))
- if immediate then self:set_scroll_to(position, menu)
- else self:scroll_to(position, menu) end
- end
-end
-
----@param index? integer
----@param menu? MenuStack
-function Menu:select_index(index, menu)
- menu = menu or self.current
- menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil
- request_render()
-end
-
----@param value? any
----@param menu? MenuStack
-function Menu:select_value(value, menu)
- menu = menu or self.current
- local index = itable_find(menu.items, function(item) return item.value == value end)
- self:select_index(index, 5)
-end
-
----@param menu? MenuStack
-function Menu:deactivate_items(menu)
- menu = menu or self.current
- for _, item in ipairs(menu.items) do item.active = false end
- request_render()
-end
-
----@param index? integer
----@param menu? MenuStack
-function Menu:activate_index(index, menu)
- menu = menu or self.current
- if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end
- request_render()
-end
-
----@param index? integer
----@param menu? MenuStack
-function Menu:activate_one_index(index, menu)
- self:deactivate_items(menu)
- self:activate_index(index, menu)
-end
-
----@param value? any
----@param menu? MenuStack
-function Menu:activate_value(value, menu)
- menu = menu or self.current
- local index = itable_find(menu.items, function(item) return item.value == value end)
- self:activate_index(index, menu)
-end
-
----@param value? any
----@param menu? MenuStack
-function Menu:activate_one_value(value, menu)
- menu = menu or self.current
- local index = itable_find(menu.items, function(item) return item.value == value end)
- self:activate_one_index(index, menu)
-end
-
----@param id string
-function Menu:activate_submenu(id)
- local submenu = self.by_id[id]
- if submenu then
- self.current = submenu
- request_render()
- else
- msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id))
- end
- self:reset_navigation()
-end
-
----@param index? integer
----@param menu? MenuStack
-function Menu:delete_index(index, menu)
- menu = menu or self.current
- if (index and index >= 1 and index <= #menu.items) then
- table.remove(menu.items, index)
- self:update_content_dimensions()
- self:scroll_to_index(menu.selected_index, menu)
- end
-end
-
----@param value? any
----@param menu? MenuStack
-function Menu:delete_value(value, menu)
- menu = menu or self.current
- local index = itable_find(menu.items, function(item) return item.value == value end)
- self:delete_index(index)
-end
-
----@param menu? MenuStack
-function Menu:prev(menu)
- menu = menu or self.current
- menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1)
- self:scroll_to_index(menu.selected_index, menu, true)
-end
-
----@param menu? MenuStack
-function Menu:next(menu)
- menu = menu or self.current
- menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items)
- self:scroll_to_index(menu.selected_index, menu, true)
-end
-
-function Menu:back()
- if self.opts.on_back then
- self.opts.on_back()
- if self.is_closed then return end
- end
-
- local menu = self.current
- local parent = menu.parent_menu
-
- if parent then
- menu.selected_index = nil
- self.current = parent
- self:update_dimensions()
- self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end)
- self.opacity = 1 -- in case tween above canceled fade in animation
- else
- self:close()
- end
-end
-
----@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean}
-function Menu:open_selected_item(opts)
- opts = opts or {}
- local menu = self.current
- if menu.selected_index then
- local item = menu.items[menu.selected_index]
- -- Is submenu
- if item.items then
- self.current = item
- if opts.preselect_submenu_item then
- item.selected_index = #item.items > 0 and 1 or nil
- end
- self:update_dimensions()
- self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end)
- self.opacity = 1 -- in case tween above canceled fade in animation
- else
- self.callback(item.value)
- if not item.keep_open and not opts.keep_open then self:close() end
- end
- end
-end
-
-function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end
-function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end
-function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end
-
-function Menu:on_display() self:update_dimensions() end
-function Menu:on_prop_fullormaxed() self:update_content_dimensions() end
-
-function Menu:on_global_mbtn_left_down()
- if self.proximity_raw == 0 then
- self.drag_data = {{y = cursor.y, time = mp.get_time()}}
- self.current.fling = nil
- else
- if cursor.x < self.ax then self:back()
- else self:close() end
- end
-end
-
-function Menu:fling_distance()
- local first, last = self.drag_data[1], self.drag_data[#self.drag_data]
- if mp.get_time() - last.time > 0.05 then return 0 end
- for i = #self.drag_data - 1, 1, -1 do
- local drag = self.drag_data[i]
- if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end
- end
- return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10
-end
-
-function Menu:on_global_mbtn_left_up()
- if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then
- self:select_item_below_cursor()
- self:open_selected_item({preselect_submenu_item = false})
- end
- if self.is_dragging then
- local distance = self:fling_distance()
- if math.abs(distance) > 50 then
- self.current.fling = {
- y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time,
- easing = ease_out_quart, duration = 0.5, update_cursor = true,
- }
- end
- end
- self.is_dragging = false
- self.drag_data = nil
-end
-
-
-function Menu:on_global_mouse_move()
- self.mouse_nav = true
- if self.drag_data then
- self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10
- local distance = self.drag_data[#self.drag_data].y - cursor.y
- if distance ~= 0 then self:set_scroll_by(distance) end
- self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()}
- end
- if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor()
- else self.current.selected_index = nil end
- request_render()
-end
-
-function Menu:on_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end
-function Menu:on_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end
-
-function Menu:on_pgup()
- local menu = self.current
- local items_per_page = round((menu.height / self.scroll_step) * 0.4)
- local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page
- menu.selected_index = clamp(1, paged_index, #menu.items)
- if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
-end
-
-function Menu:on_pgdwn()
- local menu = self.current
- local items_per_page = round((menu.height / self.scroll_step) * 0.4)
- local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page
- menu.selected_index = clamp(1, paged_index, #menu.items)
- if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
-end
-
-function Menu:on_home()
- self.current.selected_index = math.min(1, #self.current.items)
- if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
-end
-
-function Menu:on_end()
- self.current.selected_index = #self.current.items
- if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
-end
-
-function Menu:add_key_binding(key, name, fn, flags)
- self.key_bindings[#self.key_bindings + 1] = name
- mp.add_forced_key_binding(key, name, fn, flags)
-end
-
-function Menu:enable_key_bindings()
- -- The `mp.set_key_bindings()` method would be easier here, but that
- -- doesn't support 'repeatable' flag, so we are stuck with this monster.
- self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable')
- self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable')
- self:add_key_binding('left', 'menu-back1', self:create_key_action('back'))
- self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect'))
- self:add_key_binding('shift+right', 'menu-select-soft1', self:create_key_action('open_selected_item_soft'))
- self:add_key_binding('shift+mbtn_left', 'menu-select-soft', self:create_key_action('open_selected_item_soft'))
- self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back'))
- self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back'))
- self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect'))
- self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect'))
- self:add_key_binding('shift+enter', 'menu-select-alt5', self:create_key_action('open_selected_item_soft'))
- self:add_key_binding('shift+kp_enter', 'menu-select-alt6', self:create_key_action('open_selected_item_soft'))
- self:add_key_binding('esc', 'menu-close', self:create_key_action('close'))
- self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable')
- self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable')
- self:add_key_binding('home', 'menu-home', self:create_key_action('on_home'))
- self:add_key_binding('end', 'menu-end', self:create_key_action('on_end'))
-end
-
-function Menu:disable_key_bindings()
- for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end
- self.key_bindings = {}
-end
-
-function Menu:create_key_action(name)
- return function(...)
- self.mouse_nav = false
- self:maybe(name, ...)
- end
-end
-
-function Menu:render()
- local update_cursor = false
- for _, menu in ipairs(self.all) do
- if menu.fling then
- update_cursor = update_cursor or menu.fling.update_cursor or false
- local time_delta = state.render_last_time - menu.fling.time
- local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1))
- self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu)
- if progress < 1 then request_render() else menu.fling = nil end
- end
- end
- if update_cursor then self:select_item_below_cursor() end
-
- local ass = assdraw.ass_new()
- local opacity = options.menu_opacity * self.opacity
- local spacing = self.item_padding
- local icon_size = self.font_size
-
- function draw_menu(menu, x, y, opacity)
- local ax, ay, bx, by = x, y, x + menu.width, y + menu.height
- local draw_title = menu.is_root and menu.title
- local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
- local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
- local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
- local selected_index = menu.selected_index or -1
- -- remove menu_opacity to start off with full opacity, but still decay for parent menus
- local text_opacity = opacity / options.menu_opacity
-
- -- Background
- ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, {
- color = bg, opacity = opacity, radius = 4,
- })
-
- for index = start_index, end_index, 1 do
- local item = menu.items[index]
- local next_item = menu.items[index + 1]
- local is_highlighted = selected_index == index or item.active
- local next_is_active = next_item and next_item.active
- local next_is_highlighted = selected_index == index + 1 or next_is_active
-
- if not item then break end
-
- local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1)
- local item_by = item_ay + self.item_height
- local item_center_y = item_ay + (self.item_height / 2)
- local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil
- local content_ax, content_bx = ax + spacing, bx - spacing
- local font_color = item.active and fgt or bgt
- local shadow_color = item.active and fg or bg
-
- -- Separator
- local separator_ay = item.separator and item_by - 1 or item_by
- local separator_by = item_by + (item.separator and 2 or 1)
- if is_highlighted then separator_ay = item_by + 1 end
- if next_is_highlighted then separator_by = item_by end
- if separator_by - separator_ay > 0 and item_by < by then
- ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, {
- color = fg, opacity = opacity * (item.separator and 0.08 or 0.06),
- })
- end
-
- -- Highlight
- local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0)
- if highlight_opacity > 0 then
- ass:rect(ax + 2, item_ay, bx - 2, item_by, {
- radius = 2, color = fg, opacity = highlight_opacity * text_opacity,
- clip = item_clip,
- })
- end
-
- -- Icon
- if item.icon then
- local x, y = content_bx - (icon_size / 2), item_center_y
- if item.icon == 'spinner' then
- ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8})
- else
- ass:icon(x, y, icon_size * 1.5, item.icon, {
- color = font_color, opacity = text_opacity, clip = item_clip,
- shadow = 1, shadow_color = shadow_color,
- })
- end
- content_bx = content_bx - icon_size - spacing
- end
-
- local title_cut_x = content_bx
- if item.hint_width > 0 then
- -- controls title & hint clipping proportional to the ratio of their widths
- local title_content_ratio = item.title_width / (item.title_width + item.hint_width)
- title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio
- + (item.title_width > 0 and spacing / 2 or 0))
- end
-
- -- Hint
- if item.hint then
- item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint)
- local clip = '\\clip(' .. title_cut_x .. ',' ..
- math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')'
- ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, {
- size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip,
- shadow = 1, shadow_color = shadow_color,
- })
- end
-
- -- Title
- if item.title then
- item.ass_safe_title = item.ass_safe_title or ass_escape(item.title)
- local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ','
- .. title_cut_x .. ',' .. math.min(item_by, by) .. ')'
- ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, {
- size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2,
- opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip,
- shadow = 1, shadow_color = shadow_color,
- })
- end
- end
-
- -- Menu title
- if draw_title then
- local title_ay = ay - self.item_height
- local title_height = self.item_height - 3
- menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title)
-
- -- Background
- ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, {
- color = fg, opacity = opacity * 0.8, radius = 2,
- })
- ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', {
- size = 80, color = bg, opacity = opacity * 0.1,
- })
-
- -- Title
- ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, {
- size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity,
- clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')',
- })
- end
-
- -- Scrollbar
- if menu.scroll_height > 0 then
- local groove_height = menu.height - 2
- local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40)
- local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
- ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8})
- end
- end
-
- -- Main menu
- draw_menu(self.current, self.ax, self.ay, opacity)
-
- -- Parent menus
- local parent_menu = self.current.parent_menu
- local parent_offset_x = self.ax
- local parent_opacity_factor = options.menu_parent_opacity
- local menu_gap = 2
-
- while parent_menu do
- parent_offset_x = parent_offset_x - parent_menu.width - menu_gap
- draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity)
- parent_opacity_factor = parent_opacity_factor * parent_opacity_factor
- parent_menu = parent_menu.parent_menu
- end
-
- -- Selected menu
- local selected_menu = self.current.items[self.current.selected_index]
-
- if selected_menu and selected_menu.items then
- draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity)
- end
-
- return ass
-end
-
-return Menu
diff --git a/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua
deleted file mode 100644
index 82a7e43..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua
+++ /dev/null
@@ -1,80 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@class PauseIndicator : Element
-local PauseIndicator = class(Element)
-
-function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end
-function PauseIndicator:init()
- Element.init(self, 'pause_indicator')
- self.ignores_menu = true
- self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
- self.paused = state.pause
- self.type = options.pause_indicator
- self.is_manual = options.pause_indicator == 'manual'
- self.fadeout_requested = false
- self.opacity = 0
-
- mp.observe_property('pause', 'bool', function(_, paused)
- if Elements.timeline.pressed then return end
- if options.pause_indicator == 'flash' then
- if self.paused == paused then return end
- self:flash()
- elseif options.pause_indicator == 'static' then
- self:decide()
- end
- end)
-end
-
-function PauseIndicator:flash()
- if not self.is_manual and self.type ~= 'flash' then return end
- -- can't wait for pause property event listener to set this, because when this is used inside a binding like:
- -- cycle pause; script-binding uosc/flash-pause-indicator
- -- the pause event is not fired fast enough, and indicator starts rendering with old icon
- self.paused = mp.get_property_native('pause')
- if self.is_manual then self.type = 'flash' end
- self.opacity = 1
- self:tween_property('opacity', 1, 0, 0.15)
-end
-
--- decides whether static indicator should be visible or not
-function PauseIndicator:decide()
- if not self.is_manual and self.type ~= 'static' then return end
- self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary
- if self.is_manual then self.type = 'static' end
- self.opacity = self.paused and 1 or 0
- request_render()
-
- -- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored.
- -- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more
- mp.add_timeout(.05, function() osd:update() end)
-end
-
-function PauseIndicator:render()
- if self.opacity == 0 then return end
-
- local ass = assdraw.ass_new()
- local is_static = self.type == 'static'
-
- -- Background fadeout
- if is_static then
- ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3})
- end
-
- -- Icon
- local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15))
- size = size + size * (1 - self.opacity)
-
- if self.paused then
- ass:icon(display.width / 2, display.height / 2, size, 'pause',
- {border = 1, opacity = self.base_icon_opacity * self.opacity}
- )
- else
- ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow',
- {border = 1, opacity = self.base_icon_opacity * self.opacity}
- )
- end
-
- return ass
-end
-
-return PauseIndicator
diff --git a/.config/mpv/scripts/uosc_shared/elements/Speed.lua b/.config/mpv/scripts/uosc_shared/elements/Speed.lua
deleted file mode 100644
index 9681ccf..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Speed.lua
+++ /dev/null
@@ -1,183 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; }
-
----@class Speed : Element
-local Speed = class(Element)
-
----@param props? ElementProps
-function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end
-function Speed:init(props)
- Element.init(self, 'speed', props)
-
- self.width = 0
- self.height = 0
- self.notches = 10
- self.notch_every = 0.1
- ---@type number
- self.notch_spacing = nil
- ---@type number
- self.font_size = nil
- ---@type Dragging|nil
- self.dragging = nil
-end
-
-function Speed:on_coordinates()
- self.height, self.width = self.by - self.ay, self.bx - self.ax
- self.notch_spacing = self.width / (self.notches + 1)
- self.font_size = round(self.height * 0.48 * options.font_scale)
-end
-
-function Speed:speed_step(speed, up)
- if options.speed_step_is_factor then
- if up then
- return speed * options.speed_step
- else
- return speed * 1 / options.speed_step
- end
- else
- if up then
- return speed + options.speed_step
- else
- return speed - options.speed_step
- end
- end
-end
-
-function Speed:on_mbtn_left_down()
- -- Don't accept clicks while hidden.
- if self:get_visibility() <= 0 then return end
- self:tween_stop() -- Stop and cleanup possible ongoing animations
- self.dragging = {
- start_time = mp.get_time(),
- start_x = cursor.x,
- distance = 0,
- speed_distance = 0,
- start_speed = state.speed,
- }
-end
-
-function Speed:on_global_mouse_move()
- if not self.dragging then return end
-
- self.dragging.distance = cursor.x - self.dragging.start_x
- self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every)
-
- local speed_current = state.speed
- local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance
- speed_drag_current = clamp(0.01, speed_drag_current, 100)
- local drag_dir_up = speed_drag_current > speed_current
-
- local speed_step_next = speed_current
- local speed_drag_diff = math.abs(speed_drag_current - speed_current)
- while math.abs(speed_step_next - speed_current) < speed_drag_diff do
- speed_step_next = self:speed_step(speed_step_next, drag_dir_up)
- end
- local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up)
-
- local speed_new = speed_step_prev
- local speed_next_diff = math.abs(speed_drag_current - speed_step_next)
- local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev)
- if speed_next_diff < speed_prev_diff then
- speed_new = speed_step_next
- end
-
- if speed_new ~= speed_current then
- mp.set_property_native('speed', speed_new)
- end
-end
-
-function Speed:on_mbtn_left_up()
- -- Reset speed on short clicks
- if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then
- mp.set_property_native('speed', 1)
- end
-end
-
-function Speed:on_global_mbtn_left_up()
- self.dragging = nil
- request_render()
-end
-
-function Speed:on_global_mouse_leave()
- self.dragging = nil
- request_render()
-end
-
-function Speed:on_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end
-function Speed:on_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end
-
-function Speed:render()
- local visibility = self:get_visibility()
- local opacity = self.dragging and 1 or visibility
-
- if opacity <= 0 then return end
-
- local ass = assdraw.ass_new()
-
- -- Background
- ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity})
-
- -- Coordinates
- local ax, ay = self.ax, self.ay
- local bx, by = self.bx, ay + self.height
- local half_width = (self.width / 2)
- local half_x = ax + half_width
-
- -- Notches
- local speed_at_center = state.speed
- if self.dragging then
- speed_at_center = self.dragging.start_speed + self.dragging.speed_distance
- speed_at_center = clamp(0.01, speed_at_center, 100)
- end
- local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every
- local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing)
- local guide_size = math.floor(self.height / 7.5)
- local notch_by = by - guide_size
- local notch_ay_big = ay + round(self.font_size * 1.1)
- local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
- local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
- local from_to_index = math.floor(self.notches / 2)
-
- for i = -from_to_index, from_to_index do
- local notch_speed = nearest_notch_speed + (i * self.notch_every)
-
- if notch_speed >= 0 and notch_speed <= 100 then
- local notch_x = nearest_notch_x + (i * self.notch_spacing)
- local notch_thickness = 1
- local notch_ay = notch_ay_small
- if (notch_speed % (self.notch_every * 10)) < 0.00000001 then
- notch_ay = notch_ay_big
- notch_thickness = 1.5
- elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then
- notch_ay = notch_ay_medium
- end
-
- ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, {
- color = fg, border = 1, border_color = bg,
- opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity,
- })
- end
- end
-
- -- Center guide
- ass:new_event()
- ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}')
- ass:opacity(opacity)
- ass:pos(0, 0)
- ass:draw_start()
- ass:move_to(half_x, by - 2 - guide_size)
- ass:line_to(half_x + guide_size, by - 2)
- ass:line_to(half_x - guide_size, by - 2)
- ass:draw_stop()
-
- -- Speed value
- local speed_text = (round(state.speed * 100) / 100) .. 'x'
- ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, {
- size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity,
- })
-
- return ass
-end
-
-return Speed
diff --git a/.config/mpv/scripts/uosc_shared/elements/Timeline.lua b/.config/mpv/scripts/uosc_shared/elements/Timeline.lua
deleted file mode 100644
index 5ef798f..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Timeline.lua
+++ /dev/null
@@ -1,394 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@class Timeline : Element
-local Timeline = class(Element)
-
-function Timeline:new() return Class.new(self) --[[@as Timeline]] end
-function Timeline:init()
- Element.init(self, 'timeline')
- self.pressed = false
- self.obstructed = false
- self.size_max = 0
- self.size_min = 0
- self.size_min_override = options.timeline_start_hidden and 0 or nil
- self.font_size = 0
- self.top_border = options.timeline_border
- self.hovered_chapter = nil
-
- -- Release any dragging when file gets unloaded
- mp.register_event('end-file', function() self.pressed = false end)
-end
-
-function Timeline:get_visibility()
- return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self))
- or Element.get_visibility(self)
-end
-
-function Timeline:decide_enabled()
- local previous = self.enabled
- self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil
- if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end
-end
-
-function Timeline:get_effective_size_min()
- return self.size_min_override or self.size_min
-end
-
-function Timeline:get_effective_size()
- if Elements.speed and Elements.speed.dragging then return self.size_max end
- local size_min = self:get_effective_size_min()
- return size_min + math.ceil((self.size_max - size_min) * self:get_visibility())
-end
-
-function Timeline:get_effective_line_width()
- return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width
-end
-
-function Timeline:get_is_hovered() return self.enabled and (self.proximity_raw == 0 or self.hovered_chapter ~= nil) end
-
-function Timeline:update_dimensions()
- if state.fullormaxed then
- self.size_min = options.timeline_size_min_fullscreen
- self.size_max = options.timeline_size_max_fullscreen
- else
- self.size_min = options.timeline_size_min
- self.size_max = options.timeline_size_max
- end
- self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale)
- self.ax = Elements.window_border.size
- self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border
- self.bx = display.width - Elements.window_border.size
- self.by = display.height - Elements.window_border.size
- self.width = self.bx - self.ax
- self.chapter_size = math.max((self.by - self.ay) / 10, 3)
- self.chapter_size_hover = self.chapter_size * 2
-
- -- Disable if not enough space
- local available_space = display.height - Elements.window_border.size * 2
- if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
- self.obstructed = available_space < self.size_max + 10
- self:decide_enabled()
-end
-
-function Timeline:get_time_at_x(x)
- local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0)
- local time_width = self.width - line_width - 1
- local fax = (time_width) * state.time / state.duration
- local fbx = fax + line_width
- -- time starts 0.5 pixels in
- x = x - self.ax - 0.5
- if x > fbx then x = x - line_width
- elseif x > fax then x = fax end
- local progress = clamp(0, x / time_width, 1)
- return state.duration * progress
-end
-
----@param fast? boolean
-function Timeline:set_from_cursor(fast)
- if state.time and state.duration then
- mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact')
- end
-end
-function Timeline:clear_thumbnail() mp.commandv('script-message-to', 'thumbfast', 'clear') end
-
-function Timeline:determine_chapter_click_handler()
- if self.hovered_chapter then
- if not self.on_global_mbtn_left_down then
- self.on_global_mbtn_left_down = function()
- if self.hovered_chapter then mp.commandv('seek', self.hovered_chapter.time, 'absolute+exact') end
- end
- end
- else
- if self.on_global_mbtn_left_down then
- self.on_global_mbtn_left_down = nil
- if self.proximity_raw ~= 0 then self:clear_thumbnail() end
- end
- end
-end
-
-function Timeline:on_mbtn_left_down()
- -- `self.on_global_mbtn_left_down` has precedent
- if self.on_global_mbtn_left_down then return end
-
- self.pressed = true
- self.pressed_pause = state.pause
- mp.set_property_native('pause', true)
- self:set_from_cursor()
-end
-function Timeline:on_prop_duration() self:decide_enabled() end
-function Timeline:on_prop_time() self:decide_enabled() end
-function Timeline:on_prop_border() self:update_dimensions() end
-function Timeline:on_prop_fullormaxed() self:update_dimensions() end
-function Timeline:on_display() self:update_dimensions() end
-function Timeline:on_mouse_leave()
- if not self.hovered_chapter then self:clear_thumbnail() end
-end
-function Timeline:on_global_mbtn_left_up()
- if self.pressed then
- mp.set_property_native('pause', self.pressed_pause)
- self.pressed = false
- end
- self:clear_thumbnail()
-end
-function Timeline:on_global_mouse_leave()
- self.pressed = false
- self:clear_thumbnail()
-end
-
-Timeline.seek_timer = mp.add_timeout(0.05, function() Elements.timeline:set_from_cursor() end)
-Timeline.seek_timer:kill()
-function Timeline:on_global_mouse_move()
- if self.pressed then
- if self.width / state.duration < 10 then
- self:set_from_cursor(true)
- self.seek_timer:kill()
- self.seek_timer:resume()
- else self:set_from_cursor() end
- end
- self:determine_chapter_click_handler()
-end
-function Timeline:on_wheel_up() mp.commandv('seek', options.timeline_step) end
-function Timeline:on_wheel_down() mp.commandv('seek', -options.timeline_step) end
-
-function Timeline:render()
- if self.size_max == 0 then return end
-
- local size_min = self:get_effective_size_min()
- local size = self:get_effective_size()
- local visibility = self:get_visibility()
-
- if size < 1 then return end
-
- local ass = assdraw.ass_new()
-
- -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
- local hide_text_below = math.max(self.font_size * 0.8, size_min * 2)
- local hide_text_ramp = hide_text_below / 2
- local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp
-
- local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4)
- local progress = state.time / state.duration
- local is_line = options.timeline_style == 'line'
-
- -- Foreground & Background bar coordinates
- local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by
- local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby
- local fcy = fay + (size / 2)
-
- local line_width = 0
-
- if is_line then
- local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1)
- local line_width_max = self:get_effective_line_width()
- local max_min_width_delta = size_min > 0
- and line_width_max - line_width_max * options.timeline_line_width_minimized_scale
- or 0
- line_width = line_width_max - (max_min_width_delta * minimized_fraction)
- fax = bax + (self.width - line_width) * progress
- fbx = fax + line_width
- line_width = line_width - 1
- else
- fax, fbx = bax, bax + self.width * progress
- end
-
- local foreground_size = fby - fay
- local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping
-
- -- time starts 0.5 pixels in
- local time_ax = bax + 0.5
- local time_width = self.width - line_width - 1
-
- -- time to x: calculates x coordinate so that it never lies inside of the line
- local function t2x(time)
- local x = time_ax + time_width * time / state.duration
- return time <= state.time and x or x + line_width
- end
-
- -- Background
- ass:new_event()
- ass:pos(0, 0)
- ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}')
- ass:opacity(options.timeline_opacity)
- ass:draw_start()
- ass:rect_cw(bax, bay, fax, bby) --left of progress
- ass:rect_cw(fbx, bay, bbx, bby) --right of progress
- ass:rect_cw(fax, bay, fbx, fay) --above progress
- ass:draw_stop()
-
- -- Progress
- ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity})
-
- -- Uncached ranges
- local buffered_time = nil
- if state.uncached_ranges then
- local opts = {size = 80, anchor_y = fby}
- local texture_char = visibility > 0 and 'b' or 'a'
- local offset = opts.size / (visibility > 0 and 24 or 28)
- for _, range in ipairs(state.uncached_ranges) do
- if not buffered_time and (range[1] > state.time or range[2] > state.time) then
- buffered_time = range[1] - state.time
- end
- if options.timeline_cache then
- local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
- local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
- opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax
- ass:texture(ax, fay, bx, fby, texture_char, opts)
- opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset
- ass:texture(ax, fay, bx, fby, texture_char, opts)
- end
- end
- end
-
- -- Custom ranges
- for _, chapter_range in ipairs(state.chapter_ranges) do
- local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start)
- local rbx = chapter_range['end'] > state.duration - 0.1 and bbx
- or t2x(math.min(chapter_range['end'], state.duration))
- ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity})
- end
-
- -- Chapters
- self.hovered_chapter = nil
- if (options.timeline_chapters_opacity > 0
- and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b)
- ) then
- local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size
- local diamond_radius_hovered = diamond_radius * 2
- local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1
-
- if diamond_radius > 0 then
- local function draw_chapter(time, radius)
- local chapter_x, chapter_y = t2x(time), fay - 1
- ass:new_event()
- ass:append(string.format(
- '{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
- diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
- ))
- ass:draw_start()
- ass:move_to(chapter_x - radius, chapter_y)
- ass:line_to(chapter_x, chapter_y - radius)
- ass:line_to(chapter_x + radius, chapter_y)
- ass:line_to(chapter_x, chapter_y + radius)
- ass:draw_stop()
- end
-
- if #state.chapters > 0 then
- -- Find hovered chapter indicator
- local hovered_chapter, closest_delta = nil, infinity
-
- if self.proximity_raw < diamond_radius_hovered then
- for i, chapter in ipairs(state.chapters) do
- local chapter_x, chapter_y = t2x(chapter.time), fay - 1
- local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2)
- if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then
- hovered_chapter, closest_delta = chapter, cursor_chapter_delta
- end
- end
- end
-
- for i, chapter in ipairs(state.chapters) do
- if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
- end
-
- -- Render hovered chapter above others
- if hovered_chapter then
- draw_chapter(hovered_chapter.time, diamond_radius_hovered)
- self.hovered_chapter = hovered_chapter
- self:determine_chapter_click_handler()
- end
- end
-
- if state.ab_loop_a and state.ab_loop_a > 0 then draw_chapter(state.ab_loop_a) end
- if state.ab_loop_b and state.ab_loop_b > 0 then draw_chapter(state.ab_loop_b) end
- end
- end
-
- local function draw_timeline_text(x, y, align, text, opts)
- opts.color, opts.border_color = fgt, fg
- opts.clip = '\\clip(' .. foreground_coordinates .. ')'
- ass:txt(x, y, align, text, opts)
- opts.color, opts.border_color = bgt, bg
- opts.clip = '\\iclip(' .. foreground_coordinates .. ')'
- ass:txt(x, y, align, text, opts)
- end
-
- -- Time values
- if text_opacity > 0 then
- local time_opts = {size = self.font_size, opacity = text_opacity, border = 2}
- -- Upcoming cache time
- if buffered_time and options.buffered_time_threshold > 0 and buffered_time < options.buffered_time_threshold then
- local x, align = fbx + 5, 4
- local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1}
- local human = round(math.max(buffered_time, 0)) .. 's'
- local width = text_width(human, cache_opts)
- local time_width = text_width('00:00:00', time_opts)
- local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width
- if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end
- draw_timeline_text(x, fcy, align, human, cache_opts)
- end
-
- -- Elapsed time
- if state.time_human then
- draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts)
- end
-
- -- End time
- if state.duration_or_remaining_time_human then
- draw_timeline_text(bbx - spacing, fcy, 6, state.duration_or_remaining_time_human, time_opts)
- end
- end
-
- -- Hovered time and chapter
- if (self.proximity_raw == 0 or self.pressed or self.hovered_chapter) and
- not (Elements.speed and Elements.speed.dragging) then
- local cursor_x = self.hovered_chapter and t2x(self.hovered_chapter.time) or cursor.x
- local hovered_seconds = self.hovered_chapter and self.hovered_chapter.time or self:get_time_at_x(cursor.x)
-
- -- Cursor line
- -- 0.5 to switch when the pixel is half filled in
- local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg
- local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby
- ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2})
- local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by}
-
- -- Timestamp
- local offset = #state.chapters > 0 and 10 or 4
- local opts = {size = self.font_size, offset = offset}
- opts.width_overwrite = text_width('00:00:00', opts)
- ass:tooltip(tooltip_anchor, format_time(hovered_seconds), opts)
- tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset
-
- -- Thumbnail
- if not thumbnail.disabled and thumbnail.width ~= 0 and thumbnail.height ~= 0 then
- local scale_x, scale_y = display.scale_x, display.scale_y
- local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y)
- local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y
- local thumb_width, thumb_height = thumbnail.width, thumbnail.height
- local thumb_x = round(clamp(
- thumb_x_margin, cursor_x * scale_x - thumb_width / 2,
- display.width * scale_x - thumb_width - thumb_x_margin
- ))
- local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height)
- local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y
- local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y
- ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2})
- mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y)
- tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay
- end
-
- -- Chapter title
- if #state.chapters > 0 then
- local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true)
- if chapter and not chapter.is_end_only then
- ass:tooltip(tooltip_anchor, chapter.title_wrapped, {
- size = self.font_size, offset = 10, responsive = false, bold = true,
- width_overwrite = chapter.title_wrapped_width * self.font_size,
- })
- end
- end
- end
-
- return ass
-end
-
-return Timeline
diff --git a/.config/mpv/scripts/uosc_shared/elements/TopBar.lua b/.config/mpv/scripts/uosc_shared/elements/TopBar.lua
deleted file mode 100644
index 977d5b8..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/TopBar.lua
+++ /dev/null
@@ -1,182 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()}
-
----@class TopBarButton : Element
-local TopBarButton = class(Element)
-
----@param id string
----@param props TopBarButtonProps
-function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end
-function TopBarButton:init(id, props)
- Element.init(self, id, props)
- self.anchor_id = 'top_bar'
- self.icon = props.icon
- self.background = props.background
- self.command = props.command
-end
-
-function TopBarButton:on_mbtn_left_down()
- mp.command(type(self.command) == 'function' and self.command() or self.command)
-end
-
-function TopBarButton:render()
- local visibility = self:get_visibility()
- if visibility <= 0 then return end
- local ass = assdraw.ass_new()
-
- -- Background on hover
- if self.proximity_raw == 0 then
- ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility})
- end
-
- local width, height = self.bx - self.ax, self.by - self.ay
- local icon_size = math.min(width, height) * 0.5
- ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, {
- opacity = visibility, border = options.text_border,
- })
-
- return ass
-end
-
---[[ TopBar ]]
-
----@class TopBar : Element
-local TopBar = class(Element)
-
-function TopBar:new() return Class.new(self) --[[@as TopBar]] end
-function TopBar:init()
- Element.init(self, 'top_bar')
- self.pressed = false
- self.size, self.size_max, self.size_min = 0, 0, 0
- self.icon_size, self.spacing, self.font_size, self.title_bx = 1, 1, 1, 1
- self.size_min_override = options.timeline_start_hidden and 0 or nil
- self.top_border = options.timeline_border
-
- local function decide_maximized_command()
- return state.border
- and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
- or 'set window-maximized no;cycle fullscreen'
- end
-
- -- Order aligns from right to left
- self.buttons = {
- TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}),
- TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = decide_maximized_command}),
- TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}),
- }
-end
-
-function TopBar:decide_enabled()
- if options.top_bar == 'no-border' then
- self.enabled = not state.border or state.fullscreen
- else
- self.enabled = options.top_bar == 'always'
- end
- self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title)
- for _, element in ipairs(self.buttons) do
- element.enabled = self.enabled and options.top_bar_controls
- end
-end
-
-function TopBar:update_dimensions()
- self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size
- self.icon_size = round(self.size * 0.5)
- self.spacing = math.ceil(self.size * 0.25)
- self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale)
- self.button_width = round(self.size * 1.15)
- self.ay = Elements.window_border.size
- self.bx = display.width - Elements.window_border.size
- self.by = self.size + Elements.window_border.size
- self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0)
- self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx
-
- local button_bx = self.bx
- for _, element in pairs(self.buttons) do
- element.ax, element.bx = button_bx - self.button_width, button_bx
- element.ay, element.by = self.ay, self.by
- button_bx = button_bx - self.button_width
- end
-end
-
-function TopBar:on_prop_border()
- self:decide_enabled()
- self:update_dimensions()
-end
-
-function TopBar:on_prop_fullscreen()
- self:decide_enabled()
- self:update_dimensions()
-end
-
-function TopBar:on_prop_maximized()
- self:decide_enabled()
- self:update_dimensions()
-end
-
-function TopBar:on_display() self:update_dimensions() end
-
-function TopBar:render()
- local visibility = self:get_visibility()
- if visibility <= 0 then return end
- local ass = assdraw.ass_new()
-
- -- Window title
- if options.top_bar_title and (state.title or state.has_playlist) then
- local bg_margin = math.floor((self.size - self.font_size) / 4)
- local padding = self.font_size / 2
- local title_ax = self.ax + bg_margin
- local title_ay = self.ay + bg_margin
- local max_bx = self.title_bx - self.spacing
-
- -- Playlist position
- if state.has_playlist then
- local text = state.playlist_pos .. '' .. state.playlist_count
- local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
- .. state.playlist_count
- local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
- local bx = round(title_ax + text_width(text, opts) + padding * 2)
- ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2})
- ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts)
- title_ax = bx + bg_margin
- end
-
- -- Title
- local text = state.title
- if max_bx - title_ax > self.font_size * 3 and text and text ~= '' then
- local opts = {
- size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility,
- clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by),
- }
- local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2)
- local by = self.by - bg_margin
- ass:rect(title_ax, title_ay, bx, by, {
- color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
- })
- ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, text, opts)
- title_ay = by + 1
- end
-
- -- Subtitle: current chapter
- if state.current_chapter and max_bx - title_ax > self.font_size * 3 then
- local font_size = self.font_size * 0.8
- local height = font_size * 1.5
- local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title
- local by = title_ay + height
- local opts = {
- size = font_size, italic = true, wrap = 2, color = bgt,
- border = 1, border_color = bg, opacity = visibility * 0.8,
- }
- local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2)
- opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
- ass:rect(title_ax, title_ay, bx, by, {
- color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
- })
- ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts)
- end
- end
-
- return ass
-end
-
-return TopBar
diff --git a/.config/mpv/scripts/uosc_shared/elements/Volume.lua b/.config/mpv/scripts/uosc_shared/elements/Volume.lua
deleted file mode 100644
index 8533ebd..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/Volume.lua
+++ /dev/null
@@ -1,240 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
---[[ MuteButton ]]
-
----@class MuteButton : Element
-local MuteButton = class(Element)
----@param props? ElementProps
-function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end
-function MuteButton:on_mbtn_left_down() mp.commandv('cycle', 'mute') end
-function MuteButton:render()
- local visibility = self:get_visibility()
- if visibility <= 0 then return end
- local ass = assdraw.ass_new()
- local icon_name = state.mute and 'volume_off' or 'volume_up'
- local width = self.bx - self.ax
- ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name,
- {border = options.text_border, opacity = options.volume_opacity * visibility, align = 2}
- )
- return ass
-end
-
---[[ VolumeSlider ]]
-
----@class VolumeSlider : Element
-local VolumeSlider = class(Element)
----@param props? ElementProps
-function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end
-function VolumeSlider:init(props)
- Element.init(self, 'volume_slider', props)
- self.pressed = false
- self.nudge_y = 0 -- vertical position where volume overflows 100
- self.nudge_size = 0
- self.draw_nudge = false
- self.spacing = 0
- self.radius = 1
-end
-
-function VolumeSlider:set_volume(volume)
- volume = round(volume / options.volume_step) * options.volume_step
- if state.volume == volume then return end
- mp.commandv('set', 'volume', clamp(0, volume, state.volume_max))
-end
-
-function VolumeSlider:set_from_cursor()
- local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border)
- self:set_volume(volume_fraction * state.volume_max)
-end
-
-function VolumeSlider:on_coordinates()
- if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end
- local width = self.bx - self.ax
- self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max))
- self.nudge_size = round(width * 0.18)
- self.draw_nudge = self.ay < self.nudge_y
- self.spacing = round(width * 0.2)
- self.radius = math.max(2, (self.bx - self.ax) / 10)
-end
-function VolumeSlider:on_mbtn_left_down()
- self.pressed = true
- self:set_from_cursor()
-end
-function VolumeSlider:on_global_mbtn_left_up() self.pressed = false end
-function VolumeSlider:on_global_mouse_leave() self.pressed = false end
-function VolumeSlider:on_global_mouse_move()
- if self.pressed then self:set_from_cursor() end
-end
-function VolumeSlider:on_wheel_up() self:set_volume(state.volume + options.volume_step) end
-function VolumeSlider:on_wheel_down() self:set_volume(state.volume - options.volume_step) end
-
-function VolumeSlider:render()
- local visibility = self:get_visibility()
- local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by
- local width, height = bx - ax, by - ay
-
- if width <= 0 or height <= 0 or visibility <= 0 then return end
-
- local ass = assdraw.ass_new()
- local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -infinity, self.nudge_size
- local volume_y = self.ay + options.volume_border +
- ((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
-
- -- Draws a rectangle with nudge at requested position
- ---@param p number Padding from slider edges.
- ---@param cy? number A y coordinate where to clip the path from the bottom.
- function create_nudged_path(p, cy)
- cy = cy or ay + p
- local ax, bx, by = ax + p, bx - p, by - p
- local r = math.max(1, self.radius - p)
- local d, rh = r * 2, r / 2
- local nudge_size = ((quarter_pi_sin * (nudge_size - p)) + p) / quarter_pi_sin
- local path = assdraw.ass_new()
- path:move_to(bx - r, by)
- path:line_to(ax + r, by)
- if cy > by - d then
- local subtracted_radius = (d - (cy - (by - d))) / 2
- local xbd = (r - subtracted_radius * 1.35) -- x bezier delta
- path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy)
- path:line_to(bx - r, cy)
- path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by)
- else
- path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r)
- local nudge_bottom_y = nudge_y + nudge_size
-
- if cy + rh <= nudge_bottom_y then
- path:line_to(ax, nudge_bottom_y)
- if cy <= nudge_y then
- path:line_to((ax + nudge_size), nudge_y)
- local nudge_top_y = nudge_y - nudge_size
- if cy <= nudge_top_y then
- local r, rh = r, rh
- if cy > nudge_top_y - r then
- r = nudge_top_y - cy
- rh = r / 2
- end
- path:line_to(ax, nudge_top_y)
- path:line_to(ax, cy + r)
- path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
- path:line_to(bx - r, cy)
- path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
- path:line_to(bx, nudge_top_y)
- else
- local triangle_side = cy - nudge_top_y
- path:line_to((ax + triangle_side), cy)
- path:line_to((bx - triangle_side), cy)
- end
- path:line_to((bx - nudge_size), nudge_y)
- else
- local triangle_side = nudge_bottom_y - cy
- path:line_to((ax + triangle_side), cy)
- path:line_to((bx - triangle_side), cy)
- end
- path:line_to(bx, nudge_bottom_y)
- else
- path:line_to(ax, cy + r)
- path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
- path:line_to(bx - r, cy)
- path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
- end
- path:line_to(bx, by - r)
- path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by)
- end
- return path
- end
-
- -- BG & FG paths
- local bg_path = create_nudged_path(0)
- local fg_path = create_nudged_path(options.volume_border, volume_y)
-
- -- Background
- ass:new_event()
- ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg ..
- '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}')
- ass:opacity(options.volume_opacity, visibility)
- ass:pos(0, 0)
- ass:draw_start()
- ass:append(bg_path.text)
- ass:draw_stop()
-
- -- Foreground
- ass:new_event()
- ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}')
- ass:opacity(options.volume_opacity, visibility)
- ass:pos(0, 0)
- ass:draw_start()
- ass:append(fg_path.text)
- ass:draw_stop()
-
- -- Current volume value
- local volume_string = tostring(round(state.volume * 10) / 10)
- local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale)
- if volume_y < self.by - self.spacing then
- ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
- size = font_size, color = fgt, opacity = visibility,
- clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
- })
- end
- if volume_y > self.by - self.spacing - font_size then
- ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
- size = font_size, color = bgt, opacity = visibility,
- clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
- })
- end
-
- -- Disabled stripes for no audio
- if not state.has_audio then
- local fg_100_path = create_nudged_path(options.volume_border)
- local texture_opts = {
- size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax,
- clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')',
- }
- ass:texture(ax, ay, bx, by, 'a', texture_opts)
- texture_opts.color = '000000'
- texture_opts.anchor_x = ax + texture_opts.size / 28
- ass:texture(ax, ay, bx, by, 'a', texture_opts)
- end
-
- return ass
-end
-
---[[ Volume ]]
-
----@class Volume : Element
-local Volume = class(Element)
-
-function Volume:new() return Class.new(self) --[[@as Volume]] end
-function Volume:init()
- Element.init(self, 'volume')
- self.mute = MuteButton:new({anchor_id = 'volume'})
- self.slider = VolumeSlider:new({anchor_id = 'volume'})
-end
-
-function Volume:get_visibility()
- return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self)
-end
-
-function Volume:update_dimensions()
- local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size
- local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar
- local min_y = top_bar.enabled and top_bar.by or 0
- local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay)
- or display.height - top_bar.size
- local available_height = max_y - min_y
- local max_height = available_height * 0.8
- local height = round(math.min(width * 8, max_height))
- self.enabled = height > width * 2 -- don't render if too small
- local margin = (width / 2) + Elements.window_border.size
- self.ax = round(options.volume == 'left' and margin or display.width - margin - width)
- self.ay = min_y + round((available_height - height) / 2)
- self.bx = round(self.ax + width)
- self.by = round(self.ay + height)
- self.mute.enabled, self.slider.enabled = self.enabled, self.enabled
- self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by)
- self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay)
-end
-
-function Volume:on_display() self:update_dimensions() end
-function Volume:on_prop_border() self:update_dimensions() end
-function Volume:on_controls_reflow() self:update_dimensions() end
-
-return Volume
diff --git a/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua b/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua
deleted file mode 100644
index c5544f5..0000000
--- a/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua
+++ /dev/null
@@ -1,33 +0,0 @@
-local Element = require('uosc_shared/elements/Element')
-
----@class WindowBorder : Element
-local WindowBorder = class(Element)
-
-function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end
-function WindowBorder:init()
- Element.init(self, 'window_border')
- self.ignores_menu = true
- self.size = 0
-end
-
-function WindowBorder:decide_enabled()
- self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border
- self.size = self.enabled and options.window_border_size or 0
-end
-
-function WindowBorder:on_prop_border() self:decide_enabled() end
-function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end
-
-function WindowBorder:render()
- if self.size > 0 then
- local ass = assdraw.ass_new()
- local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' ..
- (display.width - self.size) .. ',' .. (display.height - self.size) .. ')'
- ass:rect(0, 0, display.width + 1, display.height + 1, {
- color = bg, clip = clip, opacity = options.window_border_opacity,
- })
- return ass
- end
-end
-
-return WindowBorder