add documentation site with markdown rendering (M12)
This commit is contained in:
@@ -9,6 +9,8 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/yuin/goldmark v1.7.0
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
golang.org/x/crypto v0.21.0
|
golang.org/x/crypto v0.21.0
|
||||||
golang.org/x/image v0.15.0
|
golang.org/x/image v0.15.0
|
||||||
golang.org/x/oauth2 v0.18.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
|
|||||||
@@ -219,6 +219,15 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Documentation site
|
||||||
|
docsHandler, err := web.NewDocsHandler()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to initialize docs handler: %v", err)
|
||||||
|
} else {
|
||||||
|
r.Handle("/docs", docsHandler)
|
||||||
|
r.Handle("/docs/*", docsHandler)
|
||||||
|
}
|
||||||
|
|
||||||
// Static file servers for packages and assets
|
// Static file servers for packages and assets
|
||||||
// Downloads - serve package files with proper headers
|
// Downloads - serve package files with proper headers
|
||||||
r.Handle("/downloads/*", http.StripPrefix("/downloads/",
|
r.Handle("/downloads/*", http.StripPrefix("/downloads/",
|
||||||
|
|||||||
304
portal/internal/web/docs.go
Normal file
304
portal/internal/web/docs.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed docs/*
|
||||||
|
var docsFS embed.FS
|
||||||
|
|
||||||
|
// DocsHandler serves documentation pages
|
||||||
|
type DocsHandler struct {
|
||||||
|
md goldmark.Markdown
|
||||||
|
template *template.Template
|
||||||
|
docs fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocsHandler creates a new documentation handler
|
||||||
|
func NewDocsHandler() (*DocsHandler, error) {
|
||||||
|
// Configure goldmark with extensions
|
||||||
|
md := goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM, // GitHub Flavored Markdown
|
||||||
|
extension.Table,
|
||||||
|
extension.Strikethrough,
|
||||||
|
extension.TaskList,
|
||||||
|
highlighting.NewHighlighting(
|
||||||
|
highlighting.WithStyle("monokai"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
html.WithUnsafe(), // Allow raw HTML
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the page template
|
||||||
|
tmpl, err := template.New("doc").Parse(docPageTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get embedded docs filesystem
|
||||||
|
docs, err := fs.Sub(docsFS, "docs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DocsHandler{
|
||||||
|
md: md,
|
||||||
|
template: tmpl,
|
||||||
|
docs: docs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handles documentation requests
|
||||||
|
func (h *DocsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the requested path
|
||||||
|
docPath := strings.TrimPrefix(r.URL.Path, "/docs")
|
||||||
|
if docPath == "" || docPath == "/" {
|
||||||
|
docPath = "/index"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the path and add .md extension
|
||||||
|
docPath = path.Clean(docPath)
|
||||||
|
if !strings.HasSuffix(docPath, ".md") {
|
||||||
|
docPath = docPath + ".md"
|
||||||
|
}
|
||||||
|
docPath = strings.TrimPrefix(docPath, "/")
|
||||||
|
|
||||||
|
// Read the markdown file
|
||||||
|
content, err := fs.ReadFile(h.docs, docPath)
|
||||||
|
if err != nil {
|
||||||
|
// Try index.md in directory
|
||||||
|
if !strings.HasSuffix(docPath, "/index.md") {
|
||||||
|
dirPath := strings.TrimSuffix(docPath, ".md") + "/index.md"
|
||||||
|
content, err = fs.ReadFile(h.docs, dirPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract title from first heading
|
||||||
|
title := extractTitle(content)
|
||||||
|
if title == "" {
|
||||||
|
title = "Documentation"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert markdown to HTML
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := h.md.Convert(content, &buf); err != nil {
|
||||||
|
http.Error(w, "Failed to render documentation", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build navigation
|
||||||
|
nav := h.buildNavigation(docPath)
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
data := docPageData{
|
||||||
|
Title: title,
|
||||||
|
Content: template.HTML(buf.String()),
|
||||||
|
Navigation: nav,
|
||||||
|
CurrentPath: "/" + strings.TrimSuffix(docPath, ".md"),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := h.template.Execute(w, data); err != nil {
|
||||||
|
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type docPageData struct {
|
||||||
|
Title string
|
||||||
|
Content template.HTML
|
||||||
|
Navigation []navSection
|
||||||
|
CurrentPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type navSection struct {
|
||||||
|
Title string
|
||||||
|
Items []navItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type navItem struct {
|
||||||
|
Title string
|
||||||
|
Path string
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildNavigation creates the documentation navigation structure
|
||||||
|
func (h *DocsHandler) buildNavigation(currentPath string) []navSection {
|
||||||
|
currentPath = "/" + strings.TrimSuffix(currentPath, ".md")
|
||||||
|
|
||||||
|
return []navSection{
|
||||||
|
{
|
||||||
|
Title: "Getting Started",
|
||||||
|
Items: []navItem{
|
||||||
|
{Title: "Introduction", Path: "/docs", Active: currentPath == "/index"},
|
||||||
|
{Title: "Quick Start", Path: "/docs/getting-started", Active: currentPath == "/getting-started"},
|
||||||
|
{Title: "FAQ", Path: "/docs/faq", Active: currentPath == "/faq"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Guides",
|
||||||
|
Items: []navItem{
|
||||||
|
{Title: "UI Design", Path: "/docs/guides/ui-design", Active: currentPath == "/guides/ui-design"},
|
||||||
|
{Title: "Lua Scripting", Path: "/docs/guides/lua-scripting", Active: currentPath == "/guides/lua-scripting"},
|
||||||
|
{Title: "Permissions", Path: "/docs/guides/permissions", Active: currentPath == "/guides/permissions"},
|
||||||
|
{Title: "Best Practices", Path: "/docs/guides/best-practices", Active: currentPath == "/guides/best-practices"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Reference",
|
||||||
|
Items: []navItem{
|
||||||
|
{Title: "Lua API", Path: "/docs/api/lua-api", Active: currentPath == "/api/lua-api"},
|
||||||
|
{Title: "Manifest", Path: "/docs/api/manifest", Active: currentPath == "/api/manifest"},
|
||||||
|
{Title: "CLI", Path: "/docs/cli", Active: currentPath == "/cli"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Help",
|
||||||
|
Items: []navItem{
|
||||||
|
{Title: "Troubleshooting", Path: "/docs/troubleshooting", Active: currentPath == "/troubleshooting"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTitle extracts the first H1 heading from markdown
|
||||||
|
func extractTitle(content []byte) string {
|
||||||
|
lines := bytes.Split(content, []byte("\n"))
|
||||||
|
for _, line := range lines {
|
||||||
|
line = bytes.TrimSpace(line)
|
||||||
|
if bytes.HasPrefix(line, []byte("# ")) {
|
||||||
|
return string(bytes.TrimPrefix(line, []byte("# ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// docPageTemplate is the HTML template for documentation pages
|
||||||
|
const docPageTemplate = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}} - Mosis Docs</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
/* Code highlighting */
|
||||||
|
pre {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
:not(pre) > code {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
/* Typography */
|
||||||
|
.prose h1 { font-size: 2rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; color: #f8fafc; }
|
||||||
|
.prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; color: #f8fafc; border-bottom: 1px solid #334155; padding-bottom: 0.5rem; }
|
||||||
|
.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #f8fafc; }
|
||||||
|
.prose h4 { font-size: 1rem; font-weight: 600; margin-top: 1rem; margin-bottom: 0.5rem; color: #f8fafc; }
|
||||||
|
.prose p { margin-bottom: 1rem; line-height: 1.7; }
|
||||||
|
.prose a { color: #38bdf8; text-decoration: none; }
|
||||||
|
.prose a:hover { text-decoration: underline; }
|
||||||
|
.prose ul, .prose ol { margin-bottom: 1rem; padding-left: 1.5rem; }
|
||||||
|
.prose li { margin-bottom: 0.5rem; }
|
||||||
|
.prose ul { list-style-type: disc; }
|
||||||
|
.prose ol { list-style-type: decimal; }
|
||||||
|
.prose table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
||||||
|
.prose th, .prose td { border: 1px solid #334155; padding: 8px 12px; text-align: left; }
|
||||||
|
.prose th { background-color: #1e293b; font-weight: 600; }
|
||||||
|
.prose blockquote { border-left: 4px solid #38bdf8; padding-left: 1rem; margin: 1rem 0; color: #94a3b8; }
|
||||||
|
.prose hr { border: none; border-top: 1px solid #334155; margin: 2rem 0; }
|
||||||
|
.prose strong { color: #f8fafc; }
|
||||||
|
/* Task lists */
|
||||||
|
.prose input[type="checkbox"] { margin-right: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-900 text-slate-300 min-h-screen">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-slate-800 border-b border-slate-700 sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
|
<a href="/docs" class="text-xl font-bold text-white flex items-center gap-2">
|
||||||
|
<svg class="w-8 h-8 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
||||||
|
</svg>
|
||||||
|
Mosis Docs
|
||||||
|
</a>
|
||||||
|
<nav class="flex items-center gap-6">
|
||||||
|
<a href="/" class="text-slate-300 hover:text-white transition">Home</a>
|
||||||
|
<a href="/dashboard" class="text-slate-300 hover:text-white transition">Dashboard</a>
|
||||||
|
<a href="https://github.com/omixlab/mosis" class="text-slate-300 hover:text-white transition">GitHub</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto flex">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="w-64 flex-shrink-0 border-r border-slate-700 min-h-[calc(100vh-73px)] sticky top-[73px] self-start hidden lg:block">
|
||||||
|
<nav class="p-4 space-y-6">
|
||||||
|
{{range .Navigation}}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">{{.Title}}</h3>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{{range .Items}}
|
||||||
|
<li>
|
||||||
|
<a href="{{.Path}}" class="block px-3 py-2 rounded-md text-sm transition {{if .Active}}bg-sky-500/10 text-sky-400{{else}}text-slate-300 hover:bg-slate-800{{end}}">
|
||||||
|
{{.Title}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<article class="prose max-w-4xl mx-auto px-8 py-12">
|
||||||
|
{{.Content}}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="border-t border-slate-700 px-8 py-6 mt-12">
|
||||||
|
<div class="max-w-4xl mx-auto flex items-center justify-between text-sm text-slate-400">
|
||||||
|
<p>© 2024 OmixLab LTD. All rights reserved.</p>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a href="/privacy" class="hover:text-white transition">Privacy</a>
|
||||||
|
<a href="/terms" class="hover:text-white transition">Terms</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
806
portal/internal/web/docs/api/lua-api.md
Normal file
806
portal/internal/web/docs/api/lua-api.md
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
# Lua API Reference
|
||||||
|
|
||||||
|
Complete reference for the Mosis Lua API available to apps.
|
||||||
|
|
||||||
|
## Global Objects
|
||||||
|
|
||||||
|
### document
|
||||||
|
|
||||||
|
The current RML document. Use to query and modify UI elements.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Get element by ID
|
||||||
|
local elem = document:GetElementById("my-id")
|
||||||
|
|
||||||
|
-- Get elements by tag
|
||||||
|
local buttons = document:GetElementsByTagName("button")
|
||||||
|
|
||||||
|
-- Get elements by class
|
||||||
|
local cards = document:GetElementsByClassName("card")
|
||||||
|
```
|
||||||
|
|
||||||
|
### event
|
||||||
|
|
||||||
|
Available in event handler functions. Contains information about the triggering event.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function handleClick(event)
|
||||||
|
local target = event:GetCurrentElement()
|
||||||
|
local eventType = event.type
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Document Methods
|
||||||
|
|
||||||
|
### GetElementById(id)
|
||||||
|
|
||||||
|
Returns the element with the specified ID, or `nil` if not found.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local element = document:GetElementById("username-input")
|
||||||
|
if element then
|
||||||
|
element.inner_rml = "Found!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### GetElementsByTagName(tag)
|
||||||
|
|
||||||
|
Returns a table of all elements with the specified tag name.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local buttons = document:GetElementsByTagName("button")
|
||||||
|
for i, btn in ipairs(buttons) do
|
||||||
|
btn:SetClass("styled", true)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### GetElementsByClassName(class)
|
||||||
|
|
||||||
|
Returns a table of all elements with the specified class name.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local items = document:GetElementsByClassName("list-item")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CreateElement(tag)
|
||||||
|
|
||||||
|
Creates a new element with the specified tag name.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local div = document:CreateElement("div")
|
||||||
|
div.inner_rml = "New element"
|
||||||
|
parent:AppendChild(div)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CreateTextNode(text)
|
||||||
|
|
||||||
|
Creates a text node with the specified content.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local text = document:CreateTextNode("Hello")
|
||||||
|
element:AppendChild(text)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element Properties
|
||||||
|
|
||||||
|
### inner_rml
|
||||||
|
|
||||||
|
Gets or sets the inner RML content of an element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Get content
|
||||||
|
local content = element.inner_rml
|
||||||
|
|
||||||
|
-- Set content (parses RML)
|
||||||
|
element.inner_rml = "<strong>Bold text</strong>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### id
|
||||||
|
|
||||||
|
Gets or sets the element's ID.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local id = element.id
|
||||||
|
element.id = "new-id"
|
||||||
|
```
|
||||||
|
|
||||||
|
### style
|
||||||
|
|
||||||
|
Access to the element's inline styles.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
element.style.width = "100dp"
|
||||||
|
element.style.backgroundColor = "#ff0000"
|
||||||
|
element.style.display = "none"
|
||||||
|
```
|
||||||
|
|
||||||
|
### parent_node
|
||||||
|
|
||||||
|
Returns the parent element, or `nil` if none.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local parent = element.parent_node
|
||||||
|
```
|
||||||
|
|
||||||
|
### first_child / last_child
|
||||||
|
|
||||||
|
Returns the first or last child element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local first = container.first_child
|
||||||
|
local last = container.last_child
|
||||||
|
```
|
||||||
|
|
||||||
|
### next_sibling / previous_sibling
|
||||||
|
|
||||||
|
Returns the next or previous sibling element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local next = element.next_sibling
|
||||||
|
```
|
||||||
|
|
||||||
|
### child_nodes
|
||||||
|
|
||||||
|
Returns a table of all child elements.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local children = element.child_nodes
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
print(child.id)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### tag_name
|
||||||
|
|
||||||
|
Returns the element's tag name (lowercase).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tag = element.tag_name -- "div", "button", etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### offset_width / offset_height
|
||||||
|
|
||||||
|
Returns the rendered dimensions of the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local width = element.offset_width
|
||||||
|
local height = element.offset_height
|
||||||
|
```
|
||||||
|
|
||||||
|
### offset_left / offset_top
|
||||||
|
|
||||||
|
Returns the position relative to the offset parent.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local x = element.offset_left
|
||||||
|
local y = element.offset_top
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element Methods
|
||||||
|
|
||||||
|
### GetAttribute(name)
|
||||||
|
|
||||||
|
Returns the value of the specified attribute.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local value = input:GetAttribute("value")
|
||||||
|
local placeholder = input:GetAttribute("placeholder")
|
||||||
|
```
|
||||||
|
|
||||||
|
### SetAttribute(name, value)
|
||||||
|
|
||||||
|
Sets the value of the specified attribute.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
input:SetAttribute("placeholder", "Enter text...")
|
||||||
|
button:SetAttribute("disabled", "disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
### RemoveAttribute(name)
|
||||||
|
|
||||||
|
Removes the specified attribute.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
button:RemoveAttribute("disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
### HasAttribute(name)
|
||||||
|
|
||||||
|
Returns `true` if the element has the specified attribute.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if button:HasAttribute("disabled") then
|
||||||
|
print("Button is disabled")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### SetClass(name, add)
|
||||||
|
|
||||||
|
Adds or removes a class from the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Add class
|
||||||
|
element:SetClass("active", true)
|
||||||
|
|
||||||
|
-- Remove class
|
||||||
|
element:SetClass("active", false)
|
||||||
|
```
|
||||||
|
|
||||||
|
### IsClassSet(name)
|
||||||
|
|
||||||
|
Returns `true` if the element has the specified class.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if element:IsClassSet("selected") then
|
||||||
|
print("Element is selected")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### AppendChild(element)
|
||||||
|
|
||||||
|
Appends a child element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local child = document:CreateElement("div")
|
||||||
|
parent:AppendChild(child)
|
||||||
|
```
|
||||||
|
|
||||||
|
### InsertBefore(element, reference)
|
||||||
|
|
||||||
|
Inserts an element before the reference element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
parent:InsertBefore(newElement, referenceElement)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RemoveChild(element)
|
||||||
|
|
||||||
|
Removes a child element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
parent:RemoveChild(childElement)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Focus()
|
||||||
|
|
||||||
|
Sets focus to the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
input:Focus()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blur()
|
||||||
|
|
||||||
|
Removes focus from the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
input:Blur()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Click()
|
||||||
|
|
||||||
|
Simulates a click on the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
button:Click()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ScrollIntoView(alignToTop)
|
||||||
|
|
||||||
|
Scrolls the element into view.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
element:ScrollIntoView(true) -- align to top
|
||||||
|
element:ScrollIntoView(false) -- align to bottom
|
||||||
|
```
|
||||||
|
|
||||||
|
### AddEventListener(event, handler)
|
||||||
|
|
||||||
|
Adds an event listener to the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
button:AddEventListener("click", function(event)
|
||||||
|
print("Clicked!")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RemoveEventListener(event, handler)
|
||||||
|
|
||||||
|
Removes an event listener from the element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local handler = function(event) print("Click") end
|
||||||
|
button:AddEventListener("click", handler)
|
||||||
|
button:RemoveEventListener("click", handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Object
|
||||||
|
|
||||||
|
### type
|
||||||
|
|
||||||
|
The event type string (e.g., "click", "change").
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if event.type == "click" then
|
||||||
|
-- handle click
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### target_element
|
||||||
|
|
||||||
|
The element that originally triggered the event.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local target = event.target_element
|
||||||
|
```
|
||||||
|
|
||||||
|
### current_element
|
||||||
|
|
||||||
|
The element the event handler is attached to.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local current = event.current_element
|
||||||
|
```
|
||||||
|
|
||||||
|
### GetCurrentElement()
|
||||||
|
|
||||||
|
Returns the current element (same as `current_element`).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local elem = event:GetCurrentElement()
|
||||||
|
```
|
||||||
|
|
||||||
|
### StopPropagation()
|
||||||
|
|
||||||
|
Stops the event from bubbling up to parent elements.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
event:StopPropagation()
|
||||||
|
```
|
||||||
|
|
||||||
|
### StopImmediatePropagation()
|
||||||
|
|
||||||
|
Stops the event and prevents other handlers on the same element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
event:StopImmediatePropagation()
|
||||||
|
```
|
||||||
|
|
||||||
|
### parameters
|
||||||
|
|
||||||
|
Table containing event-specific parameters.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Mouse events
|
||||||
|
local x = event.parameters.mouse_x
|
||||||
|
local y = event.parameters.mouse_y
|
||||||
|
local button = event.parameters.button -- 0=left, 1=right, 2=middle
|
||||||
|
|
||||||
|
-- Keyboard events
|
||||||
|
local key = event.parameters.key_identifier
|
||||||
|
local ctrl = event.parameters.ctrl_key
|
||||||
|
local shift = event.parameters.shift_key
|
||||||
|
local alt = event.parameters.alt_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### navigateTo(screen)
|
||||||
|
|
||||||
|
Navigates to a screen, pushing to history.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
navigateTo("settings") -- loads assets/settings.rml
|
||||||
|
navigateTo("screens/profile") -- loads assets/screens/profile.rml
|
||||||
|
```
|
||||||
|
|
||||||
|
### goBack()
|
||||||
|
|
||||||
|
Navigates back to the previous screen.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
goBack()
|
||||||
|
```
|
||||||
|
|
||||||
|
### goHome()
|
||||||
|
|
||||||
|
Navigates to the home screen, clearing history.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
goHome()
|
||||||
|
```
|
||||||
|
|
||||||
|
### replaceTo(screen)
|
||||||
|
|
||||||
|
Replaces current screen without adding to history.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
replaceTo("login") -- no back navigation possible
|
||||||
|
```
|
||||||
|
|
||||||
|
### canGoBack()
|
||||||
|
|
||||||
|
Returns `true` if there's a previous screen in history.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if canGoBack() then
|
||||||
|
backButton.style.display = "block"
|
||||||
|
else
|
||||||
|
backButton.style.display = "none"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timers
|
||||||
|
|
||||||
|
### setTimeout(callback, delay)
|
||||||
|
|
||||||
|
Executes callback once after delay (milliseconds). Returns timer ID.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local id = setTimeout(function()
|
||||||
|
print("Executed after 1 second")
|
||||||
|
end, 1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clearTimeout(id)
|
||||||
|
|
||||||
|
Cancels a timeout.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local id = setTimeout(callback, 1000)
|
||||||
|
clearTimeout(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### setInterval(callback, interval)
|
||||||
|
|
||||||
|
Executes callback repeatedly. Returns timer ID.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local id = setInterval(function()
|
||||||
|
updateClock()
|
||||||
|
end, 1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clearInterval(id)
|
||||||
|
|
||||||
|
Cancels an interval.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
clearInterval(intervalId)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Persistent key-value storage. Data persists between app sessions.
|
||||||
|
|
||||||
|
### storage.set(key, value)
|
||||||
|
|
||||||
|
Stores a value. Value can be string, number, boolean, or table.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
storage.set("username", "alice")
|
||||||
|
storage.set("settings", { darkMode = true, fontSize = 16 })
|
||||||
|
storage.set("highScore", 1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### storage.get(key)
|
||||||
|
|
||||||
|
Retrieves a stored value, or `nil` if not found.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local username = storage.get("username")
|
||||||
|
local settings = storage.get("settings")
|
||||||
|
if settings then
|
||||||
|
print(settings.darkMode)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### storage.remove(key)
|
||||||
|
|
||||||
|
Removes a stored value.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
storage.remove("tempData")
|
||||||
|
```
|
||||||
|
|
||||||
|
### storage.clear()
|
||||||
|
|
||||||
|
Removes all stored values.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
storage.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
### storage.keys()
|
||||||
|
|
||||||
|
Returns a table of all storage keys.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local keys = storage.keys()
|
||||||
|
for i, key in ipairs(keys) do
|
||||||
|
print(key)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP (requires `network` permission)
|
||||||
|
|
||||||
|
### http.get(url, callback)
|
||||||
|
|
||||||
|
Makes a GET request.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.get("https://api.example.com/data", function(response)
|
||||||
|
if response.ok then
|
||||||
|
local data = json.decode(response.body)
|
||||||
|
print(data.message)
|
||||||
|
else
|
||||||
|
print("Error: " .. response.status)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### http.post(url, options, callback)
|
||||||
|
|
||||||
|
Makes a POST request.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.post("https://api.example.com/submit", {
|
||||||
|
headers = {
|
||||||
|
["Content-Type"] = "application/json",
|
||||||
|
["Authorization"] = "Bearer token123"
|
||||||
|
},
|
||||||
|
body = json.encode({ name = "test" })
|
||||||
|
}, function(response)
|
||||||
|
print("Status: " .. response.status)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### http.request(options, callback)
|
||||||
|
|
||||||
|
Makes a custom HTTP request.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.request({
|
||||||
|
method = "PUT",
|
||||||
|
url = "https://api.example.com/resource/1",
|
||||||
|
headers = { ["Content-Type"] = "application/json" },
|
||||||
|
body = json.encode({ updated = true }),
|
||||||
|
timeout = 5000 -- milliseconds
|
||||||
|
}, function(response)
|
||||||
|
print(response.status)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Object
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `ok` | boolean | `true` if status is 200-299 |
|
||||||
|
| `status` | number | HTTP status code |
|
||||||
|
| `statusText` | string | Status message |
|
||||||
|
| `headers` | table | Response headers |
|
||||||
|
| `body` | string | Response body |
|
||||||
|
|
||||||
|
## JSON
|
||||||
|
|
||||||
|
### json.encode(value)
|
||||||
|
|
||||||
|
Converts a Lua value to a JSON string.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local str = json.encode({
|
||||||
|
name = "Alice",
|
||||||
|
items = {"a", "b", "c"},
|
||||||
|
count = 3
|
||||||
|
})
|
||||||
|
-- '{"name":"Alice","items":["a","b","c"],"count":3}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### json.decode(str)
|
||||||
|
|
||||||
|
Parses a JSON string into a Lua value.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = json.decode('{"name":"Alice","age":25}')
|
||||||
|
print(data.name) -- "Alice"
|
||||||
|
print(data.age) -- 25
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
### print(...)
|
||||||
|
|
||||||
|
Outputs to the debug console. Accepts multiple arguments.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print("Debug message")
|
||||||
|
print("Value:", someValue, "Count:", count)
|
||||||
|
```
|
||||||
|
|
||||||
|
### console.log(...)
|
||||||
|
|
||||||
|
Alias for `print()`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
console.log("Hello")
|
||||||
|
```
|
||||||
|
|
||||||
|
### console.warn(...)
|
||||||
|
|
||||||
|
Logs a warning message.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
console.warn("Something might be wrong")
|
||||||
|
```
|
||||||
|
|
||||||
|
### console.error(...)
|
||||||
|
|
||||||
|
Logs an error message.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
console.error("Something went wrong:", errorMessage)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
|
||||||
|
### tostring(value)
|
||||||
|
|
||||||
|
Converts a value to a string.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local str = tostring(123) -- "123"
|
||||||
|
```
|
||||||
|
|
||||||
|
### tonumber(value)
|
||||||
|
|
||||||
|
Converts a value to a number.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local num = tonumber("123") -- 123
|
||||||
|
local invalid = tonumber("abc") -- nil
|
||||||
|
```
|
||||||
|
|
||||||
|
### type(value)
|
||||||
|
|
||||||
|
Returns the type of a value as a string.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type("hello") -- "string"
|
||||||
|
type(123) -- "number"
|
||||||
|
type(true) -- "boolean"
|
||||||
|
type({}) -- "table"
|
||||||
|
type(nil) -- "nil"
|
||||||
|
type(print) -- "function"
|
||||||
|
```
|
||||||
|
|
||||||
|
### pairs(table)
|
||||||
|
|
||||||
|
Iterator for all key-value pairs.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for key, value in pairs(myTable) do
|
||||||
|
print(key, value)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### ipairs(table)
|
||||||
|
|
||||||
|
Iterator for array elements (integer keys starting from 1).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for index, value in ipairs(myArray) do
|
||||||
|
print(index, value)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### pcall(func, ...)
|
||||||
|
|
||||||
|
Calls a function in protected mode (catches errors).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local success, result = pcall(function()
|
||||||
|
return json.decode(maybeInvalidJson)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
print("Parsed:", result)
|
||||||
|
else
|
||||||
|
print("Error:", result)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Libraries
|
||||||
|
|
||||||
|
### string
|
||||||
|
|
||||||
|
```lua
|
||||||
|
string.len(s) -- length
|
||||||
|
string.upper(s) -- uppercase
|
||||||
|
string.lower(s) -- lowercase
|
||||||
|
string.sub(s, i, j) -- substring
|
||||||
|
string.find(s, pattern) -- find pattern
|
||||||
|
string.gsub(s, pattern, repl) -- replace
|
||||||
|
string.match(s, pattern) -- match pattern
|
||||||
|
string.format(fmt, ...) -- format string
|
||||||
|
string.byte(s, i) -- character code
|
||||||
|
string.char(...) -- character from code
|
||||||
|
string.rep(s, n) -- repeat string
|
||||||
|
string.reverse(s) -- reverse string
|
||||||
|
string.split(s, sep) -- split by separator (extension)
|
||||||
|
string.trim(s) -- trim whitespace (extension)
|
||||||
|
```
|
||||||
|
|
||||||
|
### math
|
||||||
|
|
||||||
|
```lua
|
||||||
|
math.abs(x) -- absolute value
|
||||||
|
math.ceil(x) -- round up
|
||||||
|
math.floor(x) -- round down
|
||||||
|
math.round(x) -- round to nearest (extension)
|
||||||
|
math.max(...) -- maximum
|
||||||
|
math.min(...) -- minimum
|
||||||
|
math.sqrt(x) -- square root
|
||||||
|
math.pow(x, y) -- power
|
||||||
|
math.exp(x) -- e^x
|
||||||
|
math.log(x) -- natural log
|
||||||
|
math.sin(x) -- sine
|
||||||
|
math.cos(x) -- cosine
|
||||||
|
math.tan(x) -- tangent
|
||||||
|
math.asin(x) -- arc sine
|
||||||
|
math.acos(x) -- arc cosine
|
||||||
|
math.atan(x) -- arc tangent
|
||||||
|
math.atan2(y, x) -- arc tangent of y/x
|
||||||
|
math.deg(x) -- radians to degrees
|
||||||
|
math.rad(x) -- degrees to radians
|
||||||
|
math.random() -- random 0-1
|
||||||
|
math.random(n) -- random 1-n
|
||||||
|
math.random(m, n) -- random m-n
|
||||||
|
math.randomseed(x) -- set random seed
|
||||||
|
math.pi -- 3.14159...
|
||||||
|
math.huge -- infinity
|
||||||
|
```
|
||||||
|
|
||||||
|
### table
|
||||||
|
|
||||||
|
```lua
|
||||||
|
table.insert(t, value) -- append
|
||||||
|
table.insert(t, pos, value) -- insert at position
|
||||||
|
table.remove(t) -- remove last
|
||||||
|
table.remove(t, pos) -- remove at position
|
||||||
|
table.sort(t) -- sort ascending
|
||||||
|
table.sort(t, comp) -- sort with comparator
|
||||||
|
table.concat(t, sep) -- join to string
|
||||||
|
table.unpack(t) -- unpack to values (extension)
|
||||||
|
table.pack(...) -- pack values to table (extension)
|
||||||
|
```
|
||||||
|
|
||||||
|
### os
|
||||||
|
|
||||||
|
```lua
|
||||||
|
os.time() -- current timestamp
|
||||||
|
os.time(t) -- timestamp from table
|
||||||
|
os.date() -- current date string
|
||||||
|
os.date(format) -- formatted date
|
||||||
|
os.date(format, t) -- formatted date for timestamp
|
||||||
|
os.date("*t") -- date as table
|
||||||
|
os.difftime(t2, t1) -- time difference
|
||||||
|
os.clock() -- CPU time used
|
||||||
|
```
|
||||||
|
|
||||||
|
Date format codes:
|
||||||
|
- `%Y` - 4-digit year
|
||||||
|
- `%m` - month (01-12)
|
||||||
|
- `%d` - day (01-31)
|
||||||
|
- `%H` - hour (00-23)
|
||||||
|
- `%M` - minute (00-59)
|
||||||
|
- `%S` - second (00-59)
|
||||||
|
- `%a` - abbreviated weekday
|
||||||
|
- `%A` - full weekday
|
||||||
|
- `%b` - abbreviated month
|
||||||
|
- `%B` - full month
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Lua Scripting Guide](../guides/lua-scripting.md) - Tutorials and examples
|
||||||
|
- [Permissions Guide](../guides/permissions.md) - Permission system
|
||||||
|
- [UI Design Guide](../guides/ui-design.md) - RML/RCSS reference
|
||||||
341
portal/internal/web/docs/api/manifest.md
Normal file
341
portal/internal/web/docs/api/manifest.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# Manifest Reference
|
||||||
|
|
||||||
|
Every Mosis app requires a `manifest.json` file in the root of the package. This file describes your app and its requirements.
|
||||||
|
|
||||||
|
## Complete Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.myapp",
|
||||||
|
"name": "My App",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "assets/main.rml",
|
||||||
|
"permissions": [],
|
||||||
|
"min_mosis_version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Developer Name",
|
||||||
|
"email": "dev@example.com",
|
||||||
|
"url": "https://example.com"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"32": "icons/icon-32.png",
|
||||||
|
"64": "icons/icon-64.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
},
|
||||||
|
"description": "A short description of your app",
|
||||||
|
"category": "utilities",
|
||||||
|
"screenshots": [
|
||||||
|
"screenshots/1.png",
|
||||||
|
"screenshots/2.png"
|
||||||
|
],
|
||||||
|
"locales": {
|
||||||
|
"default": "en",
|
||||||
|
"supported": ["en", "es", "fr"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Fields
|
||||||
|
|
||||||
|
### id
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Unique package identifier in reverse domain notation. Must match the ID registered in the developer portal.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"id": "com.yourcompany.appname"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Lowercase letters, numbers, and periods only
|
||||||
|
- Must have at least two segments (e.g., `com.app`)
|
||||||
|
- Maximum 255 characters
|
||||||
|
- Cannot start or end with a period
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Display name shown to users. Maximum 50 characters.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"name": "My Awesome App"
|
||||||
|
```
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Human-readable version string following semantic versioning (MAJOR.MINOR.PATCH).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"version": "1.0.0"
|
||||||
|
"version": "2.1.3-beta"
|
||||||
|
```
|
||||||
|
|
||||||
|
### version_code
|
||||||
|
|
||||||
|
**Type:** `integer`
|
||||||
|
|
||||||
|
Numeric version code that must increase with each release. Used to determine if an update is available.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"version_code": 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Must be a positive integer
|
||||||
|
- Must be greater than all previously published versions
|
||||||
|
- Maximum value: 2147483647
|
||||||
|
|
||||||
|
### entry
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Path to the main RML file, relative to package root.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"entry": "assets/main.rml"
|
||||||
|
```
|
||||||
|
|
||||||
|
### author
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
Information about the app developer.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"author": {
|
||||||
|
"name": "Developer Name",
|
||||||
|
"email": "dev@example.com",
|
||||||
|
"url": "https://example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `name` | string | Yes | Developer or company name |
|
||||||
|
| `email` | string | Yes | Contact email |
|
||||||
|
| `url` | string | No | Website URL |
|
||||||
|
|
||||||
|
### icons
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
App icons at various sizes. At minimum, provide a 128px icon.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"icons": {
|
||||||
|
"32": "icons/icon-32.png",
|
||||||
|
"64": "icons/icon-64.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported sizes: 32, 64, 128, 256, 512
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- PNG format recommended
|
||||||
|
- Square aspect ratio
|
||||||
|
- No transparency on edges (for proper display)
|
||||||
|
|
||||||
|
## Optional Fields
|
||||||
|
|
||||||
|
### description
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Short description shown in app listings. Maximum 200 characters.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"description": "A simple calculator for everyday math."
|
||||||
|
```
|
||||||
|
|
||||||
|
### permissions
|
||||||
|
|
||||||
|
**Type:** `array<string>`
|
||||||
|
|
||||||
|
List of permissions your app requires. Apps cannot access restricted features without declaring permissions.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"permissions": ["storage", "network"]
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Permissions](#permissions-reference) below.
|
||||||
|
|
||||||
|
### min_mosis_version
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
Minimum Mosis version required to run this app.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"min_mosis_version": "1.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
If omitted, defaults to `"1.0.0"`.
|
||||||
|
|
||||||
|
### category
|
||||||
|
|
||||||
|
**Type:** `string`
|
||||||
|
|
||||||
|
App store category for discovery.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"category": "productivity"
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid categories:
|
||||||
|
- `games`
|
||||||
|
- `entertainment`
|
||||||
|
- `productivity`
|
||||||
|
- `utilities`
|
||||||
|
- `social`
|
||||||
|
- `communication`
|
||||||
|
- `lifestyle`
|
||||||
|
- `education`
|
||||||
|
- `health`
|
||||||
|
- `finance`
|
||||||
|
- `news`
|
||||||
|
- `other`
|
||||||
|
|
||||||
|
### screenshots
|
||||||
|
|
||||||
|
**Type:** `array<string>`
|
||||||
|
|
||||||
|
Paths to screenshot images for app store listing.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"screenshots": [
|
||||||
|
"screenshots/home.png",
|
||||||
|
"screenshots/settings.png",
|
||||||
|
"screenshots/detail.png"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- PNG format
|
||||||
|
- 1080x1920 (9:16 portrait) recommended
|
||||||
|
- Maximum 5 screenshots
|
||||||
|
|
||||||
|
### locales
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
Internationalization configuration.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"locales": {
|
||||||
|
"default": "en",
|
||||||
|
"supported": ["en", "es", "fr", "de", "ja"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `default` | string | Default locale code |
|
||||||
|
| `supported` | array | List of supported locale codes |
|
||||||
|
|
||||||
|
Locale files should be placed in `locales/{code}.json`.
|
||||||
|
|
||||||
|
## Permissions Reference
|
||||||
|
|
||||||
|
| Permission | Description | Example Use |
|
||||||
|
|------------|-------------|-------------|
|
||||||
|
| `storage` | Persist data locally | Save user preferences |
|
||||||
|
| `network` | Make HTTP requests | Fetch remote data |
|
||||||
|
| `clipboard` | Read/write clipboard | Copy text |
|
||||||
|
| `notifications` | Show notifications | Reminders |
|
||||||
|
| `camera` | Access device camera | Photo capture |
|
||||||
|
| `location` | Get device location | Maps, weather |
|
||||||
|
| `contacts` | Read contacts | Contact picker |
|
||||||
|
| `microphone` | Record audio | Voice notes |
|
||||||
|
|
||||||
|
### Permission Declaration
|
||||||
|
|
||||||
|
```json
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"network"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Users are informed of permissions before installing. Request only what you need.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The package builder validates your manifest. Common errors:
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| `Invalid package ID` | ID doesn't match pattern | Use `com.company.app` format |
|
||||||
|
| `Missing required field` | Required field omitted | Add the field |
|
||||||
|
| `Invalid version_code` | Not a positive integer | Use positive number |
|
||||||
|
| `Icon not found` | Icon path doesn't exist | Check file paths |
|
||||||
|
| `Invalid permission` | Unknown permission | Use valid permission name |
|
||||||
|
|
||||||
|
## Example: Minimal Manifest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.hello",
|
||||||
|
"name": "Hello World",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "main.rml",
|
||||||
|
"author": {
|
||||||
|
"name": "Developer",
|
||||||
|
"email": "dev@example.com"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"128": "icon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Full Manifest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.acme.calculator",
|
||||||
|
"name": "ACME Calculator",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"version_code": 5,
|
||||||
|
"entry": "assets/main.rml",
|
||||||
|
"description": "A powerful calculator with scientific functions.",
|
||||||
|
"category": "utilities",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"clipboard"
|
||||||
|
],
|
||||||
|
"min_mosis_version": "1.2.0",
|
||||||
|
"author": {
|
||||||
|
"name": "ACME Corp",
|
||||||
|
"email": "apps@acme.com",
|
||||||
|
"url": "https://acme.com"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"32": "icons/icon-32.png",
|
||||||
|
"64": "icons/icon-64.png",
|
||||||
|
"128": "icons/icon-128.png",
|
||||||
|
"256": "icons/icon-256.png"
|
||||||
|
},
|
||||||
|
"screenshots": [
|
||||||
|
"screenshots/basic.png",
|
||||||
|
"screenshots/scientific.png",
|
||||||
|
"screenshots/history.png"
|
||||||
|
],
|
||||||
|
"locales": {
|
||||||
|
"default": "en",
|
||||||
|
"supported": ["en", "es", "fr", "de"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Getting Started](../getting-started.md) - Create your first app
|
||||||
|
- [Permissions Guide](../guides/permissions.md) - Understanding permissions
|
||||||
|
- [Publishing Guide](../guides/publishing.md) - Submit your app
|
||||||
576
portal/internal/web/docs/cli.md
Normal file
576
portal/internal/web/docs/cli.md
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
# CLI Reference
|
||||||
|
|
||||||
|
The Mosis CLI (`mosis`) is a command-line tool for building, testing, and publishing Mosis apps.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Download the installer from your [Developer Dashboard](/dashboard) or use:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Using winget
|
||||||
|
winget install omixlab.mosis-cli
|
||||||
|
|
||||||
|
# Or download directly
|
||||||
|
curl -o mosis-cli.exe https://dl.omixlab.com/cli/windows/mosis.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Homebrew
|
||||||
|
brew install omixlab/tap/mosis-cli
|
||||||
|
|
||||||
|
# Or download directly
|
||||||
|
curl -fsSL https://dl.omixlab.com/cli/macos/mosis > /usr/local/bin/mosis
|
||||||
|
chmod +x /usr/local/bin/mosis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download binary
|
||||||
|
curl -fsSL https://dl.omixlab.com/cli/linux/mosis > ~/.local/bin/mosis
|
||||||
|
chmod +x ~/.local/bin/mosis
|
||||||
|
|
||||||
|
# Or using snap
|
||||||
|
sudo snap install mosis-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new project
|
||||||
|
mosis init myapp
|
||||||
|
|
||||||
|
# Build the package
|
||||||
|
cd myapp
|
||||||
|
mosis build
|
||||||
|
|
||||||
|
# Test locally
|
||||||
|
mosis run
|
||||||
|
|
||||||
|
# Login and publish
|
||||||
|
mosis login
|
||||||
|
mosis publish
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### mosis init
|
||||||
|
|
||||||
|
Create a new Mosis app project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis init <name> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `name` - Project name (creates directory)
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--template <name>` | Use a starter template |
|
||||||
|
| `--package-id <id>` | Set package ID |
|
||||||
|
| `--no-git` | Don't initialize git repo |
|
||||||
|
|
||||||
|
**Templates:**
|
||||||
|
- `default` - Basic app structure
|
||||||
|
- `minimal` - Bare minimum files
|
||||||
|
- `navigation` - Multi-screen with navigation
|
||||||
|
- `form` - Form handling example
|
||||||
|
- `list` - Scrollable list example
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
mosis init myapp --template navigation --package-id com.example.myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── manifest.json
|
||||||
|
├── icon.png
|
||||||
|
├── assets/
|
||||||
|
│ ├── main.rml
|
||||||
|
│ └── styles.rcss
|
||||||
|
└── .mosis/
|
||||||
|
└── config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis build
|
||||||
|
|
||||||
|
Build a `.mosis` package from your project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis build [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-o, --output <path>` | Output file path |
|
||||||
|
| `--no-sign` | Skip signing (dev only) |
|
||||||
|
| `--verbose` | Show detailed output |
|
||||||
|
| `--validate-only` | Validate without building |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
mosis build -o dist/myapp.mosis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build Process:**
|
||||||
|
1. Validates manifest.json
|
||||||
|
2. Checks all referenced files exist
|
||||||
|
3. Validates RML/RCSS syntax
|
||||||
|
4. Creates compressed package
|
||||||
|
5. Signs with developer key (if available)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis validate
|
||||||
|
|
||||||
|
Validate your project without building.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis validate [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--strict` | Enable strict validation |
|
||||||
|
| `--fix` | Auto-fix simple issues |
|
||||||
|
|
||||||
|
**Checks performed:**
|
||||||
|
- Manifest schema validation
|
||||||
|
- Required files existence
|
||||||
|
- Icon sizes and formats
|
||||||
|
- RML/RCSS syntax
|
||||||
|
- Lua syntax
|
||||||
|
- Package size limits
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```
|
||||||
|
✓ manifest.json is valid
|
||||||
|
✓ All required icons present
|
||||||
|
✓ Entry point exists: assets/main.rml
|
||||||
|
✓ RML syntax valid (3 files)
|
||||||
|
✓ RCSS syntax valid (2 files)
|
||||||
|
✓ Lua syntax valid (1 file)
|
||||||
|
✓ Package size: 45KB (under 10MB limit)
|
||||||
|
|
||||||
|
Validation passed!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis run
|
||||||
|
|
||||||
|
Run your app in the local designer for testing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis run [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--device <name>` | Target device profile |
|
||||||
|
| `--scale <factor>` | Window scale factor |
|
||||||
|
| `--hot-reload` | Enable hot reload (default) |
|
||||||
|
| `--no-hot-reload` | Disable hot reload |
|
||||||
|
|
||||||
|
**Device profiles:**
|
||||||
|
- `phone` - Standard phone (1080x1920)
|
||||||
|
- `tablet` - Tablet (1200x1920)
|
||||||
|
- `watch` - Watch (360x360)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
mosis run --device phone --scale 0.5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis login
|
||||||
|
|
||||||
|
Authenticate with the developer portal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis login [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--token <token>` | Use API token directly |
|
||||||
|
| `--browser` | Open browser for OAuth |
|
||||||
|
|
||||||
|
**Interactive login:**
|
||||||
|
```bash
|
||||||
|
$ mosis login
|
||||||
|
Opening browser for authentication...
|
||||||
|
✓ Logged in as developer@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token login (for CI/CD):**
|
||||||
|
```bash
|
||||||
|
mosis login --token YOUR_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis logout
|
||||||
|
|
||||||
|
Log out of the developer portal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis logout
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis publish
|
||||||
|
|
||||||
|
Upload and submit your app for review.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis publish [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--notes <text>` | Release notes |
|
||||||
|
| `--notes-file <path>` | Release notes from file |
|
||||||
|
| `--draft` | Upload as draft (don't submit) |
|
||||||
|
| `--track <name>` | Release track (production/beta) |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
mosis publish --notes "Bug fixes and performance improvements"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Builds package (if needed)
|
||||||
|
2. Uploads to portal
|
||||||
|
3. Runs automated validation
|
||||||
|
4. Submits for review (unless `--draft`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis status
|
||||||
|
|
||||||
|
Check the status of your app submissions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis status [app-id]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```
|
||||||
|
com.example.myapp
|
||||||
|
|
||||||
|
Latest Version: 1.2.0 (code: 5)
|
||||||
|
Status: In Review
|
||||||
|
Submitted: 2 hours ago
|
||||||
|
|
||||||
|
Previous Versions:
|
||||||
|
1.1.0 (4) - Published
|
||||||
|
1.0.0 (1) - Published
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis keys
|
||||||
|
|
||||||
|
Manage signing keys.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis keys <subcommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Subcommands:**
|
||||||
|
|
||||||
|
#### keys generate
|
||||||
|
|
||||||
|
Generate a new signing keypair.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis keys generate [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-o, --output <path>` | Output directory |
|
||||||
|
| `--name <name>` | Key name |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis keys generate --name production
|
||||||
|
Generated keypair:
|
||||||
|
Private: ~/.mosis/keys/production.key
|
||||||
|
Public: ~/.mosis/keys/production.pub
|
||||||
|
|
||||||
|
Keep your private key secure! Never share it.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### keys register
|
||||||
|
|
||||||
|
Upload your public key to the portal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis keys register <key-file>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis keys register ~/.mosis/keys/production.pub
|
||||||
|
✓ Key registered successfully
|
||||||
|
Key ID: k_abc123xyz
|
||||||
|
Algorithm: Ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
#### keys list
|
||||||
|
|
||||||
|
List registered keys.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis keys list
|
||||||
|
ID Name Created Status
|
||||||
|
k_abc123xyz production 2024-01-15 Active
|
||||||
|
k_def456uvw development 2024-01-10 Active
|
||||||
|
```
|
||||||
|
|
||||||
|
#### keys revoke
|
||||||
|
|
||||||
|
Revoke a registered key.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis keys revoke <key-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis config
|
||||||
|
|
||||||
|
Manage CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis config <subcommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Subcommands:**
|
||||||
|
|
||||||
|
#### config get
|
||||||
|
|
||||||
|
Get a configuration value.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis config get <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### config set
|
||||||
|
|
||||||
|
Set a configuration value.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis config set <key> <value>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### config list
|
||||||
|
|
||||||
|
List all configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis config list
|
||||||
|
api_url = https://api.omixlab.com
|
||||||
|
designer_path = /usr/local/bin/mosis-designer
|
||||||
|
default_key = production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration keys:**
|
||||||
|
| Key | Description | Default |
|
||||||
|
|-----|-------------|---------|
|
||||||
|
| `api_url` | API endpoint | https://api.omixlab.com |
|
||||||
|
| `designer_path` | Path to designer | (auto-detected) |
|
||||||
|
| `default_key` | Default signing key | (none) |
|
||||||
|
| `auto_build` | Build before publish | true |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis doctor
|
||||||
|
|
||||||
|
Diagnose common issues with your setup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis doctor
|
||||||
|
Checking Mosis CLI installation...
|
||||||
|
|
||||||
|
✓ CLI version: 1.2.0
|
||||||
|
✓ Designer found: /usr/local/bin/mosis-designer
|
||||||
|
✓ Authenticated as: developer@example.com
|
||||||
|
✓ Signing key configured: production
|
||||||
|
✓ Network connectivity OK
|
||||||
|
|
||||||
|
All checks passed!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis version
|
||||||
|
|
||||||
|
Show CLI version information.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mosis version
|
||||||
|
mosis-cli version 1.2.0
|
||||||
|
Built: 2024-01-15
|
||||||
|
Go: 1.21.5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### mosis help
|
||||||
|
|
||||||
|
Show help for any command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis help [command]
|
||||||
|
mosis <command> --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MOSIS_API_URL` | Override API endpoint |
|
||||||
|
| `MOSIS_TOKEN` | API token for authentication |
|
||||||
|
| `MOSIS_KEY_PATH` | Path to signing key |
|
||||||
|
| `MOSIS_NO_COLOR` | Disable colored output |
|
||||||
|
| `MOSIS_DEBUG` | Enable debug logging |
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
### Global Config
|
||||||
|
|
||||||
|
Location: `~/.mosis/config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_url": "https://api.omixlab.com",
|
||||||
|
"default_key": "production",
|
||||||
|
"auto_build": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Config
|
||||||
|
|
||||||
|
Location: `.mosis/config.json` (in project root)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"signing_key": "production",
|
||||||
|
"build_output": "dist/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | General error |
|
||||||
|
| 2 | Invalid arguments |
|
||||||
|
| 3 | Authentication required |
|
||||||
|
| 4 | Validation failed |
|
||||||
|
| 5 | Network error |
|
||||||
|
| 6 | Build failed |
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Publish Mosis App
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Mosis CLI
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://dl.omixlab.com/cli/linux/mosis > mosis
|
||||||
|
chmod +x mosis
|
||||||
|
sudo mv mosis /usr/local/bin/
|
||||||
|
|
||||||
|
- name: Build and Publish
|
||||||
|
env:
|
||||||
|
MOSIS_TOKEN: ${{ secrets.MOSIS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mosis build
|
||||||
|
mosis publish --notes "Release ${GITHUB_REF#refs/tags/}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
publish:
|
||||||
|
image: ubuntu:latest
|
||||||
|
script:
|
||||||
|
- curl -fsSL https://dl.omixlab.com/cli/linux/mosis > /usr/local/bin/mosis
|
||||||
|
- chmod +x /usr/local/bin/mosis
|
||||||
|
- mosis build
|
||||||
|
- mosis publish --notes "Release $CI_COMMIT_TAG"
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
variables:
|
||||||
|
MOSIS_TOKEN: $MOSIS_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Command not found"
|
||||||
|
|
||||||
|
Ensure the CLI is in your PATH:
|
||||||
|
```bash
|
||||||
|
echo $PATH
|
||||||
|
which mosis
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Authentication failed"
|
||||||
|
|
||||||
|
Re-login:
|
||||||
|
```bash
|
||||||
|
mosis logout
|
||||||
|
mosis login
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Build failed: Invalid manifest"
|
||||||
|
|
||||||
|
Run validation for details:
|
||||||
|
```bash
|
||||||
|
mosis validate --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Network error"
|
||||||
|
|
||||||
|
Check connectivity:
|
||||||
|
```bash
|
||||||
|
mosis doctor
|
||||||
|
curl -I https://api.omixlab.com/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Getting Started](getting-started.md) - First app tutorial
|
||||||
|
- [Publishing Guide](guides/publishing.md) - Submission tips
|
||||||
|
- [API Reference](api/lua-api.md) - Lua API documentation
|
||||||
267
portal/internal/web/docs/faq.md
Normal file
267
portal/internal/web/docs/faq.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
### What is Mosis?
|
||||||
|
|
||||||
|
Mosis is a virtual smartphone OS for VR games and applications. It provides a phone-like device that users can interact with inside VR environments, complete with real smartphone functionality.
|
||||||
|
|
||||||
|
### Who can develop apps for Mosis?
|
||||||
|
|
||||||
|
Anyone! Sign up for a free developer account to start building apps. There's no fee to register or submit apps.
|
||||||
|
|
||||||
|
### What can I build with Mosis?
|
||||||
|
|
||||||
|
You can build any app that works on a phone screen:
|
||||||
|
- Utilities (calculators, converters, timers)
|
||||||
|
- Productivity (notes, to-do lists, calendars)
|
||||||
|
- Games (puzzles, casual games)
|
||||||
|
- Entertainment (media players, readers)
|
||||||
|
- Social apps (chat, messaging)
|
||||||
|
- And more!
|
||||||
|
|
||||||
|
### How do users get my app?
|
||||||
|
|
||||||
|
Users discover and install apps through the Mosis App Store, which is built into the virtual phone. Published apps appear in store listings where users can browse, search, and install.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### What languages/technologies do I need to know?
|
||||||
|
|
||||||
|
Mosis apps use:
|
||||||
|
- **RML** - Similar to HTML for structure
|
||||||
|
- **RCSS** - Similar to CSS for styling
|
||||||
|
- **Lua** - Lightweight scripting language for logic
|
||||||
|
|
||||||
|
If you know HTML/CSS, you'll find RML/RCSS very familiar. Lua is simple to learn and has many tutorials available.
|
||||||
|
|
||||||
|
### Can I use JavaScript instead of Lua?
|
||||||
|
|
||||||
|
No, Mosis uses Lua for scripting. Lua was chosen for its:
|
||||||
|
- Lightweight footprint
|
||||||
|
- Easy sandboxing for security
|
||||||
|
- Simple learning curve
|
||||||
|
- Fast execution
|
||||||
|
|
||||||
|
### Can I use React/Vue/Angular?
|
||||||
|
|
||||||
|
No, Mosis uses its own RML/RCSS system based on RmlUi. Standard web frameworks won't work, but the concepts are similar enough that web developers can adapt quickly.
|
||||||
|
|
||||||
|
### What IDEs are supported?
|
||||||
|
|
||||||
|
Use any text editor! VS Code is recommended with these extensions:
|
||||||
|
- Lua Language Server
|
||||||
|
- XML/HTML tools for RML editing
|
||||||
|
|
||||||
|
### Is there a visual designer?
|
||||||
|
|
||||||
|
The Desktop Designer provides:
|
||||||
|
- Live preview of your app
|
||||||
|
- Hot reload on file changes
|
||||||
|
- Hierarchy inspection
|
||||||
|
- Screenshot capture
|
||||||
|
|
||||||
|
It's included in your developer tools download.
|
||||||
|
|
||||||
|
### Can I test on a real device?
|
||||||
|
|
||||||
|
Yes! You can:
|
||||||
|
1. Install the Designer on your PC
|
||||||
|
2. Build a .mosis package
|
||||||
|
3. Sideload onto a VR device with MosisService installed
|
||||||
|
|
||||||
|
### How large can my app be?
|
||||||
|
|
||||||
|
The maximum package size is **10MB**. This is plenty for most apps. If you need more:
|
||||||
|
- Optimize images (use TGA format)
|
||||||
|
- Remove unused assets
|
||||||
|
- Load large data from the network
|
||||||
|
|
||||||
|
### Can I use external APIs?
|
||||||
|
|
||||||
|
Yes, with the `network` permission. Make HTTPS requests to any API:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.get("https://api.example.com/data", function(response)
|
||||||
|
local data = json.decode(response.body)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can I access the device camera/microphone?
|
||||||
|
|
||||||
|
Yes, with the appropriate permissions:
|
||||||
|
- `camera` - For photo capture
|
||||||
|
- `microphone` - For audio recording
|
||||||
|
|
||||||
|
Users will be prompted to grant access.
|
||||||
|
|
||||||
|
### Can my app run in the background?
|
||||||
|
|
||||||
|
Currently, apps only run when visible. Background execution is planned for future versions.
|
||||||
|
|
||||||
|
### Can I access native device features?
|
||||||
|
|
||||||
|
Mosis apps are sandboxed for security. Available device features:
|
||||||
|
- Storage
|
||||||
|
- Network
|
||||||
|
- Camera (with permission)
|
||||||
|
- Microphone (with permission)
|
||||||
|
- Clipboard
|
||||||
|
- Notifications
|
||||||
|
|
||||||
|
Direct hardware access (Bluetooth, USB, etc.) is not available.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
### How long does review take?
|
||||||
|
|
||||||
|
Most apps are reviewed within 24-48 hours. Apps requesting sensitive permissions may take longer.
|
||||||
|
|
||||||
|
Automated checks run instantly. Manual review is triggered for:
|
||||||
|
- First-time developers
|
||||||
|
- Sensitive permissions
|
||||||
|
- Flagged content
|
||||||
|
|
||||||
|
### Why was my app rejected?
|
||||||
|
|
||||||
|
Check the rejection reason in your dashboard. Common reasons:
|
||||||
|
- Crashes on launch
|
||||||
|
- Missing required assets
|
||||||
|
- Policy violations
|
||||||
|
- Inappropriate content
|
||||||
|
- Misleading metadata
|
||||||
|
|
||||||
|
### Can I update my app?
|
||||||
|
|
||||||
|
Yes! Submit a new version with:
|
||||||
|
- Higher `version_code`
|
||||||
|
- Updated `version` string
|
||||||
|
- Release notes
|
||||||
|
|
||||||
|
Updates go through the same review process.
|
||||||
|
|
||||||
|
### Can I remove my app from the store?
|
||||||
|
|
||||||
|
Yes, go to your app's settings and choose "Unpublish". Users who installed it can keep using it, but it won't appear in searches.
|
||||||
|
|
||||||
|
### Can I have paid apps?
|
||||||
|
|
||||||
|
Currently, all apps are free. Paid apps and in-app purchases are planned for future versions.
|
||||||
|
|
||||||
|
### What's the revenue share?
|
||||||
|
|
||||||
|
When monetization launches, the split will be:
|
||||||
|
- **70%** to developers
|
||||||
|
- **30%** to Mosis platform
|
||||||
|
|
||||||
|
### Can I distribute outside the store?
|
||||||
|
|
||||||
|
Yes, you can share `.mosis` files directly. However:
|
||||||
|
- Users must enable sideloading
|
||||||
|
- Updates won't be automatic
|
||||||
|
- No store discoverability
|
||||||
|
|
||||||
|
## Technical
|
||||||
|
|
||||||
|
### What RML/RCSS version is supported?
|
||||||
|
|
||||||
|
Mosis uses RmlUi 6.x. The [UI Design Guide](guides/ui-design.md) covers supported features. Not all CSS3 features are available.
|
||||||
|
|
||||||
|
### What Lua version is supported?
|
||||||
|
|
||||||
|
Lua 5.4 with some restrictions for sandboxing. See the [Lua Scripting Guide](guides/lua-scripting.md) for details.
|
||||||
|
|
||||||
|
### Are there size limits for storage?
|
||||||
|
|
||||||
|
Each app has 5MB of local storage. For more data, use network storage.
|
||||||
|
|
||||||
|
### Can I use databases?
|
||||||
|
|
||||||
|
Use the `storage` API for key-value storage. SQLite is not directly available, but you can:
|
||||||
|
- Store JSON data
|
||||||
|
- Use a remote database via network
|
||||||
|
|
||||||
|
### How do I handle different screen sizes?
|
||||||
|
|
||||||
|
Design for the standard phone screen (1080x1920 logical pixels). Use:
|
||||||
|
- `dp` units for consistent sizing
|
||||||
|
- Flexbox for flexible layouts
|
||||||
|
- Percentage widths for adaptability
|
||||||
|
|
||||||
|
### Can I create multiple screens?
|
||||||
|
|
||||||
|
Yes, use the navigation system:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
navigateTo("settings") -- Load settings.rml
|
||||||
|
goBack() -- Return to previous screen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can apps communicate with each other?
|
||||||
|
|
||||||
|
Currently, apps are isolated. Inter-app communication is planned for future versions.
|
||||||
|
|
||||||
|
### What happens if my app crashes?
|
||||||
|
|
||||||
|
Crashes are caught by the sandbox. The user sees an error message and can restart. Crash reports are sent to your analytics dashboard (if telemetry is enabled).
|
||||||
|
|
||||||
|
### Can I access the file system?
|
||||||
|
|
||||||
|
No direct file system access. Use:
|
||||||
|
- `storage` API for persisted data
|
||||||
|
- Bundled assets for static files
|
||||||
|
- `http` API for remote files
|
||||||
|
|
||||||
|
## Account & Legal
|
||||||
|
|
||||||
|
### Is there a developer fee?
|
||||||
|
|
||||||
|
No, developer accounts are free. There's no cost to register, develop, or publish apps.
|
||||||
|
|
||||||
|
### Can I transfer my app to another developer?
|
||||||
|
|
||||||
|
Contact support to request a transfer.
|
||||||
|
|
||||||
|
### What content is not allowed?
|
||||||
|
|
||||||
|
- Malware or security exploits
|
||||||
|
- Hate speech or discrimination
|
||||||
|
- Adult content (unless properly rated)
|
||||||
|
- Copyright infringement
|
||||||
|
- Privacy violations
|
||||||
|
- Impersonation of other apps/brands
|
||||||
|
|
||||||
|
See the full content policy in your developer agreement.
|
||||||
|
|
||||||
|
### Do I need a privacy policy?
|
||||||
|
|
||||||
|
You need a privacy policy if your app:
|
||||||
|
- Collects user data
|
||||||
|
- Uses analytics
|
||||||
|
- Makes network requests
|
||||||
|
- Accesses contacts, location, etc.
|
||||||
|
|
||||||
|
### Who owns the IP for my app?
|
||||||
|
|
||||||
|
You retain all intellectual property rights to your app. By publishing on Mosis, you grant a license to distribute it through the store.
|
||||||
|
|
||||||
|
### Can I use open source code?
|
||||||
|
|
||||||
|
Yes, but respect the licenses:
|
||||||
|
- MIT, BSD, Apache: Generally safe
|
||||||
|
- GPL: May require source distribution
|
||||||
|
- Proprietary: Check terms carefully
|
||||||
|
|
||||||
|
## More Questions?
|
||||||
|
|
||||||
|
If your question isn't answered here:
|
||||||
|
|
||||||
|
1. Check the [Troubleshooting](troubleshooting.md) guide
|
||||||
|
2. Search the developer forum
|
||||||
|
3. Contact support through your dashboard
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Getting Started](getting-started.md) - Create your first app
|
||||||
|
- [Troubleshooting](troubleshooting.md) - Common problems and solutions
|
||||||
|
- [API Reference](api/lua-api.md) - Complete API documentation
|
||||||
190
portal/internal/web/docs/getting-started.md
Normal file
190
portal/internal/web/docs/getting-started.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
This guide walks you through creating your first Mosis app in under 10 minutes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Mosis developer account ([sign up here](/register))
|
||||||
|
- Text editor (VS Code recommended)
|
||||||
|
- Desktop Designer for testing (download from portal)
|
||||||
|
|
||||||
|
## Step 1: Create a New App
|
||||||
|
|
||||||
|
1. Log in to the [Developer Portal](/dashboard)
|
||||||
|
2. Click **Create New App**
|
||||||
|
3. Fill in the details:
|
||||||
|
- **Package ID**: `com.yourname.myapp` (unique identifier)
|
||||||
|
- **App Name**: My First App
|
||||||
|
- **Description**: A simple hello world app
|
||||||
|
4. Click **Create**
|
||||||
|
|
||||||
|
## Step 2: Set Up Your Project
|
||||||
|
|
||||||
|
Create a project folder with this structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── manifest.json
|
||||||
|
├── icon.png
|
||||||
|
└── assets/
|
||||||
|
├── main.rml
|
||||||
|
└── styles.rcss
|
||||||
|
```
|
||||||
|
|
||||||
|
### manifest.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.yourname.myapp",
|
||||||
|
"name": "My First App",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "assets/main.rml",
|
||||||
|
"permissions": [],
|
||||||
|
"min_mosis_version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Your Name",
|
||||||
|
"email": "you@example.com"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"32": "icon.png",
|
||||||
|
"64": "icon.png",
|
||||||
|
"128": "icon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### assets/main.rml
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<rml>
|
||||||
|
<head>
|
||||||
|
<title>My First App</title>
|
||||||
|
<link type="text/rcss" href="styles.rcss"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Hello, Mosis!</h1>
|
||||||
|
<p>This is my first app.</p>
|
||||||
|
<button id="click-me" onclick="handleClick()">Click Me</button>
|
||||||
|
<p id="counter">Clicks: 0</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
local clicks = 0
|
||||||
|
|
||||||
|
function handleClick()
|
||||||
|
clicks = clicks + 1
|
||||||
|
document:GetElementById("counter").inner_rml = "Clicks: " .. clicks
|
||||||
|
end
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
|
```
|
||||||
|
|
||||||
|
### assets/styles.rcss
|
||||||
|
|
||||||
|
```css
|
||||||
|
body {
|
||||||
|
font-family: LatoLatin;
|
||||||
|
background-color: #1a1a2e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 20dp;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24dp;
|
||||||
|
margin-bottom: 10dp;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16dp;
|
||||||
|
margin-bottom: 20dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #00d4ff;
|
||||||
|
color: #1a1a2e;
|
||||||
|
padding: 12dp 24dp;
|
||||||
|
border-radius: 8dp;
|
||||||
|
font-size: 16dp;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #00b8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #0099cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#counter {
|
||||||
|
font-size: 18dp;
|
||||||
|
margin-top: 20dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### icon.png
|
||||||
|
|
||||||
|
Create a 128x128 PNG icon for your app. Use any image editor or find a placeholder icon.
|
||||||
|
|
||||||
|
## Step 3: Test Locally
|
||||||
|
|
||||||
|
1. Download and install the Desktop Designer from your dashboard
|
||||||
|
2. Open a terminal in your project folder
|
||||||
|
3. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis-designer.exe assets/main.rml
|
||||||
|
```
|
||||||
|
|
||||||
|
The designer window opens showing your app. Changes to RML, RCSS, or Lua files automatically reload.
|
||||||
|
|
||||||
|
## Step 4: Build Your Package
|
||||||
|
|
||||||
|
From your project folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosis build
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `myapp.mosis` - your packaged app ready for submission.
|
||||||
|
|
||||||
|
## Step 5: Submit for Review
|
||||||
|
|
||||||
|
1. Go to your app in the Developer Portal
|
||||||
|
2. Click **Create New Version**
|
||||||
|
3. Upload your `.mosis` package
|
||||||
|
4. Add release notes
|
||||||
|
5. Click **Submit for Review**
|
||||||
|
|
||||||
|
Your app will be reviewed automatically. If it passes all checks, you can publish it to the store.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [UI Design Guide](guides/ui-design.md) - Learn RML/RCSS in depth
|
||||||
|
- [Lua Scripting Guide](guides/lua-scripting.md) - Add complex interactivity
|
||||||
|
- [Permissions Guide](guides/permissions.md) - Request device capabilities
|
||||||
|
- [Publishing Guide](guides/publishing.md) - Tips for successful submissions
|
||||||
|
|
||||||
|
## Example Apps
|
||||||
|
|
||||||
|
Check out these example apps to learn from:
|
||||||
|
|
||||||
|
| App | Description | Source |
|
||||||
|
|-----|-------------|--------|
|
||||||
|
| Calculator | Basic calculator | [View](examples/calculator.md) |
|
||||||
|
| Notes | Simple note-taking | [View](examples/notes.md) |
|
||||||
|
| Timer | Countdown timer | [View](examples/timer.md) |
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Join our [Discord community](#)
|
||||||
|
- Check the [FAQ](faq.md)
|
||||||
|
- Search the [Troubleshooting guide](troubleshooting.md)
|
||||||
535
portal/internal/web/docs/guides/best-practices.md
Normal file
535
portal/internal/web/docs/guides/best-practices.md
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
# Best Practices
|
||||||
|
|
||||||
|
Guidelines for building high-quality Mosis apps that users love.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Minimize DOM Queries
|
||||||
|
|
||||||
|
Cache element references instead of querying repeatedly:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Queries on every frame
|
||||||
|
function updateScore()
|
||||||
|
document:GetElementById("score").inner_rml = tostring(score)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Good: Cache the reference
|
||||||
|
local scoreElement
|
||||||
|
|
||||||
|
function onLoad()
|
||||||
|
scoreElement = document:GetElementById("score")
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateScore()
|
||||||
|
scoreElement.inner_rml = tostring(score)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch DOM Updates
|
||||||
|
|
||||||
|
Group multiple changes together:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Multiple separate updates
|
||||||
|
elem1.style.color = "red"
|
||||||
|
elem2.style.color = "red"
|
||||||
|
elem3.style.color = "red"
|
||||||
|
|
||||||
|
-- Good: Use a class
|
||||||
|
parent:SetClass("error-state", true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Efficient Data Structures
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- For frequent lookups, use tables as maps
|
||||||
|
local itemLookup = {}
|
||||||
|
for i, item in ipairs(items) do
|
||||||
|
itemLookup[item.id] = item
|
||||||
|
end
|
||||||
|
|
||||||
|
-- O(1) lookup instead of O(n) search
|
||||||
|
local item = itemLookup["item-123"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Up Timers
|
||||||
|
|
||||||
|
Always clear intervals when navigating away:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local updateInterval
|
||||||
|
|
||||||
|
function onScreenLoad()
|
||||||
|
updateInterval = setInterval(function()
|
||||||
|
updateData()
|
||||||
|
end, 1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onScreenUnload()
|
||||||
|
if updateInterval then
|
||||||
|
clearInterval(updateInterval)
|
||||||
|
updateInterval = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Load Content
|
||||||
|
|
||||||
|
Don't load everything at startup:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Load data when user scrolls to section
|
||||||
|
function onSectionVisible(sectionId)
|
||||||
|
if not loadedSections[sectionId] then
|
||||||
|
loadSectionData(sectionId)
|
||||||
|
loadedSections[sectionId] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
### Provide Feedback
|
||||||
|
|
||||||
|
Show users that actions are happening:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function onSubmit()
|
||||||
|
-- Show loading state immediately
|
||||||
|
submitButton:SetClass("loading", true)
|
||||||
|
submitButton:SetAttribute("disabled", "disabled")
|
||||||
|
|
||||||
|
http.post(url, data, function(response)
|
||||||
|
submitButton:SetClass("loading", false)
|
||||||
|
submitButton:RemoveAttribute("disabled")
|
||||||
|
|
||||||
|
if response.ok then
|
||||||
|
showSuccess("Saved!")
|
||||||
|
else
|
||||||
|
showError("Failed to save")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handle Errors Gracefully
|
||||||
|
|
||||||
|
Never show raw error messages to users:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.get(url, function(response)
|
||||||
|
if response.ok then
|
||||||
|
displayData(json.decode(response.body))
|
||||||
|
else
|
||||||
|
-- User-friendly message
|
||||||
|
showMessage("Unable to load data. Please check your connection.")
|
||||||
|
|
||||||
|
-- Log details for debugging
|
||||||
|
console.error("API error:", response.status, response.body)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make Touch Targets Large Enough
|
||||||
|
|
||||||
|
Minimum 48dp for touchable elements:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.button {
|
||||||
|
min-width: 48dp;
|
||||||
|
min-height: 48dp;
|
||||||
|
padding: 12dp 24dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
min-height: 56dp;
|
||||||
|
padding: 16dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support Undo for Destructive Actions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local deletedItem = nil
|
||||||
|
local undoTimeout = nil
|
||||||
|
|
||||||
|
function deleteItem(itemId)
|
||||||
|
deletedItem = items[itemId]
|
||||||
|
items[itemId] = nil
|
||||||
|
updateList()
|
||||||
|
|
||||||
|
showUndoSnackbar("Item deleted", function()
|
||||||
|
-- Undo callback
|
||||||
|
items[itemId] = deletedItem
|
||||||
|
deletedItem = nil
|
||||||
|
updateList()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Clear undo after 5 seconds
|
||||||
|
undoTimeout = setTimeout(function()
|
||||||
|
deletedItem = nil
|
||||||
|
permanentlyDelete(itemId)
|
||||||
|
end, 5000)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remember User State
|
||||||
|
|
||||||
|
Restore position and selections when returning:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function onScreenUnload()
|
||||||
|
storage.set("list_scroll_position", scrollContainer.scroll_top)
|
||||||
|
storage.set("selected_tab", currentTab)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onScreenLoad()
|
||||||
|
local scrollPos = storage.get("list_scroll_position")
|
||||||
|
if scrollPos then
|
||||||
|
scrollContainer.scroll_top = scrollPos
|
||||||
|
end
|
||||||
|
|
||||||
|
local tab = storage.get("selected_tab")
|
||||||
|
if tab then
|
||||||
|
selectTab(tab)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
### Use Local Variables
|
||||||
|
|
||||||
|
Local variables are faster and prevent global pollution:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Global
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
-- Good: Local
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
-- Good: Module-level local
|
||||||
|
local Utils = {}
|
||||||
|
local cache = {} -- Private to module
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handle Edge Cases
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function divide(a, b)
|
||||||
|
if b == 0 then
|
||||||
|
console.warn("Division by zero")
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return a / b
|
||||||
|
end
|
||||||
|
|
||||||
|
function getUsername(user)
|
||||||
|
if not user then
|
||||||
|
return "Unknown"
|
||||||
|
end
|
||||||
|
return user.name or user.email or "Unknown"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Meaningful Names
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad
|
||||||
|
local t = {}
|
||||||
|
local n = 0
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
local userScores = {}
|
||||||
|
local attemptCount = 0
|
||||||
|
|
||||||
|
-- Bad
|
||||||
|
function p(x)
|
||||||
|
return x * 100
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
function toPercentage(decimal)
|
||||||
|
return decimal * 100
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep Functions Small
|
||||||
|
|
||||||
|
Each function should do one thing:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Does too much
|
||||||
|
function processUser(userId)
|
||||||
|
local user = fetchUser(userId)
|
||||||
|
validateUser(user)
|
||||||
|
updateUserStats(user)
|
||||||
|
sendWelcomeEmail(user)
|
||||||
|
logActivity(user)
|
||||||
|
return formatUserResponse(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Good: Composed of small functions
|
||||||
|
function processNewUser(userId)
|
||||||
|
local user = fetchUser(userId)
|
||||||
|
if not isValidUser(user) then
|
||||||
|
return nil, "Invalid user"
|
||||||
|
end
|
||||||
|
initializeUserStats(user)
|
||||||
|
queueWelcomeEmail(user)
|
||||||
|
return user
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comment Why, Not What
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Describes what (obvious from code)
|
||||||
|
-- Increment counter by 1
|
||||||
|
counter = counter + 1
|
||||||
|
|
||||||
|
-- Good: Explains why
|
||||||
|
-- Reset retry count after successful connection
|
||||||
|
-- to prevent unnecessary backoff on next attempt
|
||||||
|
retryCount = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Validate All Input
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function searchItems(query)
|
||||||
|
-- Sanitize input
|
||||||
|
if type(query) ~= "string" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
query = query:sub(1, 100) -- Limit length
|
||||||
|
query = query:gsub("[^%w%s]", "") -- Remove special chars
|
||||||
|
|
||||||
|
return performSearch(query)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't Trust External Data
|
||||||
|
|
||||||
|
```lua
|
||||||
|
http.get(url, function(response)
|
||||||
|
local success, data = pcall(function()
|
||||||
|
return json.decode(response.body)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
console.error("Invalid JSON from API")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validate structure
|
||||||
|
if type(data.items) ~= "table" then
|
||||||
|
console.error("Missing items array")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
processItems(data.items)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Never Store Secrets in Code
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Hardcoded API key
|
||||||
|
local API_KEY = "sk-12345abcde"
|
||||||
|
|
||||||
|
-- Good: Use environment/config
|
||||||
|
local apiKey = config.get("api_key")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sanitize Display Content
|
||||||
|
|
||||||
|
When displaying user-generated content, prevent injection:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function displayComment(text)
|
||||||
|
-- Escape HTML entities
|
||||||
|
text = text:gsub("&", "&")
|
||||||
|
text = text:gsub("<", "<")
|
||||||
|
text = text:gsub(">", ">")
|
||||||
|
|
||||||
|
commentElement.inner_rml = text
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### Use Semantic Elements
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Bad: Divs for everything -->
|
||||||
|
<div class="button" onclick="submit()">Submit</div>
|
||||||
|
|
||||||
|
<!-- Good: Proper elements -->
|
||||||
|
<button onclick="submit()">Submit</button>
|
||||||
|
|
||||||
|
<!-- Good: Headings create hierarchy -->
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<h2>Account</h2>
|
||||||
|
<h2>Notifications</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provide Text Alternatives
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Images should describe their purpose -->
|
||||||
|
<img src="icons/search.tga" alt="Search"/>
|
||||||
|
|
||||||
|
<!-- Icons with meaning need labels -->
|
||||||
|
<button aria-label="Close">
|
||||||
|
<img src="icons/close.tga"/>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ensure Color Contrast
|
||||||
|
|
||||||
|
Text should have at least 4.5:1 contrast ratio:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Good contrast */
|
||||||
|
.light-text {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #1a1a2e; /* Contrast: 12.6:1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bad contrast */
|
||||||
|
.low-contrast {
|
||||||
|
color: #888888;
|
||||||
|
background-color: #666666; /* Contrast: 1.3:1 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't Rely on Color Alone
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Bad: Only color indicates error */
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Good: Icon + color + text */
|
||||||
|
.error {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
.error::before {
|
||||||
|
content: "⚠ ";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Error States
|
||||||
|
|
||||||
|
Don't just test the happy path:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Test these scenarios:
|
||||||
|
-- 1. Empty data
|
||||||
|
-- 2. Network failure
|
||||||
|
-- 3. Invalid input
|
||||||
|
-- 4. Timeouts
|
||||||
|
-- 5. Missing permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Navigation Flows
|
||||||
|
|
||||||
|
Ensure users can:
|
||||||
|
- Navigate forward and back
|
||||||
|
- Return to the home screen
|
||||||
|
- Handle the back button at any screen
|
||||||
|
|
||||||
|
### Test Edge Cases
|
||||||
|
|
||||||
|
- Very long text/names
|
||||||
|
- Empty lists
|
||||||
|
- Maximum values
|
||||||
|
- Rapid repeated actions
|
||||||
|
- Interrupted operations
|
||||||
|
|
||||||
|
### Use Debug Logging
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local DEBUG = true
|
||||||
|
|
||||||
|
function debugLog(...)
|
||||||
|
if DEBUG then
|
||||||
|
print("[DEBUG]", ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In production build, set DEBUG = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Use Meaningful Version Numbers
|
||||||
|
|
||||||
|
Follow semantic versioning:
|
||||||
|
- **MAJOR**: Breaking changes
|
||||||
|
- **MINOR**: New features, backward compatible
|
||||||
|
- **PATCH**: Bug fixes
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.1.3",
|
||||||
|
"version_code": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Write Good Release Notes
|
||||||
|
|
||||||
|
```
|
||||||
|
Version 2.1.0
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
- Added dark mode support
|
||||||
|
- New export to PDF feature
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
- Faster loading times
|
||||||
|
- Better error messages
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
- Fixed crash when opening empty files
|
||||||
|
- Fixed date format on some devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Before Submitting
|
||||||
|
|
||||||
|
1. Run on the Designer
|
||||||
|
2. Test all features manually
|
||||||
|
3. Check on a real device if possible
|
||||||
|
4. Verify all assets load correctly
|
||||||
|
5. Test offline behavior
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
Before submitting your app:
|
||||||
|
|
||||||
|
- [ ] All features work as expected
|
||||||
|
- [ ] Error states are handled gracefully
|
||||||
|
- [ ] Loading states shown during async operations
|
||||||
|
- [ ] Touch targets are at least 48dp
|
||||||
|
- [ ] Text is readable (contrast ratio ≥ 4.5:1)
|
||||||
|
- [ ] No console errors in normal usage
|
||||||
|
- [ ] Timers and intervals cleaned up properly
|
||||||
|
- [ ] User data persists correctly
|
||||||
|
- [ ] App works after fresh install
|
||||||
|
- [ ] Version number and code are updated
|
||||||
|
- [ ] Release notes are meaningful
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [UI Design Guide](ui-design.md) - Design patterns
|
||||||
|
- [Lua Scripting Guide](lua-scripting.md) - Code patterns
|
||||||
|
- [Troubleshooting](../troubleshooting.md) - Common issues
|
||||||
506
portal/internal/web/docs/guides/lua-scripting.md
Normal file
506
portal/internal/web/docs/guides/lua-scripting.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# Lua Scripting Guide
|
||||||
|
|
||||||
|
Mosis apps use Lua for scripting and interactivity. Each app runs in an isolated sandbox with access to Mosis-specific APIs.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Embed Lua directly in your RML files:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<body>
|
||||||
|
<button onclick="sayHello()">Click Me</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function sayHello()
|
||||||
|
print("Hello from Lua!")
|
||||||
|
end
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use external files:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<head>
|
||||||
|
<script src="scripts/app.lua"/>
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lua Basics
|
||||||
|
|
||||||
|
If you're new to Lua, here's a quick primer:
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Local variables (preferred)
|
||||||
|
local name = "Mosis"
|
||||||
|
local count = 42
|
||||||
|
local enabled = true
|
||||||
|
local items = {"apple", "banana", "cherry"}
|
||||||
|
|
||||||
|
-- Global variables (avoid when possible)
|
||||||
|
globalVar = "accessible everywhere"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Basic function
|
||||||
|
function greet(name)
|
||||||
|
return "Hello, " .. name .. "!"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function with multiple returns
|
||||||
|
function getPosition()
|
||||||
|
return 100, 200
|
||||||
|
end
|
||||||
|
|
||||||
|
local x, y = getPosition()
|
||||||
|
|
||||||
|
-- Anonymous functions
|
||||||
|
local double = function(n) return n * 2 end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Control Flow
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- If statements
|
||||||
|
if score > 100 then
|
||||||
|
print("High score!")
|
||||||
|
elseif score > 50 then
|
||||||
|
print("Good job!")
|
||||||
|
else
|
||||||
|
print("Keep trying!")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Loops
|
||||||
|
for i = 1, 10 do
|
||||||
|
print(i)
|
||||||
|
end
|
||||||
|
|
||||||
|
for index, value in ipairs(items) do
|
||||||
|
print(index, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
while condition do
|
||||||
|
-- loop body
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Array-like table
|
||||||
|
local colors = {"red", "green", "blue"}
|
||||||
|
print(colors[1]) -- "red" (Lua is 1-indexed)
|
||||||
|
|
||||||
|
-- Dictionary-like table
|
||||||
|
local user = {
|
||||||
|
name = "Alice",
|
||||||
|
age = 25,
|
||||||
|
premium = true
|
||||||
|
}
|
||||||
|
print(user.name)
|
||||||
|
print(user["age"])
|
||||||
|
|
||||||
|
-- Mixed table
|
||||||
|
local app = {
|
||||||
|
name = "MyApp",
|
||||||
|
version = "1.0",
|
||||||
|
features = {"dark mode", "notifications"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DOM Manipulation
|
||||||
|
|
||||||
|
Access and modify UI elements using the `document` object:
|
||||||
|
|
||||||
|
### Getting Elements
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- By ID
|
||||||
|
local button = document:GetElementById("my-button")
|
||||||
|
|
||||||
|
-- By tag name
|
||||||
|
local paragraphs = document:GetElementsByTagName("p")
|
||||||
|
|
||||||
|
-- By class name
|
||||||
|
local cards = document:GetElementsByClassName("card")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying Content
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local element = document:GetElementById("message")
|
||||||
|
|
||||||
|
-- Set inner content (HTML-like)
|
||||||
|
element.inner_rml = "<strong>Hello!</strong>"
|
||||||
|
|
||||||
|
-- Get inner content
|
||||||
|
local content = element.inner_rml
|
||||||
|
|
||||||
|
-- Set text only (safer, no HTML parsing)
|
||||||
|
element:SetInnerRML("Plain text here")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying Attributes
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local input = document:GetElementById("username")
|
||||||
|
|
||||||
|
-- Get attribute
|
||||||
|
local value = input:GetAttribute("value")
|
||||||
|
|
||||||
|
-- Set attribute
|
||||||
|
input:SetAttribute("placeholder", "Enter username")
|
||||||
|
|
||||||
|
-- Remove attribute
|
||||||
|
input:RemoveAttribute("disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying Styles
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local box = document:GetElementById("box")
|
||||||
|
|
||||||
|
-- Set individual properties
|
||||||
|
box.style.width = "200dp"
|
||||||
|
box.style.backgroundColor = "#00d4ff"
|
||||||
|
box.style.display = "none" -- hide element
|
||||||
|
|
||||||
|
-- Read properties
|
||||||
|
local width = box.style.width
|
||||||
|
```
|
||||||
|
|
||||||
|
### Classes
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local element = document:GetElementById("panel")
|
||||||
|
|
||||||
|
-- Add class
|
||||||
|
element:SetClass("active", true)
|
||||||
|
|
||||||
|
-- Remove class
|
||||||
|
element:SetClass("active", false)
|
||||||
|
|
||||||
|
-- Check class
|
||||||
|
if element:IsClassSet("active") then
|
||||||
|
print("Panel is active")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Handling
|
||||||
|
|
||||||
|
### Inline Events
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button onclick="handleClick()">Click</button>
|
||||||
|
<input onchange="handleChange(event)"/>
|
||||||
|
<div onmouseover="handleHover()"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Listeners
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local button = document:GetElementById("my-button")
|
||||||
|
|
||||||
|
-- Add listener
|
||||||
|
button:AddEventListener("click", function(event)
|
||||||
|
print("Button clicked!")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Remove listener (need reference)
|
||||||
|
local handler = function(event)
|
||||||
|
print("Clicked")
|
||||||
|
end
|
||||||
|
button:AddEventListener("click", handler)
|
||||||
|
button:RemoveEventListener("click", handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Object
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function handleEvent(event)
|
||||||
|
-- Event type
|
||||||
|
print(event.type) -- "click", "change", etc.
|
||||||
|
|
||||||
|
-- Target element
|
||||||
|
local target = event:GetCurrentElement()
|
||||||
|
|
||||||
|
-- Mouse position (for mouse events)
|
||||||
|
local x = event.parameters.mouse_x
|
||||||
|
local y = event.parameters.mouse_y
|
||||||
|
|
||||||
|
-- Stop propagation
|
||||||
|
event:StopPropagation()
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `click` | Element clicked |
|
||||||
|
| `dblclick` | Element double-clicked |
|
||||||
|
| `mousedown` | Mouse button pressed |
|
||||||
|
| `mouseup` | Mouse button released |
|
||||||
|
| `mouseover` | Mouse enters element |
|
||||||
|
| `mouseout` | Mouse leaves element |
|
||||||
|
| `focus` | Element gains focus |
|
||||||
|
| `blur` | Element loses focus |
|
||||||
|
| `change` | Input value changed |
|
||||||
|
| `submit` | Form submitted |
|
||||||
|
| `keydown` | Key pressed |
|
||||||
|
| `keyup` | Key released |
|
||||||
|
|
||||||
|
## Timers
|
||||||
|
|
||||||
|
### setTimeout
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Execute once after delay
|
||||||
|
local timerId = setTimeout(function()
|
||||||
|
print("Executed after 1 second")
|
||||||
|
end, 1000) -- milliseconds
|
||||||
|
|
||||||
|
-- Cancel timer
|
||||||
|
clearTimeout(timerId)
|
||||||
|
```
|
||||||
|
|
||||||
|
### setInterval
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Execute repeatedly
|
||||||
|
local intervalId = setInterval(function()
|
||||||
|
print("Tick")
|
||||||
|
end, 1000)
|
||||||
|
|
||||||
|
-- Cancel interval
|
||||||
|
clearInterval(intervalId)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Persist data between app sessions:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Save data
|
||||||
|
storage.set("username", "Alice")
|
||||||
|
storage.set("settings", {
|
||||||
|
darkMode = true,
|
||||||
|
notifications = false
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Load data
|
||||||
|
local username = storage.get("username")
|
||||||
|
local settings = storage.get("settings")
|
||||||
|
|
||||||
|
-- Delete data
|
||||||
|
storage.remove("username")
|
||||||
|
|
||||||
|
-- Clear all data
|
||||||
|
storage.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
Navigate between screens in your app:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Navigate to screen
|
||||||
|
navigateTo("settings") -- loads assets/settings.rml
|
||||||
|
|
||||||
|
-- Go back
|
||||||
|
goBack()
|
||||||
|
|
||||||
|
-- Go to home screen
|
||||||
|
goHome()
|
||||||
|
|
||||||
|
-- Replace current screen (no back)
|
||||||
|
replaceTo("login")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation Events
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Listen for navigation
|
||||||
|
onNavigate(function(screenName)
|
||||||
|
print("Navigated to: " .. screenName)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Listen for back
|
||||||
|
onBack(function()
|
||||||
|
print("Going back")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Requests
|
||||||
|
|
||||||
|
Make network requests (requires `network` permission):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- GET request
|
||||||
|
http.get("https://api.example.com/data", function(response)
|
||||||
|
if response.ok then
|
||||||
|
local data = json.decode(response.body)
|
||||||
|
print(data.message)
|
||||||
|
else
|
||||||
|
print("Error: " .. response.status)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- POST request
|
||||||
|
http.post("https://api.example.com/submit", {
|
||||||
|
headers = {
|
||||||
|
["Content-Type"] = "application/json"
|
||||||
|
},
|
||||||
|
body = json.encode({
|
||||||
|
name = "Alice",
|
||||||
|
action = "subscribe"
|
||||||
|
})
|
||||||
|
}, function(response)
|
||||||
|
print("Status: " .. response.status)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Parse JSON string
|
||||||
|
local data = json.decode('{"name": "Alice", "age": 25}')
|
||||||
|
print(data.name)
|
||||||
|
|
||||||
|
-- Convert to JSON string
|
||||||
|
local str = json.encode({
|
||||||
|
items = {"a", "b", "c"},
|
||||||
|
count = 3
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date and Time
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Current timestamp
|
||||||
|
local now = os.time()
|
||||||
|
|
||||||
|
-- Format date
|
||||||
|
local formatted = os.date("%Y-%m-%d %H:%M:%S", now)
|
||||||
|
|
||||||
|
-- Parse date components
|
||||||
|
local t = os.date("*t", now)
|
||||||
|
print(t.year, t.month, t.day, t.hour, t.min, t.sec)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### String Functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Concatenation
|
||||||
|
local greeting = "Hello, " .. name .. "!"
|
||||||
|
|
||||||
|
-- String functions
|
||||||
|
string.upper("hello") -- "HELLO"
|
||||||
|
string.lower("HELLO") -- "hello"
|
||||||
|
string.sub("hello", 1, 3) -- "hel"
|
||||||
|
string.find("hello", "ll") -- 3
|
||||||
|
string.gsub("hello", "l", "L") -- "heLLo"
|
||||||
|
string.format("Score: %d", 100) -- "Score: 100"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Math Functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
math.floor(3.7) -- 3
|
||||||
|
math.ceil(3.2) -- 4
|
||||||
|
math.round(3.5) -- 4
|
||||||
|
math.abs(-5) -- 5
|
||||||
|
math.min(1, 2, 3) -- 1
|
||||||
|
math.max(1, 2, 3) -- 3
|
||||||
|
math.random() -- 0-1
|
||||||
|
math.random(1, 6) -- 1-6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table Functions
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Insert
|
||||||
|
table.insert(items, "new item")
|
||||||
|
table.insert(items, 1, "at beginning")
|
||||||
|
|
||||||
|
-- Remove
|
||||||
|
table.remove(items) -- remove last
|
||||||
|
table.remove(items, 1) -- remove first
|
||||||
|
|
||||||
|
-- Sort
|
||||||
|
table.sort(items)
|
||||||
|
table.sort(items, function(a, b) return a > b end) -- descending
|
||||||
|
|
||||||
|
-- Length
|
||||||
|
local count = #items
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sandbox Restrictions
|
||||||
|
|
||||||
|
For security, these are **NOT** available:
|
||||||
|
|
||||||
|
- `os.execute`, `io.popen` - No shell commands
|
||||||
|
- `loadfile`, `dofile` - No arbitrary file loading
|
||||||
|
- `require` - No external modules (use `import` for app modules)
|
||||||
|
- `debug` library - No debugging hooks
|
||||||
|
- `rawget`, `rawset` - No metatable bypass
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use local variables** - Faster and prevents pollution
|
||||||
|
2. **Handle errors** - Use `pcall` for operations that might fail
|
||||||
|
3. **Clean up timers** - Clear intervals when navigating away
|
||||||
|
4. **Minimize DOM queries** - Cache element references
|
||||||
|
5. **Batch updates** - Group style changes together
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local success, result = pcall(function()
|
||||||
|
-- Code that might fail
|
||||||
|
local data = json.decode(invalidJson)
|
||||||
|
return data
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
print("Parsed:", result)
|
||||||
|
else
|
||||||
|
print("Error:", result)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Pattern
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- utils.lua
|
||||||
|
local Utils = {}
|
||||||
|
|
||||||
|
function Utils.formatCurrency(amount)
|
||||||
|
return string.format("$%.2f", amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Utils.capitalize(str)
|
||||||
|
return str:sub(1,1):upper() .. str:sub(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Utils
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- main.lua
|
||||||
|
local Utils = import("utils")
|
||||||
|
|
||||||
|
print(Utils.formatCurrency(19.99))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Permissions Guide](permissions.md) - Request device capabilities
|
||||||
|
- [API Reference](../api/lua-api.md) - Complete API documentation
|
||||||
|
- [Debugging Guide](debugging.md) - Debug your Lua code
|
||||||
396
portal/internal/web/docs/guides/permissions.md
Normal file
396
portal/internal/web/docs/guides/permissions.md
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
# Permissions Guide
|
||||||
|
|
||||||
|
Mosis apps run in a secure sandbox with limited access to device features. To access sensitive capabilities, apps must declare permissions in their manifest.
|
||||||
|
|
||||||
|
## Why Permissions?
|
||||||
|
|
||||||
|
Permissions protect user privacy and security by:
|
||||||
|
|
||||||
|
1. **Informing users** what an app can access before installation
|
||||||
|
2. **Limiting damage** if an app misbehaves
|
||||||
|
3. **Maintaining trust** in the Mosis ecosystem
|
||||||
|
|
||||||
|
## Declaring Permissions
|
||||||
|
|
||||||
|
Add permissions to your `manifest.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.myapp",
|
||||||
|
"name": "My App",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"network"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only request permissions your app actually needs. Users are more likely to trust apps with fewer permissions.
|
||||||
|
|
||||||
|
## Available Permissions
|
||||||
|
|
||||||
|
### storage
|
||||||
|
|
||||||
|
**Description:** Persist data locally between app sessions.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Save user preferences
|
||||||
|
- Cache data for offline use
|
||||||
|
- Store app state
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
storage.set("key", value)
|
||||||
|
storage.get("key")
|
||||||
|
storage.remove("key")
|
||||||
|
storage.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** All apps have access to in-memory storage during a session. The `storage` permission enables persistence across sessions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### network
|
||||||
|
|
||||||
|
**Description:** Make HTTP/HTTPS requests to external servers.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Fetch data from APIs
|
||||||
|
- Submit form data
|
||||||
|
- Load remote content
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
http.get(url, callback)
|
||||||
|
http.post(url, options, callback)
|
||||||
|
http.request(options, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- HTTPS only (HTTP blocked for security)
|
||||||
|
- Cannot access localhost or internal IPs
|
||||||
|
- Subject to CORS policies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### clipboard
|
||||||
|
|
||||||
|
**Description:** Read from and write to the system clipboard.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Copy text or data
|
||||||
|
- Paste user content
|
||||||
|
- Share functionality
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
clipboard.write(text)
|
||||||
|
clipboard.read(callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### notifications
|
||||||
|
|
||||||
|
**Description:** Display system notifications to the user.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Reminders
|
||||||
|
- Alerts
|
||||||
|
- Background updates
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
notifications.show({
|
||||||
|
title = "Reminder",
|
||||||
|
body = "Your timer is done!",
|
||||||
|
icon = "icons/alarm.png"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- Notifications may be rate-limited
|
||||||
|
- Users can disable notifications per-app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### camera
|
||||||
|
|
||||||
|
**Description:** Capture photos using the device camera.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Photo capture
|
||||||
|
- QR code scanning
|
||||||
|
- Augmented reality
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
camera.capture({
|
||||||
|
quality = "high",
|
||||||
|
facing = "back"
|
||||||
|
}, function(result)
|
||||||
|
if result.success then
|
||||||
|
local imageData = result.data
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- User prompt before first access
|
||||||
|
- Cannot record video (photo only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### microphone
|
||||||
|
|
||||||
|
**Description:** Record audio from the device microphone.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Voice notes
|
||||||
|
- Audio messages
|
||||||
|
- Voice commands
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
microphone.start()
|
||||||
|
microphone.stop(function(result)
|
||||||
|
local audioData = result.data
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- User prompt before first access
|
||||||
|
- Maximum recording duration enforced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### location
|
||||||
|
|
||||||
|
**Description:** Access device location information.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Weather apps
|
||||||
|
- Maps
|
||||||
|
- Location-based features
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
location.get(function(result)
|
||||||
|
if result.success then
|
||||||
|
print(result.latitude, result.longitude)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
location.watch(function(result)
|
||||||
|
-- Called on location changes
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- User prompt before first access
|
||||||
|
- Approximate location only (no precise GPS)
|
||||||
|
- Battery impact warning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### contacts
|
||||||
|
|
||||||
|
**Description:** Read device contacts.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Contact picker
|
||||||
|
- Address book integration
|
||||||
|
- Sharing with friends
|
||||||
|
|
||||||
|
**API access:**
|
||||||
|
```lua
|
||||||
|
contacts.pick(function(result)
|
||||||
|
if result.success then
|
||||||
|
print(result.name, result.phone)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
contacts.getAll(function(result)
|
||||||
|
for i, contact in ipairs(result.contacts) do
|
||||||
|
print(contact.name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- Read-only access
|
||||||
|
- User prompt before first access
|
||||||
|
|
||||||
|
## Permission Levels
|
||||||
|
|
||||||
|
| Level | Description | Example |
|
||||||
|
|-------|-------------|---------|
|
||||||
|
| **Normal** | Low risk, minimal review | storage |
|
||||||
|
| **Sensitive** | Requires user prompt | camera, microphone, location |
|
||||||
|
| **Dangerous** | Extensive review required | contacts |
|
||||||
|
|
||||||
|
## Runtime Behavior
|
||||||
|
|
||||||
|
### First-Time Prompts
|
||||||
|
|
||||||
|
Some permissions trigger a user prompt on first use:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- First call triggers prompt
|
||||||
|
camera.capture(options, function(result)
|
||||||
|
if result.denied then
|
||||||
|
-- User denied permission
|
||||||
|
showPermissionExplanation()
|
||||||
|
elseif result.success then
|
||||||
|
-- Permission granted
|
||||||
|
handlePhoto(result.data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Permission Status
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Check if permission is granted
|
||||||
|
if permissions.check("camera") then
|
||||||
|
-- Already have permission
|
||||||
|
showCameraButton()
|
||||||
|
else
|
||||||
|
-- Need to request
|
||||||
|
showRequestButton()
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requesting at Runtime
|
||||||
|
|
||||||
|
```lua
|
||||||
|
permissions.request("camera", function(granted)
|
||||||
|
if granted then
|
||||||
|
startCamera()
|
||||||
|
else
|
||||||
|
showAlternative()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Minimize Permissions
|
||||||
|
|
||||||
|
Only request what you need. An app with fewer permissions:
|
||||||
|
- Builds more user trust
|
||||||
|
- Passes review faster
|
||||||
|
- Has smaller attack surface
|
||||||
|
|
||||||
|
### 2. Request at the Right Time
|
||||||
|
|
||||||
|
Don't request all permissions at startup. Request when the user takes an action that needs it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Bad: Request on app start
|
||||||
|
function onAppStart()
|
||||||
|
permissions.request("camera") -- Why?
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Good: Request when needed
|
||||||
|
function onTakePhotoClicked()
|
||||||
|
permissions.request("camera", function(granted)
|
||||||
|
if granted then
|
||||||
|
camera.capture(options, handlePhoto)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Explain Why
|
||||||
|
|
||||||
|
Tell users why you need a permission before requesting:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div id="permission-explanation" style="display: none;">
|
||||||
|
<p>This app needs camera access to scan QR codes.</p>
|
||||||
|
<button onclick="requestCamera()">Enable Camera</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handle Denial Gracefully
|
||||||
|
|
||||||
|
Apps should work (with reduced functionality) even if permissions are denied:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function capturePhoto()
|
||||||
|
if not permissions.check("camera") then
|
||||||
|
-- Offer alternative
|
||||||
|
showManualEntryOption()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Proceed with camera
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Don't Ask Again Immediately
|
||||||
|
|
||||||
|
If a user denies a permission, don't immediately ask again:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local lastDenied = storage.get("camera_denied_time")
|
||||||
|
if lastDenied and os.time() - lastDenied < 86400 then
|
||||||
|
-- Wait at least 24 hours before asking again
|
||||||
|
return
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Impact
|
||||||
|
|
||||||
|
Permission requests affect app review:
|
||||||
|
|
||||||
|
| Permission | Review Impact |
|
||||||
|
|------------|---------------|
|
||||||
|
| storage, network | Automatic approval |
|
||||||
|
| clipboard | Quick review |
|
||||||
|
| notifications | Standard review |
|
||||||
|
| camera, microphone | Extended review |
|
||||||
|
| location | Extended review |
|
||||||
|
| contacts | Manual review required |
|
||||||
|
|
||||||
|
Apps requesting sensitive permissions must:
|
||||||
|
1. Justify the need in submission notes
|
||||||
|
2. Use the permission appropriately
|
||||||
|
3. Respect user privacy
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Permission not declared"
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Cannot use camera without 'camera' permission
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the permission to your manifest:
|
||||||
|
```json
|
||||||
|
"permissions": ["camera"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission denied by user"
|
||||||
|
|
||||||
|
Handle this gracefully in your code:
|
||||||
|
```lua
|
||||||
|
if result.denied then
|
||||||
|
showAlternativeUI()
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission blocked"
|
||||||
|
|
||||||
|
The user permanently blocked the permission. Direct them to settings:
|
||||||
|
```lua
|
||||||
|
if result.blocked then
|
||||||
|
showMessage("Please enable camera in system settings")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Manifest Reference](../api/manifest.md) - Full manifest documentation
|
||||||
|
- [Security Guide](security.md) - App security best practices
|
||||||
|
- [Publishing Guide](publishing.md) - App review process
|
||||||
395
portal/internal/web/docs/guides/ui-design.md
Normal file
395
portal/internal/web/docs/guides/ui-design.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# UI Design Guide
|
||||||
|
|
||||||
|
Mosis uses RML (RmlUi Markup Language) and RCSS (RmlUi CSS) for building user interfaces. If you know HTML and CSS, you'll feel right at home.
|
||||||
|
|
||||||
|
## RML Basics
|
||||||
|
|
||||||
|
RML is similar to HTML but with some differences optimized for UI rendering.
|
||||||
|
|
||||||
|
### Document Structure
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<rml>
|
||||||
|
<head>
|
||||||
|
<title>App Title</title>
|
||||||
|
<link type="text/rcss" href="styles.rcss"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Your UI here -->
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Elements
|
||||||
|
|
||||||
|
| Element | Usage |
|
||||||
|
|---------|-------|
|
||||||
|
| `<div>` | Container/layout |
|
||||||
|
| `<p>` | Paragraph text |
|
||||||
|
| `<span>` | Inline text |
|
||||||
|
| `<h1>` - `<h6>` | Headings |
|
||||||
|
| `<img>` | Images |
|
||||||
|
| `<button>` | Clickable buttons |
|
||||||
|
| `<input>` | Text input fields |
|
||||||
|
| `<select>` | Dropdown menus |
|
||||||
|
| `<progress>` | Progress bars |
|
||||||
|
|
||||||
|
### Layout Example
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div class="app-bar">
|
||||||
|
<div class="app-bar-nav" onclick="goBack()">
|
||||||
|
<img src="../../icons/back.tga"/>
|
||||||
|
</div>
|
||||||
|
<span class="app-bar-title">My App</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Welcome</h2>
|
||||||
|
<p>This is a card component.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dock">
|
||||||
|
<button class="dock-item" onclick="navigateTo('home')">
|
||||||
|
<img src="icons/home.tga"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## RCSS Styling
|
||||||
|
|
||||||
|
RCSS is CSS with some limitations and extensions.
|
||||||
|
|
||||||
|
### Supported Properties
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- `display` (block, inline, inline-block, flex, none)
|
||||||
|
- `position` (static, relative, absolute, fixed)
|
||||||
|
- `width`, `height`, `min-width`, `max-width`, `min-height`, `max-height`
|
||||||
|
- `margin`, `padding` (including directional variants)
|
||||||
|
- `flex`, `flex-direction`, `flex-wrap`, `justify-content`, `align-items`
|
||||||
|
- `overflow` (visible, hidden, scroll, auto)
|
||||||
|
|
||||||
|
**Visual:**
|
||||||
|
- `background-color`, `background` (with decorators)
|
||||||
|
- `color`
|
||||||
|
- `border`, `border-radius`
|
||||||
|
- `opacity`
|
||||||
|
- `box-shadow` (via decorators)
|
||||||
|
|
||||||
|
**Typography:**
|
||||||
|
- `font-family`
|
||||||
|
- `font-size`
|
||||||
|
- `font-weight` (normal, bold)
|
||||||
|
- `font-style` (normal, italic)
|
||||||
|
- `text-align` (left, center, right)
|
||||||
|
- `line-height`
|
||||||
|
- `text-decoration`
|
||||||
|
|
||||||
|
### Units
|
||||||
|
|
||||||
|
| Unit | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `dp` | Density-independent pixels (recommended) |
|
||||||
|
| `px` | Pixels |
|
||||||
|
| `%` | Percentage of parent |
|
||||||
|
| `em` | Relative to font size |
|
||||||
|
|
||||||
|
Always use `dp` for consistent sizing across devices:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.button {
|
||||||
|
padding: 12dp 24dp;
|
||||||
|
font-size: 16dp;
|
||||||
|
border-radius: 8dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Hex colors */
|
||||||
|
color: #ffffff;
|
||||||
|
color: #fff;
|
||||||
|
color: #00d4ff80; /* with alpha */
|
||||||
|
|
||||||
|
/* RGB/RGBA */
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
color: rgba(0, 212, 255, 0.5);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pseudo-classes
|
||||||
|
|
||||||
|
```css
|
||||||
|
button {
|
||||||
|
background-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #00b8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #0099cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
border: 2dp solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flexbox Layout
|
||||||
|
|
||||||
|
RCSS supports flexbox for modern layouts:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div class="row">
|
||||||
|
<span>Left</span>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<span>Right</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
Images should be in TGA format for best performance:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<img src="icons/star.tga"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported formats:
|
||||||
|
- TGA (recommended)
|
||||||
|
- PNG
|
||||||
|
- JPEG
|
||||||
|
|
||||||
|
### Image Sizing
|
||||||
|
|
||||||
|
```css
|
||||||
|
img {
|
||||||
|
width: 32dp;
|
||||||
|
height: 32dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aspect ratio maintained */
|
||||||
|
img.icon {
|
||||||
|
width: 24dp;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Elements
|
||||||
|
|
||||||
|
### Text Input
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<input type="text" id="username" placeholder="Enter username"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12dp;
|
||||||
|
background-color: #2a2a4e;
|
||||||
|
border: 1dp solid #3a3a5e;
|
||||||
|
border-radius: 8dp;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select/Dropdown
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<select id="country">
|
||||||
|
<option value="us">United States</option>
|
||||||
|
<option value="uk">United Kingdom</option>
|
||||||
|
<option value="ca">Canada</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progress Bar
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<progress id="loading" value="0.5" max="1"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8dp;
|
||||||
|
background-color: #2a2a4e;
|
||||||
|
border-radius: 4dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress fill {
|
||||||
|
background-color: #00d4ff;
|
||||||
|
border-radius: 4dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scrolling
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div class="scroll-container">
|
||||||
|
<div class="scroll-content">
|
||||||
|
<!-- Long content here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.scroll-container {
|
||||||
|
height: 300dp;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decorators
|
||||||
|
|
||||||
|
RCSS uses decorators for advanced visual effects:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Gradient background */
|
||||||
|
.gradient {
|
||||||
|
decorator: horizontal-gradient(#1a1a2e #2a2a4e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image background */
|
||||||
|
.card {
|
||||||
|
decorator: image(background.tga);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border image */
|
||||||
|
.fancy-border {
|
||||||
|
decorator: ninepatch(border.tga, 10dp, 10dp, 10dp, 10dp);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Animations
|
||||||
|
|
||||||
|
RCSS supports keyframe animations:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fade-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transitions
|
||||||
|
|
||||||
|
```css
|
||||||
|
.button {
|
||||||
|
transition: background-color 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #00b8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
Design for the Mosis phone screen (1080x1920 logical pixels):
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Base styles for portrait */
|
||||||
|
.content {
|
||||||
|
padding: 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust for available space */
|
||||||
|
.app-bar {
|
||||||
|
height: 56dp;
|
||||||
|
padding: 0 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock {
|
||||||
|
height: 64dp;
|
||||||
|
padding: 8dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Tokens
|
||||||
|
|
||||||
|
Use CSS variables for consistent theming:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #00d4ff;
|
||||||
|
--primary-dark: #00b8e6;
|
||||||
|
--background: #1a1a2e;
|
||||||
|
--surface: #2a2a4e;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--spacing-sm: 8dp;
|
||||||
|
--spacing-md: 16dp;
|
||||||
|
--spacing-lg: 24dp;
|
||||||
|
--radius-sm: 4dp;
|
||||||
|
--radius-md: 8dp;
|
||||||
|
--radius-lg: 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: var(--primary);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use dp units** - Ensures consistent sizing across devices
|
||||||
|
2. **Test touch targets** - Minimum 48dp for touchable elements
|
||||||
|
3. **Maintain contrast** - Ensure text is readable (4.5:1 ratio minimum)
|
||||||
|
4. **Use semantic structure** - Proper headings, lists, etc.
|
||||||
|
5. **Optimize images** - Use TGA format, appropriate sizes
|
||||||
|
6. **Keep it simple** - Mobile-first design, avoid clutter
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Lua Scripting Guide](lua-scripting.md) - Add interactivity
|
||||||
|
- [Components Library](components.md) - Pre-built UI components
|
||||||
|
- [Theme Reference](theme.md) - Complete theming guide
|
||||||
51
portal/internal/web/docs/index.md
Normal file
51
portal/internal/web/docs/index.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Mosis Developer Documentation
|
||||||
|
|
||||||
|
Welcome to the Mosis developer documentation. Mosis is a virtual smartphone OS for VR games and applications, providing a phone-like device that users can interact with inside VR environments.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Getting Started](getting-started.md) - Create your first Mosis app
|
||||||
|
- [UI Guide](guides/ui-design.md) - Design beautiful interfaces with RML/RCSS
|
||||||
|
- [Lua Scripting](guides/lua-scripting.md) - Add interactivity with Lua
|
||||||
|
- [API Reference](api/lua-api.md) - Complete Lua API documentation
|
||||||
|
- [Manifest Reference](api/manifest.md) - App manifest schema
|
||||||
|
|
||||||
|
## What is Mosis?
|
||||||
|
|
||||||
|
Mosis provides a virtual phone interface for VR applications. Developers can create apps that run inside this virtual phone, offering users familiar smartphone experiences within VR games.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **RML/RCSS UI** - HTML/CSS-like markup for building interfaces
|
||||||
|
- **Lua Scripting** - Lightweight scripting for app logic
|
||||||
|
- **Sandboxed Execution** - Secure isolation per app
|
||||||
|
- **Cross-Platform** - Works with Unity, Unreal Engine, and more
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Your VR Game/App │
|
||||||
|
│ (Unity, Unreal, native Android) │
|
||||||
|
└──────────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────▼──────────────────────┐
|
||||||
|
│ MosisService │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ Your Mosis App │ │
|
||||||
|
│ │ ┌─────────┐ ┌─────────────┐ │ │
|
||||||
|
│ │ │ RML/CSS │ │ Lua Scripts │ │ │
|
||||||
|
│ │ └─────────┘ └─────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- [Troubleshooting](troubleshooting.md) - Common issues and solutions
|
||||||
|
- [FAQ](faq.md) - Frequently asked questions
|
||||||
|
- [API Status](https://status.omixlab.com) - Service status page
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Mosis is developed by OmixLab LTD. For questions or feedback, contact us through the developer portal.
|
||||||
469
portal/internal/web/docs/troubleshooting.md
Normal file
469
portal/internal/web/docs/troubleshooting.md
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Solutions for common issues when developing Mosis apps.
|
||||||
|
|
||||||
|
## Build Errors
|
||||||
|
|
||||||
|
### "Invalid manifest: missing required field"
|
||||||
|
|
||||||
|
Your `manifest.json` is missing a required field.
|
||||||
|
|
||||||
|
**Solution:** Check these required fields:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.app",
|
||||||
|
"name": "App Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "assets/main.rml",
|
||||||
|
"author": {
|
||||||
|
"name": "Your Name",
|
||||||
|
"email": "you@example.com"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"128": "icon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Invalid package ID format"
|
||||||
|
|
||||||
|
Package IDs must follow reverse domain notation.
|
||||||
|
|
||||||
|
**Valid:**
|
||||||
|
- `com.example.myapp`
|
||||||
|
- `com.yourname.calculator`
|
||||||
|
- `io.github.user.app`
|
||||||
|
|
||||||
|
**Invalid:**
|
||||||
|
- `myapp` (needs domain prefix)
|
||||||
|
- `Com.Example.App` (must be lowercase)
|
||||||
|
- `com..app` (no double dots)
|
||||||
|
- `com.app.` (can't end with dot)
|
||||||
|
|
||||||
|
### "Entry point not found"
|
||||||
|
|
||||||
|
The `entry` file specified in manifest doesn't exist.
|
||||||
|
|
||||||
|
**Solution:** Verify the path:
|
||||||
|
```json
|
||||||
|
"entry": "assets/main.rml"
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that `assets/main.rml` exists relative to your manifest file.
|
||||||
|
|
||||||
|
### "Icon not found"
|
||||||
|
|
||||||
|
An icon file specified in manifest doesn't exist.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check file paths are correct
|
||||||
|
2. Ensure files exist
|
||||||
|
3. Use forward slashes in paths
|
||||||
|
|
||||||
|
```json
|
||||||
|
"icons": {
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Package too large"
|
||||||
|
|
||||||
|
Package exceeds the 10MB limit.
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Compress images (use TGA or optimized PNG)
|
||||||
|
- Remove unused assets
|
||||||
|
- Move large files to external CDN
|
||||||
|
- Check for accidentally included files
|
||||||
|
|
||||||
|
## Runtime Errors
|
||||||
|
|
||||||
|
### "attempt to index nil value"
|
||||||
|
|
||||||
|
You're accessing a property on a nil variable.
|
||||||
|
|
||||||
|
**Common causes:**
|
||||||
|
|
||||||
|
1. **Element not found:**
|
||||||
|
```lua
|
||||||
|
-- Bad
|
||||||
|
local elem = document:GetElementById("typo")
|
||||||
|
elem.inner_rml = "Hello" -- Error: elem is nil
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
local elem = document:GetElementById("correct-id")
|
||||||
|
if elem then
|
||||||
|
elem.inner_rml = "Hello"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Table key doesn't exist:**
|
||||||
|
```lua
|
||||||
|
-- Bad
|
||||||
|
local data = json.decode(response.body)
|
||||||
|
print(data.user.name) -- Error if user is nil
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
if data and data.user then
|
||||||
|
print(data.user.name)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### "attempt to call nil value"
|
||||||
|
|
||||||
|
You're calling a function that doesn't exist.
|
||||||
|
|
||||||
|
**Common causes:**
|
||||||
|
|
||||||
|
1. **Typo in function name:**
|
||||||
|
```lua
|
||||||
|
-- Bad: navigateto (lowercase t)
|
||||||
|
navigateto("settings")
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
navigateTo("settings")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Missing permission:**
|
||||||
|
```lua
|
||||||
|
-- Error if 'network' permission not declared
|
||||||
|
http.get(url, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission denied"
|
||||||
|
|
||||||
|
You're using an API without the required permission.
|
||||||
|
|
||||||
|
**Solution:** Add permission to manifest:
|
||||||
|
```json
|
||||||
|
"permissions": ["storage", "network"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Network request failed"
|
||||||
|
|
||||||
|
HTTP request couldn't complete.
|
||||||
|
|
||||||
|
**Common causes:**
|
||||||
|
|
||||||
|
1. **No network permission:**
|
||||||
|
```json
|
||||||
|
"permissions": ["network"]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Invalid URL:**
|
||||||
|
```lua
|
||||||
|
-- Bad: missing protocol
|
||||||
|
http.get("api.example.com/data", callback)
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
http.get("https://api.example.com/data", callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **HTTP not allowed (HTTPS only):**
|
||||||
|
```lua
|
||||||
|
-- Bad
|
||||||
|
http.get("http://example.com/data", callback)
|
||||||
|
|
||||||
|
-- Good
|
||||||
|
http.get("https://example.com/data", callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **CORS error:** The server doesn't allow cross-origin requests. Contact the API provider or use a CORS proxy.
|
||||||
|
|
||||||
|
### "Storage quota exceeded"
|
||||||
|
|
||||||
|
You've exceeded the 5MB storage limit.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Clear unnecessary data: `storage.clear()`
|
||||||
|
- Use selective removal: `storage.remove("large-key")`
|
||||||
|
- Store only essential data
|
||||||
|
- Consider using network storage for large data
|
||||||
|
|
||||||
|
## UI Issues
|
||||||
|
|
||||||
|
### Element not displaying
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
|
||||||
|
1. **Display not set to none:**
|
||||||
|
```css
|
||||||
|
/* Element might be hidden */
|
||||||
|
.element {
|
||||||
|
display: none; /* Remove this */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Size is zero:**
|
||||||
|
```css
|
||||||
|
.element {
|
||||||
|
width: 0; /* Add dimensions */
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Element is off-screen:**
|
||||||
|
```css
|
||||||
|
.element {
|
||||||
|
position: absolute;
|
||||||
|
left: -1000dp; /* Move to visible area */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Z-index issues:**
|
||||||
|
```css
|
||||||
|
.element {
|
||||||
|
z-index: 1; /* Bring to front */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Click events not working
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
|
||||||
|
1. **Function exists:**
|
||||||
|
```xml
|
||||||
|
<button onclick="handleClick()">Click</button>
|
||||||
|
```
|
||||||
|
```lua
|
||||||
|
-- Make sure function is defined
|
||||||
|
function handleClick()
|
||||||
|
print("Clicked!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Element is overlapped:**
|
||||||
|
Another element might be blocking clicks. Check z-index and position.
|
||||||
|
|
||||||
|
3. **Element has pointer-events: none:**
|
||||||
|
```css
|
||||||
|
.element {
|
||||||
|
/* Remove this */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styles not applying
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
|
||||||
|
1. **Stylesheet is linked:**
|
||||||
|
```xml
|
||||||
|
<head>
|
||||||
|
<link type="text/rcss" href="styles.rcss"/>
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Selector is correct:**
|
||||||
|
```css
|
||||||
|
/* Class selector needs dot */
|
||||||
|
.my-class { }
|
||||||
|
|
||||||
|
/* ID selector needs hash */
|
||||||
|
#my-id { }
|
||||||
|
|
||||||
|
/* Tag selector has no prefix */
|
||||||
|
button { }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Specificity issues:**
|
||||||
|
More specific selectors override less specific ones:
|
||||||
|
```css
|
||||||
|
/* Less specific */
|
||||||
|
button { color: blue; }
|
||||||
|
|
||||||
|
/* More specific - wins */
|
||||||
|
.btn.primary { color: red; }
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Units are correct:**
|
||||||
|
```css
|
||||||
|
/* Use dp units */
|
||||||
|
padding: 12dp;
|
||||||
|
|
||||||
|
/* Not px on mobile */
|
||||||
|
padding: 12px; /* May not work correctly */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout breaks on different screens
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. **Use dp units instead of px:**
|
||||||
|
```css
|
||||||
|
padding: 16dp; /* Scales properly */
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use flexbox:**
|
||||||
|
```css
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use percentage widths:**
|
||||||
|
```css
|
||||||
|
.card {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400dp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text is cut off
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. **Allow wrapping:**
|
||||||
|
```css
|
||||||
|
.text {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add overflow scrolling:**
|
||||||
|
```css
|
||||||
|
.container {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use ellipsis (if supported):**
|
||||||
|
```css
|
||||||
|
.text {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designer Issues
|
||||||
|
|
||||||
|
### Hot reload not working
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. **Save the file** - Changes only reload on save
|
||||||
|
2. **Check file is in watch path**
|
||||||
|
3. **Restart designer** - Sometimes needed after many changes
|
||||||
|
4. **Check for syntax errors** - Invalid files may not reload
|
||||||
|
|
||||||
|
### Designer crashes on startup
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. **Check file paths:**
|
||||||
|
```bash
|
||||||
|
# Make sure path exists
|
||||||
|
mosis-designer.exe ../assets/main.rml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Try a simple file first:**
|
||||||
|
```xml
|
||||||
|
<rml>
|
||||||
|
<head><title>Test</title></head>
|
||||||
|
<body><p>Hello</p></body>
|
||||||
|
</rml>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check for missing assets:**
|
||||||
|
Images or fonts that don't exist can cause crashes.
|
||||||
|
|
||||||
|
4. **Update graphics drivers:**
|
||||||
|
The designer uses OpenGL which requires up-to-date drivers.
|
||||||
|
|
||||||
|
### Rendering looks different on device
|
||||||
|
|
||||||
|
**Common causes:**
|
||||||
|
|
||||||
|
1. **Font differences** - Ensure fonts are bundled
|
||||||
|
2. **DPI scaling** - Use dp units consistently
|
||||||
|
3. **Color profiles** - Use standard sRGB colors
|
||||||
|
|
||||||
|
## Submission Issues
|
||||||
|
|
||||||
|
### "Version code must be higher"
|
||||||
|
|
||||||
|
Each new version needs a higher version_code.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.1",
|
||||||
|
"version_code": 2 // Increment from previous
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Signature verification failed"
|
||||||
|
|
||||||
|
Your package signature is invalid.
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. **Rebuild the package:**
|
||||||
|
```bash
|
||||||
|
mosis build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check signing key is registered:**
|
||||||
|
```bash
|
||||||
|
mosis keys list
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Re-register your key if needed:**
|
||||||
|
```bash
|
||||||
|
mosis keys register ~/.mosis/keys/production.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Review rejected"
|
||||||
|
|
||||||
|
Check the rejection reason in your dashboard. Common issues:
|
||||||
|
|
||||||
|
| Reason | Solution |
|
||||||
|
|--------|----------|
|
||||||
|
| Inappropriate content | Remove violating content |
|
||||||
|
| Misleading description | Update description to match functionality |
|
||||||
|
| Crashes on launch | Fix startup errors |
|
||||||
|
| Missing privacy policy | Add privacy policy for data-collecting apps |
|
||||||
|
| Impersonation | Don't copy other apps |
|
||||||
|
|
||||||
|
## Getting More Help
|
||||||
|
|
||||||
|
### Check Logs
|
||||||
|
|
||||||
|
**Designer logs:**
|
||||||
|
```bash
|
||||||
|
mosis-designer.exe app.rml --log debug.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lua errors:**
|
||||||
|
```lua
|
||||||
|
-- Add error handling
|
||||||
|
local success, err = pcall(function()
|
||||||
|
-- Your code
|
||||||
|
end)
|
||||||
|
if not success then
|
||||||
|
print("Error:", err)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Issues
|
||||||
|
|
||||||
|
Check if others have encountered the same issue:
|
||||||
|
- Developer forum
|
||||||
|
- GitHub issues
|
||||||
|
- Stack Overflow (tag: mosis)
|
||||||
|
|
||||||
|
### Contact Support
|
||||||
|
|
||||||
|
If you're still stuck:
|
||||||
|
1. Gather error messages and logs
|
||||||
|
2. Create minimal reproduction case
|
||||||
|
3. Submit through developer portal support
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [FAQ](faq.md) - Frequently asked questions
|
||||||
|
- [Lua API Reference](api/lua-api.md) - API documentation
|
||||||
|
- [UI Design Guide](guides/ui-design.md) - Styling reference
|
||||||
Reference in New Issue
Block a user