1058 lines
32 KiB
Lua
1058 lines
32 KiB
Lua
local Logging = require("logging")
|
|
local CACHE_FILE = "/.storage.cache"
|
|
local LOCK_FILE = "/.storage.lock"
|
|
local LOG_FILE_PATH = "latest.log"
|
|
local Logger = Logging.firstLoad(
|
|
Logging.LogLevel.TRACE,
|
|
Logging.OUTPUTS.combine(
|
|
Logging.OUTPUTS.file(LOG_FILE_PATH)
|
|
--Logging.OUTPUTS.stdout
|
|
)
|
|
)
|
|
|
|
local Chest = require("storage.chest")
|
|
local ItemGroup = require("storage.itemgroup")
|
|
local Storage = require("storage")
|
|
local Text = require("gfx.text")
|
|
local List = require("gfx.list")
|
|
local Event = require("gfx.event")
|
|
local Padding = require("gfx.padding")
|
|
local Container = require("gfx.container")
|
|
local Element = require("gfx.element")
|
|
local Progress = require("gfx.progress")
|
|
local TextProgress = require("gfx.textprogress")
|
|
local Children = require("gfx.prop.children")
|
|
local Orientation = require("gfx.prop.orientation")
|
|
|
|
local function lock()
|
|
if fs.exists(LOCK_FILE) then
|
|
return false
|
|
end
|
|
|
|
-- Create lock file
|
|
return fs.open(LOCK_FILE, "w+").close()
|
|
end
|
|
|
|
local function unlock()
|
|
if not fs.exists(LOCK_FILE) then
|
|
return false
|
|
end
|
|
|
|
-- Delete lock file
|
|
local result, reason = pcall(fs.delete, LOCK_FILE)
|
|
if not result then
|
|
Logger:error("ERROR: Lock file could not be deleted: " .. reason)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function isLocked()
|
|
return fs.exists(LOCK_FILE)
|
|
end
|
|
|
|
local function ignoreClick()
|
|
return true
|
|
end
|
|
|
|
local function saveState(fname, ctrl)
|
|
local ser = ctrl:toSerializable()
|
|
local file = fs.open(fname, "w+")
|
|
file.write(textutils.serialize(ser))
|
|
file.close()
|
|
Logger:info("saved state to", fname)
|
|
end
|
|
|
|
local function loadState(fname, node)
|
|
if not isLocked() then
|
|
Logger:info("Not locked! Loading cache...")
|
|
local file = fs.open(fname, "r")
|
|
if file ~= nil then
|
|
local ser = textutils.unserialize(file.readAll())
|
|
file.close()
|
|
return Storage:fromSerializable(ser)
|
|
end
|
|
end
|
|
|
|
Logger:info("Controller must scan chests...")
|
|
local nodeName = peripheral.getName(node)
|
|
local storageChests = {peripheral.find("inventory")}
|
|
for i,v in ipairs(storageChests) do
|
|
if peripheral.getName(v) == nodeName then
|
|
table.remove(storageChests, i)
|
|
break
|
|
end
|
|
end
|
|
local controller = Storage:fromPeripherals(Storage.assumeHomogeneous(storageChests))
|
|
saveState(fname, controller)
|
|
unlock()
|
|
return controller
|
|
end
|
|
|
|
local function updatePageRender(state, pages)
|
|
if state.nextPage ~= nil then
|
|
state.changingPage = state.nextPage ~= state.currentPage
|
|
state.currentPage = state.nextPage
|
|
state.nextPage = nil
|
|
|
|
state.monitor.clear()
|
|
state._pageRender = pages[state.currentPage](state)
|
|
os.queueEvent("dummy_event")
|
|
else
|
|
state.changingPage = false
|
|
end
|
|
end
|
|
|
|
local function renderDefault(state, rootElement)
|
|
rootElement:setDirty(true)
|
|
rootElement:setParent(state.monitor)
|
|
rootElement:_reload()
|
|
return function()
|
|
local event = {os.pullEvent()}
|
|
local handled = rootElement:handleEvent(event)
|
|
if not handled and event[1] == "monitor_resize" then
|
|
local width, height = state.monitor.getSize()
|
|
if state.width ~= width or state.height ~= height then
|
|
state.width = width
|
|
state.height = height
|
|
|
|
-- Trigger re-render
|
|
rootElement:setDirty(true)
|
|
rootElement:_reload()
|
|
end
|
|
end
|
|
|
|
if not handled and not Event.isClickEvent(event) then
|
|
os.queueEvent(table.unpack(event))
|
|
end
|
|
|
|
rootElement:draw()
|
|
end
|
|
end
|
|
|
|
local function NOOP() end
|
|
|
|
local function getCenterPad(contentWidth, widthBudget)
|
|
if widthBudget < contentWidth then
|
|
return 0, 0
|
|
end
|
|
|
|
local pad = (widthBudget - contentWidth) / 2
|
|
return math.floor(pad), math.ceil(pad)
|
|
end
|
|
|
|
local function fitText(text, widthBudget, alignEnd)
|
|
if #text <= widthBudget then
|
|
return text
|
|
end
|
|
local textStart = alignEnd and (#text - widthBudget + 1) or 1
|
|
local textEnd = (alignEnd and #text) or widthBudget
|
|
return text:sub(textStart, textEnd)
|
|
end
|
|
|
|
local PAGES = {
|
|
MAIN = function(state)
|
|
local pageState = state:currentPageState({
|
|
sortMode = 1,
|
|
filter = "",
|
|
displayKeyboard = false,
|
|
currentPage = 1,
|
|
reloadState = function() end
|
|
})
|
|
|
|
local function _genSort(func, invert, tiebreaker)
|
|
return function(a, b)
|
|
local aRes = func(a)
|
|
local bRes = func(b)
|
|
return (aRes == bRes and tiebreaker ~= nil and tiebreaker(a, b)) or ((aRes < bRes) ~= invert)
|
|
end
|
|
end
|
|
|
|
local function sortByCount(invert, tiebreaker)
|
|
return _genSort(function(v) return v:getItemCount() end, invert, tiebreaker)
|
|
end
|
|
|
|
local function sortByName(invert, tiebreaker)
|
|
return _genSort(function(v) return v:getSimpleName() end, invert, tiebreaker)
|
|
end
|
|
|
|
local function sortByType(invert, tiebreaker)
|
|
return _genSort(function(v) return v:getName() end, invert, tiebreaker)
|
|
end
|
|
|
|
local function sortByDamage(invert, tiebreaker)
|
|
return _genSort(function(v)
|
|
local damage = v:getDamage()
|
|
return (damage == nil or damage == 0) and 1 or (damage / v:getMaxDamage())
|
|
end, invert, tiebreaker)
|
|
end
|
|
|
|
local function composeSortFuncs(...)
|
|
local funcs = {...}
|
|
|
|
local invert = false
|
|
local compose = nil
|
|
for i=#funcs,1,-1 do
|
|
local current = funcs[i]
|
|
if type(current) == "boolean" then
|
|
invert = current
|
|
goto continue
|
|
end
|
|
compose = current(invert, compose)
|
|
invert = false
|
|
::continue::
|
|
end
|
|
return compose
|
|
end
|
|
|
|
local SORT_MODE = {
|
|
composeSortFuncs(sortByCount, true, sortByName, sortByType, sortByDamage),
|
|
composeSortFuncs(sortByName, sortByType, sortByDamage, sortByCount),
|
|
composeSortFuncs(sortByType, sortByDamage, sortByCount, sortByName),
|
|
composeSortFuncs(sortByCount, sortByName, sortByType, sortByDamage),
|
|
composeSortFuncs(sortByName, true, sortByType, sortByDamage, sortByCount),
|
|
composeSortFuncs(sortByType, true, sortByDamage, sortByCount, sortByName)
|
|
}
|
|
|
|
|
|
local function matchFilter(filter, str)
|
|
if #filter == 0 then
|
|
return true
|
|
end
|
|
|
|
local cur = 1
|
|
filter = filter:lower()
|
|
str = str:lower()
|
|
for i=1,#str do
|
|
if str:sub(i,i) == filter:sub(cur,cur) then
|
|
cur = cur + 1
|
|
if cur > #filter then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
---- BOTTOM BAR
|
|
local keyboardButton = Text:new{
|
|
id = "action_keyboard",
|
|
text = "KBD"
|
|
}
|
|
|
|
local importButton = Text:new{
|
|
id = "action_import",
|
|
text = "IMPORT"
|
|
}
|
|
|
|
local sortButton = Text:new{
|
|
id = "action_sort"
|
|
}
|
|
|
|
local function tabActionButtonID(value)
|
|
return value < 0 and ("<"..tostring(-value)) or (tostring(value)..">")
|
|
end
|
|
|
|
local function tabActionButton(count)
|
|
local text = tabActionButtonID(count)
|
|
return Text:new{ id = text, text = text }
|
|
end
|
|
|
|
local function tabActionButtonValue(index, interval, count, sign)
|
|
-- | s = 1: i - 1
|
|
-- k(i, c, s) <|
|
|
-- | s = -1: c - i
|
|
|
|
-- Simple solution (7 operations):
|
|
-- (c + 1)(1 - s) - 2
|
|
-- k(i, c, s) = ------------------ + i*s
|
|
-- 2
|
|
|
|
-- (`tabActionButtonID` abbreviated as `tabID`)
|
|
-- tabID(i, t, c, s) = max(1, k(i, c, s) * t * s)
|
|
|
|
-- E.g.
|
|
-- interval = 5, count = 3, sign = 1
|
|
-- i=1: tabID(i, interval, count, sign) = max(1, k(1, 2, 1) * 5) * 1 = max(1, (-1 + 1) * 5) = max(1, 0) = 1
|
|
-- i=2: tabID(i, interval, count, sign) = max(1, k(2, 2, 1) * 5) * 1 = max(1, (-1 + 2) * 5) = max(1, 5) = 5
|
|
-- i=3: tabID(i, interval, count, sign) = max(1, k(3, 2, 1) * 5) * 1 = max(1, (-1 + 3) * 5) = max(1, 10) = 10
|
|
|
|
-- E.g.
|
|
-- interval = 5, count = 3, sign = -1
|
|
-- i=1: tabID(i, interval, count, sign) = max(1, k(1, 2, -1) * 5) * (-1) = -max(1, (3 + -1) * 5) = -max(1, 10) = -10
|
|
-- i=2: tabID(i, interval, count, sign) = max(1, k(2, 2, -1) * 5) * (-1) = -max(1, (3 + -2) * 5) = -max(1, 5) = -5
|
|
-- i=3: tabID(i, interval, count, sign) = max(1, k(3, 2, -1) * 5) * (-1) = -max(1, (3 + -3) * 5) = -max(1, 0) = -1
|
|
return math.max(1, (((((count + 1) * (1 - sign)) - 2) / 2) + (sign * index)) * interval) * sign
|
|
end
|
|
|
|
local function tabActionList(interval, count, sign, spacing)
|
|
local buttons = {}
|
|
if count > 0 then
|
|
table.insert(buttons, tabActionButton(tabActionButtonValue(1, interval, count, sign)))
|
|
for i=2,count do
|
|
table.insert(buttons, Element:new{ width = spacing })
|
|
table.insert(buttons, tabActionButton(tabActionButtonValue(i, interval, count, sign)))
|
|
end
|
|
end
|
|
|
|
return List:new{
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = buttons
|
|
}
|
|
end
|
|
|
|
local ACTION_COUNT = 2
|
|
local ACTION_INTERVAL = 5
|
|
local ACTION_SPACING = 1
|
|
|
|
local actions = {
|
|
tabActionList(ACTION_INTERVAL, ACTION_COUNT, -1, ACTION_SPACING),
|
|
List:new {
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
keyboardButton,
|
|
Element:new{ width = 1 },
|
|
importButton,
|
|
Element:new{ width = 1 },
|
|
sortButton
|
|
}
|
|
},
|
|
tabActionList(ACTION_INTERVAL, ACTION_COUNT, 1, ACTION_SPACING)
|
|
}
|
|
|
|
for i=#actions+1,1,-1 do
|
|
table.insert(actions, i, Element:new{ width = 0 })
|
|
end
|
|
|
|
local bottomBarList = List:new {
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = actions
|
|
}
|
|
|
|
local function calculateActionSpaces()
|
|
if #actions == 1 then
|
|
return -- NOP for single padding element
|
|
end
|
|
|
|
local usedSpace = 0
|
|
for i=2,#actions,2 do
|
|
usedSpace = usedSpace + actions[i]:getWidth()
|
|
end
|
|
|
|
local barWidth = state.width
|
|
local freeSpace = barWidth - usedSpace
|
|
local asymmetry = freeSpace % ((#actions + 1) / 2)
|
|
local part = (freeSpace - asymmetry) / ((#actions + 1) / 2)
|
|
for i=1,#actions,2 do
|
|
actions[i]:setWidth((i <= asymmetry and 1 or 0) + part)
|
|
end
|
|
bottomBarList:_reload()
|
|
end
|
|
|
|
|
|
local storageSatProgress = TextProgress:new{}
|
|
local aboveEntries = { storageSatProgress }
|
|
local belowEntries = { bottomBarList }
|
|
local entries = {}
|
|
for _,v in ipairs(aboveEntries) do
|
|
table.insert(entries, v)
|
|
end
|
|
|
|
local groupEntryListBudget = state.height
|
|
for _,v in ipairs(aboveEntries) do
|
|
groupEntryListBudget = groupEntryListBudget - v:getHeight()
|
|
end
|
|
for _,v in ipairs(belowEntries) do
|
|
groupEntryListBudget = groupEntryListBudget - v:getHeight()
|
|
end
|
|
|
|
local GroupEntryID = {
|
|
PADDING = "padding",
|
|
NAME = "name",
|
|
COUNT = "count"
|
|
}
|
|
|
|
for i=1,groupEntryListBudget do
|
|
local bgColor = (i % 2 == 0) and colors.lightGray or colors.black
|
|
local fgColor = (i % 2 == 0) and colors.black or colors.lightGray
|
|
table.insert(
|
|
entries,
|
|
List:new{
|
|
id = tostring(i),
|
|
bgColor = bgColor,
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
Padding:new{
|
|
id = GroupEntryID.PADDING,
|
|
bgColor = bgColor,
|
|
element = Text:new{ id = GroupEntryID.NAME, bgColor = bgColor, fgColor = fgColor }
|
|
},
|
|
Text:new{
|
|
id = GroupEntryID.COUNT,
|
|
bgColor = bgColor,
|
|
fgColor = fgColor
|
|
}
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
for _,v in ipairs(belowEntries) do
|
|
table.insert(entries, v)
|
|
end
|
|
|
|
local mainList = List:new{
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = entries
|
|
}
|
|
|
|
local KEY_BACKSPACE = "backspace"
|
|
local ID_FILTER_DISPLAY = "display_filter"
|
|
local ID_FILTER_PADDING = "display_filter_pad"
|
|
local KEYBOARD_BG_COLOR = colors.gray
|
|
local KEYBOARD_KEY_COLOR = colors.gray
|
|
local KEYBOARD_FILTER_BG_COLOR = colors.lightGray
|
|
local KEYBOARD_FILTER_TEXT_COLOR_SOME = colors.black
|
|
local KEYBOARD_FILTER_TEXT_COLOR_NONE = colors.red
|
|
local keyboardLines = {
|
|
lines = {
|
|
{ backspace = true, "1234567890" },
|
|
{ "qwertyuiop" },
|
|
{ "asdfghjkl" },
|
|
{ "zxcvbnm_:" }
|
|
},
|
|
elements = { }
|
|
}
|
|
|
|
local function charInputKeyList(chars, backspace)
|
|
local keys = { }
|
|
for i=1,#chars do
|
|
local key = chars:sub(i, i)
|
|
-- ((not backspace) and i == #keys and 0) or 1
|
|
table.insert(keys, Padding:new{ onClick = ignoreClick, bgColor = KEYBOARD_BG_COLOR, right = 1, element = Text:new{
|
|
id = key,
|
|
text = key,
|
|
bgColor = KEYBOARD_KEY_COLOR,
|
|
onClick = function()
|
|
pageState.filter = pageState.filter..key
|
|
pageState.reloadState()
|
|
return true
|
|
end
|
|
}})
|
|
end
|
|
if backspace then
|
|
table.insert(keys, Text:new{
|
|
id = KEY_BACKSPACE,
|
|
text = "<--",
|
|
bgColor = KEYBOARD_KEY_COLOR,
|
|
onClick = function()
|
|
pageState.filter = fitText(pageState.filter, math.max(0, #pageState.filter - 1))
|
|
pageState.reloadState()
|
|
return true
|
|
end
|
|
})
|
|
end
|
|
return List:new{
|
|
bgColor = KEYBOARD_BG_COLOR,
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = keys
|
|
}
|
|
end
|
|
|
|
local KEYBOARD_HPAD = 1
|
|
local KEYBOARD_VPAD = 1
|
|
local keyboardWidth = KEYBOARD_HPAD * 2
|
|
for _,line in ipairs(keyboardLines.lines) do
|
|
local keyLineList = charInputKeyList(line[1], line.backspace)
|
|
keyboardWidth = math.max(keyboardWidth, keyLineList:getWidth())
|
|
table.insert(keyboardLines.elements, keyLineList)
|
|
end
|
|
|
|
-- TODO: Pad elements properly
|
|
|
|
local paddedFilterText = Padding:new{
|
|
id = ID_FILTER_PADDING,
|
|
bgColor = KEYBOARD_FILTER_BG_COLOR,
|
|
element = Text:new{ id = ID_FILTER_DISPLAY, bgColor = KEYBOARD_FILTER_BG_COLOR }
|
|
}
|
|
table.insert(keyboardLines.elements, 1, paddedFilterText)
|
|
|
|
local keyboardList = Padding:new{
|
|
onClick = ignoreClick,
|
|
bgColor = KEYBOARD_BG_COLOR,
|
|
left = KEYBOARD_HPAD,
|
|
right = KEYBOARD_HPAD,
|
|
top = KEYBOARD_VPAD,
|
|
bottom = KEYBOARD_VPAD,
|
|
element = List:new{
|
|
bgColor = KEYBOARD_BG_COLOR,
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = keyboardLines.elements
|
|
}
|
|
}
|
|
|
|
local screenContainer = Container:new{
|
|
[Children:getId()] = {
|
|
mainList,
|
|
keyboardList
|
|
},
|
|
x = 1,
|
|
y = 1,
|
|
strict = true
|
|
}
|
|
|
|
local function reloadState()
|
|
screenContainer:setWidth(state.width)
|
|
screenContainer:setHeight(state.height)
|
|
|
|
-- Enumerate inventory stats
|
|
local emptyCount = 0
|
|
local totalCount = 0
|
|
pageState.stacks = ItemGroup.collectStacks(state.controller:find(function(stack)
|
|
if stack:isEmpty() then
|
|
emptyCount = emptyCount + 1
|
|
return false
|
|
else
|
|
totalCount = totalCount + 1
|
|
return matchFilter(pageState.filter, stack:getSimpleName()) or matchFilter(pageState.filter, stack:getDisplayName()) or (pageState.filter:find(":") ~= nil and matchFilter(pageState.filter, stack:getName()))
|
|
end
|
|
end))
|
|
totalCount = totalCount + emptyCount
|
|
table.sort(pageState.stacks, SORT_MODE[pageState.sortMode])
|
|
|
|
local lastPage = #pageState.stacks % groupEntryListBudget
|
|
|
|
pageState.pages = (#pageState.stacks - lastPage) / groupEntryListBudget + (lastPage == 0 and 0 or 1)
|
|
|
|
|
|
-- Set dynamic states for elements
|
|
sortButton:setText("<"..tostring(pageState.sortMode)..">")
|
|
local totalCountStr = tostring(totalCount)
|
|
local availCount = totalCount - emptyCount
|
|
local availCountStr = tostring(availCount)
|
|
local storageSat = availCountStr / totalCount
|
|
|
|
storageSatProgress:setText(
|
|
(availCountStr)..
|
|
(" "):rep(math.max(0, math.floor(state.width / 2) - #availCountStr - 1))..
|
|
"/"..
|
|
(" "):rep(math.max(0, math.ceil(state.width / 2) - #totalCountStr))..
|
|
totalCountStr
|
|
)
|
|
storageSatProgress:setProgress(storageSat)
|
|
storageSatProgress:setFgColor((storageSat <= 0.33 and colors.green) or (storageSat <= 0.66 and colors.orange) or colors.red)
|
|
|
|
local basePageIndex = (pageState.currentPage - 1) * groupEntryListBudget
|
|
for i=1,groupEntryListBudget do
|
|
local group = pageState.stacks[basePageIndex + i]
|
|
|
|
local isVisible = group ~= nil
|
|
local listEntry = mainList:findById(tostring(i))
|
|
listEntry:setVisible(isVisible)
|
|
if isVisible then
|
|
local nameText = listEntry:findById(GroupEntryID.NAME)
|
|
local namePadding = listEntry:findById(GroupEntryID.PADDING)
|
|
local countText = listEntry:findById(GroupEntryID.COUNT)
|
|
|
|
countText:setText(tostring(group:getItemCount()))
|
|
|
|
local nameTextBudget = state.width - countText:getWidth() - 1
|
|
local name = fitText(group:getSimpleName(), nameTextBudget)
|
|
local padding = state.width - #name - countText:getWidth()
|
|
|
|
nameText:setText(name)
|
|
namePadding:setPadding{ right = padding }
|
|
listEntry:_reload()
|
|
listEntry:setOnClick(function()
|
|
state:setPage("GROUP_DETAIL", group)
|
|
return true
|
|
end)
|
|
else
|
|
listEntry:setOnClick(nil)
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: redefined-local
|
|
local keyboardWidth = keyboardList:getWidth()
|
|
keyboardList:setPos(math.floor((screenContainer:getWidth() - keyboardWidth) / 2), screenContainer:getHeight() - keyboardList:getHeight() - bottomBarList:getHeight())
|
|
|
|
local keyboardInnerWidth = keyboardList:getInnerWidth()
|
|
|
|
local filterDisplayedText = fitText(pageState.filter, keyboardInnerWidth, true)
|
|
local filterText = keyboardList:findById(ID_FILTER_DISPLAY)
|
|
filterText:setText(filterDisplayedText)
|
|
filterText:setFgColor(#pageState.stacks == 0 and KEYBOARD_FILTER_TEXT_COLOR_NONE or KEYBOARD_FILTER_TEXT_COLOR_SOME)
|
|
|
|
local filterPadding = keyboardList:findById(ID_FILTER_PADDING)
|
|
filterPadding:setPadding{
|
|
right = keyboardInnerWidth - filterText:getWidth()
|
|
}
|
|
|
|
keyboardList:setVisible(pageState.displayKeyboard)
|
|
|
|
calculateActionSpaces()
|
|
screenContainer:setDirty(true)
|
|
end
|
|
|
|
local function bindTabActionButtons()
|
|
local function onClickHandler(change)
|
|
return function()
|
|
local newValue = math.max(1, math.min(pageState.currentPage + change, pageState.pages))
|
|
if newValue ~= pageState.currentPage then
|
|
pageState.currentPage = newValue
|
|
reloadState()
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
for i=1,ACTION_COUNT do
|
|
local value = tabActionButtonValue(i, ACTION_INTERVAL, ACTION_COUNT, 1)
|
|
bottomBarList:findById(tabActionButtonID(value)):setOnClick(onClickHandler(value))
|
|
end
|
|
for i=1,ACTION_COUNT do
|
|
local value = tabActionButtonValue(i, ACTION_INTERVAL, ACTION_COUNT, -1)
|
|
bottomBarList:findById(tabActionButtonID(value)):setOnClick(onClickHandler(value))
|
|
end
|
|
end
|
|
bindTabActionButtons()
|
|
|
|
keyboardButton:setOnClick(function()
|
|
Logger:debug("Toggling keyboard...")
|
|
pageState.displayKeyboard = not pageState.displayKeyboard
|
|
|
|
reloadState()
|
|
return true
|
|
end)
|
|
|
|
importButton:setOnClick(function()
|
|
Logger:debug("Importing...")
|
|
state.node:rescan()
|
|
-- Safely handle transfers, priming computer for a full reset/rescan in case server stops mid-transaction
|
|
state:itemTransaction(function()
|
|
for _,nodeStack in ipairs(state.node:find(function(s) return not s:isEmpty() end)) do
|
|
if not state.controller:insertStack(nodeStack) then
|
|
Logger:warn("Couldn't find a free slot for: ", nodeStack:getSimpleName())
|
|
end
|
|
end
|
|
end)
|
|
|
|
reloadState()
|
|
return true
|
|
end)
|
|
|
|
sortButton:setOnClick(function()
|
|
Logger:debug("Reorganizing...")
|
|
pageState.sortMode = (pageState.sortMode % #SORT_MODE) + 1
|
|
reloadState()
|
|
return true
|
|
end)
|
|
|
|
pageState.reloadState = reloadState
|
|
|
|
reloadState()
|
|
|
|
return renderDefault(state, screenContainer)
|
|
end,
|
|
|
|
GROUP_DETAIL = function(state)
|
|
local paddingSide = 0
|
|
local paddingTop = 0
|
|
|
|
local group = state:getExtra()
|
|
if group == nil then
|
|
Logger:error("No group passed to GROUP_DETAIL")
|
|
state:setPage("MAIN")
|
|
return NOOP
|
|
end
|
|
|
|
local itemName = fitText(group:getSimpleName(), state.width - (paddingSide * 2))
|
|
local itemLeftPad, itemRightPad = getCenterPad(#itemName, state.width - (paddingSide * 2))
|
|
|
|
local paddedTitle = Padding:new{
|
|
top = 1,
|
|
left = itemLeftPad,
|
|
right = itemRightPad,
|
|
bottom = 1,
|
|
element = Text:new{
|
|
text = itemName,
|
|
bgColor = colors.gray
|
|
},
|
|
bgColor = colors.gray
|
|
}
|
|
|
|
local simpleDetailsList = List:new{
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
Padding:new{
|
|
top = 0,
|
|
left = 1,
|
|
right = 1,
|
|
bottom = 0,
|
|
bgColor = colors.red,
|
|
element = List:new{
|
|
bgColor = colors.red,
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = {
|
|
Text:new{ text = "Count", bgColor = colors.red },
|
|
Text:new{ text = tostring(group:getItemCount()), bgColor = colors.red }
|
|
}
|
|
}
|
|
},
|
|
Padding:new{
|
|
bgColor = colors.blue,
|
|
top = 0,
|
|
left = 1,
|
|
right = 1,
|
|
bottom = 0,
|
|
element = List:new{
|
|
bgColor = colors.blue,
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = {
|
|
Text:new{ text = "Slots", bgColor = colors.blue },
|
|
Text:new{ text = tostring(group:getStackCount()), bgColor = colors.blue }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
local damage = group:getDamage()
|
|
local maxDamage = group:getMaxDamage()
|
|
local damageBar = nil
|
|
if damage ~= nil and maxDamage ~= nil then
|
|
damageBar = Padding:new{
|
|
left = 1,
|
|
right = 1,
|
|
element = Progress:new{
|
|
progress = (damage == 0 and maxDamage or damage / maxDamage),
|
|
width = state.width - 2,
|
|
height = 1,
|
|
fgColor = colors.green
|
|
}
|
|
}
|
|
end
|
|
|
|
local infoElements = List:new{
|
|
[Children:getId()] = {
|
|
paddedTitle,
|
|
Padding:new{
|
|
top = 0,
|
|
bottom = 1,
|
|
left = 0,
|
|
right = 0,
|
|
element = simpleDetailsList
|
|
},
|
|
damageBar
|
|
},
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
width = state.width
|
|
}
|
|
|
|
local stuffContainer = Container:new{
|
|
[Children:getId()] = {
|
|
Padding:new{
|
|
top = paddingTop,
|
|
left = paddingSide,
|
|
right = paddingSide,
|
|
bottom = 0,
|
|
element = infoElements,
|
|
},
|
|
Padding:new{
|
|
x = 1,
|
|
y = state.height - 2,
|
|
top = 1,
|
|
left = 1,
|
|
right = 1,
|
|
bottom = 1,
|
|
bgColor = colors.gray,
|
|
element = Text:new{
|
|
text = "Request",
|
|
bgColor = colors.gray
|
|
},
|
|
onClick = function(e, x, y, s)
|
|
state:setPage("REQUEST", group)
|
|
return true
|
|
end
|
|
}
|
|
},
|
|
width = state.width,
|
|
height = state.height,
|
|
parent = state.monitor,
|
|
onClick = function(e, x, y, s)
|
|
state:setPage("MAIN")
|
|
return true
|
|
end
|
|
}
|
|
|
|
return renderDefault(state, stuffContainer)
|
|
end,
|
|
|
|
REQUEST = function(state)
|
|
local group = state:getExtra()
|
|
if group == nil then
|
|
Logger:error("No group passed to REQUEST")
|
|
state:setPage("MAIN")
|
|
return NOOP
|
|
end
|
|
|
|
local pageState = state:currentPageState(
|
|
{ request = 0 },
|
|
state.changingPage
|
|
)
|
|
|
|
local itemName = fitText(group:getSimpleName(), state.width)
|
|
local itemLeftPad, itemRightPad = getCenterPad(#itemName, state.width)
|
|
|
|
local paddedTitle = Padding:new{
|
|
top = 1,
|
|
left = itemLeftPad,
|
|
right = itemRightPad,
|
|
bottom = 1,
|
|
element = Text:new{
|
|
text = itemName,
|
|
bgColor = colors.gray
|
|
},
|
|
bgColor = colors.gray
|
|
}
|
|
|
|
local function makeRequestButton(increment)
|
|
local text = increment > 0 and ("+"..tostring(increment)) or tostring(increment)
|
|
return Text:new{ id = text, text = text }
|
|
end
|
|
|
|
local PADDING_RQC_H = 1
|
|
local paddedRequestCount = Padding:new{
|
|
top = 3,
|
|
left = PADDING_RQC_H,
|
|
right = PADDING_RQC_H,
|
|
bottom = 0,
|
|
element = List:new{
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = {
|
|
List:new{
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
Text:new{ text = "Request" },
|
|
Padding:new{
|
|
top = 0,
|
|
bottom = 0,
|
|
left = math.floor((state.width - (PADDING_RQC_H * 2) - #("Request") - #("Available")) / 2),
|
|
right = math.ceil((state.width - (PADDING_RQC_H * 2) - #("Request") - #("Available")) / 2),
|
|
element = Text:new{ text = "/" }
|
|
},
|
|
Text:new{ text = "Available" },
|
|
}
|
|
},
|
|
List:new{
|
|
id = "list_request_count",
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
Text:new{ id = "data_request", text = tostring(pageState.request) },
|
|
Padding:new{
|
|
id = "data_divider",
|
|
top = 0,
|
|
bottom = 0,
|
|
element = Text:new{ text = "/" }
|
|
},
|
|
Text:new{ id = "data_available", text = tostring(group:getItemCount()) },
|
|
}
|
|
},
|
|
Progress:new{
|
|
id = "request_capacity",
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
width = state.width - (PADDING_RQC_H * 2),
|
|
height = 1,
|
|
progress = pageState.request / group:getItemCount()
|
|
},
|
|
Element:new{
|
|
height = 1,
|
|
width = 0
|
|
},
|
|
Padding:new{
|
|
top = 0,
|
|
bottom = 0,
|
|
left = math.floor((state.width - 12)/2),
|
|
right = math.ceil((state.width - 12)/2),
|
|
element = List:new{
|
|
[Orientation:getId()] = Orientation.HORIZONTAL,
|
|
[Children:getId()] = {
|
|
makeRequestButton(-5),
|
|
Element:new{ width = 1 },
|
|
makeRequestButton(-1),
|
|
Element:new{ width = 2 },
|
|
makeRequestButton(1),
|
|
Element:new{ width = 1 },
|
|
makeRequestButton(5),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
local function paddedButton(label, width, height, parentHeight, parentWidth, onClick, bgColor, x, y)
|
|
local labelElement = Text:new{ text = label, bgColor = bgColor }
|
|
return Padding:new{
|
|
x = (parentWidth + (x or 1)) % parentWidth,
|
|
y = (parentHeight + (y or 1)) % parentHeight,
|
|
top = math.ceil((height - labelElement:getHeight()) / 2),
|
|
bottom = math.floor((height - labelElement:getHeight()) / 2),
|
|
left = math.ceil((width - labelElement:getWidth()) / 2),
|
|
right = math.floor((width - labelElement:getWidth()) / 2),
|
|
element = labelElement,
|
|
bgColor = bgColor,
|
|
onClick = onClick
|
|
}
|
|
end
|
|
|
|
local stuffContainer = List:new{
|
|
[Orientation:getId()] = Orientation.VERTICAL,
|
|
[Children:getId()] = {
|
|
paddedTitle,
|
|
paddedRequestCount
|
|
},
|
|
onClick = function(e, x, y, s)
|
|
state:setPage("GROUP_DETAIL", group)
|
|
return true
|
|
end
|
|
}
|
|
-- Anchored to (1,1)
|
|
local windowWrapper = Container:new{
|
|
[Children:getId()] = {
|
|
stuffContainer,
|
|
paddedButton(
|
|
"Fetch",
|
|
state.width / 2,
|
|
3,
|
|
state.height,
|
|
state.width,
|
|
function()
|
|
Logger:debug("Exporting to access node")
|
|
state.node:rescan()
|
|
state:itemTransaction(function()
|
|
group:transferTo(state.node, pageState.request)
|
|
end)
|
|
state:setPage("MAIN")
|
|
return true
|
|
end,
|
|
colors.green,
|
|
1,
|
|
-2
|
|
),
|
|
paddedButton(
|
|
"Back",
|
|
state.width / 2,
|
|
3,
|
|
state.height,
|
|
state.width,
|
|
function()
|
|
state:setPage("GROUP_DETAIL", group)
|
|
return true
|
|
end,
|
|
colors.red,
|
|
math.ceil(state.width / 2) + 1,
|
|
-2
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
-- Set up event management
|
|
local function updateDisplayState()
|
|
Logger:debug("updateDisplayState")
|
|
local dataRequestText = paddedRequestCount:findById("data_request")
|
|
local dataDividerPad = paddedRequestCount:findById("data_divider")
|
|
local dataAvailableText = paddedRequestCount:findById("data_available")
|
|
local requestCapProgress = paddedRequestCount:findById("request_capacity")
|
|
local requestCountList = paddedRequestCount:findById("list_request_count")
|
|
|
|
local requestText = tostring(pageState.request)
|
|
local availableText = tostring(group:getItemCount())
|
|
|
|
dataRequestText:setText(requestText)
|
|
dataDividerPad:setPadding{
|
|
left = math.ceil((state.width - 1 - (PADDING_RQC_H * 2) - #requestText - #availableText) / 2),
|
|
right = math.floor((state.width - 1 - (PADDING_RQC_H * 2) - #requestText - #availableText) / 2)
|
|
}
|
|
dataAvailableText:setText(availableText)
|
|
requestCapProgress:setProgress(pageState.request / group:getItemCount())
|
|
|
|
requestCountList:adjustPositions()
|
|
stuffContainer:setDirty(true)
|
|
end
|
|
|
|
local function bindRequestButton(increment)
|
|
local id = increment > 0 and ("+"..tostring(increment)) or tostring(increment)
|
|
paddedRequestCount:findById(id):setOnClick(function(e, x, y, s)
|
|
local newValue = math.max(0, math.min(pageState.request + increment, group:getItemCount()))
|
|
if pageState.request ~= newValue then
|
|
pageState.request = newValue
|
|
updateDisplayState()
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
bindRequestButton(-5)
|
|
bindRequestButton(-1)
|
|
bindRequestButton(1)
|
|
bindRequestButton(5)
|
|
|
|
updateDisplayState()
|
|
|
|
return renderDefault(state, windowWrapper)
|
|
end
|
|
}
|
|
|
|
|
|
local accessNode = Chest:fromPeripheral("minecraft:trapped_chest", true)
|
|
---@diagnostic disable-next-line: need-check-nil
|
|
local controller = loadState(CACHE_FILE, accessNode:getInventory())
|
|
|
|
local nTerm = term.native()
|
|
local nTermW, nTermH = nTerm.getSize()
|
|
local monitor = window.create(nTerm, 1, 1, nTermW, nTermH, true) --peripheral.find("monitor")
|
|
local width, height = monitor.getSize()
|
|
|
|
local CONTROLLER_STATE = {
|
|
controller = controller,
|
|
node = accessNode,
|
|
width = width,
|
|
height = height,
|
|
monitor = monitor,
|
|
nextPage = "MAIN",
|
|
exit = false,
|
|
pageState = {}
|
|
}
|
|
|
|
function CONTROLLER_STATE:itemTransaction(transact)
|
|
lock()
|
|
transact()
|
|
saveState(CACHE_FILE, self.controller)
|
|
unlock()
|
|
end
|
|
|
|
function CONTROLLER_STATE:currentPageState(default, override)
|
|
self.pageState[self.currentPage] = ((not override or ((type(override) == "function") and not override(self.pageState[self.currentPage]))) and self.pageState[self.currentPage]) or default
|
|
return self.pageState[self.currentPage]
|
|
end
|
|
|
|
function CONTROLLER_STATE:setPage(page, extra)
|
|
Logger:debug("new page", page, extra)
|
|
self.nextPage = page
|
|
self.extra = extra
|
|
end
|
|
|
|
function CONTROLLER_STATE:getExtra()
|
|
return self.extra
|
|
end
|
|
|
|
function CONTROLLER_STATE:reloadPage()
|
|
self.nextPage = self.currentPage
|
|
end
|
|
|
|
while not CONTROLLER_STATE.exit do
|
|
updatePageRender(CONTROLLER_STATE, PAGES)
|
|
CONTROLLER_STATE._pageRender()
|
|
end |