add shell architecture with persistent status bar, nav bar, and content fragments
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
#include <RmlUi/Lua/LuaType.h>
|
#include <RmlUi/Lua/LuaType.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -65,8 +66,10 @@ static std::ofstream g_log_file;
|
|||||||
|
|
||||||
// Simulator mode
|
// Simulator mode
|
||||||
static bool g_simulator_mode = false;
|
static bool g_simulator_mode = false;
|
||||||
|
static bool g_shell_mode = false; // Use persistent shell instead of direct documents
|
||||||
static std::string g_test_apps_path; // Path to test-apps directory
|
static std::string g_test_apps_path; // Path to test-apps directory
|
||||||
static std::string g_simulator_home_path; // Path to simulator home.rml
|
static std::string g_simulator_home_path; // Path to simulator home.rml
|
||||||
|
static std::string g_shell_path; // Path to shell.rml for shell mode
|
||||||
static std::string g_main_assets_path; // Path to main assets (for goHome)
|
static std::string g_main_assets_path; // Path to main assets (for goHome)
|
||||||
static std::vector<mosis::AppInfo> g_discovered_apps;
|
static std::vector<mosis::AppInfo> g_discovered_apps;
|
||||||
static std::string g_current_app_id; // Currently running app (empty = home)
|
static std::string g_current_app_id; // Currently running app (empty = home)
|
||||||
@@ -284,7 +287,8 @@ static void PrintUsage() {
|
|||||||
<< " --log FILE Write all log messages to file\n"
|
<< " --log FILE Write all log messages to file\n"
|
||||||
<< " --hierarchy FILE Continuously dump UI hierarchy to JSON\n"
|
<< " --hierarchy FILE Continuously dump UI hierarchy to JSON\n"
|
||||||
<< "\nSimulator mode:\n"
|
<< "\nSimulator mode:\n"
|
||||||
<< " --simulator Run in simulator mode (app launcher)\n"
|
<< " --simulator Run in simulator mode (uses shell by default)\n"
|
||||||
|
<< " --no-shell Disable shell (use direct document loading)\n"
|
||||||
<< " --test-apps PATH Path to test-apps directory (default: ./test-apps)\n"
|
<< " --test-apps PATH Path to test-apps directory (default: ./test-apps)\n"
|
||||||
<< "\nTest modes:\n"
|
<< "\nTest modes:\n"
|
||||||
<< " --record FILE Record user actions to JSON file\n"
|
<< " --record FILE Record user actions to JSON file\n"
|
||||||
@@ -343,6 +347,9 @@ int main(int argc, char* argv[]) {
|
|||||||
g_test_output_path = argv[++i];
|
g_test_output_path = argv[++i];
|
||||||
} else if (arg == "--simulator") {
|
} else if (arg == "--simulator") {
|
||||||
g_simulator_mode = true;
|
g_simulator_mode = true;
|
||||||
|
g_shell_mode = true; // Shell mode is implicit in simulator mode
|
||||||
|
} else if (arg == "--no-shell") {
|
||||||
|
g_shell_mode = false; // Disable shell (use direct document loading)
|
||||||
} else if (arg == "--test-apps" && i + 1 < argc) {
|
} else if (arg == "--test-apps" && i + 1 < argc) {
|
||||||
g_test_apps_path = argv[++i];
|
g_test_apps_path = argv[++i];
|
||||||
} else if (arg[0] != '-') {
|
} else if (arg[0] != '-') {
|
||||||
@@ -393,11 +400,27 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override document path to main home screen
|
// Override document path to main home screen or shell
|
||||||
if (!g_simulator_home_path.empty()) {
|
if (!g_simulator_home_path.empty()) {
|
||||||
document_path = g_simulator_home_path;
|
|
||||||
// Also set the main assets path for proper resource loading
|
// Also set the main assets path for proper resource loading
|
||||||
g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string();
|
g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string();
|
||||||
|
|
||||||
|
// If shell mode, use shell instead of home
|
||||||
|
if (g_shell_mode) {
|
||||||
|
g_shell_path = (fs::path(g_main_assets_path) / "apps" / "shell" / "shell.rml").string();
|
||||||
|
if (fs::exists(g_shell_path)) {
|
||||||
|
document_path = g_shell_path;
|
||||||
|
std::cout << "Shell mode enabled" << std::endl;
|
||||||
|
std::cout << "Shell: " << g_shell_path << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Warning: Could not find shell.rml, falling back to direct mode" << std::endl;
|
||||||
|
document_path = g_simulator_home_path;
|
||||||
|
g_shell_mode = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document_path = g_simulator_home_path;
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "Simulator mode enabled" << std::endl;
|
std::cout << "Simulator mode enabled" << std::endl;
|
||||||
std::cout << "Test apps path: " << g_test_apps_path << std::endl;
|
std::cout << "Test apps path: " << g_test_apps_path << std::endl;
|
||||||
std::cout << "Home screen: " << g_simulator_home_path << std::endl;
|
std::cout << "Home screen: " << g_simulator_home_path << std::endl;
|
||||||
@@ -896,7 +919,50 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
|
|||||||
return 1;
|
return 1;
|
||||||
});
|
});
|
||||||
lua_setglobal(L, "switchAppSandbox");
|
lua_setglobal(L, "switchAppSandbox");
|
||||||
std::cout << "Registered Lua loadScreen, goHome, and switchAppSandbox functions" << std::endl;
|
|
||||||
|
// Register loadAppContent function for shell-based app loading
|
||||||
|
// loadAppContent(element, path) - loads RML content into an element's inner_rml
|
||||||
|
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||||
|
// Get element from first argument (RmlUi element userdata)
|
||||||
|
Rml::Element* element = Rml::Lua::LuaType<Rml::Element>::check(L, 1);
|
||||||
|
const char* path = luaL_checkstring(L, 2);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
std::cerr << "loadAppContent: Invalid element" << std::endl;
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path relative to assets directory
|
||||||
|
std::string full_path;
|
||||||
|
if (fs::path(path).is_absolute()) {
|
||||||
|
full_path = path;
|
||||||
|
} else {
|
||||||
|
full_path = (fs::path(g_main_assets_path) / path).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
std::ifstream file(full_path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "loadAppContent: Cannot open file: " << full_path << std::endl;
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
std::string content = buffer.str();
|
||||||
|
|
||||||
|
// Set as inner_rml
|
||||||
|
element->SetInnerRML(content);
|
||||||
|
|
||||||
|
Rml::Log::Message(Rml::Log::LT_INFO, "Loaded app content from: %s", path);
|
||||||
|
lua_pushboolean(L, true);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
lua_setglobal(L, "loadAppContent");
|
||||||
|
|
||||||
|
std::cout << "Registered Lua loadScreen, goHome, switchAppSandbox, and loadAppContent functions" << std::endl;
|
||||||
|
|
||||||
// Register simulator API (if in simulator mode)
|
// Register simulator API (if in simulator mode)
|
||||||
if (g_simulator_mode) {
|
if (g_simulator_mode) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
|
|||||||
| MosisService | ✅ Working | RmlUi rendering, touch input, navigation |
|
| MosisService | ✅ Working | RmlUi rendering, touch input, navigation |
|
||||||
| App Management | ✅ Working | Install/uninstall apps, sandbox integration |
|
| App Management | ✅ Working | Install/uninstall apps, sandbox integration |
|
||||||
| Lua Sandbox | ✅ Working | 149 security tests passing |
|
| Lua Sandbox | ✅ Working | 149 security tests passing |
|
||||||
| Desktop Designer | ✅ Working | Hot-reload, hierarchy dump, recording |
|
| Desktop Designer | ✅ Working | Hot-reload, shell mode, hierarchy dump, recording |
|
||||||
| Designer Tests | ✅ 5/5 Passing | Navigation tests automated |
|
| Designer Tests | ✅ 5/5 Passing | Navigation tests automated |
|
||||||
| MosisVR (Unity) | ✅ Building | OpenGL backend working, Vulkan in progress |
|
| MosisVR (Unity) | ✅ Building | OpenGL backend working, Vulkan in progress |
|
||||||
| MosisUnreal | ✅ Working | Vulkan texture import via UE5 RHI, phone actor with mesh |
|
| MosisUnreal | ✅ Working | Vulkan texture import via UE5 RHI, phone actor with mesh |
|
||||||
@@ -35,6 +35,7 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
|
|||||||
| Android Service | `src/main/` | Native service running RmlUi renderer |
|
| Android Service | `src/main/` | Native service running RmlUi renderer |
|
||||||
| App Management | `src/main/cpp/apps/` | App install/uninstall/launch with sandbox |
|
| App Management | `src/main/cpp/apps/` | App install/uninstall/launch with sandbox |
|
||||||
| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation (22 modules) |
|
| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation (22 modules) |
|
||||||
|
| System Shell | `src/main/assets/apps/shell/` | Persistent status bar, nav bar, overlays |
|
||||||
| Desktop Designer | `designer/` | UI development with hot-reload |
|
| Desktop Designer | `designer/` | UI development with hot-reload |
|
||||||
| Designer Tests | `designer-test/` | Automated UI testing framework |
|
| Designer Tests | `designer-test/` | Automated UI testing framework |
|
||||||
| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests (149 tests) |
|
| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests (149 tests) |
|
||||||
@@ -48,7 +49,7 @@ All detailed documentation is in `docs/`:
|
|||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| [BUILD-COMMANDS.md](BUILD-COMMANDS.md) | Android, Desktop Designer, and test build commands |
|
| [BUILD-COMMANDS.md](BUILD-COMMANDS.md) | Android, Desktop Designer, and test build commands |
|
||||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Native libraries, IPC flow, code structure |
|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Native libraries, IPC flow, code structure |
|
||||||
| [DESKTOP-DESIGNER.md](DESKTOP-DESIGNER.md) | Hot-reload, recording, key files |
|
| [DESKTOP-DESIGNER.md](DESKTOP-DESIGNER.md) | Shell architecture, hot-reload, recording |
|
||||||
| [TESTING-FRAMEWORK.md](TESTING-FRAMEWORK.md) | Automated UI testing, writing tests |
|
| [TESTING-FRAMEWORK.md](TESTING-FRAMEWORK.md) | Automated UI testing, writing tests |
|
||||||
| [UI-ASSETS.md](UI-ASSETS.md) | Asset structure, navigation system, element IDs |
|
| [UI-ASSETS.md](UI-ASSETS.md) | Asset structure, navigation system, element IDs |
|
||||||
| [MATERIAL-DESIGN.md](MATERIAL-DESIGN.md) | Icons, MDL components, usage guide |
|
| [MATERIAL-DESIGN.md](MATERIAL-DESIGN.md) | Icons, MDL components, usage guide |
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
The desktop designer (`designer/`) provides rapid UI development with:
|
The desktop designer (`designer/`) provides rapid UI development with:
|
||||||
|
|
||||||
- **Hot-reload**: Automatically reloads when RML/RCSS/Lua files change
|
- **Hot-reload**: Automatically reloads when RML/RCSS/Lua files change
|
||||||
|
- **Shell Mode**: Persistent system UI (status bar, nav bar) with apps loading into container
|
||||||
- **UI Hierarchy Dumping**: Exports element tree to JSON for inspection
|
- **UI Hierarchy Dumping**: Exports element tree to JSON for inspection
|
||||||
- **Screenshot Capture**: PNG export via F12 key
|
- **Screenshot Capture**: PNG export via F12 key
|
||||||
- **Logging**: Detailed output for debugging navigation and events
|
- **Logging**: Detailed output for debugging navigation and events
|
||||||
@@ -13,44 +14,57 @@ The desktop designer (`designer/`) provides rapid UI development with:
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `designer/src/main.cpp` | Main entry point, GLFW window, event loop |
|
| `designer/main.cpp` | Main entry point, GLFW window, event loop |
|
||||||
| `designer/src/desktop_kernel.cpp` | RmlUi context management, rendering |
|
| `designer/src/desktop_sandbox.cpp` | Per-app sandbox isolation |
|
||||||
|
| `designer/src/app_discovery.cpp` | Scans directories for app manifests |
|
||||||
| `designer/src/testing/ui_inspector.cpp` | UI hierarchy JSON export |
|
| `designer/src/testing/ui_inspector.cpp` | UI hierarchy JSON export |
|
||||||
| `designer/src/testing/visual_capture.cpp` | PNG screenshot capture and comparison |
|
| `designer/src/testing/visual_capture.cpp` | PNG screenshot capture |
|
||||||
| `designer/src/testing/action_recorder.cpp` | Record user interactions to JSON |
|
| `designer/src/testing/action_recorder.cpp` | Record user interactions |
|
||||||
| `designer/src/testing/action_player.cpp` | Playback recorded actions |
|
| `designer/src/testing/action_player.cpp` | Playback recorded actions |
|
||||||
| `designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp` | GLFW backend with input hooks |
|
|
||||||
|
|
||||||
## Command Line Options
|
## Command Line Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--simulator Enable simulator mode (shows home screen with third-party apps)
|
General:
|
||||||
--test-apps <path> Path to test-apps directory (default: auto-detect)
|
--resolution WxH Set window resolution (default: 540x960)
|
||||||
--assets <path> Set assets directory (default: derived from document)
|
--assets PATH Set assets directory (default: derived from document)
|
||||||
--log <path> Write logs to file
|
--log FILE Write all log messages to file
|
||||||
--hierarchy <path> Dump UI hierarchy JSON each frame
|
--hierarchy FILE Continuously dump UI hierarchy to JSON
|
||||||
--dump Single-shot dump mode (screenshot + hierarchy)
|
|
||||||
--record <path> Enable recording mode (F5 to start/stop)
|
Simulator mode:
|
||||||
--playback <path> Play back recorded actions from JSON
|
--simulator Run in simulator mode (uses shell by default)
|
||||||
--resolution WxH Set window resolution (default: 540x960)
|
--no-shell Disable shell (use direct document loading)
|
||||||
|
--test-apps PATH Path to test-apps directory (default: ./base-apps)
|
||||||
|
|
||||||
|
Test modes:
|
||||||
|
--record FILE Record user actions to JSON file
|
||||||
|
--playback FILE Playback actions from JSON file
|
||||||
|
--screenshot FILE Take screenshot and exit
|
||||||
|
--dump-hierarchy FILE Dump UI hierarchy to JSON and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the Designer
|
## Running the Designer
|
||||||
|
|
||||||
### Simulator Mode (Recommended for Testing Apps)
|
### Simulator Mode with Shell (Recommended)
|
||||||
|
|
||||||
To run the designer with third-party test apps visible:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd MosisService
|
cd MosisService
|
||||||
./designer/build/Release/mosis-designer.exe --simulator --test-apps test-apps
|
./designer/build/Release/mosis-designer.exe --simulator --test-apps base-apps
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
- Load the home screen from `src/main/assets/apps/home/home.rml`
|
- Load the **system shell** with persistent status bar and navigation bar
|
||||||
- Scan `test-apps/` for apps with valid `manifest.json`
|
- Load home screen content into the shell's app container
|
||||||
- Display discovered apps in the home screen grid
|
- Scan `base-apps/` for apps with valid `manifest.json`
|
||||||
- Enable the `mosis.apps` Lua API for app launching
|
- Enable app navigation with proper back button support
|
||||||
|
|
||||||
|
### Simulator Mode without Shell
|
||||||
|
|
||||||
|
To test the old direct-document approach:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./designer/build/Release/mosis-designer.exe --simulator --test-apps base-apps --no-shell
|
||||||
|
```
|
||||||
|
|
||||||
### Direct Document Mode
|
### Direct Document Mode
|
||||||
|
|
||||||
@@ -64,6 +78,144 @@ To load a specific RML document directly:
|
|||||||
|
|
||||||
| Key | Function |
|
| Key | Function |
|
||||||
|-----|----------|
|
|-----|----------|
|
||||||
| F5 | Start/stop recording (when --record is enabled) |
|
| F5 | Reload document / Start-stop recording |
|
||||||
| F6 | Pause/resume playback (when --playback is enabled) |
|
| F12 | Toggle RmlUi debugger |
|
||||||
| F12 | Take screenshot |
|
| ESC | Back navigation |
|
||||||
|
| R | Start/Stop recording (in interactive mode) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shell Architecture
|
||||||
|
|
||||||
|
The shell (`apps/shell/`) provides a persistent system UI layer that prevents apps from taking over the full screen.
|
||||||
|
|
||||||
|
### Shell Components
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ Status Bar (36px) │ ← Time, WiFi, Signal, Battery
|
||||||
|
├──────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ App Container │ ← App content loads here
|
||||||
|
│ (flex: 1) │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────┤
|
||||||
|
│ Navigation Bar (56px) │ ← Back, Home, Recents
|
||||||
|
└──────────────────────────────┘
|
||||||
|
|
||||||
|
Overlay Layers (z-index order):
|
||||||
|
- Dialog overlay (z: 600) ← Modal dialogs
|
||||||
|
- Toast container (z: 500) ← Toast notifications
|
||||||
|
- Notification panel (z: 400) ← Pull-down notifications
|
||||||
|
- Loading overlay (z: 300) ← App loading spinner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `apps/shell/shell.rml` | Shell document with status bar, nav bar, overlays |
|
||||||
|
| `apps/shell/shell.lua` | Navigation, toasts, dialogs, notifications |
|
||||||
|
|
||||||
|
### App Content Fragments
|
||||||
|
|
||||||
|
Apps in shell mode use **content fragments** (`*_content.rml`) instead of full documents:
|
||||||
|
|
||||||
|
```rml
|
||||||
|
<!-- camera_content.rml - No <rml>, <head>, or document structure -->
|
||||||
|
<style>
|
||||||
|
.camera-content { ... }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="camera-content">
|
||||||
|
<!-- App UI here -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Lua API
|
||||||
|
|
||||||
|
Functions available to apps via shell:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Navigation
|
||||||
|
shellNavigateTo("settings") -- Navigate to system app
|
||||||
|
shellLaunchApp(id, path, entry) -- Launch external app
|
||||||
|
shellGoBack() -- Go to previous app
|
||||||
|
shellGoHome() -- Go to home (clear history)
|
||||||
|
|
||||||
|
-- UI Feedback
|
||||||
|
showToast("Message") -- Show toast notification
|
||||||
|
showToast("Error!", "error") -- Toast types: default, success, error, warning
|
||||||
|
showDialog("Title", "Message", onConfirm, onCancel)
|
||||||
|
|
||||||
|
-- Notifications
|
||||||
|
addNotification("Title", "Text", icon, app_id)
|
||||||
|
toggleNotifications() -- Show/hide notification panel
|
||||||
|
clearNotifications()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Content Fragments
|
||||||
|
|
||||||
|
1. Create `appname_content.rml` (not a full document)
|
||||||
|
2. Include styles via `<style>` tag
|
||||||
|
3. Use shell functions for navigation (`shellNavigateTo`, `showToast`, etc.)
|
||||||
|
4. Don't include status bar or nav bar (shell provides these)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```rml
|
||||||
|
<!-- settings_content.rml -->
|
||||||
|
<style>
|
||||||
|
.settings-list { padding: 16px; }
|
||||||
|
.settings-item { padding: 16px; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="settings-item" onclick="showToast('WiFi settings')">
|
||||||
|
WiFi
|
||||||
|
</div>
|
||||||
|
<div class="settings-item" onclick="showToast('Bluetooth settings')">
|
||||||
|
Bluetooth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Fragment Locations
|
||||||
|
|
||||||
|
| App | Content Fragment Path |
|
||||||
|
|-----|----------------------|
|
||||||
|
| Home | `apps/home/home_content.rml` |
|
||||||
|
| Camera | `apps/camera/camera_content.rml` |
|
||||||
|
| Dialer | `apps/dialer/dialer_content.rml` |
|
||||||
|
| Messages | `apps/messages/messages_content.rml` |
|
||||||
|
| Browser | `apps/browser/browser_content.rml` |
|
||||||
|
| Settings | `apps/settings/settings_content.rml` |
|
||||||
|
| Music | `apps/music/music_content.rml` |
|
||||||
|
| Store | `apps/store/store_content.rml` |
|
||||||
|
| Contacts | `apps/contacts/contacts_content.rml` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hot Reload
|
||||||
|
|
||||||
|
The designer watches for file changes in the assets directory:
|
||||||
|
|
||||||
|
- **RML files**: Reloads document
|
||||||
|
- **RCSS files**: Reloads stylesheets
|
||||||
|
- **Lua files**: Reloads scripts
|
||||||
|
- **TGA/PNG files**: Reloads textures
|
||||||
|
|
||||||
|
Press F5 to force reload.
|
||||||
|
|
||||||
|
## UI Hierarchy Dump
|
||||||
|
|
||||||
|
Use `--hierarchy` to continuously dump the UI element tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./designer/build/Release/mosis-designer.exe --simulator --hierarchy ui_tree.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON contains element bounds, classes, IDs, and visibility - useful for automated testing.
|
||||||
|
|
||||||
|
## Action Recording and Playback
|
||||||
|
|
||||||
|
See [TESTING-FRAMEWORK.md](TESTING-FRAMEWORK.md) for details on recording and playing back UI interactions.
|
||||||
|
|||||||
2
run-simulator.bat
Normal file
2
run-simulator.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
designer\build\Release\mosis-designer.exe --simulator --test-apps base-apps %*
|
||||||
11
src/main/assets/apps/browser/browser_content.rml
Normal file
11
src/main/assets/apps/browser/browser_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Browser App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Browser</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Browser App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
44
src/main/assets/apps/camera/camera_content.rml
Normal file
44
src/main/assets/apps/camera/camera_content.rml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!-- Camera App Content Fragment -->
|
||||||
|
<!-- Loaded into shell's #app-container -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="camera-content">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div class="camera-top-bar">
|
||||||
|
<div class="camera-btn" onclick="showToast('Flash: Auto')">
|
||||||
|
<img src="../../icons/flash.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="camera-btn">
|
||||||
|
<img src="../../icons/timer.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="camera-btn" onclick="showToast('Settings')">
|
||||||
|
<img src="../../icons/settings.tga"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Viewfinder -->
|
||||||
|
<div class="viewfinder-container">
|
||||||
|
<div class="viewfinder">
|
||||||
|
<span class="viewfinder-text">Camera Preview</span>
|
||||||
|
</div>
|
||||||
|
<div class="focus-indicator"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode Selector -->
|
||||||
|
<div class="mode-selector">
|
||||||
|
<span class="mode-item">Video</span>
|
||||||
|
<span class="mode-item active">Photo</span>
|
||||||
|
<span class="mode-item">Portrait</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Controls -->
|
||||||
|
<div class="camera-bottom-bar">
|
||||||
|
<div class="gallery-preview"></div>
|
||||||
|
<div class="shutter-btn" onclick="showToast('Photo captured!', 'success')">
|
||||||
|
<div class="shutter-btn-inner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="switch-camera-btn" onclick="showToast('Switched camera')">
|
||||||
|
<img src="../../icons/switch-camera.tga"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/main/assets/apps/contacts/contacts_content.rml
Normal file
11
src/main/assets/apps/contacts/contacts_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Contacts App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Contacts</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Contacts App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/main/assets/apps/dialer/dialer_content.rml
Normal file
11
src/main/assets/apps/dialer/dialer_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Dialer App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Phone</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Dialer App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
76
src/main/assets/apps/home/home_content.rml
Normal file
76
src/main/assets/apps/home/home_content.rml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!-- Home Screen Content Fragment -->
|
||||||
|
<!-- Loaded into shell's #app-container -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="home-content-area">
|
||||||
|
<!-- App Grid -->
|
||||||
|
<div class="home-grid">
|
||||||
|
<div id="installed-apps" class="app-grid-section">
|
||||||
|
<!-- Apps populated dynamically -->
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #F44336;" onclick="shellNavigateTo('browser')">
|
||||||
|
<img src="../../icons/browser.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Browser</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #9C27B0;" onclick="shellNavigateTo('camera')">
|
||||||
|
<img src="../../icons/camera.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Camera</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #FF9800;" onclick="shellNavigateTo('contacts')">
|
||||||
|
<img src="../../icons/contacts.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Contacts</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #4CAF50;" onclick="shellNavigateTo('dialer')">
|
||||||
|
<img src="../../icons/phone.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Phone</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #2196F3;" onclick="shellNavigateTo('messages')">
|
||||||
|
<img src="../../icons/message.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Messages</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #E91E63;" onclick="shellNavigateTo('music')">
|
||||||
|
<img src="../../icons/music.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Music</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #607D8B;" onclick="shellNavigateTo('settings')">
|
||||||
|
<img src="../../icons/settings.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<div class="app-icon-image" style="background-color: #3F51B5;" onclick="shellNavigateTo('store')">
|
||||||
|
<img src="../../icons/store.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-icon-label">Store</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dock -->
|
||||||
|
<div class="home-dock">
|
||||||
|
<div class="dock-item" style="background-color: #4CAF50;" onclick="shellNavigateTo('dialer')">
|
||||||
|
<img src="../../icons/phone.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="dock-item" style="background-color: #2196F3;" onclick="shellNavigateTo('messages')">
|
||||||
|
<img src="../../icons/message.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="dock-item" style="background-color: #FF9800;" onclick="shellNavigateTo('contacts')">
|
||||||
|
<img src="../../icons/contacts.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="dock-item" style="background-color: #F44336;" onclick="shellNavigateTo('browser')">
|
||||||
|
<img src="../../icons/browser.tga"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/main/assets/apps/messages/messages_content.rml
Normal file
11
src/main/assets/apps/messages/messages_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Messages App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Messages</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Messages App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/main/assets/apps/music/music_content.rml
Normal file
11
src/main/assets/apps/music/music_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Music App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Music</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Music App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/main/assets/apps/settings/settings_content.rml
Normal file
11
src/main/assets/apps/settings/settings_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Settings App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Settings App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
357
src/main/assets/apps/shell/shell.lua
Normal file
357
src/main/assets/apps/shell/shell.lua
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
-- System Shell for Mosis
|
||||||
|
-- Provides persistent navigation, notifications, toasts, and dialogs
|
||||||
|
|
||||||
|
local shell_document = nil
|
||||||
|
local app_container = nil
|
||||||
|
|
||||||
|
-- Navigation state (persistent in shell)
|
||||||
|
local nav_history = {}
|
||||||
|
local current_app = "home"
|
||||||
|
local current_app_path = nil
|
||||||
|
|
||||||
|
-- Dialog callback
|
||||||
|
local dialog_callback = nil
|
||||||
|
|
||||||
|
-- Notification state
|
||||||
|
local notifications_expanded = false
|
||||||
|
|
||||||
|
-- ===== INITIALIZATION =====
|
||||||
|
|
||||||
|
function initShell(doc)
|
||||||
|
print("[Shell] Initializing system shell...")
|
||||||
|
shell_document = doc
|
||||||
|
app_container = doc:GetElementById("app-container")
|
||||||
|
|
||||||
|
if not app_container then
|
||||||
|
print("[Shell] ERROR: app-container not found!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update time display
|
||||||
|
updateTime()
|
||||||
|
|
||||||
|
-- Load home screen by default
|
||||||
|
loadAppContent_internal("home", "apps/home/home_content.rml")
|
||||||
|
|
||||||
|
print("[Shell] Shell initialized")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update status bar time
|
||||||
|
function updateTime()
|
||||||
|
local time_elem = shell_document:GetElementById("shell-time")
|
||||||
|
if time_elem then
|
||||||
|
time_elem.inner_rml = "12:30"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== APP LOADING =====
|
||||||
|
|
||||||
|
-- Internal function to load app content
|
||||||
|
function loadAppContent_internal(app_id, app_path)
|
||||||
|
if not app_container then
|
||||||
|
print("[Shell] ERROR: No app container")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
print("[Shell] Loading app: " .. app_id .. " from " .. app_path)
|
||||||
|
|
||||||
|
-- Show loading overlay
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
-- Load app content using C++ function
|
||||||
|
if loadAppContent then
|
||||||
|
local success = loadAppContent(app_container, app_path)
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
current_app = app_id
|
||||||
|
current_app_path = app_path
|
||||||
|
print("[Shell] App loaded: " .. app_id)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
print("[Shell] Failed to load app: " .. app_id)
|
||||||
|
showToast("Failed to load app", "error")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
showLoading(false)
|
||||||
|
print("[Shell] ERROR: loadAppContent not available, using fallback")
|
||||||
|
|
||||||
|
-- Fallback: try to load via inner_rml if we can read the file
|
||||||
|
-- This won't work without C++ support, but shows the intent
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Navigate to a system app by ID
|
||||||
|
function shellNavigateTo(app_id)
|
||||||
|
local app_paths = {
|
||||||
|
home = "apps/home/home_content.rml",
|
||||||
|
dialer = "apps/dialer/dialer_content.rml",
|
||||||
|
calling = "apps/dialer/calling_content.rml",
|
||||||
|
contacts = "apps/contacts/contacts_content.rml",
|
||||||
|
contact_detail = "apps/contacts/contact_detail_content.rml",
|
||||||
|
messages = "apps/messages/messages_content.rml",
|
||||||
|
chat = "apps/messages/chat_content.rml",
|
||||||
|
settings = "apps/settings/settings_content.rml",
|
||||||
|
browser = "apps/browser/browser_content.rml",
|
||||||
|
store = "apps/store/store_content.rml",
|
||||||
|
camera = "apps/camera/camera_content.rml",
|
||||||
|
music = "apps/music/music_content.rml"
|
||||||
|
}
|
||||||
|
|
||||||
|
local path = app_paths[app_id]
|
||||||
|
if path then
|
||||||
|
-- Push current app to history
|
||||||
|
if current_app and current_app ~= app_id then
|
||||||
|
table.insert(nav_history, {
|
||||||
|
app_id = current_app,
|
||||||
|
app_path = current_app_path
|
||||||
|
})
|
||||||
|
print("[Shell] Pushed to history: " .. current_app .. " (depth: " .. #nav_history .. ")")
|
||||||
|
end
|
||||||
|
return loadAppContent_internal(app_id, path)
|
||||||
|
else
|
||||||
|
print("[Shell] Unknown app: " .. tostring(app_id))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== NAVIGATION =====
|
||||||
|
|
||||||
|
-- Go back to previous app
|
||||||
|
function shellGoBack()
|
||||||
|
print("[Shell] goBack called (history depth: " .. #nav_history .. ")")
|
||||||
|
|
||||||
|
-- Hide notifications if open
|
||||||
|
if notifications_expanded then
|
||||||
|
toggleNotifications()
|
||||||
|
end
|
||||||
|
|
||||||
|
if #nav_history > 0 then
|
||||||
|
local previous = table.remove(nav_history)
|
||||||
|
print("[Shell] Going back to: " .. previous.app_id)
|
||||||
|
|
||||||
|
-- Don't push to history when going back
|
||||||
|
local temp_app = current_app
|
||||||
|
current_app = nil
|
||||||
|
current_app_path = nil
|
||||||
|
|
||||||
|
local success = loadAppContent_internal(previous.app_id, previous.app_path)
|
||||||
|
if not success then
|
||||||
|
-- Restore state on failure
|
||||||
|
current_app = temp_app
|
||||||
|
end
|
||||||
|
return success
|
||||||
|
else
|
||||||
|
print("[Shell] No history - already at root")
|
||||||
|
if current_app ~= "home" then
|
||||||
|
shellGoHome()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Go to home screen (clear history)
|
||||||
|
function shellGoHome()
|
||||||
|
print("[Shell] goHome called")
|
||||||
|
|
||||||
|
-- Hide notifications if open
|
||||||
|
if notifications_expanded then
|
||||||
|
toggleNotifications()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear history
|
||||||
|
nav_history = {}
|
||||||
|
|
||||||
|
-- Load home
|
||||||
|
current_app = nil
|
||||||
|
current_app_path = nil
|
||||||
|
loadAppContent_internal("home", "apps/home/home_content.rml")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show recents (placeholder)
|
||||||
|
function shellShowRecents()
|
||||||
|
print("[Shell] showRecents called")
|
||||||
|
showToast("Recent apps not implemented", "warning")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Launch external app (from base-apps)
|
||||||
|
function shellLaunchApp(package_id, app_path, entry_point)
|
||||||
|
print("[Shell] Launching external app: " .. package_id)
|
||||||
|
|
||||||
|
-- Push current to history
|
||||||
|
if current_app then
|
||||||
|
table.insert(nav_history, {
|
||||||
|
app_id = current_app,
|
||||||
|
app_path = current_app_path
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For external apps, we need content version of the entry
|
||||||
|
-- Convert "app.rml" to "app_content.rml" or load directly
|
||||||
|
local content_path = app_path .. "/" .. entry_point
|
||||||
|
|
||||||
|
-- Switch sandbox context if available
|
||||||
|
if switchAppSandbox then
|
||||||
|
switchAppSandbox(package_id, app_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
return loadAppContent_internal(package_id, content_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== TOASTS =====
|
||||||
|
|
||||||
|
function showToast(message, type)
|
||||||
|
type = type or "default"
|
||||||
|
|
||||||
|
local container = shell_document:GetElementById("toast-container")
|
||||||
|
if not container then
|
||||||
|
print("[Shell] Toast container not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local class = "toast"
|
||||||
|
if type == "success" then
|
||||||
|
class = "toast toast-success"
|
||||||
|
elseif type == "error" then
|
||||||
|
class = "toast toast-error"
|
||||||
|
elseif type == "warning" then
|
||||||
|
class = "toast toast-warning"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add toast element
|
||||||
|
local toast_html = '<div class="' .. class .. '">' .. message .. '</div>'
|
||||||
|
container.inner_rml = container.inner_rml .. toast_html
|
||||||
|
|
||||||
|
-- Auto-remove after 3 seconds (would need timer API)
|
||||||
|
if mosis and mosis.timer then
|
||||||
|
mosis.timer.setTimeout(function()
|
||||||
|
-- Remove first toast
|
||||||
|
container.inner_rml = ""
|
||||||
|
end, 3000)
|
||||||
|
end
|
||||||
|
|
||||||
|
print("[Shell] Toast: " .. message .. " (" .. type .. ")")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== DIALOGS =====
|
||||||
|
|
||||||
|
function showDialog(title, message, on_confirm, on_cancel)
|
||||||
|
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||||
|
local title_elem = shell_document:GetElementById("dialog-title")
|
||||||
|
local message_elem = shell_document:GetElementById("dialog-message")
|
||||||
|
|
||||||
|
if overlay and title_elem and message_elem then
|
||||||
|
title_elem.inner_rml = title
|
||||||
|
message_elem.inner_rml = message
|
||||||
|
overlay:SetClass("visible", true)
|
||||||
|
|
||||||
|
dialog_callback = {
|
||||||
|
confirm = on_confirm,
|
||||||
|
cancel = on_cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[Shell] Dialog shown: " .. title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function dialogConfirm()
|
||||||
|
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||||
|
if overlay then
|
||||||
|
overlay:SetClass("visible", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
if dialog_callback and dialog_callback.confirm then
|
||||||
|
dialog_callback.confirm()
|
||||||
|
end
|
||||||
|
dialog_callback = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function dialogCancel()
|
||||||
|
local overlay = shell_document:GetElementById("dialog-overlay")
|
||||||
|
if overlay then
|
||||||
|
overlay:SetClass("visible", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
if dialog_callback and dialog_callback.cancel then
|
||||||
|
dialog_callback.cancel()
|
||||||
|
end
|
||||||
|
dialog_callback = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== NOTIFICATIONS =====
|
||||||
|
|
||||||
|
function toggleNotifications()
|
||||||
|
local panel = shell_document:GetElementById("notification-panel")
|
||||||
|
if panel then
|
||||||
|
notifications_expanded = not notifications_expanded
|
||||||
|
panel:SetClass("expanded", notifications_expanded)
|
||||||
|
print("[Shell] Notifications " .. (notifications_expanded and "expanded" or "collapsed"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function addNotification(title, text, icon, app_id)
|
||||||
|
local list = shell_document:GetElementById("notification-list")
|
||||||
|
if not list then return end
|
||||||
|
|
||||||
|
icon = icon or "../../icons/message.tga"
|
||||||
|
local notification_html = [[
|
||||||
|
<div class="notification-item" onclick="onNotificationClick(']] .. (app_id or "") .. [[')">
|
||||||
|
<div class="notification-icon">
|
||||||
|
<img src="]] .. icon .. [["/>
|
||||||
|
</div>
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-title">]] .. title .. [[</div>
|
||||||
|
<div class="notification-text">]] .. text .. [[</div>
|
||||||
|
</div>
|
||||||
|
<span class="notification-time">now</span>
|
||||||
|
</div>
|
||||||
|
]]
|
||||||
|
|
||||||
|
list.inner_rml = notification_html .. list.inner_rml
|
||||||
|
print("[Shell] Notification added: " .. title)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onNotificationClick(app_id)
|
||||||
|
toggleNotifications()
|
||||||
|
if app_id and app_id ~= "" then
|
||||||
|
shellNavigateTo(app_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function clearNotifications()
|
||||||
|
local list = shell_document:GetElementById("notification-list")
|
||||||
|
if list then
|
||||||
|
list.inner_rml = ""
|
||||||
|
print("[Shell] Notifications cleared")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== LOADING OVERLAY =====
|
||||||
|
|
||||||
|
function showLoading(visible)
|
||||||
|
local overlay = shell_document:GetElementById("loading-overlay")
|
||||||
|
if overlay then
|
||||||
|
overlay:SetClass("visible", visible)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== UTILITY =====
|
||||||
|
|
||||||
|
function shellGetCurrentApp()
|
||||||
|
return current_app
|
||||||
|
end
|
||||||
|
|
||||||
|
function shellCanGoBack()
|
||||||
|
return #nav_history > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make shell functions globally available for apps
|
||||||
|
_G.showToast = showToast
|
||||||
|
_G.showDialog = showDialog
|
||||||
|
_G.addNotification = addNotification
|
||||||
|
_G.shellNavigateTo = shellNavigateTo
|
||||||
|
_G.shellLaunchApp = shellLaunchApp
|
||||||
|
|
||||||
|
print("[Shell] Shell script loaded")
|
||||||
650
src/main/assets/apps/shell/shell.rml
Normal file
650
src/main/assets/apps/shell/shell.rml
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
<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="shell.lua"></script>
|
||||||
|
<title>Mosis Shell</title>
|
||||||
|
<style>
|
||||||
|
.shell {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #121212;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status bar at top - always visible */
|
||||||
|
.shell-status-bar {
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: #000000;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-status-bar-time {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-status-bar-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-status-bar-icons img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App content area - fills middle space */
|
||||||
|
#app-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System navigation bar at bottom - always visible */
|
||||||
|
.shell-nav-bar {
|
||||||
|
height: 56px;
|
||||||
|
min-height: 56px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-nav-btn {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-nav-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-nav-btn:active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-nav-btn img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Home indicator line */
|
||||||
|
.shell-home-indicator {
|
||||||
|
width: 134px;
|
||||||
|
height: 5px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== OVERLAY LAYERS ===== */
|
||||||
|
|
||||||
|
/* Toast container - bottom of screen, above nav bar */
|
||||||
|
#toast-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 70px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 500;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background-color: #323232;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: toast-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background-color: #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
background-color: #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-in {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog/Modal overlay */
|
||||||
|
#dialog-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 600;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog-overlay.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-box {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #B0B0B0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn-cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #BB86FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn-cancel:hover {
|
||||||
|
background-color: rgba(187, 134, 252, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn-confirm {
|
||||||
|
background-color: #BB86FC;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn-confirm:hover {
|
||||||
|
background-color: #D4A9FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification panel - slides down from top */
|
||||||
|
#notification-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 36px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
z-index: 400;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification-panel.expanded {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item:hover {
|
||||||
|
background-color: #383838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #BB86FC;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B0B0B0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808080;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading overlay */
|
||||||
|
#loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 36px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 56px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 300;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-overlay.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #BB86FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== APP CONTENT STYLES ===== */
|
||||||
|
/* Styles for content fragments loaded into #app-container */
|
||||||
|
|
||||||
|
/* Home Content */
|
||||||
|
.home-content-area {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-grid {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 25%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-image:hover {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-image img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-dock {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 24px 24px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-item {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-item:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-item img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Camera Content */
|
||||||
|
.camera-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #000000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewfinder-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewfinder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewfinder-text {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-indicator {
|
||||||
|
position: absolute;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border: 2px solid #FFFFFF;
|
||||||
|
border-radius: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-bottom-bar {
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-preview {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutter-btn {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 36px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutter-btn:hover {
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutter-btn-inner {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 3px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-camera-btn {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-camera-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-camera-btn img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-item {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-item:hover {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-item.active {
|
||||||
|
color: #FFD700;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic App Content - for apps without specific styles */
|
||||||
|
.app-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="shell" onload="initShell(document)">
|
||||||
|
<!-- System Status Bar -->
|
||||||
|
<div class="shell-status-bar" onclick="toggleNotifications()">
|
||||||
|
<span id="shell-time" class="shell-status-bar-time">12:30</span>
|
||||||
|
<div class="shell-status-bar-icons">
|
||||||
|
<img src="../../icons/wifi.tga"/>
|
||||||
|
<img src="../../icons/signal.tga"/>
|
||||||
|
<img src="../../icons/battery.tga"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App Content Container -->
|
||||||
|
<div id="app-container">
|
||||||
|
<!-- App content will be loaded here dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Navigation Bar -->
|
||||||
|
<div class="shell-nav-bar">
|
||||||
|
<div class="shell-nav-btn" onclick="shellGoBack()">
|
||||||
|
<img src="../../icons/back.tga"/>
|
||||||
|
</div>
|
||||||
|
<div class="shell-nav-btn" onclick="shellGoHome()">
|
||||||
|
<div class="shell-home-indicator"></div>
|
||||||
|
</div>
|
||||||
|
<div class="shell-nav-btn" onclick="shellShowRecents()">
|
||||||
|
<img src="../../icons/menu.tga"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== OVERLAY LAYERS ===== -->
|
||||||
|
|
||||||
|
<!-- Notification Panel (swipe down from status bar) -->
|
||||||
|
<div id="notification-panel">
|
||||||
|
<div class="notification-list" id="notification-list">
|
||||||
|
<!-- Notifications will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toast-container">
|
||||||
|
<!-- Toasts will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div id="loading-overlay">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog/Modal Overlay -->
|
||||||
|
<div id="dialog-overlay">
|
||||||
|
<div class="dialog-box">
|
||||||
|
<div id="dialog-title" class="dialog-title">Title</div>
|
||||||
|
<div id="dialog-message" class="dialog-message">Message</div>
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<div class="dialog-btn dialog-btn-cancel" onclick="dialogCancel()">Cancel</div>
|
||||||
|
<div class="dialog-btn dialog-btn-confirm" onclick="dialogConfirm()">OK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
11
src/main/assets/apps/store/store_content.rml
Normal file
11
src/main/assets/apps/store/store_content.rml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Store App Content Fragment -->
|
||||||
|
<!-- Styles are in shell.rml -->
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
<div class="app-header">
|
||||||
|
<span class="app-header-title">Store</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-body">
|
||||||
|
<span class="placeholder-text">Store App</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user