- Toast starts hidden (opacity 0, translateY 30dp) - Transition property animates opacity and transform over 0.3s - Adding toast-show class triggers slide-up animation - Removing toast-show class triggers slide-down animation
685 lines
22 KiB
Lua
685 lines
22 KiB
Lua
-- 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 (skip animation on initial load)
|
|
loadAppContent_internal("home", "apps/home/home_content.rml", true)
|
|
|
|
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 with optional animation
|
|
function loadAppContent_internal(app_id, app_path, skip_animation)
|
|
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
|
|
-- Hide container before loading to prevent flash
|
|
app_container:SetClass("app-hidden", true)
|
|
|
|
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)
|
|
|
|
-- Show container after content is loaded
|
|
app_container:SetClass("app-hidden", false)
|
|
|
|
-- If home was loaded, populate apps dynamically
|
|
if app_id == "home" then
|
|
populateHomeApps()
|
|
end
|
|
|
|
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",
|
|
["sandbox-test"] = "apps/sandbox-test/main_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 =====
|
|
|
|
-- Execute callback for navigation (animations removed to fix flash)
|
|
local function playCloseAnimation(callback)
|
|
-- Just execute callback immediately - loadAppContent handles hiding
|
|
if callback then callback() end
|
|
end
|
|
|
|
-- 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)
|
|
|
|
-- Play closing animation, then load previous app
|
|
playCloseAnimation(function()
|
|
-- 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
|
|
end)
|
|
return true
|
|
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 = {}
|
|
|
|
-- Play closing animation, then load home
|
|
playCloseAnimation(function()
|
|
current_app = nil
|
|
current_app_path = nil
|
|
loadAppContent_internal("home", "apps/home/home_content.rml")
|
|
end)
|
|
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 =====
|
|
|
|
local active_toast_id = nil
|
|
|
|
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
|
|
|
|
-- Cancel any pending hide timer
|
|
if active_toast_id and clearTimeout then
|
|
clearTimeout(active_toast_id)
|
|
active_toast_id = nil
|
|
end
|
|
|
|
local type_class = ""
|
|
if type == "success" then
|
|
type_class = " toast-success"
|
|
elseif type == "error" then
|
|
type_class = " toast-error"
|
|
elseif type == "warning" then
|
|
type_class = " toast-warning"
|
|
end
|
|
|
|
-- Create toast in hidden state (opacity 0, translated down)
|
|
local toast_html = '<div id="active-toast" class="toast' .. type_class .. '"><span>' .. message .. '</span></div>'
|
|
container.inner_rml = toast_html
|
|
|
|
-- Add toast-show class after brief delay to trigger transition
|
|
if setTimeout then
|
|
setTimeout(function()
|
|
local toast = shell_document:GetElementById("active-toast")
|
|
if toast then
|
|
toast:SetClass("toast-show", true)
|
|
end
|
|
end, 10)
|
|
|
|
-- Auto-hide after 2.5 seconds
|
|
active_toast_id = setTimeout(function()
|
|
local toast = shell_document:GetElementById("active-toast")
|
|
if toast then
|
|
-- Remove show class to animate out
|
|
toast:SetClass("toast-show", false)
|
|
-- Remove element after transition
|
|
setTimeout(function()
|
|
container.inner_rml = ""
|
|
active_toast_id = nil
|
|
end, 300)
|
|
end
|
|
end, 2500)
|
|
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
|
|
|
|
-- ===== DYNAMIC APP DISCOVERY =====
|
|
|
|
-- Default colors for apps without custom colors
|
|
local app_colors = {
|
|
["com.mosis.browser"] = "#F44336",
|
|
["com.mosis.camera"] = "#9C27B0",
|
|
["com.mosis.contacts"] = "#FF9800",
|
|
["com.mosis.dialer"] = "#4CAF50",
|
|
["com.mosis.messages"] = "#2196F3",
|
|
["com.mosis.music"] = "#E91E63",
|
|
["com.mosis.settings"] = "#607D8B",
|
|
["com.mosis.store"] = "#3F51B5",
|
|
}
|
|
|
|
-- Default color for unknown apps
|
|
local default_colors = {
|
|
"#FF5722", "#009688", "#795548", "#673AB7",
|
|
"#3F51B5", "#00BCD4", "#8BC34A", "#CDDC39"
|
|
}
|
|
local color_index = 1
|
|
|
|
function getAppColor(package_id)
|
|
if app_colors[package_id] then
|
|
return app_colors[package_id]
|
|
end
|
|
-- Assign a default color
|
|
local color = default_colors[color_index]
|
|
color_index = (color_index % #default_colors) + 1
|
|
app_colors[package_id] = color
|
|
return color
|
|
end
|
|
|
|
-- Built-in system apps (fallback when mosis.apps not available or empty)
|
|
local builtin_apps = {
|
|
{package_id = "com.mosis.browser", name = "Browser", icon = "../../icons/browser.tga"},
|
|
{package_id = "com.mosis.camera", name = "Camera", icon = "../../icons/camera.tga"},
|
|
{package_id = "com.mosis.contacts", name = "Contacts", icon = "../../icons/contacts.tga"},
|
|
{package_id = "com.mosis.dialer", name = "Phone", icon = "../../icons/phone.tga"},
|
|
{package_id = "com.mosis.messages", name = "Messages", icon = "../../icons/message.tga"},
|
|
{package_id = "com.mosis.music", name = "Music", icon = "../../icons/music.tga"},
|
|
{package_id = "com.mosis.settings", name = "Settings", icon = "../../icons/settings.tga"},
|
|
{package_id = "com.mosis.store", name = "Store", icon = "../../icons/store.tga"},
|
|
}
|
|
|
|
-- Populate home screen with discovered apps
|
|
function populateHomeApps()
|
|
local apps = nil
|
|
|
|
-- Try to get installed apps from mosis.apps API
|
|
if mosis and mosis.apps then
|
|
apps = mosis.apps.getInstalled()
|
|
end
|
|
|
|
-- Fall back to built-in apps if none discovered
|
|
if not apps or #apps == 0 then
|
|
print("[Shell] Using built-in system apps")
|
|
apps = builtin_apps
|
|
end
|
|
|
|
print("[Shell] Populating home with " .. #apps .. " apps")
|
|
|
|
local grid_container = shell_document:GetElementById("installed-apps")
|
|
if not grid_container then
|
|
print("[Shell] installed-apps container not found")
|
|
return
|
|
end
|
|
|
|
local apps_html = ""
|
|
local dock_apps = {}
|
|
|
|
for _, app in ipairs(apps) do
|
|
-- Skip home app itself
|
|
if app.package_id ~= "com.mosis.home" then
|
|
local color = getAppColor(app.package_id)
|
|
local icon = app.icon or "../../icons/app.tga"
|
|
local name = app.name or app.package_id
|
|
|
|
-- Build app icon HTML
|
|
local app_html = [[
|
|
<div class="app-icon">
|
|
<div class="app-icon-image" style="background-color: ]] .. color .. [[;"
|
|
onclick="launchDiscoveredApp(']] .. app.package_id .. [[')">
|
|
<img src="]] .. icon .. [["/>
|
|
</div>
|
|
<span class="app-icon-label">]] .. name .. [[</span>
|
|
</div>
|
|
]]
|
|
apps_html = apps_html .. app_html
|
|
|
|
-- Track dock apps (dialer, messages, contacts, browser)
|
|
if app.package_id == "com.mosis.dialer" then
|
|
dock_apps.dialer = {icon = icon, color = color, id = app.package_id}
|
|
elseif app.package_id == "com.mosis.messages" then
|
|
dock_apps.messages = {icon = icon, color = color, id = app.package_id}
|
|
elseif app.package_id == "com.mosis.contacts" then
|
|
dock_apps.contacts = {icon = icon, color = color, id = app.package_id}
|
|
elseif app.package_id == "com.mosis.browser" then
|
|
dock_apps.browser = {icon = icon, color = color, id = app.package_id}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Set grid content
|
|
grid_container.inner_rml = apps_html
|
|
|
|
-- Populate dock
|
|
local dock_container = shell_document:GetElementById("home-dock")
|
|
if dock_container and (dock_apps.dialer or dock_apps.messages or dock_apps.contacts or dock_apps.browser) then
|
|
local dock_html = ""
|
|
local dock_order = {"dialer", "messages", "contacts", "browser"}
|
|
for _, key in ipairs(dock_order) do
|
|
local app = dock_apps[key]
|
|
if app then
|
|
dock_html = dock_html .. [[
|
|
<div class="dock-item" style="background-color: ]] .. app.color .. [[;"
|
|
onclick="launchDiscoveredApp(']] .. app.id .. [[')">
|
|
<img src="]] .. app.icon .. [["/>
|
|
</div>
|
|
]]
|
|
end
|
|
end
|
|
dock_container.inner_rml = dock_html
|
|
end
|
|
|
|
print("[Shell] Home populated with " .. #apps - 1 .. " apps")
|
|
end
|
|
|
|
-- Launch a discovered app by package_id
|
|
function launchDiscoveredApp(package_id)
|
|
print("[Shell] Launching app: " .. package_id)
|
|
|
|
-- Check if it's a built-in system app
|
|
local app_id = package_id:gsub("com.mosis.", "")
|
|
local builtin_ids = {
|
|
browser = true, camera = true, contacts = true, dialer = true,
|
|
messages = true, music = true, settings = true, store = true,
|
|
["sandbox-test"] = true
|
|
}
|
|
|
|
if builtin_ids[app_id] then
|
|
-- Use shellNavigateTo for built-in apps
|
|
return shellNavigateTo(app_id)
|
|
end
|
|
|
|
-- Try to find in installed apps
|
|
if mosis and mosis.apps then
|
|
local apps = mosis.apps.getInstalled()
|
|
for _, app in ipairs(apps) do
|
|
if app.package_id == package_id then
|
|
print("[Shell] Launching installed app: " .. package_id)
|
|
|
|
-- Push current to history
|
|
if current_app 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
|
|
|
|
-- Build content path - convert entry.rml to entry_content.rml
|
|
local entry = app.entry_point or "main.rml"
|
|
local content_entry = entry:gsub("%.rml$", "_content.rml")
|
|
local content_path = "apps/" .. package_id:gsub("com.mosis.", "") .. "/" .. content_entry
|
|
|
|
-- Switch sandbox if available
|
|
if switchAppSandbox then
|
|
switchAppSandbox(package_id, app.install_path)
|
|
end
|
|
|
|
return loadAppContent_internal(package_id, content_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
print("[Shell] App not found: " .. package_id)
|
|
showToast("App not found", "error")
|
|
return false
|
|
end
|
|
|
|
-- ===== SANDBOX TEST FUNCTIONS =====
|
|
|
|
function testSandboxTimer()
|
|
local status = shell_document:GetElementById("timer-status")
|
|
local results = shell_document:GetElementById("sandbox-results")
|
|
|
|
if not setTimeout then
|
|
if status then status.inner_rml = "Timer API not available" end
|
|
return
|
|
end
|
|
|
|
if status then status.inner_rml = "Timer started..." end
|
|
|
|
setTimeout(function()
|
|
if status then status.inner_rml = "Timer completed!" end
|
|
if results then results.inner_rml = results.inner_rml .. "\nTimer: PASSED (1 second delay)" end
|
|
showToast("Timer test passed!", "success")
|
|
end, 1000)
|
|
end
|
|
|
|
function testSandboxJSON()
|
|
local status = shell_document:GetElementById("json-status")
|
|
local results = shell_document:GetElementById("sandbox-results")
|
|
|
|
if not json then
|
|
if status then status.inner_rml = "JSON API not available" end
|
|
return
|
|
end
|
|
|
|
local test_data = {name = "test", value = 42, nested = {a = 1, b = 2}}
|
|
local encoded = json.encode(test_data)
|
|
local decoded = json.decode(encoded)
|
|
|
|
if decoded and decoded.name == "test" and decoded.value == 42 then
|
|
if status then status.inner_rml = "JSON encode/decode: PASSED" end
|
|
if results then results.inner_rml = results.inner_rml .. "\nJSON: PASSED" end
|
|
showToast("JSON test passed!", "success")
|
|
else
|
|
if status then status.inner_rml = "JSON test: FAILED" end
|
|
showToast("JSON test failed!", "error")
|
|
end
|
|
end
|
|
|
|
function testSandboxCrypto()
|
|
local status = shell_document:GetElementById("crypto-status")
|
|
local results = shell_document:GetElementById("sandbox-results")
|
|
|
|
if not crypto then
|
|
if status then status.inner_rml = "Crypto API not available" end
|
|
return
|
|
end
|
|
|
|
local hash = crypto.sha256("test")
|
|
if hash and #hash > 0 then
|
|
if status then status.inner_rml = "SHA256: " .. hash:sub(1, 16) .. "..." end
|
|
if results then results.inner_rml = results.inner_rml .. "\nCrypto: PASSED" end
|
|
showToast("Crypto test passed!", "success")
|
|
else
|
|
if status then status.inner_rml = "Crypto test: FAILED" end
|
|
showToast("Crypto test failed!", "error")
|
|
end
|
|
end
|
|
|
|
function testSandboxStorage()
|
|
local status = shell_document:GetElementById("storage-status")
|
|
local results = shell_document:GetElementById("sandbox-results")
|
|
|
|
if not fs then
|
|
if status then status.inner_rml = "Storage API not available" end
|
|
return
|
|
end
|
|
|
|
local test_content = "Hello from sandbox test!"
|
|
local write_ok, write_err = fs.write("/data/test.txt", test_content)
|
|
|
|
if write_ok then
|
|
local read_content, read_err = fs.read("/data/test.txt")
|
|
if read_content == test_content then
|
|
if status then status.inner_rml = "Storage read/write: PASSED" end
|
|
if results then results.inner_rml = results.inner_rml .. "\nStorage: PASSED" end
|
|
showToast("Storage test passed!", "success")
|
|
else
|
|
local err_msg = read_err or "content mismatch"
|
|
if status then status.inner_rml = "Storage read: " .. err_msg end
|
|
showToast("Storage read failed!", "error")
|
|
end
|
|
else
|
|
local err_msg = write_err or "unknown error"
|
|
if status then status.inner_rml = "Storage write: " .. err_msg end
|
|
showToast("Storage write failed!", "error")
|
|
end
|
|
end
|
|
|
|
-- Make shell functions globally available for apps
|
|
_G.showToast = showToast
|
|
_G.showDialog = showDialog
|
|
_G.addNotification = addNotification
|
|
_G.shellNavigateTo = shellNavigateTo
|
|
_G.shellLaunchApp = shellLaunchApp
|
|
_G.launchDiscoveredApp = launchDiscoveredApp
|
|
_G.populateHomeApps = populateHomeApps
|
|
_G.testSandboxTimer = testSandboxTimer
|
|
_G.testSandboxJSON = testSandboxJSON
|
|
_G.testSandboxCrypto = testSandboxCrypto
|
|
_G.testSandboxStorage = testSandboxStorage
|
|
|
|
print("[Shell] Shell script loaded")
|