local Logging = require("logging") local LogLevel = Logging.LogLevel local CACHE_FILE = "/.storage.cache" local LOCK_FILE = "/.storage.lock" local LOG_FILE_PATH = "latest.log" local Logger = Logging.firstLoad( Logging.OUTPUTS.file(LOG_FILE_PATH, LogLevel.INFO), Logging.OUTPUTS.stdout(LogLevel.CRITICAL) ) 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 = 3 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() if #pageState.filter > 0 then pageState.filter = fitText(pageState.filter, math.max(0, #pageState.filter - 1)) pageState.reloadState() end 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, onKey = function(elem, keyCode, held) if keyCode == keys.backspace then if #pageState.filter > 0 then pageState.filter = fitText(pageState.filter, math.max(0, #pageState.filter - 1)) pageState.reloadState() end return true elseif keyCode == keys.enter then pageState.filter = "" pageState.reloadState() return true end return false end, onChar = function(elem, charCode) pageState.filter = pageState.filter..charCode pageState.reloadState() return true end, 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 requestButtons = List:new{ [Orientation:getId()] = Orientation.HORIZONTAL, [Children:getId()] = { makeRequestButton(-64), Element:new{ width = 1 }, makeRequestButton(-16), Element:new{ width = 1 }, makeRequestButton(-8), Element:new{ width = 1 }, makeRequestButton(-1), Element:new{ width = 2 }, makeRequestButton(1), Element:new{ width = 1 }, makeRequestButton(8), Element:new{ width = 1 }, makeRequestButton(16), Element:new{ width = 1 }, makeRequestButton(64), } } local rButtonsLeft, rButtonsRight = getCenterPad(requestButtons:getWidth(), state.width) 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 = rButtonsLeft, right = rButtonsRight, element = requestButtons } } } } 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(-64) bindRequestButton(-16) bindRequestButton(-8) bindRequestButton(-1) bindRequestButton(1) bindRequestButton(8) bindRequestButton(16) bindRequestButton(64) 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