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