App Development Guide
How to create, register, and deploy apps for the wtf_group tablet.
App Structure
Every tablet app is a React component in ui/src/apps/<AppName>/index.jsx.
ui/src/apps/
├── MyNewApp/
│ └── index.jsx # Main app componentMinimal App
import { useTablet } from '../../context/TabletContext';
export default function MyNewApp() {
const { themeColors } = useTablet();
return (
<div style={{ height: '100%', padding: 16 }}>
<h1 style={{ color: themeColors.text }}>My App</h1>
<p style={{ color: themeColors.textDim }}>Hello world</p>
</div>
);
}Registering an App
Method 1: App Registry (Static)
Add to ui/src/lib/appRegistry.js:
import { GroupsIcon, CalculatorIcon } from './icons';
export const APP_REGISTRY = {
// ... existing apps ...
my_new_app: {
id: 'my_new_app',
name: 'My App',
description: 'Description of my app',
iconComponent: CalculatorIcon, // SVG component from icons.jsx
iconText: '🚀', // Fallback emoji (if no iconComponent)
color: '#ff6600',
category: 'tools', // system, tools, productivity, internet, media, info, developer
builtin: true, // true = always installed, false = downloadable
downloadable: true, // true = shows in App Store
showOnDesktop: true, // true = shows desktop icon
singleton: true, // true = only one window allowed
defaultWidth: 600,
defaultHeight: 400,
minWidth: 300,
minHeight: 200,
},
};Then add the component mapping in App.jsx:
import MyNewApp from './apps/MyNewApp';
const APP_COMPONENTS = {
// ... existing ...
my_new_app: MyNewApp,
};Method 2: Lua Registration (Dynamic)
From another resource:
-- client.lua
exports['wtf_group']:RegisterApp({
id = 'my_external_app',
name = 'External App',
icon = 'fas fa-puzzle-piece',
component = 'MyExternalApp',
category = 'custom',
color = '#ff6600',
builtin = false,
showOnDesktop = true,
singleton = true,
defaultWidth = 600,
defaultHeight = 400,
})Using Settings
All apps share the centralized settings via useTablet():
import { useTablet } from '../../context/TabletContext';
function MyApp() {
const {
themeColors, // Derived colors (accent, text, surface, border, etc.)
accentColor, // Raw accent hex
fontFamily, // CSS font-family string
fontSize, // Base font size in px
uiScale, // UI scale percentage (100 = normal)
borderRadius, // Border radius in px
showAnimations, // Animation toggle
showGlowEffects,// Glow effects toggle
} = useTablet();
return (
<div style={{
background: themeColors.windowBg,
color: themeColors.text,
fontFamily: fontFamily,
borderRadius: borderRadius,
}}>
{/* Your app content */}
</div>
);
}Persisting App Data
Use dbStorage.js to save data to MySQL:
import { useState, useEffect } from 'react';
import { loadAppDataFromDB, saveAppDataToDB } from '../../lib/dbStorage';
function MyApp() {
const [data, setData] = useState([]);
// Load from DB on mount
useEffect(() => {
loadAppDataFromDB('my_app', 'notes').then(saved => {
if (saved && Array.isArray(saved)) setData(saved);
}).catch(() => {});
}, []);
// Save to DB on change
const save = (newData) => {
setData(newData);
saveAppDataToDB('my_app', 'notes', newData);
};
return <div>...</div>;
}Storage keys:
appId— Your app's ID (e.g.,'my_app')key— Data key (e.g.,'notes','settings','bookmarks')value— Any JSON-serializable data
App Store (Downloadable Apps)
For apps that players can install/uninstall:
- Set
builtin: falseanddownloadable: truein registry - Add to
COMPONENT_MAPinAppStore/index.jsx - The App Store handles install/uninstall lifecycle
VPN Shield App (Built-in Downloadable)
The VPN app is a downloadable app that provides encrypted proxy connections.
Registry entry (lib/appRegistry.js):
vpn: {
id: 'vpn',
name: 'VPN Shield',
description: 'Encrypted proxy connection for anonymous browsing',
iconComponent: VpnIcon,
color: '#388e3c',
category: 'darkweb', // Darkweb category — gated by VPN
darkweb: true, // Requires darkweb VPN proxy to install
downloadable: true,
showOnDesktop: true,
singleton: true,
defaultWidth: 420,
defaultHeight: 520,
}Server-side (server/vpn.lua):
VpnMgr.GetProxies(source)— loads proxies + connection state + keysVpnMgr.Connect(source, proxyId)— connects to proxyVpnMgr.Disconnect(source)— disconnectsVpnMgr.ActivateKey(source, keyCode)— activates license key- Exports:
IsVpnConnected,IsPlayerConnectedToProxy,GetPlayerActiveProxy
Client-side (client/vpn.lua):
- 4 NUI callbacks forwarding to server events
- Receives
vpnDataevent and forwards to React viaSendNUIMessage
Data storage (in wtf_tablet_app_data):
app_id='vpn', data_key='connection'→{ proxy_id, proxy_name, connected, expires }app_id='vpn', data_key='keys'→[{ key_code, proxy_id, permanent, expires }, ...]
Full documentation: VPN Shield App
Window Props
Apps can receive props from the window:
function MyApp({初始Data, mode }) {
// Props passed via openApp('my_app', { props: { initialData: '...' } })
}Update props from inside the app:
import { useApp } from '../../context/AppContext';
function MyApp() {
const { updateWindowProps } = useApp();
const windowId = useApp().windows.find(w => w.appId === 'my_app')?.id;
// Update props
updateWindowProps(windowId, { mode: 'edit' });
}Sound Effects
import { Sounds } from '../../lib/sounds';
Sounds.click(); // Button click
Sounds.hover(); // Mouse hover
Sounds.open(); // App/window open
Sounds.close(); // App/window close
Sounds.minimize(); // Window minimize
Sounds.maximize(); // Window maximize
Sounds.notify(); // Notification received
Sounds.error(); // Error occurred
Sounds.success(); // Success action
Sounds.tick(); // Subtle interaction
Sounds.type(); // TypingSVG Icons
Create custom icons in ui/src/lib/icons.jsx:
export function MyIcon({ size = 16, color = 'currentColor' }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24"
fill="none" stroke={color} strokeWidth="2"
strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" />
</svg>
);
}Then reference in registry:
import { MyIcon } from '../lib/icons';
// ...
iconComponent: MyIcon,Best Practices
- Always use
useTablet()for theme colors — never hardcode colors - Use
themeColorsproperties —windowBg,surface,text,textDim,accent,border, etc. - Save data to DB — use
saveAppDataToDB()for anything persistent - Load from DB on mount — use
loadAppDataFromDB()inuseEffect - Use
Sounds.*for audio feedback - Use SVG icons — not font-awesome or images
- Set proper
minWidth/minHeight— prevent windows from being too small - Use
singleton: truefor most apps — prevents duplicate windows - Keep apps self-contained — each app should work independently