fix app layouts: remove style tags from content fragments, use component classes

This commit is contained in:
2026-01-20 17:40:38 +01:00
parent 4b47611902
commit 82bc0c78fe
25 changed files with 5350 additions and 537 deletions

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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="browser.lua"></script>
<title>Browser</title> <title>Browser</title>
<style> <style>
.browser-toolbar { .browser-toolbar {
@@ -30,12 +31,17 @@
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.browser-nav-btn:active {
background-color: rgba(255, 255, 255, 0.2);
}
.browser-nav-btn img { .browser-nav-btn img {
width: 28px; width: 28px;
height: 28px; height: 28px;
pointer-events: none;
} }
.browser-nav-btn.disabled img { .browser-nav-btn.disabled {
opacity: 0.3; opacity: 0.3;
} }
@@ -46,6 +52,11 @@
padding: 10px 16px; padding: 10px 16px;
background-color: #2D2D2D; background-color: #2D2D2D;
border-radius: 20px; border-radius: 20px;
cursor: pointer;
}
.browser-url-bar:hover {
background-color: #3D3D3D;
} }
.browser-secure-icon { .browser-secure-icon {
@@ -145,12 +156,14 @@
.browser-tab-btn:hover { .browser-tab-btn:hover {
color: #FFFFFF; color: #FFFFFF;
background-color: rgba(255, 255, 255, 0.05);
} }
.browser-tab-btn img { .browser-tab-btn img {
width: 28px; width: 28px;
height: 28px; height: 28px;
margin-bottom: 4px; margin-bottom: 4px;
pointer-events: none;
} }
.browser-tab-btn span { .browser-tab-btn span {
@@ -166,7 +179,7 @@
} }
</style> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)" data-model="browser"> <body class="app-screen" onload="initLayout(document); initBrowser(document)" data-model="browser">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -179,66 +192,52 @@
<!-- Browser Toolbar --> <!-- Browser Toolbar -->
<div class="browser-toolbar"> <div class="browser-toolbar">
<div class="browser-nav-btn" onclick="goBack()"> <div class="browser-nav-btn" onclick="browserBack()">
<img src="../../icons/back.tga"/> <img src="../../icons/back.tga"/>
</div> </div>
<div class="browser-nav-btn disabled"> <div class="browser-nav-btn" onclick="browserForward()">
<img src="../../icons/forward.tga"/> <img src="../../icons/forward.tga"/>
</div> </div>
<div class="browser-url-bar"> <div class="browser-url-bar">
<span class="browser-secure-icon">S</span> <span class="browser-secure-icon" id="secure-icon">S</span>
<input class="browser-url" type="text" value="example.com"/> <input class="browser-url" type="text" value="example.com" id="url-input" onchange="onUrlSubmit()"/>
</div> </div>
<div class="browser-nav-btn"> <div class="browser-nav-btn" onclick="browserRefresh()">
<img src="../../icons/refresh.tga"/> <img src="../../icons/refresh.tga"/>
</div> </div>
<div class="browser-nav-btn"> <div class="browser-nav-btn" onclick="showBrowserMenu()">
<img src="../../icons/more.tga"/> <img src="../../icons/more.tga"/>
</div> </div>
</div> </div>
<!-- Browser Content --> <!-- Browser Content -->
<div class="browser-content"> <div class="browser-content" id="browser-content">
<div class="browser-page"> <div class="browser-page">
<div class="browser-page-title">Example Domain</div> <div class="browser-page-title" id="page-title">Example Domain</div>
<div class="browser-page-text"> <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. 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>
<div class="browser-page-text"> <div class="browser-page-text">
<span class="browser-page-link">More information...</span> <span class="browser-page-link" onclick="navigateToUrl('iana.org')">More information...</span>
</div>
<div style="margin-top: 32px; padding-top: 16px; border-top: 1px solid #e0e0e0;">
<div class="browser-page-title" style="font-size: 18px;">Related Links</div>
<div class="browser-search-item">
<div class="browser-search-title">IANA - IANA-managed Reserved Domains</div>
<div class="browser-search-url">www.iana.org > domains > reserved</div>
<div class="browser-search-desc">Certain domains are set aside and unavailable for registration.</div>
</div>
<div class="browser-search-item">
<div class="browser-search-title">RFC 2606 - Reserved Top Level DNS Names</div>
<div class="browser-search-url">tools.ietf.org > html > rfc2606</div>
<div class="browser-search-desc">This document describes domain names reserved for documentation.</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bottom Bar --> <!-- Bottom Bar -->
<div class="browser-bottom-bar"> <div class="browser-bottom-bar">
<div class="browser-tab-btn" onclick="goHome()"> <div class="browser-tab-btn" onclick="browserHome()">
<img src="../../icons/home.tga"/> <img src="../../icons/home.tga"/>
<span>Home</span> <span>Home</span>
</div> </div>
<div class="browser-tab-btn"> <div class="browser-tab-btn" onclick="showTabs()">
<span class="browser-tabs-indicator">1</span> <span class="browser-tabs-indicator" id="tab-count">1</span>
<span>Tabs</span> <span>Tabs</span>
</div> </div>
<div class="browser-tab-btn"> <div class="browser-tab-btn" onclick="newTab()">
<img src="../../icons/add.tga"/> <img src="../../icons/add.tga"/>
<span>New Tab</span> <span>New Tab</span>
</div> </div>
<div class="browser-tab-btn"> <div class="browser-tab-btn" onclick="showBrowserMenu()">
<img src="../../icons/menu.tga"/> <img src="../../icons/menu.tga"/>
<span>Menu</span> <span>Menu</span>
</div> </div>

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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="camera.lua"></script>
<title>Camera</title> <title>Camera</title>
<style> <style>
.camera-screen { .camera-screen {
@@ -40,9 +41,14 @@
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
.camera-btn:active {
background-color: rgba(255, 255, 255, 0.3);
}
.camera-btn img { .camera-btn img {
width: 28px; width: 28px;
height: 28px; height: 28px;
pointer-events: none;
} }
/* Viewfinder */ /* Viewfinder */
@@ -169,10 +175,15 @@
justify-content: center; justify-content: center;
} }
.gallery-preview:hover {
background-color: #444444;
}
.gallery-preview img { .gallery-preview img {
width: 28px; width: 28px;
height: 28px; height: 28px;
opacity: 0.7; opacity: 0.7;
pointer-events: none;
} }
.capture-btn { .capture-btn {
@@ -203,6 +214,20 @@
background-color: #FFFFFF; background-color: #FFFFFF;
} }
.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;
}
.switch-camera-btn { .switch-camera-btn {
width: 48px; width: 48px;
height: 48px; height: 48px;
@@ -221,13 +246,12 @@
.switch-camera-btn img { .switch-camera-btn img {
width: 28px; width: 28px;
height: 28px; height: 28px;
pointer-events: none;
} }
/* Indicators */ /* Indicators */
.flash-indicator { .indicator {
position: absolute; position: absolute;
top: 100px;
left: 16px;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
padding: 6px 12px; padding: 6px 12px;
border-radius: 12px; border-radius: 12px;
@@ -235,15 +259,30 @@
color: #FFFFFF; color: #FFFFFF;
} }
.flash-indicator {
top: 100px;
left: 16px;
}
.timer-indicator { .timer-indicator {
position: absolute;
top: 100px; top: 100px;
right: 16px; right: 16px;
background-color: rgba(0, 0, 0, 0.4); }
padding: 6px 12px;
border-radius: 12px; .recording-indicator {
font-size: 14px; top: 140px;
color: #FFFFFF; left: 50%;
transform: translateX(-50%);
display: none;
align-items: center;
gap: 8px;
}
.recording-dot {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: #F44336;
} }
/* Zoom control */ /* Zoom control */
@@ -270,6 +309,10 @@
cursor: pointer; cursor: pointer;
} }
.zoom-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.zoom-level { .zoom-level {
font-size: 16px; font-size: 16px;
color: #FFD700; color: #FFD700;
@@ -279,7 +322,7 @@
} }
</style> </style>
</head> </head>
<body class="app-screen camera-screen" onload="initLayout(document)"> <body class="app-screen camera-screen" onload="initLayout(document); initCamera(document)">
<!-- System Status Bar (transparent) --> <!-- System Status Bar (transparent) -->
<div class="system-status-bar" style="background-color: transparent; position: absolute; top: 0; left: 0; right: 0; z-index: 20;"> <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> <span id="status-time" class="system-status-time">12:30</span>
@@ -296,13 +339,13 @@
<img src="../../icons/close.tga"/> <img src="../../icons/close.tga"/>
</div> </div>
<div style="display: flex; gap: 12px;"> <div style="display: flex; gap: 12px;">
<div class="camera-btn"> <div class="camera-btn" onclick="toggleFlash()">
<img src="../../icons/flash.tga"/> <img src="../../icons/flash.tga"/>
</div> </div>
<div class="camera-btn"> <div class="camera-btn" onclick="toggleTimer()">
<img src="../../icons/timer.tga"/> <img src="../../icons/timer.tga"/>
</div> </div>
<div class="camera-btn"> <div class="camera-btn" onclick="openCameraSettings()">
<img src="../../icons/settings.tga"/> <img src="../../icons/settings.tga"/>
</div> </div>
</div> </div>
@@ -311,7 +354,7 @@
<!-- Viewfinder Area --> <!-- Viewfinder Area -->
<div class="viewfinder-container"> <div class="viewfinder-container">
<div class="viewfinder" id="camera-viewfinder"> <div class="viewfinder" id="camera-viewfinder">
<div class="viewfinder-placeholder"> <div class="viewfinder-placeholder" id="viewfinder-placeholder">
<div class="viewfinder-placeholder-icon">C</div> <div class="viewfinder-placeholder-icon">C</div>
<div>Camera Preview</div> <div>Camera Preview</div>
<div style="font-size: 14px; margin-top: 8px; color: #555555;"> <div style="font-size: 14px; margin-top: 8px; color: #555555;">
@@ -328,39 +371,43 @@
</div> </div>
<!-- Focus Indicator --> <!-- Focus Indicator -->
<div class="focus-indicator"></div> <div class="focus-indicator" id="focus-indicator"></div>
</div> </div>
<!-- Indicators --> <!-- Indicators -->
<div class="flash-indicator">Flash: Auto</div> <div class="indicator flash-indicator" id="flash-indicator">Flash: Auto</div>
<div class="timer-indicator">Timer: Off</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 --> <!-- Zoom Control -->
<div class="zoom-control"> <div class="zoom-control">
<div class="zoom-btn">-</div> <div class="zoom-btn" onclick="zoomOut()">-</div>
<span class="zoom-level">1.0x</span> <span class="zoom-level" id="zoom-level">1.0x</span>
<div class="zoom-btn">+</div> <div class="zoom-btn" onclick="zoomIn()">+</div>
</div> </div>
</div> </div>
<!-- Camera Modes --> <!-- Camera Modes -->
<div class="camera-modes"> <div class="camera-modes">
<span class="camera-mode">Night</span> <span id="mode-night" class="camera-mode" onclick="switchMode('Night')">Night</span>
<span class="camera-mode">Portrait</span> <span id="mode-portrait" class="camera-mode" onclick="switchMode('Portrait')">Portrait</span>
<span class="camera-mode active">Photo</span> <span id="mode-photo" class="camera-mode active" onclick="switchMode('Photo')">Photo</span>
<span class="camera-mode">Video</span> <span id="mode-video" class="camera-mode" onclick="switchMode('Video')">Video</span>
<span class="camera-mode">More</span> <span id="mode-more" class="camera-mode" onclick="switchMode('More')">More</span>
</div> </div>
<!-- Bottom Bar --> <!-- Bottom Bar -->
<div class="camera-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"/> <img src="../../icons/gallery.tga"/>
</div> </div>
<div class="capture-btn" id="capture-button"> <div class="capture-btn" id="capture-button" onclick="capture()">
<div class="capture-btn-inner"></div> <div class="capture-btn-inner"></div>
</div> </div>
<div class="switch-camera-btn"> <div class="switch-camera-btn" onclick="switchCamera()">
<img src="../../icons/switch-camera.tga"/> <img src="../../icons/switch-camera.tga"/>
</div> </div>
</div> </div>

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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="contacts.lua"></script>
<title>Contacts</title> <title>Contacts</title>
<style> <style>
.contacts-list { .contacts-list {
@@ -89,6 +90,123 @@
pointer-events: none; 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 app bottom tabs */
.phone-tabs { .phone-tabs {
height: 72px; height: 72px;
@@ -123,9 +241,17 @@
.phone-tab span { .phone-tab span {
font-size: 14px; font-size: 14px;
} }
/* Search style adjustments */
.search-input {
flex: 1;
background-color: transparent;
font-size: 18px;
color: #FFFFFF;
}
</style> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)" data-model="contacts"> <body class="app-screen" onload="initLayout(document); initContacts(document)" data-model="contacts">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -143,162 +269,67 @@
</div> </div>
<span class="app-bar-title">Contacts</span> <span class="app-bar-title">Contacts</span>
<div class="app-bar-actions"> <div class="app-bar-actions">
<div class="app-bar-action"> <div class="app-bar-action" onclick="addContact()">
<img src="../../icons/search.tga"/>
</div>
<div class="app-bar-action">
<img src="../../icons/add.tga"/> <img src="../../icons/add.tga"/>
</div> </div>
</div> </div>
</div> </div>
<!-- Contacts List Container -->
<div id="contacts-list-container" class="app-content" style="display: flex; flex-direction: column;">
<!-- Search Bar --> <!-- Search Bar -->
<div class="search-bar"> <div class="search-bar">
<img src="../../icons/search.tga" class="search-icon" style="width: 24px; height: 24px;"/> <img src="../../icons/search.tga" class="search-icon" style="width: 24px; height: 24px;"/>
<input class="search-input" type="text" placeholder="Search contacts"/> <input class="search-input" type="text" placeholder="Search contacts" onchange="onSearchInput(this)"/>
</div> </div>
<!-- Contacts List --> <!-- Contacts List -->
<div class="app-content"> <div class="contacts-list" id="contacts-list">
<div class="contacts-list"> <!-- Populated by Lua -->
<!-- A -->
<div class="contact-letter">A</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #E91E63;">A</div>
<div class="contact-info">
<div class="contact-name">Alice Johnson</div>
<div class="contact-phone">+1 555-0101</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #9C27B0;">A</div>
<div class="contact-info">
<div class="contact-name">Andrew Smith</div>
<div class="contact-phone">+1 555-0102</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div> </div>
</div> </div>
<!-- B --> <!-- Contact Detail View -->
<div class="contact-letter">B</div> <div id="contact-detail">
<div class="contact-item"> <div class="detail-header">
<div class="contact-avatar" style="background-color: #2196F3;">B</div> <div class="detail-avatar" id="detail-avatar">A</div>
<div class="contact-info"> <div class="detail-name" id="detail-name">Contact Name</div>
<div class="contact-name">Bob Williams</div>
<div class="contact-phone">+1 555-0201</div>
</div> </div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/> <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> </div>
<!-- C --> <div class="detail-info" id="detail-info">
<div class="contact-letter">C</div> <!-- Populated by Lua -->
<div class="contact-item">
<div class="contact-avatar" style="background-color: #4CAF50;">C</div>
<div class="contact-info">
<div class="contact-name">Carol Davis</div>
<div class="contact-phone">+1 555-0301</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #FF9800;">C</div>
<div class="contact-info">
<div class="contact-name">Chris Miller</div>
<div class="contact-phone">+1 555-0302</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div> </div>
<!-- D --> <div style="padding: 16px;">
<div class="contact-letter">D</div> <div class="btn btn-outlined" style="width: 100%; text-align: center;" onclick="hideContactDetail()">
<div class="contact-item"> Back to Contacts
<div class="contact-avatar" style="background-color: #F44336;">D</div>
<div class="contact-info">
<div class="contact-name">David Brown</div>
<div class="contact-phone">+1 555-0401</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<!-- E -->
<div class="contact-letter">E</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #00BCD4;">E</div>
<div class="contact-info">
<div class="contact-name">Emma Wilson</div>
<div class="contact-phone">+1 555-0501</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<!-- J -->
<div class="contact-letter">J</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #673AB7;">J</div>
<div class="contact-info">
<div class="contact-name">John Doe</div>
<div class="contact-phone">+1 555-1234</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<!-- M -->
<div class="contact-letter">M</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #3F51B5;">M</div>
<div class="contact-info">
<div class="contact-name">Mary Taylor</div>
<div class="contact-phone">+1 555-0601</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #009688;">M</div>
<div class="contact-info">
<div class="contact-name">Michael Lee</div>
<div class="contact-phone">+1 555-0602</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div>
<!-- S -->
<div class="contact-letter">S</div>
<div class="contact-item">
<div class="contact-avatar" style="background-color: #795548;">S</div>
<div class="contact-info">
<div class="contact-name">Sarah Anderson</div>
<div class="contact-phone">+1 555-0701</div>
</div>
<div class="contact-call-btn">
<img src="../../icons/phone.tga"/>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- FAB --> <!-- FAB -->
<div class="btn-fab"> <div class="btn-fab" onclick="addContact()">
<img src="../../icons/add.tga" style="width: 32px; height: 32px;"/> <img src="../../icons/add.tga" style="width: 32px; height: 32px;"/>
</div> </div>
@@ -308,7 +339,7 @@
<img src="../../icons/dialpad.tga"/> <img src="../../icons/dialpad.tga"/>
<span>Keypad</span> <span>Keypad</span>
</div> </div>
<div class="phone-tab"> <div class="phone-tab" onclick="switchTab('recent')">
<img src="../../icons/history.tga"/> <img src="../../icons/history.tga"/>
<span>Recent</span> <span>Recent</span>
</div> </div>

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

@@ -4,45 +4,30 @@
<link type="text/rcss" href="../../ui/theme.rcss"/> <link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/> <link type="text/rcss" href="../../ui/components.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/phone.lua"></script> <script src="calling.lua"></script>
<title>Calling</title> <title>Calling</title>
<style> <style>
.calling-screen { .calling-screen {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(180deg, #1a237e 0%, #121212 100%); background: linear-gradient(180deg, #1a237e 0%, #000000 100%);
background-color: #1a237e;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.calling-status { .calling-header {
margin-top: 80px; padding-top: 60px;
font-size: 18px; text-align: center;
color: #4CAF50;
text-transform: uppercase;
letter-spacing: 2px;
} }
.calling-name { .call-avatar {
margin-top: 24px;
font-size: 32px;
font-weight: 300;
color: #FFFFFF;
}
.calling-number {
margin-top: 8px;
font-size: 16px;
color: #B3B3B3;
}
.calling-avatar {
margin-top: 48px;
width: 120px; width: 120px;
height: 120px; height: 120px;
border-radius: 60px; border-radius: 60px;
background-color: #BB86FC; background-color: #BB86FC;
margin: 0 auto 24px auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -50,77 +35,157 @@
color: #000000; color: #000000;
} }
.calling-actions { .call-name {
position: absolute; font-size: 32px;
bottom: 120px; font-weight: 500;
display: flex; color: #FFFFFF;
gap: 48px; margin-bottom: 8px;
} }
.call-action-btn { .call-number {
width: 64px; font-size: 18px;
height: 64px; color: #B3B3B3;
border-radius: 32px; margin-bottom: 16px;
background-color: rgba(255, 255, 255, 0.1); }
.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; 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; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
} }
.call-action-btn:hover { .call-control-btn:hover {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.25);
} }
.call-action-btn img { .call-control-btn.active {
background-color: #FFFFFF;
}
.call-control-btn img {
width: 32px; width: 32px;
height: 32px; height: 32px;
pointer-events: none; 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 { .end-call-btn {
position: absolute; width: 80px;
bottom: 40px; height: 80px;
width: 72px; border-radius: 40px;
height: 72px;
border-radius: 36px;
background-color: #F44336; background-color: #F44336;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
margin: 0 auto;
} }
.end-call-btn:hover { .end-call-btn:hover {
background-color: #E53935; background-color: #E53935;
transform: scale(1.05);
}
.end-call-btn:active {
background-color: #C62828;
transform: scale(0.95);
} }
.end-call-btn img { .end-call-btn img {
width: 32px; width: 40px;
height: 32px; height: 40px;
pointer-events: none; pointer-events: none;
transform: rotate(135deg);
} }
</style> </style>
</head> </head>
<body class="calling-screen"> <body class="calling-screen" onload="initCalling(document)">
<div class="calling-status">Calling...</div> <!-- Caller Info -->
<div class="calling-name" id="calling-name">Unknown</div> <div class="calling-header">
<div class="calling-number" id="calling-number"></div> <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>
<div class="calling-avatar" id="calling-avatar">?</div> <!-- Spacer -->
<div class="calling-content"></div>
<div class="calling-actions"> <!-- Call Controls -->
<div class="call-action-btn"> <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"/> <img src="../../icons/dialpad.tga"/>
<span class="call-control-label">Keypad</span>
</div> </div>
<div class="call-action-btn"> <div id="btn-speaker" class="call-control-btn" onclick="toggleSpeaker()">
<img src="../../icons/contacts.tga"/> <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>
</div> </div>
<!-- End Call Button -->
<div class="end-call-container">
<div class="end-call-btn" onclick="endCall()"> <div class="end-call-btn" onclick="endCall()">
<img src="../../icons/phone.tga"/> <img src="../../icons/call_end.tga"/>
</div>
</div> </div>
</body> </body>
</rml> </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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="dialer.lua"></script>
<title>Phone</title> <title>Phone</title>
<style> <style>
.dialer-content { .dialer-content {
@@ -14,6 +15,61 @@
flex-direction: column; 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 app bottom tabs */
.phone-tabs { .phone-tabs {
height: 72px; height: 72px;
@@ -50,7 +106,7 @@
} }
</style> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)" data-model="phone"> <body class="app-screen" onload="initLayout(document); initDialer(document)">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -71,56 +127,60 @@
<!-- Dialer Content --> <!-- Dialer Content -->
<div class="dialer-content"> <div class="dialer-content">
<!-- Keypad View -->
<div id="keypad-content">
<!-- Dial Display --> <!-- Dial Display -->
<div class="dial-display">{{ dial_number }}</div> <div class="dial-display" id="dial-display">
<span style="color: #666666;">Enter number</span>
</div>
<!-- Dial Pad --> <!-- Dial Pad -->
<div class="dial-pad"> <div class="dial-pad">
<div class="dial-key" data-event-click="dial_press('1')"> <div class="dial-key" onclick="dial_press('1')">
<span class="dial-key-number">1</span> <span class="dial-key-number">1</span>
<span class="dial-key-letters"></span> <span class="dial-key-letters"></span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('2')"> <div class="dial-key" onclick="dial_press('2')">
<span class="dial-key-number">2</span> <span class="dial-key-number">2</span>
<span class="dial-key-letters">ABC</span> <span class="dial-key-letters">ABC</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('3')"> <div class="dial-key" onclick="dial_press('3')">
<span class="dial-key-number">3</span> <span class="dial-key-number">3</span>
<span class="dial-key-letters">DEF</span> <span class="dial-key-letters">DEF</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('4')"> <div class="dial-key" onclick="dial_press('4')">
<span class="dial-key-number">4</span> <span class="dial-key-number">4</span>
<span class="dial-key-letters">GHI</span> <span class="dial-key-letters">GHI</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('5')"> <div class="dial-key" onclick="dial_press('5')">
<span class="dial-key-number">5</span> <span class="dial-key-number">5</span>
<span class="dial-key-letters">JKL</span> <span class="dial-key-letters">JKL</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('6')"> <div class="dial-key" onclick="dial_press('6')">
<span class="dial-key-number">6</span> <span class="dial-key-number">6</span>
<span class="dial-key-letters">MNO</span> <span class="dial-key-letters">MNO</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('7')"> <div class="dial-key" onclick="dial_press('7')">
<span class="dial-key-number">7</span> <span class="dial-key-number">7</span>
<span class="dial-key-letters">PQRS</span> <span class="dial-key-letters">PQRS</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('8')"> <div class="dial-key" onclick="dial_press('8')">
<span class="dial-key-number">8</span> <span class="dial-key-number">8</span>
<span class="dial-key-letters">TUV</span> <span class="dial-key-letters">TUV</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('9')"> <div class="dial-key" onclick="dial_press('9')">
<span class="dial-key-number">9</span> <span class="dial-key-number">9</span>
<span class="dial-key-letters">WXYZ</span> <span class="dial-key-letters">WXYZ</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('*')"> <div class="dial-key" onclick="dial_press('*')">
<span class="dial-key-number">*</span> <span class="dial-key-number">*</span>
<span class="dial-key-letters"></span> <span class="dial-key-letters"></span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('0')"> <div class="dial-key" onclick="dial_press('0')">
<span class="dial-key-number">0</span> <span class="dial-key-number">0</span>
<span class="dial-key-letters">+</span> <span class="dial-key-letters">+</span>
</div> </div>
<div class="dial-key" data-event-click="dial_press('#')"> <div class="dial-key" onclick="dial_press('#')">
<span class="dial-key-number">#</span> <span class="dial-key-number">#</span>
<span class="dial-key-letters"></span> <span class="dial-key-letters"></span>
</div> </div>
@@ -129,26 +189,34 @@
<!-- Call Actions --> <!-- Call Actions -->
<div class="dial-actions"> <div class="dial-actions">
<div style="width: 56px;"></div> <div style="width: 56px;"></div>
<div class="dial-call-btn" data-event-click="make_call()"> <div class="dial-call-btn" onclick="make_call()">
<img src="../../icons/call_small.tga" style="width: 32px; height: 32px; pointer-events: none;"/> <img src="../../icons/call_small.tga" style="width: 32px; height: 32px; pointer-events: none;"/>
</div> </div>
<div class="btn-icon" data-event-click="dial_backspace()" style="width: 56px; height: 56px;"> <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;"/> <img src="../../icons/backspace.tga" style="width: 32px; height: 32px; pointer-events: none;"/>
</div> </div>
</div> </div>
</div> </div>
<!-- Recent Calls View -->
<div id="recent-content">
<div id="recent-list">
<!-- Populated by Lua -->
</div>
</div>
</div>
<!-- Phone App Bottom Tabs --> <!-- Phone App Bottom Tabs -->
<div class="phone-tabs"> <div class="phone-tabs">
<div class="phone-tab active"> <div id="tab-keypad" class="phone-tab active" onclick="switchTab('keypad')">
<img src="../../icons/dialpad.tga"/> <img src="../../icons/dialpad.tga"/>
<span>Keypad</span> <span>Keypad</span>
</div> </div>
<div class="phone-tab"> <div id="tab-recent" class="phone-tab" onclick="switchTab('recent')">
<img src="../../icons/history.tga"/> <img src="../../icons/history.tga"/>
<span>Recent</span> <span>Recent</span>
</div> </div>
<div class="phone-tab" onclick="navigateTo('contacts')"> <div id="tab-contacts" class="phone-tab" onclick="navigateTo('contacts')">
<img src="../../icons/contacts.tga"/> <img src="../../icons/contacts.tga"/>
<span>Contacts</span> <span>Contacts</span>
</div> </div>

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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="messages.lua"></script>
<title>Messages</title> <title>Messages</title>
<style> <style>
.conversations-list { .conversations-list {
@@ -84,9 +85,131 @@
justify-content: center; justify-content: center;
margin-left: 8px; 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> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)" data-model="messages"> <body class="app-screen" onload="initLayout(document); initMessages(document)" data-model="messages">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -97,6 +220,8 @@
</div> </div>
</div> </div>
<!-- Conversations List Container -->
<div id="conversations-container" style="display: flex; flex-direction: column; flex: 1;">
<!-- App Bar --> <!-- App Bar -->
<div class="app-bar"> <div class="app-bar">
<div class="app-bar-back" onclick="goBack()"> <div class="app-bar-back" onclick="goBack()">
@@ -112,97 +237,13 @@
<!-- Conversations List --> <!-- Conversations List -->
<div class="app-content with-nav"> <div class="app-content with-nav">
<div class="conversations-list"> <div class="conversations-list" id="conversations-list">
<!-- Alice --> <!-- Populated by Lua -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #E91E63;">A</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Alice Johnson</span>
<span class="conversation-time">2:34 PM</span>
</div>
<div class="conversation-preview">Hey! Are you coming to the party tonight?</div>
</div>
<div class="conversation-unread">2</div>
</div>
<!-- Bob -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #2196F3;">B</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Bob Williams</span>
<span class="conversation-time">1:15 PM</span>
</div>
<div class="conversation-preview">Thanks for the help yesterday!</div>
</div>
</div>
<!-- Carol -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #4CAF50;">C</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Carol Davis</span>
<span class="conversation-time">Yesterday</span>
</div>
<div class="conversation-preview">The meeting has been rescheduled to Friday</div>
</div>
</div>
<!-- David -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #FF9800;">D</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">David Brown</span>
<span class="conversation-time">Yesterday</span>
</div>
<div class="conversation-preview">Can you send me the files?</div>
</div>
<div class="conversation-unread">1</div>
</div>
<!-- Emma -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #9C27B0;">E</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Emma Wilson</span>
<span class="conversation-time">Mon</span>
</div>
<div class="conversation-preview">See you at the coffee shop!</div>
</div>
</div>
<!-- Frank -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #00BCD4;">F</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Frank Miller</span>
<span class="conversation-time">Sun</span>
</div>
<div class="conversation-preview">Great game last night!</div>
</div>
</div>
<!-- Grace -->
<div class="conversation-item">
<div class="conversation-avatar" style="background-color: #673AB7;">G</div>
<div class="conversation-content">
<div class="conversation-header">
<span class="conversation-name">Grace Lee</span>
<span class="conversation-time">Sat</span>
</div>
<div class="conversation-preview">Happy birthday! 🎂</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- FAB --> <!-- FAB -->
<div class="btn-fab"> <div class="btn-fab" onclick="newConversation()">
<img src="../../icons/add.tga" style="width: 32px; height: 32px;"/> <img src="../../icons/add.tga" style="width: 32px; height: 32px;"/>
</div> </div>
@@ -216,5 +257,43 @@
<img src="../../icons/menu.tga"/> <img src="../../icons/menu.tga"/>
</div> </div>
</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> </body>
</rml> </rml>

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

@@ -6,20 +6,30 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="music.lua"></script>
<title>Music</title> <title>Music</title>
<style> <style>
.music-content { .music-content {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding-bottom: 140px;
} }
.mini-player { .mini-player {
position: absolute;
bottom: 56px;
left: 0;
right: 0;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 16px; padding: 8px 16px;
background-color: #282828; background-color: #282828;
border-top-width: 1px; border-top: 1px solid #333333;
border-top-color: #333333; cursor: pointer;
}
.mini-player:hover {
background-color: #333333;
} }
.mini-player-art { .mini-player-art {
@@ -68,6 +78,7 @@
.mini-control-btn img { .mini-control-btn img {
width: 28px; width: 28px;
height: 28px; height: 28px;
pointer-events: none;
} }
.section-header { .section-header {
@@ -104,10 +115,12 @@
.recent-item { .recent-item {
min-width: 120px; min-width: 120px;
cursor: pointer; cursor: pointer;
padding: 8px;
border-radius: 8px;
} }
.recent-item:hover { .recent-item:hover {
opacity: 0.9; background-color: rgba(255, 255, 255, 0.05);
} }
.recent-art { .recent-art {
@@ -214,11 +227,14 @@
} }
.music-bottom-nav { .music-bottom-nav {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex; display: flex;
height: 56px; height: 56px;
background-color: #1E1E1E; background-color: #1E1E1E;
border-top-width: 1px; border-top: 1px solid #282828;
border-top-color: #282828;
} }
.nav-item { .nav-item {
@@ -243,12 +259,29 @@
width: 28px; width: 28px;
height: 28px; height: 28px;
margin-bottom: 4px; margin-bottom: 4px;
pointer-events: none;
} }
.nav-item span { .nav-item span {
font-size: 14px; font-size: 14px;
} }
/* 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-1 { background-color: #667eea; }
.bg-gradient-2 { background-color: #f093fb; } .bg-gradient-2 { background-color: #f093fb; }
.bg-gradient-3 { background-color: #4facfe; } .bg-gradient-3 { background-color: #4facfe; }
@@ -260,7 +293,7 @@
.bg-solid-blue { background-color: #2563eb; } .bg-solid-blue { background-color: #2563eb; }
</style> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)"> <body class="app-screen" onload="initLayout(document); initMusic(document)">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -278,7 +311,7 @@
</div> </div>
<span class="app-bar-title">Music</span> <span class="app-bar-title">Music</span>
<div class="app-bar-actions"> <div class="app-bar-actions">
<div class="app-bar-action"> <div class="app-bar-action" onclick="openSearch()">
<img src="../../icons/search.tga"/> <img src="../../icons/search.tga"/>
</div> </div>
</div> </div>
@@ -292,28 +325,28 @@
</div> </div>
<!-- Quick Access Grid --> <!-- Quick Access Grid -->
<div class="quick-access"> <div class="quick-access" id="quick-access">
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('liked')">
<div class="quick-card-art bg-solid-red">L</div> <div class="quick-card-art bg-solid-red">L</div>
<span class="quick-card-title">Liked Songs</span> <span class="quick-card-title">Liked Songs</span>
</div> </div>
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('daily1')">
<div class="quick-card-art bg-gradient-1">D</div> <div class="quick-card-art bg-gradient-1">D</div>
<span class="quick-card-title">Daily Mix 1</span> <span class="quick-card-title">Daily Mix 1</span>
</div> </div>
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('release')">
<div class="quick-card-art bg-solid-green">R</div> <div class="quick-card-art bg-solid-green">R</div>
<span class="quick-card-title">Release Radar</span> <span class="quick-card-title">Release Radar</span>
</div> </div>
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('chill')">
<div class="quick-card-art bg-gradient-2">C</div> <div class="quick-card-art bg-gradient-2">C</div>
<span class="quick-card-title">Chill Vibes</span> <span class="quick-card-title">Chill Vibes</span>
</div> </div>
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('workout')">
<div class="quick-card-art bg-solid-blue">W</div> <div class="quick-card-art bg-solid-blue">W</div>
<span class="quick-card-title">Workout Mix</span> <span class="quick-card-title">Workout Mix</span>
</div> </div>
<div class="quick-card"> <div class="quick-card" onclick="openPlaylist('focus')">
<div class="quick-card-art bg-gradient-3">F</div> <div class="quick-card-art bg-gradient-3">F</div>
<span class="quick-card-title">Focus Flow</span> <span class="quick-card-title">Focus Flow</span>
</div> </div>
@@ -325,23 +358,23 @@
<span class="section-action">SEE ALL</span> <span class="section-action">SEE ALL</span>
</div> </div>
<div class="recent-row"> <div class="recent-row" id="recent-row">
<div class="recent-item"> <div class="recent-item" onclick="openPlaylist('pop')">
<div class="recent-art bg-gradient-4">P</div> <div class="recent-art bg-gradient-4">P</div>
<div class="recent-title">Pop Hits</div> <div class="recent-title">Pop Hits</div>
<div class="recent-subtitle">Playlist</div> <div class="recent-subtitle">Playlist</div>
</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-art bg-gradient-5">E</div>
<div class="recent-title">Electronic</div> <div class="recent-title">Electronic</div>
<div class="recent-subtitle">Playlist</div> <div class="recent-subtitle">Playlist</div>
</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-art bg-gradient-1">J</div>
<div class="recent-title">Jazz Classics</div> <div class="recent-title">Jazz Classics</div>
<div class="recent-subtitle">Playlist</div> <div class="recent-subtitle">Playlist</div>
</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-art bg-gradient-2">R</div>
<div class="recent-title">Rock Legends</div> <div class="recent-title">Rock Legends</div>
<div class="recent-subtitle">Playlist</div> <div class="recent-subtitle">Playlist</div>
@@ -354,7 +387,7 @@
<span class="section-action">SEE ALL</span> <span class="section-action">SEE ALL</span>
</div> </div>
<div class="playlist-item"> <div class="playlist-item" onclick="openPlaylist('daily1')">
<div class="playlist-art bg-gradient-3">1</div> <div class="playlist-art bg-gradient-3">1</div>
<div class="playlist-info"> <div class="playlist-info">
<div class="playlist-title">Daily Mix 1</div> <div class="playlist-title">Daily Mix 1</div>
@@ -362,7 +395,7 @@
</div> </div>
</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-art bg-gradient-4">2</div>
<div class="playlist-info"> <div class="playlist-info">
<div class="playlist-title">Daily Mix 2</div> <div class="playlist-title">Daily Mix 2</div>
@@ -370,7 +403,7 @@
</div> </div>
</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-art bg-gradient-5">D</div>
<div class="playlist-info"> <div class="playlist-info">
<div class="playlist-title">Discover Weekly</div> <div class="playlist-title">Discover Weekly</div>
@@ -380,17 +413,20 @@
</div> </div>
<!-- Mini Player --> <!-- Mini Player -->
<div class="mini-player"> <div class="mini-player" onclick="openNowPlaying()">
<div class="mini-player-art">M</div> <div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="mini-player-art" id="mini-player-art">M</div>
<div class="mini-player-info"> <div class="mini-player-info">
<div class="mini-player-title">Midnight City</div> <div class="mini-player-title" id="mini-player-title">Midnight City</div>
<div class="mini-player-artist">M83</div> <div class="mini-player-artist" id="mini-player-artist">M83</div>
</div> </div>
<div style="display: flex; gap: 4px;"> <div style="display: flex; gap: 4px;">
<div class="mini-control-btn"> <div class="mini-control-btn" onclick="toggleLike(); event.stopPropagation();">
<img src="../../icons/heart.tga"/> <img src="../../icons/heart.tga"/>
</div> </div>
<div class="mini-control-btn"> <div class="mini-control-btn" id="mini-play-btn" onclick="togglePlay(); event.stopPropagation();">
<img src="../../icons/play.tga"/> <img src="../../icons/play.tga"/>
</div> </div>
</div> </div>
@@ -402,11 +438,11 @@
<img src="../../icons/home.tga"/> <img src="../../icons/home.tga"/>
<span>Home</span> <span>Home</span>
</div> </div>
<div class="nav-item"> <div class="nav-item" onclick="openSearch()">
<img src="../../icons/search.tga"/> <img src="../../icons/search.tga"/>
<span>Search</span> <span>Search</span>
</div> </div>
<div class="nav-item"> <div class="nav-item" onclick="openLibrary()">
<img src="../../icons/library.tga"/> <img src="../../icons/library.tga"/>
<span>Library</span> <span>Library</span>
</div> </div>

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

@@ -6,6 +6,7 @@
<link type="text/rcss" href="../../ui/layout.rcss"/> <link type="text/rcss" href="../../ui/layout.rcss"/>
<script src="../../scripts/navigation.lua"></script> <script src="../../scripts/navigation.lua"></script>
<script src="../../scripts/layout.lua"></script> <script src="../../scripts/layout.lua"></script>
<script src="settings.lua"></script>
<title>Settings</title> <title>Settings</title>
<style> <style>
.settings-list { .settings-list {
@@ -46,6 +47,10 @@
background-color: #252525; background-color: #252525;
} }
.settings-item:active {
background-color: #2A2A2A;
}
.settings-item + .settings-item { .settings-item + .settings-item {
border-top: 1px #333333; border-top: 1px #333333;
} }
@@ -63,6 +68,7 @@
width: 32px; width: 32px;
height: 32px; height: 32px;
opacity: 0.7; opacity: 0.7;
pointer-events: none;
} }
.settings-content { .settings-content {
@@ -95,6 +101,10 @@
position: relative; position: relative;
} }
.settings-toggle:hover {
background-color: #777777;
}
.settings-toggle.active { .settings-toggle.active {
background-color: rgba(187, 134, 252, 0.5); background-color: rgba(187, 134, 252, 0.5);
} }
@@ -160,7 +170,7 @@
} }
</style> </style>
</head> </head>
<body class="app-screen" onload="initLayout(document)"> <body class="app-screen" onload="initLayout(document); initSettings(document)">
<!-- System Status Bar --> <!-- System Status Bar -->
<div class="system-status-bar"> <div class="system-status-bar">
<span id="status-time" class="system-status-time">12:30</span> <span id="status-time" class="system-status-time">12:30</span>
@@ -188,7 +198,7 @@
<div class="app-content with-nav"> <div class="app-content with-nav">
<div class="settings-list"> <div class="settings-list">
<!-- User Card --> <!-- User Card -->
<div class="user-card"> <div class="user-card" onclick="openUserProfile()">
<div class="user-avatar">U</div> <div class="user-avatar">U</div>
<div class="user-info"> <div class="user-info">
<div class="user-name">User</div> <div class="user-name">User</div>
@@ -200,38 +210,38 @@
<!-- Network Section --> <!-- Network Section -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-header">Network</div> <div class="settings-header">Network</div>
<div class="settings-item"> <div class="settings-item" onclick="openWifiSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/wifi.tga"/> <img src="../../icons/wifi.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Wi-Fi</div> <div class="settings-title">Wi-Fi</div>
<div class="settings-subtitle">Connected to MosisNetwork</div> <div class="settings-subtitle" id="subtitle-wifi">Connected to MosisNetwork</div>
</div> </div>
<div class="settings-toggle active"> <div id="toggle-wifi" class="settings-toggle active" onclick="toggleWifi(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div> <div class="settings-toggle-thumb"></div>
</div> </div>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openBluetoothSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/signal.tga"/> <img src="../../icons/bluetooth.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Bluetooth</div> <div class="settings-title">Bluetooth</div>
<div class="settings-subtitle">Off</div> <div class="settings-subtitle" id="subtitle-bluetooth">Off</div>
</div> </div>
<div class="settings-toggle"> <div id="toggle-bluetooth" class="settings-toggle" onclick="toggleBluetooth(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div> <div class="settings-toggle-thumb"></div>
</div> </div>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/signal.tga"/> <img src="../../icons/airplane.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Airplane Mode</div> <div class="settings-title">Airplane Mode</div>
</div> </div>
<div class="settings-toggle"> <div id="toggle-airplane" class="settings-toggle" onclick="toggleAirplaneMode()">
<div class="settings-toggle-thumb"></div> <div class="settings-toggle-thumb"></div>
</div> </div>
</div> </div>
@@ -240,9 +250,9 @@
<!-- Device Section --> <!-- Device Section -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-header">Device</div> <div class="settings-header">Device</div>
<div class="settings-item"> <div class="settings-item" onclick="openDisplaySettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/settings.tga"/> <img src="../../icons/brightness.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Display</div> <div class="settings-title">Display</div>
@@ -250,9 +260,9 @@
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openSoundSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/music.tga"/> <img src="../../icons/volume.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Sound</div> <div class="settings-title">Sound</div>
@@ -260,9 +270,9 @@
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openNotificationsSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/message.tga"/> <img src="../../icons/notifications.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Notifications</div> <div class="settings-title">Notifications</div>
@@ -270,23 +280,23 @@
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openBatterySettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/battery.tga"/> <img src="../../icons/battery.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Battery</div> <div class="settings-title">Battery</div>
<div class="settings-subtitle">85% - 4h 30m remaining</div> <div class="settings-subtitle" id="subtitle-battery">85% - Not charging</div>
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openStorageSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/files.tga"/> <img src="../../icons/storage.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Storage</div> <div class="settings-title">Storage</div>
<div class="settings-subtitle">32 GB of 128 GB used</div> <div class="settings-subtitle" id="subtitle-storage">32 GB of 128 GB used</div>
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
@@ -295,9 +305,9 @@
<!-- Privacy Section --> <!-- Privacy Section -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-header">Privacy &amp; Security</div> <div class="settings-header">Privacy &amp; Security</div>
<div class="settings-item"> <div class="settings-item" onclick="openLockScreenSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/account.tga"/> <img src="../../icons/lock.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Lock Screen</div> <div class="settings-title">Lock Screen</div>
@@ -305,9 +315,9 @@
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openPrivacySettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/account.tga"/> <img src="../../icons/privacy.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Privacy</div> <div class="settings-title">Privacy</div>
@@ -315,15 +325,15 @@
</div> </div>
<span class="settings-action">></span> <span class="settings-action">></span>
</div> </div>
<div class="settings-item"> <div class="settings-item" onclick="openLocationSettings()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/maps.tga"/> <img src="../../icons/location.tga"/>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-title">Location</div> <div class="settings-title">Location</div>
<div class="settings-subtitle">On - High accuracy</div> <div class="settings-subtitle" id="subtitle-location">On - High accuracy</div>
</div> </div>
<div class="settings-toggle active"> <div id="toggle-location" class="settings-toggle active" onclick="toggleLocation(); event.stopPropagation();">
<div class="settings-toggle-thumb"></div> <div class="settings-toggle-thumb"></div>
</div> </div>
</div> </div>
@@ -332,7 +342,7 @@
<!-- About Section --> <!-- About Section -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-header">About</div> <div class="settings-header">About</div>
<div class="settings-item"> <div class="settings-item" onclick="openAboutPhone()">
<div class="settings-icon"> <div class="settings-icon">
<img src="../../icons/phone.tga"/> <img src="../../icons/phone.tga"/>
</div> </div>

View File

@@ -1,11 +1,138 @@
<!-- Browser App Content Fragment --> <!-- Browser App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content">
<div class="app-header"> <!-- URL bar -->
<span class="app-header-title">Browser</span> <div class="app-bar" style="padding: 8px 12px;">
<div class="btn-icon" onclick="browserBack()" style="width: 40px; height: 40px;">
<img src="../../icons/back.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="search-bar" style="flex: 1; margin: 0 8px; padding: 8px 16px;">
<span style="color: #4CAF50; margin-right: 8px;">S</span>
<input type="text" id="url-input" style="flex: 1; background-color: transparent; color: #FFFFFF; font-size: 14px;" value="mosis.app"/>
</div>
<div class="btn-icon" onclick="browserRefresh()" style="width: 40px; height: 40px;">
<img src="../../icons/refresh.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="btn-icon" onclick="showTabs()" style="width: 40px; height: 40px;">
<span style="font-size: 14px; color: #FFFFFF; font-weight: bold;">1</span>
</div>
</div>
<!-- Page content -->
<div style="flex: 1; overflow: auto; background-color: #FFFFFF; padding: 24px;">
<div style="text-align: center;">
<div style="font-size: 48px; font-weight: bold; color: #BB86FC; margin-bottom: 24px;">Mosis</div>
<div class="search-bar" style="margin-bottom: 32px; background-color: #f1f3f4;">
<input type="text" style="flex: 1; background-color: transparent; color: #333333; font-size: 16px; text-align: center;" placeholder="Search or enter URL"/>
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 16px;">
<div onclick="navigateTo('google.com')" style="cursor: pointer; width: 80px; display: flex; flex-direction: column; align-items: center;">
<div style="width: 48px; height: 48px; border-radius: 8px; background-color: #4285F4; display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 20px; margin-bottom: 8px;">G</div>
<span style="font-size: 12px; color: #666666;">Google</span>
</div>
<div onclick="navigateTo('youtube.com')" style="cursor: pointer; width: 80px; display: flex; flex-direction: column; align-items: center;">
<div style="width: 48px; height: 48px; border-radius: 8px; background-color: #FF0000; display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 20px; margin-bottom: 8px;">Y</div>
<span style="font-size: 12px; color: #666666;">YouTube</span>
</div>
<div onclick="navigateTo('github.com')" style="cursor: pointer; width: 80px; display: flex; flex-direction: column; align-items: center;">
<div style="width: 48px; height: 48px; border-radius: 8px; background-color: #333333; display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 20px; margin-bottom: 8px;">G</div>
<span style="font-size: 12px; color: #666666;">GitHub</span>
</div>
<div onclick="navigateTo('reddit.com')" style="cursor: pointer; width: 80px; display: flex; flex-direction: column; align-items: center;">
<div style="width: 48px; height: 48px; border-radius: 8px; background-color: #FF4500; display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 20px; margin-bottom: 8px;">R</div>
<span style="font-size: 12px; color: #666666;">Reddit</span>
</div>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="bottom-nav">
<div class="bottom-nav-item" onclick="browserBack()">
<img src="../../icons/back.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="bottom-nav-item" onclick="browserForward()">
<img src="../../icons/forward.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="bottom-nav-item" onclick="browserHome()">
<img src="../../icons/home.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="bottom-nav-item" onclick="addBookmark()">
<img src="../../icons/star.tga" style="width: 24px; height: 24px;"/>
</div>
<div class="bottom-nav-item" onclick="showBrowserMenu()">
<img src="../../icons/menu.tga" style="width: 24px; height: 24px;"/>
</div> </div>
<div class="app-body">
<span class="placeholder-text">Browser App</span>
</div> </div>
</div> </div>
<script>
local current_url = "mosis.app"
function navigateTo(url)
print("[Browser] Navigating to: " .. url)
current_url = url
local doc = shell_document
if doc then
local url_input = doc:GetElementById("url-input")
if url_input then
url_input.value = url
end
end
if showToast then
showToast("Loading " .. url)
end
end
function browserBack()
print("[Browser] Back")
if showToast then
showToast("Back")
end
end
function browserForward()
print("[Browser] Forward")
if showToast then
showToast("No forward history")
end
end
function browserRefresh()
print("[Browser] Refresh")
if showToast then
showToast("Page refreshed")
end
end
function browserHome()
print("[Browser] Home")
navigateTo("mosis.app")
end
function showTabs()
print("[Browser] Show tabs")
if showToast then
showToast("1 tab open")
end
end
function addBookmark()
print("[Browser] Add bookmark")
if showToast then
showToast("Bookmark added")
end
end
function showBrowserMenu()
print("[Browser] Menu")
if showToast then
showToast("Browser menu")
end
end
print("[Browser] Content loaded")
</script>

View File

@@ -1,44 +1,281 @@
<!-- Camera App Content Fragment --> <!-- Camera App Content Fragment -->
<!-- Loaded into shell's #app-container --> <!-- Uses classes from shell.rml -->
<!-- Styles are in shell.rml -->
<div class="camera-content"> <div class="camera-content">
<!-- Top Bar --> <!-- Top Bar -->
<div class="camera-top-bar"> <div class="camera-top-bar">
<div class="camera-btn" onclick="showToast('Flash: Auto')"> <div style="display: flex; gap: 12px;">
<img src="../../icons/flash.tga"/> <div class="camera-btn" id="flash-btn" onclick="toggleFlash()">
<img src="../../icons/flash.tga" style="width: 24px; height: 24px;"/>
</div> </div>
<div class="camera-btn"> <div class="camera-btn" onclick="toggleTimer()">
<img src="../../icons/timer.tga"/> <img src="../../icons/timer.tga" style="width: 24px; height: 24px;"/>
</div> </div>
<div class="camera-btn" onclick="showToast('Settings')">
<img src="../../icons/settings.tga"/>
</div> </div>
<span id="flash-indicator" style="font-size: 12px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.4); padding: 6px 12px; border-radius: 16px;">Flash: Auto</span>
<div class="camera-btn" onclick="openCameraSettings()">
<img src="../../icons/settings.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<!-- Recording Indicator -->
<div id="recording-indicator" style="position: absolute; top: 80px; left: 50%; display: none; align-items: center; gap: 8px; background-color: rgba(0, 0, 0, 0.6); padding: 8px 16px; border-radius: 20px;">
<div style="width: 12px; height: 12px; border-radius: 6px; background-color: #F44336;"></div>
<span id="recording-time" style="font-size: 14px; color: #FFFFFF; font-weight: bold;">00:00</span>
</div> </div>
<!-- Viewfinder --> <!-- Viewfinder -->
<div class="viewfinder-container"> <div class="viewfinder-container" onclick="tapToFocus()">
<div class="viewfinder"> <div class="viewfinder" id="viewfinder">
<span class="viewfinder-text">Camera Preview</span> <span style="font-size: 64px; color: #444444; margin-bottom: 16px;">C</span>
<span style="color: #666666; font-size: 18px;">Camera Preview</span>
<span style="font-size: 14px; color: #555555; margin-top: 8px;">Tap to focus</span>
</div> </div>
<div class="focus-indicator"></div> <div id="focus-indicator" style="position: absolute; width: 80px; height: 80px; border-radius: 8px; opacity: 0;"></div>
</div>
<!-- Zoom Controls -->
<div style="position: absolute; bottom: 180px; left: 50%; display: flex; gap: 16px;">
<div onclick="setZoom(0.5)" style="width: 40px; height: 40px; border-radius: 20px; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 12px; font-weight: bold; cursor: pointer;">0.5x</div>
<div id="zoom-1x" onclick="setZoom(1)" style="width: 40px; height: 40px; border-radius: 20px; background-color: #FFFFFF; display: flex; align-items: center; justify-content: center; color: #000000; font-size: 12px; font-weight: bold; cursor: pointer;">1x</div>
<div onclick="setZoom(2)" style="width: 40px; height: 40px; border-radius: 20px; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; color: #FFFFFF; font-size: 12px; font-weight: bold; cursor: pointer;">2x</div>
</div> </div>
<!-- Mode Selector --> <!-- Mode Selector -->
<div class="mode-selector"> <div class="camera-mode-selector">
<span class="mode-item">Video</span> <span class="camera-mode" id="mode-night" onclick="switchMode('night')">Night</span>
<span class="mode-item active">Photo</span> <span class="camera-mode" id="mode-portrait" onclick="switchMode('portrait')">Portrait</span>
<span class="mode-item">Portrait</span> <span class="camera-mode active" id="mode-photo" onclick="switchMode('photo')">Photo</span>
<span class="camera-mode" id="mode-video" onclick="switchMode('video')">Video</span>
</div> </div>
<!-- Bottom Controls --> <!-- Bottom Controls -->
<div class="camera-bottom-bar"> <div class="camera-bottom-bar">
<div class="gallery-preview"></div> <div class="camera-gallery-preview" onclick="openGallery()"></div>
<div class="shutter-btn" onclick="showToast('Photo captured!', 'success')"> <div class="camera-shutter-btn" id="shutter-btn" onclick="capture()">
<div class="shutter-btn-inner"></div> <div id="shutter-inner" class="camera-shutter-inner"></div>
</div> </div>
<div class="switch-camera-btn" onclick="showToast('Switched camera')"> <div class="camera-switch-btn" onclick="switchCamera()">
<img src="../../icons/switch-camera.tga"/> <img src="../../icons/switch-camera.tga" style="width: 28px; height: 28px;"/>
</div> </div>
</div> </div>
</div> </div>
<script>
local current_mode = "photo"
local flash_mode = "auto"
local timer_mode = "off"
local is_front = false
local is_recording = false
local record_time = 0
local record_timer = nil
local zoom_level = 1
local function getDoc()
return shell_document
end
function switchMode(mode)
current_mode = mode
local doc = getDoc()
if not doc then return end
local modes = {"night", "portrait", "photo", "video"}
for _, m in ipairs(modes) do
local el = doc:GetElementById("mode-" .. m)
if el then
el:SetClass("active", m == mode)
end
end
local inner = doc:GetElementById("shutter-inner")
if inner then
if mode == "video" then
inner:SetClass("camera-shutter-inner", false)
inner:SetClass("camera-shutter-video", true)
else
inner:SetClass("camera-shutter-video", false)
inner:SetClass("camera-shutter-inner", true)
end
end
print("[Camera] Mode: " .. mode)
if showToast then
showToast(mode:sub(1,1):upper() .. mode:sub(2) .. " mode")
end
end
function toggleFlash()
if flash_mode == "auto" then
flash_mode = "on"
elseif flash_mode == "on" then
flash_mode = "off"
else
flash_mode = "auto"
end
local doc = getDoc()
if doc then
local indicator = doc:GetElementById("flash-indicator")
if indicator then
indicator.inner_rml = "Flash: " .. flash_mode:sub(1,1):upper() .. flash_mode:sub(2)
end
end
print("[Camera] Flash: " .. flash_mode)
end
function toggleTimer()
if timer_mode == "off" then
timer_mode = "3s"
elseif timer_mode == "3s" then
timer_mode = "10s"
else
timer_mode = "off"
end
print("[Camera] Timer: " .. timer_mode)
if showToast then
showToast("Timer: " .. timer_mode)
end
end
function setZoom(level)
zoom_level = level
print("[Camera] Zoom: " .. level .. "x")
if showToast then
showToast(level .. "x zoom")
end
end
function switchCamera()
is_front = not is_front
local doc = getDoc()
if doc then
local vf = doc:GetElementById("viewfinder")
if vf then
if is_front then
vf.inner_rml = [[
<span style="font-size: 64px; color: #444444; margin-bottom: 16px;">F</span>
<span style="color: #666666; font-size: 18px;">Front Camera</span>
<span style="font-size: 14px; color: #555555; margin-top: 8px;">Tap to focus</span>
]]
else
vf.inner_rml = [[
<span style="font-size: 64px; color: #444444; margin-bottom: 16px;">C</span>
<span style="color: #666666; font-size: 18px;">Camera Preview</span>
<span style="font-size: 14px; color: #555555; margin-top: 8px;">Tap to focus</span>
]]
end
end
end
print("[Camera] Switched to " .. (is_front and "front" or "back") .. " camera")
if showToast then
showToast(is_front and "Front camera" or "Back camera")
end
end
function capture()
if current_mode == "video" then
if is_recording then
stopRecording()
else
startRecording()
end
else
takePhoto()
end
end
function takePhoto()
print("[Camera] Photo captured!")
if showToast then
showToast("Photo saved")
end
end
function startRecording()
is_recording = true
record_time = 0
local doc = getDoc()
if doc then
local indicator = doc:GetElementById("recording-indicator")
if indicator then
indicator.style.display = "flex"
end
local inner = doc:GetElementById("shutter-inner")
if inner then
inner:SetClass("camera-shutter-video", false)
inner:SetClass("camera-shutter-stop", true)
end
end
if setInterval then
record_timer = setInterval(function()
record_time = record_time + 1
local mins = math.floor(record_time / 60)
local secs = record_time % 60
local doc = getDoc()
if doc then
local time_el = doc:GetElementById("recording-time")
if time_el then
time_el.inner_rml = string.format("%02d:%02d", mins, secs)
end
end
end, 1000)
end
print("[Camera] Recording started")
end
function stopRecording()
is_recording = false
if record_timer and clearInterval then
clearInterval(record_timer)
record_timer = nil
end
local doc = getDoc()
if doc then
local indicator = doc:GetElementById("recording-indicator")
if indicator then
indicator.style.display = "none"
end
local inner = doc:GetElementById("shutter-inner")
if inner then
inner:SetClass("camera-shutter-stop", false)
inner:SetClass("camera-shutter-video", true)
end
end
local mins = math.floor(record_time / 60)
local secs = record_time % 60
print("[Camera] Recording stopped: " .. string.format("%02d:%02d", mins, secs))
if showToast then
showToast(string.format("Video saved (%02d:%02d)", mins, secs))
end
end
function tapToFocus()
print("[Camera] Tap to focus")
end
function openGallery()
print("[Camera] Opening gallery")
if showToast then
showToast("Gallery")
end
end
function openCameraSettings()
print("[Camera] Camera settings")
if showToast then
showToast("Camera settings")
end
end
print("[Camera] Content loaded")
</script>

View File

@@ -1,11 +1,143 @@
<!-- Contacts App Content Fragment --> <!-- Contacts App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content">
<div class="app-header"> <div class="app-bar">
<span class="app-header-title">Contacts</span> <span class="app-bar-title">Contacts</span>
</div> </div>
<div class="app-body">
<span class="placeholder-text">Contacts App</span> <div class="search-bar">
<img src="../../icons/search.tga" style="width: 24px; height: 24px; margin-right: 12px; opacity: 0.6;"/>
<span class="search-input">Search contacts</span>
</div>
<div class="list" style="flex: 1; overflow: auto;">
<div class="list-header">A</div>
<div class="list-item" onclick="openContact('Alice Johnson')">
<div class="list-item-avatar" style="background-color: #4CAF50;">A</div>
<div class="list-item-content">
<span class="list-item-title">Alice Johnson</span>
<span class="list-item-subtitle">+1 555-0101</span>
</div>
<div class="btn-icon" onclick="callContact('Alice Johnson'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">B</div>
<div class="list-item" onclick="openContact('Bob Williams')">
<div class="list-item-avatar" style="background-color: #2196F3;">B</div>
<div class="list-item-content">
<span class="list-item-title">Bob Williams</span>
<span class="list-item-subtitle">+1 555-0102</span>
</div>
<div class="btn-icon" onclick="callContact('Bob Williams'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">C</div>
<div class="list-item" onclick="openContact('Carol Davis')">
<div class="list-item-avatar" style="background-color: #9C27B0;">C</div>
<div class="list-item-content">
<span class="list-item-title">Carol Davis</span>
<span class="list-item-subtitle">+1 555-0103</span>
</div>
<div class="btn-icon" onclick="callContact('Carol Davis'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">D</div>
<div class="list-item" onclick="openContact('David Miller')">
<div class="list-item-avatar" style="background-color: #FF9800;">D</div>
<div class="list-item-content">
<span class="list-item-title">David Miller</span>
<span class="list-item-subtitle">+1 555-0104</span>
</div>
<div class="btn-icon" onclick="callContact('David Miller'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">E</div>
<div class="list-item" onclick="openContact('Emma Wilson')">
<div class="list-item-avatar" style="background-color: #E91E63;">E</div>
<div class="list-item-content">
<span class="list-item-title">Emma Wilson</span>
<span class="list-item-subtitle">+1 555-0105</span>
</div>
<div class="btn-icon" onclick="callContact('Emma Wilson'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">J</div>
<div class="list-item" onclick="openContact('John Smith')">
<div class="list-item-avatar" style="background-color: #3F51B5;">J</div>
<div class="list-item-content">
<span class="list-item-title">John Smith</span>
<span class="list-item-subtitle">+1 555-0109</span>
</div>
<div class="btn-icon" onclick="callContact('John Smith'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">M</div>
<div class="list-item" onclick="openContact('Mom')">
<div class="list-item-avatar" style="background-color: #E91E63;">M</div>
<div class="list-item-content">
<span class="list-item-title">Mom</span>
<span class="list-item-subtitle">+1 555-0110</span>
</div>
<div class="btn-icon" onclick="callContact('Mom'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
<div class="list-header">S</div>
<div class="list-item" onclick="openContact('Sarah')">
<div class="list-item-avatar" style="background-color: #9C27B0;">S</div>
<div class="list-item-content">
<span class="list-item-title">Sarah</span>
<span class="list-item-subtitle">+1 555-0111</span>
</div>
<div class="btn-icon" onclick="callContact('Sarah'); event.stopPropagation();">
<img src="../../icons/phone.tga" style="width: 24px; height: 24px;"/>
</div>
</div>
</div>
<div class="btn-fab" onclick="addContact()">
<img src="../../icons/add.tga" style="width: 32px; height: 32px;"/>
</div> </div>
</div> </div>
<script>
function openContact(name)
print("[Contacts] Opening contact: " .. name)
if showToast then
showToast("Contact: " .. name)
end
end
function callContact(name)
print("[Contacts] Calling: " .. name)
if showToast then
showToast("Calling " .. name .. "...")
end
if shellNavigateTo then
shellNavigateTo("calling")
end
end
function addContact()
print("[Contacts] Add new contact")
if showToast then
showToast("Add new contact")
end
end
print("[Contacts] Content loaded")
</script>

View File

@@ -0,0 +1,468 @@
<!-- Calling Screen Content Fragment -->
<!-- In-call interface with controls -->
<style>
.calling-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
.calling-info {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.calling-avatar {
width: 120px;
height: 120px;
border-radius: 60px;
background-color: #BB86FC;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 24px;
}
.calling-name {
font-size: 32px;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 8px;
}
.calling-status {
font-size: 18px;
color: #B0B0B0;
margin-bottom: 8px;
}
.calling-timer {
font-size: 24px;
color: #FFFFFF;
font-weight: 300;
}
/* Call controls */
.call-controls {
padding: 32px 24px 48px;
}
.controls-row {
display: flex;
justify-content: center;
gap: 32px;
margin-bottom: 32px;
}
.control-btn {
width: 64px;
height: 64px;
border-radius: 32px;
background-color: rgba(255, 255, 255, 0.15);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.control-btn:hover {
background-color: rgba(255, 255, 255, 0.25);
}
.control-btn.active {
background-color: #FFFFFF;
}
.control-btn.active img {
filter: invert(1);
}
.control-btn img {
width: 28px;
height: 28px;
}
.control-label {
font-size: 12px;
color: #FFFFFF;
margin-top: 8px;
text-align: center;
}
.control-btn.active .control-label {
color: #000000;
}
/* End call button */
.end-call-row {
display: flex;
justify-content: center;
}
.end-call-btn {
width: 72px;
height: 72px;
border-radius: 36px;
background-color: #F44336;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.end-call-btn:hover {
background-color: #E53935;
}
.end-call-btn img {
width: 36px;
height: 36px;
transform: rotate(135deg);
}
/* Dialpad overlay */
.dialpad-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #1a1a1a;
padding: 24px;
border-radius: 24px 24px 0 0;
display: none;
}
.dialpad-overlay.visible {
display: block;
}
.dialpad-row {
display: flex;
justify-content: center;
gap: 24px;
margin-bottom: 16px;
}
.dialpad-key {
width: 64px;
height: 64px;
border-radius: 32px;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.dialpad-key:hover {
background-color: #3d3d3d;
}
.dialpad-digit {
font-size: 24px;
color: #FFFFFF;
}
.dialpad-letters {
font-size: 10px;
color: #888888;
}
.dialpad-close {
text-align: center;
padding: 16px;
color: #BB86FC;
font-size: 16px;
cursor: pointer;
}
</style>
<div class="calling-content">
<div class="calling-info">
<div id="calling-avatar" class="calling-avatar">J</div>
<div id="calling-name" class="calling-name">John Smith</div>
<div id="calling-status" class="calling-status">Calling...</div>
<div id="calling-timer" class="calling-timer"></div>
</div>
<div class="call-controls">
<div class="controls-row">
<div class="control-btn" id="btn-mute" onclick="toggleMute()">
<img src="../../icons/mic.tga"/>
<span class="control-label">Mute</span>
</div>
<div class="control-btn" id="btn-keypad" onclick="toggleKeypad()">
<img src="../../icons/keypad.tga"/>
<span class="control-label">Keypad</span>
</div>
<div class="control-btn" id="btn-speaker" onclick="toggleSpeaker()">
<img src="../../icons/speaker.tga"/>
<span class="control-label">Speaker</span>
</div>
</div>
<div class="controls-row">
<div class="control-btn" id="btn-add" onclick="addCall()">
<img src="../../icons/add.tga"/>
<span class="control-label">Add call</span>
</div>
<div class="control-btn" id="btn-hold" onclick="toggleHold()">
<img src="../../icons/pause.tga"/>
<span class="control-label">Hold</span>
</div>
<div class="control-btn" id="btn-video" onclick="toggleVideo()">
<img src="../../icons/camera.tga"/>
<span class="control-label">Video</span>
</div>
</div>
<div class="end-call-row">
<div class="end-call-btn" onclick="endCall()">
<img src="../../icons/phone.tga"/>
</div>
</div>
</div>
<!-- Dialpad overlay -->
<div id="dialpad-overlay" class="dialpad-overlay">
<div class="dialpad-row">
<div class="dialpad-key" onclick="dialDTMF('1')">
<span class="dialpad-digit">1</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('2')">
<span class="dialpad-digit">2</span>
<span class="dialpad-letters">ABC</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('3')">
<span class="dialpad-digit">3</span>
<span class="dialpad-letters">DEF</span>
</div>
</div>
<div class="dialpad-row">
<div class="dialpad-key" onclick="dialDTMF('4')">
<span class="dialpad-digit">4</span>
<span class="dialpad-letters">GHI</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('5')">
<span class="dialpad-digit">5</span>
<span class="dialpad-letters">JKL</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('6')">
<span class="dialpad-digit">6</span>
<span class="dialpad-letters">MNO</span>
</div>
</div>
<div class="dialpad-row">
<div class="dialpad-key" onclick="dialDTMF('7')">
<span class="dialpad-digit">7</span>
<span class="dialpad-letters">PQRS</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('8')">
<span class="dialpad-digit">8</span>
<span class="dialpad-letters">TUV</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('9')">
<span class="dialpad-digit">9</span>
<span class="dialpad-letters">WXYZ</span>
</div>
</div>
<div class="dialpad-row">
<div class="dialpad-key" onclick="dialDTMF('*')">
<span class="dialpad-digit">*</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('0')">
<span class="dialpad-digit">0</span>
<span class="dialpad-letters">+</span>
</div>
<div class="dialpad-key" onclick="dialDTMF('#')">
<span class="dialpad-digit">#</span>
</div>
</div>
<div class="dialpad-close" onclick="toggleKeypad()">Hide</div>
</div>
</div>
<script>
-- Calling state
local call_state = {
is_muted = false,
is_speaker = false,
is_hold = false,
is_connected = false,
call_duration = 0,
keypad_visible = false
}
local timer_id = nil
local function getDoc()
return shell_document
end
-- Simulate call connection
local function simulateConnection()
if setTimeout then
setTimeout(function()
call_state.is_connected = true
local doc = getDoc()
if doc then
local status = doc:GetElementById("calling-status")
if status then
status.inner_rml = "Connected"
end
end
startCallTimer()
end, 2000)
end
end
-- Start call timer
function startCallTimer()
if setInterval then
timer_id = setInterval(function()
call_state.call_duration = call_state.call_duration + 1
updateTimerDisplay()
end, 1000)
end
end
-- Update timer display
function updateTimerDisplay()
local doc = getDoc()
if not doc then return end
local timer = doc:GetElementById("calling-timer")
if timer then
local mins = math.floor(call_state.call_duration / 60)
local secs = call_state.call_duration % 60
timer.inner_rml = string.format("%02d:%02d", mins, secs)
end
end
-- Toggle mute
function toggleMute()
call_state.is_muted = not call_state.is_muted
local doc = getDoc()
if doc then
local btn = doc:GetElementById("btn-mute")
if btn then
btn:SetClass("active", call_state.is_muted)
end
end
if showToast then
showToast(call_state.is_muted and "Muted" or "Unmuted")
end
print("[Calling] Mute: " .. tostring(call_state.is_muted))
end
-- Toggle speaker
function toggleSpeaker()
call_state.is_speaker = not call_state.is_speaker
local doc = getDoc()
if doc then
local btn = doc:GetElementById("btn-speaker")
if btn then
btn:SetClass("active", call_state.is_speaker)
end
end
if showToast then
showToast(call_state.is_speaker and "Speaker on" or "Speaker off")
end
print("[Calling] Speaker: " .. tostring(call_state.is_speaker))
end
-- Toggle hold
function toggleHold()
call_state.is_hold = not call_state.is_hold
local doc = getDoc()
if doc then
local btn = doc:GetElementById("btn-hold")
if btn then
btn:SetClass("active", call_state.is_hold)
end
local status = doc:GetElementById("calling-status")
if status then
if call_state.is_hold then
status.inner_rml = "On Hold"
elseif call_state.is_connected then
status.inner_rml = "Connected"
end
end
end
if showToast then
showToast(call_state.is_hold and "Call on hold" or "Call resumed")
end
print("[Calling] Hold: " .. tostring(call_state.is_hold))
end
-- Toggle keypad
function toggleKeypad()
call_state.keypad_visible = not call_state.keypad_visible
local doc = getDoc()
if doc then
local overlay = doc:GetElementById("dialpad-overlay")
if overlay then
overlay:SetClass("visible", call_state.keypad_visible)
end
end
print("[Calling] Keypad: " .. tostring(call_state.keypad_visible))
end
-- Dial DTMF tone
function dialDTMF(digit)
print("[Calling] DTMF: " .. digit)
if showToast then
showToast("DTMF: " .. digit)
end
end
-- Add call
function addCall()
if showToast then
showToast("Add call not available")
end
end
-- Toggle video
function toggleVideo()
if showToast then
showToast("Video call not available")
end
end
-- End call
function endCall()
print("[Calling] Ending call")
-- Stop timer
if timer_id and clearInterval then
clearInterval(timer_id)
timer_id = nil
end
if showToast then
local mins = math.floor(call_state.call_duration / 60)
local secs = call_state.call_duration % 60
showToast(string.format("Call ended (%02d:%02d)", mins, secs))
end
-- Navigate back
if shellGoBack then
shellGoBack()
end
end
-- Initialize on load
simulateConnection()
print("[Calling] Content loaded")
</script>

View File

@@ -1,11 +1,261 @@
<!-- Dialer App Content Fragment --> <!-- Dialer App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content">
<div class="app-header"> <!-- Tabs -->
<span class="app-header-title">Phone</span> <div class="bottom-nav" style="height: 56px; position: relative; top: 0;">
<div id="tab-keypad" class="bottom-nav-item active" onclick="switchDialerTab('keypad')">
<span style="font-size: 14px; font-weight: 600;">Keypad</span>
</div>
<div id="tab-recent" class="bottom-nav-item" onclick="switchDialerTab('recent')">
<span style="font-size: 14px; font-weight: 600;">Recent</span>
</div>
<div id="tab-contacts" class="bottom-nav-item" onclick="switchDialerTab('contacts')">
<span style="font-size: 14px; font-weight: 600;">Contacts</span>
</div>
</div>
<!-- Keypad View -->
<div id="view-keypad" style="flex: 1; display: flex; flex-direction: column;">
<div class="dial-display" id="dialer-number"></div>
<div class="dial-pad">
<div class="dial-key" onclick="dialDigit('1')">
<span class="dial-key-number">1</span>
<span class="dial-key-letters"></span>
</div>
<div class="dial-key" onclick="dialDigit('2')">
<span class="dial-key-number">2</span>
<span class="dial-key-letters">ABC</span>
</div>
<div class="dial-key" onclick="dialDigit('3')">
<span class="dial-key-number">3</span>
<span class="dial-key-letters">DEF</span>
</div>
<div class="dial-key" onclick="dialDigit('4')">
<span class="dial-key-number">4</span>
<span class="dial-key-letters">GHI</span>
</div>
<div class="dial-key" onclick="dialDigit('5')">
<span class="dial-key-number">5</span>
<span class="dial-key-letters">JKL</span>
</div>
<div class="dial-key" onclick="dialDigit('6')">
<span class="dial-key-number">6</span>
<span class="dial-key-letters">MNO</span>
</div>
<div class="dial-key" onclick="dialDigit('7')">
<span class="dial-key-number">7</span>
<span class="dial-key-letters">PQRS</span>
</div>
<div class="dial-key" onclick="dialDigit('8')">
<span class="dial-key-number">8</span>
<span class="dial-key-letters">TUV</span>
</div>
<div class="dial-key" onclick="dialDigit('9')">
<span class="dial-key-number">9</span>
<span class="dial-key-letters">WXYZ</span>
</div>
<div class="dial-key" onclick="dialDigit('*')">
<span class="dial-key-number">*</span>
<span class="dial-key-letters"></span>
</div>
<div class="dial-key" onclick="dialDigit('0')">
<span class="dial-key-number">0</span>
<span class="dial-key-letters">+</span>
</div>
<div class="dial-key" onclick="dialDigit('#')">
<span class="dial-key-number">#</span>
<span class="dial-key-letters"></span>
</div>
</div>
<div class="dial-actions">
<div class="btn-icon" onclick="openVideoCall()">
<img src="../../icons/camera.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="dial-call-btn" onclick="makeCall()">
<img src="../../icons/phone.tga" style="width: 36px; height: 36px;"/>
</div>
<div class="btn-icon" onclick="deleteDigit()">
<img src="../../icons/back.tga" style="width: 28px; height: 28px;"/>
</div>
</div>
</div>
<!-- Recent Calls View -->
<div id="view-recent" style="flex: 1; overflow: auto; display: none;">
<div class="list">
<div class="list-item" onclick="callContact('Mom')">
<div class="list-item-avatar" style="background-color: #E91E63;">M</div>
<div class="list-item-content">
<span class="list-item-title">Mom</span>
<span class="list-item-subtitle" style="color: #4CAF50;">Outgoing - 5 min</span>
</div>
<span style="font-size: 14px; color: #888888;">10:30 AM</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="callContact('John Smith')">
<div class="list-item-avatar" style="background-color: #2196F3;">J</div>
<div class="list-item-content">
<span class="list-item-title">John Smith</span>
<span class="list-item-subtitle" style="color: #F44336;">Missed</span>
</div>
<span style="font-size: 14px; color: #888888;">Yesterday</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="callContact('Work')">
<div class="list-item-avatar" style="background-color: #FF9800;">W</div>
<div class="list-item-content">
<span class="list-item-title">Work</span>
<span class="list-item-subtitle">Incoming - 12 min</span>
</div>
<span style="font-size: 14px; color: #888888;">Yesterday</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="callContact('Sarah')">
<div class="list-item-avatar" style="background-color: #9C27B0;">S</div>
<div class="list-item-content">
<span class="list-item-title">Sarah</span>
<span class="list-item-subtitle" style="color: #4CAF50;">Outgoing - 23 min</span>
</div>
<span style="font-size: 14px; color: #888888;">Monday</span>
</div>
</div>
</div>
<!-- Contacts View -->
<div id="view-contacts" style="flex: 1; overflow: auto; display: none;">
<div class="search-bar">
<img src="../../icons/search.tga" style="width: 24px; height: 24px; margin-right: 12px; opacity: 0.6;"/>
<span style="color: #B3B3B3;">Search contacts</span>
</div>
<div class="list">
<div class="list-item" onclick="callContact('Alice Johnson')">
<div class="list-item-avatar" style="background-color: #4CAF50;">A</div>
<span class="list-item-title">Alice Johnson</span>
</div>
<div class="list-item" onclick="callContact('Bob Williams')">
<div class="list-item-avatar" style="background-color: #2196F3;">B</div>
<span class="list-item-title">Bob Williams</span>
</div>
<div class="list-item" onclick="callContact('Carol Davis')">
<div class="list-item-avatar" style="background-color: #9C27B0;">C</div>
<span class="list-item-title">Carol Davis</span>
</div>
<div class="list-item" onclick="callContact('David Miller')">
<div class="list-item-avatar" style="background-color: #FF9800;">D</div>
<span class="list-item-title">David Miller</span>
</div>
<div class="list-item" onclick="callContact('Emma Wilson')">
<div class="list-item-avatar" style="background-color: #E91E63;">E</div>
<span class="list-item-title">Emma Wilson</span>
</div>
</div> </div>
<div class="app-body">
<span class="placeholder-text">Dialer App</span>
</div> </div>
</div> </div>
<script>
local dialed_number = ""
local current_tab = "keypad"
local function getDoc()
return shell_document
end
function switchDialerTab(tab)
current_tab = tab
local doc = getDoc()
if not doc then return end
local tabs = {"keypad", "recent", "contacts"}
for _, t in ipairs(tabs) do
local tab_el = doc:GetElementById("tab-" .. t)
local view_el = doc:GetElementById("view-" .. t)
if tab_el then
if t == tab then
tab_el:SetClass("active", true)
else
tab_el:SetClass("active", false)
end
end
if view_el then
if t == tab then
view_el.style.display = "flex"
else
view_el.style.display = "none"
end
end
end
print("[Dialer] Switched to tab: " .. tab)
end
function dialDigit(digit)
dialed_number = dialed_number .. digit
updateDisplay()
print("[Dialer] Dialed: " .. digit .. " -> " .. dialed_number)
end
function deleteDigit()
if #dialed_number > 0 then
dialed_number = dialed_number:sub(1, -2)
updateDisplay()
print("[Dialer] Deleted digit -> " .. dialed_number)
end
end
function updateDisplay()
local doc = getDoc()
if not doc then return end
local display = doc:GetElementById("dialer-number")
if display then
local formatted = dialed_number
if #dialed_number > 6 then
formatted = dialed_number:sub(1, 3) .. "-" .. dialed_number:sub(4, 6) .. "-" .. dialed_number:sub(7)
elseif #dialed_number > 3 then
formatted = dialed_number:sub(1, 3) .. "-" .. dialed_number:sub(4)
end
display.inner_rml = formatted
end
end
function makeCall()
if #dialed_number > 0 then
print("[Dialer] Calling: " .. dialed_number)
if showToast then
showToast("Calling " .. dialed_number .. "...")
end
if shellNavigateTo then
shellNavigateTo("calling")
end
else
if showToast then
showToast("Enter a number to call")
end
end
end
function callContact(name)
print("[Dialer] Calling contact: " .. name)
if showToast then
showToast("Calling " .. name .. "...")
end
if shellNavigateTo then
shellNavigateTo("calling")
end
end
function openVideoCall()
if #dialed_number > 0 then
if showToast then
showToast("Video call to " .. dialed_number)
end
else
if showToast then
showToast("Enter a number for video call")
end
end
end
print("[Dialer] Content loaded")
</script>

View File

@@ -0,0 +1,117 @@
<!-- Chat Content Fragment -->
<!-- Uses classes from components.rcss -->
<div class="app-content">
<div class="app-bar">
<div class="app-bar-nav" onclick="goBackToMessages()">
<img src="../../icons/back.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-avatar" id="chat-avatar" style="background-color: #9C27B0; width: 40px; height: 40px; margin-right: 12px;">S</div>
<div class="list-item-content" style="flex: 1;">
<span class="list-item-title" id="chat-name">Sarah</span>
<span class="list-item-subtitle" style="color: #4CAF50;">Online</span>
</div>
<div class="btn-icon" onclick="startCall()">
<img src="../../icons/phone.tga" style="width: 28px; height: 28px;"/>
</div>
</div>
<!-- Messages Area -->
<div class="chat-container" id="messages-area" style="flex: 1; overflow: auto;">
<div class="chat-timestamp">Today</div>
<div class="chat-bubble chat-bubble-received">
Hey! How are you doing?
</div>
<div class="chat-bubble chat-bubble-sent">
I'm good! Just finished some work.
</div>
<div class="chat-bubble chat-bubble-received">
Nice! Are you coming to the party tonight?
</div>
<div class="chat-bubble chat-bubble-sent">
Yeah, I'll be there around 8pm
</div>
<div class="chat-bubble chat-bubble-received">
Perfect! Can't wait to see you there!
</div>
<div class="chat-bubble chat-bubble-sent">
Same here! Should I bring anything?
</div>
<div class="chat-bubble chat-bubble-received">
Just yourself! Maybe some snacks if you want
</div>
<div class="chat-bubble chat-bubble-received">
Hey! Are you coming to the party tonight?
</div>
</div>
<!-- Input Area -->
<div class="chat-input-container">
<div class="btn-icon" onclick="attachFile()">
<img src="../../icons/add.tga" style="width: 28px; height: 28px;"/>
</div>
<input type="text" class="chat-input" id="message-input" placeholder="Message"/>
<div class="chat-send-btn" onclick="sendMessage()">
<img src="../../icons/send.tga" style="width: 28px; height: 28px;"/>
</div>
</div>
</div>
<script>
function goBackToMessages()
print("[Chat] Going back to messages")
if shellGoBack then
shellGoBack()
end
end
function startCall()
print("[Chat] Starting call")
if showToast then
showToast("Calling Sarah...")
end
if shellNavigateTo then
shellNavigateTo("calling")
end
end
function attachFile()
print("[Chat] Attach file")
if showToast then
showToast("Attach file")
end
end
function sendMessage()
local doc = shell_document
if doc then
local input = doc:GetElementById("message-input")
if input and input.value and input.value ~= "" then
local msg = input.value
print("[Chat] Sending: " .. msg)
local area = doc:GetElementById("messages-area")
if area then
local new_msg = [[<div class="chat-bubble chat-bubble-sent">]] .. msg .. [[</div>]]
area.inner_rml = area.inner_rml .. new_msg
end
input.value = ""
if showToast then
showToast("Message sent")
end
end
end
end
print("[Chat] Content loaded")
</script>

View File

@@ -1,11 +1,119 @@
<!-- Messages App Content Fragment --> <!-- Messages App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content">
<div class="app-header"> <div class="app-bar">
<span class="app-header-title">Messages</span> <span class="app-bar-title">Messages</span>
<div class="app-bar-actions">
<div class="btn-icon" onclick="searchMessages()">
<img src="../../icons/search.tga" style="width: 28px; height: 28px;"/>
</div> </div>
<div class="app-body"> </div>
<span class="placeholder-text">Messages App</span> </div>
<div class="list" style="flex: 1; overflow: auto;">
<div class="list-item" onclick="openChat('Sarah')">
<div class="list-item-avatar" style="background-color: #9C27B0;">S</div>
<div class="list-item-content">
<span class="list-item-title" style="color: #BB86FC;">Sarah</span>
<span class="list-item-subtitle" style="color: #FFFFFF;">Hey! Are you coming to the party tonight?</span>
</div>
<span style="font-size: 14px; color: #888888;">2 min</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('John Smith')">
<div class="list-item-avatar" style="background-color: #3F51B5;">J</div>
<div class="list-item-content">
<span class="list-item-title">John Smith</span>
<span class="list-item-subtitle">Thanks for your help yesterday!</span>
</div>
<span style="font-size: 14px; color: #888888;">1 hr</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('Mom')">
<div class="list-item-avatar" style="background-color: #E91E63;">M</div>
<div class="list-item-content">
<span class="list-item-title" style="color: #BB86FC;">Mom</span>
<span class="list-item-subtitle" style="color: #FFFFFF;">Don't forget to call grandma!</span>
</div>
<span style="font-size: 14px; color: #888888;">3 hr</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('Work Group')">
<div class="list-item-avatar" style="background-color: #FF9800;">W</div>
<div class="list-item-content">
<span class="list-item-title">Work Group</span>
<span class="list-item-subtitle">Bob: Meeting moved to 3pm</span>
</div>
<span style="font-size: 14px; color: #888888;">Yesterday</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('Alice Johnson')">
<div class="list-item-avatar" style="background-color: #4CAF50;">A</div>
<div class="list-item-content">
<span class="list-item-title">Alice Johnson</span>
<span class="list-item-subtitle">See you at the coffee shop!</span>
</div>
<span style="font-size: 14px; color: #888888;">Yesterday</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('David Miller')">
<div class="list-item-avatar" style="background-color: #FF9800;">D</div>
<div class="list-item-content">
<span class="list-item-title">David Miller</span>
<span class="list-item-subtitle">The project is almost done</span>
</div>
<span style="font-size: 14px; color: #888888;">Monday</span>
</div>
<div class="list-divider"></div>
<div class="list-item" onclick="openChat('Emma Wilson')">
<div class="list-item-avatar" style="background-color: #E91E63;">E</div>
<div class="list-item-content">
<span class="list-item-title">Emma Wilson</span>
<span class="list-item-subtitle">That was a great movie!</span>
</div>
<span style="font-size: 14px; color: #888888;">Sunday</span>
</div>
</div>
<div class="btn-fab" onclick="newMessage()">
<img src="../../icons/edit.tga" style="width: 32px; height: 32px;"/>
</div> </div>
</div> </div>
<script>
function openChat(name)
print("[Messages] Opening chat: " .. name)
if shellNavigateTo then
shellNavigateTo("chat")
end
end
function searchMessages()
print("[Messages] Search")
if showToast then
showToast("Search messages")
end
end
function newMessage()
print("[Messages] New message")
if showToast then
showToast("New message")
end
end
print("[Messages] Content loaded")
</script>

View File

@@ -1,11 +1,175 @@
<!-- Music App Content Fragment --> <!-- Music App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content" style="position: relative;">
<div class="app-header"> <div class="app-bar">
<span class="app-header-title">Music</span> <span class="app-bar-title">Good afternoon</span>
<div class="btn-icon" onclick="openMusicSettings()">
<img src="../../icons/settings.tga" style="width: 28px; height: 28px;"/>
</div>
</div>
<div style="flex: 1; overflow: auto; padding-bottom: 100px;">
<!-- Quick Access Grid -->
<div style="display: flex; flex-wrap: wrap; gap: 8px; padding: 16px;">
<div class="quick-card" onclick="openPlaylist('liked')" style="width: 48%;">
<div class="quick-card-art" style="background-color: #7c3aed;">L</div>
<span class="quick-card-title">Liked Songs</span>
</div>
<div class="quick-card" onclick="openPlaylist('daily1')" style="width: 48%;">
<div class="quick-card-art" style="background-color: #667eea;">D</div>
<span class="quick-card-title">Daily Mix 1</span>
</div>
<div class="quick-card" onclick="openPlaylist('release')" style="width: 48%;">
<div class="quick-card-art" style="background-color: #16a34a;">R</div>
<span class="quick-card-title">Release Radar</span>
</div>
<div class="quick-card" onclick="openPlaylist('chill')" style="width: 48%;">
<div class="quick-card-art" style="background-color: #f093fb;">C</div>
<span class="quick-card-title">Chill Vibes</span>
</div>
</div>
<!-- Recently Played -->
<div class="section-header">
<span class="section-title">Recently Played</span>
<span class="section-action" onclick="showAllRecent()">SEE ALL</span>
</div>
<div style="display: flex; overflow-x: auto; padding: 0 16px; gap: 16px;">
<div class="recent-item" onclick="openPlaylist('pop')">
<div class="recent-art" style="background-color: #43e97b;">P</div>
<div class="recent-title">Pop Hits</div>
<div class="recent-subtitle">Playlist</div>
</div>
<div class="recent-item" onclick="openPlaylist('electronic')">
<div class="recent-art" style="background-color: #fa709a;">E</div>
<div class="recent-title">Electronic</div>
<div class="recent-subtitle">Playlist</div>
</div>
<div class="recent-item" onclick="openPlaylist('jazz')">
<div class="recent-art" style="background-color: #667eea;">J</div>
<div class="recent-title">Jazz Classics</div>
<div class="recent-subtitle">Playlist</div>
</div>
</div>
<!-- Made For You -->
<div class="section-header">
<span class="section-title">Made For You</span>
<span class="section-action" onclick="showAllMixes()">SEE ALL</span>
</div>
<div class="playlist-item" onclick="openPlaylist('daily1')">
<div class="playlist-art" style="background-color: #4facfe;">1</div>
<div class="playlist-info">
<div class="playlist-title">Daily Mix 1</div>
<div class="playlist-meta">Based on your listening</div>
</div>
</div>
<div class="playlist-item" onclick="openPlaylist('daily2')">
<div class="playlist-art" style="background-color: #43e97b;">2</div>
<div class="playlist-info">
<div class="playlist-title">Daily Mix 2</div>
<div class="playlist-meta">Electronic, Ambient, Chill</div>
</div>
</div>
<div class="playlist-item" onclick="openPlaylist('discover')">
<div class="playlist-art" style="background-color: #fa709a;">D</div>
<div class="playlist-info">
<div class="playlist-title">Discover Weekly</div>
<div class="playlist-meta">Your weekly mixtape</div>
</div>
</div>
</div>
<!-- Mini Player -->
<div class="mini-player" onclick="openNowPlaying()" style="position: absolute; bottom: 0; left: 0; right: 0;">
<div class="mini-player-art" id="mini-art" style="background-color: #667eea;">M</div>
<div class="mini-player-info">
<div class="mini-player-title" id="mini-title">Midnight City</div>
<div class="mini-player-artist" id="mini-artist">M83</div>
</div>
<div class="mini-player-controls">
<div class="mini-control-btn" onclick="toggleLike(); event.stopPropagation();">
<img src="../../icons/heart.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="mini-control-btn" id="play-btn" onclick="togglePlay(); event.stopPropagation();">
<img id="play-icon" src="../../icons/play.tga" style="width: 28px; height: 28px;"/>
</div>
</div> </div>
<div class="app-body">
<span class="placeholder-text">Music App</span>
</div> </div>
</div> </div>
<script>
local is_playing = false
local current_track = {
title = "Midnight City",
artist = "M83"
}
local function getDoc()
return shell_document
end
function togglePlay()
is_playing = not is_playing
local doc = getDoc()
if doc then
local icon = doc:GetElementById("play-icon")
if icon then
icon:SetAttribute("src", is_playing and "../../icons/pause.tga" or "../../icons/play.tga")
end
end
print("[Music] " .. (is_playing and "Playing" or "Paused"))
if showToast then
showToast(is_playing and "Playing" or "Paused")
end
end
function toggleLike()
print("[Music] Liked")
if showToast then
showToast("Added to Liked Songs")
end
end
function openPlaylist(id)
print("[Music] Opening playlist: " .. id)
if showToast then
showToast("Playlist: " .. id)
end
end
function openNowPlaying()
print("[Music] Now playing")
if showToast then
showToast("Now Playing: " .. current_track.title)
end
end
function showAllRecent()
print("[Music] Show all recent")
if showToast then
showToast("Recently played")
end
end
function showAllMixes()
print("[Music] Show all mixes")
if showToast then
showToast("Made for you")
end
end
function openMusicSettings()
print("[Music] Settings")
if showToast then
showToast("Music settings")
end
end
print("[Music] Content loaded")
</script>

View File

@@ -1,11 +1,309 @@
<!-- Settings App Content Fragment --> <!-- Settings App Content Fragment -->
<!-- Styles are in shell.rml --> <!-- Uses classes from components.rcss -->
<div class="app-content"> <div class="app-content">
<div class="app-header"> <div class="app-bar">
<span class="app-header-title">Settings</span> <span class="app-bar-title">Settings</span>
</div>
<div class="search-bar">
<img class="search-icon" src="../../icons/search.tga"/>
<span class="search-input">Search settings</span>
</div>
<div style="flex: 1; overflow: auto;">
<!-- Profile Card -->
<div class="list-item" onclick="openProfile()">
<div class="list-item-avatar" style="background-color: #BB86FC;">U</div>
<div class="list-item-content">
<span class="list-item-title">User</span>
<span class="list-item-subtitle">user@mosis.app</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="divider"></div>
<!-- Network Section -->
<div class="list-header">Network</div>
<div class="list-item" onclick="toggleWifi()">
<div class="list-item-icon" style="background-color: #2196F3; border-radius: 24px;">
<img src="../../icons/wifi.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Wi-Fi</span>
<span id="wifi-status" class="list-item-subtitle">Connected to MosisNetwork</span>
</div>
<div id="toggle-wifi" class="toggle active">
<div class="toggle-thumb"></div>
</div>
</div>
<div class="list-item" onclick="toggleBluetooth()">
<div class="list-item-icon" style="background-color: #3F51B5; border-radius: 24px;">
<img src="../../icons/bluetooth.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Bluetooth</span>
<span id="bt-status" class="list-item-subtitle">Off</span>
</div>
<div id="toggle-bluetooth" class="toggle">
<div class="toggle-thumb"></div>
</div>
</div>
<div class="list-item" onclick="toggleAirplane()">
<div class="list-item-icon" style="background-color: #FF9800; border-radius: 24px;">
<img src="../../icons/plane.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Airplane Mode</span>
<span class="list-item-subtitle">Off</span>
</div>
<div id="toggle-airplane" class="toggle">
<div class="toggle-thumb"></div>
</div>
</div>
<div class="divider"></div>
<!-- Device Section -->
<div class="list-header">Device</div>
<div class="list-item" onclick="openDisplay()">
<div class="list-item-icon" style="background-color: #9C27B0; border-radius: 24px;">
<img src="../../icons/display.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Display</span>
<span class="list-item-subtitle">Brightness, Dark mode</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="list-item" onclick="openSound()">
<div class="list-item-icon" style="background-color: #E91E63; border-radius: 24px;">
<img src="../../icons/volume.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Sound</span>
<span class="list-item-subtitle">Volume, Ringtone</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="list-item" onclick="openNotifications()">
<div class="list-item-icon" style="background-color: #00BCD4; border-radius: 24px;">
<img src="../../icons/notifications.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Notifications</span>
<span class="list-item-subtitle">App notifications</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="list-item" onclick="openBattery()">
<div class="list-item-icon" style="background-color: #4CAF50; border-radius: 24px;">
<img src="../../icons/battery.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Battery</span>
<span class="list-item-subtitle">85% - Not charging</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="list-item" onclick="openStorage()">
<div class="list-item-icon" style="background-color: #795548; border-radius: 24px;">
<img src="../../icons/storage.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Storage</span>
<span class="list-item-subtitle">32 GB of 128 GB used</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="divider"></div>
<!-- Privacy Section -->
<div class="list-header">Privacy</div>
<div class="list-item" onclick="toggleLocation()">
<div class="list-item-icon" style="background-color: #F44336; border-radius: 24px;">
<img src="../../icons/location.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Location</span>
<span id="location-status" class="list-item-subtitle">On - High accuracy</span>
</div>
<div id="toggle-location" class="toggle active">
<div class="toggle-thumb"></div>
</div>
</div>
<div class="list-item" onclick="openPrivacy()">
<div class="list-item-icon" style="background-color: #607D8B; border-radius: 24px;">
<img src="../../icons/lock.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">Privacy</span>
<span class="list-item-subtitle">Permissions, Data</span>
</div>
<span class="list-item-action">&gt;</span>
</div>
<div class="divider"></div>
<!-- About Section -->
<div class="list-header">About</div>
<div class="list-item" onclick="openAbout()">
<div class="list-item-icon" style="background-color: #BB86FC; border-radius: 24px;">
<img src="../../icons/info.tga" style="width: 28px; height: 28px;"/>
</div>
<div class="list-item-content">
<span class="list-item-title">About Phone</span>
<span class="list-item-subtitle">Mosis v1.0</span>
</div>
<span class="list-item-action">&gt;</span>
</div> </div>
<div class="app-body">
<span class="placeholder-text">Settings App</span>
</div> </div>
</div> </div>
<script>
local settings = {
wifi = true,
bluetooth = false,
airplane = false,
location = true
}
local function getDoc()
return shell_document
end
function toggleWifi()
settings.wifi = not settings.wifi
local doc = getDoc()
if doc then
local toggle = doc:GetElementById("toggle-wifi")
local status = doc:GetElementById("wifi-status")
if toggle then
toggle:SetClass("active", settings.wifi)
end
if status then
status.inner_rml = settings.wifi and "Connected to MosisNetwork" or "Off"
end
end
print("[Settings] Wi-Fi: " .. tostring(settings.wifi))
if showToast then
showToast(settings.wifi and "Wi-Fi enabled" or "Wi-Fi disabled")
end
end
function toggleBluetooth()
settings.bluetooth = not settings.bluetooth
local doc = getDoc()
if doc then
local toggle = doc:GetElementById("toggle-bluetooth")
local status = doc:GetElementById("bt-status")
if toggle then
toggle:SetClass("active", settings.bluetooth)
end
if status then
status.inner_rml = settings.bluetooth and "On" or "Off"
end
end
print("[Settings] Bluetooth: " .. tostring(settings.bluetooth))
if showToast then
showToast(settings.bluetooth and "Bluetooth enabled" or "Bluetooth disabled")
end
end
function toggleAirplane()
settings.airplane = not settings.airplane
local doc = getDoc()
if doc then
local toggle = doc:GetElementById("toggle-airplane")
if toggle then
toggle:SetClass("active", settings.airplane)
end
if settings.airplane then
settings.wifi = false
settings.bluetooth = false
local wt = doc:GetElementById("toggle-wifi")
local bt = doc:GetElementById("toggle-bluetooth")
if wt then wt:SetClass("active", false) end
if bt then bt:SetClass("active", false) end
end
end
print("[Settings] Airplane: " .. tostring(settings.airplane))
if showToast then
showToast(settings.airplane and "Airplane mode on" or "Airplane mode off")
end
end
function toggleLocation()
settings.location = not settings.location
local doc = getDoc()
if doc then
local toggle = doc:GetElementById("toggle-location")
local status = doc:GetElementById("location-status")
if toggle then
toggle:SetClass("active", settings.location)
end
if status then
status.inner_rml = settings.location and "On - High accuracy" or "Off"
end
end
print("[Settings] Location: " .. tostring(settings.location))
if showToast then
showToast(settings.location and "Location enabled" or "Location disabled")
end
end
function openProfile()
print("[Settings] Profile")
if showToast then showToast("User profile") end
end
function openDisplay()
print("[Settings] Display")
if showToast then showToast("Display settings") end
end
function openSound()
print("[Settings] Sound")
if showToast then showToast("Sound settings") end
end
function openNotifications()
print("[Settings] Notifications")
if showToast then showToast("Notification settings") end
end
function openBattery()
print("[Settings] Battery")
if showToast then showToast("Battery: 85%") end
end
function openStorage()
print("[Settings] Storage")
if showToast then showToast("Storage: 25% used") end
end
function openPrivacy()
print("[Settings] Privacy")
if showToast then showToast("Privacy settings") end
end
function openAbout()
print("[Settings] About")
if showToast then showToast("Mosis Virtual Phone v1.0") end
end
print("[Settings] Content loaded")
</script>