НИИ ИЗНАНКАIZNANKA.TECH · VPN

Документация автора · формат 1 · клиент 6.0+

Как сделать тему

От новой палитры до своей сцены на HTML и JS. Всё на одной странице.

Уровня
3
Токенов
17
Меток окна
24

00Обзор

Что такое тема

Тема меняет, как выглядит и звучит клиент НИИ Изнанка на компьютере.

  • С версии 6.0 темы не вшиты в клиент
  • Каждая тема лежит в каталоге отдельным пакетом .nftheme
  • Клиент скачивает тему, только когда её выбрали
  • В сборке осталась одна базовая тема: НИИ ИЗНАНКА

Сделать тему может любой. Пакет это обычный 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 "МОЯ ТЕМА"

# 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": "ваш_ник",
  "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.json64 КБ
▸ Видео
Самое тяжёлое в теме. Ролик 1280×720 в H.264 на 2-3 Мбит/с весит около 20 МБ за минуту. Короче заставка, быстрее скачается тема.

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 строктитры: чей арт, музыка, шрифты
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": ["арт и звук: 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подсветка в центре окна
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.

НИИ ИЗНАНКА 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знак и надпись клиента
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-statusoff, connecting, on, disconnecting, switching, error
data-nf-themeid твоей темы
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 секунд
Без видеокарты
В виртуалке или на удалённом рабочем столе холст рисуется процессором. 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строкасменился язык клиента
leaveid новой темытему меняют. Верни 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

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/: только свои файлы
Клиент НИИ Изнанкаключ, конфигурация, адресатоннель, файлы, сеть

сетьключфайлыокна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добавьте <script src="/__nf/sdk.js"> в <head>
id «…» занятэто id другого автора или темы студии. Выбери свой
Версия не новее опубликованнойподними version
окно не двигаетсяв сцене нет nf.window.dragRegions()
картинка из интернета не грузитсятак и задумано. Положи её в пакет

14Публикация

Проверка и каталог

  1. Войди в студию через кабинет: по почте или ссылкой из бота
  2. Перетащи пакет. Сайт проверит его тем же кодом, что и nftheme check, и поставит в очередь
  3. Модератор откроет тему в песочнице и в клиенте, посмотрит код. Решение придёт в Telegram, при отказе с причиной
  4. Одобренную версию сервер подписывает, и она появляется в каталоге. У кого тема уже стоит, клиент обновит её сам

Что смотрит модератор

ПройдётНе пройдёт
тема работает: подключение, выбор узла, диалогикнопку подключения не найти или не понять
превью: честный снимок самой темыпревью от другой темы или чужой арт для привлечения
читаемый код. Известные библиотеки с версией и лицензиейзапутанный или сжатый код без исходника, eval, код из строк
арт, музыка, шрифты свои или с правом использования, источники в creditsчужое без разрешения. По жалобе правообладателя тему снимут
из темы можно уйти: смена темы, свернуть, закрытьспрятанная кнопка темы, ловушки, мигание во весь экран без предупреждения
звук через sounds или nf.sound, уважает настройкизвук мимо настроек, громкие сюрпризы
в простое тема почти не грузит процессорвентилятор взлетает, пока окно свёрнуто
фальшивые экраны ввода ключа или оплаты, реклама, ссылки, рефералки, 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 КБ. Удалили тему: они удалятся вместе с ней.

Сколько ждать проверку?

Обычно до суток. Решение придёт в Telegram.

Темы студии устроены так же?

Почти. BERSERK, CYBERPUNK и другие лежат в каталоге отдельными пакетами с подписью студии. Главная тема НИИ ИЗНАНКА к тому же встроена в клиент. Темам студии можно встраиваться в окно глубже: поле host доступно только пакетам с подписью студии.

Готовы?

Выкладывай свою