From 12ebcf3cd3e6a0fd61df6e1f52b6df400ab82dad Mon Sep 17 00:00:00 2001 From: tomasklaen Date: Thu, 12 Oct 2023 17:29:32 +0200 Subject: [PATCH] feat: added `disable_elements` option and `disable-elements` script message Allows disabling elements and various indicators by adding their IDs to the list: ```conf disable_elements=timeline,audio_indicator ``` Also includes a new script message listener `disable-elements`, that does the same thing: ```lua local id = mp.get_script_name() mp.commandv('script-message-to', 'uosc', 'disable-elements', id, 'timeline,audio_indicator') ``` It'll register what elements each script wants disabled. The element will be enabled only when it is not disabled by neither user nor any script. To cancel or re-enable the elements, just pass an empty list: ```lua mp.commandv('script-message-to', 'uosc', 'disable-elements', id, '') ``` ref #686, closes #592 --- README.md | 10 +++ script-opts/uosc.conf | 5 ++ scripts/uosc/elements/BufferingIndicator.lua | 3 +- scripts/uosc/elements/Controls.lua | 35 ++++++--- scripts/uosc/elements/Curtain.lua | 2 +- scripts/uosc/elements/CycleButton.lua | 19 ++--- scripts/uosc/elements/Element.lua | 35 ++++++++- scripts/uosc/elements/Elements.lua | 33 ++++++-- scripts/uosc/elements/Menu.lua | 11 ++- scripts/uosc/elements/PauseIndicator.lua | 4 +- scripts/uosc/elements/Timeline.lua | 25 +++--- scripts/uosc/elements/TopBar.lua | 30 +++++-- scripts/uosc/elements/Volume.lua | 83 +++++++++++--------- scripts/uosc/elements/WindowBorder.lua | 3 +- scripts/uosc/lib/std.lua | 53 +++++++++++-- scripts/uosc/lib/utils.lua | 2 +- scripts/uosc/main.lua | 74 +++++++++++++---- 17 files changed, 306 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 3cafbfde..e4fa13d8 100644 --- a/README.md +++ b/README.md @@ -651,6 +651,16 @@ mp.commandv('script-message-to', 'uosc', 'overwrite-binding', 'stream-quality', To cancel the overwrite and return to default behavior, just omit the `` parameter. +### `disable-elements ` + +Set what uosc elements your script wants to disable. To cancel or re-enable them, send the message again with an empty string in place of `element_ids`. + +```lua +mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(), 'timeline,volume') +``` + +Using `'user'` as `script_id` will overwrite user's `disable_elements` config. Elements will be enabled only when neither user, nor any script requested them to be disabled. + ## Why _uosc_? It stood for micro osc as it used to render just a couple rectangles before it grew to what it is today. And now it means a minimalist UI design direction where everything is out of your way until needed. diff --git a/script-opts/uosc.conf b/script-opts/uosc.conf index 74711cc7..bd69ae2e 100644 --- a/script-opts/uosc.conf +++ b/script-opts/uosc.conf @@ -226,3 +226,8 @@ chapter_range_patterns=openings:オープニング;endings:エンディング # `slang` is a keyword to inherit values from `--slang` mpv config. # Supports paths to custom json files: `languages=~~/custom.json,slang,en` languages=slang,en + +# A comma separated list of element IDs to disable. Available IDs: +# window_border, top_bar, timeline, controls, volume, +# audio_indicator, buffering_indicator, pause_indicator +disable_elements= diff --git a/scripts/uosc/elements/BufferingIndicator.lua b/scripts/uosc/elements/BufferingIndicator.lua index 8344774b..b6065606 100644 --- a/scripts/uosc/elements/BufferingIndicator.lua +++ b/scripts/uosc/elements/BufferingIndicator.lua @@ -5,9 +5,10 @@ local BufferingIndicator = class(Element) function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end function BufferingIndicator:init() - Element.init(self, 'buffer_indicator') + Element.init(self, 'buffer_indicator', {render_order = 2}) self.ignores_menu = true self.enabled = false + self:decide_enabled() end function BufferingIndicator:decide_enabled() diff --git a/scripts/uosc/elements/Controls.lua b/scripts/uosc/elements/Controls.lua index 5ec4df3d..495fa390 100644 --- a/scripts/uosc/elements/Controls.lua +++ b/scripts/uosc/elements/Controls.lua @@ -13,7 +13,7 @@ local Controls = class(Element) function Controls:new() return Class.new(self) --[[@as Controls]] end function Controls:init() - Element.init(self, 'controls') + Element.init(self, 'controls', {render_order = 6}) ---@type ControlItem[] All control elements serialized from `options.controls`. self.controls = {} ---@type ControlItem[] Only controls that match current dispositions. @@ -22,6 +22,11 @@ function Controls:init() self:init_options() end +function Controls:destroy() + self:destroy_elements() + Element.destroy(self) +end + function Controls:init_options() -- Serialize control elements local shorthands = { @@ -109,6 +114,7 @@ function Controls:init_options() )) else local element = Button:new('control_' .. i, { + render_order = self.render_order, icon = params[1], anchor_id = 'controls', on_click = function() mp.command(params[2]) end, @@ -140,14 +146,18 @@ function Controls:init_options() end local element = CycleButton:new('control_' .. i, { - prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip, + render_order = self.render_order, + 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'}) + local element = Speed:new({anchor_id = 'controls', render_order = self.render_order}) local scale = tonumber(params[1]) or 1.3 table_assign(control, { element = element, sizing = 'dynamic', scale = scale, ratio = 3.5, ratio_min = 2, @@ -215,24 +225,23 @@ function Controls:register_badge_updater(badge, element) 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 + else self:observe_mp_property(observable_name, handler) end end function Controls:get_visibility() - return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered() + return Elements:ev('speed', 'dragging') and 1 or Elements:maybe('timeline', 'get_is_hovered') and -1 or Element.get_visibility(self) end function Controls:update_dimensions() - local window_border = Elements.window_border.size + local window_border = Elements:ev('window_border', 'size', 0) local size = round(options.controls_size * state.scale) local spacing = round(options.controls_spacing * state.scale) local margin = round(options.controls_margin * state.scale) -- 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 end + local available_space = display.height - window_border * 2 - Elements:ev('top_bar', 'size', 0) + - Elements:ev('timeline', 'size', 0) self.enabled = available_space > size + 10 -- Reset hide/enabled flags @@ -245,7 +254,7 @@ function Controls:update_dimensions() -- Container self.bx = display.width - window_border - margin - self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin + self.by = Elements:ev('timeline', 'ay', display.height - window_border) - margin self.ax, self.ay = window_border + margin, self.by - size -- Controls @@ -332,10 +341,14 @@ function Controls:on_prop_title_bar() self:update_dimensions() end function Controls:on_prop_fullormaxed() self:update_dimensions() end function Controls:on_timeline_enabled() self:update_dimensions() end -function Controls:on_options() +function Controls:destroy_elements() for _, control in ipairs(self.controls) do if control.element then control.element:destroy() end end +end + +function Controls:on_options() + self:destroy_elements() self:init_options() end diff --git a/scripts/uosc/elements/Curtain.lua b/scripts/uosc/elements/Curtain.lua index 0d997a3e..63f06100 100644 --- a/scripts/uosc/elements/Curtain.lua +++ b/scripts/uosc/elements/Curtain.lua @@ -5,7 +5,7 @@ local Curtain = class(Element) function Curtain:new() return Class.new(self) --[[@as Curtain]] end function Curtain:init() - Element.init(self, 'curtain', {ignores_menu = true}) + Element.init(self, 'curtain', {ignores_menu = true, render_order = 999}) self.opacity = 0 ---@type string[] self.dependents = {} diff --git a/scripts/uosc/elements/CycleButton.lua b/scripts/uosc/elements/CycleButton.lua index e3bb4a67..60598494 100644 --- a/scripts/uosc/elements/CycleButton.lua +++ b/scripts/uosc/elements/CycleButton.lua @@ -34,8 +34,8 @@ function CycleButton:init(id, props) 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 function handle_change(name, value) + value = type(value) == 'boolean' and (value and 'yes' or 'no') or tostring(value or '') 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 @@ -46,19 +46,14 @@ function CycleButton:init(id, props) 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]) + self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end + 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]) + self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end + handle_change(self.prop, state[self.prop]) else - mp.observe_property(self.prop, 'string', self.handle_change) + self:observe_mp_property(self.prop, handle_change) end end -function CycleButton:destroy() - Button.destroy(self) - mp.unobserve_property(self.handle_change) -end - return CycleButton diff --git a/scripts/uosc/elements/Element.lua b/scripts/uosc/elements/Element.lua index d382373e..883c89e9 100644 --- a/scripts/uosc/elements/Element.lua +++ b/scripts/uosc/elements/Element.lua @@ -1,4 +1,4 @@ ----@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;} +---@alias ElementProps {enabled?: boolean; render_order?: number; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;} -- Base class all elements inherit from. ---@class Element : Class @@ -8,6 +8,7 @@ local Element = class() ---@param props? ElementProps function Element:init(id, props) self.id = id + self.render_order = 1 -- `false` means element won't be rendered, or receive events self.enabled = true -- Element coordinates @@ -24,6 +25,8 @@ function Element:init(id, props) self.ignores_menu = false ---@type nil|string ID of an element from which this one should inherit visibility. self.anchor_id = nil + ---@type fun()[] Disposer functions called when element is destroyed. + self._disposers = {} if props then table_assign(self, props) end @@ -40,6 +43,7 @@ function Element:init(id, props) end function Element:destroy() + for _, disposer in ipairs(self._disposers) do disposer() end self.destroyed = true Elements:remove(self) end @@ -70,7 +74,10 @@ function Element:is_persistent() local persist = config[self.id .. '_persistency'] return persist and ( (persist.audio and state.is_audio) - or (persist.paused and state.pause and (not Elements.timeline.pressed or Elements.timeline.pressed.pause)) + or ( + persist.paused and state.pause + and (not Elements.timeline or not Elements.timeline.pressed or Elements.timeline.pressed.pause) + ) or (persist.video and state.is_video) or (persist.image and state.is_image) or (persist.idle and state.is_idle) @@ -154,4 +161,28 @@ function Element:flash() end end +-- Register disposer to be called when element is destroyed. +---@param disposer fun() +function Element:register_disposer(disposer) + if not itable_index_of(self._disposers, disposer) then + self._disposers[#self._disposers + 1] = disposer + end +end + +-- Automatically registers disposer for the passed callback. +---@param event string +---@param callback fun() +function Element:register_mp_event(event, callback) + mp.register_event(event, callback) + self:register_disposer(function() mp.unregister_event(callback) end) +end + +-- Automatically registers disposer for the observer. +---@param name string +---@param callback fun(name: string, value: any) +function Element:observe_mp_property(name, callback) + mp.observe_property(name, 'native', callback) + self:register_disposer(function() mp.unobserve_property(callback) end) +end + return Element diff --git a/scripts/uosc/elements/Elements.lua b/scripts/uosc/elements/Elements.lua index fc1cc55f..b928ac92 100644 --- a/scripts/uosc/elements/Elements.lua +++ b/scripts/uosc/elements/Elements.lua @@ -1,4 +1,4 @@ -local Elements = {itable = {}} +local Elements = {_all = {}} ---@param element Element function Elements:add(element) @@ -9,9 +9,12 @@ function Elements:add(element) if self:has(element.id) then Elements:remove(element.id) end - self.itable[#self.itable + 1] = element + self._all[#self._all + 1] = element self[element.id] = element + -- Sort by render order + table.sort(self._all, function(a, b) return a.render_order < b.render_order end) + request_render() end @@ -22,7 +25,7 @@ function Elements:remove(idOrElement) if element then if not element.destroyed then element:destroy() end element.enabled = false - self.itable = itable_delete_value(self.itable, self[id]) + self._all = itable_delete_value(self._all, self[id]) self[id] = nil request_render() end @@ -95,7 +98,7 @@ 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) + local elements = itable_filter(self._all, function(element) return itable_index_of(ids, element.id) ~= nil end) for _, element in ipairs(elements) do element:flash() end end @@ -108,8 +111,8 @@ end -- Disabled elements don't receive these events. ---@param name string Event name. function Elements:proximity_trigger(name, ...) - for i = #self.itable, 1, -1 do - local element = self.itable[i] + for i = #self._all, 1, -1 do + local element = self._all[i] if element.enabled then if element.proximity_raw == 0 then if element:trigger(name, ...) == 'stop_propagation' then break end @@ -119,7 +122,23 @@ function Elements:proximity_trigger(name, ...) end end +-- Returns a property of an element with a passed `id` if it exists, with an optional fallback. +---@param id string +---@param prop string +---@param fallback any +function Elements:ev(id, prop, fallback) + if self[id] and self[id].enabled and self[id][prop] ~= nil then return self[id][prop] end + return fallback +end + +-- Calls a method on an element with passed `id` if it exists. +---@param id string +---@param method string +function Elements:maybe(id, method, ...) + if self[id] then return self[id]:maybe(method, ...) end +end + function Elements:has(id) return self[id] ~= nil end -function Elements:ipairs() return ipairs(self.itable) end +function Elements:ipairs() return ipairs(self._all) end return Elements diff --git a/scripts/uosc/elements/Menu.lua b/scripts/uosc/elements/Menu.lua index bbfce839..8902be64 100644 --- a/scripts/uosc/elements/Menu.lua +++ b/scripts/uosc/elements/Menu.lua @@ -78,7 +78,7 @@ function Menu:new(data, callback, opts) return Class.new(self, data, callback, o ---@param callback MenuCallback ---@param opts? MenuOptions function Menu:init(data, callback, opts) - Element.init(self, 'menu', {ignores_menu = true}) + Element.init(self, 'menu', {ignores_menu = true, render_order = 1000}) -----@type fun() self.callback = callback @@ -130,7 +130,7 @@ function Menu:init(data, callback, opts) self:tween_property('opacity', 0, 1) self:enable_key_bindings() - Elements.curtain:register('menu') + Elements:maybe('curtain', 'register', 'menu') if self.opts.on_open then self.opts.on_open() end end @@ -138,7 +138,7 @@ 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 not self.is_being_replaced then Elements:maybe('curtain', 'unregister', 'menu') end if utils.shared_script_property_set then utils.shared_script_property_set('uosc-menu-type', nil) end @@ -969,11 +969,14 @@ function Menu:disable_key_bindings() self.key_bindings = {} end +-- Check if menu is not closed or closing. +function Menu:is_alive() return not self.is_closing and not self.is_closed end + -- Wraps a function so that it won't run if menu is closing or closed. ---@param fn function() function Menu:create_action(fn) return function(...) - if not self.is_closing and not self.is_closed then fn(...) end + if self:is_alive() then fn(...) end end end diff --git a/scripts/uosc/elements/PauseIndicator.lua b/scripts/uosc/elements/PauseIndicator.lua index 22c03624..deb4e44e 100644 --- a/scripts/uosc/elements/PauseIndicator.lua +++ b/scripts/uosc/elements/PauseIndicator.lua @@ -5,7 +5,7 @@ local PauseIndicator = class(Element) function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end function PauseIndicator:init() - Element.init(self, 'pause_indicator') + Element.init(self, 'pause_indicator', {render_order = 3}) self.ignores_menu = true self.paused = state.pause self.fadeout_requested = false @@ -44,7 +44,7 @@ function PauseIndicator:decide() end function PauseIndicator:on_prop_pause() - if Elements.timeline.pressed then return end + if Elements:ev('timeline', 'pressed') then return end if options.pause_indicator == 'flash' then if self.paused == state.pause then return end self:flash() diff --git a/scripts/uosc/elements/Timeline.lua b/scripts/uosc/elements/Timeline.lua index c7f1a105..60985116 100644 --- a/scripts/uosc/elements/Timeline.lua +++ b/scripts/uosc/elements/Timeline.lua @@ -5,7 +5,7 @@ local Timeline = class(Element) function Timeline:new() return Class.new(self) --[[@as Timeline]] end function Timeline:init() - Element.init(self, 'timeline') + Element.init(self, 'timeline', {render_order = 5}) ---@type false|{pause: boolean, distance: number, last: {x: number, y: number}} self.pressed = false self.obstructed = false @@ -19,14 +19,14 @@ function Timeline:init() self.has_thumbnail = false self:decide_progress_size() + self:update_dimensions() -- Release any dragging when file gets unloaded - mp.register_event('end-file', function() self.pressed = false end) + self:register_mp_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) + return math.max(Elements:ev('controls', 'proximity', 0), Element.get_visibility(self)) end function Timeline:decide_enabled() @@ -36,7 +36,7 @@ function Timeline:decide_enabled() end function Timeline:get_effective_size() - if Elements.speed and Elements.speed.dragging then return self.size end + if Elements:ev('speed', 'dragging') then return self.size end return self.progress_size + math.ceil((self.size - self.progress_size) * self:get_visibility()) end @@ -48,17 +48,17 @@ function Timeline:update_dimensions() self.line_width = round(options.timeline_line_width * state.scale) self.progress_line_width = round(options.progress_line_width * state.scale) self.font_size = math.floor(math.min((self.size + 60) * 0.2, self.size * 0.96) * options.font_scale) - self.ax = Elements.window_border.size - self.ay = display.height - Elements.window_border.size - self.size - self.top_border - self.bx = display.width - Elements.window_border.size - self.by = display.height - Elements.window_border.size + local window_border_size = Elements:ev('window_border', 'size', 0) + self.ax = window_border_size + self.ay = display.height - window_border_size - self.size - self.top_border + self.bx = display.width - window_border_size + self.by = display.height - 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 + local available_space = display.height - window_border_size * 2 - Elements:ev('top_bar', 'size', 0) self.obstructed = available_space < self.size + 10 self:decide_enabled() end @@ -375,8 +375,7 @@ function Timeline:render() -- Hovered time and chapter local rendered_thumbnail = false - if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and - not (Elements.speed and Elements.speed.dragging) then + if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and not Elements:ev('speed', 'dragging') then local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x) diff --git a/scripts/uosc/elements/TopBar.lua b/scripts/uosc/elements/TopBar.lua index f8326859..e8c4f877 100644 --- a/scripts/uosc/elements/TopBar.lua +++ b/scripts/uosc/elements/TopBar.lua @@ -47,7 +47,7 @@ local TopBar = class(Element) function TopBar:new() return Class.new(self) --[[@as TopBar]] end function TopBar:init() - Element.init(self, 'top_bar') + Element.init(self, 'top_bar', {render_order = 4}) self.size = 0 self.icon_size, self.spacing, self.font_size, self.title_bx, self.title_by = 1, 1, 1, 1, 1 self.show_alt_title = false @@ -61,14 +61,27 @@ function TopBar:init() -- 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 = get_maximized_command}), - TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}), + TopBarButton:new('tb_close', { + icon = 'close', background = '2311e8', command = 'quit', render_order = self.render_order + }), + TopBarButton:new('tb_max', { + icon = 'crop_square', background = '222222', command = get_maximized_command, + render_order = self.render_order + }), + TopBarButton:new('tb_min', { + icon = 'minimize', background = '222222', command = 'cycle window-minimized', + render_order = self.render_order + }), } self:decide_titles() end +function TopBar:destroy() + for _, button in ipairs(self.buttons) do button:destroy() end + Element.destroy(self) +end + function TopBar:decide_enabled() if options.top_bar == 'no-border' then self.enabled = not state.border or state.title_bar == false or state.fullscreen @@ -117,11 +130,12 @@ function TopBar:update_dimensions() 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 + local window_border_size = Elements:ev('window_border', 'size', 0) + self.ay = window_border_size + self.bx = display.width - window_border_size + self.by = self.size + 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 ~= 'no' or state.has_playlist) and Elements.window_border.size or self.title_bx + self.ax = (options.top_bar_title ~= 'no' or state.has_playlist) and window_border_size or self.title_bx local button_bx = self.bx for _, element in pairs(self.buttons) do diff --git a/scripts/uosc/elements/Volume.lua b/scripts/uosc/elements/Volume.lua index 5d2c7766..b1446afe 100644 --- a/scripts/uosc/elements/Volume.lua +++ b/scripts/uosc/elements/Volume.lua @@ -1,27 +1,5 @@ local Element = require('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:get_visibility() return Elements.volume:get_visibility(self) end -function MuteButton:render() - local visibility = self:get_visibility() - if visibility <= 0 then return end - if self.proximity_raw == 0 then - cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end - 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 * state.scale, opacity = visibility, align = 2} - ) - return ass -end - --[[ VolumeSlider ]] ---@class VolumeSlider : Element @@ -222,33 +200,40 @@ 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'}) + Element.init(self, 'volume', {render_order = 7}) + self.size = 0 + self.mute_ay = 0 + self.slider = VolumeSlider:new({anchor_id = 'volume', render_order = self.render_order}) + self:update_dimensions() +end + +function Volume:destroy() + self.slider:destroy() + Element.destroy(self) end function Volume:get_visibility() - return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self) + return self.slider.pressed and 1 or Elements:maybe('timeline', 'get_is_hovered') and -1 + or Element.get_visibility(self) end function Volume:update_dimensions() - local width = round(options.volume_size * state.scale) - 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 + self.size = round(options.volume_size * state.scale) + local min_y = Elements:ev('top_bar', 'by', 0) + local max_y = Elements:ev('controls', 'ay') or Elements:ev('timeline', 'ay') + or display.height - Elements:ev('top_bar', 'size', 0) 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) + local height = round(math.min(self.size * 8, max_height)) + self.enabled = height > self.size * 2 -- don't render if too small + local margin = (self.size / 2) + Elements:ev('window_border', 'size', 0) + self.ax = round(options.volume == 'left' and margin or display.width - margin - self.size) self.ay = min_y + round((available_height - height) / 2) - self.bx = round(self.ax + width) + self.bx = round(self.ax + self.size) 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) + self.mute_ay = self.by - self.size + self.slider.enabled = self.enabled + self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute_ay) end function Volume:on_display() self:update_dimensions() end @@ -257,4 +242,24 @@ function Volume:on_prop_title_bar() self:update_dimensions() end function Volume:on_controls_reflow() self:update_dimensions() end function Volume:on_options() self:update_dimensions() end +function Volume:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + + -- Mute button + local mute_rect = {ax = self.ax, ay = self.mute_ay, bx = self.bx, by = self.by} + if get_point_to_rectangle_proximity(cursor, mute_rect) == 0 then + cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end + end + local ass = assdraw.ass_new() + local icon_name = state.mute and 'volume_off' or 'volume_up' + local width_half = (mute_rect.bx - mute_rect.ax) / 2 + local height_half = (mute_rect.by - mute_rect.ay) / 2 + local icon_size = math.min(width_half, height_half) * 1.5 + ass:icon(mute_rect.ax + width_half, mute_rect.ay + height_half, icon_size, icon_name, + {border = options.text_border * state.scale, opacity = visibility, align = 5} + ) + return ass +end + return Volume diff --git a/scripts/uosc/elements/WindowBorder.lua b/scripts/uosc/elements/WindowBorder.lua index 6a13d596..5e046426 100644 --- a/scripts/uosc/elements/WindowBorder.lua +++ b/scripts/uosc/elements/WindowBorder.lua @@ -5,9 +5,10 @@ local WindowBorder = class(Element) function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end function WindowBorder:init() - Element.init(self, 'window_border') + Element.init(self, 'window_border', {render_order = 1}) self.ignores_menu = true self.size = 0 + self:decide_enabled() end function WindowBorder:decide_enabled() diff --git a/scripts/uosc/lib/std.lua b/scripts/uosc/lib/std.lua index 37871da8..485eb87c 100644 --- a/scripts/uosc/lib/std.lua +++ b/scripts/uosc/lib/std.lua @@ -52,6 +52,16 @@ function split(str, pattern) return list end +-- Handles common option and message inputs that need to be split by comma when strings. +---@param input string|string[]|nil +---@return string[] +function comma_split(input) + if not input then return {} end + if type(input) == 'table' then return itable_map(input, tostring) end + local str = tostring(input) + return str:match('^%s*$') and {} or split(str, ' *, *') +end + -- Get index of the last appearance of `sub` in `str`. ---@param str string ---@param sub string @@ -146,13 +156,28 @@ function itable_slice(itable, start_pos, end_pos) end ---@generic T ----@param a T[]|nil ----@param b T[]|nil +---@param ...T[]|nil +---@return T[] +function itable_join(...) + local result = {} + for _, table in ipairs({...}) do + if table then for _, value in ipairs(table) do result[#result + 1] = value end end + end + return result +end + +---@generic T +---@param ...T[]|nil ---@return T[] -function itable_join(a, b) +function itable_join_unique(...) local result = {} - if a then for _, value in ipairs(a) do result[#result + 1] = value end end - if b then for _, value in ipairs(b) do result[#result + 1] = value end end + for _, table in ipairs({...}) do + if table then + for _, value in ipairs(table) do + if not itable_index_of(result, value) then result[#result + 1] = value end + end + end + end return result end @@ -163,6 +188,24 @@ function itable_append(target, source) return target end +---@generic T +---@param input table +---@return T[] +function table_keys(input) + local keys = {} + for key, _ in pairs(input) do keys[#keys + 1] = key end + return keys +end + +---@generic T +---@param input table +---@return T[] +function table_values(input) + local values = {} + for _, value in pairs(input) do values[#values + 1] = value end + return values +end + ---@param target any[] ---@param source any[] ---@param props? string[] diff --git a/scripts/uosc/lib/utils.lua b/scripts/uosc/lib/utils.lua index 9d0915d5..96ef9467 100644 --- a/scripts/uosc/lib/utils.lua +++ b/scripts/uosc/lib/utils.lua @@ -659,7 +659,7 @@ function render() local ass = assdraw.ass_new() -- Audio indicator - if state.is_audio and not state.has_image then + if state.is_audio and not state.has_image and not Manager.disabled.audio_indicator then local smaller_side = math.min(display.width, display.height) ass:icon(display.width / 2, display.height / 2, smaller_side / 3, 'graphic_eq', {color = fg, opacity = 0.5}) end diff --git a/scripts/uosc/main.lua b/scripts/uosc/main.lua index e91c4b08..7aa6aa49 100644 --- a/scripts/uosc/main.lua +++ b/scripts/uosc/main.lua @@ -97,10 +97,12 @@ defaults = { chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80', chapter_range_patterns = 'openings:オープニング;endings:エンディング', languages = 'slang,en', + disable_elements = '', } options = table_shallow_copy(defaults) opt.read_options(options, 'uosc', function(_) update_human_times() + Manager:disable('user', options.disable_elements) Elements:trigger('options') Elements:update_proximities() request_render() @@ -900,9 +902,9 @@ bind_command('flash-top-bar', function() Elements:flash({'top_bar'}) end) bind_command('flash-volume', function() Elements:flash({'volume'}) end) bind_command('flash-speed', function() Elements:flash({'speed'}) end) bind_command('flash-pause-indicator', function() Elements:flash({'pause_indicator'}) end) -bind_command('toggle-progress', function() Elements.timeline:toggle_progress() end) -bind_command('toggle-title', function() Elements.top_bar:toggle_title() end) -bind_command('decide-pause-indicator', function() Elements.pause_indicator:decide() end) +bind_command('toggle-progress', function() Elements:maybe('timeline', 'toggle_progress') end) +bind_command('toggle-title', function() Elements:maybe('top_bar', 'toggle_title') end) +bind_command('decide-pause-indicator', function() Elements:maybe('pause_indicator', 'decide') end) bind_command('menu', function() toggle_menu_with_items() end) bind_command('menu-blurred', function() toggle_menu_with_items({mouse_nav = true}) end) bind_command('inputs', function() @@ -1090,13 +1092,14 @@ bind_command('open-file', function() end -- Update active file in directory navigation menu + local menu = nil local function handle_file_loaded() - if Menu:is_open('open-file') then - Elements.menu:activate_one_value(normalize_path(mp.get_property_native('path'))) + if menu and menu:is_alive() then + menu:activate_one_value(normalize_path(mp.get_property_native('path'))) end end - open_file_navigation_menu( + menu = open_file_navigation_menu( directory, function(path) mp.commandv('loadfile', path) end, { @@ -1141,7 +1144,7 @@ bind_command('delete-file-next', function() local is_local_file = state.path and not is_protocol(state.path) if is_local_file then - if Menu:is_open('open-file') then Elements.menu:delete_value(state.path) end + if Menu:is_open('open-file') then Elements:maybe('menu', 'delete_value', state.path) end end if state.has_playlist then @@ -1266,14 +1269,57 @@ mp.register_script_message('set-min-visibility', function(visibility, elements) end) mp.register_script_message('flash-elements', function(elements) Elements:flash(split(elements, ' *, *')) end) mp.register_script_message('overwrite-binding', function(name, command) key_binding_overwrites[name] = command end) +mp.register_script_message('disable-elements', function(id, elements) Manager:disable(id, elements) end) --[[ ELEMENTS ]] -require('elements/WindowBorder'):new() -require('elements/BufferingIndicator'):new() -require('elements/PauseIndicator'):new() -require('elements/TopBar'):new() -require('elements/Timeline'):new() -if options.controls and options.controls ~= 'never' then require('elements/Controls'):new() end -if itable_index_of({'left', 'right'}, options.volume) then require('elements/Volume'):new() end +-- Dynamic elements +local constructors = { + window_border = require('elements/WindowBorder'), + buffering_indicator = require('elements/BufferingIndicator'), + pause_indicator = require('elements/PauseIndicator'), + top_bar = require('elements/TopBar'), + timeline = require('elements/Timeline'), + controls = options.controls and options.controls ~= 'never' and require('elements/Controls'), + volume = itable_index_of({'left', 'right'}, options.volume) and require('elements/Volume'), +} + +Manager = { + -- Managed disable-able element IDs + _ids = itable_join(table_keys(constructors), {'audio_indicator'}), + ---@type table A map of manager id and a list of element ids it disables + _disabled_by = {}, + ---@type table + disabled = {} +} + +---@param manager_id string +---@param element_ids string|string[]|nil `foo,bar` or `{'foo', 'bar'}`. +function Manager:disable(manager_id, element_ids) + self._disabled_by[manager_id] = comma_split(element_ids) + self:_commit() +end + +function Manager:_commit() + local disabled_ids = itable_join_unique(table.unpack(table_values(self._disabled_by))) + + -- Create as needed + for _, id in ipairs(self._ids) do + local constructor = constructors[id] + self.disabled[id] = itable_index_of(disabled_ids, id) ~= nil + if not self.disabled[id] then + if not Elements:has(id) and constructor then constructor:new() end + else + Elements:maybe(id, 'destroy') + end + end + + -- We use `on_display` event to tell elements to update their dimensions + Elements:trigger('display') +end + +-- Initial commit +Manager:disable('user', options.disable_elements) + +-- Required elements require('elements/Curtain'):new()