A theme is an offline ZIP package: HTML/CSS/JS handles the lock screen, desktop background, and widgets, while the app icon grid, Dock, paging, and tap behavior are handled by the native launcher. The flow: prepare assets → upload in Creator Center → live preview → submit for review. Pick the path that fits you:
Prepare a wallpaper (icons optional) and upload via the lite theme form — the platform packages it automatically. Want animated ambience? Let AI write the theme package for you.
Download the starter project, edit the H5 ambience layer, declare layout in the manifest, subscribe to data via ThemeSDK, then ZIP and upload to preview.
You can ship a theme without writing code: choose "lite theme" when creating one, upload a wallpaper, and replace the built-in app icons one by one. The platform handles the manifest and packaging, sharing the same preview and review flow as ZIP uploads.
Finished a lite theme and want to upgrade to animated ambience (custom clock / greetings / sticker effects)? The same wallpaper and icons can be reused directly — see the next section, Generate with AI.
Want animated ambience (custom clock / greetings / sticker effects) without hand-writing code? This section is for you. If you only need new wallpaper and icons, the previous section's lite theme form is enough. A good theme's polish comes mostly from art assets — the wallpaper and icons — code alone can't produce it. Prepare the assets per the checklist below first (draw them, source them, or use an image generator; the style is entirely yours, as long as specs and naming match), then send the asset list plus a style description to a coding AI and let it assemble the theme package.
| Asset | Path / naming | Required | Specs & composition |
|---|---|---|---|
| Wallpaper | assets/wallpaper.png | Strongly recommended | Portrait, 1170×2532 or larger. Leave the top third for the clock / greeting (no key subjects there); leave the bottom ~15% for the Dock tray |
| Night wallpaper | assets/wallpaper_night.png | Optional | A night version with the same composition as the day one; the code switches by time (18:00–6:00) |
| App icon set | icons/<appId>.png | Recommended | 256×256 square, artwork filling the canvas with no empty margins (devices apply a uniform 22% corner radius — no need to pre-round). Which icons and how to name the files: see the list below the table; keep the whole set stylistically consistent, with a recognizable distinct shape per app |
| Decorative stickers | assets/sticker-*.png | Optional | Transparent PNGs to dress up the ambience layer (corner marks / tape / mascots etc.) |
There are 18 icons in total; use the app ID in parentheses as the file name (e.g. icons/wechat.png): WeTalk (wechat), Messages (sms), Phone (phone_call), Memories (memorial_memory), Alt account (alt_identity), Their memories (memory_of_me), Forum (forum), Wallet (wallet), Shine Shopping (shopping_shine), Calendar (xiaoyueli), Inspiration (punchline_dict), Shine Guide (shine_guide), Dark Web (darknet), WeChat link (wechat_link), App Store (store), Theme Store (theme_store), Settings (settings), Close (close). Full mapping table: Custom app icons. You don't have to draw them all — just the common ones — but include an icons/default.png as the fallback for undrawn apps, so no entry degrades to a text / emoji icon.
At the end of the prompt, describe your style and list the assets you prepared. The AI outputs three files: manifest.json / desktop.html / main.js. It works without assets too — write "no assets" in the list and the AI implements everything in CSS.
You are a developer of "desktop themes" for the Shine virtual phone. I'll describe the theme style I want and tell you which art assets I've prepared; output a complete theme package of 3 files: manifest.json, desktop.html, main.js.
Output format: before each file print a single separator line "===== filename =====", then the file content verbatim; no markdown code fences, no explanations, no prefix or suffix. I'll zip these 3 files together with the asset folders and upload the ZIP.
—— What a theme is ——
A theme is an "ambience layer" H5 page stacked underneath the virtual phone's desktop: it only draws background ambience, a clock, a greeting, and decorative stickers.
The app icon grid, Dock, page swiping, and taps are all handled by the native Launcher layered on top — never build your own app grid, never call openApp / saveLayout.
—— Art asset conventions (the main source of polish; prefer the images I provide) ——
- The ZIP may contain assets/wallpaper.png (wallpaper), assets/wallpaper_night.png (night wallpaper, optional),
and assets/sticker-*.png (decorative stickers) — reference them all by relative path (CSS background-image or <img>);
stretch the wallpaper as a full-screen background; when a night wallpaper exists, switch using data.system.time.hour (<6 || >=18 counts as night)
- icons/<appId>.png are app icon overrides: the native Launcher reads them automatically; don't reference them in the manifest or code
- Set the manifest's shell.backdrop_image to the wallpaper path; native code paints it under the WebView before first paint to prevent a white flash
- Don't invent file names for assets I didn't provide; fill any gaps with CSS gradients / textures / inline SVG
- No network requests of any kind: no fetch / XHR / remote images / remote fonts / CDNs — every asset must live inside the ZIP
—— Fixed manifest.json skeleton (fine-tune on top of it, don't change the structure; the // comments below are notes for you — the manifest.json you output must be valid pure JSON with no comments) ——
{
"schema_version": 3, // always 3, don't change
"name": "<theme name>",
"version": "1.0.0",
"entries": { "desktop": "desktop.html" }, // ambience layer entry page, fixed
"scripts": ["main.js"], // loaded after the platform injects ThemeSDK
"shell": {
"status_bar": "light", // status bar text: light = white (dark wallpaper), dark = black (light wallpaper)
"backdrop_image": "assets/wallpaper.png" // required when a wallpaper exists: painted natively before first paint to prevent a white flash
},
"launcher": {
"grid": { "cols": 4, "anchor": { "left": 0.04, "right": 0.04 } },
// 4-column icon grid, 4% margin on each side
"segments": [
{ "type": "zone", "id": "hero", "selector": "[data-theme-zone=\"hero\"]", "fallback_top": 0.3 },
// measures the hero placeholder's height; the app grid starts below it
{ "type": "apps", "fill": true } // fill the remaining space with app icons
],
"dock": { "height": 84, "padding_h": 12, "bottom": 0.05 },
// bottom Dock: height / horizontal padding / bottom offset ratio
"icon_style": "flat", // icon look, see below
"overflow_pages": { "grid_top": "full" },// from page 2 on the grid starts at the top
"page_dots": "theme" // page dots follow the theme's colors
}
}
- Pick status_bar by wallpaper brightness: dark wallpaper → light (white text), light wallpaper → dark (black text)
- icon_style is one of five: plain (original art) | flat | polaroid (photo frame) | pill | glass — match the theme's mood
- On light wallpapers add "label": { "color": "#<dark color>", "shadow": "light" } inside launcher, otherwise app labels default to white and become unreadable
—— desktop.html conventions ——
- Decorative DOM only; wrap the first-page-only header (clock / greeting / character name) in <header data-theme-zone="hero"> — the Launcher measures its height and starts the app grid below it
- All CSS inline inside <style>; reference the wallpaper / stickers via relative paths to files in the package, draw everything else with CSS gradients / inline SVG
- Safe area (important): env(safe-area-inset-*) is always 0 in Android WebView; top containers must use
padding-top: calc(12px + max(env(safe-area-inset-top, 0px), var(--shine-safe-area-top, 0px)));
- No <script> or <script src> tags — all logic goes in main.js (the platform loads it per manifest.scripts after injecting ThemeSDK)
—— main.js conventions ——
(function () {
'use strict';
function render(data) { // dataChanged: full render on first frame / character change / page swipe
// draw the clock from data.system.time.hour/minute
// data.character.name is the phone owner's name (the user's in-story persona) — use it for the greeting;
// to address the other character use data.character.worldName — don't mix them up
// when system.launcherPage > 0 set hero's opacity to '0' (fade out on swipe; pure background ambience can stay visible)
ThemeSDK.reportLayout(); // must re-report whenever hero's height / visibility changes
}
function lightRefresh() { // dataUpdated: per-minute clock tick (page swipes go through dataChanged); only touch textContent and visibility
var data = ThemeSDK.getData(); // no reportLayout here, no image redraws
}
ThemeSDK.on('dataChanged', render);
ThemeSDK.on('dataUpdated', lightRefresh);
ThemeSDK.ready(); // must be called, otherwise real devices fall back to the native desktop within 3 seconds
ThemeSDK.reportLayout();
})();
- Available data: data.system.time { hour, minute }, data.system.launcherPage (0 = first page),
data.character { name: phone owner's (the user's persona) name, worldName: the other character's name, worldTitle: current world name (fall back to worldName when missing on older apps) }
- ES5 syntax only: var / function / string concatenation; no arrow functions, template literals, optional chaining ?., or destructuring — it must run on old Android WebViews too
—— Performance red lines (real devices are mostly low/mid-range Android) ——
- Animate only with CSS transitions/animations on transform / opacity; no large-area backdrop-filter
- No requestAnimationFrame loops or high-frequency setInterval redraws; clock updates come from dataUpdated pushes — don't start your own timers
- Load large images once (a full-screen background is enough); don't keep swapping image src in JS
—— The theme style I want ——
[Describe it here, e.g.: cream-colored journal style, a big clock at the top with a handwritten-feel greeting, collage decorations on the lower half of the wallpaper, polaroid-style icons]
—— Assets I've prepared (already in the ZIP, reference them accordingly) ——
[List the files you actually prepared, e.g.:
assets/wallpaper.png, assets/wallpaper_night.png, assets/sticker-moon.png,
icons/ contains the full set of 18 icons.
If you have no images at all, write "no assets" so the AI implements everything in CSS]Put the AI's three files together with assets/, icons/ in one folder, compress to ZIP, then go to to create an entry and upload. Flip through two pages in the preview on the right (check against the Pre-publish checklist). Not happy? Keep iterating in the same AI conversation — "make the clock bigger", "switch to dark" — and replace the whole ZIP. Once it looks right, set the preview image, price it, and submit for review.
{
"schema_version": 3, // Always 3; don't bump to 4 — older app versions reject the whole package
"name": "Shine", // Theme name (shown in the store and on the settings page)
"version": "3.1.0", // Semantic version; bump it when updating the package
"entries": { "desktop": "desktop.html" }, // Ambience layer entry page, always desktop.html
"scripts": ["main.js"], // Scripts loaded in order after the platform injects ThemeSDK
"shell": { "status_bar": "dark" }, // Status bar text: dark = black (light wallpaper), light = white (dark wallpaper)
"launcher": { // Layout declaration for the native Launcher
"grid": { "cols": 4, "anchor": { "left": 0.04, "right": 0.04 } },
// 4-column icon grid, 4% margin on each side
"segments": [ // Top-to-bottom segments on the first page
{ "type": "zone", "id": "hero", "selector": "[data-theme-zone=\"hero\"]" },
// zone: measures the ambience layer's hero placeholder; the grid avoids it
{ "type": "apps", "rows": 1 }, // One row of app icons
{ "type": "widget", "id": "quote", "entry": "widgets/quote.html",
"span": { "cols": 4, "rows": 2 } }, // Widget: standalone H5, spans 4 cols × 2 rows
{ "type": "apps", "fill": true } // Fill the remaining space with app icons
],
"overflow_pages": { "grid_top": "full" },// Grid top edge from page 2 on: full = start at the top, same = same top edge as page 1
"dock": { "height": 84, "padding_h": 12, "style": "shelf", "bottom": 0.065 },
// Bottom Dock: height / horizontal padding / style / bottom offset ratio
"icon_style": "polaroid", // Icon look: plain|flat|polaroid|pill|glass
"icon_tokens": { "tilt": "random", "frame_radius": 7, "shadow": "warm" },
// Look tweaks: random tilt / frame corner radius / warm shadow
"page_dots": "theme" // Page dots follow the theme's colors
}
}Comments are for explanation only — manifest.json is plain JSON, and // in the real file will fail to parse. Remove them after copying.
plain — Original: No chrome, same as the default desktop icons; the lite-theme defaultflat — Flat rounded: Default; light rounded backingpolaroid — Polaroid: White frame + warm shadow, suits wall-style themespill — Pill: Gradient rounded block, iOS-ishglass — Frosted glass: Semi-transparent with a stroke, suits dark wallpapersNewer app versions support an optional set of launcher tokens: app label color (rescues white text on light themes), icon chrome icon_chrome / MiniApp outline via icon_chrome_fallback, and Dock tray colors and label toggles. Write them straight into a schema 3 package (older apps ignore them; do not bump schema_version to 4 — older app versions reject the whole package).
// All fields below are optional; write them straight into a schema 3 package's launcher block — older apps ignore them
"launcher": {
"icon_style": "plain", // New "plain": original art as-is (matches the default desktop)
"label": { "color": "#5C4038", "shadow": "light" },
// App label color + shadow none|light|dark|auto (rescues white text on light themes)
"icon_chrome": { // Image chrome: backing + overlay, applied to every app without exception
"back": "chrome/back.png", "upon": "chrome/upon.png",
"scale": 0.72, "radius": 10
},
"icon_chrome_fallback": { // Applied only to apps the theme drew no icon for —
"back": "chrome/back.png", "scale": 0.62 // use this to give MiniApps a unified ring; don't use icon_chrome (you'd get a card inside a card)
},
"dock": {
"style": "glass", // Degradation fallback style for older apps
"labels": false, // Whether the Dock shows app labels
"surface": { "color": "#FFF9F2", "border_color": "#D8C2AE", "border_width": 1,
"radius": 18, "inset_h": 4, "padding_v": 8, "shadow": "warm" },
"background": "chrome/dock.png" // Image tray backing, takes priority over surface
}
}Create icons/ at the theme ZIP root and drop in images named by app ID, e.g.icons/wechat.png. No manifest registration needed — the native launcher matches them automatically. Bitmaps are recommended (PNG/WebP/JPG, 256×256 square suggested): devices apply a uniform 22% corner radius and decode at display size, so results are stable. SVG is supported but fine detail blurs at small sizes — only use it for simple geometric shapes. With multiple formats of the same name, the first match wins in png → webp → jpg → svg order.
| App name | App ID | Icon file |
|---|---|---|
| WeTalk | wechat | icons/wechat.png |
| Messages | sms | icons/sms.png |
| Phone | phone_call | icons/phone_call.png |
| Memories | memorial_memory | icons/memorial_memory.png |
| Alt account | alt_identity | icons/alt_identity.png |
| Their memories | memory_of_me | icons/memory_of_me.png |
| Forum | forum | icons/forum.png |
| Wallet | wallet | icons/wallet.png |
| Shine Shopping | shopping_shine | icons/shopping_shine.png |
| Calendar | xiaoyueli | icons/xiaoyueli.png |
| Inspiration | punchline_dict | icons/punchline_dict.png |
| Shine Guide | shine_guide | icons/shine_guide.png |
| Dark Web | darknet | icons/darknet.png |
| WeChat link | wechat_link | icons/wechat_link.png |
| Entry name | System ID | Icon file |
|---|---|---|
| App Store | __store__ | icons/store.png |
| Theme Store | __theme_store__ | icons/theme_store.png |
| Settings | __settings__ | icons/settings.png |
| Close | __close__ | icons/close.png |
Match order: specific app ID → icons/default.png (fallback, applied to system / built-in apps without a dedicated icon) → the app's own icon → system icon or emoji. MiniApps are the exception: they always use their own uploaded icon and are not themed — default doesn't apply to them either.
// v3 ambience layer ThemeSDK (desktop.html + main.js)
// 1. Decorative placeholder — background ambience stays visible full-screen
// <div data-theme-zone="hero">...</div>
ThemeSDK.ready();
ThemeSDK.reportLayout();
// 2. Subscribe to data + fade out the first-page-only header (greeting/clock/date bar) on page swipe
// system.launcherPage: current Launcher page (0 = first page)
ThemeSDK.on('dataChanged', function (data) {
renderAtmosphere(data); // system.time / character
var page = (data.system && data.system.launcherPage) || 0;
document.querySelector('[data-theme-zone="hero"]').style.opacity =
page > 0 ? '0' : '1'; // fade out so it doesn't show through the page-2 grid; pure background ambience (gradient/texture) can stay visible
ThemeSDK.reportLayout(); // must re-report whenever hero's height/visibility changes
});
// 3. Safe area (important): the platform injects --shine-safe-area-{top,right,bottom,left}
// env(safe-area-inset-*) is always 0 in Android WebView; top UI must fall back with max():
// padding-top: calc(12px + max(env(safe-area-inset-top, 0px),
// var(--shine-safe-area-top, 0px)));
// 4. Music (optional): the ambience layer can draw its own music chip/turntable, anywhere you like
//
// Data: data.music = { hasTracks, isPlaying, isBuffering,
// trackId, title, artist, coverUrl }
// Pushed at low frequency (on track/playback-state changes); no playback progress —
// drive animation by toggling CSS with isPlaying.
//
// Taps: the ambience layer never receives taps directly (stays display-only, doesn't block swipe gestures).
// Mark tappable elements with data-theme-action; the platform overlays transparent
// native tap targets at the positions reported by reportLayout (page 0 only):
// <div class="chip-prev" data-theme-action="musicPrev"></div> <!-- previous track -->
// <div class="chip-play" data-theme-action="musicToggle"></div> <!-- play/pause -->
// <div class="chip-next" data-theme-action="musicNext"></div> <!-- next track -->
// <div class="chip-info" data-theme-action="musicOpen"></div> <!-- open the player page -->
// Note: avoid nesting one action element inside another; after elements move or
// change visibility you must call ThemeSDK.reportLayout() again,
// otherwise the tap targets stay at the old positions.
//
// Degradation contract (must handle): on older app versions / the character's phone /
// when the user turns the music card off, data.music is undefined — degrade the
// music UI to pure decoration (tap targets won't be mounted):
ThemeSDK.on('dataChanged', function (data) {
var m = data.music;
var chip = document.getElementById('music-chip');
chip.className = !m ? 'chip off' : (m.isPlaying ? 'chip playing' : 'chip');
document.getElementById('chip-title').textContent =
!m ? "Today's playlist" : (m.hasTracks ? (m.title || 'Music') : 'Add music');
});
// 5. World actions (optional): switch story / view their phone can be drawn into the ambience too
//
// Taps: same data-theme-action mechanism (transparent native tap targets, page 0 only):
// <div id="world-card" data-theme-action="worldSwitch"></div> <!-- switch story -->
// <div id="ta-phone" data-theme-action="taPhoneOpen"></div> <!-- view their phone -->
// Read the current world name from character.worldTitle (fall back to worldName on older apps).
// Once the theme draws a worldSwitch entry, the native "switch world" floating button
// steps aside on the first page (from page 1 on it comes back as a fallback);
// if you don't draw one, the floating button covers it the whole time — the entry is never lost.
// To hide the entry you must use display:none (or remove it from the flow) and call
// reportLayout() again — elements with visibility:hidden / opacity:0 still report
// their position and leave an invisible tap area behind.
//
// Degradation contract (must handle): older app versions don't send system.actions —
// when the array lacks an action, draw the entry as pure decoration or hide it
// (tap targets won't be mounted):
ThemeSDK.on('dataChanged', function (data) {
var acts = (data.system && data.system.actions) || [];
document.getElementById('world-card').className =
acts.indexOf('worldSwitch') >= 0 ? 'world-card' : 'world-card off';
});
// The app grid is drawn natively by Flutter on top of the ambience; page-2 icons start from the top, the background stays visible
// Forbidden: openApp / saveLayout / building your own app gridApp list fields: id / name / kind / icon / iconUrl / iconEmoji / badge. System entry ids: __store__, __theme_store__, __settings__, __close__. You must call ThemeSDK.ready() after initialization, or the device falls back to the native desktop within 3 seconds.
// widgets/*.html — ThemeWidgetSDK (injected automatically by the platform)
ThemeWidgetSDK.ready(function (data) {
var name = (data.character && data.character.name) || 'Guest';
document.getElementById('title').textContent = name;
});
// Data: { system, character }, same fields as the ambience layer
// Forbidden: fetch / openApp / external links
// manifest: { "type":"widget", "entry":"widgets/quote.html",
// "span": { "cols": 4, "rows": 2 } }
//
// Desktop music doesn't go through widgets: the ambience layer draws it itself
// with data.music + data-theme-action (see section 4 of the ThemeSDK guide).