local mon = peripheral.find("monitor") assert(mon, "No monitor found") local monName = peripheral.getName(mon) mon.setTextScale(0.5) local SCREEN_WIDTH, SCREEN_HEIGHT = mon.getSize() local oldTerm = term.redirect(mon) local function ensureDependency(path, url) if fs.exists(path) then return end assert(shell and type(shell.run) == "function", "Shell API is not available") assert(shell.run("wget", url, path) and fs.exists(path), "Could not download dependency: " .. path) end ensureDependency("Pine3D.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/Pine3D.lua") ensureDependency("betterblittle.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/betterblittle.lua") ensureDependency("morefonts-pe.lua", "https://raw.githubusercontent.com/MichielP1807/more-fonts/main/pine3d/morefonts-pe.lua") local Pine3D = require("Pine3D") local mf = require("morefonts-pe") local frame = Pine3D.newFrame() frame:setBackgroundColor(colors.black) local function findStorageBridge() return peripheral.find("meBridge") or peripheral.find("me_bridge") or peripheral.find("rsBridge") or peripheral.find("rs_bridge") end local meSystem = findStorageBridge() local function ensureStorageBridge() if not meSystem then meSystem = findStorageBridge() end return meSystem end local chainItemsByBaseId local chainAllItemsByBaseId local baseItemIds local allChainItemIds local itemById local defaultBaseId local whitelistLookup local fallbackIcon = false local backButton = false local scrollUpButton = false local scrollDownButton = false local overviewCachedCounts = {} local overviewCachedSortedItemIds = nil local overviewScrollCurrentRow = 0 local overviewScrollTargetRow = 0 local globalItemCounts = {} local globalCountRefreshInterval = 5 local globalCountRefreshJob local globalCountRefreshTimer local globalCountLastRefreshAt = 0 local drawOverview local function ensureWhitelistLoaded() if whitelistLookup ~= nil then return end if not fs.exists("whitelist.json") then whitelistLookup = false return end local file = assert(fs.open("whitelist.json", "r")) local data = textutils.unserializeJSON(file.readAll()) file.close() assert(type(data) == "table", "Invalid whitelist.json data") if #data == 0 then whitelistLookup = false return end whitelistLookup = {} for i = 1, #data do local baseId = data[i] assert(type(baseId) == "string", "Invalid whitelist.json entry") whitelistLookup[baseId] = true end end local function getCompressionLevel(itemId, stage) if stage == "item" then return 0 end if type(stage) == "string" then local level = tonumber(stage:match("^(%d+)x$")) if level then return level end end if type(itemId) == "string" then local level = tonumber(itemId:match("_(%d+)x$")) if level then return level end end return 0 end local function createChainItem(itemId, iconNfp, stage) return { id = itemId, icon_nfp = iconNfp, stage = stage, compression_level = getCompressionLevel(itemId, stage), equivalent_factor = 1, } end local function applyChainEquivalentFactors(baseId, orderedItems) local baseIndex = nil for i = 1, #orderedItems do if orderedItems[i].id == baseId then baseIndex = i break end end assert(baseIndex, "Missing base item in chain: " .. tostring(baseId)) for i = 1, #orderedItems do orderedItems[i].equivalent_factor = 9 ^ (i - baseIndex) end end local function ensureChainsLoaded() if not chainItemsByBaseId then ensureWhitelistLoaded() local file = assert(fs.open("atc_chains.json", "r")) local data = textutils.unserializeJSON(file.readAll()) file.close() local chains = data if type(data) == "table" and type(data.chains) == "table" then chains = data.chains end assert(type(chains) == "table", "Invalid JSON data") chainItemsByBaseId = {} chainAllItemsByBaseId = {} baseItemIds = {} allChainItemIds = {} itemById = {} for _, chain in ipairs(chains) do local baseId = chain.base_id or chain[1] if not whitelistLookup or whitelistLookup[baseId] then if not defaultBaseId then defaultBaseId = baseId end baseItemIds[#baseItemIds + 1] = baseId local orderedItems = {} local pageItems = {} if chain.base_id then for _, item in ipairs(chain.items or {}) do local chainItem = createChainItem(item.item_id, item.icon_nfp_16x16, item.stage) orderedItems[#orderedItems + 1] = chainItem itemById[item.item_id] = chainItem allChainItemIds[#allChainItemIds + 1] = item.item_id end else local compactItems = nil if type(chain[2]) == "number" then compactItems = chain[3] or {} elseif type(chain[2]) == "table" then compactItems = chain[2] elseif type(chain[2]) == "string" and type(chain[3]) == "table" then orderedItems[1] = createChainItem(baseId, chain[2], "item") itemById[baseId] = orderedItems[1] allChainItemIds[#allChainItemIds + 1] = baseId compactItems = chain[3] else error("Invalid compact chain entry for " .. tostring(baseId)) end for i = 1, #compactItems do local compactItem = compactItems[i] local chainItem = createChainItem(compactItem[1], compactItem[2], compactItem[3]) orderedItems[#orderedItems + 1] = chainItem itemById[compactItem[1]] = chainItem allChainItemIds[#allChainItemIds + 1] = compactItem[1] end end applyChainEquivalentFactors(baseId, orderedItems) for i = 1, #orderedItems do pageItems[#pageItems + 1] = orderedItems[i] end chainAllItemsByBaseId[baseId] = orderedItems chainItemsByBaseId[baseId] = pageItems end end end end local function getPageItems(base_id) ensureChainsLoaded() return chainItemsByBaseId[base_id or defaultBaseId] or {} end local function getAllChainItems(base_id) ensureChainsLoaded() return chainAllItemsByBaseId[base_id or defaultBaseId] or {} end local function getBaseItemIds() ensureChainsLoaded() return baseItemIds end local function getItemById(item_id) ensureChainsLoaded() return itemById[item_id] end local function parseNfpImage(nfp) if type(nfp) ~= "string" or nfp == "" then return nil end local image = {} local y = 1 for line in (nfp .. "\n"):gmatch("(.-)\n") do local row = {} for x = 1, #line do local c = line:sub(x, x) if c ~= " " then local value = tonumber(c, 16) if value then row[x] = 2 ^ value end end end image[y] = row y = y + 1 end return image end local function parseIconData(iconData) if type(iconData) == "string" then return parseNfpImage(iconData) end if type(iconData) ~= "table" then return nil end local offsetX = tonumber(iconData[1]) or 0 local offsetY = tonumber(iconData[2]) or 0 local croppedImage = parseNfpImage(iconData[3]) if not croppedImage then return nil end if offsetX == 0 and offsetY == 0 then return croppedImage end local image = {} for y, row in pairs(croppedImage) do local shiftedRow = {} for x, value in pairs(row) do shiftedRow[x + offsetX] = value end image[y + offsetY] = shiftedRow end return image end local function getItemIcon(item) if not item then return nil end if item.icon == nil then item.icon = parseIconData(item.icon_nfp) or false item.icon_nfp = nil end return item.icon or nil end local function getMeItemCount(itemId) local bridge = ensureStorageBridge() if not bridge or not itemId then return 0 end if type(bridge.getItem) == "function" then local ok, item = pcall(bridge.getItem, { name = itemId }) if ok and item then return tonumber(item.amount or item.count or item.qty) or 0 end end return 0 end local function listBridgeItems(bridge) local listItems = bridge.listItems or bridge.getItems or bridge.listAvailableItems if not listItems then return nil end local ok, items = pcall(listItems, {}) if ok and type(items) == "table" then local normalized = {} for _, item in pairs(items) do normalized[#normalized + 1] = item end return normalized end ok, items = pcall(listItems) if ok and type(items) == "table" then local normalized = {} for _, item in pairs(items) do normalized[#normalized + 1] = item end return normalized end return nil end local function getMeItemCounts(itemIds) local counts = {} for i = 1, #itemIds do counts[itemIds[i]] = 0 end local bridge = ensureStorageBridge() if not bridge then return counts end local snapshot = listBridgeItems(bridge) if not snapshot then return counts end local wanted = {} for i = 1, #itemIds do wanted[itemIds[i]] = true end for i = 1, #snapshot do local item = snapshot[i] local itemId = item and (item.name or item.id or item.item_id) if itemId and wanted[itemId] then counts[itemId] = tonumber(item.amount or item.count or item.qty) or 0 end end return counts end local function makeZeroCounts(itemIds) local counts = {} for i = 1, #itemIds do counts[itemIds[i]] = 0 end return counts end local function createAsyncCountsFromSnapshotJob(itemIds, snapshot, batchSize, baseCounts) local counts = {} local index = 1 local wanted = {} batchSize = math.max(1, math.floor(tonumber(batchSize) or 1)) for i = 1, #itemIds do wanted[itemIds[i]] = true counts[itemIds[i]] = 0 end if type(baseCounts) == "table" then for itemId, count in pairs(baseCounts) do if wanted[itemId] then counts[itemId] = tonumber(count) or 0 end end end return { step = function() local lastIndex = math.min(#snapshot, index + batchSize - 1) for i = index, lastIndex do local item = snapshot[i] local itemId = item and (item.name or item.id or item.item_id) if itemId and wanted[itemId] then counts[itemId] = tonumber(item.amount or item.count or item.qty) or 0 end end index = lastIndex + 1 return index > #snapshot, counts end, } end local function getGlobalItemCounts() ensureChainsLoaded() return globalItemCounts end local function hasGlobalItemCounts() return next(globalItemCounts) ~= nil end local function getBaseEquivalentCount(baseId, itemCounts) local total = 0 local chainItems = getAllChainItems(baseId) for i = 1, #chainItems do local item = chainItems[i] total = total + (tonumber(itemCounts[item.id]) or 0) * (item.equivalent_factor or 1) end return total end local function getEquivalentLevelCounts(baseId, pageItems, itemCounts) local equivalentCounts = {} local baseEquivalentCount = getBaseEquivalentCount(baseId, itemCounts) for i = 1, #pageItems do local item = pageItems[i] local factor = item.equivalent_factor or 1 equivalentCounts[item.id] = math.floor(baseEquivalentCount / factor) end return equivalentCounts end local function getEquivalentBaseCounts(baseIds, itemCounts) local equivalentCounts = {} for i = 1, #baseIds do local baseId = baseIds[i] equivalentCounts[baseId] = getBaseEquivalentCount(baseId, itemCounts) end return equivalentCounts end local function createGlobalCountRefreshJob() ensureChainsLoaded() local bridge = ensureStorageBridge() if not bridge then return nil end local snapshot = listBridgeItems(bridge) if not snapshot then return nil end local job = createAsyncCountsFromSnapshotJob(allChainItemIds or {}, snapshot, 25, globalItemCounts) return { step = function() local isDone, counts = job.step() if isDone then globalItemCounts = counts globalCountLastRefreshAt = os.epoch("utc") end return isDone, counts end, } end local function shouldStartGlobalCountRefresh() if globalCountRefreshJob then return false end if not hasGlobalItemCounts() then return true end return os.epoch("utc") - globalCountLastRefreshAt >= globalCountRefreshInterval * 1000 end local function startGlobalCountRefreshIfNeeded(force) if globalCountRefreshJob then return false end if not force and not shouldStartGlobalCountRefresh() then return false end globalCountRefreshJob = createGlobalCountRefreshJob() if not globalCountRefreshJob then return false end globalCountRefreshTimer = os.startTimer(0.01) return true end local function stepGlobalCountRefresh(timerId) if not globalCountRefreshJob or timerId ~= globalCountRefreshTimer then return false, false end local isDone = globalCountRefreshJob.step() if isDone then globalCountRefreshJob = nil globalCountRefreshTimer = nil return true, true end globalCountRefreshTimer = os.startTimer(0.01) return true, false end local function getFallbackIcon() if fallbackIcon == false then fallbackIcon = parseNfpImage(table.concat({ "0000000000000000", "000000eeee000000", "00000eeeeee00000", "0000eee00eee0000", "00000e00000ee000", "0000000000eee000", "000000000eee0000", "00000000eee00000", "00000000ee000000", "0000000000000000", "00000000ee000000", "00000000ee000000", "0000000000000000", "0000000000000000", "0000000000000000", "0000000000000000", }, "\n")) end return fallbackIcon end local function getBackButton() if backButton == false then backButton = parseNfpImage(table.concat({ "e e", " e ", "e e", }, "\n")) end return backButton end local function getScrollUpButton() if scrollUpButton == false then scrollUpButton = parseNfpImage(table.concat({ " 00 ", "0000", "0 0", }, "\n")) end return scrollUpButton end local function getScrollDownButton() if scrollDownButton == false then scrollDownButton = parseNfpImage(table.concat({ "0 0", "0000", " 00 ", }, "\n")) end return scrollDownButton end local function imageSize(img) local w, h = 0, 0 for y, row in pairs(img) do h = math.max(h, y) for x, value in pairs(row) do if type(value) == "number" and value > 0 then w = math.max(w, x) end end end return w, h end local function formatCount(n) n = math.floor(tonumber(n) or 0) if n < 1000 then return tostring(n) elseif n < 100000 then local v = n / 1000 if v < 10 then return string.format("%.1fK", v):gsub("%.0K", "K") end return tostring(math.floor(v)) .. "K" elseif n < 100000000 then local v = n / 1000000 if v < 10 then return string.format("%.1fM", v):gsub("%.0M", "M") end return tostring(math.floor(v)) .. "M" elseif n < 100000000000 then local v = n / 1000000000 if v < 10 then return string.format("%.1fB", v):gsub("%.0B", "B") end return tostring(math.floor(v)) .. "B" else local v = n / 1000000000000 if v < 10 then return string.format("%.1fT", v):gsub("%.0T", "T") end return tostring(math.floor(v)) .. "T" end end local function drawNfpScaled(buffer, img, x, y, scaleX, scaleY) scaleX = scaleX or 2 scaleY = scaleY or 3 for imgY, row in pairs(img) do for imgX, col in pairs(row) do -- IMPORTANT: -- Only write real ComputerCraft colors into the Pine3D buffer. -- No strings, no nil values, no 0. if type(col) == "number" and col > 0 then local px = x + (imgX - 1) * scaleX local py = y + (imgY - 1) * scaleY for dy = 0, scaleY - 1 do for dx = 0, scaleX - 1 do local drawX = px + dx local drawY = py + dy if drawX >= 1 and drawX <= buffer.width and drawY >= 1 and drawY <= buffer.height then buffer:setPixel(drawX, drawY, col) end end end end end end end local function fillRect(buffer, x, y, width, height, color) local startX = math.max(1, x) local startY = math.max(1, y) local endX = math.min(buffer.width, x + width - 1) local endY = math.min(buffer.height, y + height - 1) if startX > endX or startY > endY then return end for py = startY, endY do for px = startX, endX do buffer:setPixel(px, py, color) end end end local function cellToPixelX(cellX) return (cellX - 1) * 2 + 1 end local function cellToPixelY(cellY) return (cellY - 1) * 3 + 1 end local function assertBufferValid(frame) local valid = { [colors.white] = true, [colors.orange] = true, [colors.magenta] = true, [colors.lightBlue] = true, [colors.yellow] = true, [colors.lime] = true, [colors.pink] = true, [colors.gray] = true, [colors.lightGray] = true, [colors.cyan] = true, [colors.purple] = true, [colors.blue] = true, [colors.brown] = true, [colors.green] = true, [colors.red] = true, [colors.black] = true, } for y = 1, frame.buffer.height do for x = 1, frame.buffer.width do local c = frame.buffer.colorValues[y][x] if not valid[c] then error("Invalid pixel in buffer at x=" .. x .. ", y=" .. y .. ": " .. tostring(c)) end end end end local function drawItem(img, cellX, cellY, count, offsetY, item) -- Convert normal monitor cells into Pine3D teletext pixels. local x = cellToPixelX(cellX) local y = cellToPixelY(cellY) + (offsetY or 0) local imgW, imgH = 0, 0 local scaleX = 1 local scaleY = 1 if img then imgW, imgH = imageSize(img) if y <= frame.buffer.height and y + imgH * scaleY - 1 >= 1 then drawNfpScaled(frame.buffer, img, x, y, scaleX, scaleY) end end local compressionLevel = item and tonumber(item.compression_level) or 0 if compressionLevel > 0 and y <= frame.buffer.height and y + 8 >= 1 then mf.writeOn(frame, tostring(compressionLevel), colors.red, x + 15, y, { font = "fonts/PixelPlace", textAlign = "right", anchorHor = "right", anchorVer = "top", condense = true, scale = 1, }) end local text = formatCount(count) local rightX = x + 16 -- x + math.max(imgW * scaleX - 1, 7) local bottomY = y + 16 -- y + math.max(imgH * scaleY - 1, 4) local options = { font = "fonts/QuinqueFive", textAlign = "right", anchorHor = "right", anchorVer = "bottom", condense = true, scale = 1, dx = 1, dy = 1, } if bottomY >= 1 and y <= frame.buffer.height then -- Shadow mf.writeOn(frame, text, colors.black, rightX + 1, bottomY + 1, options) -- White count mf.writeOn(frame, text, colors.white, rightX, bottomY, options) end end local function runScrollableGrid(options) local entries = options.getInitialEntries() local counts = options.getInitialCounts(entries) local visibleItemCount = 0 local totalRows = 0 local maxScrollRow = 0 local currentScrollRow = 0 local targetScrollRow = 0 local refreshTimer local animationTimer local pendingCacheSync = false local columns = 5 local rowsPerView = 3 local baseCellX = 3 local baseCellY = 2 local colStepCells = 8+3 local rowStepCells = 5+3 local rowStepPixels = rowStepCells * 3 local scrollbarX = SCREEN_WIDTH - 1 local scrollbarWidth = 2 local scrollbarButtonHeight = 3 local scrollbarTrackY = scrollbarButtonHeight + 1 local scrollbarTrackHeight = SCREEN_HEIGHT - scrollbarButtonHeight * 2 local scrollbarPixelX = cellToPixelX(scrollbarX) local scrollbarPixelWidth = scrollbarWidth * 2 local scrollbarTrackPixelY = cellToPixelY(scrollbarTrackY) local scrollbarTrackPixelHeight = math.max(0, scrollbarTrackHeight * 3) if options.getInitialScrollState then local initialCurrentRow, initialTargetRow = options.getInitialScrollState() currentScrollRow = tonumber(initialCurrentRow) or 0 targetScrollRow = tonumber(initialTargetRow) or currentScrollRow end local function snapScrollRow(row) return math.floor((tonumber(row) or 0) + 0.5) end local function persistScrollState() if options.setScrollState then options.setScrollState(snapScrollRow(targetScrollRow), snapScrollRow(targetScrollRow)) end end local function clampScrollRow(row) return math.max(0, math.min(maxScrollRow, row)) end local function getThumbMetrics() if scrollbarTrackPixelHeight <= 0 then return scrollbarTrackPixelY, 0 end if totalRows <= rowsPerView or maxScrollRow == 0 then return scrollbarTrackPixelY, scrollbarTrackPixelHeight end local thumbHeight = math.max(4, math.floor(scrollbarTrackPixelHeight * rowsPerView / totalRows + 0.5)) local thumbTravel = scrollbarTrackPixelHeight - thumbHeight local thumbY = scrollbarTrackPixelY + math.floor((currentScrollRow / maxScrollRow) * thumbTravel + 0.5) return thumbY, thumbHeight end local function scheduleAnimation() if not animationTimer and math.abs(targetScrollRow - currentScrollRow) > 0.001 then animationTimer = os.startTimer(0.05) end end local function scrollTo(row) local clampedRow = clampScrollRow(snapScrollRow(row)) if clampedRow ~= targetScrollRow then targetScrollRow = clampedRow persistScrollState() scheduleAnimation() end end local function syncFromCache() entries, counts = options.getEntriesAndCountsFromCache(entries, counts) end local function renderGrid() totalRows = math.ceil(#entries / columns) maxScrollRow = math.max(0, totalRows - rowsPerView) currentScrollRow = clampScrollRow(currentScrollRow) targetScrollRow = clampScrollRow(snapScrollRow(targetScrollRow)) persistScrollState() local firstVisibleRow = math.floor(currentScrollRow) local rowOffsetPixels = -math.floor((currentScrollRow - firstVisibleRow) * rowStepPixels + 0.5) local firstVisibleIndex = firstVisibleRow * columns + 1 local lastVisibleIndex = math.min(#entries, (firstVisibleRow + rowsPerView + 1) * columns) frame.buffer:clear() if options.drawChrome then options.drawChrome() end for i = firstVisibleIndex, lastVisibleIndex do local entry = entries[i] local relativeIndex = i - firstVisibleIndex local col = relativeIndex % columns local row = math.floor(relativeIndex / columns) drawItem( options.getIcon(entry), baseCellX + colStepCells * col, baseCellY + rowStepCells * row, counts[options.getId(entry)] or 0, rowOffsetPixels, entry ) end visibleItemCount = math.max(0, math.min(#entries - firstVisibleRow * columns, rowsPerView * columns)) local upButtonY = cellToPixelY(1) local downButtonY = cellToPixelY(SCREEN_HEIGHT - scrollbarButtonHeight + 1) local upColor = targetScrollRow > 0 and colors.gray or colors.lightGray local downColor = targetScrollRow < maxScrollRow and colors.gray or colors.lightGray local thumbY, thumbHeight = getThumbMetrics() fillRect(frame.buffer, scrollbarPixelX, upButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, upColor) fillRect(frame.buffer, scrollbarPixelX, downButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, downColor) if scrollbarTrackPixelHeight > 0 then fillRect(frame.buffer, scrollbarPixelX, scrollbarTrackPixelY, scrollbarPixelWidth, scrollbarTrackPixelHeight, colors.gray) fillRect(frame.buffer, scrollbarPixelX, thumbY, scrollbarPixelWidth, thumbHeight, colors.white) end drawNfpScaled(frame.buffer, getScrollUpButton(), scrollbarPixelX, upButtonY + 2, 1, 1) drawNfpScaled(frame.buffer, getScrollDownButton(), scrollbarPixelX, downButtonY + 3, 1, 1) assertBufferValid(frame) frame:drawBuffer() end syncFromCache() renderGrid() if options.shouldStartRefreshImmediately == nil or options.shouldStartRefreshImmediately(entries, counts) then startGlobalCountRefreshIfNeeded(false) end refreshTimer = os.startTimer(options.refreshSeconds or globalCountRefreshInterval) while true do local event, p1, x, y = os.pullEvent() if event == "timer" and p1 == refreshTimer then startGlobalCountRefreshIfNeeded(false) refreshTimer = os.startTimer(options.refreshSeconds or globalCountRefreshInterval) scheduleAnimation() elseif event == "timer" and p1 == animationTimer then local delta = targetScrollRow - currentScrollRow if math.abs(delta) <= 0.001 then currentScrollRow = targetScrollRow animationTimer = nil else local step = delta * 0.35 if step > 0 then step = math.max(0.08, math.min(step, 0.45)) else step = math.min(-0.08, math.max(step, -0.45)) end if math.abs(step) >= math.abs(delta) then currentScrollRow = targetScrollRow animationTimer = nil else currentScrollRow = currentScrollRow + step animationTimer = os.startTimer(0.05) end end persistScrollState() renderGrid() if not animationTimer and pendingCacheSync then pendingCacheSync = false syncFromCache() renderGrid() end elseif event == "timer" then local handled, isDone = stepGlobalCountRefresh(p1) if handled and isDone then if animationTimer or math.abs(targetScrollRow - currentScrollRow) > 0.001 then pendingCacheSync = true else syncFromCache() renderGrid() end end elseif event == "monitor_touch" and p1 == monName then if options.handleChromeTouch and options.handleChromeTouch(x, y) then return elseif x >= scrollbarX and x < scrollbarX + scrollbarWidth then if y <= scrollbarButtonHeight then scrollTo(targetScrollRow - 1) elseif y > SCREEN_HEIGHT - scrollbarButtonHeight then scrollTo(targetScrollRow + 1) elseif scrollbarTrackHeight > 0 then local touchPixelY = cellToPixelY(y) + 1 local thumbY, thumbHeight = getThumbMetrics() if (touchPixelY < thumbY or touchPixelY > thumbY + thumbHeight - 1) and maxScrollRow > 0 then local thumbTravel = scrollbarTrackPixelHeight - thumbHeight local targetPixelY = math.max( scrollbarTrackPixelY, math.min(scrollbarTrackPixelY + thumbTravel, touchPixelY - math.floor(thumbHeight / 2)) ) local progress = (targetPixelY - scrollbarTrackPixelY) / math.max(1, thumbTravel) scrollTo(progress * maxScrollRow) end end else local col = math.floor((x - baseCellX) / colStepCells) local firstVisibleRow = math.floor(currentScrollRow) local rowOffsetPixels = (currentScrollRow - firstVisibleRow) * rowStepPixels local touchPixelY = cellToPixelY(y) + 1 local row = math.floor((touchPixelY - cellToPixelY(baseCellY) + rowOffsetPixels) / rowStepPixels) if col >= 0 and col < columns and row >= 0 and row < rowsPerView + 1 then local index = (firstVisibleRow + row) * columns + col + 1 if index >= 1 and index <= #entries and index <= firstVisibleRow * columns + visibleItemCount + columns then if options.onSelect(entries[index], index) then return end end end end end end end local function drawPage(base_id) local pageItems = getPageItems(base_id) local defaultIcon = getFallbackIcon() local backIcon = getBackButton() local backWidth, backHeight = imageSize(backIcon) local backX = 1 local pageItemIds = {} for i = 1, #pageItems do pageItemIds[i] = pageItems[i].id end runScrollableGrid({ refreshSeconds = globalCountRefreshInterval, getInitialEntries = function() return pageItems end, getInitialCounts = function() return getEquivalentLevelCounts(base_id, pageItems, getGlobalItemCounts()) end, getEntriesAndCountsFromCache = function(currentEntries) return currentEntries, getEquivalentLevelCounts(base_id, pageItems, getGlobalItemCounts()) end, getId = function(entry) return entry.id end, getIcon = function(entry) return getItemIcon(entry) or defaultIcon end, drawChrome = function() drawNfpScaled(frame.buffer, backIcon, cellToPixelX(backX), 1) end, handleChromeTouch = function(x, y) if x >= backX and x < backX + backWidth and y >= 1 and y <= backHeight then drawOverview() return true end return false end, onSelect = function() return false end, }) end drawOverview = function() local items = getBaseItemIds() local defaultIcon = getFallbackIcon() local function sortOverviewItems(itemCounts) local sortedItemIds = {} for i = 1, #items do sortedItemIds[i] = items[i] end table.sort(sortedItemIds, function(a, b) local countA = itemCounts[a] or 0 local countB = itemCounts[b] or 0 if countA ~= countB then return countA > countB end return a < b end) return sortedItemIds end runScrollableGrid({ refreshSeconds = globalCountRefreshInterval, getInitialEntries = function() if overviewCachedSortedItemIds and #overviewCachedSortedItemIds > 0 then return overviewCachedSortedItemIds end return sortOverviewItems(overviewCachedCounts) end, getInitialCounts = function() local counts = getGlobalItemCounts() if next(counts) ~= nil then overviewCachedCounts = getEquivalentBaseCounts(items, counts) end return overviewCachedCounts end, getInitialScrollState = function() return overviewScrollCurrentRow, overviewScrollTargetRow end, setScrollState = function(currentRow, targetRow) overviewScrollCurrentRow = currentRow overviewScrollTargetRow = targetRow end, getEntriesAndCountsFromCache = function() overviewCachedCounts = getEquivalentBaseCounts(items, getGlobalItemCounts()) overviewCachedSortedItemIds = sortOverviewItems(overviewCachedCounts) return overviewCachedSortedItemIds, overviewCachedCounts end, getId = function(entry) return entry end, getIcon = function(entry) return getItemIcon(getItemById(entry)) or defaultIcon end, onSelect = function(entry) drawPage(entry) return true end, }) end drawOverview() term.redirect(oldTerm)