mpv: Update uosc
This repo is not including the binary 'ziggy' files. They are used for clipboard management and subtitle downloading, which I personally do not need. We are including the uosc repo as a sparsely checked-out submodule repository now, symlinking it into place. It might be a little buggy and I am not sure how jj deals with it but time will tell. A concise explanation of the idea can be found here: <https://gist.github.com/ZhuoyunZhong/2c08c8549616e03b7f508fea64130558> WIP: Add UOSC as submodule
This commit is contained in:
parent
df78905778
commit
1568ca0534
49 changed files with 4385 additions and 1813 deletions
.gitmodules
multimedia/.config/mpv/scripts
uosc
uosc_repo
.editorconfig.gitignoreLICENSE.LGPLREADME.mdgo.modgo.sum
src/uosc
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -7,3 +7,6 @@
|
||||||
[submodule "multimedia/.local/share/vimiv/plugins/batchmark"]
|
[submodule "multimedia/.local/share/vimiv/plugins/batchmark"]
|
||||||
path = multimedia/.local/share/vimiv/plugins/batchmark
|
path = multimedia/.local/share/vimiv/plugins/batchmark
|
||||||
url = https://github.com/jcjgraf/BatchMark
|
url = https://github.com/jcjgraf/BatchMark
|
||||||
|
[submodule "multimedia/.config/mpv/scripts/uosc_repo"]
|
||||||
|
path = multimedia/.config/mpv/scripts/uosc_repo
|
||||||
|
url = git@github.com:tomasklaen/uosc.git
|
||||||
|
|
1
multimedia/.config/mpv/scripts/uosc
Symbolic link
1
multimedia/.config/mpv/scripts/uosc
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
uosc_repo/src/uosc
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,170 +0,0 @@
|
||||||
local Element = require('elements/Element')
|
|
||||||
local dots = {'.', '..', '...'}
|
|
||||||
|
|
||||||
local function cleanup_output(output)
|
|
||||||
return tostring(output):gsub('%c*\n%c*', '\n'):match('^[%s%c]*(.-)[%s%c]*$')
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Updater : Element
|
|
||||||
local Updater = class(Element)
|
|
||||||
|
|
||||||
function Updater:new() return Class.new(self) --[[@as Updater]] end
|
|
||||||
function Updater:init()
|
|
||||||
Element.init(self, 'updater', {render_order = 1000})
|
|
||||||
self.output = nil
|
|
||||||
self.message = t('Updating uosc')
|
|
||||||
self.state = 'pending' -- Matches icon name
|
|
||||||
local config_dir = mp.command_native({'expand-path', '~~/'})
|
|
||||||
|
|
||||||
Elements:maybe('curtain', 'register', self.id)
|
|
||||||
|
|
||||||
local function handle_result(success, result, error)
|
|
||||||
if success and result and result.status == 0 then
|
|
||||||
self.state = 'done'
|
|
||||||
self.message = t('uosc has been installed. Restart mpv for it to take effect.')
|
|
||||||
else
|
|
||||||
self.state = 'error'
|
|
||||||
self.message = t('An error has occurred.') .. ' ' .. t('See above for clues.')
|
|
||||||
end
|
|
||||||
|
|
||||||
local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
|
|
||||||
if state.platform == 'darwin' then
|
|
||||||
output =
|
|
||||||
'Self-updater is known not to work on MacOS.\nIf you know about a solution, please make an issue and share it with us!.\n' ..
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
self.output = ass_escape(cleanup_output(output))
|
|
||||||
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update(args)
|
|
||||||
local env = utils.get_env_list()
|
|
||||||
env[#env + 1] = 'MPV_CONFIG_DIR=' .. config_dir
|
|
||||||
|
|
||||||
mp.command_native_async({
|
|
||||||
name = 'subprocess',
|
|
||||||
capture_stderr = true,
|
|
||||||
capture_stdout = true,
|
|
||||||
playback_only = false,
|
|
||||||
args = args,
|
|
||||||
env = env,
|
|
||||||
}, handle_result)
|
|
||||||
end
|
|
||||||
|
|
||||||
if state.platform == 'windows' then
|
|
||||||
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1'
|
|
||||||
update({'powershell', '-NoProfile', '-Command', 'irm ' .. url .. ' | iex'})
|
|
||||||
else
|
|
||||||
-- Detect missing dependencies. We can't just let the process run and
|
|
||||||
-- report an error, as on snap packages there's no error. Everything
|
|
||||||
-- either exits with 0, or no helpful output/error message.
|
|
||||||
local missing = {}
|
|
||||||
|
|
||||||
for _, name in ipairs({'curl', 'unzip'}) do
|
|
||||||
local result = mp.command_native({
|
|
||||||
name = 'subprocess',
|
|
||||||
capture_stdout = true,
|
|
||||||
playback_only = false,
|
|
||||||
args = {'which', name},
|
|
||||||
})
|
|
||||||
local path = cleanup_output(result and result.stdout or '')
|
|
||||||
if path == '' then
|
|
||||||
missing[#missing + 1] = name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #missing > 0 then
|
|
||||||
local stderr = 'Missing dependencies: ' .. table.concat(missing, ', ')
|
|
||||||
if config_dir:match('/snap/') then
|
|
||||||
stderr = stderr ..
|
|
||||||
'\nThis is a known error for mpv snap packages.\nYou can still update uosc by entering the Linux install command from uosc\'s readme into your terminal, it just can\'t be done this way.\nIf you know about a solution, please make an issue and share it with us!'
|
|
||||||
end
|
|
||||||
handle_result(false, {stderr = stderr})
|
|
||||||
else
|
|
||||||
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh'
|
|
||||||
update({'/bin/bash', '-c', 'source <(curl -fsSL ' .. url .. ')'})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Updater:destroy()
|
|
||||||
Elements:maybe('curtain', 'unregister', self.id)
|
|
||||||
Element.destroy(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Updater:render()
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
local text_size = math.min(20 * state.scale, display.height / 20)
|
|
||||||
local icon_size = text_size * 2
|
|
||||||
local center_x = round(display.width / 2)
|
|
||||||
|
|
||||||
local color = fg
|
|
||||||
if self.state == 'done' then
|
|
||||||
color = config.color.success
|
|
||||||
elseif self.state == 'error' then
|
|
||||||
color = config.color.error
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Divider
|
|
||||||
local divider_width = round(math.min(500 * state.scale, display.width * 0.8))
|
|
||||||
local divider_half, divider_border_half, divider_y = divider_width / 2, round(1 * state.scale), display.height * 0.65
|
|
||||||
local divider_ay, divider_by = round(divider_y - divider_border_half), round(divider_y + divider_border_half)
|
|
||||||
ass:rect(center_x - divider_half, divider_ay, center_x - icon_size, divider_by, {
|
|
||||||
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
|
||||||
})
|
|
||||||
ass:rect(center_x + icon_size, divider_ay, center_x + divider_half, divider_by, {
|
|
||||||
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
|
||||||
})
|
|
||||||
if self.state == 'pending' then
|
|
||||||
ass:spinner(center_x, divider_y, icon_size, {
|
|
||||||
color = fg, border = options.text_border * state.scale, border_color = bg,
|
|
||||||
})
|
|
||||||
else
|
|
||||||
ass:icon(center_x, divider_y, icon_size * 0.8, self.state, {
|
|
||||||
color = color, border = options.text_border * state.scale, border_color = bg,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Output
|
|
||||||
local output = self.output or dots[math.ceil((mp.get_time() % 1) * #dots)]
|
|
||||||
ass:txt(center_x, divider_y - icon_size, 2, output, {
|
|
||||||
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Message
|
|
||||||
ass:txt(center_x, divider_y + icon_size, 5, self.message, {
|
|
||||||
size = text_size, bold = true, color = color, border = options.text_border * state.scale, border_color = bg,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Button
|
|
||||||
if self.state ~= 'pending' then
|
|
||||||
-- Background
|
|
||||||
local button_y = divider_y + icon_size * 1.75
|
|
||||||
local button_rect = {
|
|
||||||
ax = round(center_x - icon_size / 2),
|
|
||||||
ay = round(button_y),
|
|
||||||
bx = round(center_x + icon_size / 2),
|
|
||||||
by = round(button_y + icon_size),
|
|
||||||
}
|
|
||||||
local is_hovered = get_point_to_rectangle_proximity(cursor, button_rect) == 0
|
|
||||||
ass:rect(button_rect.ax, button_rect.ay, button_rect.bx, button_rect.by, {
|
|
||||||
color = fg,
|
|
||||||
radius = state.radius,
|
|
||||||
opacity = is_hovered and 1 or 0.5,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Icon
|
|
||||||
local x = round(button_rect.ax + (button_rect.bx - button_rect.ax) / 2)
|
|
||||||
local y = round(button_rect.ay + (button_rect.by - button_rect.ay) / 2)
|
|
||||||
ass:icon(x, y, icon_size * 0.8, 'close', {color = bg})
|
|
||||||
|
|
||||||
cursor:zone('primary_click', button_rect, function() self:destroy() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Updater
|
|
|
@ -1,59 +0,0 @@
|
||||||
{
|
|
||||||
"Aspect ratio": "Relación de aspecto",
|
|
||||||
"Audio": "Audio",
|
|
||||||
"Audio device": "Dispositivo de audio",
|
|
||||||
"Audio devices": "Dispositivos de audio",
|
|
||||||
"Audio tracks": "Pistas de audio",
|
|
||||||
"Autoselect device": "Selección automática",
|
|
||||||
"Chapter %s": "Capítulo %s",
|
|
||||||
"Chapters": "Capítulos",
|
|
||||||
"Default": "Por defecto",
|
|
||||||
"Default %s": "Por defecto %s",
|
|
||||||
"Delete file & Next": "Eliminar archivo y siguiente",
|
|
||||||
"Delete file & Prev": "Eliminar archivo y anterior",
|
|
||||||
"Delete file & Quit": "Eliminar archivo y salir",
|
|
||||||
"Disabled": "Desactivado",
|
|
||||||
"Drives": "Unidades",
|
|
||||||
"Edition": "Edición",
|
|
||||||
"Edition %s": "Edición %s",
|
|
||||||
"Editions": "Ediciones",
|
|
||||||
"Empty": "Vacío",
|
|
||||||
"First": "Primero",
|
|
||||||
"Fullscreen": "Pantalla completa",
|
|
||||||
"Last": "Último",
|
|
||||||
"Load": "Abrir",
|
|
||||||
"Load audio": "Añadir una pista de audio",
|
|
||||||
"Load subtitles": "Añadir una pista de subtítulos",
|
|
||||||
"Load video": "Añadir una pista de vídeo",
|
|
||||||
"Loop file": "Repetir archivo",
|
|
||||||
"Loop playlist": "Repetir lista de reproducción",
|
|
||||||
"Menu": "Menú",
|
|
||||||
"Navigation": "Navegación",
|
|
||||||
"Next": "Siguiente",
|
|
||||||
"No file": "Ningún archivo",
|
|
||||||
"Open config folder": "Abrir carpeta de configuración",
|
|
||||||
"Open file": "Abrir un archivo",
|
|
||||||
"Playlist": "Lista de reproducción",
|
|
||||||
"Playlist/Files": "Lista de reproducción / Archivos",
|
|
||||||
"Prev": "Anterior",
|
|
||||||
"Previous": "Anterior",
|
|
||||||
"Quit": "Salir",
|
|
||||||
"Screenshot": "Captura de pantalla",
|
|
||||||
"Show in directory": "Acceder a la carpeta",
|
|
||||||
"Shuffle": "Reproducción aleatoria",
|
|
||||||
"Stream quality": "Calidad del flujo",
|
|
||||||
"Subtitles": "Subtítulos",
|
|
||||||
"Track": "Pista",
|
|
||||||
"Track %s": "Pista %s",
|
|
||||||
"Utils": "Utilidades",
|
|
||||||
"Video": "Vídeo",
|
|
||||||
"%s channel": "%s canal",
|
|
||||||
"%s channels": "%s canales",
|
|
||||||
"default": "por defecto",
|
|
||||||
"drive": "unidad",
|
|
||||||
"external": "externo",
|
|
||||||
"forced": "forzado",
|
|
||||||
"open file": "seleccionar un archivo",
|
|
||||||
"parent dir": "directorio padre",
|
|
||||||
"playlist or file": "archivo o lista de reproducción"
|
|
||||||
}
|
|
|
@ -1,831 +0,0 @@
|
||||||
---@param data MenuData
|
|
||||||
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
|
|
||||||
function open_command_menu(data, opts)
|
|
||||||
local function run_command(command)
|
|
||||||
if type(command) == 'string' then
|
|
||||||
mp.command(command)
|
|
||||||
else
|
|
||||||
---@diagnostic disable-next-line: deprecated
|
|
||||||
mp.commandv(unpack(command))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
---@type MenuOptions
|
|
||||||
local menu_opts = {}
|
|
||||||
if opts then
|
|
||||||
menu_opts.mouse_nav = opts.mouse_nav
|
|
||||||
if opts.on_close then menu_opts.on_close = function() run_command(opts.on_close) end end
|
|
||||||
end
|
|
||||||
local menu = Menu:open(data, run_command, menu_opts)
|
|
||||||
if opts and opts.submenu then menu:activate_submenu(opts.submenu) end
|
|
||||||
return menu
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
|
|
||||||
function toggle_menu_with_items(opts)
|
|
||||||
if Menu:is_open('menu') then
|
|
||||||
Menu:close()
|
|
||||||
else
|
|
||||||
open_command_menu({type = 'menu', items = get_menu_items(), search_submenus = true}, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any); on_paste: fun(payload: string); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}
|
|
||||||
function create_self_updating_menu_opener(opts)
|
|
||||||
return function()
|
|
||||||
if Menu:is_open(opts.type) then
|
|
||||||
Menu:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local list = mp.get_property_native(opts.list_prop)
|
|
||||||
local active = opts.active_prop and mp.get_property_native(opts.active_prop) or nil
|
|
||||||
local menu
|
|
||||||
|
|
||||||
local function update() menu:update_items(opts.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 = opts.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 = opts.type,
|
|
||||||
title = opts.title,
|
|
||||||
items = initial_items,
|
|
||||||
selected_index = selected_index,
|
|
||||||
on_paste = opts.on_paste,
|
|
||||||
},
|
|
||||||
opts.on_select, {
|
|
||||||
on_open = function()
|
|
||||||
mp.observe_property(opts.list_prop, 'native', handle_list_prop_change)
|
|
||||||
if opts.active_prop then
|
|
||||||
mp.observe_property(opts.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,
|
|
||||||
on_move_item = opts.on_move_item,
|
|
||||||
on_delete_item = opts.on_delete_item,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command, download_command)
|
|
||||||
local function serialize_tracklist(tracklist)
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
if download_command then
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = t('Download'), bold = true, italic = true, hint = t('search online'), value = '{download}',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if load_command then
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = t('Load'), bold = true, italic = true, hint = t('open file'), value = '{load}',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if #items > 0 then
|
|
||||||
items[#items].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 = t('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'] == 1
|
|
||||||
and t('%s channel', track['audio-channels'])
|
|
||||||
or t('%s channels', track['audio-channels']))
|
|
||||||
end
|
|
||||||
if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end
|
|
||||||
if track.forced then h(t('forced')) end
|
|
||||||
if track.default then h(t('default')) end
|
|
||||||
if track.external then h(t('external')) end
|
|
||||||
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = (track.title and track.title or t('Track %s', 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 handle_select(value)
|
|
||||||
if value == '{download}' then
|
|
||||||
mp.command(download_command)
|
|
||||||
elseif value == '{load}' then
|
|
||||||
mp.command(load_command)
|
|
||||||
else
|
|
||||||
mp.commandv('set', track_prop, value and value or 'no')
|
|
||||||
|
|
||||||
-- If subtitle track was selected, assume the 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 = handle_select,
|
|
||||||
on_paste = function(path) load_track(track_type, path) end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], keep_open?: boolean, 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, mods: Modifiers): 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, {
|
|
||||||
types = opts.allowed_types,
|
|
||||||
hidden = options.show_hidden_files,
|
|
||||||
})
|
|
||||||
local is_root = not directory.dirname
|
|
||||||
local path_separator = path_separator(directory.path)
|
|
||||||
|
|
||||||
if not files or not directories then return end
|
|
||||||
|
|
||||||
sort_strings(directories)
|
|
||||||
sort_strings(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.platform == 'windows' then
|
|
||||||
items[#items + 1] = {title = '..', hint = t('Drives'), value = '{drives}', separator = true}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
items[#items + 1] = {title = '..', hint = t('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
|
|
||||||
|
|
||||||
---@type MenuCallback
|
|
||||||
local function open_path(path, meta)
|
|
||||||
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,
|
|
||||||
keep_open = opts.keep_open,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 and not meta.modifiers.alt and not meta.modifiers.ctrl 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, meta.modifiers)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handle_back()
|
|
||||||
if back_path then open_path(back_path, {modifiers = {}}) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local menu_data = {
|
|
||||||
type = opts.type,
|
|
||||||
title = opts.title or directory.basename .. path_separator,
|
|
||||||
items = items,
|
|
||||||
keep_open = opts.keep_open,
|
|
||||||
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 = t('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 t('Drives'), items = items, selected_index = selected_index},
|
|
||||||
handle_select
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- On demand menu items loading
|
|
||||||
do
|
|
||||||
local items = nil
|
|
||||||
function get_menu_items()
|
|
||||||
if items then return items end
|
|
||||||
|
|
||||||
local input_conf_property = mp.get_property_native('input-conf')
|
|
||||||
local input_conf_iterator
|
|
||||||
if input_conf_property:sub(1, 9) == 'memory://' then
|
|
||||||
-- mpv.net v7
|
|
||||||
local input_conf_lines = split(input_conf_property:sub(10), '\n')
|
|
||||||
local i = 0
|
|
||||||
input_conf_iterator = function()
|
|
||||||
i = i + 1
|
|
||||||
return input_conf_lines[i]
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local input_conf = input_conf_property == '' and '~~/input.conf' or input_conf_property
|
|
||||||
local input_conf_path = mp.command_native({'expand-path', input_conf})
|
|
||||||
local input_conf_meta, meta_error = utils.file_info(input_conf_path)
|
|
||||||
|
|
||||||
-- File doesn't exist
|
|
||||||
if not input_conf_meta or not input_conf_meta.is_file then
|
|
||||||
items = create_default_menu_items()
|
|
||||||
return items
|
|
||||||
end
|
|
||||||
|
|
||||||
input_conf_iterator = io.lines(input_conf_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
local main_menu = {items = {}, items_by_command = {}}
|
|
||||||
local by_id = {}
|
|
||||||
|
|
||||||
for line in input_conf_iterator do
|
|
||||||
local key, command, comment = string.match(line, '%s*([%S]+)%s+(.-)%s+#%s*(.-)%s*$')
|
|
||||||
local title = ''
|
|
||||||
|
|
||||||
if comment then
|
|
||||||
local comments = split(comment, '#')
|
|
||||||
local titles = itable_filter(comments, function(v, i) return v:match('^!') or v:match('^menu:') end)
|
|
||||||
if titles and #titles > 0 then
|
|
||||||
title = titles[1]:match('^!%s*(.*)%s*') or titles[1]:match('^menu:%s*(.*)%s*')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if title ~= '' then
|
|
||||||
local is_dummy = key:sub(1, 1) == '#'
|
|
||||||
local submenu_id = ''
|
|
||||||
local target_menu = main_menu
|
|
||||||
local title_parts = split(title or '', ' *> *')
|
|
||||||
|
|
||||||
for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do
|
|
||||||
if index < #title_parts then
|
|
||||||
submenu_id = submenu_id .. title_part
|
|
||||||
|
|
||||||
if not by_id[submenu_id] then
|
|
||||||
local items = {}
|
|
||||||
by_id[submenu_id] = {items = items, items_by_command = {}}
|
|
||||||
target_menu.items[#target_menu.items + 1] = {title = title_part, items = items}
|
|
||||||
end
|
|
||||||
|
|
||||||
target_menu = by_id[submenu_id]
|
|
||||||
else
|
|
||||||
if command == 'ignore' then break end
|
|
||||||
-- If command is already in menu, just append the key to it
|
|
||||||
if key ~= '#' and command ~= '' and target_menu.items_by_command[command] then
|
|
||||||
local hint = target_menu.items_by_command[command].hint
|
|
||||||
target_menu.items_by_command[command].hint = hint and hint .. ', ' .. key or key
|
|
||||||
else
|
|
||||||
-- Separator
|
|
||||||
if title_part:sub(1, 3) == '---' then
|
|
||||||
local last_item = target_menu.items[#target_menu.items]
|
|
||||||
if last_item then last_item.separator = true end
|
|
||||||
else
|
|
||||||
local item = {
|
|
||||||
title = title_part,
|
|
||||||
hint = not is_dummy and key or nil,
|
|
||||||
value = command,
|
|
||||||
}
|
|
||||||
if command == '' then
|
|
||||||
item.selectable = false
|
|
||||||
item.muted = true
|
|
||||||
item.italic = true
|
|
||||||
else
|
|
||||||
target_menu.items_by_command[command] = item
|
|
||||||
end
|
|
||||||
target_menu.items[#target_menu.items + 1] = item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
items = #main_menu.items > 0 and main_menu.items or create_default_menu_items()
|
|
||||||
return items
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Adapted from `stats.lua`
|
|
||||||
function get_keybinds_items()
|
|
||||||
local items = {}
|
|
||||||
local active = find_active_keybindings()
|
|
||||||
|
|
||||||
-- Convert to menu items
|
|
||||||
for _, bind in pairs(active) do
|
|
||||||
items[#items + 1] = {title = bind.cmd, hint = bind.key, value = bind.cmd}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sort
|
|
||||||
table.sort(items, function(a, b) return a.title < b.title end)
|
|
||||||
|
|
||||||
return #items > 0 and items or {
|
|
||||||
{
|
|
||||||
title = t('%s are empty', '`input-bindings`'),
|
|
||||||
selectable = false,
|
|
||||||
align = 'center',
|
|
||||||
italic = true,
|
|
||||||
muted = true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function open_stream_quality_menu()
|
|
||||||
if Menu:is_open('stream-quality') then
|
|
||||||
Menu:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ytdl_format = mp.get_property_native('ytdl-format')
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
for _, height in ipairs(config.stream_quality_options) do
|
|
||||||
local format = 'bestvideo[height<=?' .. height .. ']+bestaudio/best[height<=?' .. height .. ']'
|
|
||||||
items[#items + 1] = {title = height .. 'p', value = format, active = format == ytdl_format}
|
|
||||||
end
|
|
||||||
|
|
||||||
Menu:open({type = 'stream-quality', title = t('Stream quality'), items = items}, function(format)
|
|
||||||
mp.set_property('ytdl-format', format)
|
|
||||||
|
|
||||||
-- Reload the video to apply new format
|
|
||||||
-- This is taken from https://github.com/jgreco/mpv-youtube-quality
|
|
||||||
-- which is in turn taken from https://github.com/4e6/mpv-reload/
|
|
||||||
local duration = mp.get_property_native('duration')
|
|
||||||
local time_pos = mp.get_property('time-pos')
|
|
||||||
|
|
||||||
mp.command('playlist-play-index current')
|
|
||||||
|
|
||||||
-- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero
|
|
||||||
-- duration property. When reloading VOD, to keep the current time position
|
|
||||||
-- we should provide offset from the start. Stream doesn't have fixed start.
|
|
||||||
-- Decent choice would be to reload stream from it's current 'live' position.
|
|
||||||
-- That's the reason we don't pass the offset when reloading streams.
|
|
||||||
if duration and duration > 0 then
|
|
||||||
local function seeker()
|
|
||||||
mp.commandv('seek', time_pos, 'absolute')
|
|
||||||
mp.unregister_event(seeker)
|
|
||||||
end
|
|
||||||
mp.register_event('file-loaded', seeker)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function open_open_file_menu()
|
|
||||||
if Menu:is_open('open-file') then
|
|
||||||
Menu:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local directory
|
|
||||||
local active_file
|
|
||||||
|
|
||||||
if state.path == nil or is_protocol(state.path) then
|
|
||||||
local serialized = serialize_path(get_default_directory())
|
|
||||||
if serialized then
|
|
||||||
directory = serialized.path
|
|
||||||
active_file = nil
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local serialized = serialize_path(state.path)
|
|
||||||
if serialized then
|
|
||||||
directory = serialized.dirname
|
|
||||||
active_file = serialized.path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not directory then
|
|
||||||
msg.error('Couldn\'t serialize path "' .. state.path .. '".')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Update active file in directory navigation menu
|
|
||||||
local menu = nil
|
|
||||||
local function handle_file_loaded()
|
|
||||||
if menu and menu:is_alive() then
|
|
||||||
menu:activate_one_value(normalize_path(mp.get_property_native('path')))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
menu = open_file_navigation_menu(
|
|
||||||
directory,
|
|
||||||
function(path, mods)
|
|
||||||
if mods.ctrl then
|
|
||||||
mp.commandv('loadfile', path, 'append')
|
|
||||||
else
|
|
||||||
mp.commandv('loadfile', path)
|
|
||||||
Menu:close()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
{
|
|
||||||
type = 'open-file',
|
|
||||||
allowed_types = config.types.media,
|
|
||||||
active_path = active_file,
|
|
||||||
keep_open = true,
|
|
||||||
on_open = function() mp.register_event('file-loaded', handle_file_loaded) end,
|
|
||||||
on_close = function() mp.unregister_event(handle_file_loaded) end,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts {name: 'subtitles'|'audio'|'video'; prop: 'sub'|'audio'|'video'; allowed_types: string[]}
|
|
||||||
function create_track_loader_menu_opener(opts)
|
|
||||||
local menu_type = 'load-' .. opts.name
|
|
||||||
local title = ({
|
|
||||||
subtitles = t('Load subtitles'),
|
|
||||||
audio = t('Load audio'),
|
|
||||||
video = t('Load video'),
|
|
||||||
})[opts.name]
|
|
||||||
|
|
||||||
return function()
|
|
||||||
if Menu:is_open(menu_type) then
|
|
||||||
Menu:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local path = state.path
|
|
||||||
if path then
|
|
||||||
if is_protocol(path) then
|
|
||||||
path = false
|
|
||||||
else
|
|
||||||
local serialized_path = serialize_path(path)
|
|
||||||
path = serialized_path ~= nil and serialized_path.dirname or false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not path then
|
|
||||||
path = get_default_directory()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handle_select(path) load_track(opts.prop, path) end
|
|
||||||
|
|
||||||
open_file_navigation_menu(path, handle_select, {
|
|
||||||
type = menu_type, title = title, allowed_types = opts.allowed_types,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function open_subtitle_downloader()
|
|
||||||
local menu_type = 'download-subtitles'
|
|
||||||
---@type Menu
|
|
||||||
local menu
|
|
||||||
|
|
||||||
if Menu:is_open(menu_type) then
|
|
||||||
Menu:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local search_suggestion, file_path = '', nil
|
|
||||||
local destination_directory = mp.command_native({'expand-path', '~~/subtitles'})
|
|
||||||
local credentials = {'--api-key', config.open_subtitles_api_key, '--agent', config.open_subtitles_agent}
|
|
||||||
|
|
||||||
if state.path then
|
|
||||||
if is_protocol(state.path) then
|
|
||||||
if not is_protocol(state.title) then search_suggestion = state.title end
|
|
||||||
else
|
|
||||||
local serialized_path = serialize_path(state.path)
|
|
||||||
if serialized_path then
|
|
||||||
search_suggestion = serialized_path.filename
|
|
||||||
file_path = state.path
|
|
||||||
destination_directory = serialized_path.dirname
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local handle_select, handle_search
|
|
||||||
|
|
||||||
-- Ensures response is valid, and returns its payload, or handles error reporting,
|
|
||||||
-- and returns `nil`, indicating the consumer should abort response handling.
|
|
||||||
local function ensure_response_data(success, result, error, check)
|
|
||||||
local data
|
|
||||||
if success and result and result.status == 0 then
|
|
||||||
data = utils.parse_json(result.stdout)
|
|
||||||
if not data or not check(data) then
|
|
||||||
data = (data and data.error == true) and data or {
|
|
||||||
error = true,
|
|
||||||
message = t('invalid response json (see console for details)'),
|
|
||||||
message_verbose = 'invalid response json: ' .. utils.to_string(result.stdout),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
data = {
|
|
||||||
error = true,
|
|
||||||
message = error or t('process exited with code %s (see console for details)', result.status),
|
|
||||||
message_verbose = result.stdout .. result.stderr,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
if data.error then
|
|
||||||
local message, message_verbose = data.message or t('unknown error'), data.message_verbose or data.message
|
|
||||||
if message_verbose then msg.error(message_verbose) end
|
|
||||||
menu:update_items({
|
|
||||||
{
|
|
||||||
title = message,
|
|
||||||
hint = t('error'),
|
|
||||||
muted = true,
|
|
||||||
italic = true,
|
|
||||||
selectable = false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return data
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param data {kind: 'file', id: number}|{kind: 'page', query: string, page: number}
|
|
||||||
handle_select = function(data)
|
|
||||||
if data.kind == 'page' then
|
|
||||||
handle_search(data.query, data.page)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
menu = Menu:open({
|
|
||||||
type = menu_type .. '-result',
|
|
||||||
search_style = 'disabled',
|
|
||||||
items = {{icon = 'spinner', align = 'center', selectable = false, muted = true}},
|
|
||||||
}, function() end)
|
|
||||||
|
|
||||||
local args = itable_join({config.ziggy_path, 'download-subtitles'}, credentials, {
|
|
||||||
'--file-id', tostring(data.id),
|
|
||||||
'--destination', destination_directory,
|
|
||||||
})
|
|
||||||
|
|
||||||
mp.command_native_async({
|
|
||||||
name = 'subprocess',
|
|
||||||
capture_stderr = true,
|
|
||||||
capture_stdout = true,
|
|
||||||
playback_only = false,
|
|
||||||
args = args,
|
|
||||||
}, function(success, result, error)
|
|
||||||
if not menu:is_alive() then return end
|
|
||||||
|
|
||||||
local data = ensure_response_data(success, result, error, function(data)
|
|
||||||
return type(data.file) == 'string'
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not data then return end
|
|
||||||
|
|
||||||
load_track('sub', data.file)
|
|
||||||
|
|
||||||
menu:update_items({
|
|
||||||
{
|
|
||||||
title = t('Subtitles loaded & enabled'),
|
|
||||||
bold = true,
|
|
||||||
icon = 'check',
|
|
||||||
selectable = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title = t('Remaining downloads today: %s', data.remaining .. '/' .. data.total),
|
|
||||||
italic = true,
|
|
||||||
muted = true,
|
|
||||||
icon = 'file_download',
|
|
||||||
selectable = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title = t('Resets in: %s', data.reset_time),
|
|
||||||
italic = true,
|
|
||||||
muted = true,
|
|
||||||
icon = 'schedule',
|
|
||||||
selectable = false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param query string
|
|
||||||
---@param page number|nil
|
|
||||||
handle_search = function(query, page)
|
|
||||||
if not menu:is_alive() then return end
|
|
||||||
page = math.max(1, type(page) == 'number' and round(page) or 1)
|
|
||||||
|
|
||||||
menu:update_items({{icon = 'spinner', align = 'center', selectable = false, muted = true}})
|
|
||||||
|
|
||||||
local args = itable_join({config.ziggy_path, 'search-subtitles'}, credentials)
|
|
||||||
|
|
||||||
local languages = itable_filter(get_languages(), function(lang) return lang:match('.json$') == nil end)
|
|
||||||
args[#args + 1] = '--languages'
|
|
||||||
args[#args + 1] = table.concat(table_keys(create_set(languages)), ',') -- deduplicates stuff like `en,eng,en`
|
|
||||||
|
|
||||||
args[#args + 1] = '--page'
|
|
||||||
args[#args + 1] = tostring(page)
|
|
||||||
|
|
||||||
if file_path then
|
|
||||||
args[#args + 1] = '--hash'
|
|
||||||
args[#args + 1] = file_path
|
|
||||||
end
|
|
||||||
|
|
||||||
if query and #query > 0 then
|
|
||||||
args[#args + 1] = '--query'
|
|
||||||
args[#args + 1] = query
|
|
||||||
end
|
|
||||||
|
|
||||||
mp.command_native_async({
|
|
||||||
name = 'subprocess',
|
|
||||||
capture_stderr = true,
|
|
||||||
capture_stdout = true,
|
|
||||||
playback_only = false,
|
|
||||||
args = args,
|
|
||||||
}, function(success, result, error)
|
|
||||||
if not menu:is_alive() then return end
|
|
||||||
|
|
||||||
local data = ensure_response_data(success, result, error, function(data)
|
|
||||||
return type(data.data) == 'table' and data.page and data.total_pages
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not data then return end
|
|
||||||
|
|
||||||
local subs = itable_filter(data.data, function(sub)
|
|
||||||
return sub and sub.attributes and sub.attributes.release and type(sub.attributes.files) == 'table' and
|
|
||||||
#sub.attributes.files > 0
|
|
||||||
end)
|
|
||||||
local items = itable_map(subs, function(sub)
|
|
||||||
local hints = {sub.attributes.language}
|
|
||||||
if sub.attributes.foreign_parts_only then hints[#hints + 1] = t('foreign parts only') end
|
|
||||||
if sub.attributes.hearing_impaired then hints[#hints + 1] = t('hearing impaired') end
|
|
||||||
return {
|
|
||||||
title = sub.attributes.release,
|
|
||||||
hint = table.concat(hints, ', '),
|
|
||||||
value = {kind = 'file', id = sub.attributes.files[1].file_id},
|
|
||||||
keep_open = true,
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
if #items == 0 then
|
|
||||||
items = {
|
|
||||||
{title = t('no results'), align = 'center', muted = true, italic = true, selectable = false},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
if data.page > 1 then
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = t('Previous page'),
|
|
||||||
align = 'center',
|
|
||||||
bold = true,
|
|
||||||
italic = true,
|
|
||||||
icon = 'navigate_before',
|
|
||||||
keep_open = true,
|
|
||||||
value = {kind = 'page', query = query, page = data.page - 1},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
if data.page < data.total_pages then
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = t('Next page'),
|
|
||||||
align = 'center',
|
|
||||||
bold = true,
|
|
||||||
italic = true,
|
|
||||||
icon = 'navigate_next',
|
|
||||||
keep_open = true,
|
|
||||||
value = {kind = 'page', query = query, page = data.page + 1},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
menu:update_items(items)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local initial_items = {
|
|
||||||
{title = t('%s to search', 'ctrl+enter'), align = 'center', muted = true, italic = true, selectable = false},
|
|
||||||
}
|
|
||||||
|
|
||||||
menu = Menu:open(
|
|
||||||
{
|
|
||||||
type = menu_type,
|
|
||||||
title = t('enter query'),
|
|
||||||
items = initial_items,
|
|
||||||
search_style = 'palette',
|
|
||||||
on_search = handle_search,
|
|
||||||
search_debounce = 'submit',
|
|
||||||
search_suggestion = search_suggestion,
|
|
||||||
},
|
|
||||||
handle_select
|
|
||||||
)
|
|
||||||
end
|
|
167
multimedia/.config/mpv/scripts/uosc_repo/.editorconfig
Normal file
167
multimedia/.config/mpv/scripts/uosc_repo/.editorconfig
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# see https://github.com/CppCXY/EmmyLuaCodeStyle
|
||||||
|
[*.lua]
|
||||||
|
# [basic]
|
||||||
|
|
||||||
|
# optional space/tab
|
||||||
|
indent_style = tab
|
||||||
|
# if indent_style is space, this is valid
|
||||||
|
indent_size = 4
|
||||||
|
# if indent_style is tab, this is valid
|
||||||
|
tab_width = 4
|
||||||
|
# none/single/double
|
||||||
|
quote_style = single
|
||||||
|
|
||||||
|
continuation_indent = 4
|
||||||
|
|
||||||
|
# this mean utf8 length , if this is 'unset' then the line width is no longer checked
|
||||||
|
# this option decides when to chopdown the code
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
# optional crlf/lf/cr/auto, if it is 'auto', in windows it is crlf other platforms are lf
|
||||||
|
# in neovim the value 'auto' is not a valid option, please use 'unset'
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
# none/ comma / semicolon / only_kv_colon
|
||||||
|
table_separator_style = comma
|
||||||
|
|
||||||
|
#optional keep/never/always/smart
|
||||||
|
trailing_table_separator = smart
|
||||||
|
|
||||||
|
# keep/remove/remove_table_only/remove_string_only
|
||||||
|
call_arg_parentheses = keep
|
||||||
|
|
||||||
|
detect_end_of_line = false
|
||||||
|
|
||||||
|
# this will check text end with new line
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# [space]
|
||||||
|
space_around_table_field_list = false
|
||||||
|
|
||||||
|
space_before_attribute = false
|
||||||
|
|
||||||
|
space_before_function_open_parenthesis = false
|
||||||
|
|
||||||
|
space_before_function_call_open_parenthesis = false
|
||||||
|
|
||||||
|
space_before_closure_open_parenthesis = false
|
||||||
|
|
||||||
|
# optional always/only_string/only_table/none
|
||||||
|
# or true/false
|
||||||
|
space_before_function_call_single_arg = always
|
||||||
|
|
||||||
|
space_before_open_square_bracket = false
|
||||||
|
|
||||||
|
space_inside_function_call_parentheses = false
|
||||||
|
|
||||||
|
space_inside_function_param_list_parentheses = false
|
||||||
|
|
||||||
|
space_inside_square_brackets = false
|
||||||
|
|
||||||
|
# like t[#t+1] = 1
|
||||||
|
space_around_table_append_operator = false
|
||||||
|
|
||||||
|
ignore_spaces_inside_function_call = false
|
||||||
|
|
||||||
|
space_before_inline_comment = 1
|
||||||
|
|
||||||
|
# [operator space]
|
||||||
|
space_around_math_operator = true
|
||||||
|
|
||||||
|
space_after_comma = true
|
||||||
|
|
||||||
|
space_after_comma_in_for_statement = true
|
||||||
|
|
||||||
|
# true/false or none/always/no_space_asym
|
||||||
|
space_around_concat_operator = true
|
||||||
|
|
||||||
|
space_around_logical_operator = true
|
||||||
|
|
||||||
|
# true/false or none/always/no_space_asym
|
||||||
|
space_around_assign_operator = true
|
||||||
|
|
||||||
|
# [align]
|
||||||
|
|
||||||
|
align_call_args = false
|
||||||
|
|
||||||
|
align_function_params = false
|
||||||
|
|
||||||
|
align_continuous_assign_statement = false
|
||||||
|
|
||||||
|
align_continuous_rect_table_field = false
|
||||||
|
|
||||||
|
align_continuous_line_space = 2
|
||||||
|
|
||||||
|
align_if_branch = false
|
||||||
|
|
||||||
|
# option none / always / contain_curly/
|
||||||
|
align_array_table = none
|
||||||
|
|
||||||
|
align_continuous_similar_call_args = false
|
||||||
|
|
||||||
|
align_continuous_inline_comment = false
|
||||||
|
|
||||||
|
# option none / always / only_call_stmt
|
||||||
|
align_chain_expr = none
|
||||||
|
|
||||||
|
# [indent]
|
||||||
|
|
||||||
|
never_indent_before_if_condition = false
|
||||||
|
|
||||||
|
never_indent_comment_on_if_branch = true
|
||||||
|
|
||||||
|
keep_indents_on_empty_lines = false
|
||||||
|
|
||||||
|
# [line space]
|
||||||
|
|
||||||
|
# The following configuration supports four expressions
|
||||||
|
# keep
|
||||||
|
# fixed(n)
|
||||||
|
# min(n)
|
||||||
|
# max(n)
|
||||||
|
# for eg. min(2)
|
||||||
|
|
||||||
|
line_space_after_if_statement = keep
|
||||||
|
|
||||||
|
line_space_after_do_statement = keep
|
||||||
|
|
||||||
|
line_space_after_while_statement = keep
|
||||||
|
|
||||||
|
line_space_after_repeat_statement = keep
|
||||||
|
|
||||||
|
line_space_after_for_statement = keep
|
||||||
|
|
||||||
|
line_space_after_local_or_assign_statement = keep
|
||||||
|
|
||||||
|
line_space_after_function_statement = keep
|
||||||
|
|
||||||
|
line_space_after_expression_statement = keep
|
||||||
|
|
||||||
|
line_space_after_comment = keep
|
||||||
|
|
||||||
|
line_space_around_block = fixed(1)
|
||||||
|
|
||||||
|
# [line break]
|
||||||
|
break_all_list_when_line_exceed = false
|
||||||
|
|
||||||
|
auto_collapse_lines = false
|
||||||
|
|
||||||
|
break_before_braces = false
|
||||||
|
|
||||||
|
# [preference]
|
||||||
|
ignore_space_after_colon = false
|
||||||
|
|
||||||
|
remove_call_expression_list_finish_comma = false
|
||||||
|
|
||||||
|
# keep / always / same_line / repalce_with_newline
|
||||||
|
end_statement_with_semicolon = keep
|
3
multimedia/.config/mpv/scripts/uosc_repo/.gitignore
vendored
Normal file
3
multimedia/.config/mpv/scripts/uosc_repo/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
src/uosc/bin
|
||||||
|
release
|
||||||
|
*.zip
|
502
multimedia/.config/mpv/scripts/uosc_repo/LICENSE.LGPL
Normal file
502
multimedia/.config/mpv/scripts/uosc_repo/LICENSE.LGPL
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
545
multimedia/.config/mpv/scripts/uosc_repo/README.md
Normal file
545
multimedia/.config/mpv/scripts/uosc_repo/README.md
Normal file
|
@ -0,0 +1,545 @@
|
||||||
|
<div align="center">
|
||||||
|
<h1>uosc</h1>
|
||||||
|
<p>
|
||||||
|
Feature-rich minimalist proximity-based UI for <a href="https://mpv.io">MPV player</a>.
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<a href="https://user-images.githubusercontent.com/47283320/195073006-bfa72bcc-89d2-4dc7-b8dc-f3c13273910c.webm"><img src="https://github.com/tomasklaen/uosc/assets/47283320/9f99f2ae-3b65-4935-8af3-8b80c605f022" alt="Preview screenshot"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- UI elements hide and show based on their proximity to cursor instead of every time mouse moves. This provides 100% control over when you see the UI and when you don't. Click on the preview above to see it in action.
|
||||||
|
- When timeline is unused, it can minimize itself into a small discrete progress bar.
|
||||||
|
- Build your own context menu with nesting support by editing your `input.conf` file.
|
||||||
|
- Configurable controls bar.
|
||||||
|
- Fast and efficient thumbnails with [thumbfast](https://github.com/po5/thumbfast) integration.
|
||||||
|
- UIs for:
|
||||||
|
- Selecting subtitle/audio/video track.
|
||||||
|
- [Downloading subtitles](#download-subtitles) from [Open Subtitles](https://www.opensubtitles.com).
|
||||||
|
- Loading external subtitles.
|
||||||
|
- Selecting stream quality.
|
||||||
|
- Quick directory and playlist navigation.
|
||||||
|
- All menus are instantly searchable. Just start typing.
|
||||||
|
- Mouse scroll wheel does multiple things depending on what is the cursor hovering over:
|
||||||
|
- Timeline: seek by `timeline_step` seconds per scroll.
|
||||||
|
- Volume bar: change volume by `volume_step` per scroll.
|
||||||
|
- Speed bar: change speed by `speed_step` per scroll.
|
||||||
|
- Just hovering video with no UI widget below cursor: your configured wheel bindings from `input.conf`.
|
||||||
|
- Right click on volume or speed elements to reset them.
|
||||||
|
- Transforming chapters into timeline ranges (the red portion of the timeline in the preview).
|
||||||
|
- A lot of useful options and commands to bind keys to.
|
||||||
|
- [API for 3rd party scripts](https://github.com/tomasklaen/uosc/wiki) to extend, or use uosc to render their menus.
|
||||||
|
|
||||||
|
[Changelog](https://github.com/tomasklaen/uosc/releases).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. These commands will install or update **uosc** and place a default `uosc.conf` file into `script-opts` if it doesn't exist already.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
_Optional, needed to run a remote script the first time if not enabled already:_
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
_**NOTE**: If this command is run in an mpv installation directory with `portable_config`, it'll install there instead of `AppData`._
|
||||||
|
|
||||||
|
_**NOTE2**: The downloaded archive might trigger false positives in some antiviruses. This is explained in [FAQ below](#why-is-the-release-reported-as-malicious-by-some-antiviruses)._
|
||||||
|
|
||||||
|
### Linux & macOS
|
||||||
|
|
||||||
|
_Requires **curl** and **unzip**._
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux, we try to detect what package manager variant of the config location you're using, with precedent being:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.var/app/io.mpv.Mpv (flatpak)
|
||||||
|
~/snap/mpv
|
||||||
|
~/snap/mpv-wayland
|
||||||
|
~/.config/mpv
|
||||||
|
```
|
||||||
|
|
||||||
|
To install into any of these locations, make sure the ones above it don't exist.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
1. Download & extract [`uosc.zip`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip) into your mpv config directory. (_See the [documentation of mpv config locations](https://mpv.io/manual/master/#files)._)
|
||||||
|
|
||||||
|
2. If you don't have it already, download & extract [`uosc.conf`](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf) into `script-opts` inside your mpv config directory. It contains all of uosc options along with their default values and documentation.
|
||||||
|
|
||||||
|
2. **OPTIONAL**: `mpv.conf` tweaks to better integrate with **uosc**:
|
||||||
|
|
||||||
|
```config
|
||||||
|
# uosc provides seeking & volume indicators (via flash-timeline and flash-volume commands)
|
||||||
|
# if you decide to use them, you don't need osd-bar
|
||||||
|
osd-bar=no
|
||||||
|
|
||||||
|
# uosc will draw its own window controls and border if you disable window border
|
||||||
|
border=no
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **OPTIONAL**: To have thumbnails in timeline, install [thumbfast](https://github.com/po5/thumbfast). No other step necessary, **uosc** integrates with it seamlessly.
|
||||||
|
|
||||||
|
4. **OPTIONAL**: If the UI feels sluggish/slow while playing video, you can remedy this _a bit_ by placing this in your `mpv.conf`:
|
||||||
|
|
||||||
|
```config
|
||||||
|
video-sync=display-resample
|
||||||
|
```
|
||||||
|
|
||||||
|
Though this does come at the cost of a little bit higher CPU/GPU load.
|
||||||
|
|
||||||
|
#### What is going on?
|
||||||
|
|
||||||
|
**uosc** places performance as one of its top priorities, but it might feel a bit sluggish because during a video playback, the UI rendering frequency is chained to its frame rate. To test this, you can pause the video which will switch refresh rate to be closer or match the frequency of your monitor, and the UI should feel smoother. This is mpv limitation, and not much we can do about it on our side.
|
||||||
|
|
||||||
|
#### Build instructions
|
||||||
|
|
||||||
|
To build ziggy (our utility binary) yourself, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/build ziggy
|
||||||
|
```
|
||||||
|
|
||||||
|
Which will run the `tools/build(.ps1)` script that builds it for each platform. It requires [go](https://go.dev/) to be installed. Source code is in `src/ziggy`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
All of the available **uosc** options with their default values are documented in [`uosc.conf`](https://github.com/tomasklaen/uosc/blob/HEAD/src/uosc.conf) file ([download](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.conf)).
|
||||||
|
|
||||||
|
To change the font, **uosc** respects the mpv's `osd-font` configuration.
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
These bindings are active when any **uosc** menu is open (main menu, playlist, load/select subtitles,...):
|
||||||
|
|
||||||
|
- `up`, `down` - Select previous/next item.
|
||||||
|
- `enter` - Activate item or submenu.
|
||||||
|
- `bs` (backspace) - Activate parent menu.
|
||||||
|
- `esc` - Close menu.
|
||||||
|
- `wheel_up`, `wheel_down` - Scroll menu.
|
||||||
|
- `pgup`, `pgdwn`, `home`, `end` - Self explanatory.
|
||||||
|
- `ctrl+f` or `\` - In case `menu_type_to_search` config option is disabled, these two trigger the menu search instead.
|
||||||
|
- `ctrl+backspace` - Delete search query by word.
|
||||||
|
- `shift+backspace` - Clear search query.
|
||||||
|
- Holding `alt` while activating an item should prevent closing the menu (this is just a guideline, not all menus behave this way).
|
||||||
|
|
||||||
|
Each menu can also add its own shortcuts and bindings for special actions on items/menu, such as `del` to delete a playlist item, `ctrl+up/down/pgup/pgdwn/home/end` to move it around, etc. These are usually also exposed as item action buttons for you to find out about them that way.
|
||||||
|
|
||||||
|
Click on a faded parent menu to go back to it.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
**uosc** provides various commands with useful features to bind your preferred keys to, or populate your menu with. These are all unbound by default.
|
||||||
|
|
||||||
|
To add a keybind to one of this commands, open your `input.conf` file and add one on a new line. The command syntax is `script-binding uosc/{command-name}`.
|
||||||
|
|
||||||
|
Example to bind the `tab` key to toggle the ui visibility:
|
||||||
|
|
||||||
|
```
|
||||||
|
tab script-binding uosc/toggle-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
|
||||||
|
#### `toggle-ui`
|
||||||
|
|
||||||
|
Makes the whole UI visible until you call this command again. Useful for peeking remaining time and such while watching.
|
||||||
|
|
||||||
|
There's also a `toggle-elements <ids>` message you can send to toggle one or more specific elements by specifying their names separated by comma:
|
||||||
|
|
||||||
|
```
|
||||||
|
script-message-to uosc toggle-elements timeline,speed
|
||||||
|
```
|
||||||
|
|
||||||
|
Available element IDs: `timeline`, `controls`, `volume`, `top_bar`, `speed`
|
||||||
|
|
||||||
|
Under the hood, `toggle-ui` is using `toggle-elements`, and that is in turn using the `set-min-visibility <visibility> [<ids>]` message. `<visibility>` is a `0-1` floating point. Leave out `<ids>` to set it for all elements.
|
||||||
|
|
||||||
|
#### `toggle-progress`
|
||||||
|
|
||||||
|
Toggles the timeline progress mode on/off. Progress mode is an always visible thin version of timeline with no text labels. It can be configured using the `progress*` config options.
|
||||||
|
|
||||||
|
#### `toggle-title`
|
||||||
|
|
||||||
|
Toggles the top bar title between main and alternative title's. This can also be done by clicking on the top bar.
|
||||||
|
|
||||||
|
Only relevant if top bar is enabled, `top_bar_alt_title` is configured, and `top_bar_alt_title_place` is `toggle`.
|
||||||
|
|
||||||
|
#### `flash-ui`
|
||||||
|
|
||||||
|
Command(s) to briefly flash the whole UI. Elements are revealed for a second and then fade away.
|
||||||
|
|
||||||
|
To flash individual elements, you can use: `flash-timeline`, `flash-progress`, `flash-top-bar`, `flash-volume`, `flash-speed`, `flash-pause-indicator`, `decide-pause-indicator`
|
||||||
|
|
||||||
|
There's also a `flash-elements <ids>` message you can use to flash one or more specific elements. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
script-message-to uosc flash-elements timeline,speed
|
||||||
|
```
|
||||||
|
|
||||||
|
Available element IDs: `timeline`, `progress`, `controls`, `volume`, `top_bar`, `speed`, `pause_indicator`
|
||||||
|
|
||||||
|
This is useful in combination with other commands that modify values represented by flashed elements, for example: flashing volume element when changing the volume.
|
||||||
|
|
||||||
|
You can use it in your bindings like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
space cycle pause; script-binding uosc/flash-pause-indicator
|
||||||
|
right seek 5
|
||||||
|
left seek -5
|
||||||
|
shift+right seek 30; script-binding uosc/flash-timeline
|
||||||
|
shift+left seek -30; script-binding uosc/flash-timeline
|
||||||
|
m no-osd cycle mute; script-binding uosc/flash-volume
|
||||||
|
up no-osd add volume 10; script-binding uosc/flash-volume
|
||||||
|
down no-osd add volume -10; script-binding uosc/flash-volume
|
||||||
|
[ no-osd add speed -0.25; script-binding uosc/flash-speed
|
||||||
|
] no-osd add speed 0.25; script-binding uosc/flash-speed
|
||||||
|
\ no-osd set speed 1; script-binding uosc/flash-speed
|
||||||
|
> script-binding uosc/next; script-message-to uosc flash-elements top_bar,timeline
|
||||||
|
< script-binding uosc/prev; script-message-to uosc flash-elements top_bar,timeline
|
||||||
|
```
|
||||||
|
|
||||||
|
Case for `(flash/decide)-pause-indicator`: mpv handles frame stepping forward by briefly resuming the video, which causes pause indicator to flash, and none likes that when they are trying to compare frames. The solution is to enable manual pause indicator (`pause_indicator=manual`) and use `flash-pause-indicator` (for a brief flash) or `decide-pause-indicator` (for a static indicator) as a secondary command to appropriate bindings.
|
||||||
|
|
||||||
|
#### `menu`
|
||||||
|
|
||||||
|
Toggles default menu. Read [Menu](#menu-1) section below to find out how to fill it up with items you want there.
|
||||||
|
|
||||||
|
Note: there's also a `menu-blurred` command that opens a menu without pre-selecting the 1st item, suitable for commands triggered with a mouse, such as control bar buttons.
|
||||||
|
|
||||||
|
#### `subtitles`, `audio`, `video`
|
||||||
|
|
||||||
|
Menus to select a track of a requested type.
|
||||||
|
|
||||||
|
#### `load-subtitles`, `load-audio`, `load-video`
|
||||||
|
|
||||||
|
Displays a file explorer with directory navigation to load a requested track type.
|
||||||
|
|
||||||
|
For subtitles, the explorer only displays file types defined in `subtitle_types` option. For audio and video, the ones defined in `video_types` and `audio_types` are displayed.
|
||||||
|
|
||||||
|
#### `download-subtitles`
|
||||||
|
|
||||||
|
A menu to search and download subtitles from [Open Subtitles](https://www.opensubtitles.com). It can also be opened by selecting the **Download** option in `subtitles` menu.
|
||||||
|
|
||||||
|
We fetch results for languages defined in *uosc**'s `languages` option, which defaults to your mpv `slang` configuration.
|
||||||
|
|
||||||
|
We also hash the current file and send the hash to Open Subtitles so you can search even with empty query and if your file is known, you'll get subtitles exactly for it.
|
||||||
|
|
||||||
|
Subtitles will be downloaded to the same directory as currently opened file, or `~~/subtitles` (folder in your mpv config directory) if playing a URL.
|
||||||
|
|
||||||
|
Current Open Subtitles limit for unauthenticated requests is **5 download per day**, but searching is unlimited. Authentication raises downloads to 10, which doesn't feel like it's worth the effort of implementing it, so currently there's no way to authenticate. 5 downloads per day seems sufficient for most use cases anyway, as if you need more, you should probably just deal with it in the browser beforehand so you don't have to fiddle with the subtitle downloading menu every time you start playing a new file.
|
||||||
|
|
||||||
|
#### `playlist`
|
||||||
|
|
||||||
|
Playlist navigation.
|
||||||
|
|
||||||
|
#### `chapters`
|
||||||
|
|
||||||
|
Chapter navigation.
|
||||||
|
|
||||||
|
#### `editions`
|
||||||
|
|
||||||
|
Editions menu. Editions are different video cuts available in some mkv files.
|
||||||
|
|
||||||
|
#### `stream-quality`
|
||||||
|
|
||||||
|
Switch stream quality. This is just a basic re-assignment of `ytdl-format` mpv property from predefined options (configurable with `stream_quality_options`) and video reload, there is no fetching of available formats going on.
|
||||||
|
|
||||||
|
#### `keybinds`
|
||||||
|
|
||||||
|
Displays a command palette menu with all currently active keybindings (defined in your `input.conf` file, or registered by scripts). Useful to check what command is bound to what shortcut, or the other way around.
|
||||||
|
|
||||||
|
#### `open-file`
|
||||||
|
|
||||||
|
Open file menu. Browsing starts in current file directory, or user directory when file not available. The explorer only displays file types defined in the `video_types`, `audio_types`, and `image_types` options.
|
||||||
|
|
||||||
|
You can use `alt+enter` or `alt+click` to load the whole directory in mpv instead of navigating its contents.
|
||||||
|
You can also use `ctrl+enter` or `ctrl+click` to append a file or directory to the playlist.
|
||||||
|
|
||||||
|
#### `items`
|
||||||
|
|
||||||
|
Opens `playlist` menu when playlist exists, or `open-file` menu otherwise.
|
||||||
|
|
||||||
|
#### `next`, `prev`
|
||||||
|
|
||||||
|
Open next/previous item in playlist, or file in current directory when there is no playlist. Enable `loop-playlist` to loop around.
|
||||||
|
|
||||||
|
#### `first`, `last`
|
||||||
|
|
||||||
|
Open first/last item in playlist, or file in current directory when there is no playlist.
|
||||||
|
|
||||||
|
#### `next-file`, `prev-file`
|
||||||
|
|
||||||
|
Open next/prev file in current directory. Enable `loop-playlist` to loop around
|
||||||
|
|
||||||
|
#### `first-file`, `last-file`
|
||||||
|
|
||||||
|
Open first/last file in current directory.
|
||||||
|
|
||||||
|
#### `shuffle`
|
||||||
|
|
||||||
|
Toggle uosc's playlist/directory shuffle mode on or off.
|
||||||
|
|
||||||
|
This simply makes the next selected playlist or directory item be random, like the shuffle function of most other players. This does not modify the actual playlist in any way, in contrast to the mpv built-in command `playlist-shuffle`.
|
||||||
|
|
||||||
|
#### `delete-file-next`
|
||||||
|
|
||||||
|
Delete currently playing file and start next file in playlist (if there is a playlist) or current directory.
|
||||||
|
|
||||||
|
Useful when watching episodic content.
|
||||||
|
|
||||||
|
#### `delete-file-quit`
|
||||||
|
|
||||||
|
Delete currently playing file and quit mpv.
|
||||||
|
|
||||||
|
#### `show-in-directory`
|
||||||
|
|
||||||
|
Show current file in your operating systems' file explorer.
|
||||||
|
|
||||||
|
#### `audio-device`
|
||||||
|
|
||||||
|
Switch audio output device.
|
||||||
|
|
||||||
|
#### `paste`, `paste-to-open`, `paste-to-playlist`
|
||||||
|
|
||||||
|
Commands to paste path or URL in clipboard to either open immediately, or append to playlist.
|
||||||
|
|
||||||
|
`paste` will add to playlist if there's any (`playlist-count > 1`), or open immediately otherwise.
|
||||||
|
|
||||||
|
`paste-to-playlist` will also open the pasted file if mpv is idle (no file open).
|
||||||
|
|
||||||
|
Note: there are alternative ways to open stuff from clipboard without the need to bind these commands:
|
||||||
|
|
||||||
|
- When `open-file` menu is open → `ctrl+v` to open path/URL in clipboard.
|
||||||
|
- When `playlist` menu is open → `ctrl+v` to add path/URL in clipboard to playlist.
|
||||||
|
- When any track menu (`subtitles`, `audio`, `video`) is open → `ctrl+v` to add path/URL in clipboard as a new track.
|
||||||
|
|
||||||
|
#### `copy-to-clipboard`
|
||||||
|
|
||||||
|
Copy currently open path or URL to clipboard.
|
||||||
|
|
||||||
|
Additionally, you can also press `ctrl+c` to copy path of a selected item in `playlist` or all directory listing menus.
|
||||||
|
|
||||||
|
#### `open-config-directory`
|
||||||
|
|
||||||
|
Open directory with `mpv.conf` in file explorer.
|
||||||
|
|
||||||
|
#### `update`
|
||||||
|
|
||||||
|
Updates uosc to the latest stable release right from the UI. Available in the "Utils" section of default menu .
|
||||||
|
|
||||||
|
Supported environments:
|
||||||
|
|
||||||
|
| Env | Works | Note |
|
||||||
|
|:---|:---:|---|
|
||||||
|
| Windows | ✔️ | _Not tested on older PowerShell versions. You might need to `Set-ExecutionPolicy` from the install instructions and install with the terminal command first._ |
|
||||||
|
| Linux (apt) | ✔️ | |
|
||||||
|
| Linux (flatpak) | ✔️ | |
|
||||||
|
| Linux (snap) | ❌ | We're not allowed to access commands like `curl` even if they're installed. (Or at least this is what I think the issue is.) |
|
||||||
|
| MacOS | ❌ | `(23) Failed writing body` error, whatever that means. |
|
||||||
|
|
||||||
|
If you know about a solution to fix self-updater for any of the currently broken environments, please make an issue/PR and share it with us!
|
||||||
|
|
||||||
|
**Note:** The terminal commands from install instructions still work fine everywhere, so you can use those to update instead.
|
||||||
|
|
||||||
|
## Menu
|
||||||
|
|
||||||
|
**uosc** provides a way to build, display, and use your own menu. By default it displays a pre-configured menu with common actions.
|
||||||
|
|
||||||
|
To display the menu, add **uosc**'s `menu` command to a key of your choice. Example to bind it to **right click** and **menu** buttons:
|
||||||
|
|
||||||
|
```
|
||||||
|
mbtn_right script-binding uosc/menu
|
||||||
|
menu script-binding uosc/menu
|
||||||
|
```
|
||||||
|
|
||||||
|
To display a submenu, send a `show-submenu` message to **uosc** with first parameter specifying menu ID. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
R script-message-to uosc show-submenu "Utils > Aspect ratio"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The **menu** key is the one nobody uses between the **win** and **right_ctrl** keys (it might not be on your keyboard).
|
||||||
|
|
||||||
|
### Adding items to menu
|
||||||
|
|
||||||
|
Adding items to menu is facilitated by commenting your keybinds in `input.conf` with special comment syntax. **uosc** will than parse this file and build the context menu out of it.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
Comment has to be at the end of the line with the binding.
|
||||||
|
|
||||||
|
Comment has to start with `#!` (or `#menu:`).
|
||||||
|
|
||||||
|
Text after `#!` is an item title.
|
||||||
|
|
||||||
|
Title can be split with `>` to define nested menus. There is no limit on nesting.
|
||||||
|
|
||||||
|
Use `#` instead of a key if you don't necessarily want to bind a key to a command, but still want it in the menu.
|
||||||
|
|
||||||
|
If multiple menu items with the same command are defined, **uosc** will concatenate them into one item and just display all available shortcuts as that items' hint, while using the title of the first defined item.
|
||||||
|
|
||||||
|
Menu items are displayed in the order they are defined in `input.conf` file.
|
||||||
|
|
||||||
|
The command `ignore` does not result in a menu item, however all the folders leading up to it will still be created.
|
||||||
|
This allows more flexible structuring of the `input.conf` file.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Adds a menu item to load subtitles:
|
||||||
|
|
||||||
|
```
|
||||||
|
alt+s script-binding uosc/load-subtitles #! Load subtitles
|
||||||
|
```
|
||||||
|
|
||||||
|
Adds a stay-on-top toggle with no keybind:
|
||||||
|
|
||||||
|
```
|
||||||
|
# cycle ontop #! Toggle on-top
|
||||||
|
```
|
||||||
|
|
||||||
|
Define and display multiple shortcuts in single items' menu hint (items with same command get concatenated):
|
||||||
|
|
||||||
|
```
|
||||||
|
esc quit #! Quit
|
||||||
|
q quit #!
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a folder without defining any of its contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
# ignore #! Folder title >
|
||||||
|
```
|
||||||
|
|
||||||
|
Define an un-selectable, muted, and italic title item by using `#` as key, and omitting the command:
|
||||||
|
|
||||||
|
```
|
||||||
|
# #! Title
|
||||||
|
# #! Section > Title
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a separator between previous and next items by doing the same, but using `---` as title:
|
||||||
|
|
||||||
|
```
|
||||||
|
# #! ---
|
||||||
|
# #! Section > ---
|
||||||
|
```
|
||||||
|
|
||||||
|
Example context menu:
|
||||||
|
|
||||||
|
This is the default pre-configured menu if none is defined in your `input.conf`, but with added shortcuts. To both pause & move the window with left mouse button, so that you can have the menu on the right one, enable `click_threshold` in `uosc.conf` (see default `uosc.conf` for example/docs).
|
||||||
|
|
||||||
|
```
|
||||||
|
menu script-binding uosc/menu
|
||||||
|
mbtn_right script-binding uosc/menu
|
||||||
|
s script-binding uosc/subtitles #! Subtitles
|
||||||
|
a script-binding uosc/audio #! Audio tracks
|
||||||
|
q script-binding uosc/stream-quality #! Stream quality
|
||||||
|
p script-binding uosc/items #! Playlist
|
||||||
|
c script-binding uosc/chapters #! Chapters
|
||||||
|
> script-binding uosc/next #! Navigation > Next
|
||||||
|
< script-binding uosc/prev #! Navigation > Prev
|
||||||
|
alt+> script-binding uosc/delete-file-next #! Navigation > Delete file & Next
|
||||||
|
alt+< script-binding uosc/delete-file-prev #! Navigation > Delete file & Prev
|
||||||
|
alt+esc script-binding uosc/delete-file-quit #! Navigation > Delete file & Quit
|
||||||
|
o script-binding uosc/open-file #! Navigation > Open file
|
||||||
|
# set video-aspect-override "-1" #! Utils > Aspect ratio > Default
|
||||||
|
# set video-aspect-override "16:9" #! Utils > Aspect ratio > 16:9
|
||||||
|
# set video-aspect-override "4:3" #! Utils > Aspect ratio > 4:3
|
||||||
|
# set video-aspect-override "2.35:1" #! Utils > Aspect ratio > 2.35:1
|
||||||
|
# script-binding uosc/audio-device #! Utils > Audio devices
|
||||||
|
# script-binding uosc/editions #! Utils > Editions
|
||||||
|
ctrl+s async screenshot #! Utils > Screenshot
|
||||||
|
alt+i script-binding uosc/keybinds #! Utils > Key bindings
|
||||||
|
O script-binding uosc/show-in-directory #! Utils > Show in directory
|
||||||
|
# script-binding uosc/open-config-directory #! Utils > Open config directory
|
||||||
|
# script-binding uosc/update #! Utils > Update uosc
|
||||||
|
esc quit #! Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all the commands you can bind keys or menu items to, refer to [mpv's list of input commands documentation](https://mpv.io/manual/master/#list-of-input-commands).
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
**uosc** listens on some messages that can be sent with `script-message-to uosc` command. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
R script-message-to uosc show-submenu "Utils > Aspect ratio"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `show-submenu <menu_id>`, `show-submenu-blurred <menu_id>`
|
||||||
|
|
||||||
|
Opens one of the submenus defined in `input.conf` (read on how to build those in the Menu documentation above). To prevent 1st item being preselected, use `show-submenu-blurred` instead.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
|
||||||
|
##### `<menu_id>`
|
||||||
|
|
||||||
|
ID (title) of the submenu, including `>` subsections as defined in `input.conf`. It has to be match the title exactly.
|
||||||
|
|
||||||
|
## Scripting API
|
||||||
|
|
||||||
|
3rd party script developers can use our messaging API to integrate with uosc, or use it to render their menus. Documentation is available in [uosc Wiki](https://github.com/tomasklaen/uosc/wiki).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
If you want to help localizing uosc by either adding a new locale or fixing one that is not up to date, start by running this while in the repository root:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/intl languagecode
|
||||||
|
```
|
||||||
|
|
||||||
|
`languagecode` can be any existing locale in `src/uosc/intl/` directory, or any [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If it doesn't exist yet, the `intl` tool will create it.
|
||||||
|
|
||||||
|
This will parse the codebase for localization strings and use them to either update existing locale by removing unused and setting untranslated strings to `null`, or create a new one with all `null` strings.
|
||||||
|
|
||||||
|
You can then navigate to `src/uosc/intl/languagecode.json` and start translating.
|
||||||
|
|
||||||
|
### Setting up binaries
|
||||||
|
|
||||||
|
If you want to test or work on something that involves ziggy (our multitool binary, currently handles searching & downloading subtitles), you first need to build it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/build ziggy
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires [`go`](https://go.dev/dl/) to be installed and in path. If you don't want to bother with installing go, and there were no changes to ziggy, you can just use the binaries from [latest release](https://github.com/tomasklaen/uosc/releases/latest/download/uosc.zip). Place folder `scripts/uosc/bin` from `uosc.zip` into `src/uosc/bin`.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
#### Why is the release zip size in megabytes? Isn't this just a lua script?
|
||||||
|
|
||||||
|
We are limited in what we can do in mpv's lua scripting environment. To work around this, we include a binary tool (one for each platform), that we call to handle stuff we can't do in lua. Currently this means searching & downloading subtitles, accessing clipboard data, and in future might improve self updating, and potentially other things.
|
||||||
|
|
||||||
|
Other scripts usually choose to go the route of adding python scripts and requiring users to install the runtime. I don't like this as I want the installation process to be as seamless and as painless as possible. I also don't want to contribute to potential python version mismatch issues, because one tool depends on 2.7, other latest 3, and this one 3.9 only and no newer (real world scenario that happened to me), now have fun reconciling this. Depending on external runtimes can be a mess, and shipping a stable, tiny, and fast binary that users don't even have to know about is imo more preferable than having unstable external dependencies and additional installation steps that force everyone to install and manage hundreds of megabytes big runtimes in global `PATH`.
|
||||||
|
|
||||||
|
#### Why don't you have `uosc-{platform}.zip` releases and only include binaries for the concerned platform in each?
|
||||||
|
|
||||||
|
Then you wouldn't be able to sync your mpv config between platforms and everything _just work_.
|
||||||
|
|
||||||
|
#### Why is the release reported as malicious by some antiviruses?
|
||||||
|
|
||||||
|
Some antiviruses find our binaries suspicious due to the way go packages them. This is a known issue with all go binaries (https://go.dev/doc/faq#virus). I think the only way to solve that would be to sign them (not 100% sure though), but I'm not paying to work on free stuff. If anyone is bothered by this, and would be willing to donate a code signing certificate, let me know.
|
||||||
|
|
||||||
|
If you want to check the binaries are safe, the code is in `src/ziggy`, and you can build them yourself by running `tools/build ziggy` in the repository root.
|
||||||
|
|
||||||
|
We might eventually rewrite it in something else.
|
||||||
|
|
||||||
|
#### 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.
|
12
multimedia/.config/mpv/scripts/uosc_repo/go.mod
Normal file
12
multimedia/.config/mpv/scripts/uosc_repo/go.mod
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module uosc/bins
|
||||||
|
|
||||||
|
go 1.21.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
|
k8s.io/apimachinery v0.28.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.28.0 // indirect
|
11
multimedia/.config/mpv/scripts/uosc_repo/go.sum
Normal file
11
multimedia/.config/mpv/scripts/uosc_repo/go.sum
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
|
||||||
|
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
|
|
@ -1,6 +1,6 @@
|
||||||
local Element = require('elements/Element')
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
|
---@alias ButtonProps {icon: string; on_click?: function; is_clickable?: boolean; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
|
||||||
|
|
||||||
---@class Button : Element
|
---@class Button : Element
|
||||||
local Button = class(Element)
|
local Button = class(Element)
|
||||||
|
@ -17,13 +17,15 @@ function Button:init(id, props)
|
||||||
self.badge = props.badge
|
self.badge = props.badge
|
||||||
self.foreground = props.foreground or fg
|
self.foreground = props.foreground or fg
|
||||||
self.background = props.background or bg
|
self.background = props.background or bg
|
||||||
---@type fun()
|
self.is_clickable = true
|
||||||
|
---@type fun()|nil
|
||||||
self.on_click = props.on_click
|
self.on_click = props.on_click
|
||||||
Element.init(self, id, props)
|
Element.init(self, id, props)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
|
function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
|
||||||
function Button:handle_cursor_click()
|
function Button:handle_cursor_click()
|
||||||
|
if not self.on_click or not self.is_clickable then return end
|
||||||
-- We delay the callback to next tick, otherwise we are risking race
|
-- We delay the callback to next tick, otherwise we are risking race
|
||||||
-- conditions as we are in the middle of event dispatching.
|
-- conditions as we are in the middle of event dispatching.
|
||||||
-- For example, handler might add a menu to the end of the element stack, and that
|
-- For example, handler might add a menu to the end of the element stack, and that
|
||||||
|
@ -37,17 +39,20 @@ function Button:render()
|
||||||
cursor:zone('primary_click', self, function() self:handle_cursor_click() end)
|
cursor:zone('primary_click', self, function() self:handle_cursor_click() end)
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
local ass = assdraw.ass_new()
|
||||||
|
local is_clickable = self.is_clickable and self.on_click ~= nil
|
||||||
local is_hover = self.proximity_raw == 0
|
local is_hover = self.proximity_raw == 0
|
||||||
local is_hover_or_active = is_hover or self.active
|
|
||||||
local foreground = self.active and self.background or self.foreground
|
local foreground = self.active and self.background or self.foreground
|
||||||
local background = self.active and self.foreground or self.background
|
local background = self.active and self.foreground or self.background
|
||||||
|
local background_opacity = self.active and 1 or config.opacity.controls
|
||||||
|
|
||||||
|
if is_hover and is_clickable and background_opacity < 0.3 then background_opacity = 0.3 end
|
||||||
|
|
||||||
-- Background
|
-- Background
|
||||||
if is_hover_or_active or config.opacity.controls > 0 then
|
if background_opacity > 0 then
|
||||||
ass:rect(self.ax, self.ay, self.bx, self.by, {
|
ass:rect(self.ax, self.ay, self.bx, self.by, {
|
||||||
color = (self.active or not is_hover) and background or foreground,
|
color = (self.active or not is_hover) and background or foreground,
|
||||||
radius = state.radius,
|
radius = state.radius,
|
||||||
opacity = visibility * (self.active and 1 or (is_hover and 0.3 or config.opacity.controls)),
|
opacity = visibility * background_opacity,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
local Element = require('elements/Element')
|
local Element = require('elements/Element')
|
||||||
local Button = require('elements/Button')
|
local Button = require('elements/Button')
|
||||||
local CycleButton = require('elements/CycleButton')
|
local CycleButton = require('elements/CycleButton')
|
||||||
|
local ManagedButton = require('elements/ManagedButton')
|
||||||
local Speed = require('elements/Speed')
|
local Speed = require('elements/Speed')
|
||||||
|
|
||||||
-- sizing:
|
-- sizing:
|
||||||
|
@ -54,6 +55,7 @@ function Controls:init_options()
|
||||||
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?' .. t('Loop playlist'),
|
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?' .. t('Loop playlist'),
|
||||||
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?' .. t('Loop file'),
|
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?' .. t('Loop file'),
|
||||||
shuffle = 'toggle:shuffle:shuffle?' .. t('Shuffle'),
|
shuffle = 'toggle:shuffle:shuffle?' .. t('Shuffle'),
|
||||||
|
autoload = 'toggle:hdr_auto:autoload@uosc?' .. t('Autoload'),
|
||||||
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?' .. t('Fullscreen'),
|
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?' .. t('Fullscreen'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +165,19 @@ function Controls:init_options()
|
||||||
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
if badge then self:register_badge_updater(badge, element) end
|
if badge then self:register_badge_updater(badge, element) end
|
||||||
end
|
end
|
||||||
|
elseif kind == 'button' then
|
||||||
|
if #params ~= 1 then
|
||||||
|
mp.error(string.format(
|
||||||
|
'managed button needs 1 parameter, %d received: %s', #params, table.concat(params, '/')
|
||||||
|
))
|
||||||
|
else
|
||||||
|
local element = ManagedButton:new('control_' .. i, {
|
||||||
|
name = params[1],
|
||||||
|
render_order = self.render_order,
|
||||||
|
anchor_id = 'controls',
|
||||||
|
})
|
||||||
|
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
||||||
|
end
|
||||||
elseif kind == 'speed' then
|
elseif kind == 'speed' then
|
||||||
if not Elements.speed then
|
if not Elements.speed then
|
||||||
local element = Speed:new({anchor_id = 'controls', render_order = self.render_order})
|
local element = Speed:new({anchor_id = 'controls', render_order = self.render_order})
|
|
@ -3,6 +3,16 @@ local Button = require('elements/Button')
|
||||||
---@alias CycleState {value: any; icon: string; active?: boolean}
|
---@alias CycleState {value: any; icon: string; active?: boolean}
|
||||||
---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
|
---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
|
||||||
|
|
||||||
|
local function yes_no_to_boolean(value)
|
||||||
|
if type(value) ~= 'string' then return value end
|
||||||
|
local lowercase = trim(value):lower()
|
||||||
|
if lowercase == 'yes' or lowercase == 'no' then
|
||||||
|
return lowercase == 'yes'
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@class CycleButton : Button
|
---@class CycleButton : Button
|
||||||
local CycleButton = class(Button)
|
local CycleButton = class(Button)
|
||||||
|
|
||||||
|
@ -24,17 +34,29 @@ function CycleButton:init(id, props)
|
||||||
self.on_click = function()
|
self.on_click = function()
|
||||||
local new_state = self.states[self.current_state_index + 1] or self.states[1]
|
local new_state = self.states[self.current_state_index + 1] or self.states[1]
|
||||||
local new_value = new_state.value
|
local new_value = new_state.value
|
||||||
if self.owner then
|
if self.owner == 'uosc' then
|
||||||
|
if type(options[self.prop]) == 'number' then
|
||||||
|
options[self.prop] = tonumber(new_value) or 0
|
||||||
|
else
|
||||||
|
options[self.prop] = yes_no_to_boolean(new_value)
|
||||||
|
end
|
||||||
|
handle_options({[self.prop] = options[self.prop]})
|
||||||
|
elseif self.owner then
|
||||||
mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value)
|
mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value)
|
||||||
elseif is_state_prop then
|
elseif is_state_prop then
|
||||||
if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end
|
set_state(self.prop, yes_no_to_boolean(new_value))
|
||||||
set_state(self.prop, new_value)
|
|
||||||
else
|
else
|
||||||
mp.set_property(self.prop, new_value)
|
mp.set_property(self.prop, new_value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handle_change(name, value)
|
local function handle_change(name, value)
|
||||||
|
-- Removes unnecessary floating point digits from values like `2.00000`.
|
||||||
|
-- This happens when observing properties like `speed`.
|
||||||
|
if type(value) == 'string' and string.match(value, '^[%+%-]?%d+%.%d+$') then
|
||||||
|
value = tonumber(value)
|
||||||
|
end
|
||||||
|
|
||||||
value = type(value) == 'boolean' and (value and 'yes' or 'no') or tostring(value or '')
|
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)
|
local index = itable_find(self.states, function(state) return state.value == value end)
|
||||||
self.current_state_index = index or 1
|
self.current_state_index = index or 1
|
||||||
|
@ -46,8 +68,13 @@ function CycleButton:init(id, props)
|
||||||
local prop_parts = split(self.prop, '@')
|
local prop_parts = split(self.prop, '@')
|
||||||
if #prop_parts == 2 then -- External prop with a script owner
|
if #prop_parts == 2 then -- External prop with a script owner
|
||||||
self.prop, self.owner = prop_parts[1], prop_parts[2]
|
self.prop, self.owner = prop_parts[1], prop_parts[2]
|
||||||
self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end
|
if self.owner == 'uosc' then
|
||||||
handle_change(self.prop, external[self.prop])
|
self['on_options'] = function() handle_change(self.prop, options[self.prop]) end
|
||||||
|
handle_change(self.prop, options[self.prop])
|
||||||
|
else
|
||||||
|
self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end
|
||||||
|
handle_change(self.prop, external[self.prop])
|
||||||
|
end
|
||||||
elseif is_state_prop then -- uosc's state props
|
elseif is_state_prop then -- uosc's state props
|
||||||
self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end
|
self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end
|
||||||
handle_change(self.prop, state[self.prop])
|
handle_change(self.prop, state[self.prop])
|
|
@ -27,6 +27,8 @@ function Element:init(id, props)
|
||||||
self.anchor_id = nil
|
self.anchor_id = nil
|
||||||
---@type fun()[] Disposer functions called when element is destroyed.
|
---@type fun()[] Disposer functions called when element is destroyed.
|
||||||
self._disposers = {}
|
self._disposers = {}
|
||||||
|
---@type table<string,table<string, boolean>> Namespaced active key bindings. Default namespace is `_`.
|
||||||
|
self._key_bindings = {}
|
||||||
|
|
||||||
if props then table_assign(self, props) end
|
if props then table_assign(self, props) end
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ function Element:init(id, props)
|
||||||
local function getTo() return self.proximity end
|
local function getTo() return self.proximity end
|
||||||
local function onTweenEnd() self.forced_visibility = nil end
|
local function onTweenEnd() self.forced_visibility = nil end
|
||||||
if self.enabled then
|
if self.enabled then
|
||||||
self:tween_property('forced_visibility', 1, getTo, onTweenEnd)
|
self:tween_property('forced_visibility', self:get_visibility(), getTo, onTweenEnd)
|
||||||
else
|
else
|
||||||
onTweenEnd()
|
onTweenEnd()
|
||||||
end
|
end
|
||||||
|
@ -48,6 +50,7 @@ end
|
||||||
function Element:destroy()
|
function Element:destroy()
|
||||||
for _, disposer in ipairs(self._disposers) do disposer() end
|
for _, disposer in ipairs(self._disposers) do disposer() end
|
||||||
self.destroyed = true
|
self.destroyed = true
|
||||||
|
self:remove_key_bindings()
|
||||||
Elements:remove(self)
|
Elements:remove(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -191,4 +194,67 @@ function Element:observe_mp_property(name, type_or_callback, callback_maybe)
|
||||||
self:register_disposer(function() mp.unobserve_property(callback) end)
|
self:register_disposer(function() mp.unobserve_property(callback) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Adds a keybinding for the lifetime of the element, or until removed manually.
|
||||||
|
---@param key string mpv key identifier.
|
||||||
|
---@param fnFlags fun()|string|table<fun()|string> Callback, or `{callback, flags}` tuple. Callback can be just a method name, in which case it'll be wrapped in `create_action(callback)`.
|
||||||
|
---@param namespace? string Keybinding namespace. Default is `_`.
|
||||||
|
function Element:add_key_binding(key, fnFlags, namespace)
|
||||||
|
local name = self.id .. '-' .. key
|
||||||
|
local isTuple = type(fnFlags) == 'table'
|
||||||
|
local fn = (isTuple and fnFlags[1] or fnFlags)
|
||||||
|
local flags = isTuple and fnFlags[2] or nil
|
||||||
|
namespace = namespace or '_'
|
||||||
|
local names = self._key_bindings[namespace]
|
||||||
|
if not names then
|
||||||
|
names = {}
|
||||||
|
self._key_bindings[namespace] = names
|
||||||
|
end
|
||||||
|
names[name] = true
|
||||||
|
if type(fn) == 'string' then
|
||||||
|
fn = self:create_action(fn)
|
||||||
|
end
|
||||||
|
mp.add_forced_key_binding(key, name, fn, flags)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove all or only keybindings belonging to a specific namespace.
|
||||||
|
---@param namespace? string Optional keybinding namespace to remove.
|
||||||
|
function Element:remove_key_bindings(namespace)
|
||||||
|
local namespaces = namespace and {namespace} or table_keys(self._key_bindings)
|
||||||
|
for _, namespace in ipairs(namespaces) do
|
||||||
|
local names = self._key_bindings[namespace]
|
||||||
|
if names then
|
||||||
|
for name, _ in pairs(names) do
|
||||||
|
mp.remove_key_binding(name)
|
||||||
|
end
|
||||||
|
self._key_bindings[namespace] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks if there are any (at all or namespaced) keybindings for this element.
|
||||||
|
---@param namespace? string Only check this namespace.
|
||||||
|
function Element:has_keybindings(namespace)
|
||||||
|
if namespace then
|
||||||
|
return self._key_bindings[namespace] ~= nil
|
||||||
|
else
|
||||||
|
return #table_keys(self._key_bindings) > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if element is not destroyed or otherwise disabled.
|
||||||
|
-- Intended to be overridden by inheriting elements to add more checks.
|
||||||
|
function Element:is_alive() return not self.destroyed end
|
||||||
|
|
||||||
|
-- Wraps a function into a callback that won't run if element is destroyed or otherwise disabled.
|
||||||
|
---@param fn fun(...)|string Function or a name of a method on this class to call.
|
||||||
|
function Element:create_action(fn)
|
||||||
|
if type(fn) == 'string' then
|
||||||
|
local method = fn
|
||||||
|
fn = function(...) self[method](self, ...) end
|
||||||
|
end
|
||||||
|
return function(...)
|
||||||
|
if self:is_alive() then fn(...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return Element
|
return Element
|
|
@ -0,0 +1,29 @@
|
||||||
|
local Button = require('elements/Button')
|
||||||
|
|
||||||
|
---@alias ManagedButtonProps {name: string; anchor_id?: string; render_order?: number}
|
||||||
|
|
||||||
|
---@class ManagedButton : Button
|
||||||
|
local ManagedButton = class(Button)
|
||||||
|
|
||||||
|
---@param id string
|
||||||
|
---@param props ManagedButtonProps
|
||||||
|
function ManagedButton:new(id, props) return Class.new(self, id, props) --[[@as ManagedButton]] end
|
||||||
|
---@param id string
|
||||||
|
---@param props ManagedButtonProps
|
||||||
|
function ManagedButton:init(id, props)
|
||||||
|
---@type string | table | nil
|
||||||
|
self.command = nil
|
||||||
|
|
||||||
|
Button.init(self, id, table_assign({}, props, {on_click = function() execute_command(self.command) end}))
|
||||||
|
|
||||||
|
self:register_disposer(buttons:subscribe(props.name, function(data) self:update(data) end))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ManagedButton:update(data)
|
||||||
|
for _, prop in ipairs({'icon', 'active', 'badge', 'command', 'tooltip'}) do
|
||||||
|
self[prop] = data[prop]
|
||||||
|
end
|
||||||
|
self.is_clickable = self.command ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return ManagedButton
|
File diff suppressed because it is too large
Load diff
|
@ -22,6 +22,10 @@ function Speed:init(props)
|
||||||
self.dragging = nil
|
self.dragging = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Speed:get_visibility()
|
||||||
|
return Elements:maybe('timeline', 'get_is_hovered') and -1 or Element.get_visibility(self)
|
||||||
|
end
|
||||||
|
|
||||||
function Speed:on_coordinates()
|
function Speed:on_coordinates()
|
||||||
self.height, self.width = self.by - self.ay, self.bx - self.ax
|
self.height, self.width = self.by - self.ay, self.bx - self.ax
|
||||||
self.notch_spacing = self.width / (self.notches + 1)
|
self.notch_spacing = self.width / (self.notches + 1)
|
|
@ -163,8 +163,6 @@ function Timeline:on_global_mouse_move()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
function Timeline:handle_wheel_up() mp.commandv('seek', options.timeline_step) end
|
|
||||||
function Timeline:handle_wheel_down() mp.commandv('seek', -options.timeline_step) end
|
|
||||||
|
|
||||||
function Timeline:render()
|
function Timeline:render()
|
||||||
if self.size == 0 then return end
|
if self.size == 0 then return end
|
||||||
|
@ -186,8 +184,14 @@ function Timeline:render()
|
||||||
self:handle_cursor_down()
|
self:handle_cursor_down()
|
||||||
cursor:once('primary_up', function() self:handle_cursor_up() end)
|
cursor:once('primary_up', function() self:handle_cursor_up() end)
|
||||||
end)
|
end)
|
||||||
cursor:zone('wheel_down', self, function() self:handle_wheel_down() end)
|
if config.timeline_step ~= 0 then
|
||||||
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
|
cursor:zone('wheel_down', self, function()
|
||||||
|
mp.commandv('seek', -config.timeline_step, config.timeline_step_flag)
|
||||||
|
end)
|
||||||
|
cursor:zone('wheel_up', self, function()
|
||||||
|
mp.commandv('seek', config.timeline_step, config.timeline_step_flag)
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
local ass = assdraw.ass_new()
|
||||||
|
@ -251,15 +255,11 @@ function Timeline:render()
|
||||||
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
|
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
|
||||||
|
|
||||||
-- Uncached ranges
|
-- Uncached ranges
|
||||||
local buffered_playtime = nil
|
|
||||||
if state.uncached_ranges then
|
if state.uncached_ranges then
|
||||||
local opts = {size = 80, anchor_y = fby}
|
local opts = {size = 80, anchor_y = fby}
|
||||||
local texture_char = visibility > 0 and 'b' or 'a'
|
local texture_char = visibility > 0 and 'b' or 'a'
|
||||||
local offset = opts.size / (visibility > 0 and 24 or 28)
|
local offset = opts.size / (visibility > 0 and 24 or 28)
|
||||||
for _, range in ipairs(state.uncached_ranges) do
|
for _, range in ipairs(state.uncached_ranges) do
|
||||||
if not buffered_playtime and (range[1] > state.time or range[2] > state.time) then
|
|
||||||
buffered_playtime = (range[1] - state.time) / (state.speed or 1)
|
|
||||||
end
|
|
||||||
if options.timeline_cache then
|
if options.timeline_cache then
|
||||||
local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
|
local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
|
||||||
local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
|
local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
|
||||||
|
@ -321,7 +321,7 @@ function Timeline:render()
|
||||||
if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
|
if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
|
||||||
local circle = {point = {x = t2x(chapter.time), y = fay - 1}, r = diamond_radius_hovered}
|
local circle = {point = {x = t2x(chapter.time), y = fay - 1}, r = diamond_radius_hovered}
|
||||||
if visibility > 0 then
|
if visibility > 0 then
|
||||||
cursor:zone('primary_down', circle, function()
|
cursor:zone('primary_click', circle, function()
|
||||||
mp.commandv('seek', chapter.time, 'absolute+exact')
|
mp.commandv('seek', chapter.time, 'absolute+exact')
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -377,14 +377,15 @@ function Timeline:render()
|
||||||
if text_opacity > 0 then
|
if text_opacity > 0 then
|
||||||
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2 * state.scale}
|
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2 * state.scale}
|
||||||
-- Upcoming cache time
|
-- Upcoming cache time
|
||||||
if buffered_playtime and options.buffered_time_threshold > 0
|
local cache_duration = state.cache_duration and state.cache_duration / state.speed or nil
|
||||||
and buffered_playtime < options.buffered_time_threshold then
|
if cache_duration and options.buffered_time_threshold > 0
|
||||||
|
and cache_duration < options.buffered_time_threshold then
|
||||||
local margin = 5 * state.scale
|
local margin = 5 * state.scale
|
||||||
local x, align = fbx + margin, 4
|
local x, align = fbx + margin, 4
|
||||||
local cache_opts = {
|
local cache_opts = {
|
||||||
size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = options.text_border * state.scale,
|
size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = options.text_border * state.scale,
|
||||||
}
|
}
|
||||||
local human = round(math.max(buffered_playtime, 0)) .. 's'
|
local human = round(cache_duration) .. 's'
|
||||||
local width = text_width(human, cache_opts)
|
local width = text_width(human, cache_opts)
|
||||||
local time_width = timestamp_width(state.time_human, time_opts)
|
local time_width = timestamp_width(state.time_human, time_opts)
|
||||||
local time_width_end = timestamp_width(state.destination_time_human, time_opts)
|
local time_width_end = timestamp_width(state.destination_time_human, time_opts)
|
||||||
|
@ -449,13 +450,14 @@ function Timeline:render()
|
||||||
border_color = fg,
|
border_color = fg,
|
||||||
radius = state.radius,
|
radius = state.radius,
|
||||||
})
|
})
|
||||||
mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y)
|
local thumb_seconds = (state.rebase_start_time == false and state.start_time) and (hovered_seconds - state.start_time) or hovered_seconds
|
||||||
|
mp.commandv('script-message-to', 'thumbfast', 'thumb', thumb_seconds, thumb_x, thumb_y)
|
||||||
self.has_thumbnail, rendered_thumbnail = true, true
|
self.has_thumbnail, rendered_thumbnail = true, true
|
||||||
tooltip_anchor.ay = ay
|
tooltip_anchor.ay = ay
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Chapter title
|
-- Chapter title
|
||||||
if #state.chapters > 0 then
|
if config.opacity.chapters > 0 and #state.chapters > 0 then
|
||||||
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end,
|
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end,
|
||||||
#state.chapters, 1)
|
#state.chapters, 1)
|
||||||
if chapter and not chapter.is_end_only then
|
if chapter and not chapter.is_end_only then
|
|
@ -1,46 +1,6 @@
|
||||||
local Element = require('elements/Element')
|
local Element = require('elements/Element')
|
||||||
|
|
||||||
---@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()}
|
---@alias TopBarButtonProps {icon: string; hover_fg?: string; hover_bg?: string; command: (fun():string)}
|
||||||
|
|
||||||
---@class TopBarButton : Element
|
|
||||||
local TopBarButton = class(Element)
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
---@param props TopBarButtonProps
|
|
||||||
function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end
|
|
||||||
function TopBarButton:init(id, props)
|
|
||||||
Element.init(self, id, props)
|
|
||||||
self.anchor_id = 'top_bar'
|
|
||||||
self.icon = props.icon
|
|
||||||
self.background = props.background
|
|
||||||
self.command = props.command
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBarButton:handle_click()
|
|
||||||
mp.command(type(self.command) == 'function' and self.command() or self.command)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBarButton:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
if visibility <= 0 then return end
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
-- Background on hover
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility})
|
|
||||||
end
|
|
||||||
cursor:zone('primary_click', self, function() self:handle_click() end)
|
|
||||||
|
|
||||||
local width, height = self.bx - self.ax, self.by - self.ay
|
|
||||||
local icon_size = math.min(width, height) * 0.5
|
|
||||||
ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, {
|
|
||||||
opacity = visibility, border = options.text_border * state.scale,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ TopBar ]]
|
|
||||||
|
|
||||||
---@class TopBar : Element
|
---@class TopBar : Element
|
||||||
local TopBar = class(Element)
|
local TopBar = class(Element)
|
||||||
|
@ -49,48 +9,30 @@ function TopBar:new() return Class.new(self) --[[@as TopBar]] end
|
||||||
function TopBar:init()
|
function TopBar:init()
|
||||||
Element.init(self, 'top_bar', {render_order = 4})
|
Element.init(self, 'top_bar', {render_order = 4})
|
||||||
self.size = 0
|
self.size = 0
|
||||||
self.icon_size, self.spacing, self.font_size, self.title_bx, self.title_by = 1, 1, 1, 1, 1
|
self.icon_size, self.font_size, self.title_by = 1, 1, 1
|
||||||
self.show_alt_title = false
|
self.show_alt_title = false
|
||||||
self.main_title, self.alt_title = nil, nil
|
self.main_title, self.alt_title = nil, nil
|
||||||
|
|
||||||
local function get_maximized_command()
|
local function maximized_command()
|
||||||
if state.platform == 'windows' then
|
if state.platform == 'windows' then
|
||||||
return state.border
|
mp.command(state.border
|
||||||
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
|
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
|
||||||
or 'set window-maximized no;cycle fullscreen'
|
or 'set window-maximized no;cycle fullscreen')
|
||||||
|
else
|
||||||
|
mp.command(state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes')
|
||||||
end
|
end
|
||||||
return state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Order aligns from right to left
|
local close = {icon = 'close', hover_bg = '2311e8', hover_fg = 'ffffff', command = function() mp.command('quit') end}
|
||||||
self.buttons = {
|
local max = {icon = 'crop_square', command = maximized_command}
|
||||||
TopBarButton:new('tb_close', {
|
local min = {icon = 'minimize', command = function() mp.command('cycle window-minimized') end}
|
||||||
icon = 'close', background = '2311e8', command = 'quit', render_order = self.render_order,
|
self.buttons = options.top_bar_controls == 'left' and {close, max, min} or {min, max, close}
|
||||||
}),
|
|
||||||
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()
|
self:decide_titles()
|
||||||
self:decide_enabled()
|
self:decide_enabled()
|
||||||
self:update_dimensions()
|
self:update_dimensions()
|
||||||
end
|
end
|
||||||
|
|
||||||
function TopBar:destroy()
|
|
||||||
for _, button in ipairs(self.buttons) do button:destroy() end
|
|
||||||
Element.destroy(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:decide_enabled()
|
function TopBar:decide_enabled()
|
||||||
if options.top_bar == 'no-border' then
|
if options.top_bar == 'no-border' then
|
||||||
self.enabled = not state.border or state.title_bar == false or state.fullscreen
|
self.enabled = not state.border or state.title_bar == false or state.fullscreen
|
||||||
|
@ -98,9 +40,6 @@ function TopBar:decide_enabled()
|
||||||
self.enabled = options.top_bar == 'always'
|
self.enabled = options.top_bar == 'always'
|
||||||
end
|
end
|
||||||
self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title ~= 'no' or state.has_playlist)
|
self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title ~= 'no' or state.has_playlist)
|
||||||
for _, element in ipairs(self.buttons) do
|
|
||||||
element.enabled = self.enabled and options.top_bar_controls
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TopBar:decide_titles()
|
function TopBar:decide_titles()
|
||||||
|
@ -126,7 +65,7 @@ function TopBar:decide_titles()
|
||||||
longer_title, shorter_title = self.main_title, self.alt_title
|
longer_title, shorter_title = self.main_title, self.alt_title
|
||||||
end
|
end
|
||||||
|
|
||||||
local escaped_shorter_title = string.gsub(shorter_title --[[@as string]], '[%(%)%.%+%-%*%?%[%]%^%$%%]', '%%%1')
|
local escaped_shorter_title = regexp_escape(shorter_title --[[@as string]])
|
||||||
if string.match(longer_title --[[@as string]], escaped_shorter_title) then
|
if string.match(longer_title --[[@as string]], escaped_shorter_title) then
|
||||||
self.main_title, self.alt_title = longer_title, nil
|
self.main_title, self.alt_title = longer_title, nil
|
||||||
end
|
end
|
||||||
|
@ -136,27 +75,18 @@ end
|
||||||
function TopBar:update_dimensions()
|
function TopBar:update_dimensions()
|
||||||
self.size = round(options.top_bar_size * state.scale)
|
self.size = round(options.top_bar_size * state.scale)
|
||||||
self.icon_size = round(self.size * 0.5)
|
self.icon_size = round(self.size * 0.5)
|
||||||
self.spacing = math.ceil(self.size * 0.25)
|
self.font_size = math.floor((self.size - (math.ceil(self.size * 0.25) * 2)) * options.font_scale)
|
||||||
self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale)
|
|
||||||
self.button_width = round(self.size * 1.15)
|
|
||||||
local window_border_size = Elements:v('window_border', 'size', 0)
|
local window_border_size = Elements:v('window_border', 'size', 0)
|
||||||
|
self.ax = window_border_size
|
||||||
self.ay = window_border_size
|
self.ay = window_border_size
|
||||||
self.bx = display.width - window_border_size
|
self.bx = display.width - window_border_size
|
||||||
self.by = self.size + 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 window_border_size or self.title_bx
|
|
||||||
|
|
||||||
local button_bx = self.bx
|
|
||||||
for _, element in pairs(self.buttons) do
|
|
||||||
element.ax, element.bx = button_bx - self.button_width, button_bx
|
|
||||||
element.ay, element.by = self.ay, self.by
|
|
||||||
button_bx = button_bx - self.button_width
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TopBar:toggle_title()
|
function TopBar:toggle_title()
|
||||||
if options.top_bar_alt_title_place ~= 'toggle' then return end
|
if options.top_bar_alt_title_place ~= 'toggle' then return end
|
||||||
self.show_alt_title = not self.show_alt_title
|
self.show_alt_title = not self.show_alt_title
|
||||||
|
request_render()
|
||||||
end
|
end
|
||||||
|
|
||||||
function TopBar:on_prop_title() self:decide_titles() end
|
function TopBar:on_prop_title() self:decide_titles() end
|
||||||
|
@ -198,15 +128,54 @@ function TopBar:render()
|
||||||
local visibility = self:get_visibility()
|
local visibility = self:get_visibility()
|
||||||
if visibility <= 0 then return end
|
if visibility <= 0 then return end
|
||||||
local ass = assdraw.ass_new()
|
local ass = assdraw.ass_new()
|
||||||
|
local ax, bx = self.ax, self.bx
|
||||||
|
local margin = math.floor((self.size - self.font_size) / 4)
|
||||||
|
|
||||||
|
-- Window controls
|
||||||
|
if options.top_bar_controls then
|
||||||
|
local is_left, button_ax = options.top_bar_controls == 'left', 0
|
||||||
|
if is_left then
|
||||||
|
button_ax = ax
|
||||||
|
ax = self.size * #self.buttons
|
||||||
|
else
|
||||||
|
button_ax = bx - self.size * #self.buttons
|
||||||
|
bx = button_ax
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, button in ipairs(self.buttons) do
|
||||||
|
local rect = {ax = button_ax, ay = self.ay, bx = button_ax + self.size, by = self.by}
|
||||||
|
local is_hover = get_point_to_rectangle_proximity(cursor, rect) == 0
|
||||||
|
local opacity = is_hover and 1 or config.opacity.controls
|
||||||
|
local button_fg = is_hover and (button.hover_fg or bg) or fg
|
||||||
|
local button_bg = is_hover and (button.hover_bg or fg) or bg
|
||||||
|
|
||||||
|
cursor:zone('primary_click', rect, button.command)
|
||||||
|
|
||||||
|
local bg_size = self.size - margin
|
||||||
|
local bg_ax, bg_ay = rect.ax + (is_left and margin or 0), rect.ay + margin
|
||||||
|
local bg_bx, bg_by = bg_ax + bg_size, bg_ay + bg_size
|
||||||
|
|
||||||
|
ass:rect(bg_ax, bg_ay, bg_bx, bg_by, {
|
||||||
|
color = button_bg, opacity = visibility * opacity, radius = state.radius,
|
||||||
|
})
|
||||||
|
|
||||||
|
ass:icon(bg_ax + bg_size / 2, bg_ay + bg_size / 2, bg_size * 0.5, button.icon, {
|
||||||
|
color = button_fg,
|
||||||
|
border_color = button_bg,
|
||||||
|
opacity = visibility,
|
||||||
|
border = options.text_border * state.scale,
|
||||||
|
})
|
||||||
|
|
||||||
|
button_ax = button_ax + self.size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Window title
|
-- Window title
|
||||||
if state.title or state.has_playlist then
|
if state.title or state.has_playlist then
|
||||||
local bg_margin = math.floor((self.size - self.font_size) / 4)
|
|
||||||
local padding = self.font_size / 2
|
local padding = self.font_size / 2
|
||||||
local spacing = 1
|
local spacing = 1
|
||||||
local title_ax = self.ax + bg_margin
|
local left_aligned = options.top_bar_controls == 'left'
|
||||||
local title_ay = self.ay + bg_margin
|
local title_ax, title_bx, title_ay = ax + margin, bx - margin, self.ay + margin
|
||||||
local max_bx = self.title_bx - self.spacing
|
|
||||||
|
|
||||||
-- Playlist position
|
-- Playlist position
|
||||||
if state.has_playlist then
|
if state.has_playlist then
|
||||||
|
@ -214,11 +183,13 @@ function TopBar:render()
|
||||||
local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
|
local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
|
||||||
.. state.playlist_count
|
.. state.playlist_count
|
||||||
local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
|
local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
|
||||||
|
local rect_width = round(text_width(text, opts) + padding * 2)
|
||||||
|
local ax = left_aligned and title_bx - rect_width or title_ax
|
||||||
local rect = {
|
local rect = {
|
||||||
ax = title_ax,
|
ax = ax,
|
||||||
ay = title_ay,
|
ay = title_ay,
|
||||||
bx = round(title_ax + text_width(text, opts) + padding * 2),
|
bx = ax + rect_width,
|
||||||
by = self.by - bg_margin,
|
by = self.by - margin,
|
||||||
}
|
}
|
||||||
local opacity = get_point_to_rectangle_proximity(cursor, rect) == 0
|
local opacity = get_point_to_rectangle_proximity(cursor, rect) == 0
|
||||||
and 1 or config.opacity.playlist_position
|
and 1 or config.opacity.playlist_position
|
||||||
|
@ -228,14 +199,14 @@ function TopBar:render()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
ass:txt(rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2, 5, formatted_text, opts)
|
ass:txt(rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2, 5, formatted_text, opts)
|
||||||
title_ax = rect.bx + bg_margin
|
if left_aligned then title_bx = rect.ax - margin else title_ax = rect.bx + margin end
|
||||||
|
|
||||||
-- Click action
|
-- Click action
|
||||||
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/playlist') end)
|
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/playlist') end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Skip rendering titles if there's not enough horizontal space
|
-- Skip rendering titles if there's not enough horizontal space
|
||||||
if max_bx - title_ax > self.font_size * 3 and options.top_bar_title ~= 'no' then
|
if title_bx - title_ax > self.font_size * 3 and options.top_bar_title ~= 'no' then
|
||||||
-- Main title
|
-- Main title
|
||||||
local main_title = self.show_alt_title and self.alt_title or self.main_title
|
local main_title = self.show_alt_title and self.alt_title or self.main_title
|
||||||
if main_title then
|
if main_title then
|
||||||
|
@ -246,11 +217,13 @@ function TopBar:render()
|
||||||
opacity = visibility,
|
opacity = visibility,
|
||||||
border = options.text_border * state.scale,
|
border = options.text_border * state.scale,
|
||||||
border_color = bg,
|
border_color = bg,
|
||||||
clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by),
|
clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, title_bx, self.by),
|
||||||
}
|
}
|
||||||
local bx = round(math.min(max_bx, title_ax + text_width(main_title, opts) + padding * 2))
|
local rect_ideal_width = round(text_width(main_title, opts) + padding * 2)
|
||||||
local by = self.by - bg_margin
|
local rect_width = math.min(rect_ideal_width, title_bx - title_ax)
|
||||||
local title_rect = {ax = title_ax, ay = title_ay, bx = bx, by = by}
|
local ax = left_aligned and title_bx - rect_width or title_ax
|
||||||
|
local by = self.by - margin
|
||||||
|
local title_rect = {ax = ax, ay = title_ay, bx = ax + rect_width, by = by}
|
||||||
|
|
||||||
if options.top_bar_alt_title_place == 'toggle' then
|
if options.top_bar_alt_title_place == 'toggle' then
|
||||||
cursor:zone('primary_click', title_rect, function() self:toggle_title() end)
|
cursor:zone('primary_click', title_rect, function() self:toggle_title() end)
|
||||||
|
@ -259,7 +232,9 @@ function TopBar:render()
|
||||||
ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, {
|
ass:rect(title_rect.ax, title_rect.ay, title_rect.bx, title_rect.by, {
|
||||||
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
||||||
})
|
})
|
||||||
ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, main_title, opts)
|
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
|
||||||
|
local x = align == 6 and title_rect.bx - padding or ax + padding
|
||||||
|
ass:txt(x, self.ay + (self.size / 2), align, main_title, opts)
|
||||||
title_ay = by + spacing
|
title_ay = by + spacing
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -276,12 +251,17 @@ function TopBar:render()
|
||||||
border_color = bg,
|
border_color = bg,
|
||||||
opacity = visibility,
|
opacity = visibility,
|
||||||
}
|
}
|
||||||
local bx = round(math.min(max_bx, title_ax + text_width(self.alt_title, opts) + padding * 2))
|
local rect_ideal_width = round(text_width(self.alt_title, opts) + padding * 2)
|
||||||
|
local rect_width = math.min(rect_ideal_width, title_bx - title_ax)
|
||||||
|
local ax = left_aligned and title_bx - rect_width or title_ax
|
||||||
|
local bx = ax + rect_width
|
||||||
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
|
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
|
||||||
ass:rect(title_ax, title_ay, bx, by, {
|
ass:rect(ax, title_ay, bx, by, {
|
||||||
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
||||||
})
|
})
|
||||||
ass:txt(title_ax + padding, title_ay + height / 2, 4, self.alt_title, opts)
|
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
|
||||||
|
local x = align == 6 and bx - padding or ax + padding
|
||||||
|
ass:txt(x, title_ay + height / 2, align, self.alt_title, opts)
|
||||||
title_ay = by + spacing
|
title_ay = by + spacing
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -290,10 +270,12 @@ function TopBar:render()
|
||||||
local padding_half = round(padding / 2)
|
local padding_half = round(padding / 2)
|
||||||
local font_size = self.font_size * 0.8
|
local font_size = self.font_size * 0.8
|
||||||
local height = font_size * 1.3
|
local height = font_size * 1.3
|
||||||
local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title
|
local prefix, postfix = left_aligned and '' or '└ ', left_aligned and ' ┘' or ''
|
||||||
|
local text = prefix .. state.current_chapter.index .. ': ' .. state.current_chapter.title .. postfix
|
||||||
local next_chapter = state.chapters[state.current_chapter.index + 1]
|
local next_chapter = state.chapters[state.current_chapter.index + 1]
|
||||||
local chapter_end = next_chapter and next_chapter.time or state.duration or 0
|
local chapter_end = next_chapter and next_chapter.time or state.duration or 0
|
||||||
local remaining_time = (state.time and state.time or 0) - chapter_end
|
local remaining_time = ((state.time or 0) - chapter_end) /
|
||||||
|
(options.destination_time == 'time-remaining' and 1 or state.speed)
|
||||||
local remaining_human = format_time(remaining_time, math.abs(remaining_time))
|
local remaining_human = format_time(remaining_time, math.abs(remaining_time))
|
||||||
local opts = {
|
local opts = {
|
||||||
size = font_size,
|
size = font_size,
|
||||||
|
@ -308,32 +290,36 @@ function TopBar:render()
|
||||||
local remaining_box_width = remaining_width + padding_half * 2
|
local remaining_box_width = remaining_width + padding_half * 2
|
||||||
|
|
||||||
-- Title
|
-- Title
|
||||||
|
local max_bx = title_bx - remaining_box_width - spacing
|
||||||
|
local rect_ideal_width = round(text_width(text, opts) + padding * 2)
|
||||||
|
local rect_width = math.min(rect_ideal_width, max_bx - title_ax)
|
||||||
|
local ax = left_aligned and title_bx - rect_width or title_ax
|
||||||
local rect = {
|
local rect = {
|
||||||
ax = title_ax,
|
ax = ax,
|
||||||
ay = title_ay,
|
ay = title_ay,
|
||||||
bx = round(math.min(
|
bx = ax + rect_width,
|
||||||
max_bx - remaining_box_width - spacing,
|
|
||||||
title_ax + text_width(text, opts) + padding * 2
|
|
||||||
)),
|
|
||||||
by = title_ay + height,
|
by = title_ay + height,
|
||||||
}
|
}
|
||||||
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, rect.bx, rect.by)
|
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, rect.bx, rect.by)
|
||||||
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
||||||
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
||||||
})
|
})
|
||||||
ass:txt(rect.ax + padding, rect.ay + height / 2, 4, text, opts)
|
local align = left_aligned and rect_ideal_width == rect_width and 6 or 4
|
||||||
|
local x = align == 6 and rect.bx - padding or rect.ax + padding
|
||||||
-- Click action
|
ass:txt(x, rect.ay + height / 2, align, text, opts)
|
||||||
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/chapters') end)
|
|
||||||
|
|
||||||
-- Time
|
-- Time
|
||||||
rect.ax = rect.bx + spacing
|
local time_ax = left_aligned and rect.ax - spacing - remaining_box_width or rect.bx + spacing
|
||||||
rect.bx = rect.ax + remaining_box_width
|
local time_bx = time_ax + remaining_box_width
|
||||||
opts.clip = nil
|
opts.clip = nil
|
||||||
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
ass:rect(time_ax, rect.ay, time_bx, rect.by, {
|
||||||
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
color = bg, opacity = visibility * config.opacity.title, radius = state.radius,
|
||||||
})
|
})
|
||||||
ass:txt(rect.ax + padding_half, rect.ay + height / 2, 4, remaining_human, opts)
|
ass:txt(time_ax + padding_half, rect.ay + height / 2, 4, remaining_human, opts)
|
||||||
|
|
||||||
|
-- Click action
|
||||||
|
rect.bx = time_bx
|
||||||
|
cursor:zone('primary_click', rect, function() mp.command('script-binding uosc/chapters') end)
|
||||||
|
|
||||||
title_ay = rect.by + spacing
|
title_ay = rect.by + spacing
|
||||||
end
|
end
|
|
@ -0,0 +1,316 @@
|
||||||
|
local Element = require('elements/Element')
|
||||||
|
local dots = {'.', '..', '...'}
|
||||||
|
|
||||||
|
local function cleanup_output(output)
|
||||||
|
return tostring(output):gsub('%c*\n%c*', '\n'):match('^[%s%c]*(.-)[%s%c]*$')
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class Updater : Element
|
||||||
|
local Updater = class(Element)
|
||||||
|
|
||||||
|
function Updater:new() return Class.new(self) --[[@as Updater]] end
|
||||||
|
function Updater:init()
|
||||||
|
Element.init(self, 'updater', {render_order = 1000})
|
||||||
|
self.output = nil
|
||||||
|
self.title = ''
|
||||||
|
self.state = 'circle' -- Also used as an icon name. 'pending' maps to 'spinner'.
|
||||||
|
self.update_available = false
|
||||||
|
|
||||||
|
-- Buttons
|
||||||
|
self.check_button = {method = 'check', title = t('Check for updates')}
|
||||||
|
self.update_button = {method = 'update', title = t('Update uosc'), color = config.color.success}
|
||||||
|
self.changelog_button = {method = 'open_changelog', title = t('Open changelog')}
|
||||||
|
self.close_button = {method = 'destroy', title = t('Close') .. ' (Esc)', color = config.color.error}
|
||||||
|
self.quit_button = {method = 'quit', title = t('Quit')}
|
||||||
|
self.buttons = {self.check_button, self.close_button}
|
||||||
|
self.selected_button_index = 1
|
||||||
|
|
||||||
|
-- Key bindings
|
||||||
|
self:add_key_binding('right', 'select_next_button')
|
||||||
|
self:add_key_binding('tab', 'select_next_button')
|
||||||
|
self:add_key_binding('left', 'select_prev_button')
|
||||||
|
self:add_key_binding('shift+tab', 'select_prev_button')
|
||||||
|
self:add_key_binding('enter', 'activate_selected_button')
|
||||||
|
self:add_key_binding('kp_enter', 'activate_selected_button')
|
||||||
|
self:add_key_binding('esc', 'destroy')
|
||||||
|
|
||||||
|
Elements:maybe('curtain', 'register', self.id)
|
||||||
|
self:check()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:destroy()
|
||||||
|
Elements:maybe('curtain', 'unregister', self.id)
|
||||||
|
Element.destroy(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:quit()
|
||||||
|
mp.command('quit')
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:select_prev_button()
|
||||||
|
self.selected_button_index = self.selected_button_index - 1
|
||||||
|
if self.selected_button_index < 1 then self.selected_button_index = #self.buttons end
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:select_next_button()
|
||||||
|
self.selected_button_index = self.selected_button_index + 1
|
||||||
|
if self.selected_button_index > #self.buttons then self.selected_button_index = 1 end
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:activate_selected_button()
|
||||||
|
local button = self.buttons[self.selected_button_index]
|
||||||
|
if button then self[button.method](self) end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param msg string
|
||||||
|
function Updater:append_output(msg)
|
||||||
|
self.output = (self.output or '') .. ass_escape('\n' .. cleanup_output(msg))
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param msg string
|
||||||
|
function Updater:display_error(msg)
|
||||||
|
self.state = 'error'
|
||||||
|
self.title = t('An error has occurred.') .. ' ' .. t('See console for details.')
|
||||||
|
self:append_output(msg)
|
||||||
|
print(msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:open_changelog()
|
||||||
|
if self.state == 'pending' then return end
|
||||||
|
|
||||||
|
local url = 'https://github.com/tomasklaen/uosc/releases'
|
||||||
|
|
||||||
|
self:append_output('Opening URL: ' .. url)
|
||||||
|
|
||||||
|
call_ziggy_async({'open', url}, function(error)
|
||||||
|
if error then
|
||||||
|
self:display_error(error)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:check()
|
||||||
|
if self.state == 'pending' then return end
|
||||||
|
self.state = 'pending'
|
||||||
|
self.title = t('Checking for updates') .. '...'
|
||||||
|
|
||||||
|
local url = 'https://api.github.com/repos/tomasklaen/uosc/releases/latest'
|
||||||
|
local headers = utils.format_json({
|
||||||
|
Accept = 'application/vnd.github+json',
|
||||||
|
})
|
||||||
|
local args = {'http-get', '--headers', headers, url}
|
||||||
|
|
||||||
|
self:append_output('Fetching: ' .. url)
|
||||||
|
|
||||||
|
call_ziggy_async(args, function(error, response)
|
||||||
|
if error then
|
||||||
|
self:display_error(error)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
release = utils.parse_json(type(response.body) == 'string' and response.body or '')
|
||||||
|
if response.status == 200 and type(release) == 'table' and type(release.tag_name) == 'string' then
|
||||||
|
self.update_available = config.version ~= release.tag_name
|
||||||
|
self:append_output('Response: 200 OK')
|
||||||
|
self:append_output('Current version: ' .. config.version)
|
||||||
|
self:append_output('Latest version: ' .. release.tag_name)
|
||||||
|
if self.update_available then
|
||||||
|
self.state = 'upgrade'
|
||||||
|
self.title = t('Update available')
|
||||||
|
self.buttons = {self.update_button, self.changelog_button, self.close_button}
|
||||||
|
self.selected_button_index = 1
|
||||||
|
else
|
||||||
|
self.state = 'done'
|
||||||
|
self.title = t('Up to date')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:display_error('Response couldn\'t be parsed, is invalid, or not-OK status code.\nStatus: ' ..
|
||||||
|
response.status .. '\nBody: ' .. response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
request_render()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:update()
|
||||||
|
if self.state == 'pending' then return end
|
||||||
|
self.state = 'pending'
|
||||||
|
self.title = t('Updating uosc')
|
||||||
|
self.output = nil
|
||||||
|
request_render()
|
||||||
|
|
||||||
|
local config_dir = mp.command_native({'expand-path', '~~/'})
|
||||||
|
|
||||||
|
local function handle_result(success, result, error)
|
||||||
|
if success and result and result.status == 0 then
|
||||||
|
self.state = 'done'
|
||||||
|
self.title = t('uosc has been installed. Restart mpv for it to take effect.')
|
||||||
|
self.buttons = {self.quit_button, self.close_button}
|
||||||
|
self.selected_button_index = 1
|
||||||
|
else
|
||||||
|
self.state = 'error'
|
||||||
|
self.title = t('An error has occurred.') .. ' ' .. t('See above for clues.')
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
|
||||||
|
if state.platform == 'darwin' then
|
||||||
|
output =
|
||||||
|
'Self-updater is known not to work on MacOS.\nIf you know about a solution, please make an issue and share it with us!.\n' ..
|
||||||
|
output
|
||||||
|
end
|
||||||
|
self:append_output(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update(args)
|
||||||
|
local env = utils.get_env_list()
|
||||||
|
env[#env + 1] = 'MPV_CONFIG_DIR=' .. config_dir
|
||||||
|
|
||||||
|
mp.command_native_async({
|
||||||
|
name = 'subprocess',
|
||||||
|
capture_stderr = true,
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = args,
|
||||||
|
env = env,
|
||||||
|
}, handle_result)
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.platform == 'windows' then
|
||||||
|
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1'
|
||||||
|
update({'powershell', '-NoProfile', '-Command', 'irm ' .. url .. ' | iex'})
|
||||||
|
else
|
||||||
|
-- Detect missing dependencies. We can't just let the process run and
|
||||||
|
-- report an error, as on snap packages there's no error. Everything
|
||||||
|
-- either exits with 0, or no helpful output/error message.
|
||||||
|
local missing = {}
|
||||||
|
|
||||||
|
for _, name in ipairs({'curl', 'unzip'}) do
|
||||||
|
local result = mp.command_native({
|
||||||
|
name = 'subprocess',
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = {'which', name},
|
||||||
|
})
|
||||||
|
local path = cleanup_output(result and result.stdout or '')
|
||||||
|
if path == '' then
|
||||||
|
missing[#missing + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #missing > 0 then
|
||||||
|
local stderr = 'Missing dependencies: ' .. table.concat(missing, ', ')
|
||||||
|
if config_dir:match('/snap/') then
|
||||||
|
stderr = stderr ..
|
||||||
|
'\nThis is a known error for mpv snap packages.\nYou can still update uosc by entering the Linux install command from uosc\'s readme into your terminal, it just can\'t be done this way.\nIf you know about a solution, please make an issue and share it with us!'
|
||||||
|
end
|
||||||
|
handle_result(false, {stderr = stderr})
|
||||||
|
else
|
||||||
|
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh'
|
||||||
|
update({'/bin/bash', '-c', 'source <(curl -fsSL ' .. url .. ')'})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Updater:render()
|
||||||
|
local ass = assdraw.ass_new()
|
||||||
|
|
||||||
|
local text_size = math.min(20 * state.scale, display.height / 20)
|
||||||
|
local icon_size = text_size * 2
|
||||||
|
local center_x = round(display.width / 2)
|
||||||
|
|
||||||
|
local color = fg
|
||||||
|
if self.state == 'done' or self.update_available then
|
||||||
|
color = config.color.success
|
||||||
|
elseif self.state == 'error' then
|
||||||
|
color = config.color.error
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Divider
|
||||||
|
local divider_width = round(math.min(500 * state.scale, display.width * 0.8))
|
||||||
|
local divider_half, divider_border_half, divider_y = divider_width / 2, round(1 * state.scale), display.height * 0.65
|
||||||
|
local divider_ay, divider_by = round(divider_y - divider_border_half), round(divider_y + divider_border_half)
|
||||||
|
ass:rect(center_x - divider_half, divider_ay, center_x - icon_size, divider_by, {
|
||||||
|
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
||||||
|
})
|
||||||
|
ass:rect(center_x + icon_size, divider_ay, center_x + divider_half, divider_by, {
|
||||||
|
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
||||||
|
})
|
||||||
|
if self.state == 'pending' then
|
||||||
|
ass:spinner(center_x, divider_y, icon_size, {
|
||||||
|
color = fg, border = options.text_border * state.scale, border_color = bg,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
ass:icon(center_x, divider_y, icon_size * 0.8, self.state, {
|
||||||
|
color = color, border = options.text_border * state.scale, border_color = bg,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output
|
||||||
|
local output = self.output or dots[math.ceil((mp.get_time() % 1) * #dots)]
|
||||||
|
ass:txt(center_x, divider_y - icon_size, 2, output, {
|
||||||
|
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Title
|
||||||
|
ass:txt(center_x, divider_y + icon_size, 5, self.title, {
|
||||||
|
size = text_size, bold = true, color = color, border = options.text_border * state.scale, border_color = bg,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Buttons
|
||||||
|
local outline = round(1 * state.scale)
|
||||||
|
local spacing = outline * 9
|
||||||
|
local padding = round(text_size * 0.5)
|
||||||
|
|
||||||
|
local text_opts = {size = text_size, bold = true}
|
||||||
|
|
||||||
|
-- Calculate button text widths
|
||||||
|
local total_width = (#self.buttons - 1) * spacing
|
||||||
|
for _, button in ipairs(self.buttons) do
|
||||||
|
button.width = text_width(button.title, text_opts) + padding * 2
|
||||||
|
total_width = total_width + button.width
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render buttons
|
||||||
|
local ay = round(divider_y + icon_size * 1.8)
|
||||||
|
local ax = round(display.width / 2 - total_width / 2)
|
||||||
|
local height = text_size + padding * 2
|
||||||
|
for index, button in ipairs(self.buttons) do
|
||||||
|
local rect = {
|
||||||
|
ax = ax,
|
||||||
|
ay = ay,
|
||||||
|
bx = ax + button.width,
|
||||||
|
by = ay + height,
|
||||||
|
}
|
||||||
|
ax = rect.bx + spacing
|
||||||
|
local is_hovered = get_point_to_rectangle_proximity(cursor, rect) == 0
|
||||||
|
|
||||||
|
-- Background
|
||||||
|
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
||||||
|
color = button.color or fg,
|
||||||
|
radius = state.radius,
|
||||||
|
opacity = is_hovered and 1 or 0.8,
|
||||||
|
})
|
||||||
|
-- Selected outline
|
||||||
|
if index == self.selected_button_index then
|
||||||
|
ass:rect(rect.ax - outline * 4, rect.ay - outline * 4, rect.bx + outline * 4, rect.by + outline * 4, {
|
||||||
|
border = outline,
|
||||||
|
border_color = button.color or fg,
|
||||||
|
radius = state.radius + outline * 4,
|
||||||
|
opacity = {primary = 0, border = 0.5},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
-- Text
|
||||||
|
local x, y = rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2
|
||||||
|
ass:txt(x, y, 5, button.title, {size = text_size, bold = true, color = fgt})
|
||||||
|
|
||||||
|
cursor:zone('primary_click', rect, self:create_action(button.method))
|
||||||
|
end
|
||||||
|
|
||||||
|
return ass
|
||||||
|
end
|
||||||
|
|
||||||
|
return Updater
|
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"%s are empty": "%s están vacíos",
|
||||||
|
"%s channel": "%s canal",
|
||||||
|
"%s channels": "%s canales",
|
||||||
|
"%s to delete": "%s para eliminar",
|
||||||
|
"%s to go up in tree.": "%s para subir en el árbol",
|
||||||
|
"%s to reorder.": "%s para reordenar",
|
||||||
|
"%s to search": "%s para buscar",
|
||||||
|
"Add to playlist": "Añadir a lista de reproducción",
|
||||||
|
"Added to playlist": "Añadido a lista de reproducción",
|
||||||
|
"An error has occurred.": "Un error ha ocurrido.",
|
||||||
|
"Aspect ratio": "Relación de aspecto",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Audio device": "Dispositivo de audio",
|
||||||
|
"Audio devices": "Dispositivos de audio",
|
||||||
|
"Audio tracks": "Pistas de audio",
|
||||||
|
"Chapter %s": "Capítulo %s",
|
||||||
|
"Chapters": "Capítulos",
|
||||||
|
"Copied to clipboard": "Copiado al portapapeles",
|
||||||
|
"Default": "Por defecto",
|
||||||
|
"Default %s": "Por defecto %s",
|
||||||
|
"Delete": "Eliminar",
|
||||||
|
"Delete file & Next": "Eliminar archivo y siguiente",
|
||||||
|
"Delete file & Prev": "Eliminar archivo y anterior",
|
||||||
|
"Delete file & Quit": "Eliminar archivo y salir",
|
||||||
|
"Drives": "Unidades",
|
||||||
|
"Drop files or URLs to play here": "Soltar archivos o URLs aquí para reproducirlas",
|
||||||
|
"Edition %s": "Edición %s",
|
||||||
|
"Editions": "Ediciones",
|
||||||
|
"Empty": "Vacío",
|
||||||
|
"First": "Primero",
|
||||||
|
"Fullscreen": "Pantalla completa",
|
||||||
|
"Key bindings": "Atajos de teclas",
|
||||||
|
"Last": "Último",
|
||||||
|
"Load": "Abrir",
|
||||||
|
"Load audio": "Añadir una pista de audio",
|
||||||
|
"Load subtitles": "Añadir una pista de subtítulos",
|
||||||
|
"Load video": "Añadir una pista de vídeo",
|
||||||
|
"Loaded audio": "Audio cargado",
|
||||||
|
"Loaded subtitles": "Subtítulos cargados",
|
||||||
|
"Loaded video": "Vídeos cargados",
|
||||||
|
"Loop file": "Repetir archivo",
|
||||||
|
"Loop playlist": "Repetir lista de reproducción",
|
||||||
|
"Menu": "Menú",
|
||||||
|
"Move down": "Moverse abajo",
|
||||||
|
"Move up": "Moverse arriba",
|
||||||
|
"Navigation": "Navegación",
|
||||||
|
"Next": "Siguiente",
|
||||||
|
"Next page": "Página siguiente",
|
||||||
|
"No file": "Ningún archivo",
|
||||||
|
"Open config folder": "Abrir carpeta de configuración",
|
||||||
|
"Open file": "Abrir archivo",
|
||||||
|
"Open in browser": "Abrir en navegador",
|
||||||
|
"Open in mpv": "Abrir en mpv",
|
||||||
|
"Paste path or url to add.": "Pegar ruta o url a añadir.",
|
||||||
|
"Paste path or url to open.": "Pegar ruta o url a abrir.",
|
||||||
|
"Play/Pause": "Reproducir/Pausa",
|
||||||
|
"Playlist": "Lista de reproducción",
|
||||||
|
"Playlist/Files": "Lista de reproducción/Archivos",
|
||||||
|
"Prev": "Anterior",
|
||||||
|
"Previous": "Anterior",
|
||||||
|
"Previous page": "Página anterior",
|
||||||
|
"Quit": "Salir",
|
||||||
|
"Reload": "Recargar",
|
||||||
|
"Remaining downloads today: %s": "Descargas restantes por hoy: %s",
|
||||||
|
"Remove": "Eliminar",
|
||||||
|
"Resets in: %s": "Restablecer en: %s",
|
||||||
|
"Screenshot": "Captura de pantalla",
|
||||||
|
"Search online": "Buscar en línea",
|
||||||
|
"See above for clues.": "Vea arriba para más pistas",
|
||||||
|
"See console for details.": "Vea la consola para más detalles",
|
||||||
|
"Show in directory": "Mostrar en la carpeta",
|
||||||
|
"Shuffle": "Reproducción aleatoria",
|
||||||
|
"Something went wrong.": "Algo malió sal",
|
||||||
|
"Stream quality": "Calidad de la transmisión",
|
||||||
|
"Subtitles": "Subtítulos",
|
||||||
|
"Subtitles loaded & enabled": "Subtítulos cargados y habilitados",
|
||||||
|
"Toggle to disable.": "Alternar para deshabilitar",
|
||||||
|
"Track %s": "Pista %s",
|
||||||
|
"Update uosc": "Actualizar uosc",
|
||||||
|
"Updating uosc": "Actualizando uosc",
|
||||||
|
"Use as secondary": "Utilizar como secundario",
|
||||||
|
"Utils": "Utilidades",
|
||||||
|
"Video": "Vídeo",
|
||||||
|
"default": "por defecto",
|
||||||
|
"drive": "unidad",
|
||||||
|
"enter query": "ingresar consulta",
|
||||||
|
"external": "externo",
|
||||||
|
"forced": "forzado",
|
||||||
|
"foreign parts only": "solo partes extranjeras",
|
||||||
|
"hearing impaired": "discapacidad auditiva",
|
||||||
|
"no results": "sin resultados",
|
||||||
|
"open file": "abrir archivo",
|
||||||
|
"parent dir": "directorio padre",
|
||||||
|
"playlist or file": "archivo o lista de reproducción",
|
||||||
|
"type & ctrl+enter to search": "escriba y presione ctrl+enter para buscar",
|
||||||
|
"type to search": "escriba para buscar",
|
||||||
|
"uosc has been installed. Restart mpv for it to take effect.": "uosc ha sido instalado, Reinicie mpv para que tome efecto."
|
||||||
|
}
|
107
multimedia/.config/mpv/scripts/uosc_repo/src/uosc/intl/tr.json
Normal file
107
multimedia/.config/mpv/scripts/uosc_repo/src/uosc/intl/tr.json
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
{
|
||||||
|
"%s are empty": "%s boş",
|
||||||
|
"%s channel": "%s kanal",
|
||||||
|
"%s channels": "%s kanallar",
|
||||||
|
"%s to delete": "%s silmek için",
|
||||||
|
"%s to go up in tree.": "%s yukarı gitmek için.",
|
||||||
|
"%s to reorder.": "%s yeniden sıralamak için.",
|
||||||
|
"%s to search": "%s aramak için",
|
||||||
|
"Add to playlist": "Çalma listesine ekle",
|
||||||
|
"Added to playlist": "Çalma listesine eklendi",
|
||||||
|
"An error has occurred.": "Bir hata oluştu.",
|
||||||
|
"Aspect ratio": "En-boy oranı",
|
||||||
|
"Audio": "Ses",
|
||||||
|
"Audio device": "Ses cihazı",
|
||||||
|
"Audio devices": "Ses cihazları",
|
||||||
|
"Audio tracks": "Ses parçaları",
|
||||||
|
"Autoload": "Otomatik yükleme",
|
||||||
|
"Chapter %s": "Bölüm %s",
|
||||||
|
"Chapters": "Bölümler",
|
||||||
|
"Check for updates": "Güncellemeleri kontrol et",
|
||||||
|
"Checking for updates": "Güncellemeler kontrol ediliyor",
|
||||||
|
"Close": "Kapat",
|
||||||
|
"Copied to clipboard": "Panoya kopyalandı",
|
||||||
|
"Default": "Varsayılan",
|
||||||
|
"Default %s": "Varsayılan %s",
|
||||||
|
"Delete": "Sil",
|
||||||
|
"Delete file & Next": "Dosyayı sil & Sonraki",
|
||||||
|
"Delete file & Prev": "Dosyayı sil & Önceki",
|
||||||
|
"Delete file & Quit": "Dosyayı sil & Çık",
|
||||||
|
"Drives": "Sürücüler",
|
||||||
|
"Drop files or URLs to play here": "Dosyaları veya URL'leri buraya bırakın",
|
||||||
|
"Edition %s": "Sürüm %s",
|
||||||
|
"Editions": "Sürümler",
|
||||||
|
"Empty": "Boş",
|
||||||
|
"First": "İlk",
|
||||||
|
"Fullscreen": "Tam ekran",
|
||||||
|
"Key bindings": "Tuş atamaları",
|
||||||
|
"Last": "Son",
|
||||||
|
"Load": "Yükle",
|
||||||
|
"Load audio": "Ses yükle",
|
||||||
|
"Load subtitles": "Altyazı yükle",
|
||||||
|
"Load video": "Video yükle",
|
||||||
|
"Loaded audio": "Ses yüklendi",
|
||||||
|
"Loaded subtitles": "Altyazı yüklendi",
|
||||||
|
"Loaded video": "Video yüklendi",
|
||||||
|
"Loop file": "Dosyayı döngüye al",
|
||||||
|
"Loop playlist": "Çalma listesini döngüye al",
|
||||||
|
"Menu": "Menü",
|
||||||
|
"Move down": "Aşağı taşı",
|
||||||
|
"Move up": "Yukarı taşı",
|
||||||
|
"Navigation": "Gezinme",
|
||||||
|
"Next": "Sonraki",
|
||||||
|
"Next page": "Sonraki sayfa",
|
||||||
|
"No file": "Dosya yok",
|
||||||
|
"Nothing to copy": "Kopyalanacak bir şey yok",
|
||||||
|
"Open changelog": "Değişiklik günlüğünü aç",
|
||||||
|
"Open config folder": "Yapılandırma klasörünü aç",
|
||||||
|
"Open file": "Dosya aç",
|
||||||
|
"Open in browser": "Tarayıcıda aç",
|
||||||
|
"Open in mpv": "mpv'de aç",
|
||||||
|
"Paste path or url to add.": "Eklemek için yolu veya URL'yi yapıştırın.",
|
||||||
|
"Paste path or url to open.": "Açmak için yolu veya URL'yi yapıştırın.",
|
||||||
|
"Play/Pause": "Oynat/Duraklat",
|
||||||
|
"Playlist": "Çalma listesi",
|
||||||
|
"Playlist/Files": "Çalma listesi/Dosyalar",
|
||||||
|
"Prev": "Önceki",
|
||||||
|
"Previous": "Önceki",
|
||||||
|
"Previous page": "Önceki sayfa",
|
||||||
|
"Quit": "Çıkış",
|
||||||
|
"Reload": "Yeniden yükle",
|
||||||
|
"Remaining downloads today: %s": "Bugünkü kalan indirmeler: %s",
|
||||||
|
"Remove": "Kaldır",
|
||||||
|
"Resets in: %s": "%s içinde sıfırlanacak",
|
||||||
|
"Screenshot": "Ekran görüntüsü",
|
||||||
|
"Search online": "Çevrimiçi ara",
|
||||||
|
"See above for clues.": "İpuçları için yukarıya bakın.",
|
||||||
|
"See console for details.": "Ayrıntılar için konsola bakın.",
|
||||||
|
"Show in directory": "Dizinde göster",
|
||||||
|
"Shuffle": "Karıştır",
|
||||||
|
"Something went wrong.": "Bir şeyler ters gitti.",
|
||||||
|
"Stream quality": "Yayın kalitesi",
|
||||||
|
"Subtitles": "Altyazılar",
|
||||||
|
"Subtitles loaded & enabled": "Altyazılar yüklendi ve etkinleştirildi",
|
||||||
|
"Toggle to disable.": "Devre dışı bırakmak için değiştir.",
|
||||||
|
"Track %s": "Parça %s",
|
||||||
|
"Up to date": "Güncel",
|
||||||
|
"Update available": "Güncelleme mevcut",
|
||||||
|
"Update uosc": "uosc güncelle",
|
||||||
|
"Updating uosc": "uosc güncelleniyor",
|
||||||
|
"Use as secondary": "İkincil olarak kullan",
|
||||||
|
"Utils": "Araçlar",
|
||||||
|
"Video": "Video",
|
||||||
|
"default": "varsayılan",
|
||||||
|
"drive": "sürücü",
|
||||||
|
"enter query": "Sorgu gir",
|
||||||
|
"external": "harici",
|
||||||
|
"forced": "zorunlu",
|
||||||
|
"foreign parts only": "sadece yabancı bölümler",
|
||||||
|
"hearing impaired": "işitme engelli",
|
||||||
|
"no results": "Sonuç yok",
|
||||||
|
"open file": "Dosya aç",
|
||||||
|
"parent dir": "üst dizin",
|
||||||
|
"playlist or file": "çalma listesi veya dosya",
|
||||||
|
"type & ctrl+enter to search": "Yaz & aramak için Ctrl+Enter'a bas",
|
||||||
|
"type to search": "Aramak için yaz",
|
||||||
|
"uosc has been installed. Restart mpv for it to take effect.": "uosc yüklendi. Etkin olması için mpv'yi yeniden başlatın."
|
||||||
|
}
|
|
@ -2,7 +2,12 @@
|
||||||
"%s are empty": "%s 为空",
|
"%s are empty": "%s 为空",
|
||||||
"%s channel": "%s 声道",
|
"%s channel": "%s 声道",
|
||||||
"%s channels": "%s 声道",
|
"%s channels": "%s 声道",
|
||||||
"%s to search": "%s 进行搜索",
|
"%s to delete": "使用 %s 进行删除",
|
||||||
|
"%s to go up in tree.": "使用 %s 返回上一级",
|
||||||
|
"%s to reorder.": "使用 %s 重新排序",
|
||||||
|
"%s to search": "使用 %s 进行搜索",
|
||||||
|
"Add to playlist": "添加到播放列表",
|
||||||
|
"Added to playlist": "已添加到播放列表",
|
||||||
"An error has occurred.": "出现错误",
|
"An error has occurred.": "出现错误",
|
||||||
"Aspect ratio": "纵横比",
|
"Aspect ratio": "纵横比",
|
||||||
"Audio": "音频",
|
"Audio": "音频",
|
||||||
|
@ -11,13 +16,13 @@
|
||||||
"Audio tracks": "音频轨道",
|
"Audio tracks": "音频轨道",
|
||||||
"Chapter %s": "第 %s 章",
|
"Chapter %s": "第 %s 章",
|
||||||
"Chapters": "章节",
|
"Chapters": "章节",
|
||||||
|
"Copied to clipboard": "已复制到剪贴板",
|
||||||
"Default": "默认",
|
"Default": "默认",
|
||||||
"Default %s": "默认 %s",
|
"Default %s": "默认 %s",
|
||||||
|
"Delete": "删除",
|
||||||
"Delete file & Next": "删除文件并播放下一个",
|
"Delete file & Next": "删除文件并播放下一个",
|
||||||
"Delete file & Prev": "删除文件并播放上一个",
|
"Delete file & Prev": "删除文件并播放上一个",
|
||||||
"Delete file & Quit": "删除文件并退出",
|
"Delete file & Quit": "删除文件并退出",
|
||||||
"Disabled": "禁用",
|
|
||||||
"Download": "下载",
|
|
||||||
"Drives": "驱动器",
|
"Drives": "驱动器",
|
||||||
"Drop files or URLs to play here": "拖放文件或 URLs 到此处进行播放",
|
"Drop files or URLs to play here": "拖放文件或 URLs 到此处进行播放",
|
||||||
"Edition %s": "版本 %s",
|
"Edition %s": "版本 %s",
|
||||||
|
@ -28,56 +33,67 @@
|
||||||
"Key bindings": "键位绑定",
|
"Key bindings": "键位绑定",
|
||||||
"Last": "最后一个",
|
"Last": "最后一个",
|
||||||
"Load": "加载",
|
"Load": "加载",
|
||||||
"Load audio": "加载音频",
|
"Load audio": "加载音轨",
|
||||||
"Load subtitles": "加载字幕",
|
"Load subtitles": "加载字幕",
|
||||||
"Load video": "加载视频",
|
"Load video": "加载视频轨",
|
||||||
|
"Loaded audio": "已加载音轨",
|
||||||
|
"Loaded subtitles": "已加载字幕",
|
||||||
|
"Loaded video": "已加载视频轨",
|
||||||
"Loop file": "单个循环",
|
"Loop file": "单个循环",
|
||||||
"Loop playlist": "列表循环",
|
"Loop playlist": "列表循环",
|
||||||
"Menu": "菜单",
|
"Menu": "菜单",
|
||||||
|
"Move down": "下移",
|
||||||
|
"Move up": "上移",
|
||||||
"Navigation": "导航",
|
"Navigation": "导航",
|
||||||
"Next": "下一个",
|
"Next": "下一个",
|
||||||
"Next page": "下一页",
|
"Next page": "下一页",
|
||||||
"No file": "无文件",
|
"No file": "无文件",
|
||||||
"Open config folder": "打开设置文件夹",
|
"Open config folder": "打开配置文件夹",
|
||||||
"Open file": "打开文件",
|
"Open file": "打开文件",
|
||||||
|
"Open in browser": "在浏览器中打开",
|
||||||
|
"Open in mpv": "在 mpv 中打开",
|
||||||
|
"Paste path or url to add.": "粘贴路径或网址以添加",
|
||||||
|
"Paste path or url to open.": "粘贴路径或网址以打开",
|
||||||
"Play/Pause": "播放/暂停",
|
"Play/Pause": "播放/暂停",
|
||||||
"Playlist": "播放列表",
|
"Playlist": "播放列表",
|
||||||
"Playlist/Files": "播放/文件列表",
|
"Playlist/Files": "播放列表/文件列表",
|
||||||
"Prev": "上一个",
|
"Prev": "上一个",
|
||||||
"Previous": "上一个",
|
"Previous": "上一个",
|
||||||
"Previous page": "上一页",
|
"Previous page": "上一页",
|
||||||
"Quit": "退出",
|
"Quit": "退出",
|
||||||
|
"Reload": "重载",
|
||||||
"Remaining downloads today: %s": "今天的剩余下载量: %s",
|
"Remaining downloads today: %s": "今天的剩余下载量: %s",
|
||||||
|
"Remove": "移除",
|
||||||
"Resets in: %s": "重置: %s",
|
"Resets in: %s": "重置: %s",
|
||||||
"Screenshot": "截图",
|
"Screenshot": "截图",
|
||||||
|
"Search online": "在线搜索",
|
||||||
"See above for clues.": "线索见上文",
|
"See above for clues.": "线索见上文",
|
||||||
|
"See console for details.": "参阅控制台了解详细信息",
|
||||||
"Show in directory": "打开所在文件夹",
|
"Show in directory": "打开所在文件夹",
|
||||||
"Shuffle": "乱序",
|
"Shuffle": "乱序",
|
||||||
|
"Something went wrong.": "出错了",
|
||||||
"Stream quality": "流媒体品质",
|
"Stream quality": "流媒体品质",
|
||||||
"Subtitles": "字幕",
|
"Subtitles": "字幕",
|
||||||
"Subtitles loaded & enabled": "字幕已加载并启用",
|
"Subtitles loaded & enabled": "字幕已加载并启用",
|
||||||
|
"Toggle to disable.": "点击切换禁用状态",
|
||||||
"Track %s": "轨道 %s",
|
"Track %s": "轨道 %s",
|
||||||
"Update uosc": "更新 uosc",
|
"Update uosc": "更新 uosc",
|
||||||
"Updating uosc": "正在更新 uosc",
|
"Updating uosc": "正在更新 uosc",
|
||||||
|
"Use as secondary": "设置为次字幕",
|
||||||
"Utils": "工具",
|
"Utils": "工具",
|
||||||
"Video": "视频",
|
"Video": "视频",
|
||||||
"default": "默认",
|
"default": "默认",
|
||||||
"drive": "磁盘",
|
"drive": "磁盘",
|
||||||
"enter query": "输入查询",
|
"enter query": "输入查询",
|
||||||
"error": "错误",
|
|
||||||
"external": "外置",
|
"external": "外置",
|
||||||
"forced": "强制",
|
"forced": "强制",
|
||||||
"foreign parts only": "仅限外语部分",
|
"foreign parts only": "仅限外语部分",
|
||||||
"hearing impaired": "听力障碍",
|
"hearing impaired": "听力障碍",
|
||||||
"invalid response json (see console for details)": "无效的响应 json (请参阅控制台了解详细信息)",
|
|
||||||
"no results": "没有结果",
|
"no results": "没有结果",
|
||||||
"open file": "打开文件",
|
"open file": "打开文件",
|
||||||
"parent dir": "父文件夹",
|
"parent dir": "父文件夹",
|
||||||
"playlist or file": "播放列表或文件",
|
"playlist or file": "播放列表或文件",
|
||||||
"process exited with code %s (see console for details)": "进程以代码 %s 退出 (请参阅控制台了解详细信息)",
|
|
||||||
"search online": "在线搜索",
|
|
||||||
"type & ctrl+enter to search": "输入并按 ctrl+enter 进行搜索",
|
"type & ctrl+enter to search": "输入并按 ctrl+enter 进行搜索",
|
||||||
"type to search": "输入搜索内容",
|
"type to search": "输入搜索内容",
|
||||||
"unknown error": "未知错误",
|
|
||||||
"uosc has been installed. Restart mpv for it to take effect.": "uosc 已经安装,重新启动 mpv 使其生效"
|
"uosc has been installed. Restart mpv for it to take effect.": "uosc 已经安装,重新启动 mpv 使其生效"
|
||||||
}
|
}
|
|
@ -85,30 +85,56 @@ end
|
||||||
-- Tooltip.
|
-- Tooltip.
|
||||||
---@param element Rect
|
---@param element Rect
|
||||||
---@param value string|number
|
---@param value string|number
|
||||||
---@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, margin?: number; responsive?: boolean; lines?: integer, timestamp?: boolean}
|
---@param opts? {size?: number; align?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, margin?: number; responsive?: boolean; lines?: integer, timestamp?: boolean; invert_colors?: boolean}
|
||||||
function ass_mt:tooltip(element, value, opts)
|
function ass_mt:tooltip(element, value, opts)
|
||||||
if value == '' then return end
|
if value == '' then return end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
opts.size = opts.size or round(16 * state.scale)
|
opts.size = opts.size or round(16 * state.scale)
|
||||||
opts.border = options.text_border * state.scale
|
opts.border = options.text_border * state.scale
|
||||||
opts.border_color = bg
|
opts.border_color = opts.invert_colors and fg or bg
|
||||||
opts.margin = opts.margin or round(10 * state.scale)
|
opts.margin = opts.margin or round(10 * state.scale)
|
||||||
opts.lines = opts.lines or 1
|
opts.lines = opts.lines or 1
|
||||||
|
opts.color = opts.invert_colors and bg or fg
|
||||||
|
local offset = opts.offset or 2
|
||||||
local padding_y = round(opts.size / 6)
|
local padding_y = round(opts.size / 6)
|
||||||
local padding_x = round(opts.size / 3)
|
local padding_x = round(opts.size / 3)
|
||||||
local offset = opts.offset or 2
|
local width = (opts.width_overwrite or text_width(value, opts)) + padding_x * 2
|
||||||
local align_top = opts.responsive == false or element.ay - offset > opts.size * 2
|
local height = opts.size * opts.lines + 2 * padding_y
|
||||||
local x = element.ax + (element.bx - element.ax) / 2
|
local width_half, height_half = width / 2, height / 2
|
||||||
local y = align_top and element.ay - offset or element.by + offset
|
local margin = opts.margin + Elements:v('window_border', 'size', 0)
|
||||||
local width_half = (opts.width_overwrite or text_width(value, opts)) / 2 + padding_x
|
local align = opts.align or 8
|
||||||
local min_edge_distance = width_half + opts.margin + Elements:v('window_border', 'size', 0)
|
|
||||||
x = clamp(min_edge_distance, x, display.width - min_edge_distance)
|
local x, y = 0, 0 -- center of tooltip
|
||||||
local ax, bx = round(x - width_half), round(x + width_half)
|
|
||||||
local ay = (align_top and y - opts.size * opts.lines - 2 * padding_y or y)
|
-- Flip alignment to other side when not enough space
|
||||||
local by = (align_top and y or y + opts.size * opts.lines + 2 * padding_y)
|
if opts.responsive ~= false then
|
||||||
self:rect(ax, ay, bx, by, {color = bg, opacity = config.opacity.tooltip, radius = state.radius})
|
if align == 8 then
|
||||||
|
if element.ay - offset - height < margin then align = 2 end
|
||||||
|
elseif align == 2 then
|
||||||
|
if element.by + offset + height > display.height - margin then align = 8 end
|
||||||
|
elseif align == 6 then
|
||||||
|
if element.bx + offset + width > display.width - margin then align = 4 end
|
||||||
|
elseif align == 4 then
|
||||||
|
if element.ax - offset - width < margin then align = 6 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculate tooltip center based on alignment
|
||||||
|
if align == 8 or align == 2 then
|
||||||
|
x = clamp(margin + width_half, element.ax + (element.bx - element.ax) / 2, display.width - margin - width_half)
|
||||||
|
y = align == 8 and element.ay - offset - height_half or element.by + offset + height_half
|
||||||
|
else
|
||||||
|
x = align == 6 and element.bx + offset + width_half or element.ax - offset - width_half
|
||||||
|
y = clamp(margin + height_half, element.ay + (element.by - element.ay) / 2, display.height - margin - height_half)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw
|
||||||
|
local ax, ay, bx, by = round(x - width_half), round(y - height_half), round(x + width_half), round(y + height_half)
|
||||||
|
self:rect(ax, ay, bx, by, {
|
||||||
|
color = opts.invert_colors and fg or bg, opacity = config.opacity.tooltip, radius = state.radius
|
||||||
|
})
|
||||||
local func = opts.timestamp and self.timestamp or self.txt
|
local func = opts.timestamp and self.timestamp or self.txt
|
||||||
func(self, x, align_top and y - padding_y or y + padding_y, align_top and 2 or 8, tostring(value), opts)
|
func(self, x, y, 5, tostring(value), opts)
|
||||||
return {ax = element.ax, ay = ay, bx = element.bx, by = by}
|
return {ax = element.ax, ay = ay, bx = element.bx, by = by}
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
---@alias ButtonData {icon: string; active?: boolean; badge?: string; command?: string | string[]; tooltip?: string;}
|
||||||
|
---@alias ButtonSubscriber fun(data: ButtonData)
|
||||||
|
|
||||||
|
local buttons = {
|
||||||
|
---@type ButtonData[]
|
||||||
|
data = {},
|
||||||
|
---@type table<string, ButtonSubscriber[]>
|
||||||
|
subscribers = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@param callback fun(data: ButtonData)
|
||||||
|
function buttons:subscribe(name, callback)
|
||||||
|
local pool = self.subscribers[name]
|
||||||
|
if not pool then
|
||||||
|
pool = {}
|
||||||
|
self.subscribers[name] = pool
|
||||||
|
end
|
||||||
|
pool[#pool + 1] = callback
|
||||||
|
self:trigger(name)
|
||||||
|
return function() buttons:unsubscribe(name, callback) end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@param callback? ButtonSubscriber
|
||||||
|
function buttons:unsubscribe(name, callback)
|
||||||
|
if self.subscribers[name] then
|
||||||
|
if callback == nil then
|
||||||
|
self.subscribers[name] = {}
|
||||||
|
else
|
||||||
|
itable_delete_value(self.subscribers[name], callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
function buttons:trigger(name)
|
||||||
|
local pool = self.subscribers[name]
|
||||||
|
local data = self.data[name] or {icon = 'help_center', tooltip = 'Uninitialized button "' .. name .. '"'}
|
||||||
|
if pool then
|
||||||
|
for _, callback in ipairs(pool) do callback(data) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@param data ButtonData
|
||||||
|
function buttons:set(name, data)
|
||||||
|
buttons.data[name] = data
|
||||||
|
buttons:trigger(name)
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_script_message('set-button', function(name, data)
|
||||||
|
if type(name) ~= 'string' then
|
||||||
|
msg.error('Invalid set-button message parameter: 1st parameter (name) has to be a string.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(data) ~= 'string' then
|
||||||
|
msg.error('Invalid set-button message parameter: 2nd parameter (data) has to be a string.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = utils.parse_json(data)
|
||||||
|
if type(data) == 'table' and type(data.icon) == 'string' then
|
||||||
|
buttons:set(name, data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return buttons
|
|
@ -4,13 +4,8 @@ local char_dir = mp.get_script_directory() .. '/char-conv/'
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
local languages = get_languages()
|
local languages = get_languages()
|
||||||
for i = #languages, 1, -1 do
|
for _, lang in ipairs(languages) do
|
||||||
lang = languages[i]
|
table_assign(data, get_locale_from_json(char_dir .. lang:lower() .. '.json'))
|
||||||
if (lang == 'en') then
|
|
||||||
data = {}
|
|
||||||
else
|
|
||||||
table_assign(data, get_locale_from_json(char_dir .. lang:lower() .. '.json'))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local romanization = {}
|
local romanization = {}
|
|
@ -1,10 +1,13 @@
|
||||||
|
---@alias CursorEventHandler fun(shortcut: Shortcut)
|
||||||
|
|
||||||
local cursor = {
|
local cursor = {
|
||||||
x = math.huge,
|
x = math.huge,
|
||||||
y = math.huge,
|
y = math.huge,
|
||||||
hidden = true,
|
hidden = true,
|
||||||
hover_raw = false,
|
distance = 0, -- Distance traveled during current move. Reset by `cursor.distance_reset_timer`.
|
||||||
|
last_hover = false, -- Stores `mouse.hover` boolean of the last mouse event for enter/leave detection.
|
||||||
-- Event handlers that are only fired on zones defined during render loop.
|
-- Event handlers that are only fired on zones defined during render loop.
|
||||||
---@type {event: string, hitbox: Hitbox; handler: fun(...)}[]
|
---@type {event: string, hitbox: Hitbox; handler: CursorEventHandler}[]
|
||||||
zones = {},
|
zones = {},
|
||||||
handlers = {
|
handlers = {
|
||||||
primary_down = {},
|
primary_down = {},
|
||||||
|
@ -68,6 +71,12 @@ mp.observe_property('cursor-autohide', 'number', function(_, val)
|
||||||
cursor.autohide_timer.timeout = (val or 1000) / 1000
|
cursor.autohide_timer.timeout = (val or 1000) / 1000
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
cursor.distance_reset_timer = mp.add_timeout(0.2, function()
|
||||||
|
cursor.distance = 0
|
||||||
|
request_render()
|
||||||
|
end)
|
||||||
|
cursor.distance_reset_timer:kill()
|
||||||
|
|
||||||
-- Called at the beginning of each render
|
-- Called at the beginning of each render
|
||||||
function cursor:clear_zones()
|
function cursor:clear_zones()
|
||||||
itable_clear(self.zones)
|
itable_clear(self.zones)
|
||||||
|
@ -105,7 +114,7 @@ end
|
||||||
-- - `move` event zones are ignored due to it being a high frequency event that is currently not needed as a zone.
|
-- - `move` event zones are ignored due to it being a high frequency event that is currently not needed as a zone.
|
||||||
---@param event string
|
---@param event string
|
||||||
---@param hitbox Hitbox
|
---@param hitbox Hitbox
|
||||||
---@param callback fun(...)
|
---@param callback CursorEventHandler
|
||||||
function cursor:zone(event, hitbox, callback)
|
function cursor:zone(event, hitbox, callback)
|
||||||
self.zones[#self.zones + 1] = {event = event, hitbox = hitbox, handler = callback}
|
self.zones[#self.zones + 1] = {event = event, hitbox = hitbox, handler = callback}
|
||||||
end
|
end
|
||||||
|
@ -113,6 +122,7 @@ end
|
||||||
-- Binds a permanent cursor event handler active until manually unbound using `cursor:off()`.
|
-- Binds a permanent cursor event handler active until manually unbound using `cursor:off()`.
|
||||||
-- `_click` events are not available as permanent global events, only as zones.
|
-- `_click` events are not available as permanent global events, only as zones.
|
||||||
---@param event string
|
---@param event string
|
||||||
|
---@param callback CursorEventHandler
|
||||||
---@return fun() disposer Unbinds the event.
|
---@return fun() disposer Unbinds the event.
|
||||||
function cursor:on(event, callback)
|
function cursor:on(event, callback)
|
||||||
if self.handlers[event] and not itable_index_of(self.handlers[event], callback) then
|
if self.handlers[event] and not itable_index_of(self.handlers[event], callback) then
|
||||||
|
@ -146,7 +156,8 @@ end
|
||||||
|
|
||||||
-- Trigger the event.
|
-- Trigger the event.
|
||||||
---@param event string
|
---@param event string
|
||||||
function cursor:trigger(event, ...)
|
---@param shortcut? Shortcut
|
||||||
|
function cursor:trigger(event, shortcut)
|
||||||
local forward = true
|
local forward = true
|
||||||
|
|
||||||
-- Call raw event handlers.
|
-- Call raw event handlers.
|
||||||
|
@ -154,8 +165,8 @@ function cursor:trigger(event, ...)
|
||||||
local callbacks = self.handlers[event]
|
local callbacks = self.handlers[event]
|
||||||
if zone or #callbacks > 0 then
|
if zone or #callbacks > 0 then
|
||||||
forward = false
|
forward = false
|
||||||
if zone then zone.handler(...) end
|
if zone and shortcut then zone.handler(shortcut) end
|
||||||
for _, callback in ipairs(callbacks) do callback(...) end
|
for _, callback in ipairs(callbacks) do callback(shortcut) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Call compound/parent (click) event handlers if both start and end events are within `parent_zone.hitbox`.
|
-- Call compound/parent (click) event handlers if both start and end events are within `parent_zone.hitbox`.
|
||||||
|
@ -166,8 +177,8 @@ function cursor:trigger(event, ...)
|
||||||
forward = false -- Canceled here so we don't forward down events if they can lead to a click.
|
forward = false -- Canceled here so we don't forward down events if they can lead to a click.
|
||||||
if parent.is_end then
|
if parent.is_end then
|
||||||
local last_start_event = self.last_event[parent.start_event]
|
local last_start_event = self.last_event[parent.start_event]
|
||||||
if last_start_event and point_collides_with(last_start_event, parent_zone.hitbox) then
|
if last_start_event and point_collides_with(last_start_event, parent_zone.hitbox) and shortcut then
|
||||||
parent_zone.handler(...)
|
parent_zone.handler(create_shortcut('primary_click', shortcut.modifiers))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -179,7 +190,7 @@ function cursor:trigger(event, ...)
|
||||||
if forward_name then
|
if forward_name then
|
||||||
-- Forward events if there was no handler.
|
-- Forward events if there was no handler.
|
||||||
local active = find_active_keybindings(forward_name)
|
local active = find_active_keybindings(forward_name)
|
||||||
if active then
|
if active and active.cmd then
|
||||||
local is_wheel = event:find('wheel', 1, true)
|
local is_wheel = event:find('wheel', 1, true)
|
||||||
local is_up = event:sub(-3) == '_up'
|
local is_up = event:sub(-3) == '_up'
|
||||||
if active.owner then
|
if active.owner then
|
||||||
|
@ -255,7 +266,7 @@ function cursor:_find_history_sample()
|
||||||
return self.history:tail()
|
return self.history:tail()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Returns a table with current velocities in in pixels per second.
|
-- Returns the current velocity vector in pixels per second.
|
||||||
---@return Point
|
---@return Point
|
||||||
function cursor:get_velocity()
|
function cursor:get_velocity()
|
||||||
local snap = self:_find_history_sample()
|
local snap = self:_find_history_sample()
|
||||||
|
@ -319,6 +330,16 @@ function cursor:move(x, y)
|
||||||
Elements:trigger('global_mouse_enter')
|
Elements:trigger('global_mouse_enter')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Update current move travel distance
|
||||||
|
-- `mp.get_time() - last.time < 0.5` check is there to ignore first event after long inactivity to
|
||||||
|
-- filter out big jumps due to window being repositioned/rescaled (e.g. opening a different file).
|
||||||
|
local last = self.last_event.move
|
||||||
|
if last and last.x < math.huge and last.y < math.huge and mp.get_time() - last.time < 0.5 then
|
||||||
|
self.distance = self.distance + get_point_to_point_proximity(cursor, last)
|
||||||
|
cursor.distance_reset_timer:kill()
|
||||||
|
cursor.distance_reset_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
Elements:update_proximities()
|
Elements:update_proximities()
|
||||||
-- Update history
|
-- Update history
|
||||||
self.history:insert({x = self.x, y = self.y, time = mp.get_time()})
|
self.history:insert({x = self.x, y = self.y, time = mp.get_time()})
|
||||||
|
@ -367,44 +388,56 @@ function cursor:direction_to_rectangle_distance(rect)
|
||||||
return get_ray_to_rectangle_distance(self.x, self.y, end_x, end_y, rect)
|
return get_ray_to_rectangle_distance(self.x, self.y, end_x, end_y, rect)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cursor:create_handler(event, cb)
|
---@param event string
|
||||||
return function(...)
|
---@param shortcut Shortcut
|
||||||
call_maybe(cb, ...)
|
---@param cb? fun(shortcut: Shortcut)
|
||||||
self:trigger(event, ...)
|
function cursor:create_handler(event, shortcut, cb)
|
||||||
|
return function()
|
||||||
|
if cb then cb(shortcut) end
|
||||||
|
self:trigger(event, shortcut)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Movement
|
-- Movement
|
||||||
function handle_mouse_pos(_, mouse)
|
function handle_mouse_pos(_, mouse)
|
||||||
if not mouse then return end
|
if not mouse then return end
|
||||||
if cursor.hover_raw and not mouse.hover then
|
if cursor.last_hover and not mouse.hover then
|
||||||
cursor:leave()
|
cursor:leave()
|
||||||
else
|
elseif not (cursor.last_hover == false and mouse.hover == false) then -- filters out duplicate mouse out events
|
||||||
cursor:move(mouse.x, mouse.y)
|
cursor:move(mouse.x, mouse.y)
|
||||||
end
|
end
|
||||||
cursor.hover_raw = mouse.hover
|
cursor.last_hover = mouse.hover
|
||||||
end
|
end
|
||||||
mp.observe_property('mouse-pos', 'native', handle_mouse_pos)
|
mp.observe_property('mouse-pos', 'native', handle_mouse_pos)
|
||||||
|
|
||||||
-- Key binding groups
|
-- Key binding groups
|
||||||
mp.set_key_bindings({
|
local modifiers = {nil, 'alt', 'alt+ctrl', 'alt+shift', 'alt+ctrl+shift', 'ctrl', 'ctrl+shift', 'shift'}
|
||||||
{
|
local primary_bindings = {}
|
||||||
'mbtn_left',
|
for i = 1, #modifiers do
|
||||||
cursor:create_handler('primary_up'),
|
local mods = modifiers[i]
|
||||||
cursor:create_handler('primary_down', function(...)
|
local mp_name = (mods and mods .. '+' or '') .. 'mbtn_left'
|
||||||
|
primary_bindings[#primary_bindings + 1] = {
|
||||||
|
mp_name,
|
||||||
|
cursor:create_handler('primary_up', create_shortcut('primary_up', mods)),
|
||||||
|
cursor:create_handler('primary_down', create_shortcut('primary_down', mods), function(...)
|
||||||
handle_mouse_pos(nil, mp.get_property_native('mouse-pos'))
|
handle_mouse_pos(nil, mp.get_property_native('mouse-pos'))
|
||||||
end),
|
end),
|
||||||
},
|
}
|
||||||
}, 'mbtn_left', 'force')
|
end
|
||||||
|
mp.set_key_bindings(primary_bindings, 'mbtn_left', 'force')
|
||||||
mp.set_key_bindings({
|
mp.set_key_bindings({
|
||||||
{'mbtn_left_dbl', 'ignore'},
|
{'mbtn_left_dbl', 'ignore'},
|
||||||
}, 'mbtn_left_dbl', 'force')
|
}, 'mbtn_left_dbl', 'force')
|
||||||
mp.set_key_bindings({
|
mp.set_key_bindings({
|
||||||
{'mbtn_right', cursor:create_handler('secondary_up'), cursor:create_handler('secondary_down')},
|
{
|
||||||
|
'mbtn_right',
|
||||||
|
cursor:create_handler('secondary_up', create_shortcut('secondary_up')),
|
||||||
|
cursor:create_handler('secondary_down', create_shortcut('secondary_down')),
|
||||||
|
},
|
||||||
}, 'mbtn_right', 'force')
|
}, 'mbtn_right', 'force')
|
||||||
mp.set_key_bindings({
|
mp.set_key_bindings({
|
||||||
{'wheel_up', cursor:create_handler('wheel_up')},
|
{'wheel_up', cursor:create_handler('wheel_up', create_shortcut('wheel_up'))},
|
||||||
{'wheel_down', cursor:create_handler('wheel_down')},
|
{'wheel_down', cursor:create_handler('wheel_down', create_shortcut('wheel_down'))},
|
||||||
}, 'wheel', 'force')
|
}, 'wheel', 'force')
|
||||||
|
|
||||||
return cursor
|
return cursor
|
1135
multimedia/.config/mpv/scripts/uosc_repo/src/uosc/lib/menus.lua
Normal file
1135
multimedia/.config/mpv/scripts/uosc_repo/src/uosc/lib/menus.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
||||||
--[[ Stateless utilities missing in lua standard library ]]
|
--[[ Stateless utilities missing in lua standard library ]]
|
||||||
|
|
||||||
|
---@alias Shortcut {id: string; key: string; modifiers?: string; alt: boolean; ctrl: boolean; shift: boolean}
|
||||||
|
|
||||||
---@param number number
|
---@param number number
|
||||||
function round(number) return math.floor(number + 0.5) end
|
function round(number) return math.floor(number + 0.5) end
|
||||||
|
|
||||||
|
@ -17,6 +19,11 @@ function serialize_rgba(rgba)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Trim any white space from the start and end of the string.
|
||||||
|
---@param str string
|
||||||
|
---@return string
|
||||||
|
function trim(str) return str:match('^%s*(.-)%s*$') end
|
||||||
|
|
||||||
-- Trim any `char` from the end of the string.
|
-- Trim any `char` from the end of the string.
|
||||||
---@param str string
|
---@param str string
|
||||||
---@param char string
|
---@param char string
|
||||||
|
@ -76,12 +83,18 @@ function string_last_index_of(str, sub)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Escapes a string to be used in a matching expression.
|
||||||
|
---@param value string
|
||||||
|
function regexp_escape(value)
|
||||||
|
return string.gsub(value, '[%(%)%.%+%-%*%?%[%]%^%$%%]', '%%%1')
|
||||||
|
end
|
||||||
|
|
||||||
---@param itable table
|
---@param itable table
|
||||||
---@param value any
|
---@param value any
|
||||||
---@return integer|nil
|
---@return integer|nil
|
||||||
function itable_index_of(itable, value)
|
function itable_index_of(itable, value)
|
||||||
for index, item in ipairs(itable) do
|
for index = 1, #itable do
|
||||||
if item == value then return index end
|
if itable[index] == value then return index end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -217,6 +230,19 @@ function table_assign_props(target, source, props)
|
||||||
return target
|
return target
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Assign props from `source` to `target` that are not in `props` set.
|
||||||
|
---@generic T: table<any, any>
|
||||||
|
---@param target T
|
||||||
|
---@param source T
|
||||||
|
---@param props table<string, boolean>
|
||||||
|
---@return T
|
||||||
|
function table_assign_exclude(target, source, props)
|
||||||
|
for key, value in pairs(source) do
|
||||||
|
if not props[key] then target[key] = value end
|
||||||
|
end
|
||||||
|
return target
|
||||||
|
end
|
||||||
|
|
||||||
-- `table_assign({}, input)` without loosing types :(
|
-- `table_assign({}, input)` without loosing types :(
|
||||||
---@generic T: table<any, any>
|
---@generic T: table<any, any>
|
||||||
---@param input T
|
---@param input T
|
||||||
|
@ -244,6 +270,26 @@ function serialize_key_value_list(input, value_sanitizer)
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param key string
|
||||||
|
---@param modifiers? string
|
||||||
|
---@return Shortcut
|
||||||
|
function create_shortcut(key, modifiers)
|
||||||
|
key = key:lower()
|
||||||
|
|
||||||
|
local id_parts, modifiers_set
|
||||||
|
if modifiers then
|
||||||
|
id_parts = split(modifiers:lower(), '+')
|
||||||
|
table.sort(id_parts, function(a, b) return a < b end)
|
||||||
|
modifiers_set = create_set(id_parts)
|
||||||
|
modifiers = table.concat(id_parts, '+')
|
||||||
|
else
|
||||||
|
id_parts, modifiers, modifiers_set = {}, nil, {}
|
||||||
|
end
|
||||||
|
id_parts[#id_parts + 1] = key
|
||||||
|
|
||||||
|
return table_assign({id = table.concat(id_parts, '+'), key = key, modifiers = modifiers}, modifiers_set)
|
||||||
|
end
|
||||||
|
|
||||||
--[[ EASING FUNCTIONS ]]
|
--[[ EASING FUNCTIONS ]]
|
||||||
|
|
||||||
function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end
|
function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end
|
|
@ -382,7 +382,7 @@ do
|
||||||
---@type boolean, boolean
|
---@type boolean, boolean
|
||||||
local bold, italic = opts.bold or options.font_bold, opts.italic or false
|
local bold, italic = opts.bold or options.font_bold, opts.italic or false
|
||||||
|
|
||||||
if config.refine.text_width then
|
if not config.refine.text_width then
|
||||||
---@type {[string|number]: {[1]: number, [2]: integer}}
|
---@type {[string|number]: {[1]: number, [2]: integer}}
|
||||||
local text_width = get_cache_stage(width_cache, bold)
|
local text_width = get_cache_stage(width_cache, bold)
|
||||||
local width_px = text_width[text]
|
local width_px = text_width[text]
|
|
@ -4,9 +4,7 @@
|
||||||
---@alias Rect {ax: number, ay: number, bx: number, by: number, window_drag?: boolean}
|
---@alias Rect {ax: number, ay: number, bx: number, by: number, window_drag?: boolean}
|
||||||
---@alias Circle {point: Point, r: number, window_drag?: boolean}
|
---@alias Circle {point: Point, r: number, window_drag?: boolean}
|
||||||
---@alias Hitbox Rect|Circle
|
---@alias Hitbox Rect|Circle
|
||||||
|
---@alias ComplexBindingInfo {event: 'down' | 'repeat' | 'up' | 'press'; is_mouse: boolean; canceled: boolean; key_name?: string; key_text?: string;}
|
||||||
--- In place sorting of filenames
|
|
||||||
---@param filenames string[]
|
|
||||||
|
|
||||||
-- String sorting
|
-- String sorting
|
||||||
do
|
do
|
||||||
|
@ -223,11 +221,6 @@ function get_ray_to_rectangle_distance(ax, ay, bx, by, rect)
|
||||||
return closest
|
return closest
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Call function with args if it exists
|
|
||||||
function call_maybe(fn, ...)
|
|
||||||
if type(fn) == 'function' then fn(...) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extracts the properties used by property expansion of that string.
|
-- Extracts the properties used by property expansion of that string.
|
||||||
---@param str string
|
---@param str string
|
||||||
---@param res { [string] : boolean } | nil
|
---@param res { [string] : boolean } | nil
|
||||||
|
@ -398,9 +391,20 @@ function has_any_extension(path, extensions)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string
|
-- Executes mp command defined as a string or an itable, or does nothing if command is any other value.
|
||||||
function get_default_directory()
|
-- Returns boolean specifying if command was executed or not.
|
||||||
return mp.command_native({'expand-path', options.default_directory})
|
---@param command string | string[] | nil | any
|
||||||
|
---@return boolean executed `true` if command was executed.
|
||||||
|
function execute_command(command)
|
||||||
|
local command_type = type(command)
|
||||||
|
if command_type == 'string' then
|
||||||
|
mp.command(command)
|
||||||
|
return true
|
||||||
|
elseif command_type == 'table' and #command > 0 then
|
||||||
|
mp.command_native(command)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Serializes path into its semantic parts.
|
-- Serializes path into its semantic parts.
|
||||||
|
@ -427,19 +431,18 @@ end
|
||||||
-- Reads items in directory and splits it into directories and files tables.
|
-- Reads items in directory and splits it into directories and files tables.
|
||||||
---@param path string
|
---@param path string
|
||||||
---@param opts? {types?: string[], hidden?: boolean}
|
---@param opts? {types?: string[], hidden?: boolean}
|
||||||
---@return string[]|nil files
|
---@return string[] files
|
||||||
---@return string[]|nil directories
|
---@return string[] directories
|
||||||
|
---@return string|nil error
|
||||||
function read_directory(path, opts)
|
function read_directory(path, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local items, error = utils.readdir(path, 'all')
|
local items, error = utils.readdir(path, 'all')
|
||||||
|
local files, directories = {}, {}
|
||||||
|
|
||||||
if not items then
|
if not items then
|
||||||
msg.error('Reading files from "' .. path .. '" failed: ' .. error)
|
return files, directories, 'Reading directory "' .. path .. '" failed. Error: ' .. utils.to_string(error)
|
||||||
return nil, nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local files, directories = {}, {}
|
|
||||||
|
|
||||||
for _, item in ipairs(items) do
|
for _, item in ipairs(items) do
|
||||||
if item ~= '.' and item ~= '..' and (opts.hidden or item:sub(1, 1) ~= '.') then
|
if item ~= '.' and item ~= '..' and (opts.hidden or item:sub(1, 1) ~= '.') then
|
||||||
local info = utils.file_info(join_path(path, item))
|
local info = utils.file_info(join_path(path, item))
|
||||||
|
@ -467,8 +470,11 @@ function get_adjacent_files(file_path, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local current_meta = serialize_path(file_path)
|
local current_meta = serialize_path(file_path)
|
||||||
if not current_meta then return end
|
if not current_meta then return end
|
||||||
local files = read_directory(current_meta.dirname, {hidden = opts.hidden})
|
local files, _dirs, error = read_directory(current_meta.dirname, {hidden = opts.hidden})
|
||||||
if not files then return end
|
if error then
|
||||||
|
msg.error(error)
|
||||||
|
return
|
||||||
|
end
|
||||||
sort_strings(files)
|
sort_strings(files)
|
||||||
local current_file_index
|
local current_file_index
|
||||||
local paths = {}
|
local paths = {}
|
||||||
|
@ -546,7 +552,7 @@ end
|
||||||
function navigate_directory(delta)
|
function navigate_directory(delta)
|
||||||
if not state.path or is_protocol(state.path) then return false end
|
if not state.path or is_protocol(state.path) then return false end
|
||||||
local paths, current_index = get_adjacent_files(state.path, {
|
local paths, current_index = get_adjacent_files(state.path, {
|
||||||
types = config.types.autoload,
|
types = config.types.load,
|
||||||
hidden = options.show_hidden_files,
|
hidden = options.show_hidden_files,
|
||||||
})
|
})
|
||||||
if paths and current_index then
|
if paths and current_index then
|
||||||
|
@ -631,7 +637,7 @@ function delete_file_navigate(delta)
|
||||||
if Menu:is_open('open-file') then
|
if Menu:is_open('open-file') then
|
||||||
Elements:maybe('menu', 'delete_value', path)
|
Elements:maybe('menu', 'delete_value', path)
|
||||||
end
|
end
|
||||||
delete_file(path)
|
if path then delete_file(path) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -782,18 +788,20 @@ end
|
||||||
---@return {[string]: table}|table
|
---@return {[string]: table}|table
|
||||||
function find_active_keybindings(key)
|
function find_active_keybindings(key)
|
||||||
local bindings = mp.get_property_native('input-bindings', {})
|
local bindings = mp.get_property_native('input-bindings', {})
|
||||||
local active = {} -- map: key-name -> bind-info
|
local active_map = {} -- map: key-name -> bind-info
|
||||||
|
local active_table = {}
|
||||||
for _, bind in pairs(bindings) do
|
for _, bind in pairs(bindings) do
|
||||||
if bind.owner ~= 'uosc' and bind.priority >= 0 and (not key or bind.key == key) and (
|
if bind.owner ~= 'uosc' and bind.priority >= 0 and (not key or bind.key == key) and (
|
||||||
not active[bind.key]
|
not active_map[bind.key]
|
||||||
or (active[bind.key].is_weak and not bind.is_weak)
|
or (active_map[bind.key].is_weak and not bind.is_weak)
|
||||||
or (bind.is_weak == active[bind.key].is_weak and bind.priority > active[bind.key].priority)
|
or (bind.is_weak == active_map[bind.key].is_weak and bind.priority > active_map[bind.key].priority)
|
||||||
)
|
)
|
||||||
then
|
then
|
||||||
active[bind.key] = bind
|
active_table[#active_table + 1] = bind
|
||||||
|
active_map[bind.key] = bind
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return not key and active or active[key]
|
return key and active_map[key] or active_table
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param type 'sub'|'audio'|'video'
|
---@param type 'sub'|'audio'|'video'
|
||||||
|
@ -806,32 +814,90 @@ function load_track(type, path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string|nil
|
---@param args (string|number)[]
|
||||||
function get_clipboard()
|
---@return string|nil error
|
||||||
|
---@return table data
|
||||||
|
function call_ziggy(args)
|
||||||
local result = mp.command_native({
|
local result = mp.command_native({
|
||||||
name = 'subprocess',
|
name = 'subprocess',
|
||||||
capture_stderr = true,
|
capture_stderr = true,
|
||||||
capture_stdout = true,
|
capture_stdout = true,
|
||||||
playback_only = false,
|
playback_only = false,
|
||||||
args = {config.ziggy_path, 'get-clipboard'},
|
args = itable_join({config.ziggy_path}, args),
|
||||||
})
|
})
|
||||||
|
|
||||||
local function print_error(message)
|
if result.status ~= 0 then
|
||||||
msg.error('Getting clipboard data failed. Error: ' .. message)
|
return 'Calling ziggy failed. Exit code ' .. result.status .. ': ' .. result.stdout .. result.stderr, {}
|
||||||
end
|
end
|
||||||
|
|
||||||
if result.status == 0 then
|
local data = utils.parse_json(result.stdout)
|
||||||
local data = utils.parse_json(result.stdout)
|
if not data then
|
||||||
if data and data.payload then
|
return 'Ziggy response error. Couldn\'t parse json: ' .. result.stdout, {}
|
||||||
return data.payload
|
elseif data.error then
|
||||||
else
|
return 'Ziggy error: ' .. data.message, {}
|
||||||
print_error(data and (data.error and data.message or 'unknown error') or 'couldn\'t parse json')
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
print_error('exit code ' .. result.status .. ': ' .. result.stdout .. result.stderr)
|
return nil, data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param args (string|number)[]
|
||||||
|
---@param callback fun(error: string|nil, data: table)
|
||||||
|
---@return fun() abort Function to abort the request.
|
||||||
|
function call_ziggy_async(args, callback)
|
||||||
|
local abort_signal = mp.command_native_async({
|
||||||
|
name = 'subprocess',
|
||||||
|
capture_stderr = true,
|
||||||
|
capture_stdout = true,
|
||||||
|
playback_only = false,
|
||||||
|
args = itable_join({config.ziggy_path}, args),
|
||||||
|
}, function(success, result, error)
|
||||||
|
if not success or not result or result.status ~= 0 then
|
||||||
|
local exit_code = (result and result.status or 'unknown')
|
||||||
|
local message = error or (result and result.stdout .. result.stderr) or ''
|
||||||
|
callback('Calling ziggy failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local json = result and type(result.stdout) == 'string' and result.stdout or ''
|
||||||
|
local data = utils.parse_json(json)
|
||||||
|
if not data then
|
||||||
|
callback('Ziggy response error. Couldn\'t parse json: ' .. json, {})
|
||||||
|
elseif data.error then
|
||||||
|
callback('Ziggy error: ' .. data.message, {})
|
||||||
|
else
|
||||||
|
return callback(nil, data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
mp.abort_async_command(abort_signal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string|nil
|
||||||
|
function get_clipboard()
|
||||||
|
local err, data = call_ziggy({'get-clipboard'})
|
||||||
|
if err then
|
||||||
|
mp.commandv('show-text', 'Get clipboard error. See console for details.')
|
||||||
|
msg.error(err)
|
||||||
|
end
|
||||||
|
return data and data.payload
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param payload any
|
||||||
|
---@return string|nil payload String that was copied to clipboard.
|
||||||
|
function set_clipboard(payload)
|
||||||
|
payload = tostring(payload)
|
||||||
|
local err, data = call_ziggy({'set-clipboard', payload})
|
||||||
|
if err then
|
||||||
|
mp.commandv('show-text', 'Set clipboard error. See console for details.')
|
||||||
|
msg.error(err)
|
||||||
|
else
|
||||||
|
mp.commandv('show-text', t('Copied to clipboard') .. ': ' .. payload, 3000)
|
||||||
|
end
|
||||||
|
return data and data.payload
|
||||||
|
end
|
||||||
|
|
||||||
--[[ RENDERING ]]
|
--[[ RENDERING ]]
|
||||||
|
|
||||||
function render()
|
function render()
|
|
@ -1,8 +1,10 @@
|
||||||
--[[ uosc | https://github.com/tomasklaen/uosc ]]
|
--[[ uosc | https://github.com/tomasklaen/uosc ]]
|
||||||
local uosc_version = '5.2.0'
|
local uosc_version = '5.8.0'
|
||||||
|
|
||||||
mp.commandv('script-message', 'uosc-version', uosc_version)
|
mp.commandv('script-message', 'uosc-version', uosc_version)
|
||||||
|
|
||||||
|
mp.set_property('osc', 'no')
|
||||||
|
|
||||||
assdraw = require('mp.assdraw')
|
assdraw = require('mp.assdraw')
|
||||||
opt = require('mp.options')
|
opt = require('mp.options')
|
||||||
utils = require('mp.utils')
|
utils = require('mp.utils')
|
||||||
|
@ -23,7 +25,7 @@ defaults = {
|
||||||
progress_line_width = 20,
|
progress_line_width = 20,
|
||||||
timeline_persistency = '',
|
timeline_persistency = '',
|
||||||
timeline_border = 1,
|
timeline_border = 1,
|
||||||
timeline_step = 5,
|
timeline_step = '5',
|
||||||
timeline_cache = true,
|
timeline_cache = true,
|
||||||
|
|
||||||
controls =
|
controls =
|
||||||
|
@ -51,7 +53,7 @@ defaults = {
|
||||||
top_bar = 'no-border',
|
top_bar = 'no-border',
|
||||||
top_bar_size = 40,
|
top_bar_size = 40,
|
||||||
top_bar_persistency = '',
|
top_bar_persistency = '',
|
||||||
top_bar_controls = true,
|
top_bar_controls = 'right',
|
||||||
top_bar_title = 'yes',
|
top_bar_title = 'yes',
|
||||||
top_bar_alt_title = '',
|
top_bar_alt_title = '',
|
||||||
top_bar_alt_title_place = 'below',
|
top_bar_alt_title_place = 'below',
|
||||||
|
@ -60,7 +62,6 @@ defaults = {
|
||||||
window_border_size = 1,
|
window_border_size = 1,
|
||||||
|
|
||||||
autoload = false,
|
autoload = false,
|
||||||
autoload_types = 'video,audio,image',
|
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
|
|
||||||
scale = 1,
|
scale = 1,
|
||||||
|
@ -92,6 +93,8 @@ defaults = {
|
||||||
'aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv',
|
'aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv',
|
||||||
image_types = 'apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp',
|
image_types = 'apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp',
|
||||||
subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt',
|
subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt',
|
||||||
|
playlist_types = 'm3u,m3u8,pls,url,cue',
|
||||||
|
load_types = 'video,audio,image',
|
||||||
default_directory = '~/',
|
default_directory = '~/',
|
||||||
show_hidden_files = false,
|
show_hidden_files = false,
|
||||||
use_trash = false,
|
use_trash = false,
|
||||||
|
@ -99,10 +102,11 @@ defaults = {
|
||||||
chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80',
|
chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80',
|
||||||
chapter_range_patterns = 'openings:オープニング;endings:エンディング',
|
chapter_range_patterns = 'openings:オープニング;endings:エンディング',
|
||||||
languages = 'slang,en',
|
languages = 'slang,en',
|
||||||
|
subtitles_directory = '~~/subtitles',
|
||||||
disable_elements = '',
|
disable_elements = '',
|
||||||
}
|
}
|
||||||
options = table_copy(defaults)
|
options = table_copy(defaults)
|
||||||
opt.read_options(options, 'uosc', function(changed_options)
|
function handle_options(changed_options)
|
||||||
if changed_options.time_precision then
|
if changed_options.time_precision then
|
||||||
timestamp_zero_rep_clear_cache()
|
timestamp_zero_rep_clear_cache()
|
||||||
end
|
end
|
||||||
|
@ -112,7 +116,8 @@ opt.read_options(options, 'uosc', function(changed_options)
|
||||||
Elements:trigger('options')
|
Elements:trigger('options')
|
||||||
Elements:update_proximities()
|
Elements:update_proximities()
|
||||||
request_render()
|
request_render()
|
||||||
end)
|
end
|
||||||
|
opt.read_options(options, 'uosc', handle_options)
|
||||||
-- Normalize values
|
-- Normalize values
|
||||||
options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
|
options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
|
||||||
if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end
|
if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end
|
||||||
|
@ -126,8 +131,9 @@ if options.total_time and options.destination_time == 'playtime-remaining' then
|
||||||
elseif not itable_index_of({'total', 'playtime-remaining', 'time-remaining'}, options.destination_time) then
|
elseif not itable_index_of({'total', 'playtime-remaining', 'time-remaining'}, options.destination_time) then
|
||||||
options.destination_time = 'playtime-remaining'
|
options.destination_time = 'playtime-remaining'
|
||||||
end
|
end
|
||||||
-- Ensure required environment configuration
|
if not itable_index_of({'left', 'right'}, options.top_bar_controls) then
|
||||||
if options.autoload then mp.commandv('set', 'keep-open-pause', 'no') end
|
options.top_bar_controls = options.top_bar_controls == 'yes' and 'right' or nil
|
||||||
|
end
|
||||||
|
|
||||||
--[[ INTERNATIONALIZATION ]]
|
--[[ INTERNATIONALIZATION ]]
|
||||||
local intl = require('lib/intl')
|
local intl = require('lib/intl')
|
||||||
|
@ -184,16 +190,12 @@ config = {
|
||||||
audio = comma_split(options.audio_types),
|
audio = comma_split(options.audio_types),
|
||||||
image = comma_split(options.image_types),
|
image = comma_split(options.image_types),
|
||||||
subtitle = comma_split(options.subtitle_types),
|
subtitle = comma_split(options.subtitle_types),
|
||||||
media = comma_split(options.video_types .. ',' .. options.audio_types .. ',' .. options.image_types),
|
playlist = comma_split(options.playlist_types),
|
||||||
autoload = (function()
|
media = comma_split(options.video_types
|
||||||
---@type string[]
|
.. ',' .. options.audio_types
|
||||||
local option_values = {}
|
.. ',' .. options.image_types
|
||||||
for _, name in ipairs(comma_split(options.autoload_types)) do
|
.. ',' .. options.playlist_types),
|
||||||
local value = options[name .. '_types']
|
load = {}, -- populated by update_load_types() below
|
||||||
if type(value) == 'string' then option_values[#option_values + 1] = value end
|
|
||||||
end
|
|
||||||
return comma_split(table.concat(option_values, ','))
|
|
||||||
end)(),
|
|
||||||
},
|
},
|
||||||
stream_quality_options = comma_split(options.stream_quality_options),
|
stream_quality_options = comma_split(options.stream_quality_options),
|
||||||
top_bar_flash_on = comma_split(options.top_bar_flash_on),
|
top_bar_flash_on = comma_split(options.top_bar_flash_on),
|
||||||
|
@ -228,10 +230,39 @@ config = {
|
||||||
color = table_copy(config_defaults.color),
|
color = table_copy(config_defaults.color),
|
||||||
opacity = table_copy(config_defaults.opacity),
|
opacity = table_copy(config_defaults.opacity),
|
||||||
cursor_leave_fadeout_elements = {'timeline', 'volume', 'top_bar', 'controls'},
|
cursor_leave_fadeout_elements = {'timeline', 'volume', 'top_bar', 'controls'},
|
||||||
|
timeline_step = 5,
|
||||||
|
timeline_step_flag = '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_load_types()
|
||||||
|
local extensions = {}
|
||||||
|
local types = create_set(comma_split(options.load_types:lower()))
|
||||||
|
|
||||||
|
if types.same then
|
||||||
|
types.same = nil
|
||||||
|
if state and state.type then types[state.type] = true end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, name in ipairs(table_keys(types)) do
|
||||||
|
local type_extensions = config.types[name]
|
||||||
|
if type(type_extensions) == 'table' then
|
||||||
|
itable_append(extensions, type_extensions)
|
||||||
|
else
|
||||||
|
msg.warn('Unknown load type: ' .. name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
config.types.load = extensions
|
||||||
|
end
|
||||||
|
|
||||||
-- Updates config with values dependent on options
|
-- Updates config with values dependent on options
|
||||||
function update_config()
|
function update_config()
|
||||||
|
-- Required environment config
|
||||||
|
if options.autoload then
|
||||||
|
mp.commandv('set', 'keep-open', 'yes')
|
||||||
|
mp.commandv('set', 'keep-open-pause', 'no')
|
||||||
|
end
|
||||||
|
|
||||||
-- Adds `{element}_persistency` config properties with forced visibility states (e.g.: `{paused = true}`)
|
-- Adds `{element}_persistency` config properties with forced visibility states (e.g.: `{paused = true}`)
|
||||||
for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do
|
for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do
|
||||||
local option_name = name .. '_persistency'
|
local option_name = name .. '_persistency'
|
||||||
|
@ -257,6 +288,16 @@ function update_config()
|
||||||
-- Global color shorthands
|
-- Global color shorthands
|
||||||
fg, bg = config.color.foreground, config.color.background
|
fg, bg = config.color.foreground, config.color.background
|
||||||
fgt, bgt = config.color.foreground_text, config.color.background_text
|
fgt, bgt = config.color.foreground_text, config.color.background_text
|
||||||
|
|
||||||
|
-- Timeline step
|
||||||
|
do
|
||||||
|
local is_exact = options.timeline_step:sub(-1) == '!'
|
||||||
|
config.timeline_step = tonumber(is_exact and options.timeline_step:sub(1, -2) or options.timeline_step)
|
||||||
|
config.timeline_step_flag = is_exact and 'exact' or ''
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Other
|
||||||
|
update_load_types()
|
||||||
end
|
end
|
||||||
update_config()
|
update_config()
|
||||||
|
|
||||||
|
@ -337,11 +378,14 @@ state = {
|
||||||
alt_title = nil,
|
alt_title = nil,
|
||||||
time = nil, -- current media playback time
|
time = nil, -- current media playback time
|
||||||
speed = 1,
|
speed = 1,
|
||||||
|
---@type number|nil
|
||||||
duration = nil, -- current media duration
|
duration = nil, -- current media duration
|
||||||
time_human = nil, -- current playback time in human format
|
time_human = nil, -- current playback time in human format
|
||||||
destination_time_human = nil, -- depends on options.destination_time
|
destination_time_human = nil, -- depends on options.destination_time
|
||||||
pause = mp.get_property_native('pause'),
|
pause = mp.get_property_native('pause'),
|
||||||
|
ime_active = mp.get_property_native("input-ime"),
|
||||||
chapters = {},
|
chapters = {},
|
||||||
|
---@type {index: number; title: string}|nil
|
||||||
current_chapter = nil,
|
current_chapter = nil,
|
||||||
chapter_ranges = {},
|
chapter_ranges = {},
|
||||||
border = mp.get_property_native('border'),
|
border = mp.get_property_native('border'),
|
||||||
|
@ -354,6 +398,7 @@ state = {
|
||||||
volume = nil,
|
volume = nil,
|
||||||
volume_max = nil,
|
volume_max = nil,
|
||||||
mute = nil,
|
mute = nil,
|
||||||
|
type = nil, -- video,image,audio
|
||||||
is_idle = false,
|
is_idle = false,
|
||||||
is_video = false,
|
is_video = false,
|
||||||
is_audio = false, -- true if file is audio only (mp3, etc)
|
is_audio = false, -- true if file is audio only (mp3, etc)
|
||||||
|
@ -373,6 +418,7 @@ state = {
|
||||||
cache = nil,
|
cache = nil,
|
||||||
cache_buffering = 100,
|
cache_buffering = 100,
|
||||||
cache_underrun = false,
|
cache_underrun = false,
|
||||||
|
cache_duration = nil,
|
||||||
core_idle = false,
|
core_idle = false,
|
||||||
eof_reached = false,
|
eof_reached = false,
|
||||||
render_delay = config.render_delay,
|
render_delay = config.render_delay,
|
||||||
|
@ -386,6 +432,7 @@ state = {
|
||||||
scale = 1,
|
scale = 1,
|
||||||
radius = 0,
|
radius = 0,
|
||||||
}
|
}
|
||||||
|
buttons = require('lib/buttons')
|
||||||
thumbnail = {width = 0, height = 0, disabled = false}
|
thumbnail = {width = 0, height = 0, disabled = false}
|
||||||
external = {} -- Properties set by external scripts
|
external = {} -- Properties set by external scripts
|
||||||
key_binding_overwrites = {} -- Table of key_binding:mpv_command
|
key_binding_overwrites = {} -- Table of key_binding:mpv_command
|
||||||
|
@ -429,23 +476,32 @@ function update_fullormaxed()
|
||||||
cursor:leave()
|
cursor:leave()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function update_duration()
|
||||||
|
local duration = state._duration and ((state.rebase_start_time == false and state.start_time)
|
||||||
|
and (state._duration + state.start_time) or state._duration)
|
||||||
|
set_state('duration', duration)
|
||||||
|
update_human_times()
|
||||||
|
end
|
||||||
|
|
||||||
function update_human_times()
|
function update_human_times()
|
||||||
|
state.speed = state.speed or 1
|
||||||
if state.time then
|
if state.time then
|
||||||
state.time_human = format_time(state.time, state.duration)
|
local max_seconds = state.duration
|
||||||
if state.duration then
|
if state.duration then
|
||||||
local speed = state.speed or 1
|
|
||||||
if options.destination_time == 'playtime-remaining' then
|
if options.destination_time == 'playtime-remaining' then
|
||||||
state.destination_time_human = format_time((state.time - state.duration) / speed, state.duration)
|
max_seconds = state.speed >= 1 and state.duration or state.duration / state.speed
|
||||||
|
state.destination_time_human = format_time((state.time - state.duration) / state.speed, max_seconds)
|
||||||
elseif options.destination_time == 'total' then
|
elseif options.destination_time == 'total' then
|
||||||
state.destination_time_human = format_time(state.duration, state.duration)
|
state.destination_time_human = format_time(state.duration, max_seconds)
|
||||||
else
|
else
|
||||||
state.destination_time_human = format_time(state.time - state.duration, state.duration)
|
state.destination_time_human = format_time(state.time - state.duration, max_seconds)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
state.destination_time_human = nil
|
state.destination_time_human = nil
|
||||||
end
|
end
|
||||||
|
state.time_human = format_time(state.time, max_seconds)
|
||||||
else
|
else
|
||||||
state.time_human = nil
|
state.time_human, state.destination_time_human = nil, nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -516,7 +572,8 @@ end
|
||||||
|
|
||||||
function set_state(name, value)
|
function set_state(name, value)
|
||||||
state[name] = value
|
state[name] = value
|
||||||
call_maybe(state['on_' .. name], value)
|
local state_event = state['on_' .. name]
|
||||||
|
if state_event then state_event(value) end
|
||||||
Elements:trigger('prop_' .. name, value)
|
Elements:trigger('prop_' .. name, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -540,12 +597,16 @@ function load_file_index_in_current_directory(index)
|
||||||
|
|
||||||
local serialized = serialize_path(state.path)
|
local serialized = serialize_path(state.path)
|
||||||
if serialized and serialized.dirname then
|
if serialized and serialized.dirname then
|
||||||
local files = read_directory(serialized.dirname, {
|
local files, _dirs, error = read_directory(serialized.dirname, {
|
||||||
types = config.types.autoload,
|
types = config.types.load,
|
||||||
hidden = options.show_hidden_files,
|
hidden = options.show_hidden_files,
|
||||||
})
|
})
|
||||||
|
|
||||||
if not files then return end
|
if error then
|
||||||
|
msg.error(error)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
sort_strings(files)
|
sort_strings(files)
|
||||||
if index < 0 then index = #files + index + 1 end
|
if index < 0 then index = #files + index + 1 end
|
||||||
|
|
||||||
|
@ -568,11 +629,18 @@ function observe_display_fps(name, fps)
|
||||||
end
|
end
|
||||||
|
|
||||||
function select_current_chapter()
|
function select_current_chapter()
|
||||||
|
local current_chapter_index = state.current_chapter and state.current_chapter.index
|
||||||
local current_chapter
|
local current_chapter
|
||||||
if state.time and state.chapters then
|
if state.time and state.chapters then
|
||||||
_, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, #state.chapters, 1)
|
_, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, #state.chapters, 1)
|
||||||
end
|
end
|
||||||
set_state('current_chapter', current_chapter)
|
local new_chapter_index = current_chapter and current_chapter.index
|
||||||
|
if current_chapter_index ~= new_chapter_index then
|
||||||
|
set_state('current_chapter', current_chapter)
|
||||||
|
if itable_has(config.top_bar_flash_on, 'chapter') then
|
||||||
|
Elements:flash({'top_bar'})
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[ STATE HOOKS ]]
|
--[[ STATE HOOKS ]]
|
||||||
|
@ -602,7 +670,6 @@ if options.click_threshold > 0 then
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
mp.observe_property('osc', 'bool', function(name, value) if value == true then mp.set_property('osc', 'no') end end)
|
|
||||||
mp.register_event('file-loaded', function()
|
mp.register_event('file-loaded', function()
|
||||||
local path = normalize_path(mp.get_property_native('path'))
|
local path = normalize_path(mp.get_property_native('path'))
|
||||||
itable_delete_value(state.history, path)
|
itable_delete_value(state.history, path)
|
||||||
|
@ -688,7 +755,9 @@ mp.observe_property('playback-time', 'number', create_state_setter('time', funct
|
||||||
update_human_times()
|
update_human_times()
|
||||||
select_current_chapter()
|
select_current_chapter()
|
||||||
end))
|
end))
|
||||||
mp.observe_property('duration', 'number', create_state_setter('duration', update_human_times))
|
mp.observe_property('rebase-start-time', 'bool', create_state_setter('rebase_start_time', update_duration))
|
||||||
|
mp.observe_property('demuxer-start-time', 'number', create_state_setter('start_time', update_duration))
|
||||||
|
mp.observe_property('duration', 'number', create_state_setter('_duration', update_duration))
|
||||||
mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times))
|
mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times))
|
||||||
mp.observe_property('track-list', 'native', function(name, value)
|
mp.observe_property('track-list', 'native', function(name, value)
|
||||||
-- checks the file dispositions
|
-- checks the file dispositions
|
||||||
|
@ -713,6 +782,8 @@ mp.observe_property('track-list', 'native', function(name, value)
|
||||||
set_state('has_many_sub', types.sub > 1)
|
set_state('has_many_sub', types.sub > 1)
|
||||||
set_state('is_video', types.video > 0)
|
set_state('is_video', types.video > 0)
|
||||||
set_state('has_many_video', types.video > 1)
|
set_state('has_many_video', types.video > 1)
|
||||||
|
set_state('type', state.is_video and 'video' or state.is_audio and 'audio' or state.is_image and 'image' or nil)
|
||||||
|
update_load_types()
|
||||||
Elements:trigger('dispositions')
|
Elements:trigger('dispositions')
|
||||||
end)
|
end)
|
||||||
mp.observe_property('editions', 'number', function(_, editions)
|
mp.observe_property('editions', 'number', function(_, editions)
|
||||||
|
@ -764,6 +835,7 @@ mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
|
||||||
if cache_state then
|
if cache_state then
|
||||||
cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached']
|
cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached']
|
||||||
set_state('cache_underrun', cache_state['underrun'])
|
set_state('cache_underrun', cache_state['underrun'])
|
||||||
|
set_state('cache_duration', not cache_state.eof and cache_state['cache-duration'] or nil)
|
||||||
else
|
else
|
||||||
cached_ranges = {}
|
cached_ranges = {}
|
||||||
end
|
end
|
||||||
|
@ -771,6 +843,7 @@ mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
|
||||||
if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or
|
if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or
|
||||||
(state.cache == 'auto' and state.is_stream))) then
|
(state.cache == 'auto' and state.is_stream))) then
|
||||||
if state.uncached_ranges then set_state('uncached_ranges', nil) end
|
if state.uncached_ranges then set_state('uncached_ranges', nil) end
|
||||||
|
set_state('cache_duration', nil)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -779,7 +852,7 @@ mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
|
||||||
for _, range in ipairs(cached_ranges) do
|
for _, range in ipairs(cached_ranges) do
|
||||||
ranges[#ranges + 1] = {
|
ranges[#ranges + 1] = {
|
||||||
math.max(range['start'] or 0, 0),
|
math.max(range['start'] or 0, 0),
|
||||||
math.min(range['end'] or state.duration, state.duration),
|
math.min(range['end'] or state.duration --[[@as number]], state.duration),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
table.sort(ranges, function(a, b) return a[1] < b[1] end)
|
table.sort(ranges, function(a, b) return a[1] < b[1] end)
|
||||||
|
@ -849,34 +922,51 @@ bind_command('keybinds', function()
|
||||||
end)
|
end)
|
||||||
bind_command('download-subtitles', open_subtitle_downloader)
|
bind_command('download-subtitles', open_subtitle_downloader)
|
||||||
bind_command('load-subtitles', create_track_loader_menu_opener({
|
bind_command('load-subtitles', create_track_loader_menu_opener({
|
||||||
name = 'subtitles', prop = 'sub', allowed_types = itable_join(config.types.video, config.types.subtitle),
|
prop = 'sub',
|
||||||
|
title = t('Load subtitles'),
|
||||||
|
loaded_message = t('Loaded subtitles'),
|
||||||
|
allowed_types = itable_join(config.types.video, config.types.subtitle),
|
||||||
}))
|
}))
|
||||||
bind_command('load-audio', create_track_loader_menu_opener({
|
bind_command('load-audio', create_track_loader_menu_opener({
|
||||||
name = 'audio', prop = 'audio', allowed_types = itable_join(config.types.video, config.types.audio),
|
prop = 'audio',
|
||||||
|
title = t('Load audio'),
|
||||||
|
loaded_message = t('Loaded audio'),
|
||||||
|
allowed_types = itable_join(config.types.video, config.types.audio),
|
||||||
}))
|
}))
|
||||||
bind_command('load-video', create_track_loader_menu_opener({
|
bind_command('load-video', create_track_loader_menu_opener({
|
||||||
name = 'video', prop = 'video', allowed_types = config.types.video,
|
prop = 'video',
|
||||||
|
title = t('Load video'),
|
||||||
|
loaded_message = t('Loaded video'),
|
||||||
|
allowed_types = config.types.video,
|
||||||
|
}))
|
||||||
|
bind_command('subtitles', create_select_tracklist_type_menu_opener({
|
||||||
|
title = t('Subtitles'),
|
||||||
|
type = 'sub',
|
||||||
|
prop = 'sid',
|
||||||
|
enable_prop = 'sub-visibility',
|
||||||
|
secondary = {prop = 'secondary-sid', icon = 'vertical_align_top', enable_prop = 'secondary-sub-visibility'},
|
||||||
|
load_command = 'script-binding uosc/load-subtitles',
|
||||||
|
download_command = 'script-binding uosc/download-subtitles',
|
||||||
|
}))
|
||||||
|
bind_command('audio', create_select_tracklist_type_menu_opener({
|
||||||
|
title = t('Audio'), type = 'audio', prop = 'aid', load_command = 'script-binding uosc/load-audio',
|
||||||
|
}))
|
||||||
|
bind_command('video', create_select_tracklist_type_menu_opener({
|
||||||
|
title = t('Video'), type = 'video', prop = 'vid', load_command = 'script-binding uosc/load-video',
|
||||||
}))
|
}))
|
||||||
bind_command('subtitles', create_select_tracklist_type_menu_opener(
|
|
||||||
t('Subtitles'), 'sub', 'sid', 'script-binding uosc/load-subtitles', 'script-binding uosc/download-subtitles'
|
|
||||||
))
|
|
||||||
bind_command('audio', create_select_tracklist_type_menu_opener(
|
|
||||||
t('Audio'), 'audio', 'aid', 'script-binding uosc/load-audio'
|
|
||||||
))
|
|
||||||
bind_command('video', create_select_tracklist_type_menu_opener(
|
|
||||||
t('Video'), 'video', 'vid', 'script-binding uosc/load-video'
|
|
||||||
))
|
|
||||||
bind_command('playlist', create_self_updating_menu_opener({
|
bind_command('playlist', create_self_updating_menu_opener({
|
||||||
title = t('Playlist'),
|
title = t('Playlist'),
|
||||||
type = 'playlist',
|
type = 'playlist',
|
||||||
list_prop = 'playlist',
|
list_prop = 'playlist',
|
||||||
|
footnote = t('Paste path or url to add.') .. ' ' .. t('%s to reorder.', 'ctrl+up/down/pgup/pgdn/home/end'),
|
||||||
serializer = function(playlist)
|
serializer = function(playlist)
|
||||||
local items = {}
|
local items = {}
|
||||||
|
local force_filename = mp.get_property_native('osd-playlist-entry') == 'filename'
|
||||||
for index, item in ipairs(playlist) do
|
for index, item in ipairs(playlist) do
|
||||||
local is_url = is_protocol(item.filename)
|
local title = type(item.title) == 'string' and #item.title > 0 and item.title or false
|
||||||
local item_title = type(item.title) == 'string' and #item.title > 0 and item.title or false
|
|
||||||
items[index] = {
|
items[index] = {
|
||||||
title = item_title or (is_url and item.filename or serialize_path(item.filename).basename),
|
title = (not force_filename and title) and title
|
||||||
|
or (is_protocol(item.filename) and item.filename or serialize_path(item.filename).basename),
|
||||||
hint = tostring(index),
|
hint = tostring(index),
|
||||||
active = item.current,
|
active = item.current,
|
||||||
value = index,
|
value = index,
|
||||||
|
@ -884,11 +974,19 @@ bind_command('playlist', create_self_updating_menu_opener({
|
||||||
end
|
end
|
||||||
return items
|
return items
|
||||||
end,
|
end,
|
||||||
on_select = function(index) mp.commandv('set', 'playlist-pos-1', tostring(index)) end,
|
on_activate = function(event) mp.commandv('set', 'playlist-pos-1', tostring(event.value)) end,
|
||||||
on_move_item = function(from, to)
|
on_paste = function(event) mp.commandv('loadfile', tostring(event.value), 'append') end,
|
||||||
mp.commandv('playlist-move', tostring(math.max(from, to) - 1), tostring(math.min(from, to) - 1))
|
on_key = function(event)
|
||||||
|
if event.id == 'ctrl+c' and event.selected_item then
|
||||||
|
local payload = mp.get_property_native('playlist/' .. (event.selected_item.value - 1) .. '/filename')
|
||||||
|
set_clipboard(payload)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
on_delete_item = function(index) mp.commandv('playlist-remove', tostring(index - 1)) end,
|
on_move = function(event)
|
||||||
|
local from, to = event.from_index, event.to_index
|
||||||
|
mp.commandv('playlist-move', tostring(from - 1), tostring(to - (to > from and 0 or 1)))
|
||||||
|
end,
|
||||||
|
on_remove = function(event) mp.commandv('playlist-remove', tostring(event.value - 1)) end,
|
||||||
}))
|
}))
|
||||||
bind_command('chapters', create_self_updating_menu_opener({
|
bind_command('chapters', create_self_updating_menu_opener({
|
||||||
title = t('Chapters'),
|
title = t('Chapters'),
|
||||||
|
@ -908,7 +1006,7 @@ bind_command('chapters', create_self_updating_menu_opener({
|
||||||
end
|
end
|
||||||
return items
|
return items
|
||||||
end,
|
end,
|
||||||
on_select = function(index) mp.commandv('set', 'chapter', tostring(index - 1)) end,
|
on_activate = function(event) mp.commandv('set', 'chapter', tostring(event.value - 1)) end,
|
||||||
}))
|
}))
|
||||||
bind_command('editions', create_self_updating_menu_opener({
|
bind_command('editions', create_self_updating_menu_opener({
|
||||||
title = t('Editions'),
|
title = t('Editions'),
|
||||||
|
@ -928,7 +1026,7 @@ bind_command('editions', create_self_updating_menu_opener({
|
||||||
end
|
end
|
||||||
return items
|
return items
|
||||||
end,
|
end,
|
||||||
on_select = function(id) mp.commandv('set', 'edition', id) end,
|
on_activate = function(event) mp.commandv('set', 'edition', event.value) end,
|
||||||
}))
|
}))
|
||||||
bind_command('show-in-directory', function()
|
bind_command('show-in-directory', function()
|
||||||
-- Ignore URLs
|
-- Ignore URLs
|
||||||
|
@ -1009,8 +1107,35 @@ bind_command('audio-device', create_self_updating_menu_opener({
|
||||||
end
|
end
|
||||||
return items
|
return items
|
||||||
end,
|
end,
|
||||||
on_select = function(name) mp.commandv('set', 'audio-device', name) end,
|
on_activate = function(event) mp.commandv('set', 'audio-device', event.value) end,
|
||||||
}))
|
}))
|
||||||
|
bind_command('paste', function()
|
||||||
|
local has_playlist = mp.get_property_native('playlist-count') > 1
|
||||||
|
mp.commandv('script-binding', 'uosc/paste-to-' .. (has_playlist and 'playlist' or 'open'))
|
||||||
|
end)
|
||||||
|
bind_command('paste-to-open', function()
|
||||||
|
local payload = get_clipboard()
|
||||||
|
if payload then mp.commandv('loadfile', payload) end
|
||||||
|
end)
|
||||||
|
bind_command('paste-to-playlist', function()
|
||||||
|
-- If there's no file loaded, we use `paste-to-open`, which both opens and adds to playlist
|
||||||
|
if state.is_idle then
|
||||||
|
mp.commandv('script-binding', 'uosc/paste-to-open')
|
||||||
|
else
|
||||||
|
local payload = get_clipboard()
|
||||||
|
if payload then
|
||||||
|
mp.commandv('loadfile', payload, 'append')
|
||||||
|
mp.commandv('show-text', t('Added to playlist') .. ': ' .. payload, 3000)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
bind_command('copy-to-clipboard', function()
|
||||||
|
if state.path then
|
||||||
|
set_clipboard(state.path)
|
||||||
|
else
|
||||||
|
mp.commandv('show-text', t('Nothing to copy'), 3000)
|
||||||
|
end
|
||||||
|
end)
|
||||||
bind_command('open-config-directory', function()
|
bind_command('open-config-directory', function()
|
||||||
local config_path = mp.command_native({'expand-path', '~~/mpv.conf'})
|
local config_path = mp.command_native({'expand-path', '~~/mpv.conf'})
|
||||||
local config = serialize_path(normalize_path(config_path))
|
local config = serialize_path(normalize_path(config_path))
|
||||||
|
@ -1058,9 +1183,30 @@ mp.register_script_message('update-menu', function(json)
|
||||||
if menu then menu:update(data) end
|
if menu then menu:update(data) end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
mp.register_script_message('select-menu-item', function(type, item_index, menu_id)
|
||||||
|
local menu = Menu:is_open(type)
|
||||||
|
local index = tonumber(item_index)
|
||||||
|
if menu and index and not menu.mouse_nav then
|
||||||
|
index = round(index)
|
||||||
|
if index > 0 and index <= #menu.current.items then
|
||||||
|
menu:select_index(index, menu_id)
|
||||||
|
menu:scroll_to_index(index, menu_id, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
mp.register_script_message('close-menu', function(type)
|
mp.register_script_message('close-menu', function(type)
|
||||||
if Menu:is_open(type) then Menu:close() end
|
if Menu:is_open(type) then Menu:close() end
|
||||||
end)
|
end)
|
||||||
|
mp.register_script_message('menu-action', function(name, ...)
|
||||||
|
local menu = Menu:is_open()
|
||||||
|
if menu then
|
||||||
|
local method = ({
|
||||||
|
['search-cancel'] = 'search_cancel',
|
||||||
|
['search-query-update'] = 'search_query_update',
|
||||||
|
})[name]
|
||||||
|
if method then menu[method](menu, ...) end
|
||||||
|
end
|
||||||
|
end)
|
||||||
mp.register_script_message('thumbfast-info', function(json)
|
mp.register_script_message('thumbfast-info', function(json)
|
||||||
local data = utils.parse_json(json)
|
local data = utils.parse_json(json)
|
||||||
if type(data) ~= 'table' or not data.width or not data.height then
|
if type(data) ~= 'table' or not data.width or not data.height then
|
||||||
|
@ -1117,6 +1263,7 @@ Manager = {
|
||||||
---@param element_ids string|string[]|nil `foo,bar` or `{'foo', 'bar'}`.
|
---@param element_ids string|string[]|nil `foo,bar` or `{'foo', 'bar'}`.
|
||||||
function Manager:disable(client, element_ids)
|
function Manager:disable(client, element_ids)
|
||||||
self._disabled_by[client] = comma_split(element_ids)
|
self._disabled_by[client] = comma_split(element_ids)
|
||||||
|
---@diagnostic disable-next-line: deprecated
|
||||||
self.disabled = create_set(itable_join(unpack(table_values(self._disabled_by))))
|
self.disabled = create_set(itable_join(unpack(table_values(self._disabled_by))))
|
||||||
self:_commit()
|
self:_commit()
|
||||||
end
|
end
|
Loading…
Reference in a new issue