This commit is contained in:
2026-04-03 11:33:51 +02:00
parent 64922e1ae3
commit 0ed904319d
57 changed files with 2935 additions and 1377 deletions
+1 -1
View File
@@ -402,4 +402,4 @@
"zui": "最嘴罪醉咀蕞䮔厜璻蟕晬嗺噿嶵㠑嶊冣㝡䘹祽鋷錊酻酔樶檌㰎栬槜檇辠䘒稡纗絊",
"zun": "尊遵樽鳟撙墫噂嶟鶎銌鱒鐏捘罇鷷僔繜譐",
"zuo": "作做坐左座昨佐琢撮柞唑祚捽阼胙嘬怍酢笮葄葃蓙䔘苲莋㸲㝾䞰䎰咗㘀㘴岝岞䝫糳袏鈼㭮稓穝秨筰㛗㑅飵侳繓䋏"
}
}
+1 -1
View File
@@ -36,4 +36,4 @@ function BufferingIndicator:render()
return ass
end
return BufferingIndicator
return BufferingIndicator
+2 -2
View File
@@ -40,7 +40,7 @@ function Button:render()
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 foreground = self.active and self.background or self.foreground
local background = self.active and self.foreground or self.background
local background_opacity = self.active and 1 or config.opacity.controls
@@ -97,4 +97,4 @@ function Button:render()
return ass
end
return Button
return Button
+9 -12
View File
@@ -37,17 +37,17 @@ function Controls:init_options()
-- Serialize control elements
local shorthands = {
['play-pause'] = 'cycle:pause:pause:no/yes=play_arrow?' .. t('Play/Pause'),
menu = 'command:menu_book:script-binding uosc/menu-blurred?' .. t('Menu'),
subtitles = 'command:closed_caption:script-binding uosc/subtitles#sub>1?' .. t('Subtitles'),
menu = 'command:menu:script-binding uosc/menu-blurred?' .. t('Menu'),
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?' .. t('Subtitles'),
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?' .. t('Audio'),
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?' .. t('Audio device'),
video = 'command:smart_display:script-binding uosc/video#video>1?' .. t('Video'),
playlist = 'command:list_alt:script-binding uosc/playlist#playlist>1?' .. t('Playlist'),
chapters = 'command:library_books:script-binding uosc/chapters#chapters>1?' .. t('Chapters'),
['editions'] = 'command:movie_filter:script-binding uosc/editions#editions>1?' .. t('Editions'),
video = 'command:theaters:script-binding uosc/video#video>1?' .. t('Video'),
playlist = 'command:list_alt:script-binding uosc/playlist?' .. t('Playlist'),
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?' .. t('Chapters'),
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?' .. t('Editions'),
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?' .. t('Stream quality'),
['open-file'] = 'command:folder:script-binding uosc/open-file?' .. t('Open file'),
['items'] = 'command:list_alt:script-binding uosc/items#playlist>1?' .. t('Playlist/Files'),
['open-file'] = 'command:file_open:script-binding uosc/open-file?' .. t('Open file'),
['items'] = 'command:list_alt:script-binding uosc/items?' .. t('Playlist/Files'),
prev = 'command:arrow_back_ios:script-binding uosc/prev?' .. t('Previous'),
next = 'command:arrow_forward_ios:script-binding uosc/next?' .. t('Next'),
first = 'command:first_page:script-binding uosc/first?' .. t('First'),
@@ -271,9 +271,6 @@ function Controls:register_badge_updater(badge, element)
for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end
return count
end
elseif prop == 'playlist' then
observable_name = 'playlist-count'
serializer = function(count) return count end
else
local parts = split(prop, '@')
-- Support both new `prop@owner` and old `@prop` syntaxes
@@ -428,4 +425,4 @@ function Controls:on_options()
self:init_options()
end
return Controls
return Controls
+1 -1
View File
@@ -32,4 +32,4 @@ function Curtain:render()
return ass
end
return Curtain
return Curtain
+1 -1
View File
@@ -83,4 +83,4 @@ function CycleButton:init(id, props)
end
end
return CycleButton
return CycleButton
+1 -1
View File
@@ -262,4 +262,4 @@ function Element:create_action(fn)
end
end
return Element
return Element
+5 -5
View File
@@ -48,14 +48,14 @@ function Elements:update_proximities()
element:update_proximity()
end
if element.proximity_raw == 0 then
if element.proximity_raw <= 0 then
-- Mouse entered element area
if previous_proximity_raw ~= 0 then
if previous_proximity_raw > 0 then
mouse_enter_elements[#mouse_enter_elements + 1] = element
end
else
-- Mouse left element area
if previous_proximity_raw == 0 then
if previous_proximity_raw <= 0 then
mouse_leave_elements[#mouse_leave_elements + 1] = element
end
end
@@ -122,7 +122,7 @@ function Elements:proximity_trigger(name, ...)
for i = #self._all, 1, -1 do
local element = self._all[i]
if element.enabled then
if element.proximity_raw == 0 then
if element.proximity_raw <= 0 then
if element:trigger(name, ...) == 'stop_propagation' then break end
end
if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end
@@ -149,4 +149,4 @@ end
function Elements:has(id) return self[id] ~= nil end
function Elements:ipairs() return ipairs(self._all) end
return Elements
return Elements
+1 -1
View File
@@ -33,4 +33,4 @@ function ManagedButton:update(data)
end
end
return ManagedButton
return ManagedButton
+110 -118
View File
@@ -309,7 +309,7 @@ function Menu:update_content_dimensions()
for _, menu in ipairs(self.all) do
title_opts.bold, title_opts.italic = true, false
local max_width = text_width(menu.title, title_opts) + 2 * self.padding + 2 * self.item_padding
local max_width = text_width(menu.title, title_opts) + 2 * self.item_padding
-- Estimate width of a widest item
for _, item in ipairs(menu.items) do
@@ -323,7 +323,7 @@ function Menu:update_content_dimensions()
if estimated_width > max_width then max_width = estimated_width end
end
menu.max_width = max_width + 2 * self.padding
menu.max_width = max_width
end
self:update_dimensions()
@@ -336,20 +336,21 @@ function Menu:update_dimensions()
-- and dumb titles with no search inputs. It could use a refactor.
local margin = round(self.item_height / 2)
local external_buttons_reserve = display.width / self.item_height > 14 and self.scroll_step * 6 - margin * 2 or 0
local width_available = display.width - margin * 2 - external_buttons_reserve
local height_available = display.height - margin * 2
local width_available = display.width - margin * 2 - self.padding * 2 - external_buttons_reserve
local height_available = display.height - margin * 2 - self.padding * 2
local min_width = math.min(self.min_width, width_available)
for _, menu in ipairs(self.all) do
local width = math.max(menu.search and menu.search.max_width or 0, menu.max_width)
menu.width = round(clamp(min_width, width, width_available))
local title_height = (menu.is_root and menu.title or menu.search) and self.scroll_step + self.padding or 0
local title_height = (menu.is_root and menu.title or menu.search) and
self.scroll_step + self.separator_size + 1 or 0
local footnote_height = self.font_size * 1.5
local max_height = height_available - title_height - footnote_height
local content_height = self.scroll_step * #menu.items
menu.height = math.min(content_height - self.item_spacing, max_height)
menu.top = clamp(
title_height + margin,
title_height + margin + self.padding,
menu.search and math.min(menu.search.min_top, menu.search.source.top) or height_available,
round((height_available - menu.height + title_height) / 2)
)
@@ -364,10 +365,13 @@ function Menu:update_dimensions()
self:update_coordinates()
end
-- Updates element coordinates to match currently open (sub)menu.
-- Updates element coordinates to match padding box of currently open (sub)menu.
function Menu:update_coordinates()
local ax = round((display.width - self.current.width) / 2) + self.offset_x
self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height)
local ax = round((display.width - self.current.width) / 2 - self.padding) + self.offset_x
self:set_coordinates(
ax, self.current.top - self.padding,
ax + self.current.width + self.padding * 2, self.current.top + self.current.height + self.padding
)
end
function Menu:reset_navigation()
@@ -686,7 +690,7 @@ function Menu:on_prop_fullormaxed() self:update_content_dimensions() end
function Menu:on_options() self:update_content_dimensions() end
function Menu:handle_cursor_down()
if self.proximity_raw == 0 then
if self.proximity_raw <= 0 then
self.drag_last_y = cursor.y
self.current.fling = nil
else
@@ -696,7 +700,7 @@ end
---@param shortcut? Shortcut
function Menu:handle_cursor_up(shortcut)
if self.proximity_raw == 0 and self.drag_last_y and not self.is_dragging then
if self.proximity_raw <= -self.padding and self.drag_last_y and not self.is_dragging then
self:activate_selected_item(shortcut, true)
end
if self.is_dragging then
@@ -893,14 +897,14 @@ function search_items(items, query, recursive, prefix)
if ligature_conv_title:find(query, 1, true) then
match = true
score = 1000
local pos = get_roman_match_positions(title, query, "ligature", ligature_roman)
local pos = get_roman_match_positions(title, query, 'ligature', ligature_roman)
if pos then
ass_safe_title = highlight_match(item.title, pos, font_color, bold)
end
elseif initials_conv_title:find(query, 1, true) then
match = true
score = 900
local pos = get_roman_match_positions(title, query, "initial", initials_roman)
local pos = get_roman_match_positions(title, query, 'initial', initials_roman)
if pos then
ass_safe_title = highlight_match(item.title, pos, font_color, bold)
end
@@ -1371,7 +1375,6 @@ function Menu:render()
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
local ass = assdraw.ass_new()
local spacing = self.item_padding
local icon_size = self.font_size
---@param menu MenuStack
@@ -1380,37 +1383,43 @@ function Menu:render()
local function draw_menu(menu, x, pos)
local is_current, is_parent, is_submenu = pos == 0, pos < 0, pos > 0
local menu_opacity = (pos == 0 and 1 or config.opacity.submenu ^ math.abs(pos)) * self.opacity
local ax, ay, bx, by = x, menu.top, x + menu.width, menu.top + menu.height
-- Scrollable content area coordinates
local content_rect = {
ax = x + self.padding,
ay = menu.top,
bx = x + self.padding + menu.width,
by = menu.top + menu.height,
}
-- local ax, ay, bx, by = x + self.padding, menu.top, x + menu.width + self.padding, menu.top + menu.height
local draw_title = menu.is_root and menu.title or menu.search
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
local scroll_clip = '\\clip(0,' .. content_rect.ay .. ',' .. display.width .. ',' .. content_rect.by .. ')'
local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
local menu_rect = {
ax = ax,
ay = ay - (draw_title and self.scroll_step + self.padding or 0) - self.padding,
bx = bx,
by = by + self.padding,
local bg_rect = {
ax = x,
ay = content_rect.ay - (draw_title and self.scroll_step or 0) - self.padding,
bx = content_rect.bx + self.padding,
by = content_rect.by + self.padding,
}
local blur_selected_index = self.mouse_nav and is_current
local blur_action_index = self.mouse_nav and menu.action_index ~= nil
-- Background
ass:rect(menu_rect.ax, menu_rect.ay, menu_rect.bx, menu_rect.by, {
ass:rect(bg_rect.ax, bg_rect.ay, bg_rect.bx, bg_rect.by, {
color = bg,
opacity = menu_opacity * config.opacity.menu,
radius = state.radius > 0 and state.radius + self.padding or 0,
radius = state.radius > 0 and math.min(state.radius + self.padding, state.radius * 3) or 0,
})
if is_parent then
cursor:zone('primary_down', menu_rect, self:create_action(function() self:slide_in_menu(menu.id, x) end))
cursor:zone('primary_down', bg_rect, self:create_action(function() self:slide_in_menu(menu.id, x) end))
end
-- Scrollbar
if menu.scroll_height > 0 then
local groove_height = menu.height - 2
local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40)
local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
local sax = bx - round(self.scrollbar_size / 2)
local thumb_y = content_rect.ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
local sax = content_rect.bx - round(self.scrollbar_size / 2)
local sbx = sax + self.scrollbar_size
ass:rect(sax, thumb_y, sbx, thumb_y + thumb_height, {color = fg, opacity = menu_opacity * 0.8})
end
@@ -1419,7 +1428,7 @@ function Menu:render()
local submenu_rect, current_item = nil, is_current and menu.selected_index and menu.items[menu.selected_index]
local submenu_is_hovered = false
if current_item and current_item.items then
submenu_rect = draw_menu(current_item --[[@as MenuStack]], menu_rect.bx + self.gap, 1)
submenu_rect = draw_menu(current_item --[[@as MenuStack]], bg_rect.bx + self.gap, 1)
cursor:zone('primary_down', submenu_rect, self:create_action(function(shortcut)
self:activate_selected_item(shortcut, true)
end))
@@ -1432,21 +1441,32 @@ function Menu:render()
if not item then break end
local item_ax = menu_rect.ax + self.padding
local item_bx = menu_rect.bx - self.padding
local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1)
local item_ay = content_rect.ay - menu.scroll_y + self.scroll_step * (index - 1)
local item_by = item_ay + self.item_height
local item_center_y = item_ay + (self.item_height / 2)
local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil
local content_ax, content_bx = ax + self.padding + spacing, bx - self.padding - spacing
local item_clip = (item_ay < content_rect.ay or item_by > content_rect.by) and scroll_clip or nil
local content_ax, content_bx = content_rect.ax + self.item_padding,
content_rect.bx - self.item_padding
local is_selected = menu.selected_index == index
local item_rect_hitbox = {
ax = item_ax,
ay = math.max(item_ay, menu_rect.ay),
bx = menu_rect.bx + (item.items and self.gap or -self.padding), -- to bridge the gap with cursor
by = math.min(item_ay + self.scroll_step, menu_rect.by),
ax = content_rect.ax,
ay = math.max(item_ay, bg_rect.ay),
bx = bg_rect.bx + (item.items and self.gap or -self.padding), -- to bridge the submenu gap with cursor
by = math.min(item_ay + self.scroll_step, bg_rect.by),
}
-- Select hovered item
if is_current and self.mouse_nav and item.selectable ~= false
-- Do not select items if cursor is moving towards a submenu
and (not submenu_rect or not cursor:direction_to_rectangle_distance(submenu_rect))
and (submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) <= 0) then
menu.selected_index = index
if not is_selected then
is_selected = true
request_render()
end
end
local has_background = is_selected or item.active
local next_item = menu.items[index + 1]
local next_is_active = next_item and next_item.active
@@ -1458,22 +1478,23 @@ function Menu:render()
if action then selected_action = action end
-- Separator
if item_by < by and ((not has_background and not next_has_background) or item.separator) then
local separator_ay, separator_by = item_by, item_by + self.separator_size
if item_by < content_rect.by and ((not has_background and not next_has_background) or item.separator) then
local ay, by = item_by, item_by + self.separator_size
if has_background then
separator_ay, separator_by = separator_ay + self.separator_size, separator_by + self.separator_size
ay, by = ay + self.separator_size, by + self.separator_size
elseif next_has_background then
separator_ay, separator_by = separator_ay - self.separator_size, separator_by - self.separator_size
ay, by = ay - self.separator_size, by - self.separator_size
end
ass:rect(ax + spacing, separator_ay, bx - spacing, separator_by, {
color = fg, opacity = menu_opacity * (item.separator and 0.13 or 0.04),
})
ass:rect(
content_rect.ax + self.item_padding, ay, content_rect.bx - self.item_padding, by,
{color = fg, opacity = menu_opacity * (item.separator and 0.13 or 0.04)}
)
end
-- Background
local highlight_opacity = 0 + (item.active and 0.8 or 0) + (is_selected and 0.15 or 0)
if highlight_opacity > 0 then
ass:rect(ax + self.padding, item_ay, bx - self.padding, item_by, {
ass:rect(content_rect.ax, item_ay, content_rect.bx, item_by, {
radius = state.radius,
color = fg,
opacity = highlight_opacity * menu_opacity,
@@ -1495,9 +1516,10 @@ function Menu:render()
actions_rect = {
ay = item_ay + margin,
by = item_by - margin,
is_outside = place == 'outside' and display.width - menu_rect.bx + margin * 2 > rect_width,
is_outside = place == 'outside' and display.width - bg_rect.bx + margin * 2 > rect_width,
}
actions_rect.bx = actions_rect.is_outside and menu_rect.bx + margin + rect_width or item_bx - margin
actions_rect.bx = actions_rect.is_outside and bg_rect.bx + margin + rect_width or
content_rect.bx - margin
actions_rect.ax = actions_rect.bx
for i = 1, #actions, 1 do
@@ -1532,7 +1554,7 @@ function Menu:render()
rect.ay, rect.by, rect.bx = item_ay, item_ay + self.scroll_step, rect.bx + margin
-- Select action on cursor hover
if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) == 0 then
if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) <= 0 then
cursor:zone('primary_down', rect, self:create_action(function(shortcut)
self:activate_selected_item(shortcut, true)
end))
@@ -1553,17 +1575,18 @@ function Menu:render()
if is_selected and not selected_action then
local size = round(2 * state.scale)
local v_padding = math.min(state.radius, math.ceil(self.item_height / 3))
ass:rect(ax + self.padding - size - 1, item_ay + v_padding, ax + self.padding - 1,
item_by - v_padding, {
radius = 1 * state.scale, color = fg, opacity = menu_opacity, clip = item_clip,
})
ass:rect(
content_rect.ax - size - 1, item_ay + v_padding,
content_rect.ax - 1, item_by - v_padding,
{radius = 1 * state.scale, color = fg, opacity = menu_opacity, clip = item_clip}
)
end
-- Icon
if item.icon then
if not actions_rect or actions_rect.is_outside then
local x = (not item.title and not item.hint and item.align == 'center')
and menu_rect.ax + (menu_rect.bx - menu_rect.ax) / 2
and bg_rect.ax + (bg_rect.bx - bg_rect.ax) / 2
or content_bx - (icon_size / 2)
if item.icon == 'spinner' then
ass:spinner(x, item_center_y, icon_size * 1.5, {color = font_color, opacity = menu_opacity * 0.8})
@@ -1573,7 +1596,7 @@ function Menu:render()
})
end
end
content_bx = content_bx - icon_size - spacing
content_bx = content_bx - icon_size - self.item_padding
title_clip_bx = math.min(content_bx, title_clip_bx)
end
@@ -1581,7 +1604,7 @@ function Menu:render()
if item.hint_width > 0 then
-- controls title & hint clipping proportional to the ratio of their widths
-- both title and hint get at least 50% of the width, unless they are smaller then that
local width = content_bx - content_ax - spacing
local width = content_bx - content_ax - self.item_padding
local title_min = math.min(item.title_width, width * 0.5)
local hint_min = math.min(item.hint_width, width * 0.5)
local title_ratio = item.title_width / (item.title_width + item.hint_width)
@@ -1594,8 +1617,9 @@ function Menu:render()
-- Hint
if item.hint then
item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint)
local clip = '\\clip(' .. title_clip_bx + spacing .. ',' ..
math.max(item_ay, ay) .. ',' .. hint_clip_bx .. ',' .. math.min(item_by, by) .. ')'
local clip = '\\clip(' .. title_clip_bx + self.item_padding .. ','
.. math.max(item_ay, content_rect.ay) .. ',' .. hint_clip_bx .. ','
.. math.min(item_by, content_rect.by) .. ')'
ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, {
size = self.font_size_hint,
color = font_color,
@@ -1608,8 +1632,8 @@ function Menu:render()
-- Title
if item.title then
item.ass_safe_title = item.ass_safe_title or ass_escape(item.title)
local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ','
.. title_clip_bx .. ',' .. math.min(item_by, by) .. ')'
local clip = '\\clip(' .. content_rect.ax .. ',' .. math.max(item_ay, content_rect.ay) .. ','
.. title_clip_bx .. ',' .. math.min(item_by, content_rect.by) .. ')'
local title_x, align = content_ax, 4
if item.align == 'right' then
title_x, align = title_clip_bx, 6
@@ -1626,29 +1650,12 @@ function Menu:render()
clip = clip,
})
end
-- Select hovered item
if is_current and self.mouse_nav and item.selectable ~= false then
if submenu_rect and cursor:direction_to_rectangle_distance(submenu_rect)
or actions_rect and actions_rect.is_outside and cursor:direction_to_rectangle_distance(actions_rect) then
blur_selected_index = false
else
if submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) == 0 then
blur_selected_index = false
menu.selected_index = index
if not is_selected then
is_selected = true
request_render()
end
end
end
end
end
-- Footnote / Selected action label
if is_current and (menu.footnote or selected_action) then
local height_half = self.font_size
local icon_x, icon_y = menu_rect.ax + self.padding + self.font_size / 2, menu_rect.by + height_half
local icon_x, icon_y = content_rect.ax + self.font_size / 2, bg_rect.by + height_half
local is_icon_hovered = false
local icon_hitbox = {
ax = icon_x - height_half,
@@ -1656,14 +1663,14 @@ function Menu:render()
bx = icon_x + height_half,
by = icon_y + height_half,
}
is_icon_hovered = get_point_to_rectangle_proximity(cursor, icon_hitbox) == 0
is_icon_hovered = get_point_to_rectangle_proximity(cursor, icon_hitbox) <= 0
local text = selected_action and selected_action.label or is_icon_hovered and menu.footnote
local opacity = (is_icon_hovered and 1 or 0.5) * menu_opacity
ass:icon(icon_x, icon_y, self.font_size, is_icon_hovered and 'help' or 'help_outline', {
color = fg, border = state.scale, border_color = bg, opacity = opacity,
})
if text then
ass:txt(icon_x + self.font_size * 0.75, icon_y, 4, text, {
ass:txt(icon_x + self.font_size * 0.75, icon_y - self.font_size * 0.5, 7, ass_escape(text), {
size = self.font_size,
color = fg,
border = state.scale,
@@ -1676,43 +1683,24 @@ function Menu:render()
-- Menu title
if draw_title then
local title_height = self.item_height + self.padding - 3
local requires_submit = menu.search_debounce == 'submit'
local rect = {
ax = round(ax + spacing / 2 + self.padding),
ay = ay - self.scroll_step - self.padding * 2,
bx = round(bx - spacing / 2 - self.padding),
by = math.min(by, ay - self.padding),
ax = content_rect.ax,
ay = content_rect.ay - self.scroll_step - self.separator_size - 1,
bx = content_rect.bx,
by = content_rect.ay - self.separator_size - 1,
}
-- centers
-- Centers
rect.cx, rect.cy = round(rect.ax + (rect.bx - rect.ax) / 2), round(rect.ay + (rect.by - rect.ay) / 2)
if menu.title and not menu.ass_safe_title then
menu.ass_safe_title = ass_escape(menu.title)
end
-- Background
if menu.search then
ass:rect(ax + 3, rect.ay + 3, bx - 3, rect.ay + title_height - 1, {
color = fg .. '\\1a&HFF', opacity = menu_opacity * 0.1,
radius = state.radius > 0 and state.radius + self.padding or 0,
border = 1, border_color = fg, border_opacity = menu_opacity * 0.8
})
ass:texture(ax + 3, rect.ay + 3, bx - 3, rect.ay + title_height - 1, 'n', {
size = 80, color = bg, opacity = menu_opacity * 0.1, anchor_x = ax + 2, anchor_y = rect.ay + 2,
})
else
ass:rect(ax + 2, rect.ay + 2, bx - 2, rect.ay + title_height, {
color = fg, opacity = menu_opacity * 0.8,
radius = state.radius > 0 and state.radius + self.padding or 0,
})
ass:texture(ax + 2, rect.ay + 2, bx - 2, rect.ay + title_height, 'n', {
size = 80, color = bg, opacity = menu_opacity * 0.1,
})
end
-- Bottom border
ass:rect(ax, rect.by - self.separator_size, bx, rect.by, {color = fg, opacity = menu_opacity * 0.2})
-- Separator
ass:rect(
rect.ax, rect.by, rect.bx, rect.by + self.separator_size, {color = fg, opacity = menu_opacity * 0.2}
)
-- Blur selection (also activates search input) when user clicks title
if is_current then
@@ -1725,11 +1713,16 @@ function Menu:render()
if menu.search then
-- Icon
local icon_size, icon_opacity = self.font_size * 1.3, menu_opacity * (requires_submit and 0.5 or 1)
local icon_rect = {ax = rect.ax, ay = rect.ay, bx = ax + icon_size + spacing * 1.5, by = rect.by}
local icon_rect = {
ax = rect.ax,
ay = rect.ay,
bx = content_rect.ax + icon_size + self.item_padding * 1.5,
by = rect.by,
}
if is_current and requires_submit then
cursor:zone('primary_down', icon_rect, function() self:search_submit() end)
if get_point_to_rectangle_proximity(cursor, icon_rect) == 0 then
if get_point_to_rectangle_proximity(cursor, icon_rect) <= 0 then
icon_opacity = menu_opacity
end
end
@@ -1778,7 +1771,10 @@ function Menu:render()
-- (input is selected when `selected_index` is `nil`)
if menu.search_debounce == 'submit' and not menu.selected_index then
local size_half = round(1 * state.scale)
ass:rect(ax, rect.by - size_half, bx, rect.by + size_half, {color = fg, opacity = menu_opacity})
ass:rect(
content_rect.ax, rect.by - size_half, content_rect.bx, rect.by + size_half,
{color = fg, opacity = menu_opacity}
)
end
local input_is_blurred = menu.search_debounce == 'submit' and menu.selected_index
@@ -1793,7 +1789,7 @@ function Menu:render()
ass:txt(rect.cx, rect.cy, 5, menu.ass_safe_title, {
size = self.font_size,
bold = true,
color = bg,
color = bgt,
wrap = 2,
opacity = menu_opacity,
clip = '\\clip(' .. rect.ax .. ',' .. rect.ay .. ',' .. rect.bx .. ',' .. rect.by .. ')',
@@ -1801,16 +1797,12 @@ function Menu:render()
end
end
-- We are in mouse nav and cursor isn't hovering any item
if blur_selected_index then
menu.selected_index = nil
end
if blur_action_index then
menu.action_index = nil
request_render()
end
return menu_rect
return bg_rect
end
-- Active menu
@@ -1821,7 +1813,7 @@ function Menu:render()
local parent_offset_x, parent_horizontal_index = self.ax, -1
while parent_menu do
parent_offset_x = parent_offset_x - parent_menu.width - self.gap
parent_offset_x = parent_offset_x - parent_menu.width - self.padding * 2 - self.gap
draw_menu(parent_menu, parent_offset_x, parent_horizontal_index)
parent_horizontal_index = parent_horizontal_index - 1
parent_menu = parent_menu.parent_menu
@@ -1830,4 +1822,4 @@ function Menu:render()
return ass
end
return Menu
return Menu
+2 -2
View File
@@ -14,7 +14,7 @@ function PauseIndicator:init()
end
function PauseIndicator:init_options()
self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
self.base_icon_opacity = config.opacity.pause_indicator or (options.pause_indicator == 'flash' and 1) or 0.8
self.type = options.pause_indicator
self:on_prop_pause()
end
@@ -80,4 +80,4 @@ function PauseIndicator:render()
return ass
end
return PauseIndicator
return PauseIndicator
+1 -1
View File
@@ -192,4 +192,4 @@ function Speed:render()
return ass
end
return Speed
return Speed
+40 -7
View File
@@ -18,12 +18,20 @@ function Timeline:init()
self.progress_line_width = 0
self.is_hovered = false
self.has_thumbnail = false
self.heatmap = nil
self:decide_progress_size()
self:update_dimensions()
-- Release any dragging when file gets unloaded
self:register_mp_event('end-file', function() self.pressed = false end)
-- Load Youtube heatmap data if available
self:register_mp_event('file-loaded', function()
self.heatmap = load_youtube_heatmap()
end)
-- Release any dragging and clear heatmap when file gets unloaded
self:register_mp_event('end-file', function()
self.pressed = false
self.heatmap = nil
end)
end
function Timeline:get_visibility()
@@ -181,7 +189,7 @@ function Timeline:render()
return
end
if self.proximity_raw == 0 then
if self.proximity_raw <= 0 then
self.is_hovered = true
end
if visibility > 0 then
@@ -257,7 +265,32 @@ function Timeline:render()
ass:draw_stop()
-- Progress
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
local function draw_progress()
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
end
-- Youtube heatmap
local function draw_heatmap()
if options.timeline_heatmap ~= 'no' and self.heatmap and config.opacity.heatmap > 0 and visibility > 0 then
local is_above = options.timeline_heatmap == 'above'
local height = math.min(40, size / self.size * 40)
local ax, ay = bax, is_above and (bay - height) or (bay + self.top_border)
local bx, by = bbx, is_above and bay or bby
local opts = {color = config.color.heatmap, opacity = config.opacity.heatmap * visibility}
local clip_ay = is_above and (ay - 10) or ay
opts.clip = string.format('\\clip(%d,%d,%d,%d)', ax, clip_ay, bx, by)
ass:smooth_curve(ax, ay, bx, by, self.heatmap, opts)
end
end
-- Change draw order based on 'timeline_style' to keep the heatmap visible
if is_line then
draw_heatmap()
draw_progress()
else
draw_progress()
draw_heatmap()
end
-- Uncached ranges
if state.uncached_ranges then
@@ -380,7 +413,7 @@ function Timeline:render()
-- Time values
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 = options.text_border * state.scale}
-- Upcoming cache time
local cache_duration = state.cache_duration and state.cache_duration / state.speed or nil
if cache_duration and options.buffered_time_threshold > 0
@@ -412,7 +445,7 @@ function Timeline:render()
-- Hovered time and chapter
local rendered_thumbnail = false
if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and not Elements:v('speed', 'dragging') then
if (self.proximity_raw <= 0 or self.pressed or hovered_chapter) and not Elements:v('speed', 'dragging') then
local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x
local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x)
@@ -486,4 +519,4 @@ function Timeline:render()
return ass
end
return Timeline
return Timeline
+11 -10
View File
@@ -21,11 +21,17 @@ function TopBar:init()
self.current_chapter = nil
local function maximized_command()
mp.command(state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes')
if state.platform == 'windows' then
mp.command(state.border
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
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
local close = {icon = 'close', hover_bg = '2311e8', hover_fg = 'ffffff', command = function() mp.command('quit') end}
local max = {icon = 'crop_square', command = maximized_command, is_max = true}
local max = {icon = 'crop_square', command = maximized_command}
local min = {icon = 'minimize', command = function() mp.command('cycle window-minimized') end}
self.buttons = options.top_bar_controls == 'left' and {close, max, min} or {min, max, close}
@@ -237,13 +243,8 @@ function TopBar:render()
end
for _, button in ipairs(self.buttons) do
if button.is_max then
button.icon = state.fullscreen and 'close_fullscreen' or
(state.maximized and 'filter_none' or 'crop_square')
end
local rect = {ax = button_ax, ay = ay, bx = button_ax + self.size, by = by}
local is_hover = get_point_to_rectangle_proximity(cursor, rect) == 0
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
@@ -290,7 +291,7 @@ function TopBar:render()
bx = ax + rect_width,
by = 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
if opacity > 0 then
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
@@ -427,4 +428,4 @@ function TopBar:render()
return ass
end
return TopBar
return TopBar
+319
View File
@@ -0,0 +1,319 @@
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_down', rect, self:create_action(button.method))
-- Select hovered button
if is_hovered then self.selected_button_index = index end
end
return ass
end
return Updater
+2 -1
View File
@@ -242,6 +242,7 @@ end
function Volume:on_display() self:update_dimensions() end
function Volume:on_prop_border() self:update_dimensions() end
function Volume:on_prop_title_bar() self:update_dimensions() end
function Volume:on_prop_volume_max() self:update_dimensions() end
function Volume:on_controls_reflow() self:update_dimensions() end
function Volume:on_options() self:update_dimensions() end
@@ -280,4 +281,4 @@ function Volume:render()
return ass
end
return Volume
return Volume
+2 -2
View File
@@ -26,10 +26,10 @@ function WindowBorder:render()
local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' ..
(display.width - self.size) .. ',' .. (display.height - self.size) .. ')'
ass:rect(0, 0, display.width + 1, display.height + 1, {
color = bg, clip = clip, opacity = config.opacity.border,
color = config.color.window_border, clip = clip, opacity = config.opacity.border,
})
return ass
end
end
return WindowBorder
return WindowBorder
+1 -1
View File
@@ -80,4 +80,4 @@
"type to search": "Tippe um zu suchen",
"unknown error": "Unbekannter Fehler",
"uosc has been installed. Restart mpv for it to take effect.": "uosc wurde installiert. mpv muss neu gestarted werden um es wirksam zu machen."
}
}
+1 -1
View File
@@ -96,4 +96,4 @@
"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."
}
}
+1 -1
View File
@@ -56,4 +56,4 @@
"open file": "sélectionner un fichier",
"parent dir": "répertoire parent",
"playlist or file": "fichier ou liste de lecture"
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"Aspect ratio": "Proporizioni",
"Audio": "Audio",
"Audio device": "Dispositivo audio",
"Audio devices": "Dispositivi audio",
"Audio tracks": "Tracce audio",
"Autoselect device": "Selezione automatica dispositivo",
"Chapter %s": "Capitolo %s",
"Chapters": "Capitoli",
"Default": "Predefinito",
"Default %s": "Predefinito %s",
"Delete file & Next": "Elimina file e Successivo",
"Delete file & Prev": "Elimina file e Precedente",
"Delete file & Quit": "Elimina file e Esci",
"Disabled": "Disabilitato",
"Drives": "Unità",
"Edition": "Edizione",
"Edition %s": "Edizione %s",
"Editions": "Edizioni",
"Empty": "Vuoto",
"First": "Primo",
"Fullscreen": "Schermo intero",
"Last": "Ultimo",
"Load": "Carica",
"Load audio": "Carica traccia audio",
"Load subtitles": "Carica sottotitoli",
"Load video": "Carica traccia video",
"Loop file": "Ripeti file",
"Loop playlist": "Ripeti playlist",
"Menu": "Menu",
"Navigation": "Navigazione",
"Next": "Successivo",
"No file": "Nessun file",
"Open config folder": "Apri cartella configurazione",
"Open file": "Apri file",
"Playlist": "Playlist",
"Playlist/Files": "Playlist/File",
"Prev": "Precedente",
"Previous": "Precedente",
"Quit": "Esci",
"Screenshot": "Schermata",
"Show in directory": "Mostra nella cartella",
"Shuffle": "Riproduzione casuale",
"Stream quality": "Qualità streaming",
"Subtitles": "Sottotitoli",
"Track": "Traccia",
"Track %s": "Traccia %s",
"Utils": "Utilità",
"Video": "Video",
"%s channel": "%s canale",
"%s channels": "%s canali",
"default": "predefinito",
"drive": "unità",
"external": "esterno",
"forced": "forzato",
"open file": "seleziona file",
"parent dir": "cartella superiore",
"playlist or file": "file o playlist"
}
+1 -1
View File
@@ -104,4 +104,4 @@
"type & ctrl+enter to search": "wpisz i ctrl+enter aby wyszukać",
"type to search": "wpisz aby wyszukać",
"uosc has been installed. Restart mpv for it to take effect.": "uosc został zainstalowany. Uruchom ponownie mpv, aby zmiany zostały zastosowane."
}
}
+99
View File
@@ -0,0 +1,99 @@
{
"%s are empty": "%s estão vazios",
"%s channel": "%s canal",
"%s channels": "%s canais",
"%s to delete": "%s para excluir",
"%s to go up in tree.": "%s para subir na árvore",
"%s to reorder.": "%s para reordenar",
"%s to search": "%s para buscar",
"Add to playlist": "Adicionar à lista",
"Added to playlist": "Adicionado à lista",
"An error has occurred.": "Ocorreu um erro.",
"Aspect ratio": "Proporção da tela",
"Audio": "Áudio",
"Audio device": "Dispositivo de áudio",
"Audio devices": "Dispositivos de áudio",
"Audio tracks": "Faixas de áudio",
"Chapter %s": "Capítulo %s",
"Chapters": "Capítulos",
"Copied to clipboard": "Copiado para a área de transferência",
"Default": "Padrão",
"Default %s": "Padrão %s",
"Delete": "Excluir",
"Delete file & Next": "Excluir arquivo e Próximo",
"Delete file & Prev": "Excluir arquivo e Anterior",
"Delete file & Quit": "Excluir arquivo e Sair",
"Drives": "Unidades",
"Drop files or URLs to play here": "Arraste arquivos ou URLs para reproduzir aqui",
"Edition %s": "Edição %s",
"Editions": "Edições",
"Empty": "Vazio",
"First": "Primeiro",
"Fullscreen": "Tela cheia",
"Key bindings": "Atalhos de teclado",
"Last": "Último",
"Load": "Abrir",
"Load audio": "Carregar faixa de áudio",
"Load subtitles": "Carregar faixa de legenda",
"Load video": "Carregar faixa de vídeo",
"Loaded audio": "Áudio carregado",
"Loaded subtitles": "Legendas carregadas",
"Loaded video": "Vídeo carregado",
"Loop file": "Repetir arquivo",
"Loop playlist": "Repetir lista",
"Menu": "Menu",
"Move down": "Mover para baixo",
"Move up": "Mover para cima",
"Navigation": "Navegação",
"Next": "Próximo",
"Next page": "Próxima página",
"No file": "Nenhum arquivo",
"Open config folder": "Abrir pasta de configuração",
"Open file": "Abrir arquivo",
"Open in browser": "Abrir no navegador",
"Open in mpv": "Abrir no mpv",
"Paste path or url to add.": "Cole o caminho ou URL para adicionar.",
"Paste path or url to open.": "Cole o caminho ou URL para abrir.",
"Play/Pause": "Reproduzir/Pausar",
"Playlist": "Lista de reprodução",
"Playlist/Files": "Lista/Arquivos",
"Prev": "Anterior",
"Previous": "Anterior",
"Previous page": "Página anterior",
"Quit": "Sair",
"Reload": "Recarregar",
"Remaining downloads today: %s": "Restante de downloads hoje: %s",
"Remove": "Remover",
"Resets in: %s": "Reinicia em: %s",
"Screenshot": "Captura de tela",
"Search online": "Pesquisar online",
"See above for clues.": "Veja acima por dicas.",
"See console for details.": "Veja o console para detalhes.",
"Show in directory": "Mostrar na pasta",
"Shuffle": "Aleatório",
"Something went wrong.": "Algo deu errado.",
"Stream quality": "Qualidade da transmissão",
"Subtitles": "Legendas",
"Subtitles loaded & enabled": "Legendas carregadas e ativadas",
"Toggle to disable.": "Alternar para desativar",
"Track %s": "Faixa %s",
"Update uosc": "Atualizar uosc",
"Updating uosc": "Atualizando uosc",
"Use as secondary": "Usar como secundário",
"Utils": "Ferramentas",
"Video": "Vídeo",
"default": "padrão",
"drive": "unidade",
"enter query": "digite a consulta",
"external": "externo",
"forced": "forçada",
"foreign parts only": "somente partes estrangeiras",
"hearing impaired": "deficiência auditiva",
"no results": "sem resultados",
"open file": "abrir arquivo",
"parent dir": "diretório superior",
"playlist or file": "lista ou arquivo",
"type & ctrl+enter to search": "digite e pressione Ctrl+Enter para buscar",
"type to search": "digite para buscar",
"uosc has been installed. Restart mpv for it to take effect.": "uosc foi instalado. Reinicie o mpv para que tenha efeito."
}
+1 -1
View File
@@ -56,4 +56,4 @@
"open file": "deschide fișierul",
"parent dir": "director părinte",
"playlist or file": "fișier sau listă de redare"
}
}
+1 -1
View File
@@ -56,4 +56,4 @@
"open file": "открыть файл",
"parent dir": "родительская папка",
"playlist or file": "плейлист или файл"
}
}
+1 -1
View File
@@ -104,4 +104,4 @@
"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."
}
}
+1 -1
View File
@@ -66,4 +66,4 @@
"An error has occurred.": "Сталася помилка.",
"See above for clues.": "Дивіться підказки вище.",
"Play/Pause": "Відтворення / Пауза"
}
}
+107
View File
@@ -0,0 +1,107 @@
{
"%s are empty": "%s 是空字串",
"%s channel": "%s 聲道",
"%s channels": "%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.": "出現錯誤",
"Aspect ratio": "長寬比",
"Audio": "音訊",
"Audio device": "音訊裝置",
"Audio devices": "音訊裝置",
"Audio tracks": "音軌",
"Autoload": "自動載入",
"Chapter %s": "第 %s 章",
"Chapters": "章節",
"Check for updates": "檢查更新",
"Checking for updates": "正在檢查更新",
"Close": "關閉",
"Copied to clipboard": "已複製到剪貼簿",
"Default": "預設",
"Default %s": "預設 %s",
"Delete": "删除",
"Delete file & Next": "删除檔案並播放下一個",
"Delete file & Prev": "删除檔案並播放上一個",
"Delete file & Quit": "删除檔案並退出",
"Drives": "硬碟",
"Drop files or URLs to play here": "拖放檔案或 URLs 到此播放",
"Edition %s": "版本 %s",
"Editions": "版本",
"Empty": "空",
"First": "第一個",
"Fullscreen": "全螢幕",
"Key bindings": "快捷鍵",
"Last": "最後一個",
"Load": "載入",
"Load audio": "載入音訊",
"Load subtitles": "載入字幕",
"Load video": "載入視訊",
"Loaded audio": "已載入音訊",
"Loaded subtitles": "已載入字幕",
"Loaded video": "已載入視訊",
"Loop file": "重複播放",
"Loop playlist": "重複播放清單",
"Menu": "選單",
"Move down": "下移",
"Move up": "上移",
"Navigation": "導覽",
"Next": "下一個",
"Next page": "下一頁",
"No file": "無檔案",
"Nothing to copy": "沒有東西可以複製",
"Open changelog": "開啟更新日誌",
"Open config folder": "開啟設定檔資料夾",
"Open file": "開啟檔案",
"Open in browser": "用瀏覽器開啟",
"Open in mpv": "用 mpv 開啟",
"Paste path or url to add.": "貼上路徑或 url 以新增",
"Paste path or url to open.": "貼上路徑或 url 以開啟",
"Play/Pause": "播放/暫停",
"Playlist": "播放清單",
"Playlist/Files": "播放清單/檔案列表",
"Prev": "上一個",
"Previous": "上一個",
"Previous page": "上一頁",
"Quit": "結束",
"Reload": "重新載入",
"Remaining downloads today: %s": "今天的剩餘下載量: %s",
"Remove": "移除",
"Resets in: %s": "重置: %s",
"Screenshot": "截圖",
"Search online": "線上搜尋",
"See above for clues.": "請參閱上文提示",
"See console for details.": "詳情請參閱終端",
"Show in directory": "開啟所在資料夾",
"Shuffle": "隨機播放",
"Something went wrong.": "出錯了",
"Stream quality": "串流質素",
"Subtitles": "字幕",
"Subtitles loaded & enabled": "已載入及啟用字幕",
"Toggle to disable.": "切換以停用",
"Track %s": "音軌 %s",
"Up to date": "最新版本",
"Update available": "有可用更新",
"Update uosc": "更新 uosc",
"Updating uosc": "正在更新 uosc",
"Use as secondary": "設為副字幕",
"Utils": "工具",
"Video": "影片",
"default": "預設",
"drive": "硬碟",
"enter query": "輸入查詢",
"external": "外置",
"forced": "強制",
"foreign parts only": "只限外語部分",
"hearing impaired": "聽障",
"no results": "沒有結果",
"open file": "開啟檔案",
"parent dir": "父資料夾",
"playlist or file": "播放清單或檔案",
"type & ctrl+enter to search": "輸入並按 ctrl+enter 搜尋",
"type to search": "輸入文字以搜尋內容",
"uosc has been installed. Restart mpv for it to take effect.": "已安装 uosc ,重新開啟 mpv 使其生效"
}
+1 -1
View File
@@ -96,4 +96,4 @@
"type & ctrl+enter to search": "输入并按 ctrl+enter 进行搜索",
"type to search": "输入搜索内容",
"uosc has been installed. Restart mpv for it to take effect.": "uosc 已经安装,重新启动 mpv 使其生效"
}
}
+44
View File
@@ -266,3 +266,47 @@ function ass_mt:spinner(x, y, size, opts)
self:icon(x, y, size, 'autorenew', opts)
request_render()
end
-- Renders a smooth curve from Bezier segments.
---@param ax number
---@param ay number
---@param bx number
---@param by number
---@param points number[] Flat table of normalized points (01): start point followed by segment entries cp1x, cp1y, cp2x, cp2y, px, py, ...
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number|{primary?: number; border?: number, shadow?: number, main?: number}; clip?: string}
function ass_mt:smooth_curve(ax, ay, bx, by, points, opts)
if not points or #points < 8 then return end
opts = opts or {}
local border_size = opts.border or 0
local tags = '\\pos(0,0)\\rDefault\\an7\\blur0'
-- border
tags = tags .. '\\bord' .. border_size
-- colors
tags = tags .. '\\1c&H' .. (opts.color or fg)
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
-- opacity
if opts.opacity then tags = tags .. self.opacity(nil, opts.opacity) end
-- clip
if opts.clip then tags = tags .. opts.clip end
-- draw
self:new_event()
self.text = self.text .. '{' .. tags .. '}'
self:draw_start()
-- Scale normalized (01) coordinates to rectangle bounds
local width, height = bx - ax, by - ay
local function scale(x, y)
return ax + x * width, ay + y * height
end
local x0, y0 = scale(points[1], points[2])
self:move_to(x0, y0)
local max = math.floor((#points - 2) / 6) * 6 + 2
for i = 3, max, 6 do
local x1, y1 = scale(points[i], points[i+1])
local x2, y2 = scale(points[i+2], points[i+3])
local x3, y3 = scale(points[i+4], points[i+5])
self:bezier_curve(x1, y1, x2, y2, x3, y3)
end
self:draw_stop()
end
+1 -1
View File
@@ -71,4 +71,4 @@ mp.register_script_message('set-button', function(name, data)
end
end)
return buttons
return buttons
+1 -1
View File
@@ -63,4 +63,4 @@ function char_conv(chars, use_ligature, has_separator)
end
end
return char_conv
return char_conv
+1 -1
View File
@@ -466,4 +466,4 @@ mp.set_key_bindings({
{'wheel_down', cursor:create_handler('wheel_down', create_shortcut('wheel_down'))},
}, 'wheel', 'force')
return cursor
return cursor
+1 -1
View File
@@ -294,4 +294,4 @@ function fzy.get_implementation_name()
return "lua"
end
return fzy
return fzy
+1 -1
View File
@@ -65,4 +65,4 @@ for i = #languages, 1, -1 do
end
end
return {t = t}
return {t = t}
+33 -81
View File
@@ -208,31 +208,6 @@ function create_select_tracklist_type_menu_opener(opts)
return tonumber(mp.get_property(opts.prop)), snd and tonumber(mp.get_property(snd.prop)) or nil
end
local function escape_codec(str)
if not str or str == '' then return '' end
local codec_map = {
mpeg2 = "mpeg2",
dvvideo = "dv",
pcm = "pcm",
pgs = "pgs",
subrip = "srt",
vtt = "vtt",
dvd_sub = "vob",
dvb_sub = "dvb",
dvb_tele = "teletext",
arib = "arib"
}
for key, value in pairs(codec_map) do
if str:find(key) then
return value
end
end
return str
end
local function serialize_tracklist(tracklist)
local items = {}
@@ -285,15 +260,14 @@ function create_select_tracklist_type_menu_opener(opts)
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('%.5g fps', track['demux-fps'])) end
if track['codec'] then h(escape_codec(track.codec)) 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('%.3g kHz', track['demux-samplerate'] / 1000)) end
if track['demux-bitrate'] then h(string.format('%.0f kbps', track['demux-bitrate'] / 1000)) 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
@@ -920,7 +894,8 @@ function open_subtitle_downloader()
return
end
local search_suggestion, destination_directory = '', nil
local search_suggestion, file_path, destination_directory = '', nil, nil
local credentials = {'--api-key', config.open_subtitles_api_key, '--agent', config.open_subtitles_agent}
if state.path then
if is_protocol(state.path) then
@@ -930,6 +905,7 @@ function open_subtitle_downloader()
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
@@ -942,7 +918,6 @@ function open_subtitle_downloader()
end
local handle_download, handle_search
local url = 'https://api.opensubtitles.com/api/v1'
-- Checks if there an error, or data is invalid. If true, reports the error,
-- updates menu to inform about it, and returns true.
@@ -991,49 +966,16 @@ function open_subtitle_downloader()
end
end)
local download_url = url .. '/download'
local args = itable_join({'download-subtitles'}, credentials, {
'--file-id', tostring(data.id),
'--destination', destination_directory,
})
local headers = {
['Accept'] = 'application/json',
['Api-Key'] = config.open_subtitles_api_key,
['Content-Type'] = 'application/json',
['User-Agent'] = config.open_subtitles_agent,
}
local body = {
file_id = data.id
}
http_request_async('POST', download_url, headers, body, function(error, data)
call_ziggy_async(args, function(error, data)
if not menu:is_alive() then return end
if data and data.link then
local file_path = utils.join_path(destination_directory, data.file_name)
local arg = {
'curl',
'-sL',
'--user-agent', config.open_subtitles_agent,
'-o', file_path,
data.link
}
if should_abort(error, data, function(data) return type(data.file) == 'string' end) then return end
mp.command_native({
name = 'subprocess',
capture_stdout = true,
capture_stderr = true,
playback_only = false,
args = arg
})
end
local function check_is_valid(data)
local path = data and utils.join_path(destination_directory, data.file_name) or nil
local meta = path and utils.file_info(path) or nil
return meta and meta.is_file
end
if should_abort(error, data, check_is_valid) then return end
load_track('sub', utils.join_path(destination_directory, data.file_name))
load_track('sub', data.file)
menu:update_items({
{
@@ -1043,7 +985,7 @@ function open_subtitle_downloader()
selectable = false,
},
{
title = t('Remaining downloads today: %s', data.remaining),
title = t('Remaining downloads today: %s', data.remaining .. '/' .. data.total),
italic = true,
muted = true,
icon = 'file_download',
@@ -1068,22 +1010,32 @@ function open_subtitle_downloader()
menu:update_items({{icon = 'spinner', align = 'center', selectable = false, muted = true}})
local args = itable_join({'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`
local search_url = string.format('%s/subtitles?query=%s&languages=%s&page=%s', url, url_encode(query),
table.concat(table_keys(create_set(languages)), ','), tostring(page))
args[#args + 1] = '--page'
args[#args + 1] = tostring(page)
local headers = {
['Api-Key'] = config.open_subtitles_api_key,
['User-Agent'] = config.open_subtitles_agent,
}
if file_path then
args[#args + 1] = '--hash'
args[#args + 1] = file_path
end
http_request_async('GET', search_url, headers, nil, function(error, data)
if query and #query > 0 then
args[#args + 1] = '--query'
args[#args + 1] = query
end
call_ziggy_async(args, function(error, data)
if not menu:is_alive() then return end
local function check_is_valid(data)
return data and type(data.data) == 'table' and data.page and data.total_pages
return type(data.data) == 'table' and data.page and data.total_pages
end
if should_abort(error, data, check_is_valid) then return end
local subs = itable_filter(data.data, function(sub)
@@ -1183,4 +1135,4 @@ function open_subtitle_downloader()
end
end
)
end
end
+1 -29
View File
@@ -24,34 +24,6 @@ end
---@return string
function trim(str) return str:match('^%s*(.-)%s*$') end
---@param str string
---@return string|nil
function url_encode(str)
if str then
str = str:gsub('([^%w%-%.%_%~])', function(c)
return string.format('%%%02X', string.byte(c))
end)
end
return str
end
-- Escape special characters in url.
---@param str string
---@return string|nil
function url_decode(str)
local function hex_to_char(x)
return string.char(tonumber(x, 16))
end
if str ~= nil then
str = str:gsub('^file://', '')
str = str:gsub('%%(%x%x)', hex_to_char)
if str:find('://localhost:?') then
str = str:gsub('^.*/', '')
end
end
return str
end
-- Trim any `char` from the end of the string.
---@param str string
---@param char string
@@ -406,4 +378,4 @@ end
function CircularBuffer:clear()
itable_clear(self.data)
self.pos = 0
end
end
+1 -1
View File
@@ -657,4 +657,4 @@ function get_roman_match_positions(title, query, mode, roman)
end
return byte_positions
end
end
+93 -94
View File
@@ -130,12 +130,14 @@ function tween(from, to, setter, duration_or_callback, callback)
return finish
end
-- Returns signed distance (negative values mean how deep inside the rect the point is).
---@param point Point
---@param rect Rect
function get_point_to_rectangle_proximity(point, rect)
local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx)
local dy = math.max(rect.ay - point.y, 0, point.y - rect.by)
return math.sqrt(dx * dx + dy * dy)
local dx = math.max(rect.ax - point.x, point.x - rect.bx)
local dy = math.max(rect.ay - point.y, point.y - rect.by)
local distance = math.sqrt(math.max(0, dx)^2 + math.max(0, dy)^2)
return distance + math.min(0, math.max(dx, dy))
end
---@param point_a Point
@@ -149,7 +151,7 @@ end
---@param hitbox Hitbox
function point_collides_with(point, hitbox)
return (hitbox.r and get_point_to_point_proximity(point, hitbox.point) <= hitbox.r) or
(not hitbox.r and get_point_to_rectangle_proximity(point, hitbox --[[@as Rect]]) == 0)
(not hitbox.r and get_point_to_rectangle_proximity(point, hitbox --[[@as Rect]]) <= 0)
end
---@param lax number
@@ -221,6 +223,37 @@ function get_ray_to_rectangle_distance(ax, ay, bx, by, rect)
return closest
end
-- Converts a flat table of points to a smooth curve using Catmull-Rom to Bezier conversion.
---@param points number[] Flat table: x1, y1, x2, y2, ...
---@return number[] Flat table: start point followed by segment entries cp1x, cp1y, cp2x, cp2y, px, py, ...
function points_to_bezier(points)
if not points or #points < 4 then return {} end
local function catmullrom_to_bezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y)
local cp1x = p1x + (p2x - p0x) / 6
local cp1y = p1y + (p2y - p0y) / 6
local cp2x = p2x - (p3x - p1x) / 6
local cp2y = p2y - (p3y - p1y) / 6
return cp1x, cp1y, cp2x, cp2y
end
-- Helper to get x, y from flat table
local function get_xy(i)
return points[i * 2 - 1], points[i * 2]
end
local curve = {points[1], points[2]}
local xy_pairs = #points / 2
for i = 1, xy_pairs - 1 do
local p0x, p0y = get_xy(math.max(i - 1, 1))
local p1x, p1y = get_xy(i)
local p2x, p2y = get_xy(i+1)
local p3x, p3y = get_xy(math.min(i + 2, xy_pairs))
local cp1x, cp1y, cp2x, cp2y = catmullrom_to_bezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y)
local n = #curve
curve[n+1], curve[n+2], curve[n+3], curve[n+4], curve[n+5], curve[n+6] =
cp1x, cp1y, cp2x, cp2y, p2x, p2y
end
return curve
end
-- Extracts the properties used by property expansion of that string.
---@param str string
---@param res { [string] : boolean } | nil
@@ -892,79 +925,17 @@ function call_ziggy_async(args, callback)
end
end
---@param url string
---@param method string
---@param callback fun(error: string|nil, data: table|nil)
---@return fun() abort Function to abort the request.
function http_request_async(method, url, headers, body, callback)
local args = { 'curl', '-s', '-L', '-X', method, url }
if headers then
for k, v in pairs(headers) do
table.insert(args, '-H')
table.insert(args, string.format('%s: %s', k, v))
end
end
if body then
table.insert(args, '-d')
table.insert(args, utils.format_json(body))
end
local abort_signal = mp.command_native_async({
name = 'subprocess',
capture_stdout = true,
capture_stderr = true,
playback_only = false,
args = args
}, function(success, res, error)
local error = error ~= '' and error or res and res.stderr ~= '' and res.stderr or nil
if not success or not res or res.status ~= 0 then
msg.error('HTTP request failed: ' .. (res.stderr or 'unknown error'))
callback(error, nil)
return
end
local data = utils.parse_json(res.stdout)
callback(error, data)
end)
return function()
mp.abort_async_command(abort_signal)
end
end
---@return string|nil
function get_clipboard()
if state.current_clipboard_backend then
if state.platform == 'windows' or state.platform == 'darwin' then
return mp.get_property('clipboard/text', '')
end
if state.platform == 'linux' then
-- Wayland
if os.getenv('WAYLAND_DISPLAY') or os.getenv('WAYLAND_SOCKET') then
if state.current_clipboard_backend == "wayland" or mp.get_property_native("focused") then
return mp.get_property('clipboard/text', '')
end
local res = utils.subprocess({
args = { 'wl-paste', '-n' },
playback_only = false,
})
if not res.error then
return res.stdout
end
end
-- X11
local res = utils.subprocess({
args = { 'xclip', '-selection', 'clipboard', '-out' },
playback_only = false,
})
if not res.error then
return res.stdout
end
end
local data, err = mp.get_property('clipboard/text')
if data then
return data
end
-- Fallback to ziggy
if err and err ~= 'property not found' and err ~= 'property unavailable' then
mp.commandv('show-text', 'Get clipboard error: ' .. err)
return nil
end
local err, data = call_ziggy({'get-clipboard'})
if err then
mp.commandv('show-text', 'Get clipboard error. See console for details.')
@@ -977,23 +948,17 @@ end
---@return string|nil payload String that was copied to clipboard.
function set_clipboard(payload)
payload = tostring(payload)
if state.current_clipboard_backend then
if state.platform == 'windows' or state.platform == 'darwin' then
return mp.commandv('set', 'clipboard/text', payload)
end
if state.platform == 'linux' then
-- Wayland
if os.getenv('WAYLAND_DISPLAY') or os.getenv('WAYLAND_SOCKET') then
return utils.subprocess({ args = { 'wl-copy' }, stdin_data = payload })
end
-- X11
return utils.subprocess({
args = { 'xclip', '-silent', '-selection', 'clipboard', '-in' },
stdin_data = payload
})
end
local success, err = mp.set_property('clipboard/text', payload)
if success then
mp.commandv('show-text', t('Copied to clipboard') .. ': ' .. payload, 3000)
return payload
end
-- Fallback to ziggy
if err and err ~= 'property not found' and err ~= 'property unavailable' then
mp.commandv('show-text', 'Set clipboard error: ' .. err)
return nil
end
local err, data = call_ziggy({'set-clipboard', payload})
if err then
mp.commandv('show-text', 'Set clipboard error. See console for details.')
@@ -1004,6 +969,43 @@ function set_clipboard(payload)
return data and data.payload
end
-- Returns Youtube heatmap data if available.
---@return number[]|nil Flat table of normalized points (01)
function load_youtube_heatmap()
if not state.path or not is_protocol(state.path) then return end
-- Match mpv's ytdl whitelist
if not (state.path:match('^https?://%w+%.youtube%.com/') or
state.path:match('^https?://youtube%.com/') or
state.path:match('^https?://youtu%.be/')) then return end
local r = mp.get_property_native('user-data/mpv/ytdl/json-subprocess-result')
local ytdl_result = r and utils.parse_json(r.stdout)
if ytdl_result and ytdl_result.heatmap then
local data = ytdl_result.heatmap
local max_val = 0
local vid_length = data[#data].end_time
for _, seg in ipairs(data) do
max_val = math.max(max_val, seg.value)
end
-- Normalize and clamp to avoid gaps in heatmap
local is_above = options.timeline_heatmap == 'above'
local min_height, graph_height = 4, is_above and 40 or options.timeline_size
local max_norm_y = 1 - (min_height / graph_height)
local norm = {0, 1}
for _, seg in ipairs(data) do
local center_time = (seg.start_time + seg.end_time) / 2
local norm_x = center_time / vid_length
local norm_y = math.min(max_norm_y, 1 - (seg.value / max_val))
norm[#norm + 1], norm[#norm + 2] = norm_x, norm_y
end
-- Add final anchor
local last_y = math.min(max_norm_y, 1 - (data[#data].value / max_val))
norm[#norm + 1], norm[#norm + 2] = 1, last_y
norm[#norm + 1], norm[#norm + 2] = 1, 1
return points_to_bezier(norm)
end
end
--[[ RENDERING ]]
function render()
@@ -1012,9 +1014,6 @@ function render()
cursor:clear_zones()
-- Click on empty area detection
if setup_click_detection then setup_click_detection() end
-- Actual rendering
local ass = assdraw.ass_new()
@@ -1076,4 +1075,4 @@ function request_render()
local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time))
state.render_timer.timeout = timeout
state.render_timer:resume()
end
end
+21 -15
View File
@@ -1,5 +1,5 @@
--[[ uosc | https://github.com/tomasklaen/uosc ]]
local uosc_version = '5.10.0'
local uosc_version = '5.12.0'
mp.commandv('script-message', 'uosc-version', uosc_version)
@@ -27,6 +27,7 @@ defaults = {
timeline_border = 1,
timeline_step = '5',
timeline_cache = true,
timeline_heatmap = 'overlay',
controls =
'menu,gap,<video,audio>subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,gap,space,<video,audio>speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen',
@@ -66,7 +67,6 @@ defaults = {
scale = 1,
scale_fullscreen = 1.3,
font = '',
font_scale = 1,
text_border = 1.2,
border_radius = 4,
@@ -102,7 +102,6 @@ defaults = {
languages = 'slang,en',
subtitles_directory = '~~/subtitles',
disable_elements = '',
ziggy_path = 'default',
}
options = table_copy(defaults)
function handle_options(changed_options)
@@ -143,10 +142,12 @@ local config_defaults = {
foreground_text = serialize_rgba('000000').color,
background = serialize_rgba('000000').color,
background_text = serialize_rgba('ffffff').color,
window_border = serialize_rgba('000000').color,
curtain = serialize_rgba('111111').color,
success = serialize_rgba('a5e075').color,
error = serialize_rgba('ff616e').color,
match = serialize_rgba('69c5ff').color,
heatmap = serialize_rgba('00adee').color,
},
opacity = {
timeline = 0.9,
@@ -167,6 +168,7 @@ local config_defaults = {
audio_indicator = 0.5,
buffering_indicator = 0.3,
playlist_position = 0.8,
heatmap = 0.4,
},
}
config = {
@@ -176,7 +178,7 @@ config = {
-- sets max rendering frequency in case the
-- native rendering frequency could not be detected
render_delay = 1 / 60,
font = options.font ~= '' and options.font or mp.get_property('options/osd-font'),
font = mp.get_property('options/osd-font'),
osd_margin_x = mp.get_property('osd-margin-x'),
osd_margin_y = mp.get_property('osd-margin-y'),
osd_alignment_x = mp.get_property('osd-align-x'),
@@ -333,7 +335,7 @@ function create_default_menu_items()
{
title = t('Aspect ratio'),
items = {
{title = t('Default'), value = 'set video-aspect-override "-1"'},
{title = t('Default'), value = 'set video-aspect-override no'},
{title = '16:9', value = 'set video-aspect-override "16:9"'},
{title = '4:3', value = 'set video-aspect-override "4:3"'},
{title = '2.35:1', value = 'set video-aspect-override "2.35:1"'},
@@ -345,6 +347,7 @@ function create_default_menu_items()
{title = t('Key bindings'), value = 'script-binding uosc/keybinds'},
{title = t('Show in directory'), value = 'script-binding uosc/show-in-directory'},
{title = t('Open config folder'), value = 'script-binding uosc/open-config-directory'},
{title = t('Update uosc'), value = 'script-binding uosc/update'},
},
},
{title = t('Quit'), value = 'quit'},
@@ -381,7 +384,6 @@ state = {
ime_active = mp.get_property_native('input-ime'),
chapters = {},
chapter_ranges = {},
current_clipboard_backend = mp.get_property_native('current-clipboard-backend'),
border = mp.get_property_native('border'),
title_bar = mp.get_property_native('title-bar'),
fullscreen = mp.get_property_native('fullscreen'),
@@ -442,9 +444,7 @@ require('lib/menus')
-- Determine path to ziggy
do
local bin = 'ziggy-' .. (state.platform == 'windows' and 'windows.exe' or state.platform)
config.ziggy_path = os.getenv('MPV_UOSC_ZIGGY') or
options.ziggy_path == 'default' and join_path(mp.get_script_directory(), join_path('bin', bin)) or
utils.join_path(mp.command_native({ 'expand-path', options.ziggy_path }) or '', bin)
config.ziggy_path = os.getenv('MPV_UOSC_ZIGGY') or join_path(mp.get_script_directory(), join_path('bin', bin))
end
--[[ STATE UPDATERS ]]
@@ -746,6 +746,7 @@ mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
set_state('cache_duration', not cache_state.eof and cache_state['cache-duration'] or nil)
else
cached_ranges = {}
set_state('cache_underrun', false)
end
if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or
@@ -869,13 +870,12 @@ bind_command('playlist', create_self_updating_menu_opener({
footnote = t('Paste path or url to add.') .. ' ' .. t('%s to reorder.', 'ctrl+up/down/pgup/pgdn/home/end'),
serializer = function(playlist)
local items = {}
local playlist_titles = mp.get_property_native('user-data/playlistmanager/titles') or {}
local force_filename = mp.get_property_native('osd-playlist-entry') == 'filename'
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
items[index] = {
title = is_url and (title or playlist_titles[item.filename] or url_decode(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),
active = item.current,
value = index,
@@ -956,7 +956,10 @@ bind_command('show-in-directory', function()
end)
bind_command('stream-quality', open_stream_quality_menu)
bind_command('open-file', open_open_file_menu)
bind_command('shuffle', function() set_state('shuffle', not state.shuffle) end)
bind_command('shuffle', function()
set_state('shuffle', not state.shuffle)
mp.osd_message(state.shuffle and t('Shuffle ON') or t('Shuffle OFF'))
end)
bind_command('items', function()
if state.has_playlist then
mp.command('script-binding uosc/playlist')
@@ -1073,6 +1076,9 @@ bind_command('open-config-directory', function()
msg.error('Couldn\'t serialize config path "' .. config_path .. '".')
end
end)
bind_command('update', function()
if not Elements:has('updater') then require('elements/Updater'):new() end
end)
--[[ MESSAGE HANDLERS ]]
@@ -1198,4 +1204,4 @@ function Manager:_commit()
end
-- Initial commit
Manager:disable('user', options.disable_elements)
Manager:disable('user', options.disable_elements)