add RPC to neutralino

This commit is contained in:
Julien Maille 2026-02-09 23:55:05 +01:00
parent e9e37c9be1
commit 8f27000fcf
13 changed files with 437 additions and 464 deletions

View file

@ -41,21 +41,23 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Neutralino CLI
run: npm install -g @neutralinojs/neu
- name: Install dependencies
run: npm install
- name: Download Neutralino binaries
run: neu update
run: npx neu update
- name: Build application
run: neu build
run: npm run build
- name: Prepare Release
run: |
mkdir release
cp dist/Monochrome/resources.neu release/
cp neutralino.config.json release/
cp -r extensions release/
cp dist/Monochrome/${{ matrix.binary_source }} release/${{ matrix.binary_dest }}
shell: bash

2
.gitignore vendored
View file

@ -13,3 +13,5 @@ bin/
*.log
.storage/
auth_storage/
www

View file

@ -0,0 +1,91 @@
# bridge.ps1 - JSON Depth Fix
# $Log = Join-Path $PSScriptRoot "bridge_final.log"
function Log($m) { }
Log "--- START (DEPTH FIX) ---"
# 1. PID
$p = Get-Process Monochrome -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $p) { $p = Get-Process neutralino-win_x64 -ErrorAction SilentlyContinue | Select-Object -First 1 }
$pid_to_send = if ($p) { $p.Id } else { [System.Diagnostics.Process]::GetCurrentProcess().Id }
# 2. Discord Connection
function Get-Pipe {
for ($i = 0; $i -le 9; $i++) {
try {
$pn = "discord-ipc-$i"
$p = New-Object System.IO.Pipes.NamedPipeClientStream(".", $pn, [System.IO.Pipes.PipeDirection]::InOut)
$p.Connect(100)
return $p
} catch { }
}
return $null
}
$pipe = Get-Pipe; if (-not $pipe) { Log "Discord Fail"; exit }
function Send-Packet($op, $json) {
if ($op -eq 1) { Log "Sending Activity: $json" }
$j = [System.Text.Encoding]::UTF8.GetBytes($json)
[byte[]]$pkt = [BitConverter]::GetBytes([int]$op) + [BitConverter]::GetBytes([int]$j.Length) + $j
$pipe.Write($pkt, 0, $pkt.Length); $pipe.Flush()
}
# 3. Handshake
Send-Packet 0 (@{ v = 1; client_id = "1462186088184549661" } | ConvertTo-Json -Compress)
$h = New-Object byte[] 8; if ($pipe.Read($h, 0, 8) -eq 8) {
$l = [BitConverter]::ToInt32($h, 4); $b = New-Object byte[] $l; $pipe.Read($b, 0, $l) | Out-Null
Log "Handshake OK"
}
function Set-Activity($d, $s, $img) {
$activity = @{
details = [string]$d
state = [string]$s
type = 2
assets = @{
large_image = if ($img -and $img.StartsWith("http")) { [string]$img } else { "monochrome" }
large_text = "Monochrome"
}
}
# CRITICAL: -Depth 10 ensures 'assets' is not stringified as a class name
$payload = @{
cmd = "SET_ACTIVITY"
args = @{ pid = [int]$pid_to_send; activity = $activity }
nonce = [Guid]::NewGuid().ToString()
} | ConvertTo-Json -Compress -Depth 10
Send-Packet 1 $payload
}
Start-Sleep -Seconds 1
Set-Activity "Idling" "Monochrome" $null
# 4. Config & WS
$line = [Console]::In.ReadLine()
if (-not $line) { exit }
$config = $line | ConvertFrom-Json
$ws = New-Object System.Net.WebSockets.ClientWebSocket
try {
$uri = [Uri]"ws://127.0.0.1:$($config.nlPort)?extensionId=$($config.nlExtensionId)&connectToken=$($config.nlConnectToken)"
$ws.ConnectAsync($uri, [System.Threading.CancellationToken]::None).Wait()
Log "WS Connected"
} catch { exit }
# 5. Loop
$buf = New-Object byte[] 65536
while ($ws.State -eq "Open") {
$task = $ws.ReceiveAsync((New-Object ArraySegment[byte] -ArgumentList @(,$buf)), [System.Threading.CancellationToken]::None)
while (-not $task.Wait(1000)) { if (-not (Get-Process -Id $pid_to_send -ErrorAction SilentlyContinue)) { exit } }
if ($task.Result.Count -gt 0) {
try {
$raw = [System.Text.Encoding]::UTF8.GetString($buf, 0, $task.Result.Count)
$msg = $raw | ConvertFrom-Json
if ($msg.event -eq "discord:update") {
Set-Activity $msg.data.details $msg.data.state $msg.data.largeImageKey
}
elseif ($msg.event -eq "discord:clear") { Set-Activity "Idling" "Monochrome" $null }
} catch {}
}
}

View file

@ -0,0 +1,142 @@
# bridge.py - Production Discord RPC Bridge (Linux/macOS)
import sys, json, socket, struct, os, uuid, base64, time
CLIENT_ID = "1462186088184549661"
LAST_STATUS = ""
def get_discord_path():
for i in range(10):
path = os.path.join(os.environ.get('XDG_RUNTIME_DIR', '/tmp'), f'discord-ipc-{i}')
if os.path.exists(path): return path
return None
def send_packet(s, op, data):
payload = json.dumps(data).encode('utf-8')
header = struct.pack('<II', op, len(payload))
s.sendall(header + payload)
def recv_packet(s):
try:
header = s.recv(8)
if len(header) < 8: return None
op, length = struct.unpack('<II', header)
payload = s.recv(length)
return json.loads(payload.decode('utf-8'))
except: return None
def set_activity(ds, pid, details, state, img=None):
global LAST_STATUS
current = f"{details}-{state}-{img}"
if current == LAST_STATUS: return
LAST_STATUS = current
activity = {
"details": str(details or "Idling"),
"state": str(state or "Monochrome"),
"type": 2, # Listening
"assets": {
"large_image": img if img and img.startswith('http') else "monochrome",
"large_text": "Monochrome"
}
}
send_packet(ds, 1, {
"cmd": "SET_ACTIVITY",
"args": {"pid": pid, "activity": activity},
"nonce": str(uuid.uuid4())
})
def main():
# 1. Read config
try:
line = sys.stdin.readline()
if not line: return
config = json.loads(line)
except: return
ppid = os.getppid()
# 2. Connect to Discord
ipc_path = get_discord_path()
if not ipc_path: return
try:
ds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
ds.connect(ipc_path)
except: return
# 3. Handshake
send_packet(ds, 0, {"v": 1, "client_id": CLIENT_ID})
recv_packet(ds) # Mandatory read
time.sleep(0.5)
set_activity(ds, ppid, "Idling", "Monochrome")
# 4. Minimal WebSocket Client
ws = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ws.settimeout(1.0)
try:
ws.connect(('127.0.0.1', int(config['nlPort'])))
except: return
key = base64.b64encode(os.urandom(16)).decode()
handshake = (
f"GET /?extensionId={config['nlExtensionId']}&connectToken={config['nlConnectToken']} HTTP/1.1\r\n"
f"Host: 127.0.0.1:{config['nlPort']}\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n"
)
ws.sendall(handshake.encode())
# Skip HTTP response header
resp = b""
while b"\r\n\r\n" not in resp:
try:
chunk = ws.recv(1024)
if not chunk: break
resp += chunk
except socket.timeout: continue
# 5. Loop
while True:
# Watchdog
try:
os.kill(ppid, 0)
except OSError: break
try:
head = ws.recv(2)
if not head: break
length = head[1] & 127
if length == 126: length = struct.unpack(">H", ws.recv(2))[0]
elif length == 127: length = struct.unpack(">Q", ws.recv(8))[0]
data = b""
while len(data) < length:
data += ws.recv(length - len(data))
msg = json.loads(data.decode('utf-8'))
if msg['event'] == 'discord:update':
d = msg['data']
set_activity(ds, ppid, d.get('details'), d.get('state'), d.get('largeImageKey'))
elif msg['event'] == 'discord:clear':
set_activity(ds, ppid, "Idling", "Monochrome")
elif msg['event'] == 'windowClose':
break
except socket.timeout: continue
except: continue
# Cleanup
try:
send_packet(ds, 1, {
"cmd": "SET_ACTIVITY",
"args": {"pid": ppid, "activity": None},
"nonce": str(uuid.uuid4())
})
time.sleep(0.1)
ds.close()
except: pass
if __name__ == "__main__":
main()

View file

@ -868,7 +868,6 @@ export class LosslessAPI {
const seenTrackIds = new Set(tracks.map((t) => t.id));
const artistsToProcess = artists.slice(0, Math.min(5, artists.length));
console.log(`Processing ${artistsToProcess.length} artists for recommendations`);
const artistPromises = artistsToProcess.map(async (artist) => {
try {
@ -876,8 +875,6 @@ export class LosslessAPI {
const artistData = await this.getArtist(artist.id, { lightweight: true });
if (artistData && artistData.tracks && artistData.tracks.length > 0) {
const newTracks = artistData.tracks.filter((track) => !seenTrackIds.has(track.id)).slice(0, 4);
console.log(`Found ${newTracks.length} new tracks from ${artist.name}`);
return newTracks;
} else {
console.warn(`No tracks found for artist ${artist.name}`);
@ -897,8 +894,6 @@ export class LosslessAPI {
}
});
console.log(`Total recommended tracks found: ${recommendedTracks.length}`);
const shuffled = recommendedTracks.sort(() => 0.5 - Math.random());
return shuffled.slice(0, limit);
}

View file

@ -13,8 +13,15 @@ import { sidePanelManager } from './side-panel.js';
import { db } from './db.js';
import { syncManager } from './accounts/pocketbase.js';
import { registerSW } from 'virtual:pwa-register';
import { initializeDiscordRPC } from './discord-rpc.js';
import * as Neutralino from '@neutralinojs/lib';
import './smooth-scrolling.js';
// Assign Neutralino to window for global access
if (typeof window !== 'undefined') {
window.Neutralino = Neutralino;
}
// Lazy-loaded modules
let settingsModule = null;
let downloadsModule = null;
@ -380,18 +387,20 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize desktop environment (Neutralino)
if (window.Neutralino) {
await (async () => {
console.log('Initializing Neutralino desktop environment (Lite Mode)...');
try {
await Neutralino.init();
Neutralino.events.on('windowClose', () => {
Neutralino.app.exit();
});
console.log('Desktop environment initialized');
} catch (error) {
console.error('Failed to initialize desktop environment:', error);
}
})();
console.log('Initializing Neutralino desktop environment (Lite Mode)...');
try {
Neutralino.init();
// Register events immediately
Neutralino.events.on('windowClose', () => {
Neutralino.app.exit();
});
// Start RPC immediately after init
initializeDiscordRPC(player);
} catch (error) {
console.error('Failed to initialize desktop environment:', error);
}
}
const castBtn = document.getElementById('cast-btn');

View file

@ -384,8 +384,6 @@ export class MusicDatabase {
});
}
console.log(`${storeName}: Adding item with ID ${item.id || item.uuid || item.timestamp}`);
// Critical: Ensure key exists for IndexedDB store.put()
const keyPath = store.keyPath;
if (keyPath && !item[keyPath]) {

87
js/discord-rpc.js Normal file
View file

@ -0,0 +1,87 @@
import { getTrackTitle, getTrackArtists } from './utils.js';
export function initializeDiscordRPC(player) {
console.log('[DiscordRPC] Initializing...');
const EXTENSION_ID = 'js.neutralino.discordrpc';
function sendUpdate(track, isPaused = false) {
if (!track) return;
let coverUrl = 'monochrome';
if (track.album?.cover) {
const coverId = track.album.cover.replace(/-/g, '/');
coverUrl = `https://resources.tidal.com/images/${coverId}/320x320.jpg`;
}
const data = {
details: getTrackTitle(track),
state: getTrackArtists(track),
largeImageKey: coverUrl,
largeImageText: track.album?.title || 'Monochrome',
smallImageKey: isPaused ? 'pause' : 'play',
smallImageText: isPaused ? 'Paused' : 'Playing',
instance: false,
};
if (!isPaused && track.duration) {
const now = Date.now();
const elapsed = player.audio.currentTime * 1000;
data.startTimestamp = Math.floor((now - elapsed) / 1000);
}
console.log('[DiscordRPC] Dispatching to', EXTENSION_ID, data);
Neutralino.events.broadcast('discord:update', data).catch(e => console.error('Broadcast failed', e));
Neutralino.extensions.dispatch(EXTENSION_ID, 'discord:update', data).catch(e => console.error('Dispatch failed', e));
}
// Heartbeat & Debug Ping
setInterval(() => {
if (player.currentTrack) {
sendUpdate(player.currentTrack, player.audio.paused);
} else {
const idlingData = {
details: 'Idling',
state: 'Monochrome',
largeImageKey: 'monochrome',
largeImageText: 'Monochrome',
smallImageKey: 'pause',
smallImageText: 'Paused'
};
Neutralino.events.broadcast('discord:update', idlingData).catch(() => { });
Neutralino.extensions.dispatch(EXTENSION_ID, 'discord:update', idlingData).catch(() => { });
}
}, 5000);
function sendClear() {
Neutralino.events.broadcast('discord:clear', {}).catch(() => { });
}
player.audio.addEventListener('play', () => {
sendUpdate(player.currentTrack);
});
player.audio.addEventListener('pause', () => {
sendUpdate(player.currentTrack, true);
});
player.audio.addEventListener('loadedmetadata', () => {
if (!player.audio.paused) {
sendUpdate(player.currentTrack);
}
});
// Send initial status
if (player.currentTrack) {
sendUpdate(player.currentTrack, player.audio.paused);
} else {
Neutralino.events.broadcast('discord:update', {
details: 'Idling',
state: 'Monochrome',
largeImageKey: 'monochrome',
largeImageText: 'Monochrome',
smallImageKey: 'pause',
smallImageText: 'Paused'
}).catch(() => { });
}
}

View file

@ -402,6 +402,7 @@ export class Player {
this.updatePlayingTrackIndicator();
this.updateMediaSession(track);
this.updateMediaSessionPlaybackState();
this.updateNativeWindow(track);
try {
let streamUrl;
@ -1038,4 +1039,16 @@ export class Player {
updateBtn(timerBtn);
updateBtn(timerBtnDesktop);
}
async updateNativeWindow(track) {
if (!window.Neutralino) return;
const trackTitle = getTrackTitle(track);
const artist = getTrackArtists(track);
try {
await Neutralino.window.setTitle(`${trackTitle}${artist}`);
} catch (e) {
console.error('Failed to set window title:', e);
}
}
}

View file

@ -6,10 +6,11 @@
"description": "Lossless music streaming",
"version": "1.0.0",
"defaultMode": "window",
"documentRoot": "public/",
"documentRoot": "www/",
"url": "https://monochrome.tf",
"enableServer": false,
"enableServer": true,
"enableNativeAPI": true,
"enableExtensions": true,
"tokenSecurity": "one-time",
"modes": {
"window": {
@ -28,14 +29,25 @@
"exitProcessOnClose": true
}
},
"port": 5050,
"cli": {
"binaryName": "Monochrome",
"resourcesPath": "public/",
"resourcesPath": "www/",
"binaryVersion": "6.5.0",
"clientVersion": "6.5.0"
},
"extensions": [
{
"id": "js.neutralino.discordrpc",
"commandLinux": "python3 \"${NL_PATH}/extensions/js.neutralino.discordrpc/bridge.py\"",
"commandMac": "python3 \"${NL_PATH}/extensions/js.neutralino.discordrpc/bridge.py\"",
"commandWindows": "powershell.exe -ExecutionPolicy Bypass -File \"${NL_PATH}/extensions/js.neutralino.discordrpc/bridge.ps1\""
}
],
"nativeAllowList": [
"app.exit",
"window.*"
"window.*",
"extensions.*",
"events.*"
]
}

489
package-lock.json generated
View file

@ -19,7 +19,6 @@
},
"devDependencies": {
"@neutralinojs/neu": "^11.7.0",
"concurrently": "^9.2.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.0.0",
@ -29,8 +28,8 @@
"stylelint-config-standard": "^39.0.1",
"stylelint-config-standard-scss": "^16.0.0",
"vite": "^7.3.0",
"vite-plugin-pwa": "^1.2.0",
"wait-on": "^9.0.3"
"vite-plugin-neutralino": "^1.0.3",
"vite-plugin-pwa": "^1.2.0"
}
},
"node_modules/@apideck/better-ajv-errors": {
@ -2488,60 +2487,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz",
"integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -3200,13 +3145,6 @@
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@ -3530,13 +3468,6 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -3563,18 +3494,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
@ -3758,6 +3677,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/butterchurn": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/butterchurn/-/butterchurn-2.6.7.tgz",
@ -3933,100 +3866,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/codem-isoboxer": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz",
@ -4060,19 +3899,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -4097,47 +3923,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.3",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/configstore": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
@ -4567,6 +4352,20 @@
"integrity": "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==",
"license": "MIT"
},
"node_modules/edit-json-file": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/edit-json-file/-/edit-json-file-1.8.1.tgz",
"integrity": "sha512-x8L381+GwqxQejPipwrUZIyAg5gDQ9tLVwiETOspgXiaQztLsrOm7luBW5+Pe31aNezuzDY79YyzF+7viCRPXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"find-value": "^1.0.12",
"iterate-object": "^1.3.4",
"r-json": "^1.2.10",
"set-value": "^4.1.0",
"w-json": "^1.3.10"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -5394,23 +5193,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@ -5510,16 +5292,6 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -7072,29 +6844,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
@ -7111,16 +6860,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -7281,6 +7020,16 @@
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -7721,13 +7470,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -7958,16 +7700,6 @@
"regjsparser": "bin/parser"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -8121,16 +7853,6 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@ -8336,19 +8058,6 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -9290,6 +8999,13 @@
"node": ">=0.6.x"
}
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"dev": true,
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -9675,6 +9391,13 @@
}
}
},
"node_modules/vite-plugin-neutralino": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/vite-plugin-neutralino/-/vite-plugin-neutralino-1.0.3.tgz",
"integrity": "sha512-E/PSTCp7m7efk7fa4eE12WQ+5XGNP72gkhOv61X4i0n+NcnrwKtJgEgeCDGUXeLp4ngTfU/CP+EF55jmZs/0Jw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite-plugin-pwa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
@ -9713,26 +9436,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/wait-on": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz",
"integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"axios": "^1.13.2",
"joi": "^18.0.1",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/websocket": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
@ -10349,16 +10052,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
@ -10377,80 +10070,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yauzl": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz",

View file

@ -6,7 +6,7 @@
"main": "sw.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vite build && npx neu build",
"preview": "vite preview",
"start": "vite preview",
"lint:js": "eslint .",
@ -38,6 +38,7 @@
"stylelint-config-standard": "^39.0.1",
"stylelint-config-standard-scss": "^16.0.0",
"vite": "^7.3.0",
"vite-plugin-neutralino": "^1.0.3",
"vite-plugin-pwa": "^1.2.0"
},
"overrides": {

View file

@ -1,14 +1,16 @@
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import neutralino from 'vite-plugin-neutralino';
import authGatePlugin from './vite-plugin-auth-gate.js';
export default defineConfig({
base: './',
build: {
outDir: 'dist',
outDir: 'www',
emptyOutDir: true,
},
plugins: [
neutralino(),
authGatePlugin(),
VitePWA({
registerType: 'prompt',