MineOS/Installer/Main.lua
2025-02-10 09:49:54 -05:00

686 lines
19 KiB
Lua

-- Checking for required components
local function getComponentAddress(name)
return component.list(name)() or error("Required " .. name .. " component is missing")
end
local EEPROMAddress, internetAddress, GPUAddress =
getComponentAddress("eeprom"),
getComponentAddress("internet"),
getComponentAddress("gpu")
-- Binding GPU to screen in case it's not done yet
component.invoke(GPUAddress, "bind", getComponentAddress("screen"))
local screenWidth, screenHeight = component.invoke(GPUAddress, "getResolution")
local repositoryURL = "https://raw.githubusercontent.com/IgorTimofeev/MineOS/master/"
local installerURL = "Installer/"
local EFIURL = "EFI/Minified.lua"
local installerPath = "/MineOS installer/"
local installerPicturesPath = installerPath .. "Installer/Pictures/"
local OSPath = "/"
local temporaryFilesystemProxy, selectedFilesystemProxy
--------------------------------------------------------------------------------
-- Working with components directly before system libraries are downloaded & initialized
local function centrize(width)
return math.floor(screenWidth / 2 - width / 2)
end
local function centrizedText(y, color, text)
component.invoke(GPUAddress, "fill", 1, y, screenWidth, 1, " ")
component.invoke(GPUAddress, "setForeground", color)
component.invoke(GPUAddress, "set", centrize(#text), y, text)
end
local function title()
local y = math.floor(screenHeight / 2 - 1)
centrizedText(y, 0x2D2D2D, "MineOS")
return y + 2
end
local function progress(value)
local width = 26
local x, y, part = centrize(width), title(), math.ceil(width * value)
component.invoke(GPUAddress, "setForeground", 0x878787)
component.invoke(GPUAddress, "set", x, y, string.rep("", part))
component.invoke(GPUAddress, "setForeground", 0xC3C3C3)
component.invoke(GPUAddress, "set", x + part, y, string.rep("", width - part))
end
local function filesystemPath(path)
return path:match("^(.+%/).") or ""
end
local function filesystemName(path)
return path:match("%/?([^%/]+%/?)$")
end
local function filesystemHideExtension(path)
return path:match("(.+)%..+") or path
end
local function rawRequest(url, chunkHandler)
local internetHandle, reason = component.invoke(internetAddress, "request", repositoryURL .. url:gsub("([^%w%-%_%.%~])", function(char)
return string.format("%%%02X", string.byte(char))
end))
if internetHandle then
local chunk, reason
while true do
chunk, reason = internetHandle.read(math.huge)
if chunk then
chunkHandler(chunk)
else
if reason then
error("Internet request failed: " .. tostring(reason))
end
break
end
end
internetHandle.close()
else
error("Connection failed: " .. url)
end
end
local function request(url)
local data = ""
rawRequest(url, function(chunk)
data = data .. chunk
end)
return data
end
local function download(url, path)
selectedFilesystemProxy.makeDirectory(filesystemPath(path))
local fileHandle, reason = selectedFilesystemProxy.open(path, "wb")
if fileHandle then
rawRequest(url, function(chunk)
selectedFilesystemProxy.write(fileHandle, chunk)
end)
selectedFilesystemProxy.close(fileHandle)
else
error("File opening failed: " .. tostring(reason))
end
end
local function deserialize(text)
local result, reason = load("return " .. text, "=string")
if result then
return result()
else
error(reason)
end
end
-- Clearing screen
component.invoke(GPUAddress, "setBackground", 0xE1E1E1)
component.invoke(GPUAddress, "fill", 1, 1, screenWidth, screenHeight, " ")
-- Checking minimum system requirements
do
local function warning(text)
centrizedText(title(), 0x878787, text)
local signal
repeat
signal = computer.pullSignal()
until signal == "key_down" or signal == "touch"
computer.shutdown()
end
if component.invoke(GPUAddress, "getDepth") ~= 8 then
warning("Tier 3 GPU and screen are required")
end
if computer.totalMemory() < 1024 * 1024 * 2 then
warning("At least 2x Tier 3.5 RAM modules are required")
end
-- Searching for appropriate temporary filesystem for storing libraries, images, etc
for address in component.list("filesystem") do
local proxy = component.proxy(address)
if proxy.spaceTotal() >= 2 * 1024 * 1024 then
temporaryFilesystemProxy, selectedFilesystemProxy = proxy, proxy
break
end
end
-- If there's no suitable HDDs found - then meow
if not temporaryFilesystemProxy then
warning("At least Tier 2 HDD is required")
end
end
-- First, we need a big ass file list with localizations, applications, wallpapers
progress(0)
local files = deserialize(request(installerURL .. "Files.cfg"))
-- After that we could download required libraries for installer from it
for i = 1, #files.installerFiles do
progress(i / #files.installerFiles)
download(files.installerFiles[i], installerPath .. files.installerFiles[i])
end
-- Initializing simple package system for loading system libraries
package = {loading = {}, loaded = {}}
function require(module)
if package.loaded[module] then
return package.loaded[module]
elseif package.loading[module] then
error("already loading " .. module .. ": " .. debug.traceback())
else
package.loading[module] = true
local handle, reason = temporaryFilesystemProxy.open(installerPath .. "Libraries/" .. module .. ".lua", "rb")
if handle then
local data, chunk = ""
repeat
chunk = temporaryFilesystemProxy.read(handle, math.huge)
data = data .. (chunk or "")
until not chunk
temporaryFilesystemProxy.close(handle)
local result, reason = load(data, "=" .. module)
if result then
package.loaded[module] = result() or true
else
error(reason)
end
else
error("File opening failed: " .. tostring(reason))
end
package.loading[module] = nil
return package.loaded[module]
end
end
-- Initializing system libraries
local filesystem = require("Filesystem")
filesystem.setProxy(temporaryFilesystemProxy)
bit32 = bit32 or require("Bit32")
local image = require("Image")
local text = require("Text")
local number = require("Number")
local screen = require("Screen")
screen.setGPUAddress(GPUAddress)
local GUI = require("GUI")
local system = require("System")
local paths = require("Paths")
--------------------------------------------------------------------------------
-- Creating main UI workspace
local workspace = GUI.workspace()
workspace:addChild(GUI.panel(1, 1, workspace.width, workspace.height, 0x1E1E1E))
-- Main installer window
local window = workspace:addChild(GUI.window(1, 1, 80, 24))
window.localX, window.localY = math.ceil(workspace.width / 2 - window.width / 2), math.ceil(workspace.height / 2 - window.height / 2)
window:addChild(GUI.panel(1, 1, window.width, window.height, 0xE1E1E1))
-- Top menu
local menu = workspace:addChild(GUI.menu(1, 1, workspace.width, 0xF0F0F0, 0x787878, 0x3366CC, 0xE1E1E1))
local installerMenu = menu:addContextMenuItem("MineOS", 0x2D2D2D)
installerMenu:addItem("🗘", "Reboot").onTouch = function()
computer.shutdown(true)
end
installerMenu:addItem("", "Shutdown").onTouch = function()
computer.shutdown()
end
-- Main vertical layout
local layout = window:addChild(GUI.layout(1, 1, window.width, window.height - 2, 1, 1))
local stageButtonsLayout = window:addChild(GUI.layout(1, window.height - 1, window.width, 1, 1, 1))
stageButtonsLayout:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)
stageButtonsLayout:setSpacing(1, 1, 3)
local function loadImage(name)
return image.load(installerPicturesPath .. name .. ".pic")
end
local function newInput(width, ...)
return GUI.input(1, 1, width, 1, 0xF0F0F0, 0x787878, 0xC3C3C3, 0xF0F0F0, 0x878787, "", ...)
end
local function newSwitchAndLabel(width, color, text, state)
return GUI.switchAndLabel(1, 1, width, 6, color, 0xD2D2D2, 0xF0F0F0, 0xA5A5A5, text .. ":", state)
end
local function addTitle(color, text)
return layout:addChild(GUI.text(1, 1, color, text))
end
local function addImage(before, after, name)
if before > 0 then
layout:addChild(GUI.object(1, 1, 1, before))
end
local picture = layout:addChild(GUI.image(1, 1, loadImage(name)))
picture.height = picture.height + after
return picture
end
local function addStageButton(text)
local button = stageButtonsLayout:addChild(GUI.adaptiveRoundedButton(1, 1, 2, 0, 0xC3C3C3, 0x878787, 0xA5A5A5, 0x696969, text))
button.colors.disabled.background = 0xD2D2D2
button.colors.disabled.text = 0xB4B4B4
return button
end
local prevButton = addStageButton("<")
local nextButton = addStageButton(">")
local localization
local stage = 1
local stages = {}
local usernameInput = newInput(30, "")
local passwordInput = newInput(30, "", false, "")
local passwordSubmitInput = newInput(30, "", false, "")
local usernamePasswordText = GUI.text(1, 1, 0xCC0040, "")
local withoutPasswordSwitchAndLabel = newSwitchAndLabel(30, 0x66DB80, "", false)
local wallpapersSwitchAndLabel = newSwitchAndLabel(30, 0xFF4980, "", true)
local applicationsSwitchAndLabel = newSwitchAndLabel(30, 0x33DB80, "", true)
local localizationsSwitchAndLabel = newSwitchAndLabel(30, 0x33B6FF, "", true)
local acceptSwitchAndLabel = newSwitchAndLabel(30, 0x9949FF, "", false)
local localizationComboBox = GUI.comboBox(1, 1, 26, 1, 0xF0F0F0, 0x969696, 0xD2D2D2, 0xB4B4B4)
for i = 1, #files.localizations do
localizationComboBox:addItem(filesystemHideExtension(filesystemName(files.localizations[i]))).onTouch = function()
-- Obtaining localization table
localization = deserialize(request(installerURL .. files.localizations[i]))
-- Filling widgets with selected localization data
usernameInput.placeholderText = localization.username
passwordInput.placeholderText = localization.password
passwordSubmitInput.placeholderText = localization.submitPassword
withoutPasswordSwitchAndLabel.label.text = localization.withoutPassword
wallpapersSwitchAndLabel.label.text = localization.wallpapers
applicationsSwitchAndLabel.label.text = localization.applications
localizationsSwitchAndLabel.label.text = localization.languages
acceptSwitchAndLabel.label.text = localization.accept
end
end
local function addStage(onTouch)
table.insert(stages, function()
layout:removeChildren()
onTouch()
workspace:draw()
end)
end
local function loadStage()
if stage < 1 then
stage = 1
elseif stage > #stages then
stage = #stages
end
stages[stage]()
end
local function checkUserInputs()
local nameEmpty = #usernameInput.text == 0
local nameVaild = usernameInput.text:match("^%w[%w%s_]+$")
local passValid = withoutPasswordSwitchAndLabel.switch.state or (#passwordInput.text > 0 and #passwordSubmitInput.text > 0 and passwordInput.text == passwordSubmitInput.text)
if (nameEmpty or nameVaild) and passValid then
usernamePasswordText.hidden = true
nextButton.disabled = nameEmpty or not nameVaild or not passValid
else
usernamePasswordText.hidden = false
nextButton.disabled = true
if nameVaild then
usernamePasswordText.text = localization.passwordsArentEqual
else
usernamePasswordText.text = localization.usernameInvalid
end
end
end
local function checkLicense()
nextButton.disabled = not acceptSwitchAndLabel.switch.state
end
prevButton.onTouch = function()
stage = stage - 1
loadStage()
end
nextButton.onTouch = function()
stage = stage + 1
loadStage()
end
acceptSwitchAndLabel.switch.onStateChanged = function()
checkLicense()
workspace:draw()
end
withoutPasswordSwitchAndLabel.switch.onStateChanged = function()
passwordInput.hidden = withoutPasswordSwitchAndLabel.switch.state
passwordSubmitInput.hidden = withoutPasswordSwitchAndLabel.switch.state
checkUserInputs()
workspace:draw()
end
usernameInput.onInputFinished = function()
checkUserInputs()
workspace:draw()
end
passwordInput.onInputFinished = usernameInput.onInputFinished
passwordSubmitInput.onInputFinished = usernameInput.onInputFinished
-- Localization selection stage
addStage(function()
prevButton.disabled = true
addImage(0, 1, "Languages")
layout:addChild(localizationComboBox)
workspace:draw()
localizationComboBox:getItem(1).onTouch()
end)
-- Filesystem selection stage
addStage(function()
prevButton.disabled = false
nextButton.disabled = false
layout:addChild(GUI.object(1, 1, 1, 1))
addTitle(0x696969, localization.select)
local diskLayout = layout:addChild(GUI.layout(1, 1, layout.width, 11, 1, 1))
diskLayout:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)
diskLayout:setSpacing(1, 1, 1)
local HDDImage = loadImage("HDD")
local function select(proxy)
selectedFilesystemProxy = proxy
for i = 1, #diskLayout.children do
diskLayout.children[i].children[1].hidden = diskLayout.children[i].proxy ~= selectedFilesystemProxy
end
end
local function updateDisks()
local function diskEventHandler(workspace, disk, e1)
if e1 == "touch" then
select(disk.proxy)
workspace:draw()
end
end
local function addDisk(proxy, picture, disabled)
local disk = diskLayout:addChild(GUI.container(1, 1, 14, diskLayout.height))
disk.blockScreenEvents = true
disk:addChild(GUI.panel(1, 1, disk.width, disk.height, 0xD2D2D2))
disk:addChild(GUI.button(1, disk.height, disk.width, 1, 0xCC4940, 0xE1E1E1, 0x990000, 0xE1E1E1, localization.erase)).onTouch = function()
local list, path = proxy.list("/")
for i = 1, #list do
path = "/" .. list[i]
if proxy.address ~= temporaryFilesystemProxy.address or path ~= installerPath then
proxy.remove(path)
end
end
updateDisks()
end
if disabled then
picture = image.blend(picture, 0xFFFFFF, 0.4)
disk.disabled = true
end
disk:addChild(GUI.image(4, 2, picture))
disk:addChild(GUI.label(2, 7, disk.width - 2, 1, disabled and 0x969696 or 0x696969, text.limit(proxy.getLabel() or proxy.address, disk.width - 2))):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
disk:addChild(GUI.progressBar(2, 8, disk.width - 2, disabled and 0xCCDBFF or 0x66B6FF, disabled and 0xD2D2D2 or 0xC3C3C3, disabled and 0xC3C3C3 or 0xA5A5A5, math.floor(proxy.spaceUsed() / proxy.spaceTotal() * 100), true, true, "", "% " .. localization.used))
disk.eventHandler = diskEventHandler
disk.proxy = proxy
end
diskLayout:removeChildren()
for address in component.list("filesystem") do
local proxy = component.proxy(address)
if proxy.spaceTotal() >= 1 * 1024 * 1024 then
addDisk(
proxy,
proxy.spaceTotal() < 1 * 1024 * 1024 and floppyImage or HDDImage,
proxy.isReadOnly() or proxy.spaceTotal() < 2 * 1024 * 1024
)
end
end
select(selectedFilesystemProxy)
end
updateDisks()
end)
-- User profile setup stage
addStage(function()
checkUserInputs()
addImage(0, 0, "User")
addTitle(0x696969, localization.setup)
layout:addChild(usernameInput)
layout:addChild(passwordInput)
layout:addChild(passwordSubmitInput)
layout:addChild(usernamePasswordText)
layout:addChild(withoutPasswordSwitchAndLabel)
end)
-- Downloads customization stage
addStage(function()
nextButton.disabled = false
addImage(0, 0, "Settings")
addTitle(0x696969, localization.customize)
layout:addChild(wallpapersSwitchAndLabel)
layout:addChild(applicationsSwitchAndLabel)
layout:addChild(localizationsSwitchAndLabel)
end)
-- License acception stage
addStage(function()
checkLicense()
local lines = text.wrap({request("LICENSE")}, layout.width - 2)
local textBox = layout:addChild(GUI.textBox(1, 1, layout.width, layout.height - 3, 0xF0F0F0, 0x696969, lines, 1, 1, 1))
layout:addChild(acceptSwitchAndLabel)
end)
-- Downloading stage
addStage(function()
stageButtonsLayout:removeChildren()
-- Creating user profile
layout:removeChildren()
addImage(1, 1, "User")
addTitle(0x969696, localization.creating)
workspace:draw()
-- Renaming if possible
if not selectedFilesystemProxy.getLabel() then
selectedFilesystemProxy.setLabel("MineOS HDD")
end
local function switchProxy(runnable)
filesystem.setProxy(selectedFilesystemProxy)
runnable()
filesystem.setProxy(temporaryFilesystemProxy)
end
-- Creating system paths
local userSettings, userPaths
switchProxy(function()
paths.create(paths.system)
userSettings, userPaths = system.createUser(
usernameInput.text,
localizationComboBox:getItem(localizationComboBox.selectedItem).text,
not withoutPasswordSwitchAndLabel.switch.state and passwordInput.text or nil,
wallpapersSwitchAndLabel.switch.state
)
end)
-- Downloading files
layout:removeChildren()
addImage(3, 2, "Downloading")
local container = layout:addChild(GUI.container(1, 1, layout.width - 20, 2))
local progressBar = container:addChild(GUI.progressBar(1, 1, container.width, 0x66B6FF, 0xD2D2D2, 0xA5A5A5, 0, true, false))
local cyka = container:addChild(GUI.label(1, 2, container.width, 1, 0x969696, "")):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
-- Creating final filelist of things to download
local downloadList = {}
local function getData(item)
if type(item) == "table" then
return item.path, item.id, item.version, item.shortcut
else
return item
end
end
local function addToList(state, key)
if state then
local selectedLocalization, path, localizationName = localizationComboBox:getItem(localizationComboBox.selectedItem).text
for i = 1, #files[key] do
path = getData(files[key][i])
if filesystem.extension(path) == ".lang" then
localizationName = filesystem.hideExtension(filesystem.name(path))
if
-- If ALL loacalizations need to be downloaded
localizationsSwitchAndLabel.switch.state or
-- If it's required localization file
localizationName == selectedLocalization or
-- Downloading English "just in case" for non-english localizations
selectedLocalization ~= "English" and localizationName == "English"
then
table.insert(downloadList, files[key][i])
end
else
table.insert(downloadList, files[key][i])
end
end
end
end
addToList(true, "required")
addToList(true, "localizations")
addToList(true, "requiredWallpapers")
addToList(applicationsSwitchAndLabel.switch.state, "optional")
addToList(wallpapersSwitchAndLabel.switch.state, "optionalWallpapers")
-- Downloading files from created list
local versions, path, id, version, shortcut = {}
for i = 1, #downloadList do
path, id, version, shortcut = getData(downloadList[i])
cyka.text = text.limit(localization.installing .. " \"" .. path .. "\"", container.width, "center")
workspace:draw()
-- Download file
download(path, OSPath .. path)
-- Adding system versions data
if id then
versions[id] = {
path = OSPath .. path,
version = version or 1,
}
end
-- Create shortcut if possible
if shortcut then
switchProxy(function()
system.createShortcut(
userPaths.desktop .. filesystem.hideExtension(filesystem.name(filesystem.path(path))),
OSPath .. filesystem.path(path)
)
end)
end
progressBar.value = math.floor(i / #downloadList * 100)
workspace:draw()
end
-- Flashing EEPROM
layout:removeChildren()
addImage(1, 1, "EEPROM")
addTitle(0x969696, localization.flashing)
workspace:draw()
component.invoke(EEPROMAddress, "set", request(EFIURL))
component.invoke(EEPROMAddress, "setLabel", "MineOS EFI")
component.invoke(EEPROMAddress, "setData", selectedFilesystemProxy.address)
-- Saving system versions
switchProxy(function()
filesystem.writeTable(paths.system.versions, versions, true)
end)
-- Done info
layout:removeChildren()
addImage(1, 1, "Done")
addTitle(0x969696, localization.installed)
addStageButton(localization.reboot).onTouch = function()
computer.shutdown(true)
end
workspace:draw()
-- Removing temporary installer directory
temporaryFilesystemProxy.remove(installerPath)
end)
--------------------------------------------------------------------------------
loadStage()
workspace:start()