Compare commits

..

37 Commits

Author SHA1 Message Date
341d663b36 update Unreal build script docs: add launch command 2026-01-21 09:41:29 +01:00
cbc03c0674 fix scroll direction: use RmlUi native touch API
Switch from mouse event emulation to RmlUi's native touch API
(ProcessTouchStart/Move/End) for natural scrolling behavior.

When dragging on scrollable content, the content now follows
the finger/cursor direction instead of scrolling in reverse.

- Designer: Use touch API when left mouse button is pressed
- Android kernel: Use touch API for all touch events
2026-01-20 20:30:08 +01:00
bd8ce61897 fix Music app layout and document RmlUi encoding issue
- Fixed Music app layout using incremental edit approach
- Documented RmlUi file encoding issue in DESKTOP-DESIGNER.md
- Added workaround: copy from working file and edit incrementally
2026-01-20 19:27:15 +01:00
0da90f976f fix Store and Settings layouts: copy from working Messages file
The Write tool was introducing invisible formatting that broke RmlUi's
layout parsing, causing elements to have 0 width. Fixed by copying
from the working Messages file and editing with Edit tool instead.

- Store: simplified app list with proper full-width layout
- Settings: simplified settings list with profile, network, device options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:16:58 +01:00
6ae62a60fc fix Store layout: use list-item pattern for full-width elements 2026-01-20 18:45:57 +01:00
afa6ac22ba add missing icons: bluetooth, display, edit, info, location, lock, notifications, plane, star, storage, volume 2026-01-20 18:04:27 +01:00
d5c8dccc34 add rich Store app UI with featured banner, categories, and app listings 2026-01-20 17:52:30 +01:00
82bc0c78fe fix app layouts: remove style tags from content fragments, use component classes 2026-01-20 17:40:38 +01:00
4b47611902 add app open/close animations using RCSS
- app-opening: 0.2s quadratic-out scale from 0.9 to 1.0
- app-closing: 0.15s quadratic-in scale from 1.0 to 0.9
- Avoided back-out easing to prevent flickering
2026-01-20 15:56:33 +01:00
0b4931eaca fix toast flicker: use quadratic-out instead of back-out easing 2026-01-20 15:45:09 +01:00
6c7a78ce76 use RmlUi animation syntax for toast
Animation syntax: <duration> <tweening-function> <keyframes-name>
- toast-show class triggers toast-in animation (slide up, fade in)
- toast-hide class triggers toast-out animation (slide down, fade out)
2026-01-20 14:58:57 +01:00
be5a5db18a add toast animation using RCSS transitions
- 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
2026-01-20 13:38:10 +01:00
e722680863 remove toast animations to fix green flash 2026-01-20 13:32:57 +01:00
0d8415ba4e remove app animations to fix transition flash
Temporarily removed complex animations that were causing double-render flash.
Using simple hide/show approach during content loading instead.
2026-01-20 13:28:07 +01:00
b3055d8f1a fix transition flash between apps
- Set app-opening class BEFORE loading content to prevent flash
- Keep app-closing at opacity 0 after animation ends
- Remove unnecessary class removal in playCloseAnimation
2026-01-20 13:10:22 +01:00
efc007e487 use junctions for base-apps icon paths instead of duplicate files
Create Windows junctions at root pointing to src/main/assets/:
  mklink /J icons src\main\assets\icons
  mklink /J scripts src\main\assets\scripts
  mklink /J ui src\main\assets\ui

This avoids duplicating files while allowing base-apps manifest
paths like ../../icons/ to resolve correctly.
2026-01-20 13:04:32 +01:00
469535f79a add toast and app transition animations
Toast:
- Bigger size with bold text (18dp padding, 16dp font)
- Pop-up animation from bottom with bounce effect
- Fade-out animation when dismissing
- Cancels previous toast when showing new one

App transitions:
- Opening: fade in + scale from 0.8 to 1.0 with back-out easing
- Closing: fade out + scale from 1.0 to 0.8
- Skip animation on initial shell load
- Async close animation before loading previous app
2026-01-20 12:27:44 +01:00
11c59b890e docs: add tests/ folder convention for test output 2026-01-20 12:04:29 +01:00
17f605cf5f fix toast display and auto-dismiss
- Wrap toast message in span for proper text rendering
- Use setTimeout instead of mosis.timer for auto-dismiss
- Remove unsupported CSS animation property
- Add explicit color to toast variants
- Change px to dp units for RmlUi compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 11:45:02 +01:00
07896959ce fix sandbox test API names to match actual registration
- Timer: use global setTimeout() instead of mosis.timer.setTimeout()
- JSON: use json.encode/decode instead of mosis.json
- Crypto: use crypto.sha256 instead of mosis.crypto
- Storage: use fs.read/write with /data/ prefix instead of mosis.fs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 11:32:47 +01:00
a52b58c176 add sandbox-test app content and test functions
- Add main_content.rml for sandbox-test app with test cards for Timer, JSON, Crypto, Storage
- Add sandbox test functions to shell.lua (testSandboxTimer, testSandboxJSON, etc.)
- Register sandbox-test in builtin_ids and shellNavigateTo paths
- Add sandbox-test styles to shell.rml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 11:28:12 +01:00
2134a53921 fix Android: load shell.rml, add loadAppContent function, fallback to built-in apps 2026-01-20 10:56:58 +01:00
41fc6fdd86 fix simulator mode to use shell.rml instead of deleted home.rml 2026-01-20 10:39:27 +01:00
ab53bee5c4 add dynamic app discovery and remove unused static RML files 2026-01-20 10:14:13 +01:00
2db7eea9f1 add shell architecture with persistent status bar, nav bar, and content fragments 2026-01-20 10:09:47 +01:00
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
5de087e8e0 fix document global for external Lua scripts in designer and Android 2026-01-19 22:51:19 +01:00
a3a15b0644 docs: add simulator mode and test-apps flags to DESKTOP-DESIGNER.md 2026-01-19 22:17:37 +01:00
68398e5b60 fix sandbox app script loading and timer execution
- Add relative path resolution for filesystem-loaded documents in kernel.cpp
  - SetDocumentBasePath() tracks document directory for relative resource resolution
  - ResolvePath() resolves relative paths like "app.lua" against document base path
- Fix RmlUi context name lookup in sandbox test app (use "default" not "main")
- Add debug logging to timer_manager.cpp to trace timer creation and execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:10:45 +01:00
8cf24d8c2a fix files directory handling: use internal storage and add exception handling
- Use internal files dir instead of external (fixes scoped storage permissions)
- Pass files directory from Android context via JNI instead of hardcoding
- Add exception handling in ScanAppsDirectory to prevent crashes on permission errors
- Use std::error_code overload of fs::exists() to avoid throwing on access denial

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 21:49:58 +01:00
76d97e202b fix settings layout for RmlUi 6.2: toggle positioning and flexbox sizing 2026-01-19 19:53:29 +01:00
90b0a19a4d Fix Settings app layout for RmlUi flex rendering
Add explicit width and flex container properties to fix elements
collapsing to zero width in RmlUi:
- .settings-list: Add width: 100%, display: flex, flex-direction: column
- .settings-section: Add width: 100%, display: flex, flex-direction: column
- .settings-item: Add width: 100%, box-sizing: border-box
- .user-card: Add width: 100%, box-sizing: border-box

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:33:42 +01:00
56dc8337af Fix RmlUi CSS compatibility issues in store, settings, and music apps
- Replace linear-gradient with solid colors (not supported in RmlUi)
- Replace border shorthand with border-width/border-color properties
- Replace display:grid with flexbox in music quick-access cards
- Remove unsupported transition property
- Fix border-radius: 50% to use 9999px in theme.rcss
- Fix third-party app icon/label sizes to match system apps (72x72px, 16px)
- Replace data-model bindings with static values in settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:27:08 +01:00
6b611b1d09 Fix icon loading for third-party apps with file:// URL scheme
- Add file:// URL handling to AssetFilesInterface in kernel.cpp
- Update home.lua to use file:// prefix for absolute filesystem paths
- Add file:// URL handling to desktop file interface for consistency

This fixes RmlUi stripping the leading slash from absolute paths
when resolving img src URLs relative to the document base.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:01:31 +01:00
cb86d52705 Add sandbox API integration for RmlUi Lua state
- Register sandbox APIs (timer, JSON, crypto, VFS) into RmlUi's Lua state
- Add switchAppSandbox() function for context switching between apps
- Update goHome() to reset sandbox context when returning home
- Fix icon loading for third-party apps (handle full paths vs relative)
- Mirror changes between Android service (kernel.cpp) and desktop designer

This enables third-party apps to use sandbox APIs when running in RmlUi.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:49:35 +01:00
ea44f0bba4 Add simulator mode with app discovery and mosis.apps API
Simulator mode (--simulator flag):
- Starts from main home.rml instead of separate simulator home
- Discovers apps from test-apps folder automatically
- Shows discovered apps in home screen grid

mosis.apps API for Lua:
- getInstalled() returns array of discovered apps
- launch(package_id) starts an app with its own sandbox

goHome improvements:
- Uses g_main_assets_path to find home.rml correctly
- Works when running test apps directly
- Properly clears current app state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:01:40 +01:00
984e8715d7 Fix desktop designer click handling and add goHome API
Designer click handling:
- Fix DPI scaling in MouseButtonCallback and CursorPosCallback
- Scale coordinates from window space to framebuffer/RmlUi context
- Remove window resizing in ResizeToPhone (caused DPI mismatches)

Test framework:
- Fix SendMouseDown to use MOUSEEVENTF_MOVE before button down
- Remove double-scaling in ScaleToPhysical (WindowController handles it)
- All 5 UI navigation tests now pass

Kernel API:
- Add goHome() Lua function to return to home screen
- Stops any running third-party apps before navigating

Test app:
- Update sandbox-test to use goHome() instead of goBack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:52:15 +01:00
145 changed files with 13839 additions and 1882 deletions

17
.gitignore vendored
View File

@@ -9,7 +9,22 @@ build
/sandbox-test/test_results.json
# Test output files
screenshot_*.png
/tests/
*.png
test_*.json
test_*.txt
test_*.log
*_hierarchy.json
recorded_actions.json
# Junctions to src/main/assets/ for base-apps testing
# Create with: mklink /J icons src\main\assets\icons (etc.)
/icons
/scripts
/ui
# Sandbox data created during testing
/src/main/assets/sandbox_data/
# Misc
NUL

View File

@@ -0,0 +1,307 @@
-- browser.lua - Web browser functionality
-- Handles URL navigation, tabs, bookmarks, and history
local browser_doc = nil
local tabs = {}
local current_tab_id = 1
local history = {}
local bookmarks = {}
-- Sample page content
local pages = {
["example.com"] = {
title = "Example Domain",
secure = true,
content = [[
<div class="browser-page-title">Example Domain</div>
<div class="browser-page-text">
This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.
</div>
<div class="browser-page-text">
<span class="browser-page-link" onclick="navigateToUrl('iana.org')">More information...</span>
</div>
]]
},
["google.com"] = {
title = "Google",
secure = true,
content = [[
<div style="text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; font-weight: 500; color: #4285F4;">G</div>
<div style="font-size: 48px; font-weight: 500; color: #EA4335;">o</div>
<div style="font-size: 48px; font-weight: 500; color: #FBBC05;">o</div>
<div style="font-size: 48px; font-weight: 500; color: #4285F4;">g</div>
<div style="font-size: 48px; font-weight: 500; color: #34A853;">l</div>
<div style="font-size: 48px; font-weight: 500; color: #EA4335;">e</div>
<div style="margin-top: 32px;">
<input type="text" style="width: 80%; padding: 12px 20px; font-size: 16px; border-radius: 24px; background-color: #f1f3f4; border: none;" placeholder="Search Google"/>
</div>
</div>
]]
},
["mosis.app"] = {
title = "Mosis - Virtual Smartphone for VR",
secure = true,
content = [[
<div class="browser-page-title">Welcome to Mosis</div>
<div class="browser-page-text">
Mosis is a virtual smartphone OS for VR games and applications. Experience a phone-like device inside your virtual reality environment.
</div>
<div style="margin-top: 24px;">
<div class="browser-search-item" onclick="navigateToUrl('mosis.app/apps')">
<div class="browser-search-title">Browse Apps</div>
<div class="browser-search-desc">Discover apps for your virtual phone</div>
</div>
<div class="browser-search-item" onclick="navigateToUrl('mosis.app/developers')">
<div class="browser-search-title">For Developers</div>
<div class="browser-search-desc">Build apps for the Mosis platform</div>
</div>
</div>
]]
},
["default"] = {
title = "Page Not Found",
secure = false,
content = [[
<div style="text-align: center; padding: 60px 20px;">
<div style="font-size: 64px; color: #888888;">:(</div>
<div style="font-size: 24px; color: #333333; margin-top: 24px;">Page Not Found</div>
<div style="font-size: 16px; color: #666666; margin-top: 12px;">The requested page could not be loaded.</div>
</div>
]]
}
}
-- Initialize tabs
local function initTabs()
tabs = {
{id = 1, url = "example.com", title = "Example Domain"}
}
current_tab_id = 1
end
-- Initialize browser
function initBrowser(doc)
print("[Browser] Initializing...")
browser_doc = doc
initTabs()
loadPage(tabs[1].url)
end
-- Get current tab
local function getCurrentTab()
for _, tab in ipairs(tabs) do
if tab.id == current_tab_id then
return tab
end
end
return tabs[1]
end
-- Load a page
function loadPage(url)
if not browser_doc then return end
print("[Browser] Loading: " .. url)
-- Clean URL
url = url:gsub("^https?://", ""):gsub("^www%.", ""):gsub("/$", "")
-- Update current tab
local tab = getCurrentTab()
if tab then
tab.url = url
end
-- Add to history
table.insert(history, 1, {url = url, time = "Just now"})
-- Get page data
local page = pages[url] or pages["default"]
if tab then
tab.title = page.title
end
-- Update URL bar
local url_input = browser_doc:GetElementById("url-input")
if url_input then
url_input.value = url
end
-- Update secure icon
local secure_icon = browser_doc:GetElementById("secure-icon")
if secure_icon then
if page.secure then
secure_icon.inner_rml = "S"
secure_icon.style.color = "#4CAF50"
else
secure_icon.inner_rml = "!"
secure_icon.style.color = "#F44336"
end
end
-- Update page title
local title = browser_doc:GetElementById("page-title")
if title then
title.inner_rml = page.title
end
-- Update content
local content = browser_doc:GetElementById("browser-content")
if content then
content.inner_rml = [[<div class="browser-page">]] .. page.content .. [[</div>]]
end
-- Update tab count
updateTabCount()
end
-- Navigate to URL
function navigateToUrl(url)
loadPage(url)
end
-- Handle URL input
function onUrlSubmit()
if not browser_doc then return end
local input = browser_doc:GetElementById("url-input")
if input then
local url = input.value or ""
if url ~= "" then
loadPage(url)
end
end
end
-- Go back in history
function browserBack()
if #history > 1 then
table.remove(history, 1) -- Remove current page
local prev = history[1]
if prev then
loadPage(prev.url)
end
end
end
-- Go forward (simplified - just reload)
function browserForward()
if showToast then
showToast("No forward history")
end
end
-- Refresh page
function browserRefresh()
local tab = getCurrentTab()
if tab then
loadPage(tab.url)
if showToast then
showToast("Page refreshed")
end
end
end
-- Update tab count display
function updateTabCount()
if not browser_doc then return end
local count = browser_doc:GetElementById("tab-count")
if count then
count.inner_rml = tostring(#tabs)
end
end
-- Open new tab
function newTab()
local new_id = #tabs + 1
table.insert(tabs, {
id = new_id,
url = "mosis.app",
title = "New Tab"
})
current_tab_id = new_id
loadPage("mosis.app")
updateTabCount()
print("[Browser] New tab opened: " .. new_id)
end
-- Show tabs view
function showTabs()
print("[Browser] Show tabs")
if showToast then
showToast(#tabs .. " tab(s) open")
end
end
-- Close current tab
function closeTab()
if #tabs > 1 then
for i, tab in ipairs(tabs) do
if tab.id == current_tab_id then
table.remove(tabs, i)
break
end
end
current_tab_id = tabs[1].id
loadPage(tabs[1].url)
updateTabCount()
else
if showToast then
showToast("Cannot close last tab")
end
end
end
-- Add to bookmarks
function addBookmark()
local tab = getCurrentTab()
if tab then
table.insert(bookmarks, {
url = tab.url,
title = tab.title
})
if showToast then
showToast("Bookmark added")
end
end
end
-- Show bookmarks
function showBookmarks()
print("[Browser] Show bookmarks")
if showToast then
showToast(#bookmarks .. " bookmark(s)")
end
end
-- Show history
function showHistory()
print("[Browser] Show history")
if showToast then
showToast(#history .. " items in history")
end
end
-- Share page
function sharePage()
local tab = getCurrentTab()
if tab then
print("[Browser] Share: " .. tab.url)
if showToast then
showToast("Share: " .. tab.url)
end
end
end
-- Show menu
function showBrowserMenu()
print("[Browser] Show menu")
-- TODO: Show dropdown menu
end
-- Go to home
function browserHome()
loadPage("mosis.app")
end

View File

@@ -0,0 +1,246 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="browser.lua"></script>
<title>Browser</title>
<style>
.browser-toolbar {
display: flex;
align-items: center;
padding: 8px;
background-color: #1E1E1E;
gap: 8px;
}
.browser-nav-btn {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 24px;
}
.browser-nav-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.browser-nav-btn:active {
background-color: rgba(255, 255, 255, 0.2);
}
.browser-nav-btn img {
width: 28px;
height: 28px;
pointer-events: none;
}
.browser-nav-btn.disabled {
opacity: 0.3;
}
.browser-url-bar {
flex: 1;
display: flex;
align-items: center;
padding: 10px 16px;
background-color: #2D2D2D;
border-radius: 20px;
cursor: pointer;
}
.browser-url-bar:hover {
background-color: #3D3D3D;
}
.browser-secure-icon {
font-size: 16px;
color: #4CAF50;
margin-right: 8px;
}
.browser-url {
flex: 1;
font-size: 16px;
color: #FFFFFF;
background: transparent;
border: none;
}
.browser-content {
flex: 1;
background-color: #FFFFFF;
overflow: auto;
}
.browser-page {
padding: 16px;
color: #000000;
}
.browser-page-title {
font-size: 24px;
font-weight: bold;
color: #1a0dab;
margin-bottom: 16px;
}
.browser-page-text {
font-size: 16px;
line-height: 1.6;
color: #333333;
margin-bottom: 16px;
}
.browser-page-link {
color: #1a0dab;
cursor: pointer;
}
.browser-page-link:hover {
text-decoration: underline;
}
.browser-search-item {
margin-bottom: 20px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
}
.browser-search-item:hover {
background-color: #f0f0f0;
}
.browser-search-title {
font-size: 18px;
color: #1a0dab;
margin-bottom: 4px;
}
.browser-search-url {
font-size: 14px;
color: #006621;
margin-bottom: 4px;
}
.browser-search-desc {
font-size: 16px;
color: #545454;
line-height: 1.4;
}
.browser-bottom-bar {
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #1E1E1E;
border-top: 1px solid #333333;
}
.browser-tab-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
cursor: pointer;
color: #B3B3B3;
}
.browser-tab-btn:hover {
color: #FFFFFF;
background-color: rgba(255, 255, 255, 0.05);
}
.browser-tab-btn img {
width: 28px;
height: 28px;
margin-bottom: 4px;
pointer-events: none;
}
.browser-tab-btn span {
font-size: 14px;
}
.browser-tabs-indicator {
padding: 6px 10px;
border: 1px solid #B3B3B3;
border-radius: 6px;
font-size: 14px;
color: #B3B3B3;
}
</style>
</head>
<body class="app-screen" onload="initLayout(document); initBrowser(document)" data-model="browser">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- Browser Toolbar -->
<div class="browser-toolbar">
<div class="browser-nav-btn" onclick="browserBack()">
<img src="../../icons/back.tga"/>
</div>
<div class="browser-nav-btn" onclick="browserForward()">
<img src="../../icons/forward.tga"/>
</div>
<div class="browser-url-bar">
<span class="browser-secure-icon" id="secure-icon">S</span>
<input class="browser-url" type="text" value="example.com" id="url-input" onchange="onUrlSubmit()"/>
</div>
<div class="browser-nav-btn" onclick="browserRefresh()">
<img src="../../icons/refresh.tga"/>
</div>
<div class="browser-nav-btn" onclick="showBrowserMenu()">
<img src="../../icons/more.tga"/>
</div>
</div>
<!-- Browser Content -->
<div class="browser-content" id="browser-content">
<div class="browser-page">
<div class="browser-page-title" id="page-title">Example Domain</div>
<div class="browser-page-text">
This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.
</div>
<div class="browser-page-text">
<span class="browser-page-link" onclick="navigateToUrl('iana.org')">More information...</span>
</div>
</div>
</div>
<!-- Bottom Bar -->
<div class="browser-bottom-bar">
<div class="browser-tab-btn" onclick="browserHome()">
<img src="../../icons/home.tga"/>
<span>Home</span>
</div>
<div class="browser-tab-btn" onclick="showTabs()">
<span class="browser-tabs-indicator" id="tab-count">1</span>
<span>Tabs</span>
</div>
<div class="browser-tab-btn" onclick="newTab()">
<img src="../../icons/add.tga"/>
<span>New Tab</span>
</div>
<div class="browser-tab-btn" onclick="showBrowserMenu()">
<img src="../../icons/menu.tga"/>
<span>Menu</span>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.browser",
"name": "Browser",
"version": "1.0.0",
"version_code": 1,
"entry": "browser.rml",
"icon": "../../icons/browser.tga",
"description": "Web browser application",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"network"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,360 @@
-- camera.lua - Camera app functionality
-- Handles capture, modes, flash, zoom, and camera switching
local camera_doc = nil
local current_mode = "photo" -- photo, video, portrait, night
local flash_mode = "auto" -- auto, on, off
local timer_mode = "off" -- off, 3, 10
local is_front_camera = false
local is_recording = false
local zoom_level = 1.0
local photo_count = 0
local video_duration = 0
local video_timer_id = nil
-- Camera modes
local modes = {"Night", "Portrait", "Photo", "Video", "More"}
-- Initialize camera
function initCamera(doc)
print("[Camera] Initializing...")
camera_doc = doc
updateModeDisplay()
updateFlashDisplay()
updateTimerDisplay()
updateZoomDisplay()
end
-- Update mode display
function updateModeDisplay()
if not camera_doc then return end
for _, mode in ipairs(modes) do
local mode_el = camera_doc:GetElementById("mode-" .. mode:lower())
if mode_el then
if mode:lower() == current_mode then
mode_el:SetClass("active", true)
else
mode_el:SetClass("active", false)
end
end
end
-- Update capture button appearance for video mode
local capture_btn = camera_doc:GetElementById("capture-button")
if capture_btn then
if current_mode == "video" then
if is_recording then
capture_btn.inner_rml = [[<div class="capture-btn-stop"></div>]]
else
capture_btn.inner_rml = [[<div class="capture-btn-video"></div>]]
end
else
capture_btn.inner_rml = [[<div class="capture-btn-inner"></div>]]
end
end
end
-- Switch camera mode
function switchMode(mode)
print("[Camera] Switching to mode: " .. mode)
current_mode = mode:lower()
-- Stop recording if switching from video
if is_recording then
stopRecording()
end
updateModeDisplay()
if showToast then
showToast(mode .. " mode")
end
end
-- Toggle flash
function toggleFlash()
if flash_mode == "auto" then
flash_mode = "on"
elseif flash_mode == "on" then
flash_mode = "off"
else
flash_mode = "auto"
end
print("[Camera] Flash: " .. flash_mode)
updateFlashDisplay()
end
-- Update flash display
function updateFlashDisplay()
if not camera_doc then return end
local indicator = camera_doc:GetElementById("flash-indicator")
if indicator then
local text = "Flash: "
if flash_mode == "auto" then
text = text .. "Auto"
elseif flash_mode == "on" then
text = text .. "On"
else
text = text .. "Off"
end
indicator.inner_rml = text
end
end
-- Toggle timer
function toggleTimer()
if timer_mode == "off" then
timer_mode = "3"
elseif timer_mode == "3" then
timer_mode = "10"
else
timer_mode = "off"
end
print("[Camera] Timer: " .. timer_mode)
updateTimerDisplay()
end
-- Update timer display
function updateTimerDisplay()
if not camera_doc then return end
local indicator = camera_doc:GetElementById("timer-indicator")
if indicator then
local text = "Timer: "
if timer_mode == "off" then
text = text .. "Off"
else
text = text .. timer_mode .. "s"
end
indicator.inner_rml = text
end
end
-- Zoom in
function zoomIn()
if zoom_level < 10.0 then
zoom_level = math.min(zoom_level + 0.5, 10.0)
updateZoomDisplay()
end
end
-- Zoom out
function zoomOut()
if zoom_level > 0.5 then
zoom_level = math.max(zoom_level - 0.5, 0.5)
updateZoomDisplay()
end
end
-- Update zoom display
function updateZoomDisplay()
if not camera_doc then return end
local indicator = camera_doc:GetElementById("zoom-level")
if indicator then
indicator.inner_rml = string.format("%.1fx", zoom_level)
end
print("[Camera] Zoom: " .. zoom_level)
end
-- Switch camera (front/back)
function switchCamera()
is_front_camera = not is_front_camera
print("[Camera] Switched to " .. (is_front_camera and "front" or "back") .. " camera")
if showToast then
showToast(is_front_camera and "Front camera" or "Back camera")
end
-- Update viewfinder placeholder
local placeholder = camera_doc:GetElementById("viewfinder-placeholder")
if placeholder then
local icon = is_front_camera and "F" or "C"
placeholder.inner_rml = [[
<div class="viewfinder-placeholder-icon">]] .. icon .. [[</div>
<div>]] .. (is_front_camera and "Front Camera" or "Camera Preview") .. [[</div>
<div style="font-size: 14px; margin-top: 8px; color: #555555;">Tap to focus</div>
]]
end
end
-- Capture photo or start/stop video
function capture()
if current_mode == "video" then
if is_recording then
stopRecording()
else
startRecording()
end
else
takePhoto()
end
end
-- Take a photo
function takePhoto()
print("[Camera] Taking photo...")
-- Check timer
if timer_mode ~= "off" then
local delay = tonumber(timer_mode) * 1000
if showToast then
showToast("Timer: " .. timer_mode .. " seconds")
end
if setTimeout then
setTimeout(function()
actuallyTakePhoto()
end, delay)
else
actuallyTakePhoto()
end
else
actuallyTakePhoto()
end
end
-- Actually capture the photo
function actuallyTakePhoto()
photo_count = photo_count + 1
print("[Camera] Photo captured! Total: " .. photo_count)
-- Flash effect
if flash_mode == "on" or (flash_mode == "auto" and not is_front_camera) then
-- Simulate flash
end
-- Show capture animation/feedback
local viewfinder = camera_doc:GetElementById("camera-viewfinder")
if viewfinder then
viewfinder.style["background-color"] = "#FFFFFF"
if setTimeout then
setTimeout(function()
viewfinder.style["background-color"] = "#1a1a1a"
end, 100)
end
end
if showToast then
showToast("Photo saved")
end
-- Update gallery preview
updateGalleryPreview()
end
-- Start video recording
function startRecording()
print("[Camera] Starting recording...")
is_recording = true
video_duration = 0
updateModeDisplay()
-- Start timer
if setInterval then
video_timer_id = setInterval(function()
video_duration = video_duration + 1
updateRecordingTime()
end, 1000)
end
-- Show recording indicator
local indicator = camera_doc:GetElementById("recording-indicator")
if indicator then
indicator.style.display = "flex"
end
end
-- Stop video recording
function stopRecording()
print("[Camera] Stopping recording...")
is_recording = false
-- Stop timer
if video_timer_id and clearInterval then
clearInterval(video_timer_id)
video_timer_id = nil
end
updateModeDisplay()
-- Hide recording indicator
local indicator = camera_doc:GetElementById("recording-indicator")
if indicator then
indicator.style.display = "none"
end
if showToast then
local minutes = math.floor(video_duration / 60)
local seconds = video_duration % 60
showToast(string.format("Video saved (%02d:%02d)", minutes, seconds))
end
video_duration = 0
end
-- Update recording time display
function updateRecordingTime()
if not camera_doc then return end
local time_el = camera_doc:GetElementById("recording-time")
if time_el then
local minutes = math.floor(video_duration / 60)
local seconds = video_duration % 60
time_el.inner_rml = string.format("%02d:%02d", minutes, seconds)
end
end
-- Update gallery preview
function updateGalleryPreview()
-- In a real app, this would show the last captured photo
local preview = camera_doc:GetElementById("gallery-preview")
if preview then
preview.style["background-color"] = "#4CAF50"
end
end
-- Open gallery
function openGallery()
print("[Camera] Opening gallery...")
if navigateTo then
navigateTo("gallery")
else
if showToast then
showToast("Gallery: " .. photo_count .. " photos")
end
end
end
-- Open camera settings
function openCameraSettings()
print("[Camera] Opening settings...")
if showToast then
showToast("Camera settings")
end
end
-- Handle tap to focus
function onViewfinderTap(x, y)
print("[Camera] Focus at: " .. x .. ", " .. y)
-- Move focus indicator
local focus = camera_doc:GetElementById("focus-indicator")
if focus then
focus.style.left = x .. "px"
focus.style.top = y .. "px"
focus.style.opacity = "1"
if setTimeout then
setTimeout(function()
focus.style.opacity = "0.8"
end, 500)
end
end
end

View File

@@ -3,15 +3,14 @@
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="camera.lua"></script>
<title>Camera</title>
<style>
.camera-screen {
width: 100%;
height: 100%;
background-color: #000000;
display: flex;
flex-direction: column;
}
/* Top Controls */
@@ -21,16 +20,16 @@
align-items: center;
padding: 16px;
position: absolute;
top: 0;
top: 36px;
left: 0;
right: 0;
z-index: 10;
}
.camera-btn {
width: 56px;
height: 56px;
border-radius: 28px;
width: 48px;
height: 48px;
border-radius: 24px;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
@@ -42,9 +41,14 @@
background-color: rgba(255, 255, 255, 0.2);
}
.camera-btn:active {
background-color: rgba(255, 255, 255, 0.3);
}
.camera-btn img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
pointer-events: none;
}
/* Viewfinder */
@@ -66,7 +70,6 @@
position: relative;
}
/* Placeholder for camera feed - replace with shared texture */
.viewfinder-placeholder {
color: #666666;
font-size: 18px;
@@ -136,10 +139,10 @@
}
.camera-mode {
font-size: 18px;
font-size: 16px;
color: #B3B3B3;
cursor: pointer;
padding: 10px;
padding: 8px;
}
.camera-mode.active {
@@ -156,7 +159,7 @@
display: flex;
justify-content: space-around;
align-items: center;
padding: 24px 32px;
padding: 20px 32px;
background-color: rgba(0, 0, 0, 0.6);
}
@@ -172,10 +175,15 @@
justify-content: center;
}
.gallery-preview:hover {
background-color: #444444;
}
.gallery-preview img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
opacity: 0.7;
pointer-events: none;
}
.capture-btn {
@@ -206,10 +214,17 @@
background-color: #FFFFFF;
}
.capture-btn.video .capture-btn-inner {
width: 28px;
height: 28px;
border-radius: 6px;
.capture-btn-video {
width: 24px;
height: 24px;
border-radius: 12px;
background-color: #F44336;
}
.capture-btn-stop {
width: 24px;
height: 24px;
border-radius: 4px;
background-color: #F44336;
}
@@ -229,51 +244,51 @@
}
.switch-camera-btn img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
pointer-events: none;
}
/* Settings Overlay */
.settings-value {
/* Indicators */
.indicator {
position: absolute;
bottom: 200px;
background-color: rgba(0, 0, 0, 0.4);
padding: 6px 12px;
border-radius: 12px;
font-size: 14px;
color: #FFFFFF;
}
.flash-indicator {
top: 100px;
left: 16px;
}
.timer-indicator {
top: 100px;
right: 16px;
}
.recording-indicator {
top: 140px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.6);
padding: 10px 18px;
border-radius: 22px;
font-size: 18px;
color: #FFFFFF;
display: none;
align-items: center;
gap: 8px;
}
/* Flash modes */
.flash-indicator {
position: absolute;
top: 80px;
left: 16px;
background-color: rgba(0, 0, 0, 0.4);
padding: 8px 14px;
border-radius: 14px;
font-size: 16px;
color: #FFFFFF;
.recording-dot {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: #F44336;
}
/* Timer indicator */
.timer-indicator {
position: absolute;
top: 80px;
right: 16px;
background-color: rgba(0, 0, 0, 0.4);
padding: 8px 14px;
border-radius: 14px;
font-size: 16px;
color: #FFFFFF;
}
/* Zoom slider */
/* Zoom control */
.zoom-control {
position: absolute;
bottom: 180px;
bottom: 200px;
left: 50%;
transform: translateX(-50%);
display: flex;
@@ -282,20 +297,24 @@
}
.zoom-btn {
width: 44px;
height: 44px;
border-radius: 22px;
width: 40px;
height: 40px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-size: 20px;
color: #FFFFFF;
cursor: pointer;
}
.zoom-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.zoom-level {
font-size: 18px;
font-size: 16px;
color: #FFD700;
font-weight: 600;
min-width: 48px;
@@ -303,20 +322,30 @@
}
</style>
</head>
<body class="camera-screen">
<body class="app-screen camera-screen" onload="initLayout(document); initCamera(document)">
<!-- System Status Bar (transparent) -->
<div class="system-status-bar" style="background-color: transparent; position: absolute; top: 0; left: 0; right: 0; z-index: 20;">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- Top Bar -->
<div class="camera-top-bar">
<div class="camera-btn" onclick="goBack()">
<img src="../../icons/close.tga"/>
</div>
<div style="display: flex; gap: 12px;">
<div class="camera-btn">
<div class="camera-btn" onclick="toggleFlash()">
<img src="../../icons/flash.tga"/>
</div>
<div class="camera-btn">
<div class="camera-btn" onclick="toggleTimer()">
<img src="../../icons/timer.tga"/>
</div>
<div class="camera-btn">
<div class="camera-btn" onclick="openCameraSettings()">
<img src="../../icons/settings.tga"/>
</div>
</div>
@@ -325,11 +354,10 @@
<!-- Viewfinder Area -->
<div class="viewfinder-container">
<div class="viewfinder" id="camera-viewfinder">
<!-- This is where the shared camera texture would be rendered -->
<div class="viewfinder-placeholder">
<div class="viewfinder-placeholder" id="viewfinder-placeholder">
<div class="viewfinder-placeholder-icon">C</div>
<div>Camera Preview</div>
<div style="font-size: 16px; margin-top: 8px; color: #555555;">
<div style="font-size: 14px; margin-top: 8px; color: #555555;">
Tap to focus
</div>
</div>
@@ -343,41 +371,43 @@
</div>
<!-- Focus Indicator -->
<div class="focus-indicator"></div>
<div class="focus-indicator" id="focus-indicator"></div>
</div>
<!-- Flash Indicator -->
<div class="flash-indicator">Flash: Auto</div>
<!-- Timer Indicator -->
<div class="timer-indicator">Timer: Off</div>
<!-- Indicators -->
<div class="indicator flash-indicator" id="flash-indicator">Flash: Auto</div>
<div class="indicator timer-indicator" id="timer-indicator">Timer: Off</div>
<div class="indicator recording-indicator" id="recording-indicator">
<div class="recording-dot"></div>
<span id="recording-time">00:00</span>
</div>
<!-- Zoom Control -->
<div class="zoom-control">
<div class="zoom-btn">-</div>
<span class="zoom-level">1.0x</span>
<div class="zoom-btn">+</div>
<div class="zoom-btn" onclick="zoomOut()">-</div>
<span class="zoom-level" id="zoom-level">1.0x</span>
<div class="zoom-btn" onclick="zoomIn()">+</div>
</div>
</div>
<!-- Camera Modes -->
<div class="camera-modes">
<span class="camera-mode">Night</span>
<span class="camera-mode">Portrait</span>
<span class="camera-mode active">Photo</span>
<span class="camera-mode">Video</span>
<span class="camera-mode">More</span>
<span id="mode-night" class="camera-mode" onclick="switchMode('Night')">Night</span>
<span id="mode-portrait" class="camera-mode" onclick="switchMode('Portrait')">Portrait</span>
<span id="mode-photo" class="camera-mode active" onclick="switchMode('Photo')">Photo</span>
<span id="mode-video" class="camera-mode" onclick="switchMode('Video')">Video</span>
<span id="mode-more" class="camera-mode" onclick="switchMode('More')">More</span>
</div>
<!-- Bottom Bar -->
<div class="camera-bottom-bar">
<div class="gallery-preview">
<div class="gallery-preview" id="gallery-preview" onclick="openGallery()">
<img src="../../icons/gallery.tga"/>
</div>
<div class="capture-btn" id="capture-button">
<div class="capture-btn" id="capture-button" onclick="capture()">
<div class="capture-btn-inner"></div>
</div>
<div class="switch-camera-btn">
<div class="switch-camera-btn" onclick="switchCamera()">
<img src="../../icons/switch-camera.tga"/>
</div>
</div>

View File

@@ -0,0 +1,19 @@
{
"id": "com.mosis.camera",
"name": "Camera",
"version": "1.0.0",
"version_code": 1,
"entry": "camera.rml",
"icon": "../../icons/camera.tga",
"description": "Camera and photo capture",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"camera",
"storage"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,374 @@
-- contacts.lua - Contacts management functionality
-- Handles contact list, search, details, and actions
local contacts_doc = nil
local contacts_data = {}
local filtered_contacts = {}
local search_query = ""
local selected_contact = nil
-- Avatar colors for contacts
local avatar_colors = {
"#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3",
"#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A",
"#CDDC39", "#FFC107", "#FF9800", "#FF5722", "#795548"
}
-- Get color for contact based on name
local function getAvatarColor(name)
local sum = 0
for i = 1, #name do
sum = sum + string.byte(name, i)
end
return avatar_colors[(sum % #avatar_colors) + 1]
end
-- Initialize contacts data
local function initContactsData()
contacts_data = {
{id = "1", name = "Alice Johnson", phone = "+1 555-0101", email = "alice@email.com", company = "Tech Corp"},
{id = "2", name = "Andrew Smith", phone = "+1 555-0102", email = "andrew@email.com", company = "Design Studio"},
{id = "3", name = "Bob Williams", phone = "+1 555-0201", email = "bob@email.com", company = ""},
{id = "4", name = "Carol Davis", phone = "+1 555-0301", email = "carol.d@email.com", company = "Marketing Inc"},
{id = "5", name = "Chris Miller", phone = "+1 555-0302", email = "", company = ""},
{id = "6", name = "David Brown", phone = "+1 555-0401", email = "david.b@email.com", company = "Finance LLC"},
{id = "7", name = "Emma Wilson", phone = "+1 555-0501", email = "emma@email.com", company = "Creative Agency"},
{id = "8", name = "Frank Garcia", phone = "+1 555-0601", email = "", company = ""},
{id = "9", name = "Grace Lee", phone = "+1 555-0701", email = "grace.lee@email.com", company = "Healthcare Plus"},
{id = "10", name = "Henry Taylor", phone = "+1 555-0801", email = "henry@email.com", company = ""},
{id = "11", name = "Isabella Martinez", phone = "+1 555-0901", email = "isabella@email.com", company = "Education Center"},
{id = "12", name = "John Doe", phone = "+1 555-1234", email = "john.doe@email.com", company = "Software Inc"},
{id = "13", name = "Kate Thompson", phone = "+1 555-1101", email = "", company = "Legal Partners"},
{id = "14", name = "Liam Anderson", phone = "+1 555-1201", email = "liam.a@email.com", company = ""},
{id = "15", name = "Mary Taylor", phone = "+1 555-0601", email = "mary@email.com", company = "Consulting Group"},
{id = "16", name = "Michael Lee", phone = "+1 555-0602", email = "michael.l@email.com", company = ""},
{id = "17", name = "Noah White", phone = "+1 555-1401", email = "noah@email.com", company = "Real Estate Co"},
{id = "18", name = "Olivia Harris", phone = "+1 555-1501", email = "", company = ""},
{id = "19", name = "Peter Clark", phone = "+1 555-1601", email = "peter.c@email.com", company = "Manufacturing Ltd"},
{id = "20", name = "Sarah Anderson", phone = "+1 555-0701", email = "sarah@email.com", company = "Media Group"},
}
-- Sort by name
table.sort(contacts_data, function(a, b)
return a.name:lower() < b.name:lower()
end)
filtered_contacts = contacts_data
end
-- Initialize contacts
function initContacts(doc)
print("[Contacts] Initializing...")
contacts_doc = doc
initContactsData()
renderContacts()
end
-- Render contacts list grouped by first letter
function renderContacts()
if not contacts_doc then return end
local container = contacts_doc:GetElementById("contacts-list")
if not container then return end
local html = ""
local current_letter = ""
for _, contact in ipairs(filtered_contacts) do
local first_letter = contact.name:sub(1, 1):upper()
-- Add letter header if new letter
if first_letter ~= current_letter then
current_letter = first_letter
html = html .. [[
<div class="contact-letter">]] .. first_letter .. [[</div>
]]
end
-- Get avatar color and initial
local color = getAvatarColor(contact.name)
local initial = contact.name:sub(1, 1):upper()
html = html .. [[
<div class="contact-item" onclick="selectContact(']] .. contact.id .. [[')">
<div class="contact-avatar" style="background-color: ]] .. color .. [[;">]] .. initial .. [[</div>
<div class="contact-info">
<div class="contact-name">]] .. contact.name .. [[</div>
<div class="contact-phone">]] .. contact.phone .. [[</div>
</div>
<div class="contact-call-btn" onclick="callContact(']] .. contact.id .. [['); event.stopPropagation();">
<img src="../../icons/phone.tga"/>
</div>
</div>
]]
end
if #filtered_contacts == 0 then
html = [[
<div style="text-align: center; padding: 40px; color: #888888;">
<div style="font-size: 18px;">No contacts found</div>
</div>
]]
end
container.inner_rml = html
end
-- Search contacts
function searchContacts(query)
print("[Contacts] Searching: " .. query)
search_query = query:lower()
if search_query == "" then
filtered_contacts = contacts_data
else
filtered_contacts = {}
for _, contact in ipairs(contacts_data) do
if contact.name:lower():find(search_query, 1, true) or
contact.phone:find(search_query, 1, true) or
(contact.email and contact.email:lower():find(search_query, 1, true)) then
table.insert(filtered_contacts, contact)
end
end
end
renderContacts()
end
-- Handle search input
function onSearchInput(element)
local query = element.value or ""
searchContacts(query)
end
-- Select a contact to view details
function selectContact(contact_id)
print("[Contacts] Selected contact: " .. contact_id)
-- Find contact by ID
for _, contact in ipairs(contacts_data) do
if contact.id == contact_id then
selected_contact = contact
break
end
end
if selected_contact then
showContactDetail()
end
end
-- Show contact detail view
function showContactDetail()
if not selected_contact or not contacts_doc then return end
-- Store contact info for detail screen
if mosis and mosis.state then
mosis.state.set("selected_contact", selected_contact)
end
-- Navigate to detail screen
if navigateTo then
navigateTo("contact_detail")
else
-- Show inline detail
showContactDetailInline()
end
end
-- Show contact detail inline (if navigation not available)
function showContactDetailInline()
if not contacts_doc then return end
local detail = contacts_doc:GetElementById("contact-detail")
local list = contacts_doc:GetElementById("contacts-list-container")
if detail and list then
list.style.display = "none"
detail.style.display = "flex"
renderContactDetail()
end
end
-- Render contact detail
function renderContactDetail()
if not selected_contact or not contacts_doc then return end
local color = getAvatarColor(selected_contact.name)
local initial = selected_contact.name:sub(1, 1):upper()
local detail_avatar = contacts_doc:GetElementById("detail-avatar")
local detail_name = contacts_doc:GetElementById("detail-name")
local detail_info = contacts_doc:GetElementById("detail-info")
if detail_avatar then
detail_avatar.style["background-color"] = color
detail_avatar.inner_rml = initial
end
if detail_name then
detail_name.inner_rml = selected_contact.name
end
if detail_info then
local html = ""
-- Phone
html = html .. [[
<div class="detail-row" onclick="callContact(']] .. selected_contact.id .. [[')">
<div class="detail-icon"><img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/></div>
<div class="detail-content">
<div class="detail-label">Phone</div>
<div class="detail-value">]] .. selected_contact.phone .. [[</div>
</div>
<div class="detail-action"><img src="../../icons/call_small.tga" style="width: 24px; height: 24px;"/></div>
</div>
]]
-- Email
if selected_contact.email and selected_contact.email ~= "" then
html = html .. [[
<div class="detail-row">
<div class="detail-icon"><img src="../../icons/email.tga" style="width: 24px; height: 24px;"/></div>
<div class="detail-content">
<div class="detail-label">Email</div>
<div class="detail-value">]] .. selected_contact.email .. [[</div>
</div>
</div>
]]
end
-- Company
if selected_contact.company and selected_contact.company ~= "" then
html = html .. [[
<div class="detail-row">
<div class="detail-icon"><img src="../../icons/work.tga" style="width: 24px; height: 24px;"/></div>
<div class="detail-content">
<div class="detail-label">Company</div>
<div class="detail-value">]] .. selected_contact.company .. [[</div>
</div>
</div>
]]
end
detail_info.inner_rml = html
end
end
-- Hide contact detail
function hideContactDetail()
if not contacts_doc then return end
local detail = contacts_doc:GetElementById("contact-detail")
local list = contacts_doc:GetElementById("contacts-list-container")
if detail and list then
detail.style.display = "none"
list.style.display = "flex"
end
selected_contact = nil
end
-- Call a contact
function callContact(contact_id)
print("[Contacts] Calling contact: " .. contact_id)
local contact = nil
for _, c in ipairs(contacts_data) do
if c.id == contact_id then
contact = c
break
end
end
if contact then
-- Store call info
if mosis and mosis.state then
mosis.state.set("current_call", {
number = contact.phone,
name = contact.name
})
end
-- Navigate to calling screen
if navigateTo then
navigateTo("calling")
else
if showToast then
showToast("Calling " .. contact.name)
end
end
end
end
-- Message a contact
function messageContact(contact_id)
print("[Contacts] Messaging contact: " .. contact_id)
local contact = nil
for _, c in ipairs(contacts_data) do
if c.id == contact_id then
contact = c
break
end
end
if contact then
if mosis and mosis.state then
mosis.state.set("chat_contact", {
name = contact.name,
phone = contact.phone
})
end
if navigateTo then
navigateTo("chat")
else
if showToast then
showToast("Message " .. contact.name)
end
end
end
end
-- Add new contact
function addContact()
print("[Contacts] Add new contact")
if navigateTo then
navigateTo("add_contact")
else
if showToast then
showToast("Add contact")
end
end
end
-- Edit contact
function editContact(contact_id)
print("[Contacts] Edit contact: " .. contact_id)
if showToast then
showToast("Edit contact")
end
end
-- Delete contact
function deleteContact(contact_id)
print("[Contacts] Delete contact: " .. contact_id)
-- Find and remove contact
for i, c in ipairs(contacts_data) do
if c.id == contact_id then
table.remove(contacts_data, i)
break
end
end
-- Re-filter and render
searchContacts(search_query)
hideContactDetail()
if showToast then
showToast("Contact deleted")
end
end

View File

@@ -0,0 +1,352 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="contacts.lua"></script>
<title>Contacts</title>
<style>
.contacts-list {
flex: 1;
overflow: auto;
}
.contact-letter {
padding: 8px 16px;
font-size: 18px;
font-weight: 500;
color: #BB86FC;
background-color: #1E1E1E;
position: sticky;
top: 0;
}
.contact-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
}
.contact-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.contact-item:active {
background-color: rgba(255, 255, 255, 0.1);
}
.contact-avatar {
width: 48px;
height: 48px;
border-radius: 24px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #000000;
}
.contact-info {
flex: 1;
}
.contact-name {
font-size: 18px;
color: #FFFFFF;
}
.contact-phone {
font-size: 16px;
color: #B3B3B3;
margin-top: 2px;
}
.contact-call-btn {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 28px;
}
.contact-call-btn:hover {
background-color: rgba(76, 175, 80, 0.2);
}
.contact-call-btn:active {
background-color: rgba(76, 175, 80, 0.3);
}
.contact-call-btn img {
width: 28px;
height: 28px;
pointer-events: none;
}
/* Contact Detail View */
#contact-detail {
display: none;
flex-direction: column;
flex: 1;
}
.detail-header {
padding: 32px 16px;
text-align: center;
background-color: #1E1E1E;
}
.detail-avatar {
width: 96px;
height: 96px;
border-radius: 48px;
margin: 0 auto 16px auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
color: #000000;
}
.detail-name {
font-size: 28px;
font-weight: 500;
color: #FFFFFF;
}
.detail-actions {
display: flex;
justify-content: center;
gap: 32px;
padding: 20px;
background-color: #1E1E1E;
border-bottom: 1px solid #333333;
}
.detail-action-btn {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 8px;
}
.detail-action-btn:hover {
opacity: 0.8;
}
.detail-action-icon {
width: 48px;
height: 48px;
border-radius: 24px;
background-color: #BB86FC;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.detail-action-icon img {
width: 24px;
height: 24px;
pointer-events: none;
}
.detail-action-label {
font-size: 14px;
color: #FFFFFF;
}
.detail-info {
flex: 1;
padding: 16px;
}
.detail-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #333333;
cursor: pointer;
}
.detail-row:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.detail-icon {
width: 40px;
margin-right: 16px;
display: flex;
justify-content: center;
}
.detail-content {
flex: 1;
}
.detail-label {
font-size: 14px;
color: #888888;
}
.detail-value {
font-size: 18px;
color: #FFFFFF;
margin-top: 4px;
}
.detail-action {
padding: 8px;
}
/* Phone app bottom tabs */
.phone-tabs {
height: 72px;
background-color: #1E1E1E;
display: flex;
}
.phone-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
color: #666666;
}
.phone-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.phone-tab.active {
color: #BB86FC;
}
.phone-tab img {
width: 28px;
height: 28px;
margin-bottom: 4px;
}
.phone-tab span {
font-size: 14px;
}
/* Search style adjustments */
.search-input {
flex: 1;
background-color: transparent;
font-size: 18px;
color: #FFFFFF;
}
</style>
</head>
<body class="app-screen" onload="initLayout(document); initContacts(document)" data-model="contacts">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Contacts</span>
<div class="app-bar-actions">
<div class="app-bar-action" onclick="addContact()">
<img src="../../icons/add.tga"/>
</div>
</div>
</div>
<!-- Contacts List Container -->
<div id="contacts-list-container" class="app-content" style="display: flex; flex-direction: column;">
<!-- Search Bar -->
<div class="search-bar">
<img src="../../icons/search.tga" class="search-icon" style="width: 24px; height: 24px;"/>
<input class="search-input" type="text" placeholder="Search contacts" onchange="onSearchInput(this)"/>
</div>
<!-- Contacts List -->
<div class="contacts-list" id="contacts-list">
<!-- Populated by Lua -->
</div>
</div>
<!-- Contact Detail View -->
<div id="contact-detail">
<div class="detail-header">
<div class="detail-avatar" id="detail-avatar">A</div>
<div class="detail-name" id="detail-name">Contact Name</div>
</div>
<div class="detail-actions">
<div class="detail-action-btn" onclick="callContact(selected_contact and selected_contact.id or '')">
<div class="detail-action-icon">
<img src="../../icons/call_small.tga"/>
</div>
<span class="detail-action-label">Call</span>
</div>
<div class="detail-action-btn" onclick="messageContact(selected_contact and selected_contact.id or '')">
<div class="detail-action-icon" style="background-color: #03DAC6;">
<img src="../../icons/message.tga"/>
</div>
<span class="detail-action-label">Message</span>
</div>
<div class="detail-action-btn" onclick="editContact(selected_contact and selected_contact.id or '')">
<div class="detail-action-icon" style="background-color: #FF9800;">
<img src="../../icons/edit.tga"/>
</div>
<span class="detail-action-label">Edit</span>
</div>
</div>
<div class="detail-info" id="detail-info">
<!-- Populated by Lua -->
</div>
<div style="padding: 16px;">
<div class="btn btn-outlined" style="width: 100%; text-align: center;" onclick="hideContactDetail()">
Back to Contacts
</div>
</div>
</div>
<!-- FAB -->
<div class="btn-fab" onclick="addContact()">
<img src="../../icons/add.tga" style="width: 32px; height: 32px;"/>
</div>
<!-- Phone App Bottom Tabs -->
<div class="phone-tabs">
<div class="phone-tab" onclick="navigateTo('dialer')">
<img src="../../icons/dialpad.tga"/>
<span>Keypad</span>
</div>
<div class="phone-tab" onclick="switchTab('recent')">
<img src="../../icons/history.tga"/>
<span>Recent</span>
</div>
<div class="phone-tab active">
<img src="../../icons/contacts.tga"/>
<span>Contacts</span>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.contacts",
"name": "Contacts",
"version": "1.0.0",
"version_code": 1,
"entry": "contacts.rml",
"icon": "../../icons/contacts.tga",
"description": "Contact list and management",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"contacts"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,227 @@
-- calling.lua - In-call screen functionality
-- Handles call state, duration timer, and call controls
local calling_doc = nil
local call_state = "connecting" -- connecting, active, ended
local call_start_time = 0
local call_duration = 0
local timer_id = nil
local is_muted = false
local is_speaker = false
local is_on_hold = false
-- Call info
local call_number = ""
local call_name = ""
-- Initialize calling screen
function initCalling(doc)
print("[Calling] Initializing...")
calling_doc = doc
-- Get call info from state or use defaults
if mosis and mosis.state then
local call_info = mosis.state.get("current_call")
if call_info then
call_number = call_info.number or ""
call_name = call_info.name or call_number
end
end
-- Fallback test data
if call_number == "" then
call_number = "+1 555-0101"
call_name = "Alice Johnson"
end
-- Update UI
updateCallInfo()
-- Simulate connection after delay
if setTimeout then
setTimeout(function()
setCallState("active")
end, 2000)
else
setCallState("active")
end
end
-- Update call info display
function updateCallInfo()
if not calling_doc then return end
local name_el = calling_doc:GetElementById("call-name")
local number_el = calling_doc:GetElementById("call-number")
local status_el = calling_doc:GetElementById("call-status")
local avatar_el = calling_doc:GetElementById("call-avatar")
if name_el then
name_el.inner_rml = call_name
end
if number_el then
number_el.inner_rml = call_number
end
if avatar_el then
-- Get first letter for avatar
local initial = call_name:sub(1, 1):upper()
avatar_el.inner_rml = initial
end
end
-- Set call state
function setCallState(state)
print("[Calling] State changed to: " .. state)
call_state = state
local status_el = calling_doc:GetElementById("call-status")
local timer_el = calling_doc:GetElementById("call-timer")
if state == "connecting" then
if status_el then
status_el.inner_rml = "Calling..."
end
if timer_el then
timer_el.style.display = "none"
end
elseif state == "active" then
if status_el then
status_el.inner_rml = "Connected"
end
if timer_el then
timer_el.style.display = "block"
end
-- Start duration timer
startCallTimer()
elseif state == "ended" then
if status_el then
status_el.inner_rml = "Call ended"
end
stopCallTimer()
-- Return to dialer after delay
if setTimeout then
setTimeout(function()
if goBack then
goBack()
end
end, 1500)
end
end
end
-- Start call duration timer
function startCallTimer()
call_start_time = os.time and os.time() or 0
call_duration = 0
if setInterval then
timer_id = setInterval(function()
call_duration = call_duration + 1
updateTimerDisplay()
end, 1000)
end
end
-- Stop call timer
function stopCallTimer()
if timer_id and clearInterval then
clearInterval(timer_id)
timer_id = nil
end
end
-- Update timer display
function updateTimerDisplay()
local timer_el = calling_doc:GetElementById("call-timer")
if timer_el then
local minutes = math.floor(call_duration / 60)
local seconds = call_duration % 60
timer_el.inner_rml = string.format("%02d:%02d", minutes, seconds)
end
end
-- Toggle mute
function toggleMute()
is_muted = not is_muted
print("[Calling] Mute: " .. tostring(is_muted))
local mute_btn = calling_doc:GetElementById("btn-mute")
if mute_btn then
if is_muted then
mute_btn:SetClass("active", true)
else
mute_btn:SetClass("active", false)
end
end
if showToast then
showToast(is_muted and "Muted" or "Unmuted")
end
end
-- Toggle speaker
function toggleSpeaker()
is_speaker = not is_speaker
print("[Calling] Speaker: " .. tostring(is_speaker))
local speaker_btn = calling_doc:GetElementById("btn-speaker")
if speaker_btn then
if is_speaker then
speaker_btn:SetClass("active", true)
else
speaker_btn:SetClass("active", false)
end
end
if showToast then
showToast(is_speaker and "Speaker on" or "Speaker off")
end
end
-- Toggle hold
function toggleHold()
is_on_hold = not is_on_hold
print("[Calling] Hold: " .. tostring(is_on_hold))
local hold_btn = calling_doc:GetElementById("btn-hold")
if hold_btn then
if is_on_hold then
hold_btn:SetClass("active", true)
else
hold_btn:SetClass("active", false)
end
end
local status_el = calling_doc:GetElementById("call-status")
if status_el then
if is_on_hold then
status_el.inner_rml = "On hold"
else
status_el.inner_rml = "Connected"
end
end
end
-- Show dialpad
function showDialpad()
print("[Calling] Show dialpad")
if showToast then
showToast("Dialpad")
end
end
-- Add call (conference)
function addCall()
print("[Calling] Add call")
if showToast then
showToast("Add call")
end
end
-- End call
function endCall()
print("[Calling] Ending call")
setCallState("ended")
end

View File

@@ -0,0 +1,191 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="calling.lua"></script>
<title>Calling</title>
<style>
.calling-screen {
width: 100%;
height: 100%;
background: linear-gradient(180deg, #1a237e 0%, #000000 100%);
background-color: #1a237e;
display: flex;
flex-direction: column;
align-items: center;
}
.calling-header {
padding-top: 60px;
text-align: center;
}
.call-avatar {
width: 120px;
height: 120px;
border-radius: 60px;
background-color: #BB86FC;
margin: 0 auto 24px auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #000000;
}
.call-name {
font-size: 32px;
font-weight: 500;
color: #FFFFFF;
margin-bottom: 8px;
}
.call-number {
font-size: 18px;
color: #B3B3B3;
margin-bottom: 16px;
}
.call-status {
font-size: 18px;
color: #4CAF50;
margin-bottom: 8px;
}
.call-timer {
font-size: 24px;
color: #FFFFFF;
font-weight: 300;
display: none;
}
.calling-content {
flex: 1;
}
.call-controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 40px;
gap: 32px;
max-width: 320px;
}
.call-control-btn {
width: 72px;
height: 72px;
border-radius: 36px;
background-color: rgba(255, 255, 255, 0.15);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.call-control-btn:hover {
background-color: rgba(255, 255, 255, 0.25);
}
.call-control-btn.active {
background-color: #FFFFFF;
}
.call-control-btn img {
width: 32px;
height: 32px;
pointer-events: none;
}
.call-control-label {
font-size: 12px;
color: #FFFFFF;
margin-top: 8px;
text-align: center;
}
.call-control-btn.active .call-control-label {
color: #000000;
}
.end-call-container {
padding: 40px;
}
.end-call-btn {
width: 80px;
height: 80px;
border-radius: 40px;
background-color: #F44336;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0 auto;
}
.end-call-btn:hover {
background-color: #E53935;
transform: scale(1.05);
}
.end-call-btn:active {
background-color: #C62828;
transform: scale(0.95);
}
.end-call-btn img {
width: 40px;
height: 40px;
pointer-events: none;
}
</style>
</head>
<body class="calling-screen" onload="initCalling(document)">
<!-- Caller Info -->
<div class="calling-header">
<div class="call-avatar" id="call-avatar">A</div>
<div class="call-name" id="call-name">Alice Johnson</div>
<div class="call-number" id="call-number">+1 555-0101</div>
<div class="call-status" id="call-status">Calling...</div>
<div class="call-timer" id="call-timer">00:00</div>
</div>
<!-- Spacer -->
<div class="calling-content"></div>
<!-- Call Controls -->
<div class="call-controls">
<div id="btn-mute" class="call-control-btn" onclick="toggleMute()">
<img src="../../icons/mic_off.tga"/>
<span class="call-control-label">Mute</span>
</div>
<div id="btn-keypad" class="call-control-btn" onclick="showDialpad()">
<img src="../../icons/dialpad.tga"/>
<span class="call-control-label">Keypad</span>
</div>
<div id="btn-speaker" class="call-control-btn" onclick="toggleSpeaker()">
<img src="../../icons/volume.tga"/>
<span class="call-control-label">Speaker</span>
</div>
<div id="btn-add" class="call-control-btn" onclick="addCall()">
<img src="../../icons/add.tga"/>
<span class="call-control-label">Add call</span>
</div>
<div id="btn-hold" class="call-control-btn" onclick="toggleHold()">
<img src="../../icons/pause.tga"/>
<span class="call-control-label">Hold</span>
</div>
</div>
<!-- End Call Button -->
<div class="end-call-container">
<div class="end-call-btn" onclick="endCall()">
<img src="../../icons/call_end.tga"/>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,235 @@
-- dialer.lua - Phone dialer functionality
-- Handles dial pad input, call management, and call history
local dialer_doc = nil
local dial_number = ""
local call_history = {}
local current_tab = "keypad" -- keypad, recent, contacts
-- Sample call history data
local function initCallHistory()
call_history = {
{name = "Alice Johnson", number = "+1 555-0101", type = "incoming", time = "2:34 PM", duration = "5:23"},
{name = "Bob Williams", number = "+1 555-0201", type = "outgoing", time = "1:15 PM", duration = "2:45"},
{name = "Carol Davis", number = "+1 555-0301", type = "missed", time = "Yesterday", duration = nil},
{name = "David Brown", number = "+1 555-0401", type = "incoming", time = "Yesterday", duration = "12:30"},
{name = "Emma Wilson", number = "+1 555-0501", type = "outgoing", time = "Mon", duration = "3:15"},
{name = "+1 555-9999", number = "+1 555-9999", type = "missed", time = "Mon", duration = nil},
{name = "John Doe", number = "+1 555-1234", type = "incoming", time = "Sun", duration = "8:42"},
}
end
-- Initialize dialer
function initDialer(doc)
print("[Dialer] Initializing...")
dialer_doc = doc
dial_number = ""
initCallHistory()
updateDialDisplay()
end
-- Update the dial display
function updateDialDisplay()
if not dialer_doc then return end
local display = dialer_doc:GetElementById("dial-display")
if display then
if dial_number == "" then
display.inner_rml = '<span style="color: #666666;">Enter number</span>'
else
-- Format number for display
local formatted = formatPhoneNumber(dial_number)
display.inner_rml = formatted
end
end
end
-- Format phone number for display
function formatPhoneNumber(number)
local len = #number
if len <= 3 then
return number
elseif len <= 6 then
return number:sub(1,3) .. "-" .. number:sub(4)
elseif len <= 10 then
return "(" .. number:sub(1,3) .. ") " .. number:sub(4,6) .. "-" .. number:sub(7)
else
return "+1 (" .. number:sub(1,3) .. ") " .. number:sub(4,6) .. "-" .. number:sub(7,10)
end
end
-- Handle dial key press
function dial_press(key)
print("[Dialer] Key pressed: " .. key)
if #dial_number < 15 then
dial_number = dial_number .. key
updateDialDisplay()
-- Play haptic/sound feedback if available
if mosis and mosis.haptic then
mosis.haptic.vibrate(10)
end
end
end
-- Handle backspace
function dial_backspace()
if #dial_number > 0 then
dial_number = dial_number:sub(1, -2)
updateDialDisplay()
end
end
-- Clear dial number
function dial_clear()
dial_number = ""
updateDialDisplay()
end
-- Make a call
function make_call()
if dial_number == "" then
print("[Dialer] Cannot call: no number entered")
if showToast then
showToast("Enter a number to call")
end
return
end
print("[Dialer] Calling: " .. dial_number)
-- Add to call history
table.insert(call_history, 1, {
name = dial_number,
number = dial_number,
type = "outgoing",
time = "Just now",
duration = nil
})
-- Navigate to calling screen
if navigateTo then
-- Store call info for the calling screen
if mosis and mosis.state then
mosis.state.set("current_call", {
number = dial_number,
name = getContactName(dial_number),
start_time = os.time and os.time() or 0
})
end
navigateTo("calling")
else
-- Fallback: load calling screen directly
local calling_path = dialer_doc:GetSourceURL():gsub("dialer.rml", "calling.rml")
if mosis and mosis.loadDocument then
mosis.loadDocument(calling_path)
end
end
end
-- Get contact name by number (returns number if not found)
function getContactName(number)
for _, call in ipairs(call_history) do
if call.number == number and call.name ~= number then
return call.name
end
end
return number
end
-- Switch tabs
function switchTab(tab_name)
print("[Dialer] Switching to tab: " .. tab_name)
current_tab = tab_name
-- Update tab UI
local tabs = {"keypad", "recent", "contacts"}
for _, tab in ipairs(tabs) do
local tab_el = dialer_doc:GetElementById("tab-" .. tab)
if tab_el then
if tab == tab_name then
tab_el:SetClass("active", true)
else
tab_el:SetClass("active", false)
end
end
end
-- Show/hide content
local keypad_content = dialer_doc:GetElementById("keypad-content")
local recent_content = dialer_doc:GetElementById("recent-content")
if keypad_content then
keypad_content.style.display = (tab_name == "keypad") and "flex" or "none"
end
if recent_content then
recent_content.style.display = (tab_name == "recent") and "block" or "none"
end
-- Render recent calls if switching to that tab
if tab_name == "recent" then
renderCallHistory()
end
end
-- Render call history
function renderCallHistory()
local container = dialer_doc:GetElementById("recent-list")
if not container then return end
local html = ""
for _, call in ipairs(call_history) do
local icon_color = "#4CAF50" -- incoming = green
local icon = "phone.tga"
if call.type == "outgoing" then
icon_color = "#2196F3" -- blue
icon = "call_made.tga"
elseif call.type == "missed" then
icon_color = "#F44336" -- red
icon = "call_missed.tga"
end
local duration_text = call.duration or "Missed"
html = html .. [[
<div class="call-history-item" onclick="callNumber(']] .. call.number .. [[')">
<div class="call-history-icon" style="background-color: ]] .. icon_color .. [[;">
<img src="../../icons/]] .. icon .. [[" style="width: 24px; height: 24px;"/>
</div>
<div class="call-history-info">
<div class="call-history-name">]] .. call.name .. [[</div>
<div class="call-history-meta">]] .. call.type .. " - " .. call.time .. [[</div>
</div>
<div class="call-history-time">]] .. duration_text .. [[</div>
</div>
]]
end
container.inner_rml = html
end
-- Call a number from history
function callNumber(number)
dial_number = number:gsub("[^%d+]", "") -- Remove non-digit chars except +
updateDialDisplay()
switchTab("keypad")
make_call()
end
-- Long press on 0 for +
function dial_long_press_zero()
if dial_number == "" or dial_number:sub(-1) ~= "0" then
dial_press("+")
else
-- Replace last 0 with +
dial_number = dial_number:sub(1, -2) .. "+"
updateDialDisplay()
end
end
-- Long press on * for pause
function dial_long_press_star()
dial_press(",") -- Comma is standard pause character
end

View File

@@ -0,0 +1,225 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="dialer.lua"></script>
<title>Phone</title>
<style>
.dialer-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* Keypad content */
#keypad-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* Recent calls content */
#recent-content {
flex: 1;
display: none;
overflow: auto;
}
.call-history-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
}
.call-history-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.call-history-icon {
width: 40px;
height: 40px;
border-radius: 20px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.call-history-info {
flex: 1;
}
.call-history-name {
font-size: 16px;
color: #FFFFFF;
}
.call-history-meta {
font-size: 14px;
color: #888888;
margin-top: 2px;
}
.call-history-time {
font-size: 14px;
color: #888888;
}
/* Phone app bottom tabs */
.phone-tabs {
height: 72px;
background-color: #1E1E1E;
display: flex;
}
.phone-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
color: #666666;
}
.phone-tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.phone-tab.active {
color: #BB86FC;
}
.phone-tab img {
width: 28px;
height: 28px;
margin-bottom: 4px;
}
.phone-tab span {
font-size: 14px;
}
</style>
</head>
<body class="app-screen" onload="initLayout(document); initDialer(document)">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Phone</span>
</div>
<!-- Dialer Content -->
<div class="dialer-content">
<!-- Keypad View -->
<div id="keypad-content">
<!-- Dial Display -->
<div class="dial-display" id="dial-display">
<span style="color: #666666;">Enter number</span>
</div>
<!-- Dial Pad -->
<div class="dial-pad">
<div class="dial-key" onclick="dial_press('1')">
<span class="dial-key-number">1</span>
<span class="dial-key-letters"></span>
</div>
<div class="dial-key" onclick="dial_press('2')">
<span class="dial-key-number">2</span>
<span class="dial-key-letters">ABC</span>
</div>
<div class="dial-key" onclick="dial_press('3')">
<span class="dial-key-number">3</span>
<span class="dial-key-letters">DEF</span>
</div>
<div class="dial-key" onclick="dial_press('4')">
<span class="dial-key-number">4</span>
<span class="dial-key-letters">GHI</span>
</div>
<div class="dial-key" onclick="dial_press('5')">
<span class="dial-key-number">5</span>
<span class="dial-key-letters">JKL</span>
</div>
<div class="dial-key" onclick="dial_press('6')">
<span class="dial-key-number">6</span>
<span class="dial-key-letters">MNO</span>
</div>
<div class="dial-key" onclick="dial_press('7')">
<span class="dial-key-number">7</span>
<span class="dial-key-letters">PQRS</span>
</div>
<div class="dial-key" onclick="dial_press('8')">
<span class="dial-key-number">8</span>
<span class="dial-key-letters">TUV</span>
</div>
<div class="dial-key" onclick="dial_press('9')">
<span class="dial-key-number">9</span>
<span class="dial-key-letters">WXYZ</span>
</div>
<div class="dial-key" onclick="dial_press('*')">
<span class="dial-key-number">*</span>
<span class="dial-key-letters"></span>
</div>
<div class="dial-key" onclick="dial_press('0')">
<span class="dial-key-number">0</span>
<span class="dial-key-letters">+</span>
</div>
<div class="dial-key" onclick="dial_press('#')">
<span class="dial-key-number">#</span>
<span class="dial-key-letters"></span>
</div>
</div>
<!-- Call Actions -->
<div class="dial-actions">
<div style="width: 56px;"></div>
<div class="dial-call-btn" onclick="make_call()">
<img src="../../icons/call_small.tga" style="width: 32px; height: 32px; pointer-events: none;"/>
</div>
<div class="btn-icon" onclick="dial_backspace()" style="width: 56px; height: 56px;">
<img src="../../icons/backspace.tga" style="width: 32px; height: 32px; pointer-events: none;"/>
</div>
</div>
</div>
<!-- Recent Calls View -->
<div id="recent-content">
<div id="recent-list">
<!-- Populated by Lua -->
</div>
</div>
</div>
<!-- Phone App Bottom Tabs -->
<div class="phone-tabs">
<div id="tab-keypad" class="phone-tab active" onclick="switchTab('keypad')">
<img src="../../icons/dialpad.tga"/>
<span>Keypad</span>
</div>
<div id="tab-recent" class="phone-tab" onclick="switchTab('recent')">
<img src="../../icons/history.tga"/>
<span>Recent</span>
</div>
<div id="tab-contacts" class="phone-tab" onclick="navigateTo('contacts')">
<img src="../../icons/contacts.tga"/>
<span>Contacts</span>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.dialer",
"name": "Phone",
"version": "1.0.0",
"version_code": 1,
"entry": "dialer.rml",
"icon": "../../icons/phone.tga",
"description": "Phone dialer and call interface",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"phone"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,200 @@
-- home.lua - Home screen dynamic app rendering
-- Handles system apps and discovered third-party apps
-- System apps with their navigation keys and colors
local system_apps = {
-- Row 1
{name = "Phone", icon = "phone", color = "#4CAF50", nav = "dialer"},
{name = "Messages", icon = "message", color = "#2196F3", nav = "messages"},
{name = "Contacts", icon = "contacts", color = "#FF9800", nav = "contacts"},
{name = "Browser", icon = "browser", color = "#F44336", nav = "browser"},
-- Row 2
{name = "Gallery", icon = "gallery", color = "#9C27B0", nav = nil},
{name = "Camera", icon = "camera", color = "#00BCD4", nav = "camera"},
{name = "Settings", icon = "settings", color = "#607D8B", nav = "settings"},
{name = "Music", icon = "music", color = "#E91E63", nav = "music"},
-- Row 3
{name = "Calendar", icon = "calendar", color = "#3F51B5", nav = nil},
{name = "Clock", icon = "clock", color = "#009688", nav = nil},
{name = "Notes", icon = "notes", color = "#795548", nav = nil},
{name = "Maps", icon = "maps", color = "#FF5722", nav = nil},
-- Row 4
{name = "Store", icon = "store", color = "#8BC34A", nav = "store"},
{name = "Files", icon = "files", color = "#CDDC39", nav = nil},
{name = "Calculator", icon = "calculator", color = "#FFC107", nav = nil},
{name = "Weather", icon = "weather", color = "#673AB7", nav = nil},
}
-- State
local installed_apps = {}
local home_document = nil -- Store document reference
-- Initialize on load (receives document from onload event)
function initHome(doc)
print("[Home] Initializing home screen...")
home_document = doc
-- Get installed third-party apps
if mosis and mosis.apps then
installed_apps = mosis.apps.getInstalled() or {}
print("[Home] Found " .. #installed_apps .. " installed apps")
-- Filter to only third-party (non-system) apps
local third_party = {}
for _, app in ipairs(installed_apps) do
if not app.is_system_app then
table.insert(third_party, app)
print("[Home] Third-party app: " .. app.name .. " (" .. app.package_id .. ")")
end
end
installed_apps = third_party
else
print("[Home] Warning: mosis.apps API not available")
installed_apps = {}
end
-- Render dynamic apps
renderThirdPartyApps()
end
-- Generate a color based on package_id
function getAppColor(package_id)
local colors = {
"#BB86FC", "#03DAC6", "#FF9800", "#2196F3",
"#4CAF50", "#F44336", "#E91E63", "#3F51B5",
"#009688", "#795548", "#FF5722", "#673AB7"
}
-- Simple hash of package_id to pick a color
local hash = 0
for i = 1, #package_id do
hash = hash + package_id:byte(i)
end
return colors[(hash % #colors) + 1]
end
-- Get first letter for placeholder icon
function getAppInitial(name)
return name:sub(1, 1):upper()
end
-- Render third-party apps into the grid
function renderThirdPartyApps()
-- Use stored document reference
if not home_document then
print("[Home] Could not get document reference")
return
end
local grid = home_document:GetElementById("third-party-apps")
if not grid then
print("[Home] third-party-apps container not found")
return
end
-- Clear existing content
grid.inner_rml = ""
if #installed_apps == 0 then
print("[Home] No third-party apps to display")
return
end
-- Build HTML for each app
local html = ""
for _, app in ipairs(installed_apps) do
local color = getAppColor(app.package_id)
local initial = getAppInitial(app.name)
local icon_html
-- Check if app has an icon
if app.icon and app.icon ~= "" then
local icon_path
-- Check if icon is already a full path (starts with / or contains :/)
if app.icon:sub(1, 1) == "/" or app.icon:find(":/") then
-- Already a full path
icon_path = app.icon
elseif app.install_path and app.install_path ~= "" then
-- Relative filename - construct full path from install_path
icon_path = app.install_path .. "/" .. app.icon
else
icon_path = app.icon
end
-- Use file:// prefix for absolute paths to prevent RmlUi URL resolution
local src_path = icon_path
if icon_path:sub(1, 1) == "/" then
src_path = "file://" .. icon_path
end
-- Use img tag for actual icon
icon_html = '<img src="' .. src_path .. '" style="width: 48px; height: 48px;"/>'
print("[Home] Loading icon: " .. src_path)
else
-- Fallback to initial letter
icon_html = '<span style="font-size: 28px; color: #000000;">' .. initial .. '</span>'
end
html = html .. [[
<div class="app-icon">
<div class="app-icon-image" style="background-color: ]] .. color .. [[;"
onclick="launchThirdPartyApp(']] .. app.package_id .. [[')">
]] .. icon_html .. [[
</div>
<span class="app-icon-label">]] .. app.name .. [[</span>
</div>
]]
end
grid.inner_rml = html
print("[Home] Rendered " .. #installed_apps .. " third-party apps")
end
-- Get app info by package_id
function getAppInfo(package_id)
for _, app in ipairs(installed_apps) do
if app.package_id == package_id then
return app
end
end
return nil
end
-- Launch a third-party app
function launchThirdPartyApp(package_id)
print("[Home] Launching app: " .. package_id)
if mosis and mosis.apps then
local success = mosis.apps.launch(package_id)
if success then
print("[Home] App sandbox started: " .. package_id)
-- Get app info for sandbox switching and UI loading
local app_info = getAppInfo(package_id)
if app_info and app_info.install_path and app_info.entry_point then
-- Switch sandbox context to this app (registers timer, fs, json, crypto APIs)
if switchAppSandbox then
switchAppSandbox(package_id, app_info.install_path)
print("[Home] Sandbox context switched to: " .. package_id)
end
-- Now load the app's UI document
local entry_path = app_info.install_path .. "/" .. app_info.entry_point
print("[Home] Loading app screen: " .. entry_path)
local loaded = loadScreen(entry_path)
if loaded then
print("[Home] App UI loaded: " .. package_id)
else
print("[Home] Failed to load app UI: " .. entry_path)
end
else
print("[Home] App info missing entry point: " .. package_id)
end
else
print("[Home] Failed to launch app: " .. package_id)
end
else
print("[Home] Cannot launch app: mosis.apps not available")
end
end
-- initHome() is called via onload in home.rml

View File

@@ -43,30 +43,31 @@
width: 100%;
}
/* Third-party apps use same sizing as system apps */
#third-party-apps .app-icon {
width: 25%;
box-sizing: border-box;
}
#third-party-apps .app-icon-image {
width: 56px;
height: 56px;
border-radius: 14px;
width: 72px;
height: 72px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px auto;
margin: 0 auto 12px auto;
cursor: pointer;
}
#third-party-apps .app-icon-image:hover {
opacity: 0.8;
transform: scale(1.05);
}
#third-party-apps .app-icon-label {
display: block;
text-align: center;
font-size: 12px;
font-size: 16px;
color: #FFFFFF;
}
</style>

View File

@@ -0,0 +1,16 @@
{
"id": "com.mosis.home",
"name": "Home",
"version": "1.0.0",
"version_code": 1,
"entry": "home.rml",
"icon": "../../icons/home.tga",
"description": "Mosis home screen and app launcher",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [],
"min_api_version": 1
}

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.messages",
"name": "Messages",
"version": "1.0.0",
"version_code": 1,
"entry": "messages.rml",
"icon": "../../icons/message.tga",
"description": "SMS and messaging application",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"sms"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,398 @@
-- messages.lua - Messages app functionality
-- Handles conversation list and individual chats
local messages_doc = nil
local conversations = {}
local current_conversation = nil
local current_messages = {}
-- Avatar colors
local avatar_colors = {
"#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3",
"#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#FF9800"
}
local function getAvatarColor(name)
local sum = 0
for i = 1, #name do
sum = sum + string.byte(name, i)
end
return avatar_colors[(sum % #avatar_colors) + 1]
end
-- Initialize conversations data
local function initConversationsData()
conversations = {
{
id = "1",
name = "Alice Johnson",
phone = "+1 555-0101",
last_message = "Hey! Are you coming to the party tonight?",
time = "2:34 PM",
unread = 2,
messages = {
{sender = "them", text = "Hey!", time = "2:30 PM"},
{sender = "them", text = "What are you up to?", time = "2:31 PM"},
{sender = "me", text = "Not much, just working", time = "2:32 PM"},
{sender = "them", text = "Cool! There's a party at Mike's tonight", time = "2:33 PM"},
{sender = "them", text = "Hey! Are you coming to the party tonight?", time = "2:34 PM"},
}
},
{
id = "2",
name = "Bob Williams",
phone = "+1 555-0201",
last_message = "Thanks for the help yesterday!",
time = "1:15 PM",
unread = 0,
messages = {
{sender = "them", text = "Hey, can you help me with something?", time = "Yesterday"},
{sender = "me", text = "Sure, what do you need?", time = "Yesterday"},
{sender = "them", text = "I need help moving some furniture", time = "Yesterday"},
{sender = "me", text = "No problem, I'll be there at 2", time = "Yesterday"},
{sender = "them", text = "Thanks for the help yesterday!", time = "1:15 PM"},
}
},
{
id = "3",
name = "Carol Davis",
phone = "+1 555-0301",
last_message = "The meeting has been rescheduled to Friday",
time = "Yesterday",
unread = 0,
messages = {
{sender = "them", text = "Hi, are you free for a meeting tomorrow?", time = "Monday"},
{sender = "me", text = "Let me check my calendar", time = "Monday"},
{sender = "me", text = "Yes, I'm free at 3pm", time = "Monday"},
{sender = "them", text = "The meeting has been rescheduled to Friday", time = "Yesterday"},
}
},
{
id = "4",
name = "David Brown",
phone = "+1 555-0401",
last_message = "Can you send me the files?",
time = "Yesterday",
unread = 1,
messages = {
{sender = "them", text = "Hey, do you have the project files?", time = "Yesterday"},
{sender = "me", text = "Which ones?", time = "Yesterday"},
{sender = "them", text = "Can you send me the files?", time = "Yesterday"},
}
},
{
id = "5",
name = "Emma Wilson",
phone = "+1 555-0501",
last_message = "See you at the coffee shop!",
time = "Mon",
unread = 0,
messages = {
{sender = "me", text = "Want to grab coffee later?", time = "Mon"},
{sender = "them", text = "Sure! What time?", time = "Mon"},
{sender = "me", text = "How about 4pm at the usual place?", time = "Mon"},
{sender = "them", text = "See you at the coffee shop!", time = "Mon"},
}
},
{
id = "6",
name = "Frank Miller",
phone = "+1 555-0601",
last_message = "Great game last night!",
time = "Sun",
unread = 0,
messages = {
{sender = "them", text = "Did you watch the game?", time = "Sun"},
{sender = "me", text = "Yes! It was amazing!", time = "Sun"},
{sender = "them", text = "Great game last night!", time = "Sun"},
}
},
{
id = "7",
name = "Grace Lee",
phone = "+1 555-0701",
last_message = "Happy birthday! :)",
time = "Sat",
unread = 0,
messages = {
{sender = "them", text = "Happy birthday! :)", time = "Sat"},
{sender = "me", text = "Thank you so much! :)", time = "Sat"},
}
},
}
end
-- Initialize messages app
function initMessages(doc)
print("[Messages] Initializing...")
messages_doc = doc
initConversationsData()
renderConversations()
end
-- Render conversation list
function renderConversations()
if not messages_doc then return end
local container = messages_doc:GetElementById("conversations-list")
if not container then return end
local html = ""
for _, conv in ipairs(conversations) do
local color = getAvatarColor(conv.name)
local initial = conv.name:sub(1, 1):upper()
local unread_badge = ""
if conv.unread > 0 then
unread_badge = [[<div class="conversation-unread">]] .. conv.unread .. [[</div>]]
end
html = html .. [[
<div class="conversation-item" onclick="openConversation(']] .. conv.id .. [[')">
<div class="conversation-avatar" style="background-color: ]] .. color .. [[;">]] .. initial .. [[</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">]] .. conv.name .. [[</span>
<span class="conversation-time">]] .. conv.time .. [[</span>
</div>
<div class="conversation-preview">]] .. conv.last_message .. [[</div>
</div>
]] .. unread_badge .. [[
</div>
]]
end
container.inner_rml = html
end
-- Open a conversation
function openConversation(conv_id)
print("[Messages] Opening conversation: " .. conv_id)
-- Find conversation
for _, conv in ipairs(conversations) do
if conv.id == conv_id then
current_conversation = conv
current_messages = conv.messages
conv.unread = 0 -- Mark as read
break
end
end
if current_conversation then
-- Store for chat screen
if mosis and mosis.state then
mosis.state.set("current_chat", {
id = current_conversation.id,
name = current_conversation.name,
phone = current_conversation.phone
})
end
-- Navigate to chat
if navigateTo then
navigateTo("chat")
else
-- Inline chat view
showChatInline()
end
end
end
-- Show chat inline
function showChatInline()
if not messages_doc then return end
local list = messages_doc:GetElementById("conversations-container")
local chat = messages_doc:GetElementById("chat-container")
if list and chat then
list.style.display = "none"
chat.style.display = "flex"
renderChat()
end
end
-- Hide chat and return to list
function hideChat()
if not messages_doc then return end
local list = messages_doc:GetElementById("conversations-container")
local chat = messages_doc:GetElementById("chat-container")
if list and chat then
chat.style.display = "none"
list.style.display = "flex"
renderConversations() -- Refresh to update unread counts
end
current_conversation = nil
end
-- Render chat messages
function renderChat()
if not messages_doc or not current_conversation then return end
-- Update header
local name_el = messages_doc:GetElementById("chat-name")
local avatar_el = messages_doc:GetElementById("chat-avatar")
if name_el then
name_el.inner_rml = current_conversation.name
end
if avatar_el then
local color = getAvatarColor(current_conversation.name)
local initial = current_conversation.name:sub(1, 1):upper()
avatar_el.style["background-color"] = color
avatar_el.inner_rml = initial
end
-- Render messages
local container = messages_doc:GetElementById("chat-messages")
if not container then return end
local html = ""
for _, msg in ipairs(current_messages) do
local class = msg.sender == "me" and "message-sent" or "message-received"
html = html .. [[
<div class="message-bubble ]] .. class .. [[">]] .. msg.text .. [[</div>
]]
end
container.inner_rml = html
-- Scroll to bottom
-- Note: RmlUi may need specific handling for scroll
end
-- Send a message
function sendMessage()
if not messages_doc or not current_conversation then return end
local input = messages_doc:GetElementById("message-input")
if not input then return end
local text = input.value or ""
if text == "" then return end
print("[Messages] Sending: " .. text)
-- Add message to current conversation
table.insert(current_messages, {
sender = "me",
text = text,
time = "Just now"
})
-- Update conversation preview
current_conversation.last_message = text
current_conversation.time = "Just now"
-- Clear input
input.value = ""
-- Re-render chat
renderChat()
-- Simulate reply after delay
if setTimeout then
setTimeout(function()
simulateReply()
end, 2000 + math.random(1000, 3000))
end
end
-- Simulate a reply
function simulateReply()
if not current_conversation then return end
local replies = {
"That's great!",
"I see",
"Sounds good!",
"Let me think about it",
"Sure thing!",
"OK!",
"Thanks!",
"Got it",
"Nice!",
"Interesting..."
}
local reply = replies[math.random(#replies)]
table.insert(current_messages, {
sender = "them",
text = reply,
time = "Just now"
})
current_conversation.last_message = reply
current_conversation.time = "Just now"
renderChat()
end
-- Handle input keypress (for Enter to send)
function onMessageKeypress(event)
if event.key == "Return" or event.key == "Enter" then
sendMessage()
return true
end
return false
end
-- Start new conversation
function newConversation()
print("[Messages] New conversation")
if showToast then
showToast("New message")
end
end
-- Search conversations
function searchConversations(query)
print("[Messages] Searching: " .. query)
-- TODO: Implement search filtering
end
-- Delete conversation
function deleteConversation(conv_id)
print("[Messages] Deleting conversation: " .. conv_id)
for i, conv in ipairs(conversations) do
if conv.id == conv_id then
table.remove(conversations, i)
break
end
end
renderConversations()
if showToast then
showToast("Conversation deleted")
end
end
-- Call contact from chat
function callFromChat()
if not current_conversation then return end
print("[Messages] Calling from chat: " .. current_conversation.name)
if mosis and mosis.state then
mosis.state.set("current_call", {
number = current_conversation.phone,
name = current_conversation.name
})
end
if navigateTo then
navigateTo("calling")
else
if showToast then
showToast("Calling " .. current_conversation.name)
end
end
end

View File

@@ -0,0 +1,299 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="messages.lua"></script>
<title>Messages</title>
<style>
.conversations-list {
flex: 1;
overflow: auto;
}
.conversation-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
}
.conversation-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.conversation-item:active {
background-color: rgba(255, 255, 255, 0.1);
}
.conversation-avatar {
width: 56px;
height: 56px;
border-radius: 28px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: #000000;
}
.conversation-content {
flex: 1;
min-width: 0;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.conversation-name {
font-size: 18px;
font-weight: 500;
color: #FFFFFF;
}
.conversation-time {
font-size: 14px;
color: #666666;
}
.conversation-preview {
font-size: 16px;
color: #B3B3B3;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-unread {
width: 24px;
height: 24px;
border-radius: 12px;
background-color: #BB86FC;
color: #000000;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px;
}
/* Chat View */
#chat-container {
display: none;
flex-direction: column;
flex: 1;
}
.chat-header {
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #1E1E1E;
}
.chat-avatar {
width: 48px;
height: 48px;
border-radius: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #000000;
}
.chat-header-info {
flex: 1;
}
.chat-header-name {
font-size: 18px;
font-weight: 500;
color: #FFFFFF;
}
.chat-header-status {
font-size: 14px;
color: #4CAF50;
}
.chat-messages {
flex: 1;
overflow: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.message-bubble {
max-width: 75%;
padding: 12px 16px;
border-radius: 18px;
font-size: 18px;
line-height: 1.4;
}
.message-sent {
align-self: flex-end;
background-color: #BB86FC;
color: #000000;
border-bottom-right-radius: 4px;
}
.message-received {
align-self: flex-start;
background-color: #2D2D2D;
color: #FFFFFF;
border-bottom-left-radius: 4px;
}
.chat-input-bar {
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #1E1E1E;
gap: 8px;
}
.chat-input {
flex: 1;
padding: 12px 18px;
background-color: #2D2D2D;
border-radius: 24px;
color: #FFFFFF;
font-size: 18px;
}
.chat-input:hover {
background-color: #3D3D3D;
}
.chat-input:focus {
background-color: #353535;
}
.chat-send-btn {
width: 56px;
height: 56px;
border-radius: 28px;
background-color: #BB86FC;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.chat-send-btn:hover {
background-color: #9C64FC;
}
.chat-send-btn:active {
background-color: #7C44DC;
}
.chat-send-btn img {
width: 28px;
height: 28px;
pointer-events: none;
}
</style>
</head>
<body class="app-screen" onload="initLayout(document); initMessages(document)" data-model="messages">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- Conversations List Container -->
<div id="conversations-container" style="display: flex; flex-direction: column; flex: 1;">
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Messages</span>
<div class="app-bar-actions">
<div class="app-bar-action">
<img src="../../icons/search.tga"/>
</div>
</div>
</div>
<!-- Conversations List -->
<div class="app-content with-nav">
<div class="conversations-list" id="conversations-list">
<!-- Populated by Lua -->
</div>
</div>
<!-- FAB -->
<div class="btn-fab" onclick="newConversation()">
<img src="../../icons/add.tga" style="width: 32px; height: 32px;"/>
</div>
<!-- System Navigation Bar -->
<div class="system-nav-bar">
<div class="system-nav-btn" onclick="onBackPressed()">
<img src="../../icons/back.tga"/>
</div>
<div class="system-nav-home" onclick="onHomePressed()"></div>
<div class="system-nav-btn" onclick="onRecentPressed()">
<img src="../../icons/menu.tga"/>
</div>
</div>
</div>
<!-- Chat Container -->
<div id="chat-container">
<!-- Chat Header -->
<div class="app-bar">
<div class="app-bar-back" onclick="hideChat()">
<img src="../../icons/back.tga"/>
</div>
<div class="chat-avatar" id="chat-avatar" style="background-color: #4CAF50;">J</div>
<div class="chat-header-info">
<div class="chat-header-name" id="chat-name">Contact</div>
<div class="chat-header-status">Online</div>
</div>
<div class="btn-icon" onclick="callFromChat()">
<img src="../../icons/phone.tga" style="width: 32px; height: 32px;"/>
</div>
<div class="btn-icon">
<img src="../../icons/more.tga" style="width: 32px; height: 32px;"/>
</div>
</div>
<!-- Messages -->
<div class="chat-messages" id="chat-messages">
<!-- Populated by Lua -->
</div>
<!-- Input Bar -->
<div class="chat-input-bar">
<div class="btn-icon" style="width: 48px; height: 48px;">
<img src="../../icons/add.tga" style="width: 28px; height: 28px;"/>
</div>
<input class="chat-input" type="text" placeholder="Type a message..." id="message-input"/>
<div class="chat-send-btn" onclick="sendMessage()">
<img src="../../icons/send.tga"/>
</div>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.music",
"name": "Music",
"version": "1.0.0",
"version_code": 1,
"entry": "music.rml",
"icon": "../../icons/music.tga",
"description": "Music player application",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"storage"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,388 @@
-- music.lua - Music player functionality
-- Handles playback, playlists, library, and now playing
local music_doc = nil
-- Player state
local player_state = {
is_playing = false,
is_shuffled = false,
repeat_mode = "off", -- off, all, one
current_time = 0,
duration = 234, -- 3:54
volume = 80
}
-- Current track
local current_track = {
id = "1",
title = "Midnight City",
artist = "M83",
album = "Hurry Up, We're Dreaming",
duration = 234,
art_color = "#667eea"
}
-- Playlists
local playlists = {
{id = "liked", name = "Liked Songs", count = 127, color = "#dc2626"},
{id = "daily1", name = "Daily Mix 1", count = 50, color = "#667eea"},
{id = "release", name = "Release Radar", count = 30, color = "#16a34a"},
{id = "chill", name = "Chill Vibes", count = 45, color = "#f093fb"},
{id = "workout", name = "Workout Mix", count = 35, color = "#2563eb"},
{id = "focus", name = "Focus Flow", count = 40, color = "#4facfe"}
}
-- Recently played
local recently_played = {
{id = "pop", name = "Pop Hits", type = "Playlist", color = "#43e97b"},
{id = "electronic", name = "Electronic", type = "Playlist", color = "#fa709a"},
{id = "jazz", name = "Jazz Classics", type = "Playlist", color = "#667eea"},
{id = "rock", name = "Rock Legends", type = "Playlist", color = "#f093fb"}
}
-- Song queue
local queue = {
{id = "1", title = "Midnight City", artist = "M83", duration = 234},
{id = "2", title = "Intro", artist = "The xx", duration = 128},
{id = "3", title = "Retrograde", artist = "James Blake", duration = 233},
{id = "4", title = "Tame Impala", artist = "The Less I Know The Better", duration = 218},
{id = "5", title = "Redbone", artist = "Childish Gambino", duration = 327}
}
local current_queue_index = 1
local timer_id = nil
-- Initialize music app
function initMusic(doc)
print("[Music] Initializing...")
music_doc = doc
updateNowPlaying()
updateMiniPlayer()
renderPlaylists()
renderRecentlyPlayed()
end
-- Format time (seconds to mm:ss)
local function formatTime(seconds)
local mins = math.floor(seconds / 60)
local secs = seconds % 60
return string.format("%d:%02d", mins, secs)
end
-- Update now playing display
function updateNowPlaying()
if not music_doc then return end
local title = music_doc:GetElementById("now-playing-title")
local artist = music_doc:GetElementById("now-playing-artist")
if title then
title.inner_rml = current_track.title
end
if artist then
artist.inner_rml = current_track.artist
end
end
-- Update mini player
function updateMiniPlayer()
if not music_doc then return end
local title = music_doc:GetElementById("mini-player-title")
local artist = music_doc:GetElementById("mini-player-artist")
local art = music_doc:GetElementById("mini-player-art")
local play_btn = music_doc:GetElementById("mini-play-btn")
if title then
title.inner_rml = current_track.title
end
if artist then
artist.inner_rml = current_track.artist
end
if art then
art.style["background-color"] = current_track.art_color
art.inner_rml = current_track.title:sub(1,1):upper()
end
if play_btn then
local icon = player_state.is_playing and "pause.tga" or "play.tga"
play_btn.inner_rml = [[<img src="../../icons/]] .. icon .. [[" style="width: 28px; height: 28px;"/>]]
end
end
-- Toggle play/pause
function togglePlay()
player_state.is_playing = not player_state.is_playing
print("[Music] " .. (player_state.is_playing and "Playing" or "Paused"))
if player_state.is_playing then
startPlaybackTimer()
else
stopPlaybackTimer()
end
updateMiniPlayer()
updatePlayButton()
end
-- Start playback timer
function startPlaybackTimer()
if setInterval then
timer_id = setInterval(function()
if player_state.is_playing then
player_state.current_time = player_state.current_time + 1
if player_state.current_time >= current_track.duration then
nextTrack()
end
updateProgress()
end
end, 1000)
end
end
-- Stop playback timer
function stopPlaybackTimer()
if timer_id and clearInterval then
clearInterval(timer_id)
timer_id = nil
end
end
-- Update play button
function updatePlayButton()
if not music_doc then return end
local btn = music_doc:GetElementById("play-btn")
if btn then
local icon = player_state.is_playing and "pause.tga" or "play.tga"
btn.inner_rml = [[<img src="../../icons/]] .. icon .. [[" style="width: 48px; height: 48px;"/>]]
end
end
-- Update progress display
function updateProgress()
if not music_doc then return end
local current = music_doc:GetElementById("current-time")
local total = music_doc:GetElementById("total-time")
local progress = music_doc:GetElementById("progress-bar")
if current then
current.inner_rml = formatTime(player_state.current_time)
end
if total then
total.inner_rml = formatTime(current_track.duration)
end
if progress then
local percent = (player_state.current_time / current_track.duration) * 100
progress.style.width = percent .. "%"
end
end
-- Next track
function nextTrack()
print("[Music] Next track")
if player_state.is_shuffled then
current_queue_index = math.random(1, #queue)
else
current_queue_index = current_queue_index + 1
if current_queue_index > #queue then
if player_state.repeat_mode == "all" then
current_queue_index = 1
else
current_queue_index = #queue
player_state.is_playing = false
stopPlaybackTimer()
end
end
end
loadTrack(queue[current_queue_index])
end
-- Previous track
function previousTrack()
print("[Music] Previous track")
if player_state.current_time > 3 then
-- Restart current track
player_state.current_time = 0
else
current_queue_index = current_queue_index - 1
if current_queue_index < 1 then
current_queue_index = 1
end
loadTrack(queue[current_queue_index])
end
updateProgress()
end
-- Load a track
function loadTrack(track)
current_track.id = track.id
current_track.title = track.title
current_track.artist = track.artist
current_track.duration = track.duration
player_state.current_time = 0
updateNowPlaying()
updateMiniPlayer()
updateProgress()
print("[Music] Now playing: " .. track.title .. " - " .. track.artist)
if showToast then
showToast("Now playing: " .. track.title)
end
end
-- Toggle shuffle
function toggleShuffle()
player_state.is_shuffled = not player_state.is_shuffled
print("[Music] Shuffle: " .. tostring(player_state.is_shuffled))
local btn = music_doc:GetElementById("shuffle-btn")
if btn then
if player_state.is_shuffled then
btn:SetClass("active", true)
else
btn:SetClass("active", false)
end
end
if showToast then
showToast(player_state.is_shuffled and "Shuffle on" or "Shuffle off")
end
end
-- Toggle repeat
function toggleRepeat()
if player_state.repeat_mode == "off" then
player_state.repeat_mode = "all"
elseif player_state.repeat_mode == "all" then
player_state.repeat_mode = "one"
else
player_state.repeat_mode = "off"
end
print("[Music] Repeat: " .. player_state.repeat_mode)
local btn = music_doc:GetElementById("repeat-btn")
if btn then
if player_state.repeat_mode ~= "off" then
btn:SetClass("active", true)
else
btn:SetClass("active", false)
end
end
if showToast then
local msg = "Repeat: " .. player_state.repeat_mode
showToast(msg)
end
end
-- Toggle like
function toggleLike()
print("[Music] Toggle like")
if showToast then
showToast("Added to Liked Songs")
end
end
-- Render playlists
function renderPlaylists()
if not music_doc then return end
local container = music_doc:GetElementById("quick-access")
if not container then return end
local html = ""
for i, pl in ipairs(playlists) do
if i <= 6 then
local initial = pl.name:sub(1,1):upper()
html = html .. [[
<div class="quick-card" onclick="openPlaylist(']] .. pl.id .. [[')">
<div class="quick-card-art" style="background-color: ]] .. pl.color .. [[;">]] .. initial .. [[</div>
<span class="quick-card-title">]] .. pl.name .. [[</span>
</div>
]]
end
end
container.inner_rml = html
end
-- Render recently played
function renderRecentlyPlayed()
if not music_doc then return end
local container = music_doc:GetElementById("recent-row")
if not container then return end
local html = ""
for _, item in ipairs(recently_played) do
local initial = item.name:sub(1,1):upper()
html = html .. [[
<div class="recent-item" onclick="openPlaylist(']] .. item.id .. [[')">
<div class="recent-art" style="background-color: ]] .. item.color .. [[;">]] .. initial .. [[</div>
<div class="recent-title">]] .. item.name .. [[</div>
<div class="recent-subtitle">]] .. item.type .. [[</div>
</div>
]]
end
container.inner_rml = html
end
-- Open playlist
function openPlaylist(playlist_id)
print("[Music] Opening playlist: " .. playlist_id)
if navigateTo then
navigateTo("playlist_" .. playlist_id)
else
if showToast then
showToast("Playlist: " .. playlist_id)
end
end
end
-- Open now playing
function openNowPlaying()
print("[Music] Opening now playing...")
if navigateTo then
navigateTo("now_playing")
end
end
-- Open search
function openSearch()
print("[Music] Opening search...")
if navigateTo then
navigateTo("music_search")
else
if showToast then
showToast("Search music")
end
end
end
-- Open library
function openLibrary()
print("[Music] Opening library...")
if navigateTo then
navigateTo("music_library")
else
if showToast then
showToast("Your library")
end
end
end
-- Seek to position (0-1)
function seekTo(position)
player_state.current_time = math.floor(position * current_track.duration)
updateProgress()
end

View File

@@ -3,36 +3,40 @@
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="music.lua"></script>
<title>Music</title>
<style>
.music-screen {
width: 100%;
height: 100%;
background-color: #121212;
display: flex;
flex-direction: column;
}
.music-content {
flex: 1;
overflow: auto;
padding-bottom: 140px;
}
/* Now Playing Mini Bar */
.mini-player {
position: absolute;
bottom: 56px;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #282828;
border-top: 1px solid #333333;
cursor: pointer;
}
.mini-player:hover {
background-color: #333333;
}
.mini-player-art {
width: 48px;
height: 48px;
border-radius: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-color: #667eea;
margin-right: 12px;
display: flex;
align-items: center;
@@ -46,30 +50,25 @@
}
.mini-player-title {
font-size: 18px;
font-size: 16px;
color: #FFFFFF;
font-weight: 500;
}
.mini-player-artist {
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
margin-top: 2px;
}
.mini-player-controls {
display: flex;
gap: 8px;
}
.mini-control-btn {
width: 56px;
height: 56px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 28px;
border-radius: 24px;
}
.mini-control-btn:hover {
@@ -77,26 +76,26 @@
}
.mini-control-btn img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
pointer-events: none;
}
/* Section Headers */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 16px 12px 16px;
padding: 20px 16px 12px 16px;
}
.section-title {
font-size: 22px;
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
}
.section-action {
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #B3B3B3;
cursor: pointer;
@@ -106,7 +105,6 @@
color: #FFFFFF;
}
/* Recently Played Row */
.recent-row {
display: flex;
overflow-x: auto;
@@ -115,43 +113,49 @@
}
.recent-item {
min-width: 130px;
min-width: 120px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
}
.recent-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.recent-art {
width: 130px;
height: 130px;
width: 120px;
height: 120px;
border-radius: 8px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
font-size: 36px;
color: #FFFFFF;
}
.recent-title {
font-size: 18px;
font-size: 16px;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 4px;
}
.recent-subtitle {
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
}
/* Quick Access Cards */
.quick-access {
display: grid;
grid-template-columns: 1fr 1fr;
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 16px;
}
.quick-card {
width: 48%;
display: flex;
align-items: center;
background-color: #282828;
@@ -172,18 +176,17 @@
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-size: 18px;
color: #FFFFFF;
}
.quick-card-title {
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #FFFFFF;
padding: 0 12px;
}
/* Playlist Row */
.playlist-item {
display: flex;
align-items: center;
@@ -218,13 +221,16 @@
}
.playlist-meta {
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
margin-top: 4px;
}
/* Bottom Navigation */
.music-bottom-nav {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
height: 56px;
background-color: #1E1E1E;
@@ -241,75 +247,106 @@
color: #B3B3B3;
}
.nav-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.nav-item.active {
color: #FFFFFF;
}
.nav-item img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
margin-bottom: 4px;
pointer-events: none;
}
.nav-item span {
font-size: 16px;
font-size: 14px;
}
/* Color palette for album arts */
.bg-gradient-1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.bg-gradient-2 { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.bg-gradient-3 { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.bg-gradient-4 { background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
.bg-gradient-5 { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
.bg-gradient-6 { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); }
/* Progress bar for mini player */
.progress-container {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #404040;
}
.progress-bar {
width: 0%;
height: 100%;
background-color: #1DB954;
}
.bg-gradient-1 { background-color: #667eea; }
.bg-gradient-2 { background-color: #f093fb; }
.bg-gradient-3 { background-color: #4facfe; }
.bg-gradient-4 { background-color: #43e97b; }
.bg-gradient-5 { background-color: #fa709a; }
.bg-solid-purple { background-color: #7c3aed; }
.bg-solid-red { background-color: #dc2626; }
.bg-solid-green { background-color: #16a34a; }
.bg-solid-blue { background-color: #2563eb; }
</style>
</head>
<body class="music-screen">
<body class="app-screen" onload="initLayout(document); initMusic(document)">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-nav btn-icon" onclick="goBack()">
<img src="../../icons/back.tga" style="width: 32px; height: 32px;"/>
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Music</span>
<div class="btn-icon">
<img src="../../icons/search.tga" style="width: 32px; height: 32px;"/>
<div class="app-bar-actions">
<div class="app-bar-action" onclick="openSearch()">
<img src="../../icons/search.tga"/>
</div>
</div>
</div>
<!-- Content -->
<div class="music-content">
<!-- Good Morning Section -->
<!-- Good Afternoon Section -->
<div class="section-header">
<span class="section-title">Good afternoon</span>
</div>
<!-- Quick Access Grid -->
<div class="quick-access">
<div class="quick-card">
<div class="quick-access" id="quick-access">
<div class="quick-card" onclick="openPlaylist('liked')">
<div class="quick-card-art bg-solid-red">L</div>
<span class="quick-card-title">Liked Songs</span>
</div>
<div class="quick-card">
<div class="quick-card" onclick="openPlaylist('daily1')">
<div class="quick-card-art bg-gradient-1">D</div>
<span class="quick-card-title">Daily Mix 1</span>
</div>
<div class="quick-card">
<div class="quick-card" onclick="openPlaylist('release')">
<div class="quick-card-art bg-solid-green">R</div>
<span class="quick-card-title">Release Radar</span>
</div>
<div class="quick-card">
<div class="quick-card" onclick="openPlaylist('chill')">
<div class="quick-card-art bg-gradient-2">C</div>
<span class="quick-card-title">Chill Vibes</span>
</div>
<div class="quick-card">
<div class="quick-card" onclick="openPlaylist('workout')">
<div class="quick-card-art bg-solid-blue">W</div>
<span class="quick-card-title">Workout Mix</span>
</div>
<div class="quick-card">
<div class="quick-card" onclick="openPlaylist('focus')">
<div class="quick-card-art bg-gradient-3">F</div>
<span class="quick-card-title">Focus Flow</span>
</div>
@@ -321,23 +358,23 @@
<span class="section-action">SEE ALL</span>
</div>
<div class="recent-row">
<div class="recent-item">
<div class="recent-row" id="recent-row">
<div class="recent-item" onclick="openPlaylist('pop')">
<div class="recent-art bg-gradient-4">P</div>
<div class="recent-title">Pop Hits</div>
<div class="recent-subtitle">Playlist</div>
</div>
<div class="recent-item">
<div class="recent-item" onclick="openPlaylist('electronic')">
<div class="recent-art bg-gradient-5">E</div>
<div class="recent-title">Electronic</div>
<div class="recent-subtitle">Playlist</div>
</div>
<div class="recent-item">
<div class="recent-item" onclick="openPlaylist('jazz')">
<div class="recent-art bg-gradient-1">J</div>
<div class="recent-title">Jazz Classics</div>
<div class="recent-subtitle">Playlist</div>
</div>
<div class="recent-item">
<div class="recent-item" onclick="openPlaylist('rock')">
<div class="recent-art bg-gradient-2">R</div>
<div class="recent-title">Rock Legends</div>
<div class="recent-subtitle">Playlist</div>
@@ -350,7 +387,7 @@
<span class="section-action">SEE ALL</span>
</div>
<div class="playlist-item">
<div class="playlist-item" onclick="openPlaylist('daily1')">
<div class="playlist-art bg-gradient-3">1</div>
<div class="playlist-info">
<div class="playlist-title">Daily Mix 1</div>
@@ -358,7 +395,7 @@
</div>
</div>
<div class="playlist-item">
<div class="playlist-item" onclick="openPlaylist('daily2')">
<div class="playlist-art bg-gradient-4">2</div>
<div class="playlist-info">
<div class="playlist-title">Daily Mix 2</div>
@@ -366,35 +403,30 @@
</div>
</div>
<div class="playlist-item">
<div class="playlist-item" onclick="openPlaylist('discover')">
<div class="playlist-art bg-gradient-5">D</div>
<div class="playlist-info">
<div class="playlist-title">Discover Weekly</div>
<div class="playlist-meta">Your weekly mixtape</div>
</div>
</div>
<div class="playlist-item">
<div class="playlist-art bg-solid-green">R</div>
<div class="playlist-info">
<div class="playlist-title">Release Radar</div>
<div class="playlist-meta">New music from artists you follow</div>
</div>
</div>
</div>
<!-- Mini Player -->
<div class="mini-player">
<div class="mini-player-art">M</div>
<div class="mini-player-info">
<div class="mini-player-title">Midnight City</div>
<div class="mini-player-artist">M83</div>
<div class="mini-player" onclick="openNowPlaying()">
<div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="mini-player-controls">
<div class="mini-control-btn">
<div class="mini-player-art" id="mini-player-art">M</div>
<div class="mini-player-info">
<div class="mini-player-title" id="mini-player-title">Midnight City</div>
<div class="mini-player-artist" id="mini-player-artist">M83</div>
</div>
<div style="display: flex; gap: 4px;">
<div class="mini-control-btn" onclick="toggleLike(); event.stopPropagation();">
<img src="../../icons/heart.tga"/>
</div>
<div class="mini-control-btn">
<div class="mini-control-btn" id="mini-play-btn" onclick="togglePlay(); event.stopPropagation();">
<img src="../../icons/play.tga"/>
</div>
</div>
@@ -406,11 +438,11 @@
<img src="../../icons/home.tga"/>
<span>Home</span>
</div>
<div class="nav-item">
<div class="nav-item" onclick="openSearch()">
<img src="../../icons/search.tga"/>
<span>Search</span>
</div>
<div class="nav-item">
<div class="nav-item" onclick="openLibrary()">
<img src="../../icons/library.tga"/>
<span>Library</span>
</div>

View File

@@ -3,33 +3,11 @@
local results = {}
local logCounter = 0
local currentDocument = nil -- Cache the document reference
-- Helper to get document (try multiple methods)
-- Helper to get document (use global set by C++)
local function getDocument()
-- First try the cached document
if currentDocument then
return currentDocument
end
-- Try the global document
if document then
currentDocument = document
return document
end
-- Try to get from RmlUi context
if rmlui and rmlui.contexts and rmlui.contexts.main then
local ctx = rmlui.contexts.main
-- documents is a proxy, iterate to get first doc
if ctx.documents then
for i, doc in ipairs(ctx.documents) do
if doc then
currentDocument = doc
return currentDocument
end
end
end
end
return nil
-- The C++ code sets 'document' global after loading
return document
end
local function log(msg)
@@ -155,7 +133,7 @@ function testCrypto()
-- Test SHA256
if success then
local hash = crypto.sha256("hello world")
local hash = crypto.hash("sha256", "hello world")
if hash then
log("SHA256: " .. hash:sub(1, 32) .. "...")
else
@@ -240,10 +218,6 @@ function testStorage()
end
end
-- Initialize: cache the document when script loads
if document then
currentDocument = document
print("[LUA] Document cached on load")
end
-- Initialize
log("Sandbox Test App loaded")
log("Lua version: " .. (_VERSION or "unknown"))

View File

@@ -6,8 +6,8 @@
</head>
<body>
<div class="app-bar">
<div class="app-bar-nav btn-icon" onclick="goBack()">
<span class="icon"></span>
<div class="app-bar-nav btn-icon" onclick="goHome()">
<span class="icon">&lt;</span>
</div>
<div class="app-bar-title">Sandbox Test</div>
</div>

View File

@@ -0,0 +1,39 @@
<!-- Sandbox Test App Content Fragment -->
<!-- Styles are in shell.rml -->
<div class="sandbox-content">
<div class="sandbox-header">
<span class="sandbox-header-title">Sandbox Test</span>
</div>
<div class="sandbox-body">
<div class="sandbox-card">
<div class="sandbox-card-title">Timer Test</div>
<div id="timer-status" class="sandbox-status">Not started</div>
<div class="sandbox-btn" onclick="testSandboxTimer()">Start Timer</div>
</div>
<div class="sandbox-card">
<div class="sandbox-card-title">JSON Test</div>
<div id="json-status" class="sandbox-status">Not tested</div>
<div class="sandbox-btn" onclick="testSandboxJSON()">Test JSON</div>
</div>
<div class="sandbox-card">
<div class="sandbox-card-title">Crypto Test</div>
<div id="crypto-status" class="sandbox-status">Not tested</div>
<div class="sandbox-btn" onclick="testSandboxCrypto()">Test Crypto</div>
</div>
<div class="sandbox-card">
<div class="sandbox-card-title">Storage Test</div>
<div id="storage-status" class="sandbox-status">Not tested</div>
<div class="sandbox-btn" onclick="testSandboxStorage()">Test Storage</div>
</div>
<div class="sandbox-card">
<div class="sandbox-card-title">Results</div>
<div id="sandbox-results" class="sandbox-results">Click buttons above to run tests</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
{
"id": "com.mosis.settings",
"name": "Settings",
"version": "1.0.0",
"version_code": 1,
"entry": "settings.rml",
"icon": "../../icons/settings.tga",
"description": "System settings and configuration",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [],
"min_api_version": 1
}

View File

@@ -0,0 +1,288 @@
-- settings.lua - Settings app functionality
-- Handles toggles, navigation, and system settings
local settings_doc = nil
-- Settings state
local settings_state = {
wifi = true,
wifi_network = "MosisNetwork",
bluetooth = false,
airplane_mode = false,
location = true,
location_mode = "High accuracy",
brightness = 80,
auto_brightness = true,
dark_mode = true,
font_size = "Default",
sleep_timeout = "5 minutes",
sound_volume = 70,
ring_volume = 80,
vibration = true,
dnd = false,
battery_percent = 85,
battery_status = "Not charging",
storage_used = 32,
storage_total = 128
}
-- Initialize settings
function initSettings(doc)
print("[Settings] Initializing...")
settings_doc = doc
updateAllToggles()
updateAllSubtitles()
end
-- Update all toggle states
function updateAllToggles()
updateToggle("wifi", settings_state.wifi)
updateToggle("bluetooth", settings_state.bluetooth)
updateToggle("airplane", settings_state.airplane_mode)
updateToggle("location", settings_state.location)
end
-- Update a single toggle
function updateToggle(name, state)
if not settings_doc then return end
local toggle = settings_doc:GetElementById("toggle-" .. name)
if toggle then
if state then
toggle:SetClass("active", true)
else
toggle:SetClass("active", false)
end
end
end
-- Update all subtitles
function updateAllSubtitles()
if not settings_doc then return end
-- WiFi
local wifi_sub = settings_doc:GetElementById("subtitle-wifi")
if wifi_sub then
if settings_state.wifi then
wifi_sub.inner_rml = "Connected to " .. settings_state.wifi_network
else
wifi_sub.inner_rml = "Off"
end
end
-- Bluetooth
local bt_sub = settings_doc:GetElementById("subtitle-bluetooth")
if bt_sub then
bt_sub.inner_rml = settings_state.bluetooth and "On" or "Off"
end
-- Battery
local bat_sub = settings_doc:GetElementById("subtitle-battery")
if bat_sub then
bat_sub.inner_rml = settings_state.battery_percent .. "% - " .. settings_state.battery_status
end
-- Storage
local storage_sub = settings_doc:GetElementById("subtitle-storage")
if storage_sub then
storage_sub.inner_rml = settings_state.storage_used .. " GB of " .. settings_state.storage_total .. " GB used"
end
-- Location
local loc_sub = settings_doc:GetElementById("subtitle-location")
if loc_sub then
if settings_state.location then
loc_sub.inner_rml = "On - " .. settings_state.location_mode
else
loc_sub.inner_rml = "Off"
end
end
end
-- Toggle WiFi
function toggleWifi()
settings_state.wifi = not settings_state.wifi
print("[Settings] WiFi: " .. tostring(settings_state.wifi))
updateToggle("wifi", settings_state.wifi)
updateAllSubtitles()
if showToast then
showToast(settings_state.wifi and "WiFi enabled" or "WiFi disabled")
end
end
-- Toggle Bluetooth
function toggleBluetooth()
settings_state.bluetooth = not settings_state.bluetooth
print("[Settings] Bluetooth: " .. tostring(settings_state.bluetooth))
updateToggle("bluetooth", settings_state.bluetooth)
updateAllSubtitles()
if showToast then
showToast(settings_state.bluetooth and "Bluetooth enabled" or "Bluetooth disabled")
end
end
-- Toggle Airplane Mode
function toggleAirplaneMode()
settings_state.airplane_mode = not settings_state.airplane_mode
print("[Settings] Airplane mode: " .. tostring(settings_state.airplane_mode))
if settings_state.airplane_mode then
-- Disable wireless when airplane mode is on
settings_state.wifi = false
settings_state.bluetooth = false
updateToggle("wifi", false)
updateToggle("bluetooth", false)
end
updateToggle("airplane", settings_state.airplane_mode)
updateAllSubtitles()
if showToast then
showToast(settings_state.airplane_mode and "Airplane mode on" or "Airplane mode off")
end
end
-- Toggle Location
function toggleLocation()
settings_state.location = not settings_state.location
print("[Settings] Location: " .. tostring(settings_state.location))
updateToggle("location", settings_state.location)
updateAllSubtitles()
if showToast then
showToast(settings_state.location and "Location enabled" or "Location disabled")
end
end
-- Open WiFi settings
function openWifiSettings()
print("[Settings] Opening WiFi settings...")
if navigateTo then
navigateTo("wifi_settings")
else
if showToast then
showToast("WiFi settings")
end
end
end
-- Open Bluetooth settings
function openBluetoothSettings()
print("[Settings] Opening Bluetooth settings...")
if showToast then
showToast("Bluetooth settings")
end
end
-- Open Display settings
function openDisplaySettings()
print("[Settings] Opening Display settings...")
if navigateTo then
navigateTo("display_settings")
else
if showToast then
showToast("Display settings")
end
end
end
-- Open Sound settings
function openSoundSettings()
print("[Settings] Opening Sound settings...")
if showToast then
showToast("Sound settings")
end
end
-- Open Notifications settings
function openNotificationsSettings()
print("[Settings] Opening Notifications settings...")
if showToast then
showToast("Notification settings")
end
end
-- Open Battery settings
function openBatterySettings()
print("[Settings] Opening Battery settings...")
if showToast then
showToast("Battery: " .. settings_state.battery_percent .. "%")
end
end
-- Open Storage settings
function openStorageSettings()
print("[Settings] Opening Storage settings...")
if showToast then
local used_percent = math.floor(settings_state.storage_used / settings_state.storage_total * 100)
showToast("Storage: " .. used_percent .. "% used")
end
end
-- Open Lock Screen settings
function openLockScreenSettings()
print("[Settings] Opening Lock Screen settings...")
if showToast then
showToast("Lock screen settings")
end
end
-- Open Privacy settings
function openPrivacySettings()
print("[Settings] Opening Privacy settings...")
if showToast then
showToast("Privacy settings")
end
end
-- Open Location settings
function openLocationSettings()
print("[Settings] Opening Location settings...")
if showToast then
showToast("Location settings")
end
end
-- Open About Phone
function openAboutPhone()
print("[Settings] Opening About Phone...")
if navigateTo then
navigateTo("about_phone")
else
if showToast then
showToast("Mosis Virtual Phone v1.0")
end
end
end
-- Open User Profile
function openUserProfile()
print("[Settings] Opening User Profile...")
if showToast then
showToast("User profile")
end
end
-- Search settings
function searchSettings(query)
print("[Settings] Searching: " .. query)
-- TODO: Implement settings search
end
-- Get setting value
function getSetting(key)
return settings_state[key]
end
-- Set setting value
function setSetting(key, value)
settings_state[key] = value
print("[Settings] Set " .. key .. " = " .. tostring(value))
updateAllToggles()
updateAllSubtitles()
end

View File

@@ -0,0 +1,370 @@
<rml>
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="settings.lua"></script>
<title>Settings</title>
<style>
.settings-list {
flex: 1;
overflow: auto;
width: 100%;
display: flex;
flex-direction: column;
}
.settings-section {
margin-bottom: 8px;
width: 100%;
display: flex;
flex-direction: column;
}
.settings-header {
padding: 16px 16px 8px 16px;
font-size: 18px;
font-weight: 500;
color: #BB86FC;
}
.settings-item {
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
cursor: pointer;
background-color: #1E1E1E;
overflow: hidden;
}
.settings-item:hover {
background-color: #252525;
}
.settings-item:active {
background-color: #2A2A2A;
}
.settings-item + .settings-item {
border-top: 1px #333333;
}
.settings-icon {
width: 48px;
height: 48px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.settings-icon img {
width: 32px;
height: 32px;
opacity: 0.7;
pointer-events: none;
}
.settings-content {
flex: 1;
}
.settings-title {
font-size: 18px;
color: #FFFFFF;
}
.settings-subtitle {
font-size: 16px;
color: #B3B3B3;
margin-top: 4px;
}
.settings-action {
font-size: 20px;
color: #666666;
padding: 8px;
}
.settings-toggle {
width: 56px;
height: 32px;
border-radius: 16px;
background-color: #666666;
cursor: pointer;
position: relative;
}
.settings-toggle:hover {
background-color: #777777;
}
.settings-toggle.active {
background-color: rgba(187, 134, 252, 0.5);
}
.settings-toggle-thumb {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #B3B3B3;
position: absolute;
top: 2px;
left: 2px;
}
.settings-toggle.active .settings-toggle-thumb {
background-color: #BB86FC;
left: 26px;
}
.user-card {
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 20px 16px;
background-color: #1E1E1E;
margin-bottom: 8px;
cursor: pointer;
}
.user-card:hover {
background-color: #252525;
}
.user-avatar {
width: 64px;
height: 64px;
border-radius: 32px;
background-color: #BB86FC;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #000000;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 20px;
font-weight: 500;
color: #FFFFFF;
}
.user-email {
font-size: 16px;
color: #B3B3B3;
margin-top: 4px;
}
</style>
</head>
<body class="app-screen" onload="initLayout(document); initSettings(document)">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Settings</span>
<div class="app-bar-actions">
<div class="app-bar-action">
<img src="../../icons/search.tga"/>
</div>
</div>
</div>
<!-- Settings List -->
<div class="app-content with-nav">
<div class="settings-list">
<!-- User Card -->
<div class="user-card" onclick="openUserProfile()">
<div class="user-avatar">U</div>
<div class="user-info">
<div class="user-name">User</div>
<div class="user-email">user@mosis.local</div>
</div>
<span class="settings-action">></span>
</div>
<!-- Network Section -->
<div class="settings-section">
<div class="settings-header">Network</div>
<div class="settings-item" onclick="openWifiSettings()">
<div class="settings-icon">
<img src="../../icons/wifi.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Wi-Fi</div>
<div class="settings-subtitle" id="subtitle-wifi">Connected to MosisNetwork</div>
</div>
<div id="toggle-wifi" class="settings-toggle active" onclick="toggleWifi(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div>
</div>
</div>
<div class="settings-item" onclick="openBluetoothSettings()">
<div class="settings-icon">
<img src="../../icons/bluetooth.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Bluetooth</div>
<div class="settings-subtitle" id="subtitle-bluetooth">Off</div>
</div>
<div id="toggle-bluetooth" class="settings-toggle" onclick="toggleBluetooth(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div>
</div>
</div>
<div class="settings-item">
<div class="settings-icon">
<img src="../../icons/airplane.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Airplane Mode</div>
</div>
<div id="toggle-airplane" class="settings-toggle" onclick="toggleAirplaneMode()">
<div class="settings-toggle-thumb"></div>
</div>
</div>
</div>
<!-- Device Section -->
<div class="settings-section">
<div class="settings-header">Device</div>
<div class="settings-item" onclick="openDisplaySettings()">
<div class="settings-icon">
<img src="../../icons/brightness.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Display</div>
<div class="settings-subtitle">Brightness, wallpaper, sleep</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openSoundSettings()">
<div class="settings-icon">
<img src="../../icons/volume.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Sound</div>
<div class="settings-subtitle">Volume, ringtone, vibration</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openNotificationsSettings()">
<div class="settings-icon">
<img src="../../icons/notifications.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Notifications</div>
<div class="settings-subtitle">App notifications, Do not disturb</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openBatterySettings()">
<div class="settings-icon">
<img src="../../icons/battery.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Battery</div>
<div class="settings-subtitle" id="subtitle-battery">85% - Not charging</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openStorageSettings()">
<div class="settings-icon">
<img src="../../icons/storage.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Storage</div>
<div class="settings-subtitle" id="subtitle-storage">32 GB of 128 GB used</div>
</div>
<span class="settings-action">></span>
</div>
</div>
<!-- Privacy Section -->
<div class="settings-section">
<div class="settings-header">Privacy &amp; Security</div>
<div class="settings-item" onclick="openLockScreenSettings()">
<div class="settings-icon">
<img src="../../icons/lock.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Lock Screen</div>
<div class="settings-subtitle">PIN, pattern, fingerprint</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openPrivacySettings()">
<div class="settings-icon">
<img src="../../icons/privacy.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Privacy</div>
<div class="settings-subtitle">Permissions, account activity</div>
</div>
<span class="settings-action">></span>
</div>
<div class="settings-item" onclick="openLocationSettings()">
<div class="settings-icon">
<img src="../../icons/location.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">Location</div>
<div class="settings-subtitle" id="subtitle-location">On - High accuracy</div>
</div>
<div id="toggle-location" class="settings-toggle active" onclick="toggleLocation(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div>
</div>
</div>
</div>
<!-- About Section -->
<div class="settings-section">
<div class="settings-header">About</div>
<div class="settings-item" onclick="openAboutPhone()">
<div class="settings-icon">
<img src="../../icons/phone.tga"/>
</div>
<div class="settings-content">
<div class="settings-title">About Phone</div>
<div class="settings-subtitle">Mosis Virtual Phone v1.0</div>
</div>
<span class="settings-action">></span>
</div>
</div>
</div>
</div>
<!-- System Navigation Bar -->
<div class="system-nav-bar">
<div class="system-nav-btn" onclick="onBackPressed()">
<img src="../../icons/back.tga"/>
</div>
<div class="system-nav-home" onclick="onHomePressed()"></div>
<div class="system-nav-btn" onclick="onRecentPressed()">
<img src="../../icons/menu.tga"/>
</div>
</div>
</body>
</rml>

View File

@@ -0,0 +1,19 @@
{
"id": "com.mosis.store",
"name": "Mosis Store",
"version": "1.0.0",
"version_code": 1,
"entry": "store.rml",
"icon": "../../icons/store.tga",
"description": "App store for downloading and installing apps",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"network",
"storage"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,394 @@
-- 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()

View File

@@ -3,25 +3,18 @@
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
<script src="store.lua"></script>
<title>Store</title>
<style>
.store-screen {
width: 100%;
height: 100%;
background-color: #121212;
display: flex;
flex-direction: column;
}
.store-content {
flex: 1;
overflow: auto;
padding-bottom: 16px;
}
/* Search Bar */
.store-search {
margin: 16px;
background-color: #2D2D2D;
@@ -29,11 +22,16 @@
padding: 12px 16px;
display: flex;
align-items: center;
cursor: pointer;
}
.store-search:hover {
background-color: #3D3D3D;
}
.store-search img {
width: 28px;
height: 28px;
width: 24px;
height: 24px;
margin-right: 12px;
opacity: 0.6;
}
@@ -43,7 +41,6 @@
color: #B3B3B3;
}
/* Section Headers */
.section-header {
display: flex;
align-items: center;
@@ -58,43 +55,46 @@
}
.section-action {
font-size: 18px;
font-size: 16px;
color: #BB86FC;
cursor: pointer;
}
/* Featured Banner */
.featured-banner {
margin: 0 16px 16px 16px;
height: 160px;
background: linear-gradient(135deg, #BB86FC 0%, #6200EE 100%);
height: 140px;
background-color: #7C3AED;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: flex-end;
cursor: pointer;
}
.featured-banner:hover {
opacity: 0.95;
}
.featured-tag {
font-size: 16px;
font-size: 14px;
color: rgba(255,255,255,0.7);
text-transform: uppercase;
margin-bottom: 8px;
}
.featured-title {
font-size: 24px;
font-size: 22px;
font-weight: 600;
color: #FFFFFF;
margin-bottom: 4px;
}
.featured-subtitle {
font-size: 18px;
font-size: 16px;
color: rgba(255,255,255,0.8);
}
/* App Cards Row */
.app-cards-row {
display: flex;
overflow-x: auto;
@@ -103,7 +103,7 @@
}
.app-card {
min-width: 140px;
min-width: 130px;
background-color: #1E1E1E;
border-radius: 12px;
padding: 12px;
@@ -115,44 +115,35 @@
}
.app-card-icon {
width: 64px;
height: 64px;
width: 56px;
height: 56px;
border-radius: 14px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-size: 24px;
color: #000000;
}
.app-card-name {
font-size: 18px;
font-size: 16px;
font-weight: 500;
color: #FFFFFF;
margin-bottom: 4px;
}
.app-card-category {
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
margin-bottom: 8px;
margin-bottom: 6px;
}
.app-card-rating {
display: flex;
align-items: center;
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
}
.app-card-rating img {
width: 16px;
height: 16px;
margin-right: 4px;
}
/* App List Items */
.app-list-item {
display: flex;
align-items: center;
@@ -187,35 +178,18 @@
}
.app-list-meta {
font-size: 16px;
font-size: 14px;
color: #B3B3B3;
margin-top: 4px;
}
.app-list-rating {
display: flex;
align-items: center;
margin-top: 4px;
}
.app-list-rating img {
width: 18px;
height: 18px;
margin-right: 4px;
}
.app-list-rating span {
font-size: 16px;
color: #B3B3B3;
}
.install-btn {
background-color: #BB86FC;
color: #000000;
font-size: 16px;
font-size: 14px;
font-weight: 600;
padding: 10px 22px;
border-radius: 22px;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
}
@@ -226,10 +200,8 @@
.install-btn.installed {
background-color: transparent;
color: #BB86FC;
border: 1px solid #BB86FC;
}
/* Category Chips */
.category-chips {
display: flex;
overflow-x: auto;
@@ -257,7 +229,6 @@
color: #000000;
}
/* Bottom Nav */
.store-bottom-nav {
display: flex;
height: 56px;
@@ -274,21 +245,24 @@
color: #B3B3B3;
}
.store-nav-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.store-nav-item.active {
color: #BB86FC;
}
.store-nav-item img {
width: 32px;
height: 32px;
width: 28px;
height: 28px;
margin-bottom: 4px;
}
.store-nav-item span {
font-size: 16px;
font-size: 14px;
}
/* Color palette for app icons */
.bg-purple { background-color: #BB86FC; }
.bg-teal { background-color: #03DAC6; }
.bg-orange { background-color: #FF9800; }
@@ -297,132 +271,29 @@
.bg-red { background-color: #F44336; }
.bg-pink { background-color: #E91E63; }
.bg-indigo { background-color: #3F51B5; }
/* Dialog Overlay */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background-color: #2D2D2D;
border-radius: 16px;
padding: 24px;
min-width: 280px;
max-width: 320px;
}
.dialog-title {
font-size: 20px;
font-weight: 600;
color: #FFFFFF;
margin-bottom: 16px;
}
.dialog-message {
font-size: 16px;
color: #B3B3B3;
margin-bottom: 24px;
line-height: 1.4;
}
.dialog-status {
font-size: 14px;
color: #B3B3B3;
margin-top: 12px;
text-align: center;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-btn {
background-color: transparent;
color: #BB86FC;
font-size: 16px;
font-weight: 600;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
}
.dialog-btn:hover {
background-color: rgba(187, 134, 252, 0.1);
}
/* Progress Bar */
.progress-container {
width: 100%;
height: 4px;
background-color: #1E1E1E;
border-radius: 2px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #BB86FC;
border-radius: 2px;
transition: width 0.3s ease;
}
/* Toast */
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background-color: #323232;
color: #FFFFFF;
font-size: 14px;
padding: 12px 24px;
border-radius: 8px;
z-index: 1001;
}
/* Badge */
.badge {
position: absolute;
top: 4px;
right: 4px;
background-color: #F44336;
color: #FFFFFF;
font-size: 12px;
font-weight: 600;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.store-nav-item {
position: relative;
}
</style>
</head>
<body class="store-screen">
<body class="app-screen" onload="initLayout(document)">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-nav btn-icon" onclick="goBack()">
<img src="../../icons/back.tga" style="width: 32px; height: 32px;"/>
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">Mosis Store</span>
<div class="btn-icon">
<img src="../../icons/account.tga" style="width: 32px; height: 32px;"/>
<div class="app-bar-actions">
<div class="app-bar-action">
<img src="../../icons/account.tga"/>
</div>
</div>
</div>
@@ -447,7 +318,6 @@
<div class="category-chip">Games</div>
<div class="category-chip">Social</div>
<div class="category-chip">Productivity</div>
<div class="category-chip">Entertainment</div>
<div class="category-chip">Tools</div>
</div>
@@ -490,15 +360,11 @@
<span class="section-action">See all</span>
</div>
<!-- App List -->
<div class="app-list-item">
<div class="app-list-icon bg-purple">S</div>
<div class="app-list-info">
<div class="app-list-name">Social Hub</div>
<div class="app-list-meta">Social &bull; 12 MB</div>
<div class="app-list-rating">
<span>4.9 &bull; 1.2M downloads</span>
</div>
<div class="app-list-meta">Social - 12 MB - 4.9</div>
</div>
<div class="install-btn">Install</div>
</div>
@@ -507,10 +373,7 @@
<div class="app-list-icon bg-red">G</div>
<div class="app-list-info">
<div class="app-list-name">Games Center</div>
<div class="app-list-meta">Games &bull; 45 MB</div>
<div class="app-list-rating">
<span>4.7 &bull; 890K downloads</span>
</div>
<div class="app-list-meta">Games - 45 MB - 4.7</div>
</div>
<div class="install-btn">Install</div>
</div>
@@ -519,10 +382,7 @@
<div class="app-list-icon bg-indigo">F</div>
<div class="app-list-info">
<div class="app-list-name">File Manager</div>
<div class="app-list-meta">Tools &bull; 8 MB</div>
<div class="app-list-rating">
<span>4.6 &bull; 650K downloads</span>
</div>
<div class="app-list-meta">Tools - 8 MB - 4.6</div>
</div>
<div class="install-btn installed">Open</div>
</div>
@@ -531,100 +391,26 @@
<div class="app-list-icon bg-pink">M</div>
<div class="app-list-info">
<div class="app-list-name">Music Player</div>
<div class="app-list-meta">Music &bull; 18 MB</div>
<div class="app-list-rating">
<span>4.5 &bull; 520K downloads</span>
</div>
<div class="app-list-meta">Music - 18 MB - 4.5</div>
</div>
<div class="install-btn">Install</div>
</div>
<div class="app-list-item">
<div class="app-list-icon bg-teal">P</div>
<div class="app-list-info">
<div class="app-list-name">Photo Editor</div>
<div class="app-list-meta">Photography &bull; 32 MB</div>
<div class="app-list-rating">
<span>4.4 &bull; 410K downloads</span>
</div>
</div>
<div class="install-btn">Install</div>
</div>
<!-- New Games Section -->
<div class="section-header">
<span class="section-title">New Games</span>
<span class="section-action">See all</span>
</div>
<div class="app-cards-row">
<div class="app-card">
<div class="app-card-icon bg-red">P</div>
<div class="app-card-name">Puzzle Quest</div>
<div class="app-card-category">Puzzle</div>
<div class="app-card-rating">4.8</div>
</div>
<div class="app-card">
<div class="app-card-icon bg-green">R</div>
<div class="app-card-name">Racing VR</div>
<div class="app-card-category">Racing</div>
<div class="app-card-rating">4.6</div>
</div>
<div class="app-card">
<div class="app-card-icon bg-blue">S</div>
<div class="app-card-name">Space Explorer</div>
<div class="app-card-category">Adventure</div>
<div class="app-card-rating">4.7</div>
</div>
<div class="app-card">
<div class="app-card-icon bg-orange">C</div>
<div class="app-card-name">Card Master</div>
<div class="app-card-category">Card</div>
<div class="app-card-rating">4.5</div>
</div>
</div>
</div>
<!-- Bottom Navigation -->
<div class="store-bottom-nav">
<div id="nav-apps" class="store-nav-item active" onclick="showHome()">
<div class="store-nav-item active">
<img src="../../icons/home.tga"/>
<span>Apps</span>
</div>
<div id="nav-games" class="store-nav-item" onclick="showGames()">
<div class="store-nav-item">
<img src="../../icons/game.tga"/>
<span>Games</span>
</div>
<div id="nav-updates" class="store-nav-item" onclick="showUpdates()">
<div class="store-nav-item">
<img src="../../icons/download.tga"/>
<span>Updates</span>
<div id="updates-badge" class="badge" style="display: none;"></div>
</div>
</div>
<!-- Progress Dialog (hidden by default) -->
<div id="progress-dialog" class="dialog-overlay" style="display: none;">
<div class="dialog">
<div id="progress-title" class="dialog-title">Installing...</div>
<div class="progress-container">
<div id="progress-bar" class="progress-bar" style="width: 0%;"></div>
</div>
<div id="progress-status" class="dialog-status">Preparing...</div>
</div>
</div>
<!-- Error Dialog (hidden by default) -->
<div id="error-dialog" class="dialog-overlay" style="display: none;">
<div class="dialog">
<div class="dialog-title">Error</div>
<div id="error-message" class="dialog-message"></div>
<div class="dialog-actions">
<div class="dialog-btn" onclick="hideError()">OK</div>
</div>
</div>
</div>
<!-- Toast (hidden by default) -->
<div id="toast" class="toast" style="display: none;"></div>
</body>
</rml>

BIN
base-apps/icons/account.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/add.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/back.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/backspace.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/battery.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/browser.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/calculator.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/calendar.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/call_small.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/camera.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/clock.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/close.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/contact_phone.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/contacts.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/dialpad.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/download.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/files.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/flash.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/forward.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/gallery.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/game.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/heart.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/history.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/home.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/library.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/maps.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/menu.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/message.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/more.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/music.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/notes.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/phone.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/play.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/refresh.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/search.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/send.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/settings.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/signal.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/store.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/switch-camera.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/timer.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/weather.tga LFS Normal file

Binary file not shown.

BIN
base-apps/icons/wifi.tga LFS Normal file

Binary file not shown.

View File

@@ -0,0 +1,103 @@
-- Layout System for Virtual Smartphone
-- Provides reusable UI component helpers
-- Requires navigation.lua to be loaded first
-- Icon paths (relative to assets/)
local ICON_PATH = "../../icons/"
-- Default icons
local icons = {
back = ICON_PATH .. "back.tga",
home = ICON_PATH .. "home.tga",
menu = ICON_PATH .. "menu.tga",
search = ICON_PATH .. "search.tga",
more = ICON_PATH .. "more.tga",
close = ICON_PATH .. "close.tga",
wifi = ICON_PATH .. "wifi.tga",
signal = ICON_PATH .. "signal.tga",
battery = ICON_PATH .. "battery.tga"
}
-- Get current time formatted as HH:MM
local function getCurrentTime()
-- In sandbox, we might not have os.date, use a default
if os and os.date then
return os.date("%H:%M")
end
return "12:30"
end
-- Update status bar time
function updateStatusTime(doc)
local timeEl = doc:GetElementById("status-time")
if timeEl then
timeEl.inner_rml = getCurrentTime()
end
end
-- Initialize status bar with current time
-- Call from document onload
function initStatusBar(doc)
updateStatusTime(doc)
-- Set up timer to update time every minute if timers are available
if setTimeout then
local function updateLoop()
updateStatusTime(doc)
setTimeout(updateLoop, 60000)
end
setTimeout(updateLoop, 60000)
end
end
-- Initialize app bar back button
-- Call from document onload
function initAppBar(doc)
local backBtn = doc:GetElementById("app-bar-back")
if backBtn then
-- Back button is handled via onclick in RML
-- This is for any additional setup
end
end
-- Initialize system navigation bar
-- Call from document onload
function initSystemNav(doc)
-- Navigation buttons are handled via onclick in RML
-- This is for any additional setup
end
-- Full layout initialization
-- Call from document onload: initLayout(document)
function initLayout(doc)
initStatusBar(doc)
initAppBar(doc)
initSystemNav(doc)
print("Layout initialized")
end
-- Handle back button press (for app bar or system nav)
function onBackPressed()
if canGoBack and canGoBack() then
goBack()
else
-- If at root, go home
if goHome then
goHome()
end
end
end
-- Handle home button press
function onHomePressed()
if goHome then
goHome()
end
end
-- Handle recent apps button press (placeholder)
function onRecentPressed()
print("Recent apps pressed (not implemented)")
end
print("Layout system loaded")

View File

@@ -0,0 +1,145 @@
-- Navigation System for Virtual Smartphone
-- Handles screen transitions and state management
-- Screen registry - maps screen names to RML file paths
local screens = {
home = "apps/home/home.rml",
lock = "apps/home/lock.rml",
dialer = "apps/dialer/dialer.rml",
calling = "apps/dialer/calling.rml",
contacts = "apps/contacts/contacts.rml",
contact_detail = "apps/contacts/contact_detail.rml",
messages = "apps/messages/messages.rml",
chat = "apps/messages/chat.rml",
settings = "apps/settings/settings.rml",
browser = "apps/browser/browser.rml",
store = "apps/store/store.rml",
camera = "apps/camera/camera.rml",
music = "apps/music/music.rml"
}
-- Use global state to persist across document loads
-- Initialize only if not already set
if not _G.nav_state then
_G.nav_state = {
history = {},
current_screen = "home",
nav_direction = "none" -- "forward", "back", "home", "none"
}
end
-- Local references for convenience
local history = _G.nav_state.history
local function get_current() return _G.nav_state.current_screen end
local function set_current(s) _G.nav_state.current_screen = s end
local function get_direction() return _G.nav_state.nav_direction end
local function set_direction(d) _G.nav_state.nav_direction = d end
-- Apply animation class based on navigation direction
local function applyNavAnimation()
local dir = get_direction()
print("Applying animation, direction: " .. dir)
if dir ~= "none" and document then
-- In RmlUi Lua, get body element
local body = document.body
if body then
print("Found body element, setting class nav-" .. dir)
-- Set the appropriate animation class
if dir == "forward" then
body:SetClass("nav-forward", true)
elseif dir == "back" then
body:SetClass("nav-back", true)
elseif dir == "home" then
body:SetClass("nav-home", true)
else
body:SetClass("nav-default", true)
end
else
print("Body element not found!")
end
end
end
-- Navigate to a screen by name
function navigateTo(screen_name)
print("navigateTo called with: " .. tostring(screen_name))
local path = screens[screen_name]
if path then
-- Push current screen to history before navigating
table.insert(history, get_current())
set_current(screen_name)
set_direction("forward")
-- Load the new screen using C++ function
local success = loadScreen(path)
if success then
applyNavAnimation()
print("Navigated to: " .. screen_name .. " (history depth: " .. #history .. ")")
else
-- Restore previous state on failure
set_current(table.remove(history))
print("Failed to navigate to: " .. screen_name)
end
return success
else
print("Unknown screen: " .. screen_name)
return false
end
end
-- Go back to previous screen
function goBack()
print("goBack called (history depth: " .. #history .. ")")
if #history > 0 then
local previous = table.remove(history)
local path = screens[previous]
if path then
set_current(previous)
set_direction("back")
loadScreen(path)
applyNavAnimation()
print("Back to: " .. previous)
return true
end
else
print("No history to go back to")
end
return false
end
-- Go to home screen (clear history)
function goHome()
-- Clear the history table
for i = #history, 1, -1 do
history[i] = nil
end
set_current("home")
set_direction("home")
loadScreen(screens.home)
applyNavAnimation()
print("Navigated to home")
end
-- Get current screen name
function getCurrentScreen()
return get_current()
end
-- Check if we can go back
function canGoBack()
return #history > 0
end
-- Clear navigation history
function clearHistory()
for i = #history, 1, -1 do
history[i] = nil
end
end
-- Get history depth
function getHistoryDepth()
return #history
end
print("Navigation system initialized (current: " .. get_current() .. ", history: " .. #history .. ")")

1485
base-apps/ui/components.rcss Normal file

File diff suppressed because it is too large Load Diff

93
base-apps/ui/html.rcss Normal file
View File

@@ -0,0 +1,93 @@
body, div,
h1, h2, h3, h4,
h5, h6, p,
hr, pre,
tabset tabs
{
display: block;
}
h1
{
font-size: 2em;
margin: .67em 0;
}
h2
{
font-size: 1.5em;
margin: .75em 0;
}
h3
{
font-size: 1.17em;
margin: .83em 0;
}
h4, p
{
margin: 1.12em 0;
}
h5
{
font-size: .83em;
margin: 1.5em 0;
}
h6
{
font-size: .75em;
margin: 1.67em 0;
}
h1, h2, h3, h4,
h5, h6, strong
{
font-weight: bold;
}
em
{
font-style: italic;
}
pre
{
white-space: pre;
}
hr
{
border-width: 1px;
}
table
{
box-sizing: border-box;
display: table;
}
tr
{
box-sizing: border-box;
display: table-row;
}
td
{
box-sizing: border-box;
display: table-cell;
}
col
{
box-sizing: border-box;
display: table-column;
}
colgroup
{
display: table-column-group;
}
thead, tbody, tfoot
{
display: table-row-group;
}

270
base-apps/ui/layout.rcss Normal file
View File

@@ -0,0 +1,270 @@
/* ==============================================
Layout Components: Reusable App Layout Structure
System status bar, app bar, navigation bar
============================================== */
/* ============== System Status Bar ============== */
/* Top bar showing time, signal, wifi, battery */
.system-status-bar {
height: 36px;
padding: 0 16px;
background-color: transparent;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
color: #FFFFFF;
z-index: 1000;
}
.system-status-bar.bg-surface {
background-color: #1E1E1E;
}
.system-status-time {
font-weight: 500;
font-size: 16px;
}
.system-status-icons {
display: flex;
gap: 8px;
align-items: center;
}
.system-status-icons img {
width: 24px;
height: 24px;
pointer-events: none;
}
/* ============== App Bar ============== */
/* Title bar with back button and optional actions */
.app-bar {
height: 72px;
padding: 0 8px;
background-color: #1E1E1E;
display: flex;
align-items: center;
z-index: 900;
}
.app-bar.transparent {
background-color: transparent;
}
.app-bar.primary {
background-color: #121212;
}
.app-bar-back {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 28px;
}
.app-bar-back:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.app-bar-back:active {
background-color: rgba(255, 255, 255, 0.2);
}
.app-bar-back img {
width: 32px;
height: 32px;
pointer-events: none;
}
.app-bar-title {
flex: 1;
font-size: 24px;
font-weight: 500;
color: #FFFFFF;
padding-left: 8px;
}
.app-bar-actions {
display: flex;
gap: 4px;
}
.app-bar-action {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 28px;
}
.app-bar-action:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.app-bar-action:active {
background-color: rgba(255, 255, 255, 0.2);
}
.app-bar-action img {
width: 28px;
height: 28px;
pointer-events: none;
}
/* ============== System Navigation Bar ============== */
/* Bottom bar with back, home, and recent buttons */
.system-nav-bar {
height: 56px;
background-color: #0A0A0A;
display: flex;
align-items: center;
justify-content: space-around;
z-index: 1000;
}
.system-nav-bar.transparent {
background-color: rgba(10, 10, 10, 0.9);
}
.system-nav-btn {
width: 72px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 24px;
}
.system-nav-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.system-nav-btn:active {
background-color: rgba(255, 255, 255, 0.2);
}
.system-nav-btn img {
width: 28px;
height: 28px;
pointer-events: none;
opacity: 0.8;
}
/* Home button - pill shape */
.system-nav-home {
width: 96px;
height: 8px;
background-color: #FFFFFF;
border-radius: 4px;
cursor: pointer;
opacity: 0.6;
}
.system-nav-home:hover {
opacity: 0.8;
}
.system-nav-home:active {
opacity: 1.0;
}
/* ============== Screen Layout ============== */
/* Standard app screen structure */
.app-screen {
width: 100%;
height: 100%;
background-color: #121212;
display: flex;
flex-direction: column;
}
.app-content {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
}
/* Content padding for nav bar */
.app-content.with-nav {
padding-bottom: 56px;
}
/* Content padding for dock */
.app-content.with-dock {
padding-bottom: 100px;
}
/* ============== Combined Header ============== */
/* Status bar + App bar combined */
.app-header {
display: flex;
flex-direction: column;
background-color: #1E1E1E;
}
.app-header.transparent {
background-color: transparent;
}
.app-header .system-status-bar {
background-color: transparent;
}
/* ============== Notification Badge ============== */
.notification-badge {
position: absolute;
top: 8px;
right: 8px;
min-width: 20px;
height: 20px;
background-color: #CF6679;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
}
/* ============== Recording Indicator ============== */
/* Shown when camera/mic is active */
.recording-indicator {
position: absolute;
top: 8px;
right: 8px;
width: 12px;
height: 12px;
background-color: #F44336;
border-radius: 6px;
animation: recording-pulse 1.5s ease-in-out infinite;
}
@keyframes recording-pulse {
0%, 100% { opacity: 1.0; }
50% { opacity: 0.4; }
}
/* ============== Divider ============== */
.header-divider {
height: 1px;
background-color: #333333;
}

333
base-apps/ui/theme.rcss Normal file
View File

@@ -0,0 +1,333 @@
/* ==============================================
Theme: Material Dark for Virtual Smartphone (VR Optimized)
All sizes increased for VR readability and raycast interaction
============================================== */
/* Base body styling */
body {
font-family: LatoLatin;
background-color: #121212;
color: #FFFFFF;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
animation: 0.2s cubic-out fade-in;
}
/* ============== Typography (VR-sized) ============== */
.text-h1 {
font-size: 120px;
font-weight: 300;
}
.text-h2 {
font-size: 80px;
font-weight: 300;
}
.text-h3 {
font-size: 64px;
font-weight: 400;
}
.text-h4 {
font-size: 48px;
font-weight: 400;
}
.text-h5 {
font-size: 32px;
font-weight: 400;
}
.text-h6 {
font-size: 28px;
font-weight: 500;
}
.text-body1 {
font-size: 22px;
font-weight: 400;
}
.text-body2 {
font-size: 18px;
font-weight: 400;
}
.text-caption {
font-size: 16px;
font-weight: 400;
}
.text-overline {
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 2px;
}
/* ============== Text Colors ============== */
.text-primary {
color: #FFFFFF;
}
.text-secondary {
color: #B3B3B3;
}
.text-disabled {
color: #666666;
}
.text-accent {
color: #BB86FC;
}
.text-accent-secondary {
color: #03DAC6;
}
.text-error {
color: #CF6679;
}
/* ============== Background Colors ============== */
.bg-primary {
background-color: #121212;
}
.bg-surface {
background-color: #1E1E1E;
}
.bg-surface-variant {
background-color: #2D2D2D;
}
.bg-accent {
background-color: #BB86FC;
}
.bg-accent-secondary {
background-color: #03DAC6;
}
.bg-error {
background-color: #CF6679;
}
/* Hover highlight color - used for interactive element feedback */
.bg-hover {
background-color: rgba(255, 255, 255, 0.12);
}
/* ============== Spacing Utilities (VR-scaled) ============== */
.p-0 { padding: 0; }
.p-1 { padding: 6px; }
.p-2 { padding: 12px; }
.p-3 { padding: 18px; }
.p-4 { padding: 24px; }
.p-5 { padding: 36px; }
.p-6 { padding: 48px; }
.p-8 { padding: 72px; }
.m-0 { margin: 0; }
.m-1 { margin: 6px; }
.m-2 { margin: 12px; }
.m-3 { margin: 18px; }
.m-4 { margin: 24px; }
.m-5 { margin: 36px; }
.m-6 { margin: 48px; }
.m-8 { margin: 72px; }
.mt-1 { margin-top: 6px; }
.mt-2 { margin-top: 12px; }
.mt-3 { margin-top: 18px; }
.mt-4 { margin-top: 24px; }
.mb-1 { margin-bottom: 6px; }
.mb-2 { margin-bottom: 12px; }
.mb-3 { margin-bottom: 18px; }
.mb-4 { margin-bottom: 24px; }
/* ============== Layout Utilities ============== */
.flex {
display: flex;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
}
.flex-around {
display: flex;
justify-content: space-around;
}
.flex-1 {
flex: 1;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
/* ============== Border Utilities ============== */
.rounded {
border-radius: 6px;
}
.rounded-lg {
border-radius: 12px;
}
.rounded-xl {
border-radius: 20px;
}
.rounded-full {
border-radius: 9999px;
}
/* ============== Screen Structure ============== */
.screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #121212;
}
.screen-content {
flex: 1;
overflow: auto;
}
/* ============== Animations ============== */
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes slide-in-right {
0% { transform: translateX(100px); opacity: 0; }
100% { transform: translateX(0px); opacity: 1; }
}
@keyframes slide-in-left {
0% { transform: translateX(-100px); opacity: 0; }
100% { transform: translateX(0px); opacity: 1; }
}
@keyframes slide-up {
0% { transform: translateY(50px); opacity: 0; }
100% { transform: translateY(0px); opacity: 1; }
}
@keyframes scale-in {
0% { transform: scale(0.9); opacity: 0; }
100% { transform: scale(1.0); opacity: 1; }
}
/* Hover highlight animation */
@keyframes hover-pulse {
0% { background-color: rgba(255, 255, 255, 0.0); }
50% { background-color: rgba(255, 255, 255, 0.15); }
100% { background-color: rgba(255, 255, 255, 0.1); }
}
/* Screen transition classes */
.nav-forward {
animation: 0.2s cubic-out slide-in-right;
}
.nav-back {
animation: 0.2s cubic-out slide-in-left;
}
.nav-home {
animation: 0.2s back-out scale-in;
}
.nav-default {
animation: 0.15s cubic-out fade-in;
}
/* Animation utility classes */
.animate-fade-in {
animation: 0.2s cubic-out fade-in;
}
.animate-slide-right {
animation: 0.25s cubic-out slide-in-right;
}
.animate-slide-left {
animation: 0.25s cubic-out slide-in-left;
}
.animate-slide-up {
animation: 0.2s cubic-out slide-up;
}
.animate-scale-in {
animation: 0.2s back-out scale-in;
}
/* ============== Interactive Base Class ============== */
/* All interactive elements should use cursor: pointer and have hover feedback */
.interactive {
cursor: pointer;
}
.interactive:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.interactive:active {
background-color: rgba(255, 255, 255, 0.2);
}

View File

@@ -2,6 +2,13 @@
#include <lua.hpp>
#include <algorithm>
#ifdef __ANDROID__
#include <android/log.h>
#define TIMER_LOG(...) __android_log_print(ANDROID_LOG_INFO, "MosisOS", __VA_ARGS__)
#else
#include <cstdio>
#define TIMER_LOG(...) printf(__VA_ARGS__)
#endif
namespace mosis {
@@ -64,8 +71,12 @@ TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id,
int callback_ref, int interval_ms) {
std::lock_guard<std::mutex> lock(m_mutex);
TIMER_LOG("[Timer] SetInterval called: app=%s, interval=%dms, ref=%d",
app_id.c_str(), interval_ms, callback_ref);
// Check per-app limit
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
TIMER_LOG("[Timer] REJECTED: timer limit exceeded for %s", app_id.c_str());
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
@@ -89,6 +100,9 @@ TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id,
m_app_timer_counts[app_id]++;
m_app_timer_ids[app_id].insert(timer.id);
TIMER_LOG("[Timer] Created interval timer id=%llu for %s (total: %zu)",
timer.id, app_id.c_str(), m_timers.size());
return timer.id;
}
@@ -158,9 +172,13 @@ void TimerManager::ClearAppTimers(const std::string& app_id) {
void TimerManager::FireTimer(Timer& timer) {
if (timer.cancelled || timer.callback_ref == LUA_NOREF || !timer.L) {
TIMER_LOG("[Timer] FireTimer skipped: cancelled=%d, ref=%d, L=%p",
timer.cancelled, timer.callback_ref, timer.L);
return;
}
TIMER_LOG("[Timer] Firing timer id=%llu for %s", timer.id, timer.app_id.c_str());
lua_State* L = timer.L;
// Get the callback from registry
@@ -170,10 +188,15 @@ void TimerManager::FireTimer(Timer& timer) {
// Call the callback with protected call
int result = lua_pcall(L, 0, 0, 0);
if (result != LUA_OK) {
// Log error but don't propagate
// Log the error
const char* err = lua_tostring(L, -1);
TIMER_LOG("[Timer] ERROR in callback: %s", err ? err : "(unknown)");
lua_pop(L, 1);
} else {
TIMER_LOG("[Timer] Callback completed successfully");
}
} else {
TIMER_LOG("[Timer] ERROR: callback ref is not a function!");
lua_pop(L, 1);
}
}
@@ -209,6 +232,12 @@ int TimerManager::ProcessTimers() {
}
}
}
// Log only when we have timers to fire (to avoid spam)
if (!to_fire.empty()) {
TIMER_LOG("[Timer] ProcessTimers: %zu ready to fire out of %zu total",
to_fire.size(), m_timers.size());
}
}
// Fire timers outside the lock to allow callbacks to create new timers

View File

@@ -10,19 +10,20 @@
using namespace mosis::test;
// Helper: Scale coordinates from hierarchy (logical) to window (physical) space
// Helper: Scale coordinates from hierarchy to phone space
// The WindowController expects coordinates in phone space (540x960) and handles DPI scaling internally
void ScaleToPhysical(TestContext& ctx, int& x, int& y) {
// The hierarchy reports coordinates in RmlUi's logical space
// which may be DPI-scaled. We need to convert to physical window coordinates.
// The hierarchy reports coordinates in RmlUi's context space (540x960)
// which matches the phone resolution. No scaling needed since
// WindowController::SendClick handles DPI scaling internally.
// Just validate the hierarchy dimensions match expected phone size.
int hierarchyWidth = ctx.hierarchy.GetWidth();
int hierarchyHeight = ctx.hierarchy.GetHeight();
int windowWidth = ctx.window.GetInfo().clientWidth;
int windowHeight = ctx.window.GetInfo().clientHeight;
if (hierarchyWidth > 0 && hierarchyHeight > 0) {
x = static_cast<int>(x * static_cast<float>(windowWidth) / hierarchyWidth);
y = static_cast<int>(y * static_cast<float>(windowHeight) / hierarchyHeight);
if (hierarchyWidth != 540 || hierarchyHeight != 960) {
std::cerr << " Warning: Unexpected hierarchy size " << hierarchyWidth << "x" << hierarchyHeight << std::endl;
}
// Coordinates stay in phone space (540x960) - no scaling needed
}
// Helper: Click on an element found by ID in the hierarchy

View File

@@ -107,33 +107,47 @@ LPARAM WindowController::PhoneToClientLParam(int phoneX, int phoneY) {
bool WindowController::SendMouseDown(int phoneX, int phoneY) {
if (!m_hwnd) return false;
// Convert phone coordinates to client coordinates
// Convert phone coordinates to client coordinates (using window scale)
int clientX = static_cast<int>(phoneX * m_info.scaleX);
int clientY = static_cast<int>(phoneY * m_info.scaleY);
// Calculate screen coordinates
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Get DPI info for debugging
UINT dpi = GetDpiForWindow(m_hwnd);
// Calculate screen coordinates from client position
// On DPI-aware systems, Windows APIs return consistent coordinate spaces
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Ensure window is foreground before clicking
SetForegroundWindow(m_hwnd);
Sleep(10); // Small delay
Sleep(10);
// Use SendInput for GLFW compatibility
SetCursorPos(screenX, screenY);
Sleep(10); // Small delay for cursor move
// Get screen dimensions for absolute coordinate conversion
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Convert to normalized absolute coordinates (0-65535)
LONG absX = static_cast<LONG>((screenX * 65535) / screenWidth);
LONG absY = static_cast<LONG>((screenY * 65535) / screenHeight);
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> screen("
<< screenX << "," << screenY << ") dpi=" << dpi << std::endl;
// Send mouse move then button down
INPUT moveInput = {};
moveInput.type = INPUT_MOUSE;
moveInput.mi.dx = absX;
moveInput.mi.dy = absY;
moveInput.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
SendInput(1, &moveInput, sizeof(INPUT));
Sleep(20);
INPUT clickInput = {};
clickInput.type = INPUT_MOUSE;
clickInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &clickInput, sizeof(INPUT));
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> client("
<< clientX << "," << clientY << ") screen(" << screenX << "," << screenY
<< ") dpi=" << dpi << std::endl;
return true;
}
@@ -226,38 +240,20 @@ bool WindowController::Close() {
bool WindowController::ResizeToPhone() {
if (!m_hwnd) return false;
// Get current window rect to preserve position
// Skip resizing - the designer creates the window at the correct size
// and resizing causes DPI scaling mismatches between window and framebuffer.
// Just move window to top-left for consistent test positioning.
RECT currentRect;
::GetWindowRect(m_hwnd, &currentRect);
int currentWidth = currentRect.right - currentRect.left;
int currentHeight = currentRect.bottom - currentRect.top;
// Calculate window size needed for phone-sized client area
RECT desiredRect = {0, 0, PHONE_WIDTH, PHONE_HEIGHT};
DWORD style = ::GetWindowLong(m_hwnd, GWL_STYLE);
DWORD exStyle = ::GetWindowLong(m_hwnd, GWL_EXSTYLE);
::AdjustWindowRectEx(&desiredRect, style, FALSE, exStyle);
std::cout << "ResizeToPhone: keeping current size " << currentWidth << "x" << currentHeight
<< ", moving to (0,0)" << std::endl;
int newWidth = desiredRect.right - desiredRect.left;
int newHeight = desiredRect.bottom - desiredRect.top;
// Check screen dimensions
int screenWidth = ::GetSystemMetrics(SM_CXSCREEN);
int screenHeight = ::GetSystemMetrics(SM_CYSCREEN);
std::cout << "ResizeToPhone: screen=" << screenWidth << "x" << screenHeight
<< ", needed=" << newWidth << "x" << newHeight << std::endl;
// If screen is too small, we can't resize to full phone size
if (newHeight > screenHeight) {
std::cout << " Warning: Screen too small for full phone resolution, keeping current size" << std::endl;
return true; // Not an error, just can't resize
}
// Move window to top-left to ensure it fits on screen
int newX = 0;
int newY = 0;
std::cout << " Moving to (" << newX << "," << newY << ") size " << newWidth << "x" << newHeight << std::endl;
::MoveWindow(m_hwnd, newX, newY, newWidth, newHeight, TRUE);
// Move window to top-left for consistent positioning
::MoveWindow(m_hwnd, 0, 0, currentWidth, currentHeight, TRUE);
// Re-acquire window info
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -274,26 +270,36 @@ bool WindowController::SendClickFromBottom(int clientX, int offsetFromBottom) {
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Save current cursor position
POINT oldPos;
GetCursorPos(&oldPos);
// Ensure window is active
SetForegroundWindow(m_hwnd);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Move cursor and click
SetCursorPos(screenX, screenY);
// Get screen dimensions for absolute coordinate conversion
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Convert to normalized absolute coordinates (0-65535)
LONG absX = static_cast<LONG>((screenX * 65535) / screenWidth);
LONG absY = static_cast<LONG>((screenY * 65535) / screenHeight);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Use SendInput with absolute move + click
INPUT inputs[3] = {};
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
// Move cursor
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dx = absX;
inputs[0].mi.dy = absY;
inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
// Mouse down
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
// Mouse up
inputs[2].type = INPUT_MOUSE;
inputs[2].mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(3, inputs, sizeof(INPUT));
std::cout << "ClickFromBottom at client(" << clientX << "," << clientY
<< ") -> screen(" << screenX << "," << screenY << ")" << std::endl;

View File

@@ -18,7 +18,7 @@ include(FetchContent)
FetchContent_Declare(
rmlui
GIT_REPOSITORY https://github.com/mikke89/RmlUi.git
GIT_TAG 6.0
GIT_TAG 6.2
)
set(RMLUI_LUA_BINDINGS ON CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)

View File

@@ -16,8 +16,10 @@
#include "testing/ui_inspector.h"
#include "testing/visual_capture.h"
#include <RmlUi/Lua/Interpreter.h>
#include <RmlUi/Lua/LuaType.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <memory>
#include <chrono>
@@ -57,6 +59,11 @@ static std::string g_test_output_path; // Output file for record/screenshot/hie
static mosis::testing::ActionRecorder* g_action_recorder = nullptr;
static mosis::testing::ActionPlayer* g_action_player = nullptr;
// Touch simulation state for natural scrolling behavior
static bool g_touch_active = false;
static int g_touch_start_x = 0;
static int g_touch_start_y = 0;
// Logging and hierarchy dump for testing
static std::string g_log_file_path; // Log file path (--log)
static std::string g_hierarchy_file_path; // Continuous hierarchy dump path (--hierarchy)
@@ -64,8 +71,11 @@ static std::ofstream g_log_file;
// Simulator mode
static bool g_simulator_mode = false;
static bool g_shell_mode = false; // Use persistent shell instead of direct documents
static std::string g_test_apps_path; // Path to test-apps directory
static std::string g_simulator_home_path; // Path to simulator home.rml
static std::string g_shell_path; // Path to shell.rml for shell mode
static std::string g_main_assets_path; // Path to main assets (for goHome)
static std::vector<mosis::AppInfo> g_discovered_apps;
static std::string g_current_app_id; // Currently running app (empty = home)
@@ -76,6 +86,18 @@ bool LoadDocument(const std::string& path);
void ReloadDocument();
void PopulateSimulatorApps();
// Helper to set the 'document' global in Lua to the current document
static void SetLuaDocumentGlobal(Rml::ElementDocument* doc) {
if (!doc) return;
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
if (!L) return;
// Push the document using RmlUi's Lua type system
Rml::Lua::LuaType<Rml::ElementDocument>::push(L, doc, false);
lua_setglobal(L, "document");
std::cout << "Set Lua 'document' global to: " << doc->GetTitle() << std::endl;
}
// GLFW callbacks
static void ErrorCallback(int error, const char* description) {
std::cerr << "GLFW Error " << error << ": " << description << std::endl;
@@ -156,10 +178,30 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
// which match the RmlUi context dimensions, so no scaling needed
int mouseX = static_cast<int>(xpos);
int mouseY = static_cast<int>(ypos);
// glfwGetCursorPos returns position in screen coordinates (same as window size)
// which may differ from framebuffer size on high-DPI displays.
// We need to scale to match the RmlUi context (which matches framebuffer).
int winWidth, winHeight;
glfwGetWindowSize(window, &winWidth, &winHeight);
int fbWidth, fbHeight;
glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
// Scale cursor position: screen coords -> framebuffer coords -> RmlUi context
// On high DPI: winWidth=432, fbWidth=540, g_width=540
// Cursor in screen space needs to scale to framebuffer/context space
int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
int mouseY = static_cast<int>(ypos * fbHeight / winHeight);
// Debug logging for click events
std::cout << "MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
<< " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")"
<< " win=" << winWidth << "x" << winHeight << " fb=" << fbWidth << "x" << fbHeight << std::endl;
if (g_log_file.is_open()) {
g_log_file << "[DEBUG] MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
<< " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")" << std::endl;
g_log_file.flush();
}
int key_modifier = 0;
if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
@@ -174,12 +216,20 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
if (g_action_recorder && g_action_recorder->IsRecording()) {
g_action_recorder->RecordMouseDown(mouseX, mouseY);
}
g_context->ProcessMouseButtonDown(0, key_modifier);
// Use touch API for natural scrolling behavior
g_touch_active = true;
g_touch_start_x = mouseX;
g_touch_start_y = mouseY;
Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
g_context->ProcessTouchStart(touches, 0);
} else if (action == GLFW_RELEASE) {
if (g_action_recorder && g_action_recorder->IsRecording()) {
g_action_recorder->RecordMouseUp(mouseX, mouseY);
}
g_context->ProcessMouseButtonUp(0, key_modifier);
// End touch for natural scrolling
g_touch_active = false;
Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
g_context->ProcessTouchEnd(touches, 0);
}
}
}
@@ -187,12 +237,24 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
if (!g_context) return;
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
// which match the RmlUi context dimensions, so no scaling needed
int mouseX = static_cast<int>(xpos);
int mouseY = static_cast<int>(ypos);
// Scale from screen coordinates to framebuffer/RmlUi context coordinates
int winWidth, winHeight;
glfwGetWindowSize(window, &winWidth, &winHeight);
int fbWidth, fbHeight;
glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
int mouseY = static_cast<int>(ypos * fbHeight / winHeight);
// Always update mouse position for hover effects
g_context->ProcessMouseMove(mouseX, mouseY, 0);
// If touch is active (left button pressed), also send touch move for natural scrolling
if (g_touch_active) {
Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
g_context->ProcessTouchMove(touches, 0);
}
}
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
@@ -245,7 +307,8 @@ static void PrintUsage() {
<< " --log FILE Write all log messages to file\n"
<< " --hierarchy FILE Continuously dump UI hierarchy to JSON\n"
<< "\nSimulator mode:\n"
<< " --simulator Run in simulator mode (app launcher)\n"
<< " --simulator Run in simulator mode (uses shell by default)\n"
<< " --no-shell Disable shell (use direct document loading)\n"
<< " --test-apps PATH Path to test-apps directory (default: ./test-apps)\n"
<< "\nTest modes:\n"
<< " --record FILE Record user actions to JSON file\n"
@@ -304,6 +367,9 @@ int main(int argc, char* argv[]) {
g_test_output_path = argv[++i];
} else if (arg == "--simulator") {
g_simulator_mode = true;
g_shell_mode = true; // Shell mode is implicit in simulator mode
} else if (arg == "--no-shell") {
g_shell_mode = false; // Disable shell (use direct document loading)
} else if (arg == "--test-apps" && i + 1 < argc) {
g_test_apps_path = argv[++i];
} else if (arg[0] != '-') {
@@ -339,34 +405,42 @@ int main(int argc, char* argv[]) {
g_test_apps_path = fs::absolute(g_test_apps_path).string();
}
// Simulator home screen path
std::vector<std::string> sim_search_paths = {
"simulator/home.rml",
"../designer/assets/simulator/home.rml",
fs::absolute("simulator/home.rml").string(),
// Find main assets for simulator mode
// Look for src/main/assets/apps/shell/shell.rml relative to test-apps
fs::path test_apps_fs = fs::path(g_test_apps_path);
std::vector<fs::path> shell_search_paths = {
test_apps_fs.parent_path() / "src" / "main" / "assets" / "apps" / "shell" / "shell.rml",
fs::path("src/main/assets/apps/shell/shell.rml"),
fs::absolute("src/main/assets/apps/shell/shell.rml"),
};
for (const auto& path : sim_search_paths) {
for (const auto& path : shell_search_paths) {
if (fs::exists(path)) {
g_simulator_home_path = fs::absolute(path).string();
break;
}
}
// Override document path to simulator home
// Override document path to shell
if (!g_simulator_home_path.empty()) {
document_path = g_simulator_home_path;
// Set the main assets path for proper resource loading
// g_simulator_home_path is now shell.rml, go up 3 levels to get assets path
g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string();
g_shell_path = g_simulator_home_path;
document_path = g_shell_path;
std::cout << "Shell mode enabled" << std::endl;
std::cout << "Shell: " << g_shell_path << std::endl;
std::cout << "Simulator mode enabled" << std::endl;
std::cout << "Test apps path: " << g_test_apps_path << std::endl;
std::cout << "Simulator home: " << g_simulator_home_path << std::endl;
} else {
std::cerr << "Warning: Could not find simulator home.rml" << std::endl;
std::cerr << "Warning: Could not find shell.rml for simulator" << std::endl;
g_simulator_mode = false;
}
}
// Default document
if (document_path.empty()) {
document_path = "apps/home/home.rml";
document_path = "apps/shell/shell.rml";
}
// Derive assets path from document path if not specified
@@ -428,6 +502,26 @@ int main(int argc, char* argv[]) {
// Make assets_path absolute
assets_path = fs::absolute(assets_path).string();
std::cout << "Assets path: " << assets_path << std::endl;
// Determine main assets path (where home.rml lives)
// This is important for goHome() when running test apps
g_main_assets_path = assets_path;
// Check if we're running from test-apps folder
fs::path assets_fs = fs::path(assets_path);
if (assets_fs.filename().string().find("com.") == 0 ||
assets_fs.parent_path().filename() == "test-apps") {
// Running a test app - find the main assets
fs::path test_apps_root = assets_fs.parent_path();
if (test_apps_root.filename() != "test-apps") {
test_apps_root = test_apps_root.parent_path();
}
fs::path main_assets = test_apps_root.parent_path() / "src" / "main" / "assets";
if (fs::exists(main_assets / "apps" / "shell" / "shell.rml")) {
g_main_assets_path = main_assets.string();
std::cout << "Main assets: " << g_main_assets_path << std::endl;
}
}
std::cout << "Resolution: " << g_width << "x" << g_height << std::endl;
// Print mode info
@@ -738,6 +832,7 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
auto* document = g_context->LoadDocument(path);
if (document) {
document->Show();
SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts
g_current_document_path = path;
g_current_screen_url = path; // Track current screen for hierarchy dump
// Log using RmlUi logging so it appears in log file
@@ -750,7 +845,132 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
return 1;
});
lua_setglobal(L, "loadScreen");
std::cout << "Registered Lua loadScreen function" << std::endl;
// Register goHome function to return to home screen
lua_pushcfunction(L, [](lua_State* L) -> int {
if (!g_context) {
lua_pushboolean(L, false);
return 1;
}
std::cout << "goHome called - returning to home screen" << std::endl;
// Reset sandbox back to home context
if (g_sandbox && !g_current_app_id.empty()) {
g_sandbox->UnregisterAPIs(L);
g_sandbox->Reset();
mosis::DesktopSandboxConfig config;
config.app_id = "com.mosis.home";
config.data_root = g_main_assets_path + "/sandbox_data";
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
g_sandbox->RegisterAPIs(L);
std::cout << "Sandbox reset to home context" << std::endl;
}
// Close existing documents (except debugger)
while (g_context->GetNumDocuments() > 1) {
auto* doc = g_context->GetDocument(0);
if (doc && doc->GetSourceURL().find("__rmlui") == std::string::npos) {
doc->Close();
} else {
break;
}
}
// Load shell (which shows home by default)
std::string home_path = (fs::path(g_main_assets_path) / "apps" / "shell" / "shell.rml").string();
std::cout << "Loading shell from: " << home_path << std::endl;
auto* document = g_context->LoadDocument(home_path);
if (document) {
document->Show();
SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts
g_current_document_path = home_path;
g_current_screen_url = home_path;
g_current_app_id = ""; // Clear current app
Rml::Log::Message(Rml::Log::LT_INFO, "Returned to home screen");
lua_pushboolean(L, true);
} else {
Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load home screen from: %s", home_path.c_str());
lua_pushboolean(L, false);
}
return 1;
});
lua_setglobal(L, "goHome");
// Register switchAppSandbox function for third-party app launching
lua_pushcfunction(L, [](lua_State* L) -> int {
const char* app_id = luaL_checkstring(L, 1);
const char* install_path = luaL_checkstring(L, 2);
std::cout << "switchAppSandbox called for: " << app_id << " at " << install_path << std::endl;
// Store current app ID
g_current_app_id = app_id;
// Reset sandbox for the new app
if (g_sandbox) {
g_sandbox->UnregisterAPIs(L);
g_sandbox->Reset();
// Re-configure sandbox with app-specific data root
mosis::DesktopSandboxConfig config;
config.app_id = app_id;
config.data_root = std::string(install_path) + "/sandbox_data";
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
g_sandbox->RegisterAPIs(L);
std::cout << "Sandbox switched to: " << app_id << std::endl;
}
lua_pushboolean(L, true);
return 1;
});
lua_setglobal(L, "switchAppSandbox");
// Register loadAppContent function for shell-based app loading
// loadAppContent(element, path) - loads RML content into an element's inner_rml
lua_pushcfunction(L, [](lua_State* L) -> int {
// Get element from first argument (RmlUi element userdata)
Rml::Element* element = Rml::Lua::LuaType<Rml::Element>::check(L, 1);
const char* path = luaL_checkstring(L, 2);
if (!element) {
std::cerr << "loadAppContent: Invalid element" << std::endl;
lua_pushboolean(L, false);
return 1;
}
// Resolve path relative to assets directory
std::string full_path;
if (fs::path(path).is_absolute()) {
full_path = path;
} else {
full_path = (fs::path(g_main_assets_path) / path).string();
}
// Read file content
std::ifstream file(full_path);
if (!file.is_open()) {
std::cerr << "loadAppContent: Cannot open file: " << full_path << std::endl;
lua_pushboolean(L, false);
return 1;
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
// Set as inner_rml
element->SetInnerRML(content);
Rml::Log::Message(Rml::Log::LT_INFO, "Loaded app content from: %s", path);
lua_pushboolean(L, true);
return 1;
});
lua_setglobal(L, "loadAppContent");
std::cout << "Registered Lua loadScreen, goHome, switchAppSandbox, and loadAppContent functions" << std::endl;
// Register simulator API (if in simulator mode)
if (g_simulator_mode) {
@@ -798,6 +1018,7 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
auto* document = g_context->LoadDocument(entry);
if (document) {
document->Show();
SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts
g_current_document_path = entry;
g_current_screen_url = entry;
std::cout << "Simulator: App launched successfully" << std::endl;
@@ -842,6 +1063,7 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
auto* document = g_context->LoadDocument(g_simulator_home_path);
if (document) {
document->Show();
SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts
g_current_document_path = g_simulator_home_path;
g_current_screen_url = g_simulator_home_path;
@@ -913,6 +1135,93 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
lua_setglobal(L, "simulator");
std::cout << "Registered simulator Lua API" << std::endl;
// Also register mosis.apps API for compatibility with home.lua
// Create mosis table (or get existing)
lua_getglobal(L, "mosis");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
// Create mosis.apps table
lua_newtable(L);
// mosis.apps.getInstalled() - returns installed apps
lua_pushcfunction(L, [](lua_State* L) -> int {
// Discover apps if not already done
if (g_discovered_apps.empty() && !g_test_apps_path.empty()) {
g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
std::cout << "Discovered " << g_discovered_apps.size() << " apps in " << g_test_apps_path << std::endl;
}
// Create apps table (array format expected by home.lua)
lua_newtable(L);
int app_index = 1;
for (const auto& app : g_discovered_apps) {
lua_pushinteger(L, app_index++);
lua_newtable(L);
lua_pushstring(L, app.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, app.id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, app.GetIconPath().c_str());
lua_setfield(L, -2, "icon");
lua_pushboolean(L, app.is_system_app);
lua_setfield(L, -2, "is_system_app");
lua_pushstring(L, app.app_path.c_str());
lua_setfield(L, -2, "install_path");
lua_pushstring(L, app.entry.c_str());
lua_setfield(L, -2, "entry_point");
lua_settable(L, -3); // apps[index] = app_table
}
return 1;
});
lua_setfield(L, -2, "getInstalled");
// mosis.apps.launch(package_id) - launch an app
lua_pushcfunction(L, [](lua_State* L) -> int {
const char* package_id = luaL_checkstring(L, 1);
// Find the app
for (const auto& app : g_discovered_apps) {
if (app.id == package_id) {
std::cout << "mosis.apps.launch: Starting " << package_id << std::endl;
g_current_app_id = package_id;
// Reset sandbox for the new app
if (g_sandbox) {
g_sandbox->UnregisterAPIs(L);
g_sandbox->Reset();
mosis::DesktopSandboxConfig config;
config.app_id = package_id;
config.data_root = app.app_path + "/sandbox_data";
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
g_sandbox->RegisterAPIs(L);
}
lua_pushboolean(L, true);
return 1;
}
}
std::cerr << "mosis.apps.launch: App not found: " << package_id << std::endl;
lua_pushboolean(L, false);
return 1;
});
lua_setfield(L, -2, "launch");
lua_setfield(L, -2, "apps"); // mosis.apps = apps_table
lua_setglobal(L, "mosis");
std::cout << "Registered mosis.apps Lua API" << std::endl;
}
// Load fonts - search for fonts directory in multiple locations
@@ -989,8 +1298,9 @@ bool LoadDocument(const std::string& path) {
std::cerr << "Failed to load: " << path << std::endl;
return false;
}
document->Show();
SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts
g_current_document_path = path;
g_current_screen_url = path; // Track current screen for hierarchy dump
std::cout << "Loaded: " << path << std::endl;

View File

@@ -68,6 +68,7 @@ bool AppDiscovery::LoadAppManifest(const std::string& app_directory, AppInfo& in
info.version = j.value("version", "1.0.0");
info.icon = j.value("icon", "icon.tga");
info.description = j.value("description", "");
info.is_system_app = j.value("is_system_app", false);
// Normalize path separators
info.app_path = app_directory;

View File

@@ -11,13 +11,20 @@ struct AppInfo {
std::string name; // Display name
std::string version; // Version string
std::string entry; // Entry point (e.g., "main.rml")
std::string icon; // Icon filename (e.g., "icon.tga")
std::string icon; // Icon filename (e.g., "icon.tga") or path (e.g., "/system/icons/phone.tga")
std::string description; // App description
std::string app_path; // Full path to app directory
bool is_system_app = false; // True for system apps (com.mosis.*)
// Computed paths
std::string GetEntryPath() const { return app_path + "/" + entry; }
std::string GetIconPath() const { return app_path + "/" + icon; }
std::string GetIconPath() const {
// If icon starts with /, it's an absolute/system path - don't prepend app_path
if (!icon.empty() && icon[0] == '/') {
return icon;
}
return app_path + "/" + icon;
}
};
class AppDiscovery {

View File

@@ -19,6 +19,11 @@ void DesktopFileInterface::SetAssetsPath(const std::string& path) {
std::string DesktopFileInterface::ResolvePath(const std::string& path) const {
std::string resolved = path;
// Handle file:// URLs
if (resolved.rfind("file://", 0) == 0) {
resolved = resolved.substr(7); // Strip "file://"
}
// Handle URL-encoded Windows drive letters (D| -> D:)
// RmlUi sometimes encodes the colon in Windows paths
if (resolved.size() >= 2 && std::isalpha(resolved[0]) && resolved[1] == '|') {

386
docs/BASE_APPS.md Normal file
View File

@@ -0,0 +1,386 @@
# Base Apps Migration Plan
Convert system apps from embedded assets to standalone `.mosis` packages in `base-apps/`.
---
## Current State
### Apps in `src/main/assets/apps/`
| App | Files | Icon |
|-----|-------|------|
| home | home.rml, home.lua, lock.rml | home.tga |
| dialer | dialer.rml, calling.rml | phone.tga |
| contacts | contacts.rml | contacts.tga |
| messages | messages.rml | message.tga |
| settings | settings.rml | settings.tga |
| browser | browser.rml | browser.tga |
| camera | camera.rml | camera.tga |
| store | store.rml | store.tga |
| music | music.rml | music.tga |
### Shared Dependencies
All apps currently reference shared resources via relative paths:
```
../../ui/html.rcss - Base HTML element styles
../../ui/theme.rcss - Color scheme, typography
../../ui/components.rcss - Buttons, cards, lists
../../scripts/navigation.lua - Screen navigation system
../../icons/*.tga - App and UI icons
```
### Current Navigation Model
Apps are not truly separate - they're screens loaded via a central navigation system:
```lua
-- navigation.lua
local screens = {
home = "apps/home/home.rml",
dialer = "apps/dialer/dialer.rml",
-- etc.
}
mosis.loadDocument(screens[name])
```
---
## Target State
### Directory Structure
```
base-apps/
├── com.mosis.home/
│ ├── manifest.json
│ ├── main.rml
│ ├── home.lua
│ ├── lock.rml
│ ├── icon.tga
│ └── styles.rcss
├── com.mosis.dialer/
│ ├── manifest.json
│ ├── main.rml (dialer.rml)
│ ├── calling.rml
│ ├── icon.tga
│ └── styles.rcss
├── com.mosis.contacts/
├── com.mosis.messages/
├── com.mosis.settings/
├── com.mosis.browser/
├── com.mosis.camera/
├── com.mosis.store/
├── com.mosis.music/
└── package.bat
```
### Package IDs
| App | Package ID |
|-----|------------|
| Home | com.mosis.home |
| Dialer | com.mosis.dialer |
| Contacts | com.mosis.contacts |
| Messages | com.mosis.messages |
| Settings | com.mosis.settings |
| Browser | com.mosis.browser |
| Camera | com.mosis.camera |
| Store | com.mosis.store |
| Music | com.mosis.music |
---
## Key Decisions
### 1. Shared Resources Strategy
**Decision**: System asset path (`/system/`)
Apps reference shared resources via a system path that the kernel provides:
```html
<link type="text/rcss" href="/system/ui/theme.rcss"/>
<script src="/system/scripts/navigation.lua"></script>
```
The kernel maps `/system/` to `src/main/assets/` (or bundled assets on Android).
**Rationale**:
- Avoids duplicating ~50KB of styles/icons per app
- Single source of truth for theming
- Apps can still have local overrides
### 2. Navigation Model
**Decision**: Hybrid approach
- **System apps** (home, dialer, contacts, etc.) use shared navigation for seamless transitions
- **Third-party apps** launch via `mosis.apps.launch()` with isolated sandbox
System apps get `is_system_app = true` flag:
- Can access `/system/` resources
- Share navigation state
- Can load each other's screens directly
### 3. Home App Special Status
The home app is the launcher/shell:
- Always running in background
- Provides dock and app grid
- Launches other apps via `mosis.apps.launch(package_id)`
- Cannot be uninstalled
---
## Implementation Tasks
### Phase 1: Infrastructure
- [ ] **1.1** Rename `test-apps/` to `base-apps/`
- [ ] **1.2** Move `com.mosis.sandbox-test` to `base-apps/`
- [ ] **1.3** Implement `/system/` path mapping in kernel
- Desktop: Map to `assets/` directory
- Android: Map to bundled assets
- [ ] **1.4** Add `is_system_app` flag to manifest schema
- [ ] **1.5** Update designer `--test-apps` flag to `--apps` for consistency
### Phase 2: Convert Apps
For each app:
- [ ] **2.1** Create package directory in `base-apps/com.mosis.{name}/`
- [ ] **2.2** Create `manifest.json` with proper metadata
- [ ] **2.3** Copy RML files, rename entry to `main.rml`
- [ ] **2.4** Copy app-specific Lua scripts
- [ ] **2.5** Copy icon from `icons/{name}.tga` to `icon.tga`
- [ ] **2.6** Update resource paths to use `/system/` prefix
- [ ] **2.7** Create `styles.rcss` for app-specific styles (extract from inline)
### Phase 3: Update Home App
- [ ] **3.1** Update home.lua to use `mosis.apps.launch()` for launching
- [ ] **3.2** Keep shared navigation for system app transitions
- [ ] **3.3** Display all installed apps (base + third-party) in grid
- [ ] **3.4** Update dock icons to launch via package ID
### Phase 4: Cleanup
- [ ] **4.1** Remove old `src/main/assets/apps/` directory
- [ ] **4.2** Update build scripts to package base-apps
- [ ] **4.3** Update Android to include base-apps packages
- [ ] **4.4** Update documentation
---
## Manifest Examples
### System App (Dialer)
```json
{
"id": "com.mosis.dialer",
"name": "Phone",
"version": "1.0.0",
"version_code": 1,
"entry": "main.rml",
"icon": "icon.tga",
"description": "Make and receive phone calls",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"permissions": [
"contacts.read"
],
"is_system_app": true,
"min_api_version": 1
}
```
### Home App (Launcher)
```json
{
"id": "com.mosis.home",
"name": "Home",
"version": "1.0.0",
"version_code": 1,
"entry": "main.rml",
"icon": "icon.tga",
"description": "Mosis Home Launcher",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"permissions": [
"system.launcher"
],
"is_system_app": true,
"is_launcher": true,
"min_api_version": 1
}
```
---
## Resource Path Mapping
### Before (Relative)
```html
<link type="text/rcss" href="../../ui/theme.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<img src="../../icons/phone.tga"/>
```
### After (System Path)
```html
<link type="text/rcss" href="/system/ui/theme.rcss"/>
<script src="/system/scripts/navigation.lua"></script>
<img src="/system/icons/phone.tga"/>
```
---
## Testing Checklist
- [ ] Designer loads home from `base-apps/com.mosis.home/`
- [ ] All base apps appear in home screen grid
- [ ] Tapping app icon launches the app
- [ ] Navigation between system apps works (back button, dock)
- [ ] Third-party apps (sandbox-test) launch in isolated sandbox
- [ ] `/system/` paths resolve correctly
- [ ] Android build includes all base-apps packages
- [ ] Hot-reload still works for development
---
## Open Questions
1. **Lock screen**: Part of home app or separate `com.mosis.lockscreen`?
2. **Calling screen**: Part of dialer or system-level overlay?
3. **Notifications**: System-level or per-app handling?
4. **Settings persistence**: Shared settings service or per-app?
---
## File Changes Summary
| Action | Path |
|--------|------|
| Rename | `test-apps/``base-apps/` |
| Create | `base-apps/com.mosis.{home,dialer,...}/` |
| Create | Each app's `manifest.json` |
| Move | App RML/Lua files to package dirs |
| Update | RML paths from `../../` to `/system/` |
| Delete | `src/main/assets/apps/` (after migration) |
| Update | Designer command line args |
| Update | Android asset bundling |
---
## Reusable Layout Components
New standardized UI components for consistent app layouts. Located in `src/main/assets/ui/layout.rcss` and `src/main/assets/scripts/layout.lua`.
### Components
| Component | CSS Class | Description |
|-----------|-----------|-------------|
| Status Bar | `.system-status-bar` | Top bar with time, wifi, signal, battery |
| App Bar | `.app-bar` | Title bar with back button and actions |
| System Nav | `.system-nav-bar` | Bottom bar with back, home, recent buttons |
| App Screen | `.app-screen` | Standard app screen container |
| App Content | `.app-content` | Scrollable content area |
### Standard App Structure
```html
<body class="app-screen" onload="initLayout(document)">
<!-- System Status Bar -->
<div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- App Bar -->
<div class="app-bar">
<div class="app-bar-back" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">App Title</span>
<div class="app-bar-actions">
<div class="app-bar-action">
<img src="../../icons/search.tga"/>
</div>
</div>
</div>
<!-- Content Area -->
<div class="app-content with-nav">
<!-- App content here -->
</div>
<!-- System Navigation Bar -->
<div class="system-nav-bar">
<div class="system-nav-btn" onclick="onBackPressed()">
<img src="../../icons/back.tga"/>
</div>
<div class="system-nav-home" onclick="onHomePressed()"></div>
<div class="system-nav-btn" onclick="onRecentPressed()">
<img src="../../icons/menu.tga"/>
</div>
</div>
</body>
```
### Required Includes
```html
<head>
<link type="text/rcss" href="../../ui/html.rcss"/>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script>
</head>
```
### CSS Modifiers
| Class | Description |
|-------|-------------|
| `.app-content.with-nav` | Adds bottom padding for system nav bar |
| `.app-content.with-dock` | Adds bottom padding for dock |
| `.app-bar.transparent` | Transparent app bar background |
| `.system-status-bar.bg-surface` | Solid surface color background |
### Lua Functions (layout.lua)
| Function | Description |
|----------|-------------|
| `initLayout(doc)` | Initialize all layout components |
| `initStatusBar(doc)` | Setup status bar time updates |
| `onBackPressed()` | Handle back button (goes back or home) |
| `onHomePressed()` | Handle home button |
| `onRecentPressed()` | Handle recent apps button |
### Updated Apps
- [x] Settings - Uses full layout with system nav bar
- [x] Contacts - Uses status bar and app bar (has own bottom tabs)
- [ ] Other apps - Still using old structure
---
*Created: 2025-01-19*
*Updated: 2025-01-20*

View File

@@ -23,7 +23,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
| MosisService | ✅ Working | RmlUi rendering, touch input, navigation |
| App Management | ✅ Working | Install/uninstall apps, sandbox integration |
| Lua Sandbox | ✅ Working | 149 security tests passing |
| Desktop Designer | ✅ Working | Hot-reload, hierarchy dump, recording |
| Desktop Designer | ✅ Working | Hot-reload, shell mode, hierarchy dump, recording |
| Designer Tests | ✅ 5/5 Passing | Navigation tests automated |
| MosisVR (Unity) | ✅ Building | OpenGL backend working, Vulkan in progress |
| MosisUnreal | ✅ Working | Vulkan texture import via UE5 RHI, phone actor with mesh |
@@ -35,6 +35,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
| Android Service | `src/main/` | Native service running RmlUi renderer |
| App Management | `src/main/cpp/apps/` | App install/uninstall/launch with sandbox |
| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation (22 modules) |
| System Shell | `src/main/assets/apps/shell/` | Persistent status bar, nav bar, overlays |
| Desktop Designer | `designer/` | UI development with hot-reload |
| Designer Tests | `designer-test/` | Automated UI testing framework |
| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests (149 tests) |
@@ -48,7 +49,7 @@ All detailed documentation is in `docs/`:
|----------|-------------|
| [BUILD-COMMANDS.md](BUILD-COMMANDS.md) | Android, Desktop Designer, and test build commands |
| [ARCHITECTURE.md](ARCHITECTURE.md) | Native libraries, IPC flow, code structure |
| [DESKTOP-DESIGNER.md](DESKTOP-DESIGNER.md) | Hot-reload, recording, key files |
| [DESKTOP-DESIGNER.md](DESKTOP-DESIGNER.md) | Shell architecture, hot-reload, recording |
| [TESTING-FRAMEWORK.md](TESTING-FRAMEWORK.md) | Automated UI testing, writing tests |
| [UI-ASSETS.md](UI-ASSETS.md) | Asset structure, navigation system, element IDs |
| [MATERIAL-DESIGN.md](MATERIAL-DESIGN.md) | Icons, MDL components, usage guide |

Some files were not shown because too many files have changed in this diff Show More