local apiPath = "https://pinestore.cc/api/" if not fs.exists("installed") then fs.makeDir("installed") end local width, height = term.getSize() local renderWindow = window.create(term.current(), 1, 1, width, height) local oldTerm = term.redirect(renderWindow) local maxWidth = 70 local function updateTermSize() width, height = oldTerm.getSize() local newW = math.min(width, maxWidth) renderWindow.reposition(1 + math.floor((width - newW)*0.5 + 0.5), 1, newW, height) oldTerm.setBackgroundColor(colors.black) oldTerm.clear() end updateTermSize() local online = true local categories = { "fun", "tools", "turtle", "audio", "other", } local selectedCategory = 1 local selectedProject = 1 local projectActionsOpened = false local projectActionSelected = 1 local searchOpened = false local searchQuery = "" local searchResultsOpened = false local function getAPI(path) local res = http.get(apiPath .. path) if not res then online = false return end local data = res.readAll() res.close() return textutils.unserialiseJSON(data) end function postAPI(path, body) local res = http.post(apiPath .. path, textutils.serialiseJSON(body), {["Content-Type"] = "application/json"}) if not res then return end local data = res.readAll() res.close() return textutils.unserialiseJSON(data) end local installedInfo = { projects = {}, } if fs.exists("installed.json") then local h = fs.open("installed.json", "r") installedInfo = textutils.unserialiseJSON(h.readAll()) h.close() end local function saveInstalled() local encoded = textutils.serialiseJSON(installedInfo) local h = fs.open("installed.json", "w") h.write(encoded) h.close() end local searchResults = {} local projectsData = getAPI("projects") local projects = projectsData.projects if not projects then local ps = {} for id, project in pairs(installedInfo.projects) do ps[#ps+1] = project end projects = ps else for i = 1, #projects do local project = projects[i] if installedInfo.projects[tostring(project.id)] then installedInfo.projects[tostring(project.id)].downloads = project.downloads end end saveInstalled() end for i = #projects, 1, -1 do local project = projects[i] if not project.install_command or not project.target_file then table.remove(projects, i) end end local categoryProjects = {} for i = 1, #projects do local project = projects[i] if not project.category then project.category = "Other" end if not categoryProjects[project.category] then categoryProjects[project.category] = {} end categoryProjects[project.category][#categoryProjects[project.category]+1] = project end for category, projectList in pairs(categoryProjects) do table.sort(projectList, function(a, b) return b.downloads < a.downloads end) end local function installProject(project) -- redirect term to old one term.redirect(oldTerm) -- override fs methods local projectPath = "installed/" .. project.id .. "/" fs.makeDir(projectPath) local oldFSOpen = fs.open local oldFSMakeDir = fs.makeDir local oldFSExists = fs.exists function fs.open(path, mode) -- print("open " .. path) -- sleep(0.5) if path:sub(1, 12) == "rom/programs" then return oldFSOpen(path, mode) end return oldFSOpen(projectPath .. path, mode) end function fs.makeDir(path) return oldFSMakeDir(projectPath .. path) end function fs.exists(path) if path:sub(1, 12) == "rom/programs" then return oldFSExists(path) end return oldFSExists(projectPath .. path) end -- actually run the install command local success, res = xpcall(shell.run, debug.traceback, project.install_command) -- return old fs methods fs.open = oldFSOpen fs.makeDir = oldFSMakeDir fs.exists = oldFSExists -- use render window again oldTerm = term.redirect(renderWindow) updateTermSize() if success then -- set project info to installed installedInfo.projects[tostring(project.id)] = project saveInstalled() postAPI("newdownload", {projectId = project.id}) else error(res) end end local function startProject(project) -- override fs methods local projectPath = "installed/" .. project.id .. "/" local oldFSOpen = fs.open local oldFSMakeDir = fs.makeDir local oldFSExists = fs.exists local oldFSList = fs.list function fs.open(path, mode) if path:sub(1, 12) == "rom/programs" then return oldFSOpen(path, mode) end return oldFSOpen(projectPath .. path, mode) end function fs.makeDir(path) return oldFSMakeDir(projectPath .. path) end function fs.exists(path) return oldFSExists(projectPath .. path) end function fs.list(path) return oldFSList(projectPath .. path) end term.redirect(oldTerm) local success, res = xpcall(function() local success = shell.run(project.target_file) if not success then sleep(1) term.setTextColor(colors.white) print("\nPress any key to continue...") os.pullEvent("key") end end, debug.traceback) -- return old fs methods fs.open = oldFSOpen fs.makeDir = oldFSMakeDir fs.exists = oldFSExists fs.list = oldFSList if not success then if res:sub(1, 10) ~= "Terminated" then term.setBackgroundColor(colors.black) term.setTextColor(colors.red) term.clear() term.setCursorPos(1, 1) print(res) term.setTextColor(colors.white) sleep(1) print("\nPress any key to continue...") os.pullEvent("key") end end term.redirect(renderWindow) updateTermSize() end local function runSearch() local searchData = getAPI("search?q=" .. textutils.urlEncode(searchQuery)) searchResults = searchData.projects searchOpened = false searchResultsOpened = true selectedProject = 1 searchQuery = "" end local function drawCategories() term.setCursorPos(1, 2) for nr = 1, #categories do if nr == selectedCategory then term.setTextColor(colors.lime) else term.setTextColor(colors.green) end local str = "[" .. nr .. " " .. categories[nr] .. "] " local x, y = term.getCursorPos() if x + #str - 2 > width then print("") end term.write(str) end end local function drawProjects(projects) print("") local startX, startY = term.getCursorPos() local width, height = term.getSize() local linesAvailable = height - startY - 1 local startI = math.max(1, selectedProject - linesAvailable + 1) for i = startI, math.min(startI + linesAvailable, #projects) do if i == selectedProject then term.setTextColor(colors.yellow) term.write("> ") else term.write(" ") end local project = projects[i] if installedInfo.projects[tostring(project.id)] then term.setTextColor(colors.lightBlue) else term.setTextColor(colors.white) end term.write(project.name) term.setTextColor(colors.lightGray) term.write(" by " .. project.owner_name) local showDownloads = true local downText = " downloads" if project.downloads == 1 then downText = " download " end if width <= 25 then showDownloads = false elseif width <= 29 then downText = "" elseif width <= 42 then downText = " dls" if project.downloads == 1 then downText = " dl " end end if showDownloads then local downloadsText = project.downloads .. downText local x, y = term.getCursorPos() term.setCursorPos(width - #downloadsText + 1, y) print(downloadsText) else print("") end end end local function drawProjectOptions() print("") local projects = categoryProjects[categories[selectedCategory]] or {} if searchResultsOpened then projects = searchResults end local project = projects[selectedProject] local installed = installedInfo.projects[tostring(project.id)] for i = 1, installed and 3 or 2 do if i == projectActionSelected then term.setTextColor(colors.yellow) term.write("> ") else term.write(" ") end term.setTextColor(colors.white) if installed then if i == 1 then if project.version > installed.version then print("Update and run") else print("Run") end elseif i == 2 then print("Uninstall") elseif i == 3 then print("Back") end else if i == 1 then print("Install") elseif i == 2 then print("Back") end end end print("") term.setTextColor(colors.white) term.write(project.name) term.setTextColor(colors.lightGray) print(" by " .. project.owner_name) term.setTextColor(colors.gray) print("v" .. project.version .. " " .. os.date("%B %d, %Y", math.max(project.date_added, project.date_updated) / 1000)) print("") term.setTextColor(colors.lime) local stoppedScroll = false local scrollOld = term.scroll function term.scroll() stoppedScroll = true end print(project.description) term.scroll = scrollOld if stoppedScroll then local width, height = term.getSize() term.setCursorPos(1, height) term.clearLine() end end local function selectProjectAction() local projects = categoryProjects[categories[selectedCategory]] or {} if searchResultsOpened then projects = searchResults end local project = projects[selectedProject] local installed = installedInfo.projects[tostring(project.id)] local i = projectActionSelected if installed then if i == 1 then if project.version > installed.version then installProject(project) -- run install again to update end startProject(project) projectActionsOpened = false elseif i == 2 then fs.delete("installed/" .. project.id) installedInfo.projects[tostring(project.id)] = nil saveInstalled() projectActionsOpened = false elseif i == 3 then projectActionsOpened = false end else if i == 1 then installProject(project) elseif i == 2 then projectActionsOpened = false end end end local function drawSearch() term.setTextColor(colors.lightGray) print("\n\nSearch query:") term.setTextColor(colors.white) local lineCount = math.ceil(#searchQuery / width) for i = 1, lineCount do term.write(searchQuery:sub(1 + width*(i-1), width + width*(i-1))) if i < lineCount then local x, y = term.getCursorPos() term.setCursorPos(1, y+1) end end term.setCursorBlink(true) end local function drawSearchResults() term.setTextColor(colors.white) print("\n\nSearch results:") drawProjects(searchResults) end local function drawMain() term.setBackgroundColor(colors.black) term.clear() term.setTextColor(colors.yellow) term.setCursorPos(1, 1) local width, height = term.getSize() if width > 37 then term.write("PineStore Console v1.0") elseif width > 31 then term.write("PineStore Console") elseif width > 25 then term.write("PineStore C.") else term.write("PStore C.") end if not online then term.setTextColor(colors.orange) term.write(" offline mode") end term.setCursorBlink(false) if searchOpened then local searchText = "[TAB close]" term.setCursorPos(width - #searchText + 1, 1) term.write(searchText) drawSearch() elseif projectActionsOpened then drawProjectOptions() elseif searchResultsOpened then drawSearchResults() else if online then local searchText = "[TAB search]" term.setCursorPos(width - #searchText + 1, 1) term.write(searchText) end drawCategories() local projects = categoryProjects[categories[selectedCategory]] or {} drawProjects(projects) end renderWindow.setVisible(true) renderWindow.setVisible(false) end function runStore() while true do drawMain() -- if searchOpened then -- local searchText = "[TAB close]" -- term.setCursorPos(width - #searchText, 1) -- term.write(searchText) -- drawSearch() -- elseif searchResultsOpened then -- drawSearchResults() -- elseif projectActionsOpened then -- drawProjectOptions() -- else local event, key, x, y = os.pullEvent() if event == "key" then if searchOpened then if key == keys.tab then searchOpened = false elseif key == keys.enter then runSearch() elseif key == keys.backspace then searchQuery = searchQuery:sub(1, -2) end elseif projectActionsOpened then if key == keys.up or key == keys.w then projectActionSelected = math.max(1, projectActionSelected - 1) elseif key == keys.down or key == keys.s then local projects = categoryProjects[categories[selectedCategory]] or {} if searchResultsOpened then projects = searchResults end local project = projects[selectedProject] local installed = installedInfo.projects[tostring(project.id)] projectActionSelected = math.max(1, math.min(installed and 3 or 2, projectActionSelected + 1)) elseif key == keys.backspace or key == keys.grave then projectActionsOpened = false elseif key == keys.enter or key == keys.space then selectProjectAction() end elseif searchResultsOpened then if key == keys.up or key == keys.w then selectedProject = math.max(1, selectedProject - 1) elseif key == keys.down or key == keys.s then selectedProject = math.max(1, math.min(#searchResults, selectedProject + 1)) elseif key == keys.enter or key == keys.space then if #searchResults > 0 then projectActionsOpened = true projectActionSelected = 1 end elseif key == keys.backspace or key == keys.grave or key == keys.tab then searchResultsOpened = false end else if key >= keys.one and key <= keys.nine then selectedCategory = math.max(1, math.min(#categories, key - keys.one + 1)) elseif key == keys.left or key == keys.a then selectedCategory = math.max(1, selectedCategory - 1) elseif key == keys.right or key == keys.d then selectedCategory = math.max(1, math.min(#categories, selectedCategory + 1)) elseif key == keys.up or key == keys.w then selectedProject = math.max(1, selectedProject - 1) elseif key == keys.down or key == keys.s then local projects = categoryProjects[categories[selectedCategory]] or {} selectedProject = math.max(1, math.min(#projects, selectedProject + 1)) elseif key == keys.enter or key == keys.space then projectActionsOpened = true projectActionSelected = 1 elseif key == keys.tab and online then searchOpened = true end end elseif event == "char" then if key ~= "`" then searchQuery = searchQuery .. key end elseif event == "term_resize" then updateTermSize() end end end local success, res = xpcall(runStore, debug.traceback) term.redirect(oldTerm) if not success then if type(res) == "string" and res:sub(1, 10) == "Terminated" then return end term.setBackgroundColor(colors.black) term.setTextColor(colors.red) term.clear() term.setCursorPos(1, 1) print(res) term.setTextColor(colors.white) end