add shell architecture with persistent status bar, nav bar, and content fragments
This commit is contained in:
357
src/main/assets/apps/shell/shell.lua
Normal file
357
src/main/assets/apps/shell/shell.lua
Normal file
@@ -0,0 +1,357 @@
|
||||
-- System Shell for Mosis
|
||||
-- Provides persistent navigation, notifications, toasts, and dialogs
|
||||
|
||||
local shell_document = nil
|
||||
local app_container = nil
|
||||
|
||||
-- Navigation state (persistent in shell)
|
||||
local nav_history = {}
|
||||
local current_app = "home"
|
||||
local current_app_path = nil
|
||||
|
||||
-- Dialog callback
|
||||
local dialog_callback = nil
|
||||
|
||||
-- Notification state
|
||||
local notifications_expanded = false
|
||||
|
||||
-- ===== INITIALIZATION =====
|
||||
|
||||
function initShell(doc)
|
||||
print("[Shell] Initializing system shell...")
|
||||
shell_document = doc
|
||||
app_container = doc:GetElementById("app-container")
|
||||
|
||||
if not app_container then
|
||||
print("[Shell] ERROR: app-container not found!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Update time display
|
||||
updateTime()
|
||||
|
||||
-- Load home screen by default
|
||||
loadAppContent_internal("home", "apps/home/home_content.rml")
|
||||
|
||||
print("[Shell] Shell initialized")
|
||||
end
|
||||
|
||||
-- Update status bar time
|
||||
function updateTime()
|
||||
local time_elem = shell_document:GetElementById("shell-time")
|
||||
if time_elem then
|
||||
time_elem.inner_rml = "12:30"
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== APP LOADING =====
|
||||
|
||||
-- Internal function to load app content
|
||||
function loadAppContent_internal(app_id, app_path)
|
||||
if not app_container then
|
||||
print("[Shell] ERROR: No app container")
|
||||
return false
|
||||
end
|
||||
|
||||
print("[Shell] Loading app: " .. app_id .. " from " .. app_path)
|
||||
|
||||
-- Show loading overlay
|
||||
showLoading(true)
|
||||
|
||||
-- Load app content using C++ function
|
||||
if loadAppContent then
|
||||
local success = loadAppContent(app_container, app_path)
|
||||
showLoading(false)
|
||||
|
||||
if success then
|
||||
current_app = app_id
|
||||
current_app_path = app_path
|
||||
print("[Shell] App loaded: " .. app_id)
|
||||
return true
|
||||
else
|
||||
print("[Shell] Failed to load app: " .. app_id)
|
||||
showToast("Failed to load app", "error")
|
||||
return false
|
||||
end
|
||||
else
|
||||
showLoading(false)
|
||||
print("[Shell] ERROR: loadAppContent not available, using fallback")
|
||||
|
||||
-- Fallback: try to load via inner_rml if we can read the file
|
||||
-- This won't work without C++ support, but shows the intent
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Navigate to a system app by ID
|
||||
function shellNavigateTo(app_id)
|
||||
local app_paths = {
|
||||
home = "apps/home/home_content.rml",
|
||||
dialer = "apps/dialer/dialer_content.rml",
|
||||
calling = "apps/dialer/calling_content.rml",
|
||||
contacts = "apps/contacts/contacts_content.rml",
|
||||
contact_detail = "apps/contacts/contact_detail_content.rml",
|
||||
messages = "apps/messages/messages_content.rml",
|
||||
chat = "apps/messages/chat_content.rml",
|
||||
settings = "apps/settings/settings_content.rml",
|
||||
browser = "apps/browser/browser_content.rml",
|
||||
store = "apps/store/store_content.rml",
|
||||
camera = "apps/camera/camera_content.rml",
|
||||
music = "apps/music/music_content.rml"
|
||||
}
|
||||
|
||||
local path = app_paths[app_id]
|
||||
if path then
|
||||
-- Push current app to history
|
||||
if current_app and current_app ~= app_id then
|
||||
table.insert(nav_history, {
|
||||
app_id = current_app,
|
||||
app_path = current_app_path
|
||||
})
|
||||
print("[Shell] Pushed to history: " .. current_app .. " (depth: " .. #nav_history .. ")")
|
||||
end
|
||||
return loadAppContent_internal(app_id, path)
|
||||
else
|
||||
print("[Shell] Unknown app: " .. tostring(app_id))
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== NAVIGATION =====
|
||||
|
||||
-- Go back to previous app
|
||||
function shellGoBack()
|
||||
print("[Shell] goBack called (history depth: " .. #nav_history .. ")")
|
||||
|
||||
-- Hide notifications if open
|
||||
if notifications_expanded then
|
||||
toggleNotifications()
|
||||
end
|
||||
|
||||
if #nav_history > 0 then
|
||||
local previous = table.remove(nav_history)
|
||||
print("[Shell] Going back to: " .. previous.app_id)
|
||||
|
||||
-- Don't push to history when going back
|
||||
local temp_app = current_app
|
||||
current_app = nil
|
||||
current_app_path = nil
|
||||
|
||||
local success = loadAppContent_internal(previous.app_id, previous.app_path)
|
||||
if not success then
|
||||
-- Restore state on failure
|
||||
current_app = temp_app
|
||||
end
|
||||
return success
|
||||
else
|
||||
print("[Shell] No history - already at root")
|
||||
if current_app ~= "home" then
|
||||
shellGoHome()
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Go to home screen (clear history)
|
||||
function shellGoHome()
|
||||
print("[Shell] goHome called")
|
||||
|
||||
-- Hide notifications if open
|
||||
if notifications_expanded then
|
||||
toggleNotifications()
|
||||
end
|
||||
|
||||
-- Clear history
|
||||
nav_history = {}
|
||||
|
||||
-- Load home
|
||||
current_app = nil
|
||||
current_app_path = nil
|
||||
loadAppContent_internal("home", "apps/home/home_content.rml")
|
||||
end
|
||||
|
||||
-- Show recents (placeholder)
|
||||
function shellShowRecents()
|
||||
print("[Shell] showRecents called")
|
||||
showToast("Recent apps not implemented", "warning")
|
||||
end
|
||||
|
||||
-- Launch external app (from base-apps)
|
||||
function shellLaunchApp(package_id, app_path, entry_point)
|
||||
print("[Shell] Launching external app: " .. package_id)
|
||||
|
||||
-- Push current to history
|
||||
if current_app then
|
||||
table.insert(nav_history, {
|
||||
app_id = current_app,
|
||||
app_path = current_app_path
|
||||
})
|
||||
end
|
||||
|
||||
-- For external apps, we need content version of the entry
|
||||
-- Convert "app.rml" to "app_content.rml" or load directly
|
||||
local content_path = app_path .. "/" .. entry_point
|
||||
|
||||
-- Switch sandbox context if available
|
||||
if switchAppSandbox then
|
||||
switchAppSandbox(package_id, app_path)
|
||||
end
|
||||
|
||||
return loadAppContent_internal(package_id, content_path)
|
||||
end
|
||||
|
||||
-- ===== TOASTS =====
|
||||
|
||||
function showToast(message, type)
|
||||
type = type or "default"
|
||||
|
||||
local container = shell_document:GetElementById("toast-container")
|
||||
if not container then
|
||||
print("[Shell] Toast container not found")
|
||||
return
|
||||
end
|
||||
|
||||
local class = "toast"
|
||||
if type == "success" then
|
||||
class = "toast toast-success"
|
||||
elseif type == "error" then
|
||||
class = "toast toast-error"
|
||||
elseif type == "warning" then
|
||||
class = "toast toast-warning"
|
||||
end
|
||||
|
||||
-- Add toast element
|
||||
local toast_html = '<div class="' .. class .. '">' .. message .. '</div>'
|
||||
container.inner_rml = container.inner_rml .. toast_html
|
||||
|
||||
-- Auto-remove after 3 seconds (would need timer API)
|
||||
if mosis and mosis.timer then
|
||||
mosis.timer.setTimeout(function()
|
||||
-- Remove first toast
|
||||
container.inner_rml = ""
|
||||
end, 3000)
|
||||
end
|
||||
|
||||
print("[Shell] Toast: " .. message .. " (" .. type .. ")")
|
||||
end
|
||||
|
||||
-- ===== DIALOGS =====
|
||||
|
||||
function showDialog(title, message, on_confirm, on_cancel)
|
||||
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||
local title_elem = shell_document:GetElementById("dialog-title")
|
||||
local message_elem = shell_document:GetElementById("dialog-message")
|
||||
|
||||
if overlay and title_elem and message_elem then
|
||||
title_elem.inner_rml = title
|
||||
message_elem.inner_rml = message
|
||||
overlay:SetClass("visible", true)
|
||||
|
||||
dialog_callback = {
|
||||
confirm = on_confirm,
|
||||
cancel = on_cancel
|
||||
}
|
||||
|
||||
print("[Shell] Dialog shown: " .. title)
|
||||
end
|
||||
end
|
||||
|
||||
function dialogConfirm()
|
||||
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||
if overlay then
|
||||
overlay:SetClass("visible", false)
|
||||
end
|
||||
|
||||
if dialog_callback and dialog_callback.confirm then
|
||||
dialog_callback.confirm()
|
||||
end
|
||||
dialog_callback = nil
|
||||
end
|
||||
|
||||
function dialogCancel()
|
||||
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||
if overlay then
|
||||
overlay:SetClass("visible", false)
|
||||
end
|
||||
|
||||
if dialog_callback and dialog_callback.cancel then
|
||||
dialog_callback.cancel()
|
||||
end
|
||||
dialog_callback = nil
|
||||
end
|
||||
|
||||
-- ===== NOTIFICATIONS =====
|
||||
|
||||
function toggleNotifications()
|
||||
local panel = shell_document:GetElementById("notification-panel")
|
||||
if panel then
|
||||
notifications_expanded = not notifications_expanded
|
||||
panel:SetClass("expanded", notifications_expanded)
|
||||
print("[Shell] Notifications " .. (notifications_expanded and "expanded" or "collapsed"))
|
||||
end
|
||||
end
|
||||
|
||||
function addNotification(title, text, icon, app_id)
|
||||
local list = shell_document:GetElementById("notification-list")
|
||||
if not list then return end
|
||||
|
||||
icon = icon or "../../icons/message.tga"
|
||||
local notification_html = [[
|
||||
<div class="notification-item" onclick="onNotificationClick(']] .. (app_id or "") .. [[')">
|
||||
<div class="notification-icon">
|
||||
<img src="]] .. icon .. [["/>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">]] .. title .. [[</div>
|
||||
<div class="notification-text">]] .. text .. [[</div>
|
||||
</div>
|
||||
<span class="notification-time">now</span>
|
||||
</div>
|
||||
]]
|
||||
|
||||
list.inner_rml = notification_html .. list.inner_rml
|
||||
print("[Shell] Notification added: " .. title)
|
||||
end
|
||||
|
||||
function onNotificationClick(app_id)
|
||||
toggleNotifications()
|
||||
if app_id and app_id ~= "" then
|
||||
shellNavigateTo(app_id)
|
||||
end
|
||||
end
|
||||
|
||||
function clearNotifications()
|
||||
local list = shell_document:GetElementById("notification-list")
|
||||
if list then
|
||||
list.inner_rml = ""
|
||||
print("[Shell] Notifications cleared")
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== LOADING OVERLAY =====
|
||||
|
||||
function showLoading(visible)
|
||||
local overlay = shell_document:GetElementById("loading-overlay")
|
||||
if overlay then
|
||||
overlay:SetClass("visible", visible)
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== UTILITY =====
|
||||
|
||||
function shellGetCurrentApp()
|
||||
return current_app
|
||||
end
|
||||
|
||||
function shellCanGoBack()
|
||||
return #nav_history > 0
|
||||
end
|
||||
|
||||
-- Make shell functions globally available for apps
|
||||
_G.showToast = showToast
|
||||
_G.showDialog = showDialog
|
||||
_G.addNotification = addNotification
|
||||
_G.shellNavigateTo = shellNavigateTo
|
||||
_G.shellLaunchApp = shellLaunchApp
|
||||
|
||||
print("[Shell] Shell script loaded")
|
||||
Reference in New Issue
Block a user