NII IZNANKAIZNANKA.TECH · VPN
ZH

作者文档 · 格式 1 · 客户端 6.0+

如何制作主题

从新配色到用 HTML 和 JS 打造自己的场景,全部在这一页。

个层级
3
个令牌
17
个窗口标记
24

00概览

什么是主题

主题会改变 NII Iznanka 电脑客户端的外观和声音。

  • 从 6.0 版起,主题不再内置于客户端
  • 每个主题都以独立包的形式放在目录中 .nftheme
  • 客户端只在选中主题时才下载它
  • 安装包中只保留了一个基础主题:NII IZNANKA

任何人都能制作主题。主题包就是普通的 zip:manifest.json、图片、声音,需要的话再加上自己的 HTML、CSS 和 JS。在工作室上传后,审核员检查,服务器为版本签名,主题就会出现在所有人的目录里。

01配色

只需 manifest.json:颜色、字体、背景、音效、音乐、开场动画、自定义文案、窗口大小。无需代码。

02样式

再加上覆盖客户端窗口的 theme.css:按钮、面板和对话框可做成任意形状,还能按通道状态播放动画。

03场景

完全自己的窗口:一个 HTML、CSS 和 JS 页面。它从客户端获取状态,并通过 SDK 控制客户端。

层级可以叠加。场景同样可以有令牌和 theme.css:用来给在场景上方打开的客户端对话框上色。

可以修改的内容

项目方式层级
窗口和对话框的全部颜色tokens配色
字体:正文、标题、等宽fonts配色
背景:图片或视频、暗化、模糊background配色
连接、断开、错误、点击的音效sounds配色
背景音乐music配色
选择主题时的开场动画,带字幕intro配色
文案:“连接” → “进入球域”words配色
窗口大小、圆角和形状,甚至撕裂边缘window配色
窗口和托盘图标icon配色
窗口的任意元素:形状、布局、动画theme.css样式
整个窗口:自己的按钮、节点列表和界面scene场景
常规窗口背后的动态背景scene.mode: backdrop场景
在客户端设置窗口中加入自己的主题设置settings任意

主题的流程

  1. 01作者nftheme pack
  2. 02工作室上传到网站
  3. 03审核主题与代码
  4. 04签名工作室 Ed25519
  5. 05目录iznanka.tech
  6. 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

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.json64 KB
▸ 视频
主题里最占空间的部分。1280×720、H.264、2-3 Mbit/s 的视频每分钟约 20 MB。开场动画越短,主题下载越快。

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 个路径主题页面的截图
iconpng 路径窗口和托盘图标
license最多 60 字的字符串许可证,例如 CC-BY-4.0
credits最多 12 个字符串致谢:美术、音乐、字体的作者
homepagehttps://…主题页面上的作者链接
window对象窗口大小和形状,见第 07
tokens对象颜色和尺寸,见第 04
fonts对象字体,见第 04
background对象背景,见第 05
sounds对象事件音效,见第 05
music路径或对象背景音乐,见第 05
intro路径或对象开场动画,见第 05
words对象自定义文案,见第 06
css路径样式文件,见第 08
scene路径或对象场景,见第 09
settings数组主题设置,见第 11
manifest.json:全部字段
{
  "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窗口中央的光晕
panelrgba(12,10,22,.78)面板、菜单和对话框的背景
line#23232e边框和分隔线
text#e9e9f2正文
muted#6f6f80说明和次要文字
accent#a855f7主色:连接按钮、选中的节点、标题
accent2#7c3aed第二强调色:悬停、渐变
ok#4ade80通道已建立,“已保护”
warn#f97316维护、警告
danger#ef4444错误、通道中断
glowrgba(168,85,247,.45)强调色周围的光晕
shadowrgba(0,0,0,.6)对话框阴影
overlayrgba(0,0,0,.72)对话框下方的遮罩
radius0px按钮和面板的圆角
border1px边框粗细
gap14px面板间距

试一试

在右侧调整颜色,下方会生成可直接用于 manifest.json 的片段。

NII IZNANKA VPN
帮助主题服务器设置
荷兰 · 41 ms立陶宛 · 33 ms意大利 · 58 ms连接
STATUS已保护已接收 1.3 GB

    

字体

三种角色。客户端会加载文件并设置 --nf-font-body、--nf-font-display、--nf-font-mono。

  • body:全部文字
  • display:大号文字和按钮
  • mono:数字、日志、地址
manifest.json
"fonts": {
  "body": "assets/fonts/grotesk.woff2",
  "display": "assets/fonts/pixel.woff2",
  "mono": "assets/fonts/mono.woff2"
}
▸ 语言
客户端支持四种语言。如果字体缺少西里尔字母或汉字,客户端会换用系统字体。请在每种语言下检查主题。字体需允许嵌入的许可证,OFL 即可。

05配色

背景、声音、开场动画

背景

字段项目默认值
image背景图片无
video背景视频:静音、循环,叠在图片上方无
color图片下方的颜色无
fitcover、contain 或 fillcover
position与 CSS 相同:center、top、 left 30%center
dim暗化 0…10
blur模糊 0…40 px0

事件音效

事件音效由客户端自己播放。每个主题在客户端设置中都有独立的开关和音量。

事件播放时机
connect通道已建立
disconnect通道已断开
error连接失败或通道中断
switch在活动通道上切换节点
click在客户端窗口中点击按钮
hover悬停在按钮上
open, close对话框打开或关闭
notify通知
manifest.json
"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:仅在首次选择主题时播放开场动画
manifest.json
"intro": { "video": "assets/intro.mp4", "subtitles": "assets/intro.vtt", "volume": 0.85, "once": false }
assets/intro.vtt
WEBVTT

00:00.500 --> 00:03.200
连接已建立。欢迎。

00:03.400 --> 00:06.000
通道已开启。

06配色

自定义文案

客户端可以用主题的语言说话。BERSERK 就是这样:服务器变成了“领地”,设置变成了“装备”。现在任何主题都可以这样做。

  • 文案按语言设定,最多 40 个字符
  • 未指定的语言将保留客户端的默认文案
manifest.json
"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
manifest.json:像 BERSERK 那样的撕裂边缘
"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
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客户端标志和名称
markbrand 内的环形标志
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地址、流量和会话时长
telegramTelegram 按钮
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-statusoff, connecting, on, disconnecting, switching, error
data-nf-theme你的主题 id
data-nf-langru, en, zh, ja
data-nf-full全屏模式下存在
不要隐藏出口
主题切换按钮和窗口按钮必须可见:必须随时都能离开主题。Ctrl+Shift+Backspace 仍能恢复基础主题,但这样的主题审核员不会通过。

09场景

完全自己的窗口

场景是包内的一个 HTML 页面。客户端在沙盒中把它铺满整个窗口打开,并通过 SDK 传递状态。浏览器能做的一切在里面都能用:canvas、WebGL、CSS 动画、视频、声音、自定义字体。

manifest.json
"scene": { "entry": "scene/index.html", "mode": "full" }
mode行为
full场景占满整个窗口,客户端主界面被隐藏。客户端对话框(设置、服务器、帮助)在场景上方打开,由令牌和 theme.css 上色
backdrop场景成为常规客户端窗口背后的动态背景,不接收鼠标和键盘输入

窗口图层

  1. 开场动画intro,位于最上层
  2. 客户端对话框tokens + theme.css
  3. 场景 fullscene/index.html
  4. 客户端窗口tokens + theme.css
  5. 场景 backdrop动态背景
  6. 背景background

场景骨架

scene/index.html
<!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>
scene/scene.js
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 秒
没有显卡时
在虚拟机或远程桌面中,canvas 由 CPU 绘制。在帧循环里使用 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.previewbooleantrue 在预览中(网站或 nftheme preview),在客户端中为 false
nf.scale数字调用以下方法后的画面缩放: nf.fit()
nf.version字符串SDK 版本

事件

事件收到的数据时机
init全部数据客户端传入数据时触发一次。从这里开始绘制
statenf.state通道建立或断开,或节点、语言、声音、全屏发生变化
statsnf.stats通道建立期间每秒一次
nodesnf.nodes收到新的节点列表或延迟测量结果(F5、nf.ping())
settingsnf.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 对象

nf.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 }
}
▸ state 中没有的内容
密钥、节点地址和用户 IP。场景不需要这些。

11任意层级

主题设置

  • 在清单中描述设置,客户端会在设置窗口中你的主题区块里自动绘制它们
  • 取值由客户端保存。场景通过 nf.settings 和以下事件获取: settings
  • 最多 16 项设置
type字段取值
toggledefault:true 或 falseboolean
rangemin, max, step, default数字
selectoptions:2-12 个选项 { value, label }, default字符串
colordefault: #rrggbb#rrggbb 字符串
manifest.json
"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": "按钮周围光环的颜色" }
]
scene/scene.js
nf.on('settings', (values) => {
  document.body.classList.toggle('glitch', values.glitch);
  rings.speed = values.speed;
});

主题声音无需单独描述。只要主题包含 sounds、music 或 intro,客户端会自动添加开关和音量。

12安全

沙盒

主题是运行在存有用户 VPN 密钥的软件里的他人代码。因此客户端不信任任何主题,即使是审核过的。

场景 · 沙盒HTML、CSS、JS、canvas、视频nftheme://id/:仅限自己的文件
NII Iznanka 客户端密钥、配置、地址隧道、文件、网络

网络密钥文件窗口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 调用日志
  • 每次保存都会重新读取文件
▸ 模拟器
模拟器对场景和 SDK 的展示是准确的。常规客户端窗口相似但并非完全一致:theme.css 的最终效果请在客户端中查看。

在客户端中

  1. 主题 → 作者 → “打开主题文件夹”:从文件夹安装主题,每次保存都会重新加载
  2. “打开 .nftheme 文件”:检查打包好的主题
  3. “开发者工具”:带场景控制台、布局和网络面板的 DevTools,可以看到哪些被拦截

来自文件夹或文件的主题会标记为未验证,并与目录主题运行在同样的沙盒中。

常见错误

提示处理方法
包中没有文件“…”清单中的路径与文件不符。区分大小写
名称无效用拉丁字母重命名文件,不要有空格
内容不像 .mp3文件是其他格式。请重新编码,改名没用
场景未引入 SDK在 <head> 中加入 <script src="/__nf/sdk.js">
id“…”已被占用这是其他作者或工作室主题的 id。请换一个自己的
版本不比已发布的新提高 version
窗口无法移动场景中没有 nf.window.dragRegions()
来自互联网的图片加载不出来这是有意为之。请把它放进包里

14发布

审核与目录

  1. 通过个人中心登录工作室:使用邮箱或机器人发来的链接
  2. 拖入主题包。网站会用与 nftheme check 相同的代码校验它并加入队列
  3. 审核员会在沙盒和客户端中打开主题并查看代码。结果通过 Telegram 通知,拒绝时会附上原因
  4. 服务器为通过审核的版本签名,然后它会出现在目录中。已安装该主题的客户端会自动更新

审核员检查什么

能通过不能通过
主题可用:连接、选择节点、对话框找不到或看不懂连接按钮
预览:主题本身的真实截图用其他主题的预览或他人美术作品吸引眼球
可读的代码。知名库注明版本和许可证没有源码的混淆或压缩代码、eval、从字符串执行代码
美术、音乐、字体为原创或已获使用授权,来源写在 credits未经许可使用他人作品。权利人投诉后主题会被下架
可以离开主题:切换主题、最小化、关闭隐藏主题按钮、陷阱、无预警的全屏闪烁
声音通过 sounds 或 nf.sound 播放,遵循设置绕过设置的声音、突然的巨响
空闲时主题几乎不占用 CPU窗口最小化时风扇狂转
伪造的密钥输入或支付界面、广告、链接、推荐返利、18+、煽动和骚扰

版本、隐藏、吊销

  • 每个新版本都要重新审核。审核期间目录中保留旧版本
  • 新的提交会撤回你之前尚未审核的提交
  • 被拒绝的版本可以用同一版本号重新提交
  • 已隐藏:主题已从展示中撤下,已安装的用户仍可保留
  • 已吊销:主题有危险,客户端会停用并删除它
  • 每天最多上传 12 次

15示例

现成技巧

用图片代替连接按钮

theme.css
[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); }

断线时闪烁

theme.css
[data-nf-status='error'] [data-nf='screen'] { animation: lost .12s steps(2) 6; }
@keyframes lost { 50% { filter: invert(1) hue-rotate(160deg); } }

常规窗口背后的动态背景

manifest.json
"tokens": { "bg": "transparent", "panel": "rgba(0,0,0,.55)" },
"scene": { "entry": "stars/index.html", "mode": "backdrop" }
stars/stars.js
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); } });
})();

会话纪录

scene/scene.js
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 + ' 分钟');
  }
});

连接时的登陆画面

scene/scene.js
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 字段仅对带工作室签名的包开放。

准备好了吗?

发布你的主题