00Обзор
Что такое тема
Тема меняет, как выглядит и звучит клиент НИИ Изнанка на компьютере.
- С версии 6.0 темы не вшиты в клиент
- Каждая тема лежит в каталоге отдельным пакетом
.nftheme - Клиент скачивает тему, только когда её выбрали
- В сборке осталась одна базовая тема: НИИ ИЗНАНКА
Сделать тему может любой. Пакет это обычный 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 "МОЯ ТЕМА" # 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": "ваш_ник",
"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 МБ |
| распакованный пакет | 160 МБ |
| один файл | 48 МБ |
| файлов в пакете | 600 |
| текстовый файл | 2 МБ |
| картинка | 8 МБ, превью до 4 МБ, значок до 1 МБ |
| путь к файлу | 160 символов, до 8 уровней |
| manifest.json | 64 КБ |
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": ["арт и звук: fawn", "шрифт 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": "Помехи", "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 КБ на тему → 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 | добавьте <script src="/__nf/sdk.js"> в <head> |
id «…» занят | это id другого автора или темы студии. Выбери свой |
Версия не новее опубликованной | подними version |
| окно не двигается | в сцене нет nf.window.dragRegions() |
| картинка из интернета не грузится | так и задумано. Положи её в пакет |
14Публикация
Проверка и каталог
- Войди в студию через кабинет: по почте или ссылкой из бота
- Перетащи пакет. Сайт проверит его тем же кодом, что и
nftheme check, и поставит в очередь - Модератор откроет тему в песочнице и в клиенте, посмотрит код. Решение придёт в Telegram, при отказе с причиной
- Одобренную версию сервер подписывает, и она появляется в каталоге. У кого тема уже стоит, клиент обновит её сам
Что смотрит модератор
| Пройдёт | Не пройдёт |
|---|---|
| тема работает: подключение, выбор узла, диалоги | кнопку подключения не найти или не понять |
| превью: честный снимок самой темы | превью от другой темы или чужой арт для привлечения |
| читаемый код. Известные библиотеки с версией и лицензией | запутанный или сжатый код без исходника, eval, код из строк |
арт, музыка, шрифты свои или с правом использования, источники в credits | чужое без разрешения. По жалобе правообладателя тему снимут |
| из темы можно уйти: смена темы, свернуть, закрыть | спрятанная кнопка темы, ловушки, мигание во весь экран без предупреждения |
звук через sounds или nf.sound, уважает настройки | звук мимо настроек, громкие сюрпризы |
| в простое тема почти не грузит процессор | вентилятор взлетает, пока окно свёрнуто |
| фальшивые экраны ввода ключа или оплаты, реклама, ссылки, рефералки, 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 КБ. Удалили тему: они удалятся вместе с ней.
Сколько ждать проверку?
Обычно до суток. Решение придёт в Telegram.
Темы студии устроены так же?
Почти. BERSERK, CYBERPUNK и другие лежат в каталоге отдельными пакетами с подписью студии. Главная тема НИИ ИЗНАНКА к тому же встроена в клиент. Темам студии можно встраиваться в окно глубже: поле host доступно только пакетам с подписью студии.
Готовы?