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