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.
|
||||
# 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:
|
||||
# `{icon}` - parameter used to specify an icon name (example: `face`)
|
||||
# - 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:
|
||||
# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `stream-quality`,
|
||||
# `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
|
||||
# `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
|
||||
# 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:
|
||||
# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility
|
||||
# - `{disposition}` can be one of:
|
||||
@@ -68,9 +63,19 @@ timeline_chapters_opacity=0.8
|
||||
# - `<stream>stream-quality` - show stream quality button only for streams
|
||||
# - `<has_audio,!audio>audio` - show audio tracks button for all files that have
|
||||
# an audio track, but are not exclusively audio only files
|
||||
# Item tooltip:
|
||||
# Place `?Tooltip text` after the element config to give it a tooltip.
|
||||
# Place `#{badge}` after the element params to give it a badge. Available badges:
|
||||
# `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 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_size=32
|
||||
controls_size_fullscreen=40
|
||||
|
@@ -541,7 +541,7 @@ end
|
||||
function text_length(text)
|
||||
if not text or text == '' then return 0 end
|
||||
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
|
||||
end
|
||||
|
||||
@@ -2382,7 +2382,7 @@ end
|
||||
|
||||
--[[ 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
|
||||
local Button = class(Element)
|
||||
@@ -2390,10 +2390,13 @@ 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 options.foreground
|
||||
self.background = props.background or options.background
|
||||
---@type fun()
|
||||
@@ -2431,11 +2434,34 @@ function Button:render()
|
||||
-- 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_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
|
||||
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,
|
||||
opacity = visibility, clip = icon_clip,
|
||||
})
|
||||
|
||||
return ass
|
||||
@@ -2452,6 +2478,8 @@ 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)
|
||||
self.prop = props.prop
|
||||
self.states = props.states
|
||||
@@ -3048,21 +3076,23 @@ function Controls:init()
|
||||
Element.init(self, 'controls')
|
||||
---@type ControlItem[]
|
||||
self.controls = {}
|
||||
---@type fun()[]
|
||||
self.disposers = {}
|
||||
self:serialize()
|
||||
end
|
||||
|
||||
function Controls:serialize()
|
||||
local shorthands = {
|
||||
menu = 'command:menu:script-binding uosc/menu?Menu',
|
||||
subtitles = 'command:subtitles:script-binding uosc/subtitles?Subtitles',
|
||||
audio = 'command:graphic_eq:script-binding uosc/audio?Audio',
|
||||
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub?Subtitles',
|
||||
audio = 'command:graphic_eq:script-binding uosc/audio#audio?Audio',
|
||||
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
|
||||
video = 'command:theaters:script-binding uosc/video?Video',
|
||||
playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
|
||||
chapters = 'command:bookmarks:script-binding uosc/chapters?Chapters',
|
||||
['stream-quality'] = 'command:deblur:script-binding uosc/stream-quality?Stream quality',
|
||||
video = 'command:theaters:script-binding uosc/video#video?Video',
|
||||
playlist = 'command:list_alt:script-binding uosc/playlist#playlist?Playlist',
|
||||
chapters = 'command:bookmarks:script-binding uosc/chapters#chapters?Chapters',
|
||||
['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',
|
||||
['items'] = 'command:list_alt:script-binding uosc/items#playlist?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',
|
||||
@@ -3112,6 +3142,9 @@ function Controls:serialize()
|
||||
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)
|
||||
|
||||
@@ -3130,8 +3163,7 @@ function Controls:serialize()
|
||||
elseif kind == 'command' then
|
||||
if #params ~= 2 then
|
||||
mp.error(string.format(
|
||||
'command button needs 2 parameters, %d received: %s',
|
||||
#params, table.concat(params, '/')
|
||||
'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/')
|
||||
))
|
||||
else
|
||||
local element = Button:new('control_' .. i, {
|
||||
@@ -3139,10 +3171,12 @@ function Controls:serialize()
|
||||
anchor_id = 'controls',
|
||||
on_click = function() mp.command(params[2]) end,
|
||||
tooltip = tooltip,
|
||||
count_prop = 'sub',
|
||||
})
|
||||
self.controls[#self.controls + 1] = {
|
||||
kind = kind, 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
|
||||
@@ -3171,6 +3205,7 @@ function Controls:serialize()
|
||||
self.controls[#self.controls + 1] = {
|
||||
kind = kind, 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
|
||||
@@ -3194,10 +3229,36 @@ function Controls:clean_controls()
|
||||
for _, control in ipairs(self.controls) do
|
||||
if control.element then Elements:remove(control.element) end
|
||||
end
|
||||
for _, disposer in ipairs(self.disposers) do disposer() end
|
||||
self.controls = {}
|
||||
request_render()
|
||||
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()
|
||||
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
|
||||
|
Reference in New Issue
Block a user