update
This commit is contained in:
@@ -402,4 +402,4 @@
|
||||
"zui": "最嘴罪醉咀蕞䮔厜璻蟕晬嗺噿嶵㠑嶊冣㝡䘹祽鋷錊酻酔樶檌㰎栬槜檇辠䘒稡纗絊",
|
||||
"zun": "尊遵樽鳟撙墫噂嶟鶎銌鱒鐏捘罇鷷僔繜譐",
|
||||
"zuo": "作做坐左座昨佐琢撮柞唑祚捽阼胙嘬怍酢笮葄葃蓙䔘苲莋㸲㝾䞰䎰咗㘀㘴岝岞䝫糳袏鈼㭮稓穝秨筰㛗㑅飵侳繓䋏"
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,4 @@ function BufferingIndicator:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return BufferingIndicator
|
||||
return BufferingIndicator
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -32,4 +32,4 @@ function Curtain:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Curtain
|
||||
return Curtain
|
||||
@@ -83,4 +83,4 @@ function CycleButton:init(id, props)
|
||||
end
|
||||
end
|
||||
|
||||
return CycleButton
|
||||
return CycleButton
|
||||
@@ -262,4 +262,4 @@ function Element:create_action(fn)
|
||||
end
|
||||
end
|
||||
|
||||
return Element
|
||||
return Element
|
||||
@@ -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
|
||||
@@ -33,4 +33,4 @@ function ManagedButton:update(data)
|
||||
end
|
||||
end
|
||||
|
||||
return ManagedButton
|
||||
return ManagedButton
|
||||
+110
-118
@@ -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
|
||||
@@ -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
|
||||
@@ -192,4 +192,4 @@ function Speed:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Speed
|
||||
return Speed
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -56,4 +56,4 @@
|
||||
"open file": "sélectionner un fichier",
|
||||
"parent dir": "répertoire parent",
|
||||
"playlist or file": "fichier ou liste de lecture"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -56,4 +56,4 @@
|
||||
"open file": "deschide fișierul",
|
||||
"parent dir": "director părinte",
|
||||
"playlist or file": "fișier sau listă de redare"
|
||||
}
|
||||
}
|
||||
@@ -56,4 +56,4 @@
|
||||
"open file": "открыть файл",
|
||||
"parent dir": "родительская папка",
|
||||
"playlist or 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."
|
||||
}
|
||||
}
|
||||
@@ -66,4 +66,4 @@
|
||||
"An error has occurred.": "Сталася помилка.",
|
||||
"See above for clues.": "Дивіться підказки вище.",
|
||||
"Play/Pause": "Відтворення / Пауза"
|
||||
}
|
||||
}
|
||||
@@ -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 使其生效"
|
||||
}
|
||||
@@ -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 使其生效"
|
||||
}
|
||||
}
|
||||
@@ -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 (0–1): 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 (0–1) 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
|
||||
@@ -71,4 +71,4 @@ mp.register_script_message('set-button', function(name, data)
|
||||
end
|
||||
end)
|
||||
|
||||
return buttons
|
||||
return buttons
|
||||
@@ -63,4 +63,4 @@ function char_conv(chars, use_ligature, has_separator)
|
||||
end
|
||||
end
|
||||
|
||||
return char_conv
|
||||
return char_conv
|
||||
@@ -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
|
||||
@@ -294,4 +294,4 @@ function fzy.get_implementation_name()
|
||||
return "lua"
|
||||
end
|
||||
|
||||
return fzy
|
||||
return fzy
|
||||
@@ -65,4 +65,4 @@ for i = #languages, 1, -1 do
|
||||
end
|
||||
end
|
||||
|
||||
return {t = t}
|
||||
return {t = t}
|
||||
+33
-81
@@ -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
|
||||
@@ -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
|
||||
@@ -657,4 +657,4 @@ function get_roman_match_positions(title, query, mode, roman)
|
||||
end
|
||||
|
||||
return byte_positions
|
||||
end
|
||||
end
|
||||
+93
-94
@@ -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 (0–1)
|
||||
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
@@ -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)
|
||||
Reference in New Issue
Block a user