-- store.lua - App Store system app logic -- Milestone 10: Device-Side App Management -- State local state = { screen = "home", -- home, games, updates, search, detail installed = {}, -- Installed apps from mosis.apps updates = {}, -- Available updates featured = {}, -- Featured apps from store API categories = {}, -- Category list search_query = "", -- Current search selected_app = nil, -- Selected app for detail view is_loading = false, error_message = nil } -- Store API configuration local STORE_API = "https://portal.mosis.dev/store" -- ============================================================================ -- Initialization -- ============================================================================ function init() print("[Store] Initializing...") -- Load installed apps refreshInstalledApps() -- Check for updates checkForUpdates() -- Fetch featured apps (async) fetchFeaturedApps() end function refreshInstalledApps() if mosis and mosis.apps then state.installed = mosis.apps.getInstalled() or {} print("[Store] Loaded " .. #state.installed .. " installed apps") else print("[Store] Warning: mosis.apps API not available") state.installed = {} end end function checkForUpdates() if mosis and mosis.apps then state.updates = mosis.apps.checkUpdates() or {} print("[Store] Found " .. #state.updates .. " updates") updateBadge() end end function updateBadge() -- Update the updates tab badge local badge = document:GetElementById("updates-badge") if badge then if #state.updates > 0 then badge.inner_rml = tostring(#state.updates) badge.style.display = "block" else badge.style.display = "none" end end end -- ============================================================================ -- API Calls -- ============================================================================ function fetchFeaturedApps() state.is_loading = true -- TODO: Make HTTP request to STORE_API -- For now, use placeholder data state.featured = { { id = "com.mosis.weather", name = "Weather Pro", category = "Weather", rating = 4.8, downloads = 125000, size = 15728640, -- 15 MB description = "Beautiful forecasts for your virtual world", icon = "W", color = "#2196F3" }, { id = "com.mosis.notes", name = "Notes", category = "Productivity", rating = 4.7, downloads = 89000, size = 8388608, -- 8 MB description = "Simple note-taking app", icon = "N", color = "#03DAC6" } } state.is_loading = false render() end function searchApps(query) state.search_query = query state.screen = "search" if query == "" then state.screen = "home" render() return end state.is_loading = true render() -- TODO: Make HTTP request to STORE_API/search -- For now, filter featured apps local results = {} local lower_query = query:lower() for _, app in ipairs(state.featured) do if app.name:lower():find(lower_query) or app.category:lower():find(lower_query) then table.insert(results, app) end end state.search_results = results state.is_loading = false render() end -- ============================================================================ -- Installation -- ============================================================================ function installApp(app_id, download_url, signature) print("[Store] Installing: " .. app_id) showProgress(app_id) if mosis and mosis.apps then mosis.apps.install(download_url or "", signature or "", function(progress) updateProgress(progress) if progress.stage == "complete" then hideProgress() showToast("App installed successfully!") refreshInstalledApps() render() elseif progress.stage == "failed" then hideProgress() showError("Installation failed: " .. (progress.error or "Unknown error")) end end) else hideProgress() showError("App installation not available") end end function uninstallApp(package_id) print("[Store] Uninstalling: " .. package_id) if mosis and mosis.apps then local success = mosis.apps.uninstall(package_id) if success then showToast("App uninstalled") refreshInstalledApps() render() else showError("Failed to uninstall app") end end end function openApp(package_id) print("[Store] Launching: " .. package_id) if mosis and mosis.apps then mosis.apps.launch(package_id) end end function updateApp(package_id) print("[Store] Updating: " .. package_id) -- Find update info for _, update in ipairs(state.updates) do if update.package_id == package_id then installApp(package_id, update.download_url, update.signature) return end end showError("No update available for this app") end function updateAllApps() print("[Store] Updating all apps...") for _, update in ipairs(state.updates) do -- Queue updates (in a real implementation, this would be sequential) installApp(update.package_id, update.download_url, update.signature) end end -- ============================================================================ -- UI Helpers -- ============================================================================ function isInstalled(package_id) for _, app in ipairs(state.installed) do if app.package_id == package_id then return true end end return false end function hasUpdate(package_id) for _, update in ipairs(state.updates) do if update.package_id == package_id then return true end end return false end function formatSize(bytes) if bytes >= 1048576 then return string.format("%.1f MB", bytes / 1048576) elseif bytes >= 1024 then return string.format("%.0f KB", bytes / 1024) else return bytes .. " B" end end function formatDownloads(count) if count >= 1000000 then return string.format("%.1fM", count / 1000000) elseif count >= 1000 then return string.format("%.0fK", count / 1000) else return tostring(count) end end -- ============================================================================ -- Progress Dialog -- ============================================================================ function showProgress(app_name) local dialog = document:GetElementById("progress-dialog") if dialog then dialog.style.display = "flex" local title = document:GetElementById("progress-title") if title then title.inner_rml = "Installing " .. (app_name or "App") end end end function updateProgress(progress) local bar = document:GetElementById("progress-bar") if bar then bar.style.width = (progress.progress * 100) .. "%" end local status = document:GetElementById("progress-status") if status then local stage_names = { downloading = "Downloading...", verifying = "Verifying...", extracting = "Extracting...", registering = "Registering...", complete = "Complete!", failed = "Failed" } status.inner_rml = stage_names[progress.stage] or progress.stage end end function hideProgress() local dialog = document:GetElementById("progress-dialog") if dialog then dialog.style.display = "none" end end -- ============================================================================ -- Toast/Error Messages -- ============================================================================ function showToast(message) local toast = document:GetElementById("toast") if toast then toast.inner_rml = message toast.style.display = "block" -- Auto-hide after 3 seconds (would need timer API) end print("[Store] Toast: " .. message) end function showError(message) state.error_message = message local error_el = document:GetElementById("error-dialog") if error_el then local msg = document:GetElementById("error-message") if msg then msg.inner_rml = message end error_el.style.display = "flex" end print("[Store] Error: " .. message) end function hideError() state.error_message = nil local error_el = document:GetElementById("error-dialog") if error_el then error_el.style.display = "none" end end -- ============================================================================ -- Navigation -- ============================================================================ function showHome() state.screen = "home" setActiveTab("apps") render() end function showGames() state.screen = "games" setActiveTab("games") render() end function showUpdates() state.screen = "updates" setActiveTab("updates") checkForUpdates() render() end function showSearch() state.screen = "search" render() end function showAppDetail(app_id) state.screen = "detail" -- Find app in featured or installed for _, app in ipairs(state.featured) do if app.id == app_id then state.selected_app = app break end end render() end function setActiveTab(tab) local tabs = {"apps", "games", "updates"} for _, t in ipairs(tabs) do local el = document:GetElementById("nav-" .. t) if el then if t == tab then el:SetClass("active", true) else el:SetClass("active", false) end end end end -- ============================================================================ -- Rendering -- ============================================================================ function render() -- The RML is mostly static with dynamic data binding -- In a full implementation, we'd update innerHTML of content areas print("[Store] Rendering screen: " .. state.screen) end -- Initialize on load init()