- Rename test-apps to base-apps with proper manifest.json for each app - Add is_system_app flag to app discovery and Lua API - Fix icon path resolution for /system/icons/ paths - Add layout.lua and layout.rcss for reusable UI components - Update home screen to dynamically load all apps from manifests - Update all app RML files to use layout components - Comprehensive testing framework documentation with JSON action format - Add tests/ directory structure for automated UI testing
395 lines
10 KiB
Lua
395 lines
10 KiB
Lua
-- 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()
|