Files
MosisService/base-apps/com.mosis.store/store.lua
omigamedev 1f91d7508e add base-apps with manifests, layout system, and testing documentation
- 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
2026-01-20 09:14:05 +01:00

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()