feat: added counters and other badges to control buttons
Implements control element `#{badge}` syntax. Example: ``` command:subtitles:script-binding uosc/subtitles#sub?Subtitles ``` ref #212
This commit is contained in:
@@ -27,11 +27,11 @@ timeline_chapters_opacity=0.8
|
|||||||
|
|
||||||
# A comma delimited list of items to construct the controls bar above the timeline. Set to `never` to disable.
|
# A comma delimited list of items to construct the controls bar above the timeline. Set to `never` to disable.
|
||||||
# Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional
|
# Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional
|
||||||
# Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][?{tooltip}]`
|
# Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][#{badge}][?{tooltip}]`
|
||||||
# Common properties:
|
# Common properties:
|
||||||
# `{icon}` - parameter used to specify an icon name (example: `face`)
|
# `{icon}` - parameter used to specify an icon name (example: `face`)
|
||||||
# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons
|
# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons
|
||||||
# Available `{element}`s and their parameters:
|
# `{element}`s and their parameters:
|
||||||
# `{usoc_command}` - preconfigured shorthands for uosc commands that make sense to have as buttons:
|
# `{usoc_command}` - preconfigured shorthands for uosc commands that make sense to have as buttons:
|
||||||
# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `stream-quality`,
|
# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `stream-quality`,
|
||||||
# `open-file`, `items`, `next`, `prev`, `first`, `last`, `audio-device`
|
# `open-file`, `items`, `next`, `prev`, `first`, `last`, `audio-device`
|
||||||
@@ -47,11 +47,6 @@ timeline_chapters_opacity=0.8
|
|||||||
# `gap[:{scale}]` - display an empty gap, {scale} - factor of controls_size, default: 0.3
|
# `gap[:{scale}]` - display an empty gap, {scale} - factor of controls_size, default: 0.3
|
||||||
# `space` - fills all available space between previous and next item, useful to align items to the right
|
# `space` - fills all available space between previous and next item, useful to align items to the right
|
||||||
# - multiple spaces divide the available space among themselves, which can be used for centering
|
# - multiple spaces divide the available space among themselves, which can be used for centering
|
||||||
# Example implementations of some of the shorthands:
|
|
||||||
# - menu: `command:menu:script-binding uosc/menu`
|
|
||||||
# - fullscreen: `cycle:fullscreen:fullscreen:no/yes=fullscreen_exit!`
|
|
||||||
# - loop-playlist: `cycle:repeat:loop-playlist:no/inf!`
|
|
||||||
# - `toggle:{icon}:{prop}`: `cycle:{icon}:{prop}:no/yes!`
|
|
||||||
# Item visibility control:
|
# Item visibility control:
|
||||||
# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility
|
# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility
|
||||||
# - `{disposition}` can be one of:
|
# - `{disposition}` can be one of:
|
||||||
@@ -68,9 +63,19 @@ timeline_chapters_opacity=0.8
|
|||||||
# - `<stream>stream-quality` - show stream quality button only for streams
|
# - `<stream>stream-quality` - show stream quality button only for streams
|
||||||
# - `<has_audio,!audio>audio` - show audio tracks button for all files that have
|
# - `<has_audio,!audio>audio` - show audio tracks button for all files that have
|
||||||
# an audio track, but are not exclusively audio only files
|
# an audio track, but are not exclusively audio only files
|
||||||
# Item tooltip:
|
# Place `#{badge}` after the element params to give it a badge. Available badges:
|
||||||
# Place `?Tooltip text` after the element config to give it a tooltip.
|
# `sub`, `audio`, `video` - track type counters
|
||||||
|
# `playlist` - playlist counter that hides when there's only 1 item
|
||||||
|
# `{mpv_prop}` - any mpv prop that makes sense to you: https://mpv.io/manual/master/#property-list
|
||||||
|
# - if prop value is an array it'll display its size
|
||||||
|
# Place `?{tooltip}` after the element config to give it a tooltip.
|
||||||
# Example: `<stream>stream-quality?Stream quality`
|
# Example: `<stream>stream-quality?Stream quality`
|
||||||
|
# Example implementations of some of the available shorthands:
|
||||||
|
# menu = command:menu:script-binding uosc/menu?Menu
|
||||||
|
# subtitles = command:subtitles:script-binding uosc/subtitles#sub?Subtitles
|
||||||
|
# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen
|
||||||
|
# loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist
|
||||||
|
# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes!
|
||||||
controls=menu,gap,subtitles,<has_audio,!audio>audio,<stream>stream-quality,gap,loop-playlist,loop-file,space,speed,space,prev,items,next,shuffle,gap:1,fullscreen
|
controls=menu,gap,subtitles,<has_audio,!audio>audio,<stream>stream-quality,gap,loop-playlist,loop-file,space,speed,space,prev,items,next,shuffle,gap:1,fullscreen
|
||||||
controls_size=32
|
controls_size=32
|
||||||
controls_size_fullscreen=40
|
controls_size_fullscreen=40
|
||||||
|
@@ -541,7 +541,7 @@ end
|
|||||||
function text_length(text)
|
function text_length(text)
|
||||||
if not text or text == '' then return 0 end
|
if not text or text == '' then return 0 end
|
||||||
local text_length = 0
|
local text_length = 0
|
||||||
for _, _, length in utf8_iter(text) do text_length = text_length + length end
|
for _, _, length in utf8_iter(tostring(text)) do text_length = text_length + length end
|
||||||
return text_length
|
return text_length
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2382,7 +2382,7 @@ end
|
|||||||
|
|
||||||
--[[ Button ]]
|
--[[ Button ]]
|
||||||
|
|
||||||
---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; foreground?: string; background?: string; tooltip?: string}
|
---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
|
||||||
|
|
||||||
---@class Button : Element
|
---@class Button : Element
|
||||||
local Button = class(Element)
|
local Button = class(Element)
|
||||||
@@ -2390,10 +2390,13 @@ local Button = class(Element)
|
|||||||
---@param id string
|
---@param id string
|
||||||
---@param props ButtonProps
|
---@param props ButtonProps
|
||||||
function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end
|
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)
|
function Button:init(id, props)
|
||||||
self.icon = props.icon
|
self.icon = props.icon
|
||||||
self.active = props.active
|
self.active = props.active
|
||||||
self.tooltip = props.tooltip
|
self.tooltip = props.tooltip
|
||||||
|
self.badge = props.badge
|
||||||
self.foreground = props.foreground or options.foreground
|
self.foreground = props.foreground or options.foreground
|
||||||
self.background = props.background or options.background
|
self.background = props.background or options.background
|
||||||
---@type fun()
|
---@type fun()
|
||||||
@@ -2431,11 +2434,34 @@ function Button:render()
|
|||||||
-- Tooltip on hover
|
-- Tooltip on hover
|
||||||
if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end
|
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_width = text_width_estimate(self.badge, badge_font_size)
|
||||||
|
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, {
|
||||||
|
size = badge_font_size, color = background, opacity = visibility,
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
-- Icon
|
||||||
local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2)
|
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, {
|
ass:icon(x, y, self.font_size, self.icon, {
|
||||||
color = foreground, border = self.active and 0 or options.text_border, border_color = background,
|
color = foreground, border = self.active and 0 or options.text_border, border_color = background,
|
||||||
opacity = visibility,
|
opacity = visibility, clip = icon_clip,
|
||||||
})
|
})
|
||||||
|
|
||||||
return ass
|
return ass
|
||||||
@@ -2452,6 +2478,8 @@ local CycleButton = class(Button)
|
|||||||
---@param id string
|
---@param id string
|
||||||
---@param props CycleButtonProps
|
---@param props CycleButtonProps
|
||||||
function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end
|
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)
|
function CycleButton:init(id, props)
|
||||||
self.prop = props.prop
|
self.prop = props.prop
|
||||||
self.states = props.states
|
self.states = props.states
|
||||||
@@ -3048,21 +3076,23 @@ function Controls:init()
|
|||||||
Element.init(self, 'controls')
|
Element.init(self, 'controls')
|
||||||
---@type ControlItem[]
|
---@type ControlItem[]
|
||||||
self.controls = {}
|
self.controls = {}
|
||||||
|
---@type fun()[]
|
||||||
|
self.disposers = {}
|
||||||
self:serialize()
|
self:serialize()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Controls:serialize()
|
function Controls:serialize()
|
||||||
local shorthands = {
|
local shorthands = {
|
||||||
menu = 'command:menu:script-binding uosc/menu?Menu',
|
menu = 'command:menu:script-binding uosc/menu?Menu',
|
||||||
subtitles = 'command:subtitles:script-binding uosc/subtitles?Subtitles',
|
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub?Subtitles',
|
||||||
audio = 'command:graphic_eq:script-binding uosc/audio?Audio',
|
audio = 'command:graphic_eq:script-binding uosc/audio#audio?Audio',
|
||||||
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
|
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
|
||||||
video = 'command:theaters:script-binding uosc/video?Video',
|
video = 'command:theaters:script-binding uosc/video#video?Video',
|
||||||
playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
|
playlist = 'command:list_alt:script-binding uosc/playlist#playlist?Playlist',
|
||||||
chapters = 'command:bookmarks:script-binding uosc/chapters?Chapters',
|
chapters = 'command:bookmarks:script-binding uosc/chapters#chapters?Chapters',
|
||||||
['stream-quality'] = 'command:deblur:script-binding uosc/stream-quality?Stream quality',
|
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality',
|
||||||
['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
|
['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
|
||||||
['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files',
|
['items'] = 'command:list_alt:script-binding uosc/items#playlist?Playlist/Files',
|
||||||
prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
|
prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
|
||||||
next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
|
next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
|
||||||
first = 'command:first_page:script-binding uosc/first?First',
|
first = 'command:first_page:script-binding uosc/first?First',
|
||||||
@@ -3112,6 +3142,9 @@ function Controls:serialize()
|
|||||||
local tooltip = config_tooltip[2]
|
local tooltip = config_tooltip[2]
|
||||||
config = shorthands[config_tooltip[1]]
|
config = shorthands[config_tooltip[1]]
|
||||||
and split(shorthands[config_tooltip[1]], ' *%? *')[1] or 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 parts = split(config, ' *: *')
|
||||||
local kind, params = parts[1], itable_slice(parts, 2)
|
local kind, params = parts[1], itable_slice(parts, 2)
|
||||||
|
|
||||||
@@ -3130,8 +3163,7 @@ function Controls:serialize()
|
|||||||
elseif kind == 'command' then
|
elseif kind == 'command' then
|
||||||
if #params ~= 2 then
|
if #params ~= 2 then
|
||||||
mp.error(string.format(
|
mp.error(string.format(
|
||||||
'command button needs 2 parameters, %d received: %s',
|
'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/')
|
||||||
#params, table.concat(params, '/')
|
|
||||||
))
|
))
|
||||||
else
|
else
|
||||||
local element = Button:new('control_' .. i, {
|
local element = Button:new('control_' .. i, {
|
||||||
@@ -3139,10 +3171,12 @@ function Controls:serialize()
|
|||||||
anchor_id = 'controls',
|
anchor_id = 'controls',
|
||||||
on_click = function() mp.command(params[2]) end,
|
on_click = function() mp.command(params[2]) end,
|
||||||
tooltip = tooltip,
|
tooltip = tooltip,
|
||||||
|
count_prop = 'sub',
|
||||||
})
|
})
|
||||||
self.controls[#self.controls + 1] = {
|
self.controls[#self.controls + 1] = {
|
||||||
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,
|
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,
|
||||||
}
|
}
|
||||||
|
if badge then self:register_badge_updater(badge, element) end
|
||||||
end
|
end
|
||||||
elseif kind == 'cycle' then
|
elseif kind == 'cycle' then
|
||||||
if #params ~= 3 then
|
if #params ~= 3 then
|
||||||
@@ -3171,6 +3205,7 @@ function Controls:serialize()
|
|||||||
self.controls[#self.controls + 1] = {
|
self.controls[#self.controls + 1] = {
|
||||||
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,
|
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,
|
||||||
}
|
}
|
||||||
|
if badge then self:register_badge_updater(badge, element) end
|
||||||
end
|
end
|
||||||
elseif kind == 'speed' then
|
elseif kind == 'speed' then
|
||||||
if not Elements.speed then
|
if not Elements.speed then
|
||||||
@@ -3194,10 +3229,36 @@ function Controls:clean_controls()
|
|||||||
for _, control in ipairs(self.controls) do
|
for _, control in ipairs(self.controls) do
|
||||||
if control.element then Elements:remove(control.element) end
|
if control.element then Elements:remove(control.element) end
|
||||||
end
|
end
|
||||||
|
for _, disposer in ipairs(self.disposers) do disposer() end
|
||||||
self.controls = {}
|
self.controls = {}
|
||||||
request_render()
|
request_render()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param prop string
|
||||||
|
---@param element Element An element that supports `badge` property.
|
||||||
|
function Controls:register_badge_updater(prop, element)
|
||||||
|
local observable_name, serializer = prop, nil
|
||||||
|
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
|
||||||
|
elseif prop == 'playlist' then
|
||||||
|
observable_name = 'playlist-count'
|
||||||
|
serializer = function(count) return count and count > 1 and count or nil end
|
||||||
|
else
|
||||||
|
serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end
|
||||||
|
end
|
||||||
|
local function handler(_, value)
|
||||||
|
element.badge = serializer(value)
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
mp.observe_property(observable_name, 'native', handler)
|
||||||
|
self.disposers[#self.disposers + 1] = function() mp.unobserve_property(handler) end
|
||||||
|
end
|
||||||
|
|
||||||
function Controls:get_visibility()
|
function Controls:get_visibility()
|
||||||
local timeline_is_hovered = Elements.timeline.enabled and Elements.timeline.proximity_raw == 0
|
local timeline_is_hovered = Elements.timeline.enabled and Elements.timeline.proximity_raw == 0
|
||||||
return (Elements.speed and Elements.speed.dragging) and 1 or timeline_is_hovered
|
return (Elements.speed and Elements.speed.dragging) and 1 or timeline_is_hovered
|
||||||
|
Reference in New Issue
Block a user