Newer
Older
Kristify / installer.lua
@Erlend Erlend on 18 Jan 2023 15 KB fix: Issue in refunding on stock
-- Kristify Installer
-- Copyright © 2022-2023 Kristify - MIT License

if not http then
  error("This installer does not work without http API!")
end

print("Loading installer... Please wait...")
print("Checking for API key")
local authenticate = _G._GIT_API_KEY and { Authorization = "Bearer " .. _G._GIT_API_KEY }

print("(Down)Loading Basalt into memory")
local basaltDL = http.get("https://raw.githubusercontent.com/Kristify/kristify/main/src/libs/basalt.lua", authenticate)
assert(basaltDL, "Couldn't download Basalt!")
local basalt = load(basaltDL.readAll())()
basaltDL.close()
print("Loaded Basalt into memory")

-- Disk space
local nBarW = 0
local nLen = 0
local nStartPos = 0

-- Basic frame construction
local base = basalt.createFrame()
local title = base:addLabel()
    :setText("Kristify")
    :setPosition(2, 2)
    :setFontSize(2)

local page = base:addLabel()
    :setText("1/4")
    :setPosition("parent.w-self.w-1", 4)

base:addPane()
    :setPosition(2, 5)
    :setSize("parent.w-2", 1)
    :setBackground(false, '\140', colors.gray)

local content = base:addFrame()
    :setPosition(2, 6)
    :setSize("parent.w-2", "parent.h-5")
    :setBackground(colors.lightGray)

-- Manage keyboard input?
local blockInput = false
basalt.onEvent(function(event, value)
  if not blockInput then return end
  if event == "char"
      or (event:find("key") and value == keys.backspace or value == keys.enter) then
    return false
  end
end)

-- Create different slides
local welcome, licence, destination, install, done =
content:addFrame():setBackground(colors.lightGray):show(),
    content:addFrame():setBackground(colors.lightGray):hide(),
    content:addFrame():setBackground(colors.lightGray):hide(),
    content:addFrame():setBackground(colors.lightGray):hide(),
    content:addFrame():setBackground(colors.lightGray):hide()


--[[ Actuall installer ]]
local nRequired = 0

local tURLs = {}
tURLs.owner = "Kristify"
tURLs.repo = "kristify"
tURLs.branch = "main"
tURLs.tree = "https://api.github.com/repos/" .. tURLs.owner .. "/" .. tURLs.repo .. "/contents/?ref=" .. tURLs.branch
tURLs.infos = "https://api.github.com/repos/" .. tURLs.owner .. "/" .. tURLs.repo

local function httpError(response, err, errResponse)
  if not response then
    error("Request to GitHub denied; Reason: \'.." ..
      err .. "..\' (code " .. errResponse.getResponseCode() .. ").")
    return false
  end

  return true
end

local function getJSON(response)
  if not response then return {} end
  local tData = response.readAll()
  response.close()
  return textutils.unserialiseJSON(tData)
end

local function generateTree(sURL)
  sURL = sURL or tURLs.tree
  local function convertItem(item)
    if item.type == "file" then
      return item.name, item.download_url
    elseif item.type == "dir" then
      return item.name, generateTree(item.url)
    end
  end

  local response, sErr, errResponse = http.get(sURL, authenticate)
  httpError(response, sErr, errResponse)

  local tData = getJSON(response)
  local tTree = {}

  for _, v in pairs(tData) do
    local sName, tItem = convertItem(v)
    -- Filter stuff that is not needed
    if not
        (
        sName:sub(1, 1) == '.' or sName:find(".md") or sName == "installer.lua" or sName:find("basalt") or
            sName == "docs") then
      tTree[sName] = tItem
    end
  end

  return tTree
end

local function downloadBlob(sURL, sPath)
  local response, sErr, errResponse = http.get(sURL, authenticate)
  if not httpError(response, sErr, errResponse) then
    return false
  end

  local sData = response.readAll()
  response.close()

  local file = fs.open(sPath, 'w')
  file.write(sData)
  file.close()

  return true
end

-- Installs kristify
local pathToInstall = ""
local function installKristify(install)
  sleep(0.2)
  if pathToInstall == "" then pathToInstall = "/" end

  local status = install:getObject("_status")
  status:editLine(1, "Status: Create folder..")

  error = function(errMsg)
    status:editLine(2, "ERROR! " .. errMsg)
    return true
  end

  fs.delete(pathToInstall)
  fs.makeDir(pathToInstall)

  status:editLine(1, "Status: Generating tree..")
  status:addLine("")
  status:addLine(" \026 " .. (pathToInstall:sub(1, 1) == "/" and pathToInstall:sub(2) or pathToInstall))
  local tree = generateTree()

  local function listItems(tree, depth)
    local size = 0
    for _, _ in pairs(tree) do size = size + 1 end

    local index = 1
    for name, item in pairs(tree) do
      local begin = "   " ..
          ("\149 "):rep(depth - 1) .. (index >= size and '\141' or '\157'):rep(depth - (depth - 1)) ..
          ' '

      status:addLine(begin .. name)
      if type(item) == "table" then
        listItems(item, depth + 1)
      end
      index = index + 1
    end
  end

  listItems(tree, 1)

  status:editLine(1, "Status: Download files..")
  local index = 3
  local function nextStep(char)
    local line = status:getLine(index)
    if line then
      line = " " .. char .. " " .. line:sub(4)
      status:editLine(index, line)
    end

    index = index + 1
    line = status:getLine(index)
    if line then
      line = " \026 " .. line:sub(4)
      status:editLine(index, line)
    end
  end

  nextStep(' ')

  local function downloadItems(tree, sPath)
    sleep(0.3)
    for name, item in pairs(tree) do
      local nextPath = fs.combine(sPath, name)
      if type(item) == "table" then
        nextStep(' ')
        downloadItems(item, nextPath)
      else
        nextStep(downloadBlob(item, nextPath) and '\183' or '\019')
      end
    end
  end

  downloadItems(tree, pathToInstall)

  local response, sErr, errResponse = http.get("https://raw.githubusercontent.com/Kristify/Kristify/main/kristify.lua",
    authenticate)
  if not httpError(response, sErr, errResponse) then
    return false
  end

  local sData = response.readAll()
  response.close()

  sData = sData:gsub("local installation = \"kristify\"", "local installation = \"" .. pathToInstall .. "\"")

  local file = fs.open("/kristify.lua", 'w')
  file.write(sData)
  file.close()

  for _ = 1, 3 do
    status:addLine("")
  end

  -- Next page
  install:addButton()
      :setPosition("parent.w-9", "parent.h-1")
      :setSize(10, 1)
      :setText("Done")
      :setBackground(colors.green)
      :setForeground(colors.white)
      :onClick(function()
        install:hide()
        title:setText("Done")
        page:setText(" :)")
        done:show()
        blockInput = false
      end)
end

local function addFrame(frame, nX, nY, nW)
  frame:addLabel()
      :setPosition(nX, nY)
      :setBackground(colors.gray)
      :setForeground(colors.lightGray)
      :setText('\159')
  frame:addPane()
      :setPosition(nX, nY)
      :setSize(nX + nW, 1)
      :setBackground(colors.gray, '\143', colors.lightGray)
  frame:addLabel()
      :setPosition(nX + nW + 1, nY)
      :setBackground(colors.lightGray)
      :setForeground(colors.gray)
      :setText('\144')
  frame:addLabel()
      :setPosition(nX, nY + 1)
      :setBackground(colors.gray)
      :setForeground(colors.lightGray)
      :setText("\149")
  frame:addLabel()
      :setPosition(nX + nW + 1, nY + 1)
      :setBackground(colors.lightGray)
      :setForeground(colors.gray)
      :setText("\149")
  frame:addPane()
      :setPosition(nX, nY + 2)
      :setSize(nX + nW, 1)
      :setBackground(colors.lightGray, '\131', colors.gray)
  frame:addLabel()
      :setPosition(nX, nY + 2)
      :setBackground(colors.lightGray)
      :setForeground(colors.gray)
      :setText('\130')
  frame:addLabel()
      :setPosition(nX + nW + 1, nY + 2)
      :setBackground(colors.lightGray)
      :setForeground(colors.gray)
      :setText('\129')
end

--[[ Welcome ]]
welcome:addLabel()
    :setPosition(1, 1)
    :setSize("parent.w-2", "4")
    :setText("Welcome! Thank you for choosing this product! With Kristify you are able to set up a shop easily and comfortable!")
welcome:addLabel()
    :setPosition(1, 5)
    :setSize("parent.w-10", "3")
    :setText("To continue, click on \"Next\". To cancel, press CTRL+T at any time.")
-- Next page
welcome:addButton()
    :setPosition("parent.w-9", "parent.h-1")
    :setSize(10, 1)
    :setText("Next")
    :setBackground(colors.green)
    :setForeground(colors.white)
    :onClick(function()
      welcome:hide()
      title:setText("Licence")
      page:setText("2/4")
      licence:show()
      blockInput = true
    end)

--[[ Licence agreement ]]
local textfield = licence:addTextfield()
    :setPosition(1, 2)
    :setSize("parent.w", "parent.h-4")
    :setForeground(colors.white)
do
  local sKristifyLicence = http.get("https://raw.githubusercontent.com/Kristify/kristify/main/LICENSE")
  repeat
    local line = sKristifyLicence.readLine()
    if line then
      textfield:addLine(line)
    end
  until not line
  sKristifyLicence.close()
end
-- Checkbox frame
local nW, nH = licence:getSize()
addFrame(licence, 1, nH - 2, 1)
-- Checkbox label
licence:addLabel()
    :setPosition(4, "parent.h-1")
    :setText("I accept the agreement")
-- Instruction
licence:addLabel()
    :setPosition(1, 1)
    :setText("Scroll to read the licence agreement.")
-- Checkbox
local checkbox = licence:addCheckbox()
    :setPosition(2, "parent.h-1")
    :setBackground(colors.white)
    :setSymbol('x')
-- Next page
local nextBtn = licence:addButton()
    :setPosition("parent.w-9", "parent.h-1")
    :setSize(10, 1)
    :setText("Next")
    :setBackground(colors.gray)
    :setForeground(colors.black)
    :onClick(function()
      if checkbox:getValue() then
        licence:hide()
        title:setText("Destination")
        page:setText("3/4")
        destination:show()
        blockInput = false

        local note = destination:getObject("_note")
        error = function(err)
          note:setText(err)
          destination:removeObject("_next")
        end

        local response, sErr, errResponse = http.get(tURLs.infos, authenticate)
        httpError(response, sErr, errResponse)

        local tData = getJSON(response)
        nRequired = math.floor(tData.size - (#basaltFile / 1000) / 100 * 70) or 0
        if nRequired <= 0 then
          destination:removeObject("_next")
          note:setText("Something went wrong! Reason: \'" .. sErr .. "\' (" .. errResponse.getResponseCode() .. ')')
        end
        -- Required space
        local nRequiredLen = (nRequired * 1000) / fs.getCapacity('/') * 100
        destination:addFrame()
            :setPosition(nStartPos, 6)
            :setSize(nRequiredLen, 1)
            :setBackground(colors.lightGray)
            :addPane()
            :setPosition(1, 1)
            :setSize("parent.w", 1)
            :setBackground(false, '\140', colors.lime)
        destination:addLabel()
            :setPosition(nStartPos + 1, 9)
            :setText("\024Required")
        if nRequiredLen + nLen >= 65 then
          destination:removeObject("_freeLabel")
        end
        if nRequiredLen + nLen >= 95 then
          destination:removeObject("_next")
          note:setText("Not enough space! At least ~" .. nRequired .. "kb must be free.")
        end

        -- Cooldown
        if destination:getObject("_next") then
          destination:addThread()
              :start(function()
                local btn = destination:getObject("_next")
                for i = 3, 1, -1 do
                  btn:setText(tostring(i))
                  sleep(0.8)
                end
                btn:setText("install")
                    :setBackground(colors.green)
                    :setForeground(colors.white)
              end)
        end
      end
    end)
-- Checkbox
checkbox:onChange(function(self)
  if self:getValue() then
    nextBtn
        :setBackground(colors.green)
        :setForeground(colors.white)
  else
    nextBtn
        :setBackground(colors.gray)
        :setForeground(colors.black)
  end
end)

--[[ Destination ]]
local nW = destination:getSize()
destination:addLabel()
    :setPosition(1, 1)
    :setText("Select the installation location:")
-- Input frame
addFrame(destination, 2, 2, nW - 4)
-- Input
local input = destination:addInput()
    :setPosition(3, 3)
    :setSize("parent.w-4")
    :setBackground(colors.white)
    :setInputType("text")
    :setDefaultText("/")
    :setValue("/kristify")
-- (from/to)
destination:addLabel()
    :setPosition(1, 6)
    :setText("0b")
local maxMb = destination:addLabel()
    :setPosition("parent.w-self.w+1", 6)
    :setText(math.floor(fs.getCapacity('/') / 1000000) .. "mb")
-- Disk Space
local spaceLeft = destination:addProgressbar()
    :setPosition(3, 6)
    :setSize(nW - (2 + maxMb:getSize()), 1)
    :setDirection(0)
    :setProgressBar(colors.lightGray, "\140", colors.green)
    :setBackground(colors.lightGray)
    :setForeground(colors.gray)
    :setBackgroundSymbol('\140')
    :setProgress((fs.getCapacity('/') - fs.getFreeSpace('/')) / fs.getCapacity('/') * 100 + 1)
-- Disk space labels
nBarW = spaceLeft:getSize()
nLen = math.floor(spaceLeft:getProgress() - 0.9)
nStartPos = 3 + (nBarW / 100 * nLen)
destination:addLabel()
    :setPosition(nStartPos - 1 + (nLen <= 1 and 1 or 0), 5)
    :setForeground(colors.gray)
    :setText("\025Used")
destination:addLabel("_freeLabel")
    :setPosition("parent.w-8", 5)
    :setForeground(colors.gray)
    :setText("Free\025")
-- Note
destination:addLabel("_note")
    :setPosition(1, "parent.h-1")
    :setSize("parent.w-10", 3)
    :setForeground(colors.red)
    :setText("\026Note: The choosen folder will be completely erased during install!")
-- Next page
destination:addButton("_next")
    :setPosition("parent.w-8", "parent.h-1")
    :setSize(9, 1)
    :setText("")
    :setBackground(colors.gray)
    :setForeground(colors.black)
    :onClick(function(self)
      if self:getBackground() ~= colors.gray then
        destination:hide()
        title:setText("Installing")
        page:setText("4/4")
        install:show()
        blockInput = true

        pathToInstall = input:getValue()

        -- Install script
        destination:addThread()
            :start(function()
              installKristify(install)
            end)
      end
    end)

--[[ Install ]]
install:addLabel()
    :setPosition(1, 1)
    :setSize("parent.w", 1)
    :setText("Take a seat and wait until the magic happened!")
install:addTextfield("_status")
    :setPosition(1, 2)
    :setSize("parent.w", "parent.h-2")
    :setForeground(colors.white)
    :addKeywords(colors.red, { "ERROR!" })
    :addRule("[\149\157\141]", colors.lightGray)
    :addRule("\183", colors.green)
    :addRule("\019", colors.red)

--[[ Done ]]
done:addLabel()
    :setPosition(1, 1)
    :setText("And thats it! You now own a shop! ")
-- Checkbox label
local _, nH = done:getSize()
addFrame(done, 1, nH - 2, 1)
done:addLabel()
    :setPosition(4, "parent.h-1")
    :setText("Enable start on boot")
-- Checkbox
local checkbox = done:addCheckbox()
    :setPosition(2, "parent.h-1")
    :setBackground(colors.white)
    :setSymbol('x')
    :setValue(true)

-- Exit
done:addButton()
    :setPosition("parent.w-9", "parent.h-1")
    :setSize(10, 1)
    :setText("Exit")
    :setBackground(colors.green)
    :setForeground(colors.white)
    :onClick(function()
      local script = "shell.run(\"kristify.lua\")"
      local file = "startKristify.lua"
      if checkbox:getValue() then
        if fs.exists("/startup.lua") then
          file = fs.open("/startup.lua", 'a')
        else
          fs.makeDir("startup")
          file = fs.open(fs.combine("startup", "kristify.lua"), 'w')
        end
        file.write(script)
        file.close()
      end

      basalt.stopUpdate()
    end)

basalt.autoUpdate()
return pathToInstall