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
This commit is contained in:
19
base-apps/com.mosis.store/manifest.json
Normal file
19
base-apps/com.mosis.store/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "com.mosis.store",
|
||||
"name": "Mosis Store",
|
||||
"version": "1.0.0",
|
||||
"version_code": 1,
|
||||
"entry": "store.rml",
|
||||
"icon": "/system/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
|
||||
}
|
||||
394
base-apps/com.mosis.store/store.lua
Normal file
394
base-apps/com.mosis.store/store.lua
Normal 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()
|
||||
416
base-apps/com.mosis.store/store.rml
Normal file
416
base-apps/com.mosis.store/store.rml
Normal file
@@ -0,0 +1,416 @@
|
||||
<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="store.lua"></script>
|
||||
<title>Store</title>
|
||||
<style>
|
||||
.store-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.store-search {
|
||||
margin: 16px;
|
||||
background-color: #2D2D2D;
|
||||
border-radius: 24px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-search:hover {
|
||||
background-color: #3D3D3D;
|
||||
}
|
||||
|
||||
.store-search img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.store-search-text {
|
||||
font-size: 16px;
|
||||
color: #B3B3B3;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 8px 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.section-action {
|
||||
font-size: 16px;
|
||||
color: #BB86FC;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.featured-banner {
|
||||
margin: 0 16px 16px 16px;
|
||||
height: 140px;
|
||||
background-color: #7C3AED;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.featured-banner:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.featured-tag {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.featured-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.featured-subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.app-cards-row {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
min-width: 130px;
|
||||
background-color: #1E1E1E;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
.app-card-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.app-card-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.app-card-category {
|
||||
font-size: 14px;
|
||||
color: #B3B3B3;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.app-card-rating {
|
||||
font-size: 14px;
|
||||
color: #B3B3B3;
|
||||
}
|
||||
|
||||
.app-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-list-item:hover {
|
||||
background-color: #1E1E1E;
|
||||
}
|
||||
|
||||
.app-list-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.app-list-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-list-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.app-list-meta {
|
||||
font-size: 14px;
|
||||
color: #B3B3B3;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.install-btn {
|
||||
background-color: #BB86FC;
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.install-btn:hover {
|
||||
background-color: #D4A5FF;
|
||||
}
|
||||
|
||||
.install-btn.installed {
|
||||
background-color: transparent;
|
||||
color: #BB86FC;
|
||||
}
|
||||
|
||||
.category-chips {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.category-chip {
|
||||
background-color: #2D2D2D;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-chip:hover {
|
||||
background-color: #3D3D3D;
|
||||
}
|
||||
|
||||
.category-chip.active {
|
||||
background-color: #BB86FC;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.store-bottom-nav {
|
||||
display: flex;
|
||||
height: 56px;
|
||||
background-color: #1E1E1E;
|
||||
}
|
||||
|
||||
.store-nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #B3B3B3;
|
||||
}
|
||||
|
||||
.store-nav-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.store-nav-item.active {
|
||||
color: #BB86FC;
|
||||
}
|
||||
|
||||
.store-nav-item img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.store-nav-item span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bg-purple { background-color: #BB86FC; }
|
||||
.bg-teal { background-color: #03DAC6; }
|
||||
.bg-orange { background-color: #FF9800; }
|
||||
.bg-blue { background-color: #2196F3; }
|
||||
.bg-green { background-color: #4CAF50; }
|
||||
.bg-red { background-color: #F44336; }
|
||||
.bg-pink { background-color: #E91E63; }
|
||||
.bg-indigo { background-color: #3F51B5; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-screen" onload="initLayout(document)">
|
||||
<!-- System Status Bar -->
|
||||
<div class="system-status-bar">
|
||||
<span id="status-time" class="system-status-time">12:30</span>
|
||||
<div class="system-status-icons">
|
||||
<img src="../../icons/wifi.tga"/>
|
||||
<img src="../../icons/signal.tga"/>
|
||||
<img src="../../icons/battery.tga"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Bar -->
|
||||
<div class="app-bar">
|
||||
<div class="app-bar-back" onclick="goBack()">
|
||||
<img src="../../icons/back.tga"/>
|
||||
</div>
|
||||
<span class="app-bar-title">Mosis Store</span>
|
||||
<div class="app-bar-actions">
|
||||
<div class="app-bar-action">
|
||||
<img src="../../icons/account.tga"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Content -->
|
||||
<div class="store-content">
|
||||
<!-- Search Bar -->
|
||||
<div class="store-search">
|
||||
<img src="../../icons/search.tga"/>
|
||||
<span class="store-search-text">Search apps & games</span>
|
||||
</div>
|
||||
|
||||
<!-- Featured Banner -->
|
||||
<div class="featured-banner">
|
||||
<span class="featured-tag">Featured</span>
|
||||
<span class="featured-title">Weather Pro</span>
|
||||
<span class="featured-subtitle">Beautiful forecasts for your virtual world</span>
|
||||
</div>
|
||||
|
||||
<!-- Category Chips -->
|
||||
<div class="category-chips">
|
||||
<div class="category-chip active">For You</div>
|
||||
<div class="category-chip">Games</div>
|
||||
<div class="category-chip">Social</div>
|
||||
<div class="category-chip">Productivity</div>
|
||||
<div class="category-chip">Tools</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Section -->
|
||||
<div class="section-header">
|
||||
<span class="section-title">Recommended for You</span>
|
||||
<span class="section-action">See all</span>
|
||||
</div>
|
||||
|
||||
<div class="app-cards-row">
|
||||
<div class="app-card">
|
||||
<div class="app-card-icon bg-teal">N</div>
|
||||
<div class="app-card-name">Notes</div>
|
||||
<div class="app-card-category">Productivity</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">Calculator</div>
|
||||
<div class="app-card-category">Tools</div>
|
||||
<div class="app-card-rating">4.5</div>
|
||||
</div>
|
||||
<div class="app-card">
|
||||
<div class="app-card-icon bg-blue">W</div>
|
||||
<div class="app-card-name">Weather</div>
|
||||
<div class="app-card-category">Weather</div>
|
||||
<div class="app-card-rating">4.8</div>
|
||||
</div>
|
||||
<div class="app-card">
|
||||
<div class="app-card-icon bg-green">M</div>
|
||||
<div class="app-card-name">Maps</div>
|
||||
<div class="app-card-category">Navigation</div>
|
||||
<div class="app-card-rating">4.6</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Apps Section -->
|
||||
<div class="section-header">
|
||||
<span class="section-title">Top Free Apps</span>
|
||||
<span class="section-action">See all</span>
|
||||
</div>
|
||||
|
||||
<div class="app-list-item">
|
||||
<div class="app-list-icon bg-purple">S</div>
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-name">Social Hub</div>
|
||||
<div class="app-list-meta">Social - 12 MB - 4.9</div>
|
||||
</div>
|
||||
<div class="install-btn">Install</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list-item">
|
||||
<div class="app-list-icon bg-red">G</div>
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-name">Games Center</div>
|
||||
<div class="app-list-meta">Games - 45 MB - 4.7</div>
|
||||
</div>
|
||||
<div class="install-btn">Install</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list-item">
|
||||
<div class="app-list-icon bg-indigo">F</div>
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-name">File Manager</div>
|
||||
<div class="app-list-meta">Tools - 8 MB - 4.6</div>
|
||||
</div>
|
||||
<div class="install-btn installed">Open</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list-item">
|
||||
<div class="app-list-icon bg-pink">M</div>
|
||||
<div class="app-list-info">
|
||||
<div class="app-list-name">Music Player</div>
|
||||
<div class="app-list-meta">Music - 18 MB - 4.5</div>
|
||||
</div>
|
||||
<div class="install-btn">Install</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<div class="store-bottom-nav">
|
||||
<div class="store-nav-item active">
|
||||
<img src="../../icons/home.tga"/>
|
||||
<span>Apps</span>
|
||||
</div>
|
||||
<div class="store-nav-item">
|
||||
<img src="../../icons/game.tga"/>
|
||||
<span>Games</span>
|
||||
</div>
|
||||
<div class="store-nav-item">
|
||||
<img src="../../icons/download.tga"/>
|
||||
<span>Updates</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
Reference in New Issue
Block a user