opus/apps/Overview.lua

446 lines
10 KiB
Lua
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require = requireInjector(getfenv(1))
local Util = require('util')
local Event = require('event')
local UI = require('ui')
local Config = require('config')
local NFT = require('nft')
local class = require('class')
local FileUI = require('fileui')
multishell.setTitle(multishell.getCurrent(), 'Overview')
UI:configure('Overview', ...)
local config = {
Recent = { },
currentCategory = 'Apps',
}
local applications = { }
Config.load('Overview', config)
Config.load('apps', applications)
local defaultIcon = NFT.parse([[
8071180
8007180
7180071]])
local sx, sy = term.current().getSize()
local maxRecent = math.ceil(sx * sy / 62)
local function elipse(s, len)
if #s > len then
s = s:sub(1, len - 2) .. '..'
end
return s
end
local buttons = { }
local categories = { }
table.insert(buttons, { text = 'Recent', event = 'category' })
for _,f in pairs(applications) do
if not categories[f.category] then
categories[f.category] = true
table.insert(buttons, { text = f.category, event = 'category' })
end
end
table.insert(buttons, { text = '+', event = 'new' })
local function parseIcon(iconText)
local icon
local s, m = pcall(function()
icon = NFT.parse(iconText)
if icon then
if icon.height > 3 or icon.width > 8 then
error('Invalid size')
end
end
return icon
end)
if s then
return icon
end
return s, m
end
local page = UI.Page {
tabBar = UI.TabBar {
buttons = buttons,
},
container = UI.ViewportWindow {
y = 2,
},
notification = UI.Notification(),
accelerators = {
r = 'refresh',
e = 'edit',
s = 'shell',
l = 'lua',
[ 'control-l' ] = 'refresh',
[ 'control-n' ] = 'new',
delete = 'delete',
},
}
function page:draw()
self.tabBar:draw()
self.container:draw()
end
UI.Icon = class(UI.Window)
function UI.Icon:init(args)
local defaults = {
UIElement = 'Icon',
width = 14,
height = 4,
}
UI.setProperties(defaults, args)
UI.Window.init(self, defaults)
end
function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then
self:setFocus(self.button)
return true
elseif event.type == 'mouse_doubleclick' then
self:emit({ type = self.button.event, button = self.button })
elseif event.type == 'mouse_rightclick' then
self:setFocus(self.button)
self:emit({ type = 'edit', button = self.button })
end
return UI.Window.eventHandler(self, event)
end
function page.container:setCategory(categoryName)
-- reset the viewport window
self.children = { }
self.offy = 0
local function filter(it, f)
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
local filtered
if categoryName == 'Recent' then
filtered = { }
for _,v in ipairs(config.Recent) do
local app = Util.find(applications, 'run', v)
if app and fs.exists(app.run) then
table.insert(filtered, app)
end
end
else
filtered = filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run)
end)
table.sort(filtered, function(a, b) return a.title < b.title end)
end
for _,program in ipairs(filtered) do
local icon
if program.icon then
icon = parseIcon(program.icon)
end
if not icon then
icon = defaultIcon
end
local title = elipse(program.title, 8)
local width = math.max(icon.width + 2, #title + 2)
table.insert(self.children, UI.Icon({
width = width,
image = UI.NftImage({
x = math.floor((width - icon.width) / 2) + 1,
image = icon,
width = 5,
height = 3,
}),
button = UI.Button({
x = math.floor((width - #title - 2) / 2) + 1,
y = 4,
text = title,
backgroundColor = self.backgroundColor,
width = #title + 2,
event = 'button',
app = program,
}),
}))
end
local gutter = 2
if UI.term.width <= 26 then
gutter = 1
end
local col, row = gutter, 2
local count = #self.children
-- reposition all children
for k,child in ipairs(self.children) do
child.x = col
child.y = row
if k < count then
col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > UI.term.width then
col = gutter
row = row + 5
end
end
end
self:initChildren()
end
function page:refresh()
local pos = self.container.offy
self:focusFirst(self)
self.container:setCategory(config.currentCategory)
self.container:setScrollPosition(pos)
end
function page:resize()
self:refresh()
UI.Page.resize(self)
end
function page:eventHandler(event)
if event.type == 'category' then
self.tabBar:selectTab(event.button.text)
self.container:setCategory(event.button.text)
self.container:draw()
config.currentCategory = event.button.text
Config.update('Overview', config)
elseif event.type == 'button' then
for k,v in ipairs(config.Recent) do
if v == event.button.app.run then
table.remove(config.Recent, k)
break
end
end
table.insert(config.Recent, 1, event.button.app.run)
if #config.Recent > maxRecent then
table.remove(config.Recent, maxRecent + 1)
end
Config.update('Overview', config)
multishell.openTab({
path = '/apps/shell',
args = { event.button.app.run },
focused = true,
})
elseif event.type == 'shell' then
multishell.openTab({
path = '/apps/shell',
focused = true,
})
elseif event.type == 'lua' then
multishell.openTab({
path = '/apps/Lua.lua',
focused = true,
})
elseif event.type == 'focus_change' then
if event.focused.parent.UIElement == 'Icon' then
event.focused.parent:scrollIntoView()
end
elseif event.type == 'refresh' then
applications = { }
Config.load('apps', applications)
self:refresh()
self:draw()
self.notification:success('Refreshed')
elseif event.type == 'delete' then
local focused = page:getFocused()
if focused.app then
local _,k = Util.find(applications, 'run', focused.app.run)
if k then
table.remove(applications, k)
Config.update('apps', applications)
page:refresh()
page:draw()
self.notification:success('Removed')
end
end
elseif event.type == 'new' then
local category = 'Apps'
if config.currentCategory ~= 'Recent' then
category = config.currentCategory or 'Apps'
end
UI:setPage('editor', { category = category })
elseif event.type == 'edit' then
local focused = page:getFocused()
if focused.app then
UI:setPage('editor', focused.app)
end
else
UI.Page.eventHandler(self, event)
end
return true
end
local formWidth = math.max(UI.term.width - 14, 26)
local gutter = math.floor((UI.term.width - formWidth) / 2) + 1
local editor = UI.Page({
backgroundColor = colors.blue,
form = UI.Form({
fields = {
{ label = 'Title', key = 'title', width = 15, limit = 11, display = UI.Form.D.entry,
help = 'Application title' },
{ label = 'Run', key = 'run', width = formWidth - 11, limit = 100, display = UI.Form.D.entry,
help = 'Full path to application' },
{ label = 'Category', key = 'category', width = 15, limit = 11, display = UI.Form.D.entry,
help = 'Category of application' },
{ text = 'Accept', event = 'accept', display = UI.Form.D.button,
x = 1, y = 9, width = 10 },
{ text = 'Cancel', event = 'cancel', display = UI.Form.D.button,
x = formWidth - 11, y = 9, width = 10 },
},
labelWidth = 8,
x = gutter + 1,
y = math.max(2, math.floor((UI.term.height - 9) / 2)),
height = 9,
width = UI.term.width - (gutter * 2),
image = UI.NftImage({
y = 5,
x = 1,
height = 3,
width = 8,
}),
button = UI.Button({
x = 10,
y = 6,
text = 'Load icon',
width = 11,
event = 'loadIcon',
}),
}),
statusBar = UI.StatusBar(),
notification = UI.Notification(),
iconFile = '',
})
function editor:enable(app)
if app then
self.original = app
self.form:setValues(Util.shallowCopy(app))
local icon
if app.icon then
icon = parseIcon(app.icon)
end
self.form.image:setImage(icon)
self:setFocus(self.form.children[1])
end
UI.Page.enable(self)
end
function editor.form.image:draw()
self:clear()
UI.NftImage.draw(self)
end
function editor:updateApplications(app, original)
if original.run then
local _,k = Util.find(applications, 'run', original.run)
if k then
applications[k] = nil
end
end
table.insert(applications, app)
Config.update('apps', applications)
end
function editor:eventHandler(event)
if event.type == 'cancel' then
UI:setPreviousPage()
elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help or '')
self.statusBar:draw()
elseif event.type == 'loadIcon' then
UI:setPage(FileUI(), fs.getDir(self.iconFile), function(fileName)
if fileName then
self.iconFile = fileName
local s, m = pcall(function()
local iconLines = Util.readFile(fileName)
if not iconLines then
error('Unable to load file')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
self.form.values.icon = iconLines
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
self.notification:error(m:gsub('.*: (.*)', '%1'))
end
end
end)
elseif event.type == 'accept' then
local values = self.form.values
if #values.run > 0 and #values.title > 0 and #values.category > 0 then
UI:setPreviousPage()
self:updateApplications(values, self.original)
page:refresh()
page:draw()
else
self.notification:error('Require fields missing')
--self.statusBar:setStatus('Require fields missing')
--self.statusBar:draw()
end
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPages({
editor = editor,
main = page,
})
Event.addHandler('os_register_app', function()
applications = { }
Config.load('apps', applications)
page:refresh()
page:draw()
page:sync()
end)
page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps')
UI:setPage(page)
Event.pullEvents()
UI.term:reset()