Compare commits

..

51 Commits

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 11:28:12 +01:00
2134a53921 fix Android: load shell.rml, add loadAppContent function, fallback to built-in apps 2026-01-20 10:56:58 +01:00
41fc6fdd86 fix simulator mode to use shell.rml instead of deleted home.rml 2026-01-20 10:39:27 +01:00
ab53bee5c4 add dynamic app discovery and remove unused static RML files 2026-01-20 10:14:13 +01:00
2db7eea9f1 add shell architecture with persistent status bar, nav bar, and content fragments 2026-01-20 10:09:47 +01:00
1f91d7508e add base-apps with manifests, layout system, and testing documentation
- Rename test-apps to base-apps with proper manifest.json for each app
- Add is_system_app flag to app discovery and Lua API
- Fix icon path resolution for /system/icons/ paths
- Add layout.lua and layout.rcss for reusable UI components
- Update home screen to dynamically load all apps from manifests
- Update all app RML files to use layout components
- Comprehensive testing framework documentation with JSON action format
- Add tests/ directory structure for automated UI testing
2026-01-20 09:14:05 +01:00
5de087e8e0 fix document global for external Lua scripts in designer and Android 2026-01-19 22:51:19 +01:00
a3a15b0644 docs: add simulator mode and test-apps flags to DESKTOP-DESIGNER.md 2026-01-19 22:17:37 +01:00
68398e5b60 fix sandbox app script loading and timer execution
- Add relative path resolution for filesystem-loaded documents in kernel.cpp
  - SetDocumentBasePath() tracks document directory for relative resource resolution
  - ResolvePath() resolves relative paths like "app.lua" against document base path
- Fix RmlUi context name lookup in sandbox test app (use "default" not "main")
- Add debug logging to timer_manager.cpp to trace timer creation and execution

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:52:15 +01:00
d6b7504408 implement third-party app launching from home screen 2026-01-19 13:50:05 +01:00
ad28cf2360 add local app discovery: scan apps folder, render on home screen 2026-01-19 13:20:21 +01:00
bb31dcee00 fix remaining logger includes in Android source
Update kernel.cpp and apps/*.cpp to use core library logger.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:30:55 +01:00
58251e21cc add test output files to gitignore
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:15:00 +01:00
486c194f08 update Android to use shared mosis-core library
- Remove duplicate sandbox sources from Android (now in core/)
- Update Android CMakeLists to link mosis-core
- Add OpenSSL crypto support for Android
- Update all includes to use core library headers
- Remove duplicate logger from Android (use core logger)
- Add openssl to Android vcpkg dependencies

This removes ~5,500 lines of duplicate code by sharing
the sandbox implementation between desktop and Android.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:14:34 +01:00
33841516f1 extract shared mosis-core library from sandbox APIs
Create core/ directory with platform-agnostic sandbox components:
- Timer manager, JSON API, Crypto API, Virtual FS
- Lua sandbox, Permission gate, Audit log, Rate limiter
- Platform abstraction interfaces (IAssetInterface, IFilesystemInterface)
- Platform-agnostic logger with Android/Desktop implementations

Update designer to link against mosis-core library instead of
including sandbox sources directly.

This is the foundation for unifying the Android service and
desktop designer to share the same codebase.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 11:57:10 +01:00
f41eda6f62 add simulator mode to desktop designer for testing apps
- Add --simulator flag to launch home screen showing discovered apps
- Create app discovery system to scan test-apps/ directory
- Build simulator home screen with dark phone-like UI
- Add Lua API: simulator.launchApp, simulator.goHome, simulator.getApps
- ESC key returns to home when inside an app
- Apps displayed with icons in grid layout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 11:16:01 +01:00
02db0d849c add --screenshot-after option and fix test app document access
- Add --screenshot-after CLI option to capture screenshot after playback
- Fix sandbox test app to cache document reference for onclick handlers
- Add getDocument() helper that works with RmlUi Lua proxy objects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 10:36:14 +01:00
a583ef64a1 fix action player wait actions to properly delay playback finish
Wait actions now extend the minimum finish time based on their
timestamp + duration, ensuring timers and other async operations
have time to complete before playback ends and screenshot is taken.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 10:35:44 +01:00
8432bbb986 add sandbox support to desktop designer, fix mouse coordinates and UI issues
- Add DesktopSandbox class that integrates timer, JSON, crypto, and VirtualFS APIs
- Fix mouse coordinate handling: GLFW reports window coordinates, not physical pixels
- Fix font path resolution to search multiple locations for test apps
- Fix screenshot capture timing (capture before buffer swap)
- Fix test app CSS: use border-width instead of border:none, add display:block
- Fix test app Lua: add document nil checks, use HTML entities for symbols
- Update hot_reload to reset sandbox state on reload

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 10:22:32 +01:00
d40ea1e537 add generated icon for sandbox test app 2026-01-19 09:17:56 +01:00
d88bddbf75 fix sandbox test app: replace os.date, add goBack, remove missing icon ref 2026-01-19 09:15:30 +01:00
bbf1638f20 update roadmap with completed milestones, trim SANDBOX.md to reference milestone files 2026-01-19 09:07:31 +01:00
010e11cf6b move docs to docs/ folder, merge architecture files, update references 2026-01-19 09:02:11 +01:00
262 changed files with 19397 additions and 6858 deletions

View File

@@ -0,0 +1,75 @@
{
"permissions": {
"allow": [
"Bash(./gradlew assembleDebug:*)",
"Bash(./gradlew installDebug:*)",
"Bash(adb logcat:*)",
"Bash(adb shell am start:*)",
"Bash(adb devices:*)",
"Bash(findstr:*)",
"Bash(tree:*)",
"Bash(cmake:*)",
"Bash(\".\\\\build\\\\Debug\\\\mosis-designer.exe\" \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
"Bash(dir:*)",
"Bash(start \"\" \".\\\\build\\\\Debug\\\\mosis-designer.exe\" \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" \"D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer\\\\test\\\\diagnose.ahk\")",
"Bash(tasklist:*)",
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" /ErrorStdOut diagnose.ahk)",
"Bash(taskkill:*)",
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" full_click_test.ahk)",
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" visual_click_test.ahk)",
"Bash(\".\\\\build\\\\Debug\\\\designer-test.exe\" --designer \"..\\\\designer\\\\build\\\\Debug\\\\mosis-designer.exe\" --document \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
"Bash(./designer-test.exe)",
"Bash(\"D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer-test\\\\build\\\\Debug\\\\designer-test.exe\")",
"Bash(git add:*)",
"Bash(adb install:*)",
"Bash(adb shell am broadcast:*)",
"Bash(timeout 5 ./mosis-designer.exe:*)",
"Bash(.buildDebugmosis-designer.exe --dump D:DevMosisMosisServicesrcmainassetsappshomehome.rml)",
"Bash(cmd /c \"cd /d D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer && .\\\\build\\\\Debug\\\\mosis-designer.exe --dump ..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml 2>&1\")",
"Bash(start /min cmd /c \".\\\\build\\\\Debug\\\\mosis-designer.exe ..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml --hierarchy hierarchy.json\")",
"Bash(powershell -Command:*)",
"Bash(adb shell \"ps -A | grep mosis\")",
"Bash(adb shell:*)",
"Bash(ls:*)",
"Bash(magick:*)",
"Bash(fc:*)",
"Bash(cmp:*)",
"Bash(sort:*)",
"Bash(vcpkg search:*)",
"Bash(D:/vcpkg/vcpkg search glad)",
"Bash(timeout 8 ./mosis-designer.exe:*)",
"Bash(start mosis-designer.exe apps/home/home.rml)",
"Bash(./mosis-designer.exe:*)",
"Bash(cmd /c \"cd /d D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer && cmake --build build --config Debug\")",
"Bash(cmd /c:*)",
"Bash(\"D:/Dev/Mosis/MosisService/designer-test/build/Debug/designer-test.exe\")",
"Bash(python:*)",
"Bash(./gradlew connectedAndroidTest:*)",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnreal Android Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnreal Win64 Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
"Bash(D:)",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnrealEditor Win64 Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -noP4 -utf8output)",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4 -utf8output)",
"Bash(./gradlew.bat:*)",
"Bash(gradle assembleRelease:*)",
"Bash(\"C:\\\\Program Files\\\\Unity\\\\Hub\\\\Editor\\\\6000.3.2f1\\\\Editor\\\\Unity.exe\" -batchmode -quit -nographics -projectPath \"D:\\\\Dev\\\\Mosis\\\\MosisVR\" -executeMethod BuildScript.BuildAndroidCI -outputPath \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\Android\\\\MosisVR.apk\" -logFile \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\build3.log\")",
"Bash(gradlew.bat assembleRelease)",
"Bash(\"C:\\\\Program Files\\\\Unity\\\\Hub\\\\Editor\\\\6000.3.2f1\\\\Editor\\\\Unity.exe\" -batchmode -quit -nographics -projectPath \"D:\\\\Dev\\\\Mosis\\\\MosisVR\" -executeMethod BuildScript.BuildAndroidDirectCI -outputPath \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\Android\\\\MosisVR-direct.apk\" -logFile \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\build-direct.log\")",
"Bash(gradlew assembleDebug:*)",
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -archive -archivedirectory=\"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unreal\" -noP4)",
"Bash(adb:*)",
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/RunUAT.bat\" BuildCookRun -project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4)",
"Bash(MSYS_NO_PATHCONV=1 adb:*)",
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/Build.bat\" MosisUnrealEditor Win64 Development -Project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\")",
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/Build.bat\" MosisUnreal Android Development -Project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\")",
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/RunUAT.bat\" BuildCookRun -project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4 -utf8output)",
"Bash(git -C \"D:/Dev/Mosis/MosisUnreal\" add Plugins/MosisSDK/MosisSDK.uplugin Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp Plugins/MosisSDK/Source/MosisSDK/Private/MosisPointerComponent.cpp Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h)"
],
"additionalDirectories": [
"D:\\Dev\\Mosis\\MosisUnreal",
"D:\\Dev\\Mosis\\MosisVR"
]
}
}

21
.gitignore vendored
View File

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

View File

@@ -1,96 +0,0 @@
# MosisService Architecture
## Overview
MosisService is an Android application that combines Kotlin UI components with native C++ libraries for UI rendering and system interaction. The architecture is built around a service-oriented design using Android's Binder system and integrates with RmlUi for rich UI rendering.
## Core Components
### 1. Service Layer (mosis-service)
- **Purpose**: Main Android Binder service implementation
- **Interface**: Implements `IMosisService.aidl`
- **Functionality**:
- Touch event processing (onTouchDown, onTouchMove, onTouchUp)
- Service initialization and listener management
- Integration with kernel-based rendering system
- Asset loading through Android AssetManager
### 2. Rendering Layer (mosis-test)
- **Purpose**: UI rendering and testing infrastructure
- **Interface**: Implements `IMosisListener.aidl`
- **Functionality**:
- OpenGL ES 2.0 rendering pipeline using GLAD
- Multi-threaded rendering with EGL context management
- Surface and buffer handling for Android native windows
- Task queue management for asynchronous rendering operations
- Hardware buffer management and processing
### 3. Core Engine (Kernel)
- **Purpose**: Central rendering and event processing engine
- **Components**:
- RmlUi-based UI rendering engine
- EGL context creation and management
- Render target handling
- Touch event processing and UI updates
- Multi-threaded execution using std::thread
### 4. Supporting Libraries
- **AssetsManager**: Asset loading from Android AssetManager
- **Logger**: Cross-platform logging system
- **EGL Context**: OpenGL ES context management
- **Render Target**: Framebuffer and buffer management
- **Shader**: OpenGL shader program handling
- **External Texture**: Hardware buffer texture creation
## System Architecture
### Android Binder Integration
- Uses AIDL (Android Interface Definition Language) for cross-process communication
- Service exposes `IMosisService` interface for touch events and initialization
- Listener pattern with `IMosisListener` for rendering callbacks
- Binds to Java/Kotlin components through JNI
### Multi-threading Model
- Service layer runs in main thread for event handling
- Rendering loop runs in dedicated thread managed by Kernel
- Async task processing for UI updates
- Thread-safe communication between components
### Rendering Pipeline
1. **Initialization**: Service connects to test layer, creates EGL context
2. **Buffer Management**: Hardware buffers allocated and shared between layers
3. **Event Processing**: Touch events processed and forwarded to Kernel
4. **Rendering Loop**: Continuous rendering with frame synchronization
5. **UI Updates**: RmlUi engine updates UI based on events and data
### Data Flow
```
User Touch Events → IMosisService.onTouch* → Kernel.process → RmlUi UI Updates
Service Initialized → IMosisListener.onServiceInitialized → Rendering Setup
Buffer Available → IMosisListener.onBufferAvailable → Texture Creation
Frame Available → IMosisListener.onFrameAvailable → Frame Rendering
```
## Key Technologies
- **Android NDK**: Native development with C++23
- **CMake**: Build system for native libraries
- **RmlUi**: UI rendering engine with HTML/CSS-like markup
- **OpenGL ES 2.0**: Graphics rendering
- **GLAD**: OpenGL loader library
- **AIDL**: Inter-process communication
- **Binder**: Android's IPC mechanism
## File Structure
- `mosis-service.cpp`: Main service implementation
- `mosis-test.cpp`: Test/rendering implementation
- `kernel.cpp`: Core rendering engine and event processing
- `assets_manager.cpp`: Asset loading utilities
- `egl_context.cpp`: EGL context management
- `render_target.cpp`: Framebuffer handling
- `logger.cpp`: Cross-platform logging
## Code Standards
- Modern C++23 with smart pointers and RAII
- Thread-safe operations using std::mutex
- Resource management with proper cleanup
- Use of std::span, std::format for modern features
- Cross-platform compatibility through Android NDK

1254
CLAUDE.md

File diff suppressed because it is too large Load Diff

3569
SANDBOX.md

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,14 @@
<link type="text/rcss" href="../../ui/html.rcss"/> <link type="text/rcss" href="../../ui/html.rcss"/>
<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"/>
<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="camera.lua"></script>
<title>Camera</title> <title>Camera</title>
<style> <style>
.camera-screen { .camera-screen {
width: 100%;
height: 100%;
background-color: #000000; background-color: #000000;
display: flex;
flex-direction: column;
} }
/* Top Controls */ /* Top Controls */
@@ -21,16 +20,16 @@
align-items: center; align-items: center;
padding: 16px; padding: 16px;
position: absolute; position: absolute;
top: 0; top: 36px;
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; z-index: 10;
} }
.camera-btn { .camera-btn {
width: 56px; width: 48px;
height: 56px; height: 48px;
border-radius: 28px; border-radius: 24px;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
@@ -42,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: 32px; width: 28px;
height: 32px; height: 28px;
pointer-events: none;
} }
/* Viewfinder */ /* Viewfinder */
@@ -66,7 +70,6 @@
position: relative; position: relative;
} }
/* Placeholder for camera feed - replace with shared texture */
.viewfinder-placeholder { .viewfinder-placeholder {
color: #666666; color: #666666;
font-size: 18px; font-size: 18px;
@@ -136,10 +139,10 @@
} }
.camera-mode { .camera-mode {
font-size: 18px; font-size: 16px;
color: #B3B3B3; color: #B3B3B3;
cursor: pointer; cursor: pointer;
padding: 10px; padding: 8px;
} }
.camera-mode.active { .camera-mode.active {
@@ -156,7 +159,7 @@
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
padding: 24px 32px; padding: 20px 32px;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
} }
@@ -172,10 +175,15 @@
justify-content: center; justify-content: center;
} }
.gallery-preview:hover {
background-color: #444444;
}
.gallery-preview img { .gallery-preview img {
width: 32px; width: 28px;
height: 32px; height: 28px;
opacity: 0.7; opacity: 0.7;
pointer-events: none;
} }
.capture-btn { .capture-btn {
@@ -206,10 +214,17 @@
background-color: #FFFFFF; background-color: #FFFFFF;
} }
.capture-btn.video .capture-btn-inner { .capture-btn-video {
width: 28px; width: 24px;
height: 28px; height: 24px;
border-radius: 6px; border-radius: 12px;
background-color: #F44336;
}
.capture-btn-stop {
width: 24px;
height: 24px;
border-radius: 4px;
background-color: #F44336; background-color: #F44336;
} }
@@ -229,51 +244,51 @@
} }
.switch-camera-btn img { .switch-camera-btn img {
width: 32px; width: 28px;
height: 32px; height: 28px;
pointer-events: none;
} }
/* Settings Overlay */ /* Indicators */
.settings-value { .indicator {
position: absolute; position: absolute;
bottom: 200px; background-color: rgba(0, 0, 0, 0.4);
padding: 6px 12px;
border-radius: 12px;
font-size: 14px;
color: #FFFFFF;
}
.flash-indicator {
top: 100px;
left: 16px;
}
.timer-indicator {
top: 100px;
right: 16px;
}
.recording-indicator {
top: 140px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.6); display: none;
padding: 10px 18px; align-items: center;
border-radius: 22px; gap: 8px;
font-size: 18px;
color: #FFFFFF;
} }
/* Flash modes */ .recording-dot {
.flash-indicator { width: 12px;
position: absolute; height: 12px;
top: 80px; border-radius: 6px;
left: 16px; background-color: #F44336;
background-color: rgba(0, 0, 0, 0.4);
padding: 8px 14px;
border-radius: 14px;
font-size: 16px;
color: #FFFFFF;
} }
/* Timer indicator */ /* Zoom control */
.timer-indicator {
position: absolute;
top: 80px;
right: 16px;
background-color: rgba(0, 0, 0, 0.4);
padding: 8px 14px;
border-radius: 14px;
font-size: 16px;
color: #FFFFFF;
}
/* Zoom slider */
.zoom-control { .zoom-control {
position: absolute; position: absolute;
bottom: 180px; bottom: 200px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
@@ -282,20 +297,24 @@
} }
.zoom-btn { .zoom-btn {
width: 44px; width: 40px;
height: 44px; height: 40px;
border-radius: 22px; border-radius: 20px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 22px; font-size: 20px;
color: #FFFFFF; color: #FFFFFF;
cursor: pointer; cursor: pointer;
} }
.zoom-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.zoom-level { .zoom-level {
font-size: 18px; font-size: 16px;
color: #FFD700; color: #FFD700;
font-weight: 600; font-weight: 600;
min-width: 48px; min-width: 48px;
@@ -303,20 +322,30 @@
} }
</style> </style>
</head> </head>
<body class="camera-screen"> <body class="app-screen camera-screen" onload="initLayout(document); initCamera(document)">
<!-- System Status Bar (transparent) -->
<div class="system-status-bar" style="background-color: transparent; position: absolute; top: 0; left: 0; right: 0; z-index: 20;">
<span id="status-time" class="system-status-time">12:30</span>
<div class="system-status-icons">
<img src="../../icons/wifi.tga"/>
<img src="../../icons/signal.tga"/>
<img src="../../icons/battery.tga"/>
</div>
</div>
<!-- Top Bar --> <!-- Top Bar -->
<div class="camera-top-bar"> <div class="camera-top-bar">
<div class="camera-btn" onclick="goBack()"> <div class="camera-btn" onclick="goBack()">
<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>
@@ -325,11 +354,10 @@
<!-- Viewfinder Area --> <!-- Viewfinder Area -->
<div class="viewfinder-container"> <div class="viewfinder-container">
<div class="viewfinder" id="camera-viewfinder"> <div class="viewfinder" id="camera-viewfinder">
<!-- This is where the shared camera texture would be rendered --> <div class="viewfinder-placeholder" id="viewfinder-placeholder">
<div class="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: 16px; margin-top: 8px; color: #555555;"> <div style="font-size: 14px; margin-top: 8px; color: #555555;">
Tap to focus Tap to focus
</div> </div>
</div> </div>
@@ -343,41 +371,43 @@
</div> </div>
<!-- Focus Indicator --> <!-- Focus Indicator -->
<div class="focus-indicator"></div> <div class="focus-indicator" id="focus-indicator"></div>
</div> </div>
<!-- Flash Indicator --> <!-- Indicators -->
<div class="flash-indicator">Flash: Auto</div> <div class="indicator flash-indicator" id="flash-indicator">Flash: Auto</div>
<div class="indicator timer-indicator" id="timer-indicator">Timer: Off</div>
<!-- Timer Indicator --> <div class="indicator recording-indicator" id="recording-indicator">
<div class="timer-indicator">Timer: Off</div> <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,19 @@
{
"id": "com.mosis.camera",
"name": "Camera",
"version": "1.0.0",
"version_code": 1,
"entry": "camera.rml",
"icon": "../../icons/camera.tga",
"description": "Camera and photo capture",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"is_system_app": true,
"permissions": [
"camera",
"storage"
],
"min_api_version": 1
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
<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="home.lua"></script>
<title>Virtual Smartphone - Home</title> <title>Virtual Smartphone - Home</title>
<style> <style>
.home-screen { .home-screen {
@@ -34,9 +35,44 @@
.status-bar-icons img { .status-bar-icons img {
pointer-events: none; pointer-events: none;
} }
/* Third-party apps section */
.app-grid-section {
display: flex;
flex-wrap: wrap;
width: 100%;
}
/* Third-party apps use same sizing as system apps */
#third-party-apps .app-icon {
width: 25%;
box-sizing: border-box;
}
#third-party-apps .app-icon-image {
width: 72px;
height: 72px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px auto;
cursor: pointer;
}
#third-party-apps .app-icon-image:hover {
transform: scale(1.05);
}
#third-party-apps .app-icon-label {
display: block;
text-align: center;
font-size: 16px;
color: #FFFFFF;
}
</style> </style>
</head> </head>
<body class="home-screen"> <body class="home-screen" onload="initHome(document)">
<!-- Status Bar --> <!-- Status Bar -->
<div class="status-bar"> <div class="status-bar">
<span class="status-bar-time">12:30</span> <span class="status-bar-time">12:30</span>
@@ -121,6 +157,11 @@
<div class="app-icon-image" style="background-color: #673AB7;"><img src="../../icons/weather.tga"/></div> <div class="app-icon-image" style="background-color: #673AB7;"><img src="../../icons/weather.tga"/></div>
<span class="app-icon-label">Weather</span> <span class="app-icon-label">Weather</span>
</div> </div>
<!-- Third-party apps (dynamically populated by home.lua) -->
<div id="third-party-apps" class="app-grid-section">
<!-- Apps will be rendered here by home.lua -->
</div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -0,0 +1,223 @@
-- Sandbox Test App
-- Tests: timers, JSON, crypto, storage
local results = {}
local logCounter = 0
-- Helper to get document (use global set by C++)
local function getDocument()
-- The C++ code sets 'document' global after loading
return document
end
local function log(msg)
logCounter = logCounter + 1
table.insert(results, string.format("[%03d] %s", logCounter, msg))
local doc = getDocument()
if doc then
local el = doc:GetElementById("results")
if el then
el.inner_rml = table.concat(results, "\n")
end
end
end
-- Navigation helper
function goBack()
if navigation and navigation.back then
navigation.back()
else
log("Navigation not available")
end
end
local function setStatus(id, status, success)
local doc = getDocument()
if not doc then
print("[LUA] ERROR: document not available!")
return
end
local el = doc:GetElementById(id)
if el then
if success then
el.inner_rml = "&#x2713; " .. status
else
el.inner_rml = "&#x2717; " .. status
end
else
print("[LUA] ERROR: element not found: " .. id)
end
end
-- Timer test
function testTimer()
print("[LUA] testTimer called!")
setStatus("timer-status", "Running...", true)
log("Starting timer test...")
local count = 0
local timerId = nil
timerId = setInterval(function()
count = count + 1
log("Timer tick: " .. count)
if count >= 3 then
clearInterval(timerId)
setStatus("timer-status", "Passed (3 ticks)", true)
log("Timer test complete!")
end
end, 1000)
log("Timer started with ID: " .. tostring(timerId))
end
-- JSON test
function testJSON()
log("Starting JSON test...")
local success = true
local msg = ""
-- Test encode
local data = {
name = "test",
value = 42,
nested = { a = 1, b = 2 }
}
local encoded = json.encode(data)
if encoded then
log("Encoded: " .. encoded)
else
success = false
msg = "encode failed"
end
-- Test decode
if success then
local decoded = json.decode(encoded)
if decoded and decoded.name == "test" and decoded.value == 42 then
log("Decoded successfully, name=" .. decoded.name)
else
success = false
msg = "decode failed"
end
end
if success then
setStatus("json-status", "Passed", true)
log("JSON test complete!")
else
setStatus("json-status", "Failed: " .. msg, false)
end
end
-- Crypto test
function testCrypto()
log("Starting crypto test...")
local success = true
local msg = ""
-- Test random bytes
local bytes = crypto.randomBytes(16)
if bytes and #bytes == 16 then
log("Random bytes (hex): " .. bytes:gsub(".", function(c)
return string.format("%02x", c:byte())
end))
else
success = false
msg = "randomBytes failed"
end
-- Test SHA256
if success then
local hash = crypto.hash("sha256", "hello world")
if hash then
log("SHA256: " .. hash:sub(1, 32) .. "...")
else
success = false
msg = "sha256 failed"
end
end
-- Test HMAC
if success then
local hmac = crypto.hmac("sha256", "secret", "message")
if hmac then
log("HMAC: " .. hmac:sub(1, 32) .. "...")
else
success = false
msg = "hmac failed"
end
end
if success then
setStatus("crypto-status", "Passed", true)
log("Crypto test complete!")
else
setStatus("crypto-status", "Failed: " .. msg, false)
end
end
-- Storage test
function testStorage()
log("Starting storage test...")
local success = true
local msg = ""
-- Test write (VirtualFS requires /data/, /cache/, /temp/, or /shared/ prefix)
local writeOk = fs.write("/data/test.txt", "Hello from sandbox!")
if writeOk then
log("Write successful")
else
success = false
msg = "write failed"
end
-- Test read
if success then
local content = fs.read("/data/test.txt")
if content == "Hello from sandbox!" then
log("Read successful: " .. content)
else
success = false
msg = "read mismatch"
end
end
-- Test list
if success then
local files = fs.list("/data")
if files then
log("Files in /data: " .. #files)
for _, f in ipairs(files) do
log(" - " .. f)
end
end
end
-- Test delete
if success then
local deleteOk = fs.delete("/data/test.txt")
if deleteOk then
log("Delete successful")
else
success = false
msg = "delete failed"
end
end
if success then
setStatus("storage-status", "Passed", true)
log("Storage test complete!")
else
setStatus("storage-status", "Failed: " .. msg, false)
end
end
-- Initialize
log("Sandbox Test App loaded")
log("Lua version: " .. (_VERSION or "unknown"))

Binary file not shown.

View File

@@ -0,0 +1,46 @@
<rml>
<head>
<title>Sandbox Test</title>
<link type="text/rcss" href="styles.rcss"/>
<script src="app.lua"></script>
</head>
<body>
<div class="app-bar">
<div class="app-bar-nav btn-icon" onclick="goHome()">
<span class="icon">&lt;</span>
</div>
<div class="app-bar-title">Sandbox Test</div>
</div>
<div class="content">
<div class="card">
<div class="card-title">Timer Test</div>
<div id="timer-status">Not started</div>
<button onclick="testTimer()">Start Timer</button>
</div>
<div class="card">
<div class="card-title">JSON Test</div>
<div id="json-status">Not tested</div>
<button onclick="testJSON()">Test JSON</button>
</div>
<div class="card">
<div class="card-title">Crypto Test</div>
<div id="crypto-status">Not tested</div>
<button onclick="testCrypto()">Test Crypto</button>
</div>
<div class="card">
<div class="card-title">Storage Test</div>
<div id="storage-status">Not tested</div>
<button onclick="testStorage()">Test Storage</button>
</div>
<div class="card">
<div class="card-title">Results</div>
<div id="results">Click buttons above to run tests</div>
</div>
</div>
</body>
</rml>

View File

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

View File

@@ -0,0 +1,18 @@
{
"id": "com.mosis.sandbox-test",
"name": "Sandbox Test",
"version": "1.0.0",
"version_code": 1,
"entry": "main.rml",
"icon": "icon.tga",
"description": "Tests sandbox APIs: timers, storage, JSON, crypto",
"developer": {
"name": "Mosis Team",
"email": "dev@mosis.dev"
},
"permissions": [
"storage",
"network"
],
"min_api_version": 1
}

View File

@@ -0,0 +1,97 @@
body {
font-family: LatoLatin;
font-size: 16dp;
background-color: #121212;
color: #ffffff;
width: 100%;
height: 100%;
}
.app-bar {
display: flex;
flex-direction: row;
align-items: center;
height: 56dp;
background-color: #1e1e1e;
padding: 0 8dp;
}
.app-bar-nav {
width: 40dp;
height: 40dp;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20dp;
}
.app-bar-nav:hover {
background-color: #333333;
}
.icon {
font-size: 24dp;
}
.app-bar-title {
font-size: 20dp;
font-weight: bold;
margin-left: 16dp;
}
.content {
display: block;
padding: 16dp;
width: auto;
box-sizing: border-box;
}
.card {
display: block;
background-color: #1e1e1e;
border-radius: 12dp;
padding: 16dp;
margin-bottom: 12dp;
}
.card-title {
display: block;
font-size: 18dp;
font-weight: bold;
margin-bottom: 8dp;
color: #bb86fc;
}
.card div {
display: block;
}
button {
display: block;
background-color: #bb86fc;
color: #000000;
border-width: 0;
border-radius: 8dp;
padding: 12dp 24dp;
font-size: 14dp;
font-weight: bold;
margin-top: 8dp;
}
button:hover {
background-color: #cf9fff;
}
button:active {
background-color: #9a67ea;
}
#results {
font-family: LatoLatin;
font-size: 12dp;
background-color: #0d0d0d;
padding: 12dp;
border-radius: 8dp;
white-space: pre-wrap;
color: #00ff00;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,394 @@
-- store.lua - App Store system app logic
-- Milestone 10: Device-Side App Management
-- State
local state = {
screen = "home", -- home, games, updates, search, detail
installed = {}, -- Installed apps from mosis.apps
updates = {}, -- Available updates
featured = {}, -- Featured apps from store API
categories = {}, -- Category list
search_query = "", -- Current search
selected_app = nil, -- Selected app for detail view
is_loading = false,
error_message = nil
}
-- Store API configuration
local STORE_API = "https://portal.mosis.dev/store"
-- ============================================================================
-- Initialization
-- ============================================================================
function init()
print("[Store] Initializing...")
-- Load installed apps
refreshInstalledApps()
-- Check for updates
checkForUpdates()
-- Fetch featured apps (async)
fetchFeaturedApps()
end
function refreshInstalledApps()
if mosis and mosis.apps then
state.installed = mosis.apps.getInstalled() or {}
print("[Store] Loaded " .. #state.installed .. " installed apps")
else
print("[Store] Warning: mosis.apps API not available")
state.installed = {}
end
end
function checkForUpdates()
if mosis and mosis.apps then
state.updates = mosis.apps.checkUpdates() or {}
print("[Store] Found " .. #state.updates .. " updates")
updateBadge()
end
end
function updateBadge()
-- Update the updates tab badge
local badge = document:GetElementById("updates-badge")
if badge then
if #state.updates > 0 then
badge.inner_rml = tostring(#state.updates)
badge.style.display = "block"
else
badge.style.display = "none"
end
end
end
-- ============================================================================
-- API Calls
-- ============================================================================
function fetchFeaturedApps()
state.is_loading = true
-- TODO: Make HTTP request to STORE_API
-- For now, use placeholder data
state.featured = {
{
id = "com.mosis.weather",
name = "Weather Pro",
category = "Weather",
rating = 4.8,
downloads = 125000,
size = 15728640, -- 15 MB
description = "Beautiful forecasts for your virtual world",
icon = "W",
color = "#2196F3"
},
{
id = "com.mosis.notes",
name = "Notes",
category = "Productivity",
rating = 4.7,
downloads = 89000,
size = 8388608, -- 8 MB
description = "Simple note-taking app",
icon = "N",
color = "#03DAC6"
}
}
state.is_loading = false
render()
end
function searchApps(query)
state.search_query = query
state.screen = "search"
if query == "" then
state.screen = "home"
render()
return
end
state.is_loading = true
render()
-- TODO: Make HTTP request to STORE_API/search
-- For now, filter featured apps
local results = {}
local lower_query = query:lower()
for _, app in ipairs(state.featured) do
if app.name:lower():find(lower_query) or
app.category:lower():find(lower_query) then
table.insert(results, app)
end
end
state.search_results = results
state.is_loading = false
render()
end
-- ============================================================================
-- Installation
-- ============================================================================
function installApp(app_id, download_url, signature)
print("[Store] Installing: " .. app_id)
showProgress(app_id)
if mosis and mosis.apps then
mosis.apps.install(download_url or "", signature or "", function(progress)
updateProgress(progress)
if progress.stage == "complete" then
hideProgress()
showToast("App installed successfully!")
refreshInstalledApps()
render()
elseif progress.stage == "failed" then
hideProgress()
showError("Installation failed: " .. (progress.error or "Unknown error"))
end
end)
else
hideProgress()
showError("App installation not available")
end
end
function uninstallApp(package_id)
print("[Store] Uninstalling: " .. package_id)
if mosis and mosis.apps then
local success = mosis.apps.uninstall(package_id)
if success then
showToast("App uninstalled")
refreshInstalledApps()
render()
else
showError("Failed to uninstall app")
end
end
end
function openApp(package_id)
print("[Store] Launching: " .. package_id)
if mosis and mosis.apps then
mosis.apps.launch(package_id)
end
end
function updateApp(package_id)
print("[Store] Updating: " .. package_id)
-- Find update info
for _, update in ipairs(state.updates) do
if update.package_id == package_id then
installApp(package_id, update.download_url, update.signature)
return
end
end
showError("No update available for this app")
end
function updateAllApps()
print("[Store] Updating all apps...")
for _, update in ipairs(state.updates) do
-- Queue updates (in a real implementation, this would be sequential)
installApp(update.package_id, update.download_url, update.signature)
end
end
-- ============================================================================
-- UI Helpers
-- ============================================================================
function isInstalled(package_id)
for _, app in ipairs(state.installed) do
if app.package_id == package_id then
return true
end
end
return false
end
function hasUpdate(package_id)
for _, update in ipairs(state.updates) do
if update.package_id == package_id then
return true
end
end
return false
end
function formatSize(bytes)
if bytes >= 1048576 then
return string.format("%.1f MB", bytes / 1048576)
elseif bytes >= 1024 then
return string.format("%.0f KB", bytes / 1024)
else
return bytes .. " B"
end
end
function formatDownloads(count)
if count >= 1000000 then
return string.format("%.1fM", count / 1000000)
elseif count >= 1000 then
return string.format("%.0fK", count / 1000)
else
return tostring(count)
end
end
-- ============================================================================
-- Progress Dialog
-- ============================================================================
function showProgress(app_name)
local dialog = document:GetElementById("progress-dialog")
if dialog then
dialog.style.display = "flex"
local title = document:GetElementById("progress-title")
if title then
title.inner_rml = "Installing " .. (app_name or "App")
end
end
end
function updateProgress(progress)
local bar = document:GetElementById("progress-bar")
if bar then
bar.style.width = (progress.progress * 100) .. "%"
end
local status = document:GetElementById("progress-status")
if status then
local stage_names = {
downloading = "Downloading...",
verifying = "Verifying...",
extracting = "Extracting...",
registering = "Registering...",
complete = "Complete!",
failed = "Failed"
}
status.inner_rml = stage_names[progress.stage] or progress.stage
end
end
function hideProgress()
local dialog = document:GetElementById("progress-dialog")
if dialog then
dialog.style.display = "none"
end
end
-- ============================================================================
-- Toast/Error Messages
-- ============================================================================
function showToast(message)
local toast = document:GetElementById("toast")
if toast then
toast.inner_rml = message
toast.style.display = "block"
-- Auto-hide after 3 seconds (would need timer API)
end
print("[Store] Toast: " .. message)
end
function showError(message)
state.error_message = message
local error_el = document:GetElementById("error-dialog")
if error_el then
local msg = document:GetElementById("error-message")
if msg then
msg.inner_rml = message
end
error_el.style.display = "flex"
end
print("[Store] Error: " .. message)
end
function hideError()
state.error_message = nil
local error_el = document:GetElementById("error-dialog")
if error_el then
error_el.style.display = "none"
end
end
-- ============================================================================
-- Navigation
-- ============================================================================
function showHome()
state.screen = "home"
setActiveTab("apps")
render()
end
function showGames()
state.screen = "games"
setActiveTab("games")
render()
end
function showUpdates()
state.screen = "updates"
setActiveTab("updates")
checkForUpdates()
render()
end
function showSearch()
state.screen = "search"
render()
end
function showAppDetail(app_id)
state.screen = "detail"
-- Find app in featured or installed
for _, app in ipairs(state.featured) do
if app.id == app_id then
state.selected_app = app
break
end
end
render()
end
function setActiveTab(tab)
local tabs = {"apps", "games", "updates"}
for _, t in ipairs(tabs) do
local el = document:GetElementById("nav-" .. t)
if el then
if t == tab then
el:SetClass("active", true)
else
el:SetClass("active", false)
end
end
end
end
-- ============================================================================
-- Rendering
-- ============================================================================
function render()
-- The RML is mostly static with dynamic data binding
-- In a full implementation, we'd update innerHTML of content areas
print("[Store] Rendering screen: " .. state.screen)
end
-- Initialize on load
init()

View File

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

21
base-apps/package.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
REM Package test apps as .mosis files
setlocal enabledelayedexpansion
for /d %%d in (*) do (
if exist "%%d\manifest.json" (
echo Packaging %%d...
cd %%d
if exist "..\%%d.mosis" del "..\%%d.mosis"
tar -a -cf "..\%%d.mosis" *
cd ..
echo Created %%d.mosis
)
)
echo.
echo Done! Package files:
dir /b *.mosis 2>nul
endlocal

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

111
core/CMakeLists.txt Normal file
View File

@@ -0,0 +1,111 @@
cmake_minimum_required(VERSION 3.18)
project(mosis-core VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Core library sources - portable sandbox APIs
set(MOSIS_CORE_SOURCES
# Utility
src/logger.cpp
# Sandbox APIs (portable)
src/sandbox/timer_manager.cpp
src/sandbox/json_api.cpp
src/sandbox/crypto_api.cpp
src/sandbox/virtual_fs.cpp
src/sandbox/lua_sandbox.cpp
src/sandbox/permission_gate.cpp
src/sandbox/audit_log.cpp
src/sandbox/rate_limiter.cpp
src/sandbox/path_sandbox.cpp
)
# Optional sources that require additional dependencies
if(MOSIS_ENABLE_DATABASE)
list(APPEND MOSIS_CORE_SOURCES src/sandbox/database_manager.cpp)
endif()
if(MOSIS_ENABLE_NETWORK)
list(APPEND MOSIS_CORE_SOURCES
src/sandbox/network_manager.cpp
src/sandbox/http_validator.cpp
)
endif()
# Create static library
add_library(mosis-core STATIC ${MOSIS_CORE_SOURCES})
# Include directories
target_include_directories(mosis-core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/mosis>
$<INSTALL_INTERFACE:include>
)
# Also add internal include path for relative includes within library
target_include_directories(mosis-core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/sandbox
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/util
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/apps
)
# Platform-specific definitions
if(ANDROID)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_ANDROID)
elseif(WIN32)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_WINDOWS)
# Windows crypto library
target_link_libraries(mosis-core PRIVATE bcrypt)
elseif(APPLE)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_MACOS)
else()
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_LINUX)
endif()
# OpenSSL for crypto on non-Windows platforms
if(NOT WIN32)
find_package(OpenSSL QUIET)
if(OpenSSL_FOUND)
target_link_libraries(mosis-core PRIVATE OpenSSL::Crypto)
target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_OPENSSL)
else()
message(WARNING "OpenSSL not found - crypto functions will be stubs")
endif()
endif()
# Lua is required - parent project must provide it
if(TARGET lua_static)
target_link_libraries(mosis-core PUBLIC lua_static)
elseif(TARGET lua)
target_link_libraries(mosis-core PUBLIC lua)
else()
# Try to find Lua
find_package(Lua QUIET)
if(LUA_FOUND)
target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR})
target_link_libraries(mosis-core PUBLIC ${LUA_LIBRARIES})
else()
message(WARNING "Lua not found - parent project must provide Lua target")
endif()
endif()
# JSON library (nlohmann/json - header only)
if(TARGET nlohmann_json::nlohmann_json)
target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json)
else()
find_package(nlohmann_json QUIET)
if(nlohmann_json_FOUND)
target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json)
endif()
endif()
# SQLite for database_manager (optional)
if(MOSIS_ENABLE_DATABASE)
find_package(SQLite3 REQUIRED)
target_link_libraries(mosis-core PRIVATE SQLite::SQLite3)
target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_SQLITE)
endif()
# Export compile commands for IDE support
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

View File

@@ -0,0 +1,23 @@
// app_api.h - Lua API bindings for app management
// Milestone 10: Device-Side App Management
#pragma once
#include <string>
struct lua_State;
namespace mosis {
class AppManager;
class UpdateService;
// Register Lua APIs for app management
// - mosis.apps.* - System apps only (App Store, Settings)
// - mosis.app.* - All apps (info about current app)
void RegisterAppAPIs(lua_State* L,
AppManager* app_manager,
UpdateService* update_service,
const std::string& current_app_id,
bool is_system_app);
} // namespace mosis

View File

@@ -0,0 +1,167 @@
// app_manager.h - App installation and management
// Milestone 10: Device-Side App Management
#pragma once
#include <string>
#include <vector>
#include <map>
#include <optional>
#include <functional>
#include <mutex>
#include <chrono>
#include <cstdint>
namespace mosis {
// Forward declarations
class LuaSandboxManager;
// Information about an installed app
struct InstalledApp {
std::string package_id;
std::string name;
std::string version_name;
int version_code = 0;
std::string install_path;
std::vector<std::string> permissions;
std::chrono::system_clock::time_point installed_at;
std::chrono::system_clock::time_point updated_at;
int64_t package_size = 0;
int64_t data_size = 0;
bool is_system_app = false;
std::string entry_point;
std::string icon_path;
std::string developer_name;
};
// Progress stages during installation
struct InstallProgress {
enum class Stage {
Downloading,
Verifying,
Extracting,
Registering,
Complete,
Failed
};
Stage stage = Stage::Downloading;
float progress = 0.0f; // 0.0 - 1.0
std::string error;
static const char* StageName(Stage s) {
switch (s) {
case Stage::Downloading: return "downloading";
case Stage::Verifying: return "verifying";
case Stage::Extracting: return "extracting";
case Stage::Registering: return "registering";
case Stage::Complete: return "complete";
case Stage::Failed: return "failed";
default: return "unknown";
}
}
};
using ProgressCallback = std::function<void(const InstallProgress&)>;
// Manifest parsed from package
struct AppManifest {
std::string id;
std::string name;
std::string version;
int version_code = 0;
std::string entry;
std::string icon;
std::string description;
std::string developer_name;
std::string developer_email;
std::vector<std::string> permissions;
int min_api_version = 1;
};
class AppManager {
public:
explicit AppManager(const std::string& data_root);
~AppManager();
// Prevent copying
AppManager(const AppManager&) = delete;
AppManager& operator=(const AppManager&) = delete;
// Installation from URL
bool Install(const std::string& package_url,
const std::string& signature,
ProgressCallback callback);
// Installation from local file
bool InstallFromFile(const std::string& package_path,
ProgressCallback callback);
// Uninstallation
bool Uninstall(const std::string& package_id, bool keep_data = false);
// Updates
bool Update(const std::string& package_id,
const std::string& package_url,
const std::string& signature,
ProgressCallback callback);
// Query installed apps
std::vector<InstalledApp> GetInstalledApps() const;
std::optional<InstalledApp> GetApp(const std::string& package_id) const;
bool IsInstalled(const std::string& package_id) const;
// Data management
int64_t GetAppDataSize(const std::string& package_id) const;
bool ClearAppData(const std::string& package_id);
bool ClearAppCache(const std::string& package_id);
bool BackupAppData(const std::string& package_id);
bool RestoreAppData(const std::string& package_id);
// App launching
bool LaunchApp(const std::string& package_id);
bool StopApp(const std::string& package_id);
bool IsAppRunning(const std::string& package_id) const;
// Integration with sandbox manager
void SetSandboxManager(LuaSandboxManager* manager);
// Get paths
std::string GetDataRoot() const { return m_data_root; }
std::string GetAppPath(const std::string& package_id) const;
std::string GetAppDataPath(const std::string& package_id) const;
std::string GetAppCachePath(const std::string& package_id) const;
// System apps registration
void RegisterSystemApp(const InstalledApp& app);
private:
// Package verification
bool VerifyPackage(const std::string& path);
bool VerifySignature(const std::string& path, const std::string& signature);
// Package operations
std::optional<AppManifest> ExtractManifest(const std::string& package_path);
bool ExtractPackage(const std::string& package_path, const std::string& dest_path);
// Download helper
bool DownloadFile(const std::string& url, const std::string& dest_path,
std::function<void(float)> progress_callback);
// Registry persistence
void LoadInstalledApps();
void SaveInstalledApps();
// Directory size calculation
int64_t CalculateDirectorySize(const std::string& path) const;
// Generate unique ID
std::string GenerateUUID() const;
std::string m_data_root;
LuaSandboxManager* m_sandbox_manager = nullptr;
mutable std::mutex m_mutex;
std::map<std::string, InstalledApp> m_installed_apps;
};
} // namespace mosis

View File

@@ -0,0 +1,57 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
namespace mosis {
/**
* Platform-agnostic interface for loading assets.
* Android implements this using AAssetManager.
* Desktop implements this using filesystem operations.
*/
class IAssetInterface {
public:
virtual ~IAssetInterface() = default;
/**
* Read entire file contents as bytes.
* @param path Relative path to asset (e.g., "apps/home/home.rml")
* @return File contents, or empty vector if not found
*/
virtual std::vector<uint8_t> ReadFile(const std::string& path) = 0;
/**
* Read entire file contents as string.
* @param path Relative path to asset
* @return File contents, or empty string if not found
*/
virtual std::string ReadFileString(const std::string& path) = 0;
/**
* Check if an asset exists.
* @param path Relative path to asset
* @return true if asset exists
*/
virtual bool Exists(const std::string& path) = 0;
/**
* List files in a directory.
* @param path Relative path to directory
* @return List of filenames (not full paths)
*/
virtual std::vector<std::string> ListDirectory(const std::string& path) = 0;
/**
* Get the absolute path for an asset (if applicable).
* On Android this may return empty as assets are in APK.
* @param path Relative path to asset
* @return Absolute path or empty string
*/
virtual std::string GetAbsolutePath(const std::string& path) = 0;
};
using AssetInterfacePtr = std::shared_ptr<IAssetInterface>;
} // namespace mosis

View File

@@ -0,0 +1,98 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <functional>
namespace mosis {
/**
* Platform-agnostic interface for filesystem operations.
* Used for app data storage, not assets.
*/
class IFilesystemInterface {
public:
virtual ~IFilesystemInterface() = default;
/**
* Get the base data directory for apps.
* Android: /data/data/com.omixlab.mosis/files/
* Desktop: ./data/ or configurable
*/
virtual std::string GetDataRoot() = 0;
/**
* Get the apps installation directory.
* Contains installed app packages.
*/
virtual std::string GetAppsDirectory() = 0;
/**
* Get app-specific data directory.
* @param app_id Application ID (e.g., "com.example.app")
*/
virtual std::string GetAppDataDirectory(const std::string& app_id) = 0;
/**
* Get app-specific cache directory.
*/
virtual std::string GetAppCacheDirectory(const std::string& app_id) = 0;
/**
* Create directory if it doesn't exist.
* @return true on success
*/
virtual bool CreateDirectory(const std::string& path) = 0;
/**
* Check if path exists.
*/
virtual bool Exists(const std::string& path) = 0;
/**
* Check if path is a directory.
*/
virtual bool IsDirectory(const std::string& path) = 0;
/**
* Remove file or directory.
* @param recursive If true, remove directory contents
*/
virtual bool Remove(const std::string& path, bool recursive = false) = 0;
/**
* Read file contents.
*/
virtual std::vector<uint8_t> ReadFile(const std::string& path) = 0;
/**
* Write file contents.
*/
virtual bool WriteFile(const std::string& path, const std::vector<uint8_t>& data) = 0;
/**
* List directory contents.
*/
virtual std::vector<std::string> ListDirectory(const std::string& path) = 0;
/**
* Get file size.
* @return Size in bytes, or -1 if not found
*/
virtual int64_t GetFileSize(const std::string& path) = 0;
/**
* Copy file.
*/
virtual bool CopyFile(const std::string& src, const std::string& dst) = 0;
/**
* Move/rename file.
*/
virtual bool MoveFile(const std::string& src, const std::string& dst) = 0;
};
using FilesystemInterfacePtr = std::shared_ptr<IFilesystemInterface>;
} // namespace mosis

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