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:
tomasklaen
2022-09-19 14:47:14 +02:00
parent b610b6cda5
commit c8f4f22d15
2 changed files with 87 additions and 21 deletions

View File

@@ -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

View File

@@ -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