update
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user