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/spf13/cobra v1.8.0
|
||||
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/image v0.15.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
|
||||
// Downloads - serve package files with proper headers
|
||||
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