00概览
什么是主题
主题会改变 NII Iznanka 电脑客户端的外观和声音。
- 从 6.0 版起,主题不再内置于客户端
- 每个主题都以独立包的形式放在目录中
.nftheme - 客户端只在选中主题时才下载它
- 安装包中只保留了一个基础主题:NII IZNANKA
任何人都能制作主题。主题包就是普通的 zip:manifest.json、图片、声音,需要的话再加上自己的 HTML、CSS 和 JS。在工作室上传后,审核员检查,服务器为版本签名,主题就会出现在所有人的目录里。
只需 manifest.json:颜色、字体、背景、音效、音乐、开场动画、自定义文案、窗口大小。无需代码。
再加上覆盖客户端窗口的 theme.css:按钮、面板和对话框可做成任意形状,还能按通道状态播放动画。
完全自己的窗口:一个 HTML、CSS 和 JS 页面。它从客户端获取状态,并通过 SDK 控制客户端。
层级可以叠加。场景同样可以有令牌和 theme.css:用来给在场景上方打开的客户端对话框上色。
可以修改的内容
| 项目 | 方式 | 层级 |
|---|---|---|
| 窗口和对话框的全部颜色 | tokens | 配色 |
| 字体:正文、标题、等宽 | fonts | 配色 |
| 背景:图片或视频、暗化、模糊 | background | 配色 |
| 连接、断开、错误、点击的音效 | sounds | 配色 |
| 背景音乐 | music | 配色 |
| 选择主题时的开场动画,带字幕 | intro | 配色 |
| 文案:“连接” → “进入球域” | words | 配色 |
| 窗口大小、圆角和形状,甚至撕裂边缘 | window | 配色 |
| 窗口和托盘图标 | icon | 配色 |
| 窗口的任意元素:形状、布局、动画 | theme.css | 样式 |
| 整个窗口:自己的按钮、节点列表和界面 | scene | 场景 |
| 常规窗口背后的动态背景 | scene.mode: backdrop | 场景 |
| 在客户端设置窗口中加入自己的主题设置 | settings | 任意 |
主题的流程
- 01作者nftheme pack
- 02工作室上传到网站
- 03审核主题与代码
- 04签名工作室 Ed25519
- 05目录iznanka.tech
- 06客户端目录标签页
01快速开始
10 分钟做一个主题
你需要:
- Node.js 18 或更高版本
- 作者工具包 nftheme-kit.zip:
nftheme工具、SDK、三个模板和预览
解压即可。无需安装,没有依赖。
# 1. 模板:palette、style 或 scene(默认) node nftheme-kit/bin/nftheme.js init my-theme --level scene --name "MY THEME" # 2. 在浏览器中预览,修改会自动生效 node nftheme-kit/bin/nftheme.js preview my-theme # 3. 校验,与目录相同 node nftheme-kit/bin/nftheme.js check my-theme # 4. 打包上传 node nftheme-kit/bin/nftheme.js pack my-theme
会得到文件 my-theme-1.0.0.nftheme。
- 在客户端中检查:主题 → 作者 → “打开主题文件夹”
- 一切正常?把主题包拖进工作室
最小 manifest.json
{
"format": 1,
"id": "night-owl",
"name": "NIGHT OWL",
"version": "1.0.0",
"author": "your_nick",
"tokens": { "accent": "#ffd23f", "bg": "#07060a" }
}在旁边放一张 preview.webp(或 .png、.jpg):16:9 的主题截图,最好是 1280×720。这样就够了,这样的包能通过校验。
02安装包
安装包 .nftheme
主题包是一个 zip 压缩包。根目录放 manifest.json,其余随意安排。清单和 CSS 中的路径均相对于包的根目录。
- 把整个文件夹打包了?外层文件夹会被自动去掉
- 系统垃圾文件(
__MACOSX、.DS_Store、Thumbs.db)会被跳过
my-theme.nftheme ├── manifest.json 必需 ├── preview.webp 必需:目录卡片,16:9 ├── icon.png 窗口和托盘图标,256×256 ├── theme.css “样式”层级 ├── scene/index.html “场景”层级 ├── scene/scene.js └── assets/ 图片、声音、视频、字体
文件名与类型
- 文件名:拉丁字母、数字、点、连字符、下划线。不能有空格和西里尔字母
- 仅大小写不同的两个文件无法通过:在 Windows 上它们是同一个文件
__nf/文件夹由客户端占用- 类型按内容检测。改名为 .mp3 的图片无法通过
| 类别 | 扩展名 |
|---|---|
| 文本和代码 | json css js mjs html txt md vtt,UTF-8 编码 |
| 图片 | png jpg jpeg webp gif avif svg |
| 字体 | woff2 woff ttf otf |
| 声音 | mp3 ogg opus m4a wav |
| 视频 | mp4 webm |
限制
| 项目 | 上限 |
|---|---|
| 整个包 | 64 MB |
| 解压后的包 | 160 MB |
| 单个文件 | 48 MB |
| 包内文件数 | 600 |
| 文本文件 | 2 MB |
| 图片 | 8 MB,预览图最大 4 MB,图标最大 1 MB |
| 文件路径 | 160 个字符,最多 8 层 |
| manifest.json | 64 KB |
03参考
manifest.json
- 前五个字段必填,其余可选
- 文本可以是字符串,也可以按语言提供:
"name": { "ru": "НОЧЬ", "en": "NIGHT" } - 客户端优先使用用户的语言,其次俄语,再次英语
| 字段 | 类型 | 说明 |
|---|---|---|
format | 数字 | 固定为 1 |
id | 字符串 | 3-40 个字符:小写拉丁字母、数字、连字符。永久归你所有。工作室主题的名称已被占用 |
name | 最多 40 字的文本 | 在目录和客户端中的名称 |
version | 字符串 | 1.0.0。每次新上传的版本号都要更高 |
author | 最多 40 字的字符串 | 包中的作者。目录中显示你在工作室的昵称 |
description | 最多 400 字的文本 | 目录中的描述 |
tags | 最多 6 个字符串 | 可选。客户端和目录已不再显示 |
accent | #rrggbb | 目录中卡片的颜色 |
minClient | 字符串 | 最低客户端版本,不低于 6.0.0 |
preview | 路径 | 预览图。默认为 preview.webp、.png 或 .jpg |
screenshots | 最多 8 个路径 | 主题页面的截图 |
icon | png 路径 | 窗口和托盘图标 |
license | 最多 60 字的字符串 | 许可证,例如 CC-BY-4.0 |
credits | 最多 12 个字符串 | 致谢:美术、音乐、字体的作者 |
homepage | https://… | 主题页面上的作者链接 |
window | 对象 | 窗口大小和形状,见第 07 |
tokens | 对象 | 颜色和尺寸,见第 04 |
fonts | 对象 | 字体,见第 04 |
background | 对象 | 背景,见第 05 |
sounds | 对象 | 事件音效,见第 05 |
music | 路径或对象 | 背景音乐,见第 05 |
intro | 路径或对象 | 开场动画,见第 05 |
words | 对象 | 自定义文案,见第 06 |
css | 路径 | 样式文件,见第 08 |
scene | 路径或对象 | 场景,见第 09 |
settings | 数组 | 主题设置,见第 11 |
{
"format": 1,
"id": "night-signal",
"name": { "ru": "НОЧНОЙ СИГНАЛ", "en": "NIGHT SIGNAL" },
"version": "1.2.0",
"author": "iznanka",
"description": { "ru": "Серая панель, сигнал вместо кнопки, голос на подъём канала." },
"accent": "#c42b47",
"license": "CC-BY-4.0",
"credits": ["art and sound: fawn", "font Overused Grotesk: OFL"],
"screenshots": ["shots/on.webp", "shots/servers.webp"],
"icon": "icon.png",
"window": { "width": 1280, "height": 720 },
"tokens": { "bg": "#000", "panel": "#a0a0a0", "text": "#fff", "accent": "#c42b47" },
"fonts": { "body": "assets/grotesk.woff2", "display": "assets/pixel.woff2" },
"background": { "image": "assets/field.webp", "dim": 0.2 },
"sounds": { "connect": "assets/up.mp3", "disconnect": "assets/down.mp3", "click": "assets/tick.mp3" },
"music": { "src": "assets/menu.ogg", "volume": 0.35 },
"intro": { "video": "assets/briefing.mp4", "subtitles": "assets/briefing.vtt" },
"words": { "ru": { "connect": "ВЫСАДКА", "disconnect": "ЭВАКУАЦИЯ", "servers": "СФЕРЫ" } },
"css": "theme.css",
"scene": { "entry": "scene/index.html", "mode": "full" },
"settings": [
{ "id": "glitch", "type": "toggle", "label": "Glitch", "default": true }
]
}04配色
颜色与字体
令牌是客户端用来给窗口和所有对话框上色的变量。
- 颜色:
#hex、rgb()、rgba()、hsl()、transparent - 尺寸:
px、em、rem、% - 在 CSS 和场景中可以用
var(--nf-名称)访问
| 令牌 | 基础主题中的值 | 作用对象 |
|---|---|---|
bg | #000000 | 所有内容之下的窗口背景 |
surface | #0b0b12 | 窗口中央的光晕 |
panel | rgba(12,10,22,.78) | 面板、菜单和对话框的背景 |
line | #23232e | 边框和分隔线 |
text | #e9e9f2 | 正文 |
muted | #6f6f80 | 说明和次要文字 |
accent | #a855f7 | 主色:连接按钮、选中的节点、标题 |
accent2 | #7c3aed | 第二强调色:悬停、渐变 |
ok | #4ade80 | 通道已建立,“已保护” |
warn | #f97316 | 维护、警告 |
danger | #ef4444 | 错误、通道中断 |
glow | rgba(168,85,247,.45) | 强调色周围的光晕 |
shadow | rgba(0,0,0,.6) | 对话框阴影 |
overlay | rgba(0,0,0,.72) | 对话框下方的遮罩 |
radius | 0px | 按钮和面板的圆角 |
border | 1px | 边框粗细 |
gap | 14px | 面板间距 |
试一试
在右侧调整颜色,下方会生成可直接用于 manifest.json 的片段。
字体
三种角色。客户端会加载文件并设置 --nf-font-body、--nf-font-display、--nf-font-mono。
body:全部文字display:大号文字和按钮mono:数字、日志、地址
"fonts": {
"body": "assets/fonts/grotesk.woff2",
"display": "assets/fonts/pixel.woff2",
"mono": "assets/fonts/mono.woff2"
}05配色
背景、声音、开场动画
背景
| 字段 | 项目 | 默认值 |
|---|---|---|
image | 背景图片 | 无 |
video | 背景视频:静音、循环,叠在图片上方 | 无 |
color | 图片下方的颜色 | 无 |
fit | cover、contain 或 fill | cover |
position | 与 CSS 相同:center、top、 left 30% | center |
dim | 暗化 0…1 | 0 |
blur | 模糊 0…40 px | 0 |
事件音效
事件音效由客户端自己播放。每个主题在客户端设置中都有独立的开关和音量。
| 事件 | 播放时机 |
|---|---|
connect | 通道已建立 |
disconnect | 通道已断开 |
error | 连接失败或通道中断 |
switch | 在活动通道上切换节点 |
click | 在客户端窗口中点击按钮 |
hover | 悬停在按钮上 |
open, close | 对话框打开或关闭 |
notify | 通知 |
"sounds": { "connect": "assets/sfx/up.mp3", "disconnect": "assets/sfx/down.mp3", "hover": "assets/sfx/tick.mp3" },
"music": { "src": "assets/music/menu.ogg", "volume": 0.35 }开场动画
- 选择主题时视频全屏播放
- 按 Esc 跳过
- 字幕为 WebVTT 格式
"once": true:仅在首次选择主题时播放开场动画
"intro": { "video": "assets/intro.mp4", "subtitles": "assets/intro.vtt", "volume": 0.85, "once": false }WEBVTT 00:00.500 --> 00:03.200 连接已建立。欢迎。 00:03.400 --> 00:06.000 通道已开启。
06配色
自定义文案
客户端可以用主题的语言说话。BERSERK 就是这样:服务器变成了“领地”,设置变成了“装备”。现在任何主题都可以这样做。
- 文案按语言设定,最多 40 个字符
- 未指定的语言将保留客户端的默认文案
"words": {
"ru": { "connect": "ПРИНЯТЬ КЛЕЙМО", "disconnect": "СНЯТЬ КЛЕЙМО", "servers": "ЗЕМЛИ", "settings": "СНАРЯЖЕНИЕ" },
"en": { "connect": "TAKE THE BRAND", "servers": "LANDS" }
}| 密钥 | 俄语 | In English |
|---|---|---|
connect | ПОДКЛЮЧИТЬ | CONNECT |
disconnect | ОТКЛЮЧИТЬ | DISCONNECT |
connecting | ПОДКЛЮЧАЮ… | CONNECTING… |
disconnecting | ОТКЛЮЧАЮ… | DISCONNECTING… |
switching | ПЕРЕКЛЮЧАЮ… | SWITCHING… |
connected | ЗАЩИЩЕНО | PROTECTED |
disconnected | НЕ ЗАЩИЩЕНО | NOT PROTECTED |
servers | СЕРВЕРЫ | SERVERS |
settings | НАСТРОЙКИ | SETTINGS |
about | О ПРОГРАММЕ | ABOUT |
help | ПОМОЩЬ | HELP |
themes | ТЕМЫ | THEMES |
catalog | КАТАЛОГ ТЕМ | THEME CATALOG |
split | РАЗДЕЛЬНЫЙ ТОННЕЛЬ | SPLIT TUNNEL |
update | ОБНОВЛЕНИЕ | UPDATE |
auto | АВТОВЫБОР | AUTO |
ping | ОТКЛИК | PING |
received | ПРИНЯТО | RECEIVED |
sent | ОТДАНО | SENT |
session | СЕССИЯ | SESSION |
node | УЗЕЛ | NODE |
favorites | ИЗБРАННОЕ | FAVORITES |
maintenance | ТЕХ. РАБОТЫ | MAINTENANCE |
menu | МЕНЮ | MENU |
close | ЗАКРЫТЬ | CLOSE |
back | НАЗАД | BACK |
07配色
窗口
| 字段 | 项目 | 限制 |
|---|---|---|
width, height | 窗口大小(像素) | 800-1920 × 560-1080,默认 1000×700 |
radius | 窗口圆角 | 0-48 |
transparent | 透明窗口:没有绘制内容的地方会露出桌面 | true 或 false |
clip | 窗口形状:由 [x, y] 百分比坐标点构成的多边形 | 3-96 个点 |
- 窗口不能用鼠标拉伸:设计按单一尺寸绘制
- 全屏(F11)时客户端会整体缩放窗口,并把边缘空白涂成
bg
"window": {
"width": 1280, "height": 720,
"clip": [[0,4],[6,0],[38,2],[61,0],[100,3],[98,40],[100,71],[97,100],[55,97],[30,100],[2,98],[0,60]]
}clip 后窗口会变为透明。请确认窗口按钮仍在形状之内,否则无法点击。08样式
theme.css
theme.css 文件会叠加在客户端样式之上。
- 请依赖
data-nf标记:它们在不同客户端版本间保持不变 - 窗口内的类名(
.screen、.rows等)属于内部实现,可能随时更名 url()中的路径相对于theme.css
/* 连接按钮:霓虹环 */
[data-nf='connect'] {
border-radius: 999px;
box-shadow: 0 0 32px var(--nf-glow);
}
/* 通道建立期间的脉冲 */
[data-nf-status='connecting'] [data-nf='connect'] { animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 0 8px var(--nf-glow); } }
/* 维护中的节点颜色更淡 */
[data-nf='server'][data-nf-maint] { opacity: .5; }
/* 自定义光标 */
body { cursor: url('assets/cursor.png') 4 0, default; }窗口标记
| data-nf | 说明 |
|---|---|
screen | 基础主题的整个主窗口 |
map | 面板后方的点阵世界地图 |
brand | 客户端标志和名称 |
mark | brand 内的环形标志 |
menu | 菜单栏 |
menu-item | 菜单项。具体是哪一项由 data-nf-item 标明:help、themes、servers、settings、about |
servers | 包含节点列表和按钮的面板 |
server | 节点行。选中:aria-selected="true",维护中:data-nf-maint,收藏: data-nf-fav |
mode-switch | “自动 / 高级”切换开关 |
connect | 连接按钮 |
status | 隧道状态面板 |
status-text | “已保护 / 未保护”文字 |
progress | 通道建立进度条 |
log | 日志窗口 |
meta | 地址、流量和会话时长 |
telegram | Telegram 按钮 |
version | 版本行 |
update-chip | 旧的更新按钮。自 6.0.1 起,更新由客户端底部居中的通用横幅显示,主题既不隐藏也不移动它 |
window-controls | “最小化”、“全屏”、“关闭”按钮 |
dialog | 任意对话框。具体是哪个由 data-nf-dialog 标明:servers、settings、about、help、themes、catalog、split、update、sound |
dialog-title | 对话框标题 |
button | 对话框中的普通按钮 |
toast | 弹出通知 |
key-screen | 密钥输入界面。只能重新上色,场景不会绘制在它上方 |
状态:<body> 上的属性
| 属性 | 取值 |
|---|---|
data-nf-status | off, connecting, on, disconnecting, switching, error |
data-nf-theme | 你的主题 id |
data-nf-lang | ru, en, zh, ja |
data-nf-full | 全屏模式下存在 |
Ctrl+Shift+Backspace 仍能恢复基础主题,但这样的主题审核员不会通过。09场景
完全自己的窗口
场景是包内的一个 HTML 页面。客户端在沙盒中把它铺满整个窗口打开,并通过 SDK 传递状态。浏览器能做的一切在里面都能用:canvas、WebGL、CSS 动画、视频、声音、自定义字体。
"scene": { "entry": "scene/index.html", "mode": "full" }| mode | 行为 |
|---|---|
full | 场景占满整个窗口,客户端主界面被隐藏。客户端对话框(设置、服务器、帮助)在场景上方打开,由令牌和 theme.css 上色 |
backdrop | 场景成为常规客户端窗口背后的动态背景,不接收鼠标和键盘输入 |
窗口图层
- 开场动画intro,位于最上层
- 客户端对话框tokens + theme.css
- 场景 fullscene/index.html
- 客户端窗口tokens + theme.css
- 场景 backdrop动态背景
- 背景background
场景骨架
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="scene.css">
<script src="/__nf/sdk.js"></script>
</head>
<body>
<div id="frame">
<button id="core">-</button>
<div id="nodes"></div>
</div>
<script src="scene.js"></script>
</body>
</html>const frame = document.getElementById('frame');
nf.fit(frame, 1280, 720); // 1280×720 画面
nf.window.dragRegions([{ x: 0, y: 0, w: 1180, h: 48 }]); // 可以拖动顶部移动窗口
const core = document.getElementById('core');
core.addEventListener('click', () => nf.toggle());
function render() {
const s = nf.state;
core.textContent = s.connected ? nf.t('disconnect') : nf.t('connect');
document.body.dataset.status = s.status;
}
nf.on('init', render);
nf.on('state', render);
nf.on('nodes', (nodes) => {
document.getElementById('nodes').replaceChildren(...nodes.map((node) => {
const row = document.createElement('button');
row.textContent = node.name + ' · ' + (node.ping || '-') + ' ms';
row.onclick = () => nf.select(node.id);
return row;
}));
});需要了解的事项
- SDK 始终位于
/__nf/sdk.js,无需放入包中 - 尺寸。窗口也可能全屏。请在固定尺寸的画面中绘制,并用以下方法适配:
nf.fit() - 拖动。窗口没有边框。请用
nf.window.dragRegions()指定可拖动区域,否则窗口无法移动 - 窗口按钮。默认(
"hostChrome": true)下,客户端会在场景右上角叠加一组按钮:主题、最小化、全屏、关闭。要用nf.window.*绘制自己的按钮?请设置"hostChrome": false,模板就是这样做的 - 客户端快捷键(F1-F12、Esc)在场景内同样有效。场景自己需要某个按键时,调用
event.preventDefault(),客户端就不会收到 - 离开。切换主题时场景会收到
leave。返回一个 Promise,客户端最多等待 6 秒
shadowBlur 和 filter 会让这类机器跌到每秒 2 帧。光晕请用 drawImage 绘制现成的精灵图。窗口最小化时请停止 requestAnimationFrame,state 会收到 visible: false。10场景
SDK:对象 nf
属性
| 属性 | 类型 | 项目 |
|---|---|---|
nf.state | 对象 | 通道状态,详见下文。在 init 事件之前等于 null |
nf.stats | 对象 | { rx, tx, rxRate, txRate, ping, at }:会话字节数、速度(字节/秒)、延迟(毫秒)。通道建立期间每秒更新一次 |
nf.nodes | 数组 | 密钥可用的节点:{ id, name, country, city, ping, load, maintenance, favorite, selected }。场景拿不到节点地址 |
nf.settings | 对象 | 主题设置的取值,来自 manifest.settings |
nf.theme | 对象 | { id, version, name, base }。base 是包根目录的地址,以“/”结尾 |
nf.lang | 字符串 | 客户端语言:ru、en、zh、 ja |
nf.preview | boolean | true 在预览中(网站或 nftheme preview),在客户端中为 false |
nf.scale | 数字 | 调用以下方法后的画面缩放: nf.fit() |
nf.version | 字符串 | SDK 版本 |
事件
| 事件 | 收到的数据 | 时机 |
|---|---|---|
init | 全部数据 | 客户端传入数据时触发一次。从这里开始绘制 |
state | nf.state | 通道建立或断开,或节点、语言、声音、全屏发生变化 |
stats | nf.stats | 通道建立期间每秒一次 |
nodes | nf.nodes | 收到新的节点列表或延迟测量结果(F5、nf.ping()) |
settings | nf.settings | 用户在客户端中修改了主题设置 |
lang | 字符串 | 客户端语言已更改 |
leave | 新主题的 id | 主题正在切换。返回 Promise,客户端最多等待 6 秒让场景播完退场 |
方法
| 方法 | 作用 |
|---|---|
nf.on(event, fn) | 订阅,返回取消订阅的函数 |
nf.off(event, fn) | 取消订阅 |
nf.ready() | 场景绘制完毕,客户端移除遮幕。不调用的话会在 load 后自动执行。想让遮幕停留更久:在加载前设置 nf.manualReady = true |
nf.connect() | 建立通道 → Promise |
nf.disconnect() | 断开通道 → Promise |
nf.toggle() | 建立或断开 → Promise |
nf.select(id) | 选择节点。通道已建立时客户端会自动切换 |
nf.auto() | 恢复自动选择节点 |
nf.ping() | 测量所有节点的延迟,等同于 F5 |
nf.favorite(id) | 将节点加入或移出收藏 |
nf.open(screen) | 打开客户端界面:servers、settings、about、help、themes、catalog、split、update |
nf.toast(text) | 窗口底部的通知,最多 140 个字符 |
nf.window.minimize() | 最小化窗口 |
nf.window.close() | 收起到托盘 |
nf.window.fullscreen() | 切换全屏 |
nf.window.dragRegions(rects) | 场景中哪些区域可以拖动窗口:[{ x, y, w, h }],以场景像素计,最多 16 个 |
nf.t(key) | 当前语言下的客户端文案,已考虑 manifest.words |
nf.asset(path) | 包内文件的完整地址 |
nf.storage.get(key) | 从主题存储读取 → Promise |
nf.storage.set(key, value) | 写入任意 JSON,每个主题总计最多 64 KB → Promise |
nf.storage.remove(key) | 删除 → Promise |
nf.sound.play(path, { volume, loop }) | 播放包内文件,遵循主题的开关和音量 |
nf.sound.enabled / nf.sound.volume | 主题声音是否开启以及总音量 0..1 |
nf.fit(el, w, h, mode) | 把 w×h 画面适配到窗口。mode:contain 或 cover |
nf.format.bytes(n) / rate(n) / duration(ms) | «1.3 GB», «350 KB/s», «03:46:32» |
所有客户端调用都返回 Promise。如果客户端拒绝(例如密钥未激活),Promise 会以清晰的错误被拒绝。
state 对象
{
status: 'on', // off | connecting | on | disconnecting | switching | error
connected: true,
busy: false, // connecting、disconnecting 或 switching
node: { id: 'nl-1', name: '荷兰 · 阿姆斯特丹', country: 'NL', city: '阿姆斯特丹',
ping: 41, load: 23, maintenance: false, favorite: true, selected: true },
auto: true, // 由客户端选择节点
since: 1791232800000, // 通道建立的时间(毫秒),否则为 null
error: null, // status === 'error' 时的错误文本
lang: 'ru',
words: { connect: '连接', servers: '服务器', … }, // 已应用 manifest.words
sound: { enabled: true, volume: 0.8 },
visible: true, // 窗口在屏幕上:未最小化,也未收起到托盘
fullscreen: false,
reduceMotion: false, // 系统开启了“减少动态效果”
platform: 'windows', // windows | linux
version: '6.0.0',
sub: { plan: 'NII IZNANKA', daysLeft: 27 },
update: { available: false, version: null }
}11任意层级
主题设置
- 在清单中描述设置,客户端会在设置窗口中你的主题区块里自动绘制它们
- 取值由客户端保存。场景通过
nf.settings和以下事件获取:settings - 最多 16 项设置
| type | 字段 | 取值 |
|---|---|---|
toggle | default:true 或 false | boolean |
range | min, max, step, default | 数字 |
select | options:2-12 个选项 { value, label }, default | 字符串 |
color | default: #rrggbb | #rrggbb 字符串 |
"settings": [
{ "id": "glitch", "type": "toggle", "label": { "ru": "Помехи", "en": "Glitches" }, "default": true },
{ "id": "speed", "type": "range", "label": "光环速度", "min": 0, "max": 2, "step": 0.1, "default": 1 },
{ "id": "scene", "type": "select", "label": "场景", "default": "grid",
"options": [ { "value": "grid", "label": "网格" }, { "value": "signal", "label": "信号" } ] },
{ "id": "tint", "type": "color", "label": "颜色", "default": "#c42b47", "hint": "按钮周围光环的颜色" }
]nf.on('settings', (values) => {
document.body.classList.toggle('glitch', values.glitch);
rings.speed = values.speed;
});主题声音无需单独描述。只要主题包含 sounds、music 或 intro,客户端会自动添加开关和音量。
12安全
沙盒
主题是运行在存有用户 VPN 密钥的软件里的他人代码。因此客户端不信任任何主题,即使是审核过的。
nftheme://id/:仅限自己的文件网络密钥文件窗口cookie
- 网络完全关闭。
fetch、XMLHttpRequest、WebSocket、来自外部地址的图片和字体、WebRTC:全部由客户端拦截。主题只能使用自己包内的文件 - 场景是隔离的。它是与客户端窗口不同源的 iframe:无法访问客户端 DOM、Cookie 和
localStorage。保存自己的数据可以用nf.storage - 没有任何机密。没有密钥、节点地址和 IP,参见 state
- 不能开窗口,也不能跳转。场景无法打开新窗口、无法让页面跳走,也无法在浏览器中打开链接
- theme.css 同样无网络。
url()只能引用包内文件 - 签名。客户端只从目录安装带有工作室签名的包。文件被替换的话 sha256 对不上,签名校验失败
- 吊销。如果在已发布的主题中发现有害内容,它会被吊销。客户端在下次检查目录时会自动停用并删除它
紧急退出
Ctrl+Shift+Backspace:随时恢复基础主题。该组合键在场景之前由客户端捕获,主题无法拦截- 使用
--safe-theme参数启动:客户端直接以基础主题打开
13调试
如何测试
在浏览器中
nftheme preview my-theme 会在 127.0.0.1:4790 启动一个客户端模拟器:
- 带
data-nf标记的窗口、你的背景、令牌和theme.css - 沙盒中的场景
- 右侧控制台:通道状态、语言、声音、你的设置、校验错误、SDK 调用日志
- 每次保存都会重新读取文件
在客户端中
- 主题 → 作者 → “打开主题文件夹”:从文件夹安装主题,每次保存都会重新加载
- “打开 .nftheme 文件”:检查打包好的主题
- “开发者工具”:带场景控制台、布局和网络面板的 DevTools,可以看到哪些被拦截
来自文件夹或文件的主题会标记为未验证,并与目录主题运行在同样的沙盒中。
常见错误
| 提示 | 处理方法 |
|---|---|
包中没有文件“…” | 清单中的路径与文件不符。区分大小写 |
名称无效 | 用拉丁字母重命名文件,不要有空格 |
内容不像 .mp3 | 文件是其他格式。请重新编码,改名没用 |
场景未引入 SDK | 在 <head> 中加入 <script src="/__nf/sdk.js"> |
id“…”已被占用 | 这是其他作者或工作室主题的 id。请换一个自己的 |
版本不比已发布的新 | 提高 version |
| 窗口无法移动 | 场景中没有 nf.window.dragRegions() |
| 来自互联网的图片加载不出来 | 这是有意为之。请把它放进包里 |
14发布
审核与目录
- 通过个人中心登录工作室:使用邮箱或机器人发来的链接
- 拖入主题包。网站会用与
nftheme check相同的代码校验它并加入队列 - 审核员会在沙盒和客户端中打开主题并查看代码。结果通过 Telegram 通知,拒绝时会附上原因
- 服务器为通过审核的版本签名,然后它会出现在目录中。已安装该主题的客户端会自动更新
审核员检查什么
| 能通过 | 不能通过 |
|---|---|
| 主题可用:连接、选择节点、对话框 | 找不到或看不懂连接按钮 |
| 预览:主题本身的真实截图 | 用其他主题的预览或他人美术作品吸引眼球 |
| 可读的代码。知名库注明版本和许可证 | 没有源码的混淆或压缩代码、eval、从字符串执行代码 |
美术、音乐、字体为原创或已获使用授权,来源写在 credits | 未经许可使用他人作品。权利人投诉后主题会被下架 |
| 可以离开主题:切换主题、最小化、关闭 | 隐藏主题按钮、陷阱、无预警的全屏闪烁 |
声音通过 sounds 或 nf.sound 播放,遵循设置 | 绕过设置的声音、突然的巨响 |
| 空闲时主题几乎不占用 CPU | 窗口最小化时风扇狂转 |
| 伪造的密钥输入或支付界面、广告、链接、推荐返利、18+、煽动和骚扰 |
版本、隐藏、吊销
- 每个新版本都要重新审核。审核期间目录中保留旧版本
- 新的提交会撤回你之前尚未审核的提交
- 被拒绝的版本可以用同一版本号重新提交
- 已隐藏:主题已从展示中撤下,已安装的用户仍可保留
- 已吊销:主题有危险,客户端会停用并删除它
- 每天最多上传 12 次
15示例
现成技巧
用图片代替连接按钮
[data-nf='connect'] {
width: 220px; height: 220px; padding: 0;
border: 0; border-radius: 50%;
background: url('assets/sigil-off.png') center / contain no-repeat;
color: transparent; /* 隐藏文字,屏幕阅读器仍会朗读 */
filter: grayscale(1) brightness(.7);
transition: filter .4s, transform .2s;
}
[data-nf-status='on'] [data-nf='connect'] {
background-image: url('assets/sigil-on.png');
filter: drop-shadow(0 0 24px var(--nf-glow));
}
[data-nf='connect']:hover { transform: scale(1.04); }断线时闪烁
[data-nf-status='error'] [data-nf='screen'] { animation: lost .12s steps(2) 6; }
@keyframes lost { 50% { filter: invert(1) hue-rotate(160deg); } }常规窗口背后的动态背景
"tokens": { "bg": "transparent", "panel": "rgba(0,0,0,.55)" },
"scene": { "entry": "stars/index.html", "mode": "backdrop" }const c = document.querySelector('canvas');
const ctx = c.getContext('2d');
const stars = Array.from({ length: 220 }, () => ({ x: Math.random(), y: Math.random(), z: Math.random() }));
let speed = 0.0004;
nf.on('state', (s) => { speed = s.connected ? 0.003 : s.busy ? 0.008 : 0.0004; });
(function frame() {
c.width = innerWidth; c.height = innerHeight;
for (const s of stars) {
s.z -= speed; if (s.z <= 0) s.z = 1;
const k = 0.5 / s.z;
ctx.fillStyle = 'rgba(255,255,255,' + (1 - s.z) + ')';
ctx.fillRect((s.x - 0.5) * k * c.width + c.width / 2, (s.y - 0.5) * k * c.height + c.height / 2, 2, 2);
}
if (!nf.state || nf.state.visible !== false) requestAnimationFrame(frame);
else nf.on('state', function wake(s) { if (s.visible) { nf.off('state', wake); requestAnimationFrame(frame); } });
})();会话纪录
nf.on('state', async (s) => {
if (s.status !== 'off' || !nf.state.since) return;
const minutes = Math.round((Date.now() - nf.state.since) / 60000);
const best = (await nf.storage.get('best')) || 0;
if (minutes > best) {
await nf.storage.set('best', minutes);
nf.toast('新纪录:' + minutes + ' 分钟');
}
});连接时的登陆画面
let was = null;
nf.on('state', (s) => {
if (was !== 'on' && s.status === 'on') {
deploy.querySelector('b').textContent = s.node ? s.node.name : '';
deploy.hidden = false;
nf.sound.play('assets/deploy.mp3');
setTimeout(() => { deploy.hidden = true; }, 2200);
}
was = s.status;
});16常见问题
要点速览
Android 上会有主题吗?
会的,使用同样的格式。配色、文案和声音在那里也能用,场景将通过 WebView 运行。请确认主题在 1000×700 窗口中也正常:手机屏幕是竖屏。
可以做游戏或动漫主题吗?
可以,前提是你拥有美术和声音的权利,或者权利人不反对。请在 credits 中注明来源。权利人投诉的话,主题会被下架。
可以使用 three.js、PixiJS、GSAP 吗?
可以。把库文件放进包里(网络已关闭,无法从 CDN 加载),不要修改它,并在 credits 中注明版本和许可证。审核员会将其与官方文件比对。
主题把设置和纪录存在哪里?
manifest.settings 中的设置由客户端保存。自己的数据放进 nf.storage,最多 64 KB。删除主题时数据会一并删除。
审核需要多久?
通常在一天之内。结果通过 Telegram 通知。
工作室主题也是这样做的吗?
差不多。BERSERK、CYBERPUNK 等作为带工作室签名的独立包放在目录中。主主题 NII IZNANKA 还内置在客户端里。工作室主题可以更深入地嵌入窗口:host 字段仅对带工作室签名的包开放。
准备好了吗?