317 lines
10 KiB
Lua
317 lines
10 KiB
Lua
local Element = require('elements/Element')
|
|
local dots = {'.', '..', '...'}
|
|
|
|
local function cleanup_output(output)
|
|
return tostring(output):gsub('%c*\n%c*', '\n'):match('^[%s%c]*(.-)[%s%c]*$')
|
|
end
|
|
|
|
---@class Updater : Element
|
|
local Updater = class(Element)
|
|
|
|
function Updater:new() return Class.new(self) --[[@as Updater]] end
|
|
function Updater:init()
|
|
Element.init(self, 'updater', {render_order = 1000})
|
|
self.output = nil
|
|
self.title = ''
|
|
self.state = 'circle' -- Also used as an icon name. 'pending' maps to 'spinner'.
|
|
self.update_available = false
|
|
|
|
-- Buttons
|
|
self.check_button = {method = 'check', title = t('Check for updates')}
|
|
self.update_button = {method = 'update', title = t('Update uosc'), color = config.color.success}
|
|
self.changelog_button = {method = 'open_changelog', title = t('Open changelog')}
|
|
self.close_button = {method = 'destroy', title = t('Close') .. ' (Esc)', color = config.color.error}
|
|
self.quit_button = {method = 'quit', title = t('Quit')}
|
|
self.buttons = {self.check_button, self.close_button}
|
|
self.selected_button_index = 1
|
|
|
|
-- Key bindings
|
|
self:add_key_binding('right', 'select_next_button')
|
|
self:add_key_binding('tab', 'select_next_button')
|
|
self:add_key_binding('left', 'select_prev_button')
|
|
self:add_key_binding('shift+tab', 'select_prev_button')
|
|
self:add_key_binding('enter', 'activate_selected_button')
|
|
self:add_key_binding('kp_enter', 'activate_selected_button')
|
|
self:add_key_binding('esc', 'destroy')
|
|
|
|
Elements:maybe('curtain', 'register', self.id)
|
|
self:check()
|
|
end
|
|
|
|
function Updater:destroy()
|
|
Elements:maybe('curtain', 'unregister', self.id)
|
|
Element.destroy(self)
|
|
end
|
|
|
|
function Updater:quit()
|
|
mp.command('quit')
|
|
end
|
|
|
|
function Updater:select_prev_button()
|
|
self.selected_button_index = self.selected_button_index - 1
|
|
if self.selected_button_index < 1 then self.selected_button_index = #self.buttons end
|
|
request_render()
|
|
end
|
|
|
|
function Updater:select_next_button()
|
|
self.selected_button_index = self.selected_button_index + 1
|
|
if self.selected_button_index > #self.buttons then self.selected_button_index = 1 end
|
|
request_render()
|
|
end
|
|
|
|
function Updater:activate_selected_button()
|
|
local button = self.buttons[self.selected_button_index]
|
|
if button then self[button.method](self) end
|
|
end
|
|
|
|
---@param msg string
|
|
function Updater:append_output(msg)
|
|
self.output = (self.output or '') .. ass_escape('\n' .. cleanup_output(msg))
|
|
request_render()
|
|
end
|
|
|
|
---@param msg string
|
|
function Updater:display_error(msg)
|
|
self.state = 'error'
|
|
self.title = t('An error has occurred.') .. ' ' .. t('See console for details.')
|
|
self:append_output(msg)
|
|
print(msg)
|
|
end
|
|
|
|
function Updater:open_changelog()
|
|
if self.state == 'pending' then return end
|
|
|
|
local url = 'https://github.com/tomasklaen/uosc/releases'
|
|
|
|
self:append_output('Opening URL: ' .. url)
|
|
|
|
call_ziggy_async({'open', url}, function(error)
|
|
if error then
|
|
self:display_error(error)
|
|
return
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Updater:check()
|
|
if self.state == 'pending' then return end
|
|
self.state = 'pending'
|
|
self.title = t('Checking for updates') .. '...'
|
|
|
|
local url = 'https://api.github.com/repos/tomasklaen/uosc/releases/latest'
|
|
local headers = utils.format_json({
|
|
Accept = 'application/vnd.github+json',
|
|
})
|
|
local args = {'http-get', '--headers', headers, url}
|
|
|
|
self:append_output('Fetching: ' .. url)
|
|
|
|
call_ziggy_async(args, function(error, response)
|
|
if error then
|
|
self:display_error(error)
|
|
return
|
|
end
|
|
|
|
release = utils.parse_json(type(response.body) == 'string' and response.body or '')
|
|
if response.status == 200 and type(release) == 'table' and type(release.tag_name) == 'string' then
|
|
self.update_available = config.version ~= release.tag_name
|
|
self:append_output('Response: 200 OK')
|
|
self:append_output('Current version: ' .. config.version)
|
|
self:append_output('Latest version: ' .. release.tag_name)
|
|
if self.update_available then
|
|
self.state = 'upgrade'
|
|
self.title = t('Update available')
|
|
self.buttons = {self.update_button, self.changelog_button, self.close_button}
|
|
self.selected_button_index = 1
|
|
else
|
|
self.state = 'done'
|
|
self.title = t('Up to date')
|
|
end
|
|
else
|
|
self:display_error('Response couldn\'t be parsed, is invalid, or not-OK status code.\nStatus: ' ..
|
|
response.status .. '\nBody: ' .. response.body)
|
|
end
|
|
|
|
request_render()
|
|
end)
|
|
end
|
|
|
|
function Updater:update()
|
|
if self.state == 'pending' then return end
|
|
self.state = 'pending'
|
|
self.title = t('Updating uosc')
|
|
self.output = nil
|
|
request_render()
|
|
|
|
local config_dir = mp.command_native({'expand-path', '~~/'})
|
|
|
|
local function handle_result(success, result, error)
|
|
if success and result and result.status == 0 then
|
|
self.state = 'done'
|
|
self.title = t('uosc has been installed. Restart mpv for it to take effect.')
|
|
self.buttons = {self.quit_button, self.close_button}
|
|
self.selected_button_index = 1
|
|
else
|
|
self.state = 'error'
|
|
self.title = t('An error has occurred.') .. ' ' .. t('See above for clues.')
|
|
end
|
|
|
|
local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
|
|
if state.platform == 'darwin' then
|
|
output =
|
|
'Self-updater is known not to work on MacOS.\nIf you know about a solution, please make an issue and share it with us!.\n' ..
|
|
output
|
|
end
|
|
self:append_output(output)
|
|
end
|
|
|
|
local function update(args)
|
|
local env = utils.get_env_list()
|
|
env[#env + 1] = 'MPV_CONFIG_DIR=' .. config_dir
|
|
|
|
mp.command_native_async({
|
|
name = 'subprocess',
|
|
capture_stderr = true,
|
|
capture_stdout = true,
|
|
playback_only = false,
|
|
args = args,
|
|
env = env,
|
|
}, handle_result)
|
|
end
|
|
|
|
if state.platform == 'windows' then
|
|
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1'
|
|
update({'powershell', '-NoProfile', '-Command', 'irm ' .. url .. ' | iex'})
|
|
else
|
|
-- Detect missing dependencies. We can't just let the process run and
|
|
-- report an error, as on snap packages there's no error. Everything
|
|
-- either exits with 0, or no helpful output/error message.
|
|
local missing = {}
|
|
|
|
for _, name in ipairs({'curl', 'unzip'}) do
|
|
local result = mp.command_native({
|
|
name = 'subprocess',
|
|
capture_stdout = true,
|
|
playback_only = false,
|
|
args = {'which', name},
|
|
})
|
|
local path = cleanup_output(result and result.stdout or '')
|
|
if path == '' then
|
|
missing[#missing + 1] = name
|
|
end
|
|
end
|
|
|
|
if #missing > 0 then
|
|
local stderr = 'Missing dependencies: ' .. table.concat(missing, ', ')
|
|
if config_dir:match('/snap/') then
|
|
stderr = stderr ..
|
|
'\nThis is a known error for mpv snap packages.\nYou can still update uosc by entering the Linux install command from uosc\'s readme into your terminal, it just can\'t be done this way.\nIf you know about a solution, please make an issue and share it with us!'
|
|
end
|
|
handle_result(false, {stderr = stderr})
|
|
else
|
|
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh'
|
|
update({'/bin/bash', '-c', 'source <(curl -fsSL ' .. url .. ')'})
|
|
end
|
|
end
|
|
end
|
|
|
|
function Updater:render()
|
|
local ass = assdraw.ass_new()
|
|
|
|
local text_size = math.min(20 * state.scale, display.height / 20)
|
|
local icon_size = text_size * 2
|
|
local center_x = round(display.width / 2)
|
|
|
|
local color = fg
|
|
if self.state == 'done' or self.update_available then
|
|
color = config.color.success
|
|
elseif self.state == 'error' then
|
|
color = config.color.error
|
|
end
|
|
|
|
-- Divider
|
|
local divider_width = round(math.min(500 * state.scale, display.width * 0.8))
|
|
local divider_half, divider_border_half, divider_y = divider_width / 2, round(1 * state.scale), display.height * 0.65
|
|
local divider_ay, divider_by = round(divider_y - divider_border_half), round(divider_y + divider_border_half)
|
|
ass:rect(center_x - divider_half, divider_ay, center_x - icon_size, divider_by, {
|
|
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
|
})
|
|
ass:rect(center_x + icon_size, divider_ay, center_x + divider_half, divider_by, {
|
|
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
|
})
|
|
if self.state == 'pending' then
|
|
ass:spinner(center_x, divider_y, icon_size, {
|
|
color = fg, border = options.text_border * state.scale, border_color = bg,
|
|
})
|
|
else
|
|
ass:icon(center_x, divider_y, icon_size * 0.8, self.state, {
|
|
color = color, border = options.text_border * state.scale, border_color = bg,
|
|
})
|
|
end
|
|
|
|
-- Output
|
|
local output = self.output or dots[math.ceil((mp.get_time() % 1) * #dots)]
|
|
ass:txt(center_x, divider_y - icon_size, 2, output, {
|
|
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
|
|
})
|
|
|
|
-- Title
|
|
ass:txt(center_x, divider_y + icon_size, 5, self.title, {
|
|
size = text_size, bold = true, color = color, border = options.text_border * state.scale, border_color = bg,
|
|
})
|
|
|
|
-- Buttons
|
|
local outline = round(1 * state.scale)
|
|
local spacing = outline * 9
|
|
local padding = round(text_size * 0.5)
|
|
|
|
local text_opts = {size = text_size, bold = true}
|
|
|
|
-- Calculate button text widths
|
|
local total_width = (#self.buttons - 1) * spacing
|
|
for _, button in ipairs(self.buttons) do
|
|
button.width = text_width(button.title, text_opts) + padding * 2
|
|
total_width = total_width + button.width
|
|
end
|
|
|
|
-- Render buttons
|
|
local ay = round(divider_y + icon_size * 1.8)
|
|
local ax = round(display.width / 2 - total_width / 2)
|
|
local height = text_size + padding * 2
|
|
for index, button in ipairs(self.buttons) do
|
|
local rect = {
|
|
ax = ax,
|
|
ay = ay,
|
|
bx = ax + button.width,
|
|
by = ay + height,
|
|
}
|
|
ax = rect.bx + spacing
|
|
local is_hovered = get_point_to_rectangle_proximity(cursor, rect) == 0
|
|
|
|
-- Background
|
|
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
|
color = button.color or fg,
|
|
radius = state.radius,
|
|
opacity = is_hovered and 1 or 0.8,
|
|
})
|
|
-- Selected outline
|
|
if index == self.selected_button_index then
|
|
ass:rect(rect.ax - outline * 4, rect.ay - outline * 4, rect.bx + outline * 4, rect.by + outline * 4, {
|
|
border = outline,
|
|
border_color = button.color or fg,
|
|
radius = state.radius + outline * 4,
|
|
opacity = {primary = 0, border = 0.5},
|
|
})
|
|
end
|
|
-- Text
|
|
local x, y = rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2
|
|
ass:txt(x, y, 5, button.title, {size = text_size, bold = true, color = fgt})
|
|
|
|
cursor:zone('primary_click', rect, self:create_action(button.method))
|
|
end
|
|
|
|
return ass
|
|
end
|
|
|
|
return Updater
|