diff options
Diffstat (limited to '.config/mpv/scripts/uosc_shared/lib')
-rw-r--r-- | .config/mpv/scripts/uosc_shared/lib/ass.lua | 170 | ||||
-rw-r--r-- | .config/mpv/scripts/uosc_shared/lib/menus.lua | 282 | ||||
-rw-r--r-- | .config/mpv/scripts/uosc_shared/lib/std.lua | 181 | ||||
-rw-r--r-- | .config/mpv/scripts/uosc_shared/lib/text.lua | 421 | ||||
-rw-r--r-- | .config/mpv/scripts/uosc_shared/lib/utils.lua | 545 |
5 files changed, 0 insertions, 1599 deletions
diff --git a/.config/mpv/scripts/uosc_shared/lib/ass.lua b/.config/mpv/scripts/uosc_shared/lib/ass.lua deleted file mode 100644 index 108953f..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/ass.lua +++ /dev/null @@ -1,170 +0,0 @@ ---[[ ASSDRAW EXTENSIONS ]] - -local ass_mt = getmetatable(assdraw.ass_new()) - --- Opacity. ----@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities. ----@param fraction? number Optionally adjust the above opacity by this fraction. -function ass_mt:opacity(opacity, fraction) - fraction = fraction ~= nil and fraction or 1 - if type(opacity) == 'number' then - self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction)) - else - self.text = self.text .. string.format( - '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', - opacity_to_alpha((opacity[1] or 0) * fraction), - opacity_to_alpha((opacity[2] or 0) * fraction), - opacity_to_alpha((opacity[3] or 0) * fraction), - opacity_to_alpha((opacity[4] or 0) * fraction) - ) - end -end - --- Icon. ----@param x number ----@param y number ----@param size number ----@param name string ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number} -function ass_mt:icon(x, y, size, name, opts) - opts = opts or {} - opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false - self:txt(x, y, opts.align or 5, name, opts) -end - --- Text. --- Named `txt` because `ass.text` is a value. ----@param x number ----@param y number ----@param align number ----@param value string|number ----@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string} -function ass_mt:txt(x, y, align, value, opts) - local border_size = opts.border or 0 - local shadow_size = opts.shadow or 0 - local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0' - -- font - tags = tags .. '\\fn' .. (opts.font or config.font) - -- font size - tags = tags .. '\\fs' .. opts.size - -- bold - if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end - -- italic - if opts.italic then tags = tags .. '\\i1' end - -- rotate - if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end - -- wrap - if opts.wrap then tags = tags .. '\\q' .. opts.wrap end - -- border - tags = tags .. '\\bord' .. border_size - -- shadow - tags = tags .. '\\shad' .. shadow_size - -- colors - tags = tags .. '\\1c&H' .. (opts.color or bgt) - if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end - if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end - -- opacity - if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end - -- clip - if opts.clip then tags = tags .. opts.clip end - -- render - self:new_event() - self.text = self.text .. '{' .. tags .. '}' .. value -end - --- Tooltip. ----@param element {ax: number; ay: number; bx: number; by: number} ----@param value string|number ----@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean} -function ass_mt:tooltip(element, value, opts) - opts = opts or {} - opts.size = opts.size or 16 - opts.border = options.text_border - opts.border_color = bg - local offset = opts.offset or opts.size / 2 - local align_top = opts.responsive == false or element.ay - offset > opts.size * 2 - local x = element.ax + (element.bx - element.ax) / 2 - local y = align_top and element.ay - offset or element.by + offset - local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10 - self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts) -end - --- Rectangle. ----@param ax number ----@param ay number ----@param bx number ----@param by number ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number} -function ass_mt:rect(ax, ay, bx, by, opts) - opts = opts or {} - local border_size = opts.border or 0 - local tags = '\\pos(0,0)\\rDefault\\an7\\blur0' - -- border - tags = tags .. '\\bord' .. border_size - -- colors - tags = tags .. '\\1c&H' .. (opts.color or fg) - if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end - -- opacity - if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end - if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end - -- clip - if opts.clip then - tags = tags .. opts.clip - end - -- draw - self:new_event() - self.text = self.text .. '{' .. tags .. '}' - self:draw_start() - if opts.radius then - self:round_rect_cw(ax, ay, bx, by, opts.radius) - else - self:rect_cw(ax, ay, bx, by) - end - self:draw_stop() -end - --- Circle. ----@param x number ----@param y number ----@param radius number ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string} -function ass_mt:circle(x, y, radius, opts) - opts = opts or {} - opts.radius = radius - self:rect(x - radius, y - radius, x + radius, y + radius, opts) -end - --- Texture. ----@param ax number ----@param ay number ----@param bx number ----@param by number ----@param char string Texture font character. ----@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number} -function ass_mt:texture(ax, ay, bx, by, char, opts) - opts = opts or {} - local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay - local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')') - local tile_size, opacity = opts.size or 100, opts.opacity or 0.2 - local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size - local width, height = bx - x, by - y - local line = string.rep(char, math.ceil((width / tile_size))) - local lines = '' - for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end - self:txt( - x, y, 7, lines, - {font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip}) -end - --- Rotating spinner icon. ----@param x number ----@param y number ----@param size number ----@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;} -function ass_mt:spinner(x, y, size, opts) - opts = opts or {} - opts.rotate = (state.render_last_time * 1.75 % 1) * -360 - opts.color = opts.color or fg - self:icon(x, y, size, 'autorenew', opts) - request_render() -end diff --git a/.config/mpv/scripts/uosc_shared/lib/menus.lua b/.config/mpv/scripts/uosc_shared/lib/menus.lua deleted file mode 100644 index e2a4ccc..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/menus.lua +++ /dev/null @@ -1,282 +0,0 @@ ----@param data MenuData ----@param opts? {submenu?: string; mouse_nav?: boolean} -function open_command_menu(data, opts) - local menu = Menu:open(data, function(value) - if type(value) == 'string' then - mp.command(value) - else - ---@diagnostic disable-next-line: deprecated - mp.commandv(unpack(value)) - end - end, opts) - if opts and opts.submenu then menu:activate_submenu(opts.submenu) end - return menu -end - ----@param opts? {submenu?: string; mouse_nav?: boolean} -function toggle_menu_with_items(opts) - if Menu:is_open('menu') then Menu:close() - else open_command_menu({type = 'menu', items = config.menu_items}, opts) end -end - ----@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any)} -function create_self_updating_menu_opener(options) - return function() - if Menu:is_open(options.type) then Menu:close() return end - local list = mp.get_property_native(options.list_prop) - local active = options.active_prop and mp.get_property_native(options.active_prop) or nil - local menu - - local function update() menu:update_items(options.serializer(list, active)) end - - local ignore_initial_list = true - local function handle_list_prop_change(name, value) - if ignore_initial_list then ignore_initial_list = false - else list = value update() end - end - - local ignore_initial_active = true - local function handle_active_prop_change(name, value) - if ignore_initial_active then ignore_initial_active = false - else active = value update() end - end - - local initial_items, selected_index = options.serializer(list, active) - - -- Items and active_index are set in the handle_prop_change callback, since adding - -- a property observer triggers its handler immediately, we just let that initialize the items. - menu = Menu:open( - {type = options.type, title = options.title, items = initial_items, selected_index = selected_index}, - options.on_select, { - on_open = function() - mp.observe_property(options.list_prop, 'native', handle_list_prop_change) - if options.active_prop then - mp.observe_property(options.active_prop, 'native', handle_active_prop_change) - end - end, - on_close = function() - mp.unobserve_property(handle_list_prop_change) - mp.unobserve_property(handle_active_prop_change) - end, - }) - end -end - -function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command) - local function serialize_tracklist(tracklist) - local items = {} - - if load_command then - items[#items + 1] = { - title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true, - } - end - - local first_item_index = #items + 1 - local active_index = nil - local disabled_item = nil - - -- Add option to disable a subtitle track. This works for all tracks, - -- but why would anyone want to disable audio or video? Better to not - -- let people mistakenly select what is unwanted 99.999% of the time. - -- If I'm mistaken and there is an active need for this, feel free to - -- open an issue. - if track_type == 'sub' then - disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true} - items[#items + 1] = disabled_item - end - - for _, track in ipairs(tracklist) do - if track.type == track_type then - local hint_values = {} - local function h(value) hint_values[#hint_values + 1] = value end - - if track.lang then h(track.lang:upper()) end - if track['demux-h'] then - h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p')) - end - if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end - h(track.codec) - if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end - if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end - if track.forced then h('forced') end - if track.default then h('default') end - if track.external then h('external') end - - items[#items + 1] = { - title = (track.title and track.title or 'Track ' .. track.id), - hint = table.concat(hint_values, ', '), - value = track.id, - active = track.selected, - } - - if track.selected then - if disabled_item then disabled_item.active = false end - active_index = #items - end - end - end - - return items, active_index or first_item_index - end - - local function selection_handler(value) - if value == '{load}' then - mp.command(load_command) - else - mp.commandv('set', track_prop, value and value or 'no') - - -- If subtitle track was selected, assume user also wants to see it - if value and track_type == 'sub' then - mp.commandv('set', 'sub-visibility', 'yes') - end - end - end - - return create_self_updating_menu_opener({ - title = menu_title, - type = track_type, - list_prop = 'track-list', - serializer = serialize_tracklist, - on_select = selection_handler, - }) -end - ----@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()} - --- Opens a file navigation menu with items inside `directory_path`. ----@param directory_path string ----@param handle_select fun(path: string): nil ----@param opts NavigationMenuOptions -function open_file_navigation_menu(directory_path, handle_select, opts) - directory = serialize_path(normalize_path(directory_path)) - opts = opts or {} - - if not directory then - msg.error('Couldn\'t serialize path "' .. directory_path .. '.') - return - end - - local files, directories = read_directory(directory.path, opts.allowed_types) - local is_root = not directory.dirname - local path_separator = path_separator(directory.path) - - if not files or not directories then return end - - sort_filenames(directories) - sort_filenames(files) - - -- Pre-populate items with parent directory selector if not at root - -- Each item value is a serialized path table it points to. - local items = {} - - if is_root then - if state.os == 'windows' then - items[#items + 1] = {title = '..', hint = 'Drives', value = '{drives}', separator = true} - end - else - items[#items + 1] = {title = '..', hint = 'parent dir', value = directory.dirname, separator = true} - end - - local back_path = items[#items] and items[#items].value - local selected_index = #items + 1 - - for _, dir in ipairs(directories) do - items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator} - end - - for _, file in ipairs(files) do - items[#items + 1] = {title = file, value = join_path(directory.path, file)} - end - - for index, item in ipairs(items) do - if not item.value.is_to_parent and opts.active_path == item.value then - item.active = true - if not opts.selected_path then selected_index = index end - end - - if opts.selected_path == item.value then selected_index = index end - end - - local function open_path(path) - local is_drives = path == '{drives}' - local is_to_parent = is_drives or #path < #directory_path - local inheritable_options = { - type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path, - } - - if is_drives then - open_drives_menu(function(drive_path) - open_file_navigation_menu(drive_path, handle_select, inheritable_options) - end, { - type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path, - on_open = opts.on_open, on_close = opts.on_close, - }) - return - end - - local info, error = utils.file_info(path) - - if not info then - msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or '')) - return - end - - if info.is_dir then - -- Preselect directory we are coming from - if is_to_parent then - inheritable_options.selected_path = directory.path - end - - open_file_navigation_menu(path, handle_select, inheritable_options) - else - handle_select(path) - end - end - - local function handle_back() - if back_path then open_path(back_path) end - end - - local menu_data = { - type = opts.type, title = opts.title or directory.basename .. path_separator, items = items, - selected_index = selected_index, - } - local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back} - - return Menu:open(menu_data, open_path, menu_options) -end - --- Opens a file navigation menu with Windows drives as items. ----@param handle_select fun(path: string): nil ----@param opts? NavigationMenuOptions -function open_drives_menu(handle_select, opts) - opts = opts or {} - local process = mp.command_native({ - name = 'subprocess', - capture_stdout = true, - playback_only = false, - args = {'wmic', 'logicaldisk', 'get', 'name', '/value'}, - }) - local items, selected_index = {}, 1 - - if process.status == 0 then - for _, value in ipairs(split(process.stdout, '\n')) do - local drive = string.match(value, 'Name=([A-Z]:)') - if drive then - local drive_path = normalize_path(drive) - items[#items + 1] = { - title = drive, hint = 'drive', value = drive_path, active = opts.active_path == drive_path, - } - if opts.selected_path == drive_path then selected_index = #items end - end - end - else - msg.error(process.stderr) - end - - return Menu:open( - {type = opts.type, title = opts.title or 'Drives', items = items, selected_index = selected_index}, - handle_select - ) -end diff --git a/.config/mpv/scripts/uosc_shared/lib/std.lua b/.config/mpv/scripts/uosc_shared/lib/std.lua deleted file mode 100644 index 1261666..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/std.lua +++ /dev/null @@ -1,181 +0,0 @@ ---[[ Stateless utilities missing in lua standard library ]] - ----@param number number -function round(number) return math.floor(number + 0.5) end - ----@param min number ----@param value number ----@param max number -function clamp(min, value, max) return math.max(min, math.min(value, max)) end - ----@param rgba string `rrggbb` or `rrggbbaa` hex string. -function serialize_rgba(rgba) - local a = rgba:sub(7, 8) - return { - color = rgba:sub(5, 6) .. rgba:sub(3, 4) .. rgba:sub(1, 2), - opacity = clamp(0, tonumber(#a == 2 and a or 'ff', 16) / 255, 1), - } -end - --- Trim any `char` from the end of the string. ----@param str string ----@param char string ----@return string -function trim_end(str, char) - local char, end_i = char:byte(), 0 - for i = #str, 1, -1 do - if str:byte(i) ~= char then - end_i = i - break - end - end - return str:sub(1, end_i) -end - ----@param str string ----@param pattern string ----@return string[] -function split(str, pattern) - local list = {} - local full_pattern = '(.-)' .. pattern - local last_end = 1 - local start_index, end_index, capture = str:find(full_pattern, 1) - while start_index do - list[#list + 1] = capture - last_end = end_index + 1 - start_index, end_index, capture = str:find(full_pattern, last_end) - end - if last_end <= (#str + 1) then - capture = str:sub(last_end) - list[#list + 1] = capture - end - return list -end - --- Get index of the last appearance of `sub` in `str`. ----@param str string ----@param sub string ----@return integer|nil -function string_last_index_of(str, sub) - local sub_length = #sub - for i = #str, 1, -1 do - for j = 1, sub_length do - if str:byte(i + j - 1) ~= sub:byte(j) then break end - if j == sub_length then return i end - end - end -end - ----@param itable table ----@param value any ----@return integer|nil -function itable_index_of(itable, value) - for index, item in ipairs(itable) do - if item == value then return index end - end -end - ----@param itable table ----@param compare fun(value: any, index: number) ----@param from_end? boolean Search from the end of the table. ----@return number|nil index ----@return any|nil value -function itable_find(itable, compare, from_end) - local from, to, step = from_end and #itable or 1, from_end and 1 or #itable, from_end and -1 or 1 - for index = from, to, step do - if compare(itable[index], index) then return index, itable[index] end - end -end - ----@param itable table ----@param decider fun(value: any, index: number) -function itable_filter(itable, decider) - local filtered = {} - for index, value in ipairs(itable) do - if decider(value, index) then filtered[#filtered + 1] = value end - end - return filtered -end - ----@param itable table ----@param value any -function itable_remove(itable, value) - return itable_filter(itable, function(item) return item ~= value end) -end - ----@param itable table ----@param start_pos? integer ----@param end_pos? integer -function itable_slice(itable, start_pos, end_pos) - start_pos = start_pos and start_pos or 1 - end_pos = end_pos and end_pos or #itable - - if end_pos < 0 then end_pos = #itable + end_pos + 1 end - if start_pos < 0 then start_pos = #itable + start_pos + 1 end - - local new_table = {} - for index, value in ipairs(itable) do - if index >= start_pos and index <= end_pos then - new_table[#new_table + 1] = value - end - end - return new_table -end - ----@generic T ----@param a T[]|nil ----@param b T[]|nil ----@return T[] -function itable_join(a, b) - 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 - return result -end - ----@param target any[] ----@param source any[] -function itable_append(target, source) - for _, value in ipairs(source) do target[#target + 1] = value end - return target -end - ----@param target any[] ----@param source any[] ----@param props? string[] -function table_assign(target, source, props) - if props then - for _, name in ipairs(props) do target[name] = source[name] end - else - for prop, value in pairs(source) do target[prop] = value end - end - return target -end - ----@generic T ----@param table T ----@return T -function table_shallow_copy(table) - local result = {} - for key, value in pairs(table) do result[key] = value end - return result -end - ---[[ EASING FUNCTIONS ]] - -function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end -function ease_out_sext(x) return 1 - ((1 - x) ^ 6) end - ---[[ CLASSES ]] - ----@class Class -Class = {} -function Class:new(...) - local object = setmetatable({}, {__index = self}) - object:init(...) - return object -end -function Class:init() end -function Class:destroy() end - -function class(parent) return setmetatable({}, {__index = parent or Class}) end diff --git a/.config/mpv/scripts/uosc_shared/lib/text.lua b/.config/mpv/scripts/uosc_shared/lib/text.lua deleted file mode 100644 index 7bb456f..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/text.lua +++ /dev/null @@ -1,421 +0,0 @@ --- https://en.wikipedia.org/wiki/Unicode_block ----@alias CodePointRange {[1]: integer; [2]: integer} - ----@type CodePointRange[] -local zero_width_blocks = { - {0x0000, 0x001F}, -- C0 - {0x007F, 0x009F}, -- Delete + C1 - {0x034F, 0x034F}, -- combining grapheme joiner - {0x061C, 0x061C}, -- Arabic Letter Strong - {0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark} - {0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override} - {0x2060, 0x2060}, -- word joiner - {0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate} - {0xFEFF, 0xFEFF}, -- zero-width non-breaking space - -- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character - {0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited - {0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited - {0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited - {0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited - {0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters) - -- Egyptian Hieroglyph Format Controls and Shorthand format Controls - {0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs - {0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common - -- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters - {0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters) -} - --- All characters have the same width as the first one ----@type CodePointRange[] -local same_width_blocks = { - {0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han - {0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han - {0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han - {0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han - {0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han - {0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E 2 SIP Han - {0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F 2 SIP Han - {0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement 2 SIP Han - {0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G 3 TIP Han - {0x31350, 0x323AF}, -- CJK Unified Ideographs Extension H 3 TIP Han -} - ----Get byte count of utf-8 character at index i in str ----@param str string ----@param i integer? ----@return integer -local function utf8_char_bytes(str, i) - local char_byte = str:byte(i) - if char_byte < 0xC0 then return 1 - elseif char_byte < 0xE0 then return 2 - elseif char_byte < 0xF0 then return 3 - elseif char_byte < 0xF8 then return 4 - else return 1 end -end - ----Creates an iterator for an utf-8 encoded string ----Iterates over utf-8 characters instead of bytes ----@param str string ----@return fun(): integer?, string? -local function utf8_iter(str) - local byte_start = 1 - return function() - local start = byte_start - if #str < start then return nil end - local byte_count = utf8_char_bytes(str, start) - byte_start = start + byte_count - return start, str:sub(start, start + byte_count - 1) - end -end - ----Extract Unicode code point from utf-8 character at index i in str ----@param str string ----@param i integer ----@return integer -local function utf8_to_unicode(str, i) - local byte_count = utf8_char_bytes(str, i) - local char_byte = str:byte(i) - local unicode = char_byte - if byte_count ~= 1 then - local shift = 2 ^ (8 - byte_count) - char_byte = char_byte - math.floor(0xFF / shift) * shift - unicode = char_byte * (2 ^ 6) ^ (byte_count - 1) - end - for j = 2, byte_count do - char_byte = str:byte(i + j - 1) - 0x80 - unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j) - end - return round(unicode) -end - ----Convert Unicode code point to utf-8 string ----@param unicode integer ----@return string? -local function unicode_to_utf8(unicode) - if unicode < 0x80 then return string.char(unicode) - else - local byte_count - if unicode < 0x800 then byte_count = 2 - elseif unicode < 0x10000 then byte_count = 3 - elseif unicode < 0x110000 then byte_count = 4 - else return end -- too big - - local res = {} - local shift = 2 ^ 6 - local after_shift = unicode - for _ = byte_count, 2, -1 do - local before_shift = after_shift - after_shift = math.floor(before_shift / shift) - table.insert(res, 1, before_shift - after_shift * shift + 0x80) - end - shift = 2 ^ (8 - byte_count) - table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift) - ---@diagnostic disable-next-line: deprecated - return string.char(unpack(res)) - end -end - ----Update osd resolution if valid ----@param width integer ----@param height integer -local function update_osd_resolution(width, height) - if width > 0 and height > 0 then osd_width, osd_height = width, height end -end - -local text_osd = mp.create_osd_overlay("ass-events") -text_osd.compute_bounds, text_osd.hidden = true, true ----@type integer, integer -local osd_width, osd_height = 100, 100 -mp.observe_property('osd-dimensions', 'native', function (_, dim) - if dim then update_osd_resolution(dim.w, dim.h) end -end) - ----@param ass_text string ----@return integer, integer, integer, integer -local function measure_bounds(ass_text) - update_osd_resolution(mp.get_osd_size()) - text_osd.res_x, text_osd.res_y = osd_width, osd_height - text_osd.data = ass_text - local res = text_osd:update() - return res.x0, res.y0, res.x1, res.y1 -end - ----@type {wrap: integer; bold: boolean; italic: boolean, rotate: number; size: number} -local bounds_opts = {wrap = 2, bold = false, italic = false, rotate = 0, size = 0} - ----Measure text width and normalize to a font size of 1 ----text has to be ass safe ----@param text string ----@param size number ----@param bold boolean ----@param italic boolean ----@param horizontal boolean ----@return number, integer -local function normalized_text_width(text, size, bold, italic, horizontal) - bounds_opts.bold, bounds_opts.italic, bounds_opts.rotate = bold, italic, horizontal and 0 or -90 - local x1, y1 = nil, nil - size = size / 0.8 - -- prevent endless loop - local repetitions_left = 5 - repeat - size = size * 0.8 - bounds_opts.size = size - local ass = assdraw.ass_new() - ass:txt(0, 0, horizontal and 7 or 1, text, bounds_opts) - _, _, x1, y1 = measure_bounds(ass.text) - repetitions_left = repetitions_left - 1 - -- make sure nothing got clipped - until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0 - local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1) - return width / size, horizontal and osd_width or osd_height -end - ----Estimates character length based on utf8 byte count ----1 character length is roughly the size of a latin character ----@param char string ----@return number -local function char_length(char) - return #char > 2 and 2 or 1 -end - ----Estimates string length based on utf8 byte count ----Note: Making a string in the iterator with the character is a waste here, ----but as this function is only used when measuring whole string widths it's fine ----@param text string ----@return number -local function text_length(text) - if not text or text == '' then return 0 end - local text_length = 0 - for _, char in utf8_iter(tostring(text)) do text_length = text_length + char_length(char) end - return text_length -end - -local width_length_ratio = 0.5 ----@type {[boolean]: {[string]: {[1]: number, [2]: integer}}} -local char_width_cache = {} - ----Finds the best orientation of text on screen and returns the estimated max size ----and if the text should be drawn horizontally ----@param text string ----@return number, boolean -local function fit_on_screen(text) - local estimated_width = text_length(text) * width_length_ratio - if osd_width >= osd_height then - -- Fill the screen as much as we can, bigger is more accurate. - return math.min(osd_width / estimated_width, osd_height), true - else - return math.min(osd_height / estimated_width, osd_width), false - end -end - ----Gets next stage from cache ----@param cache {[any]: table} ----@param value any -local function get_cache_stage(cache, value) - local stage = cache[value] - if not stage then - stage = {} - cache[value] = stage - end - return stage -end - ----Is measured resolution sufficient ----@param px integer ----@return boolean -local function no_remeasure_required(px) - return px >= 800 or (px * 1.1 >= osd_width and px * 1.1 >= osd_height) -end - ----Get measured width of character ----@param char string ----@param bold boolean ----@return number, integer -local function character_width(char, bold) - ---@type {[string]: {[1]: number, [2]: integer}} - local char_widths = get_cache_stage(char_width_cache, bold) - local width_px = char_widths[char] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1], width_px[2] end - - local unicode = utf8_to_unicode(char, 1) - for _, block in ipairs(zero_width_blocks) do - if unicode >= block[1] and unicode <= block[2] then - char_widths[char] = {0, infinity} - return 0, infinity - end - end - - local measured_char = nil - for _, block in ipairs(same_width_blocks) do - if unicode >= block[1] and unicode <= block[2] then - measured_char = unicode_to_utf8(block[1]) - width_px = char_widths[measured_char] - if width_px and no_remeasure_required(width_px[2]) then - char_widths[char] = width_px - return width_px[1], width_px[2] - end - break - end - end - - if not measured_char then measured_char = char end - -- half as many repetitions for wide characters - local char_count = 10 / char_length(char) - local max_size, horizontal = fit_on_screen(measured_char:rep(char_count)) - local size = math.min(max_size * 0.9, 50) - char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100) - local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count - if measured_char == enclosing_char then enclosing_char = '' - else enclosing_width = 2 * character_width(enclosing_char, bold) end - local width_ratio, width, px = nil, nil, nil - repeat - char_count = next_char_count - local str = enclosing_char .. measured_char:rep(char_count) .. enclosing_char - width, px = normalized_text_width(str, size, bold, false, horizontal) - width = width - enclosing_width - width_ratio = width * size / (horizontal and osd_width or osd_height) - next_char_count = math.min(math.floor(char_count / width_ratio * 0.9), 100) - until width_ratio < 0.05 or width_ratio > 0.5 or char_count == next_char_count - width = width / char_count - - width_px = {width, px} - if char ~= measured_char then char_widths[measured_char] = width_px end - char_widths[char] = width_px - return width, px -end - ----Calculate text width from individual measured characters ----@param text string|number ----@param bold boolean ----@return number, integer -local function character_based_width(text, bold) - local max_width = 0 - local min_px = infinity - for line in tostring(text):gmatch("([^\n]*)\n?") do - local total_width = 0 - for _, char in utf8_iter(line) do - local width, px = character_width(char, bold) - total_width = total_width + width - if px < min_px then min_px = px end - end - if total_width > max_width then max_width = total_width end - end - return max_width, min_px -end - ----Measure width of whole text ----@param text string|number ----@param bold boolean ----@param italic boolean ----@return number, integer -local function whole_text_width(text, bold, italic) - text = tostring(text) - local size, horizontal = fit_on_screen(text) - return normalized_text_width(ass_escape(text), size * 0.9, bold, italic, horizontal) -end - ----Get scale factor calculated from font size, bold and italic ----@param opts {size: number; bold?: boolean; italic?: boolean} -local function opts_scale_factor(opts) - return (opts.italic and 1.01 or 1) * opts.size -end - ----@type {[boolean]: {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}} | {[boolean]: {[string|number]: {[1]: number, [2]: integer}}} -local width_cache = {} - ----Calculate width of text with the given opts ----@param text string|number ----@return number ----@param opts {size: number; bold?: boolean; italic?: boolean} -function text_width(text, opts) - if not text or text == '' then return 0 end - - ---@type boolean, boolean - local bold, italic = opts.bold or options.font_bold, opts.italic or false - - if options.text_width_estimation then - ---@type {[string|number]: {[1]: number, [2]: integer}} - local text_width = get_cache_stage(width_cache, bold) - local width_px = text_width[text] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts_scale_factor(opts) end - - local width, px = character_based_width(text, bold) - width_cache[bold][text] = {width, px} - return width * opts_scale_factor(opts) - else - ---@type {[string|number]: {[1]: number, [2]: integer}} - local text_width = get_cache_stage(get_cache_stage(width_cache, bold), italic) - local width_px = text_width[text] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts.size end - - local width, px = whole_text_width(text, bold, italic) - width_cache[bold][italic][text] = {width, px} - return width * opts.size - end -end - ----Wrap the text at the closest opportunity to target_line_length ----@param text string ----@param opts {size: number; bold?: boolean; italic?: boolean} ----@param target_line_length number ----@return string -function wrap_text(text, opts, target_line_length) - local target_line_width = target_line_length * width_length_ratio * opts.size - local bold, scale_factor = opts.bold or false, opts_scale_factor(opts) - local wrap_at_chars = {' ', ' ', '-', '–'} - local remove_when_wrap = {' ', ' '} - local lines = {} - for text_line in text:gmatch("([^\n]*)\n?") do - local line_width = 0 - local line_start = 1 - local before_end = nil - local before_width = 0 - local before_line_start = 0 - local before_removed_width = 0 - for char_start, char in utf8_iter(text_line) do - local char_end = char_start + #char - 1 - local can_wrap = false - for _, c in ipairs(wrap_at_chars) do - if char == c then - can_wrap = true - break - end - end - local char_width = character_width(char, bold) * scale_factor - line_width = line_width + char_width - if can_wrap or (char_end == #text_line) then - local remove = false - for _, c in ipairs(remove_when_wrap) do - if char == c then - remove = true - break - end - end - local line_width_after_remove = line_width - (remove and char_width or 0) - if line_width_after_remove < target_line_width then - before_end = remove and char_start - 1 or char_end - before_width = line_width_after_remove - before_line_start = char_end + 1 - before_removed_width = remove and char_width or 0 - else - if (target_line_width - before_width) < - (line_width_after_remove - target_line_width) then - lines[#lines + 1] = text_line:sub(line_start, before_end) - line_start = before_line_start - line_width = line_width - before_width - before_removed_width - else - lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end) - line_start = char_end + 1 - line_width = remove and line_width - char_width or line_width - line_width = 0 - end - before_end = line_start - before_width = 0 - end - end - end - if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start) - elseif text_line == '' then lines[#lines + 1] = '' end - end - return table.concat(lines, '\n') -end diff --git a/.config/mpv/scripts/uosc_shared/lib/utils.lua b/.config/mpv/scripts/uosc_shared/lib/utils.lua deleted file mode 100644 index 3202ef1..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/utils.lua +++ /dev/null @@ -1,545 +0,0 @@ ---[[ UI specific utilities that might or might not depend on its state or options ]] - --- Sorting comparator close to (but not exactly) how file explorers sort files. -sort_filenames = (function() - local symbol_order - local default_order - - if state.os == 'windows' then - symbol_order = { - ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7, - ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14, - ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20, - } - default_order = 21 - else - symbol_order = { - ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8, - ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14, - ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23, - } - default_order = 21 - end - - -- Alphanumeric sorting for humans in Lua - -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua - local function pad_number(d) - local dec, n = d:match('(%.?)0*(.+)') - return #dec > 0 and ('%.12f'):format(d) or ('%03d%s'):format(#n, n) - end - - --- In place sorting of filenames - ---@param filenames string[] - return function(filenames) - local tuples = {} - for i, filename in ipairs(filenames) do - local first_char = filename:sub(1, 1) - local order = symbol_order[first_char] or default_order - local formatted = filename:lower():gsub('%.?%d+', pad_number) - tuples[i] = {order, formatted, filename} - end - table.sort(tuples, function(a, b) - if a[1] ~= b[1] then return a[1] < b[1] end - return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2] - end) - for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end - end -end)() - --- Creates in-between frames to animate value from `from` to `to` numbers. ----@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 tween(from, to, setter, factor_or_callback, callback) - local factor = factor_or_callback - if type(factor_or_callback) == 'function' then callback = factor_or_callback end - if type(factor) ~= 'number' then factor = 0.3 end - - local current, done, timeout = from, false, nil - local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end - local cutoff = math.abs(get_to() - from) * 0.01 - - local function finish() - if not done then - done = true - timeout:kill() - if callback then callback() end - end - end - - local function tick() - local to = get_to() - current = current + ((to - current) * factor) - local is_end = math.abs(to - current) <= cutoff - setter(is_end and to or current) - request_render() - if is_end then finish() - else timeout:resume() end - end - - timeout = mp.add_timeout(state.render_delay, tick) - tick() - - return finish -end - ----@param point {x: number; y: number} ----@param rect {ax: number; ay: number; bx: number; by: number} -function get_point_to_rectangle_proximity(point, rect) - local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx) - local dy = math.max(rect.ay - point.y, 0, point.y - rect.by) - return math.sqrt(dx * dx + dy * dy) -end - --- Extracts the properties used by property expansion of that string. ----@param str string ----@param res { [string] : boolean } | nil ----@return { [string] : boolean } -function get_expansion_props(str, res) - res = res or {} - for str in str:gmatch('%$(%b{})') do - local name, str = str:match('^{[?!]?=?([^:]+):?(.*)}$') - if name then - local s = name:find('==') or nil - if s then name = name:sub(0, s - 1) end - res[name] = true - if str and str ~= '' then get_expansion_props(str, res) end - end - end - return res -end - --- Escape a string for verbatim display on the OSD. ----@param str string -function ass_escape(str) - -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if - -- it isn't followed by a recognized character, so add a zero-width - -- non-breaking space - str = str:gsub('\\', '\\\239\187\191') - str = str:gsub('{', '\\{') - str = str:gsub('}', '\\}') - -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of - -- consecutive newlines - str = str:gsub('\n', '\239\187\191\\N') - -- Turn leading spaces into hard spaces to prevent ASS from stripping them - str = str:gsub('\\N ', '\\N\\h') - str = str:gsub('^ ', '\\h') - return str -end - ----@param seconds number ----@return string -function format_time(seconds) - local human = mp.format_time(seconds) - if options.time_precision > 0 then - local formatted = string.format('%.' .. options.time_precision .. 'f', math.abs(seconds) % 1) - human = human .. '.' .. string.sub(formatted, 3) - end - return human -end - ----@param opacity number 0-1 -function opacity_to_alpha(opacity) - return 255 - math.ceil(255 * opacity) -end - -path_separator = (function() - local os_separator = state.os == 'windows' and '\\' or '/' - - -- Get appropriate path separator for the given path. - ---@param path string - ---@return string - return function(path) - return path:sub(1, 2) == '\\\\' and '\\' or os_separator - end -end)() - --- Joins paths with the OS aware path separator or UNC separator. ----@param p1 string ----@param p2 string ----@return string -function join_path(p1, p2) - local p1, separator = trim_trailing_separator(p1) - -- Prevents joining drive letters with a redundant separator (`C:\\foo`), - -- as `trim_trailing_separator()` doesn't trim separators from drive letters. - return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator.. p2 -end - --- Check if path is absolute. ----@param path string ----@return boolean -function is_absolute(path) - if path:sub(1, 2) == '\\\\' then return true - elseif state.os == 'windows' then return path:find('^%a+:') ~= nil - else return path:sub(1, 1) == '/' end -end - --- Ensure path is absolute. ----@param path string ----@return string -function ensure_absolute(path) - if is_absolute(path) then return path end - return join_path(state.cwd, path) -end - --- Remove trailing slashes/backslashes. ----@param path string ----@return string path, string trimmed_separator_type -function trim_trailing_separator(path) - local separator = path_separator(path) - path = trim_end(path, separator) - if state.os == 'windows' then - -- Drive letters on windows need trailing backslash - if path:sub(#path) == ':' then path = path .. '\\' end - else - if path == '' then path = '/' end - end - return path, separator -end - --- Ensures path is absolute, remove trailing slashes/backslashes. --- Lightweight version of normalize_path for performance critical parts. ----@param path string ----@return string -function normalize_path_lite(path) - if not path or is_protocol(path) then return path end - path = trim_trailing_separator(ensure_absolute(path)) - return path -end - --- Ensures path is absolute, remove trailing slashes/backslashes, normalization of path separators and deduplication. ----@param path string ----@return string -function normalize_path(path) - if not path or is_protocol(path) then return path end - - path = ensure_absolute(path) - local is_unc = path:sub(1, 2) == '\\\\' - if state.os == 'windows' or is_unc then path = path:gsub('/', '\\') end - path = trim_trailing_separator(path) - - --Deduplication of path separators - if is_unc then path = path:gsub('(.\\)\\+', '%1') - elseif state.os == 'windows' then path = path:gsub('\\\\+', '\\') - else path = path:gsub('//+', '/') end - - return path -end - --- Check if path is a protocol, such as `http://...`. ----@param path string -function is_protocol(path) - return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil) -end - ----@param path string ----@param extensions string[] Lowercase extensions without the dot. -function has_any_extension(path, extensions) - local path_last_dot_index = string_last_index_of(path, '.') - if not path_last_dot_index then return false end - local path_extension = path:sub(path_last_dot_index + 1):lower() - for _, extension in ipairs(extensions) do - if path_extension == extension then return true end - end - return false -end - ----@return string -function get_default_directory() - return mp.command_native({'expand-path', options.default_directory}) -end - --- Serializes path into its semantic parts. ----@param path string ----@return nil|{path: string; is_root: boolean; dirname?: string; basename: string; filename: string; extension?: string;} -function serialize_path(path) - if not path or is_protocol(path) then return end - - local normal_path = normalize_path_lite(path) - local dirname, basename = utils.split_path(normal_path) - if basename == '' then basename, dirname = dirname:sub(1, #dirname - 1), nil end - local dot_i = string_last_index_of(basename, '.') - - return { - path = normal_path, - is_root = dirname == nil, - dirname = dirname, - basename = basename, - filename = dot_i and basename:sub(1, dot_i - 1) or basename, - extension = dot_i and basename:sub(dot_i + 1) or nil, - } -end - --- Reads items in directory and splits it into directories and files tables. ----@param path string ----@param allowed_types? string[] Filter `files` table to contain only files with these extensions. ----@return string[]|nil files ----@return string[]|nil directories -function read_directory(path, allowed_types) - local items, error = utils.readdir(path, 'all') - - if not items then - msg.error('Reading files from "' .. path .. '" failed: ' .. error) - return nil, nil - end - - local files, directories = {}, {} - - for _, item in ipairs(items) do - if item ~= '.' and item ~= '..' then - local info = utils.file_info(join_path(path, item)) - if info then - if info.is_file then - if not allowed_types or has_any_extension(item, allowed_types) then - files[#files + 1] = item - end - else directories[#directories + 1] = item end - end - end - end - - return files, directories -end - --- Returns full absolute paths of files in the same directory as file_path, --- and index of the current file in the table. ----@param file_path string ----@param allowed_types? string[] -function get_adjacent_files(file_path, allowed_types) - local current_file = serialize_path(file_path) - if not current_file then return end - local files = read_directory(current_file.dirname, allowed_types) - if not files then return end - sort_filenames(files) - local current_file_index - local paths = {} - for index, file in ipairs(files) do - paths[#paths + 1] = join_path(current_file.dirname, file) - if current_file.basename == file then current_file_index = index end - end - if not current_file_index then return end - return paths, current_file_index -end - --- Navigates in a list, using delta or, when `state.shuffle` is enabled, --- randomness to determine the next item. Loops around if `loop-playlist` is enabled. ----@param list table ----@param current_index number ----@param delta number -function decide_navigation_in_list(list, current_index, delta) - if #list < 2 then return #list, list[#list] end - - if state.shuffle then - local new_index = current_index - math.randomseed(os.time()) - while current_index == new_index do new_index = math.random(#list) end - return new_index, list[new_index] - end - - local new_index = current_index + delta - if mp.get_property_native('loop-playlist') then - if new_index > #list then new_index = new_index % #list - elseif new_index < 1 then new_index = #list - new_index end - elseif new_index < 1 or new_index > #list then - return - end - - return new_index, list[new_index] -end - ----@param delta number -function navigate_directory(delta) - if not state.path or is_protocol(state.path) then return false end - local paths, current_index = get_adjacent_files(state.path, config.media_types) - if paths and current_index then - local _, path = decide_navigation_in_list(paths, current_index, delta) - if path then mp.commandv('loadfile', path) return true end - end - return false -end - ----@param delta number -function navigate_playlist(delta) - local playlist, pos = mp.get_property_native('playlist'), mp.get_property_native('playlist-pos-1') - if playlist and #playlist > 1 and pos then - local index = decide_navigation_in_list(playlist, pos, delta) - if index then mp.commandv('playlist-play-index', index - 1) return true end - end - return false -end - ----@param delta number -function navigate_item(delta) - if state.has_playlist then return navigate_playlist(delta) else return navigate_directory(delta) end -end - --- Can't use `os.remove()` as it fails on paths with unicode characters. --- Returns `result, error`, result is table of: --- `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean` ----@param path string -function delete_file(path) - local args = state.os == 'windows' and {'cmd', '/C', 'del', path} or {'rm', path} - return mp.command_native({ - name = 'subprocess', - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }) -end - -function serialize_chapter_ranges(normalized_chapters) - local ranges = {} - local simple_ranges = { - {name = 'openings', patterns = {'^op ', '^op$', ' op$', 'opening$'}, requires_next_chapter = true}, - {name = 'intros', patterns = {'^intro$'}, requires_next_chapter = true}, - {name = 'endings', patterns = {'^ed ', '^ed$', ' ed$', 'ending$', 'closing$'}}, - {name = 'outros', patterns = {'^outro$'}}, - } - local sponsor_ranges = {} - - -- Extend with alt patterns - for _, meta in ipairs(simple_ranges) do - local alt_patterns = config.chapter_ranges[meta.name] and config.chapter_ranges[meta.name].patterns - if alt_patterns then meta.patterns = itable_join(meta.patterns, alt_patterns) end - end - - -- Clone chapters - local chapters = {} - for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end - - for i, chapter in ipairs(chapters) do - -- Simple ranges - for _, meta in ipairs(simple_ranges) do - if config.chapter_ranges[meta.name] then - local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end) - if match then - local next_chapter = chapters[i + 1] - if next_chapter or not meta.requires_next_chapter then - ranges[#ranges + 1] = table_assign({ - start = chapter.time, - ['end'] = next_chapter and next_chapter.time or infinity, - }, config.chapter_ranges[meta.name]) - end - end - end - end - - -- Sponsor blocks - if config.chapter_ranges.ads then - local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)') - if id then -- ad range from sponsorblock - for j = i + 1, #chapters, 1 do - local end_chapter = chapters[j] - local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)') - if end_match then - local range = table_assign({ - start_chapter = chapter, end_chapter = end_chapter, - start = chapter.time, ['end'] = end_chapter.time, - }, config.chapter_ranges.ads) - ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range - end_chapter.is_end_only = true - break - end - end -- single chapter for ad - elseif not chapter.is_end_only and - (chapter.lowercase_title:find('%[sponsorblock%]:') or chapter.lowercase_title:find('^sponsors?')) then - local next_chapter = chapters[i + 1] - ranges[#ranges + 1] = table_assign({ - start = chapter.time, - ['end'] = next_chapter and next_chapter.time or infinity, - }, config.chapter_ranges.ads) - end - end - end - - -- Fix overlapping sponsor block segments - for index, range in ipairs(sponsor_ranges) do - local next_range = sponsor_ranges[index + 1] - if next_range then - local delta = next_range.start - range['end'] - if delta < 0 then - local mid_point = range['end'] + delta / 2 - range['end'], range.end_chapter.time = mid_point - 0.01, mid_point - 0.01 - next_range.start, next_range.start_chapter.time = mid_point, mid_point - end - end - end - table.sort(chapters, function(a, b) return a.time < b.time end) - - return chapters, ranges -end - --- Ensures chapters are in chronological order -function normalize_chapters(chapters) - if not chapters then return {} end - -- Ensure chronological order - table.sort(chapters, function(a, b) return a.time < b.time end) - -- Ensure titles - for index, chapter in ipairs(chapters) do - chapter.title = chapter.title or ('Chapter ' .. index) - chapter.lowercase_title = chapter.title:lower() - end - return chapters -end - -function serialize_chapters(chapters) - chapters = normalize_chapters(chapters) - if not chapters then return end - --- timeline font size isn't accessible here, so normalize to size 1 and then scale during rendering - local opts = {size = 1, bold = true} - for index, chapter in ipairs(chapters) do - chapter.index = index - chapter.title_wrapped = wrap_text(chapter.title, opts, 25) - chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts) - chapter.title_wrapped = ass_escape(chapter.title_wrapped) - end - return chapters -end - ---[[ RENDERING ]] - -function render() - if not display.initialized then return end - state.render_last_time = mp.get_time() - - -- Actual rendering - local ass = assdraw.ass_new() - - for _, element in Elements:ipairs() do - if element.enabled then - local result = element:maybe('render') - if result then - ass:new_event() - ass:merge(result) - end - end - end - - -- submit - if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then - return - end - - osd.res_x = display.width - osd.res_y = display.height - osd.data = ass.text - osd.z = 2000 - osd:update() - - update_margins() -end - --- Request that render() is called. --- The render is then either executed immediately, or rate-limited if it was --- called a small time ago. -state.render_timer = mp.add_timeout(0, render) -state.render_timer:kill() -function request_render() - if state.render_timer:is_enabled() then return end - local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time)) - state.render_timer.timeout = timeout - state.render_timer:resume() -end |