Skip to content

Commit

Permalink
feat: improved uosc/update to check for latest version and ask whet…
Browse files Browse the repository at this point in the history
…her to update

closes #781
  • Loading branch information
tomasklaen committed Dec 7, 2024
1 parent 6b53f5a commit 0b3cef9
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 39 deletions.
13 changes: 10 additions & 3 deletions src/uosc/elements/Element.lua
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,12 @@ end

-- Adds a keybinding for the lifetime of the element, or until removed manually.
---@param key string mpv key identifier.
---@param fnFlags fun()|table<fun()|string> Callback, or `{callback, flags}` tuple.
---@param fnFlags fun()|string|table<fun()|string> Callback, or `{callback, flags}` tuple. Callback can be just a method name, in which case it'll be wrapped in `create_action(callback)`.
---@param namespace? string Keybinding namespace. Default is `_`.
function Element:add_key_binding(key, fnFlags, namespace)
local name = self.id .. '-' .. key
local isTuple = type(fnFlags) == 'table'
local fn = (isTuple and fnFlags[1] or fnFlags) --[[@as fun()]]
local fn = (isTuple and fnFlags[1] or fnFlags)
local flags = isTuple and fnFlags[2] or nil
namespace = namespace or '_'
local names = self._key_bindings[namespace]
Expand All @@ -210,6 +210,9 @@ function Element:add_key_binding(key, fnFlags, namespace)
self._key_bindings[namespace] = names
end
names[name] = true
if type(fn) == 'string' then
fn = self:create_action(fn)
end
mp.add_forced_key_binding(key, name, fn, flags)
end

Expand Down Expand Up @@ -243,8 +246,12 @@ end
function Element:is_alive() return not self.destroyed end

-- Wraps a function into a callback that won't run if element is destroyed or otherwise disabled.
---@param fn function()
---@param fn fun(...)|string Function or a name of a method on this class to call.
function Element:create_action(fn)
if type(fn) == 'string' then
local method = fn
fn = function(...) self[method](self, ...) end
end
return function(...)
if self:is_alive() then fn(...) end
end
Expand Down
218 changes: 182 additions & 36 deletions src/uosc/elements/Updater.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,148 @@ function Updater:new() return Class.new(self) --[[@as Updater]] end
function Updater:init()
Element.init(self, 'updater', {render_order = 1000})
self.output = nil
self.message = t('Updating uosc')
self.state = 'pending' -- Matches icon name
local config_dir = mp.command_native({'expand-path', '~~/'})
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.message = t('uosc has been installed. Restart mpv for it to take effect.')
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.message = t('An error has occurred.') .. ' ' .. t('See above for clues.')
self.title = t('An error has occurred.') .. ' ' .. t('See above for clues.')
end

local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
Expand All @@ -33,10 +162,7 @@ function Updater:init()
'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.output = ass_escape(cleanup_output(output))

request_render()
self:append_output(output)
end

local function update(args)
Expand Down Expand Up @@ -89,11 +215,6 @@ function Updater:init()
end
end

function Updater:destroy()
Elements:maybe('curtain', 'unregister', self.id)
Element.destroy(self)
end

function Updater:render()
local ass = assdraw.ass_new()

Expand All @@ -102,7 +223,7 @@ function Updater:render()
local center_x = round(display.width / 2)

local color = fg
if self.state == 'done' then
if self.state == 'done' or self.update_available then
color = config.color.success
elseif self.state == 'error' then
color = config.color.error
Expand Down Expand Up @@ -134,34 +255,59 @@ function Updater:render()
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
})

-- Message
ass:txt(center_x, divider_y + icon_size, 5, self.message, {
-- 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,
})

-- Button
if self.state ~= 'pending' then
-- Background
local button_y = divider_y + icon_size * 1.75
local button_rect = {
ax = round(center_x - icon_size / 2),
ay = round(button_y),
bx = round(center_x + icon_size / 2),
by = round(button_y + icon_size),
-- 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,
}
local is_hovered = get_point_to_rectangle_proximity(cursor, button_rect) == 0
ass:rect(button_rect.ax, button_rect.ay, button_rect.bx, button_rect.by, {
color = fg,
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.5,
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})

-- Icon
local x = round(button_rect.ax + (button_rect.bx - button_rect.ax) / 2)
local y = round(button_rect.ay + (button_rect.by - button_rect.ay) / 2)
ass:icon(x, y, icon_size * 0.8, 'close', {color = bg})

cursor:zone('primary_click', button_rect, function() self:destroy() end)
cursor:zone('primary_click', rect, self:create_action(button.method))
end

return ass
Expand Down

0 comments on commit 0b3cef9

Please sign in to comment.