WIP: neutralino
# Conflicts: # js/app.js # js/neutralino-bridge.js # public/neutralino_loader.html
This commit is contained in:
parent
f90a85aef2
commit
89548fa0d3
6 changed files with 262 additions and 18 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
# bridge.ps1 - JSON Depth Fix
|
# bridge.ps1 - Diagnostic Version
|
||||||
# $Log = Join-Path $PSScriptRoot "bridge_final.log"
|
$Log = Join-Path $PSScriptRoot "bridge.log"
|
||||||
function Log($m) { }
|
function Log($m) { Add-Content $Log "$(Get-Date -f 'HH:mm:ss') - $m" }
|
||||||
|
|
||||||
Log "--- START (DEPTH FIX) ---"
|
Log "--- START (DIAGNOSTIC) ---"
|
||||||
|
|
||||||
# 1. PID
|
# 1. PID
|
||||||
$p = Get-Process Monochrome -ErrorAction SilentlyContinue | Select-Object -First 1
|
$p = Get-Process Monochrome -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
|
@ -37,16 +37,27 @@ $h = New-Object byte[] 8; if ($pipe.Read($h, 0, 8) -eq 8) {
|
||||||
Log "Handshake OK"
|
Log "Handshake OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Set-Activity($d, $s, $img) {
|
function Set-Activity($d, $s, $img, $start, $end, $large_text, $small_img, $small_txt) {
|
||||||
$activity = @{
|
$activity = @{
|
||||||
details = [string]$d
|
details = [string]$d
|
||||||
state = [string]$s
|
state = [string]$s
|
||||||
type = 2
|
type = 2
|
||||||
assets = @{
|
assets = @{
|
||||||
large_image = if ($img -and $img.StartsWith("http")) { [string]$img } else { "monochrome" }
|
large_image = if ($img -and $img.StartsWith("http")) { [string]$img } else { "monochrome" }
|
||||||
large_text = "Monochrome"
|
large_text = if ($large_text) { [string]$large_text } else { "Monochrome" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($small_img) {
|
||||||
|
$activity.assets.small_image = [string]$small_img
|
||||||
|
$activity.assets.small_text = [string]$small_txt
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($start -or $end) {
|
||||||
|
$activity.timestamps = @{}
|
||||||
|
if ($start) { $activity.timestamps.start = [long]$start }
|
||||||
|
if ($end) { $activity.timestamps.end = [long]$end }
|
||||||
|
}
|
||||||
|
|
||||||
# CRITICAL: -Depth 10 ensures 'assets' is not stringified as a class name
|
# CRITICAL: -Depth 10 ensures 'assets' is not stringified as a class name
|
||||||
$payload = @{
|
$payload = @{
|
||||||
|
|
@ -59,7 +70,7 @@ function Set-Activity($d, $s, $img) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Sleep -Seconds 1
|
Start-Sleep -Seconds 1
|
||||||
Set-Activity "Idling" "Monochrome" $null
|
Set-Activity "Idling" "Monochrome" $null $null $null $null $null $null
|
||||||
|
|
||||||
# 4. Config & WS
|
# 4. Config & WS
|
||||||
$line = [Console]::In.ReadLine()
|
$line = [Console]::In.ReadLine()
|
||||||
|
|
@ -83,9 +94,9 @@ while ($ws.State -eq "Open") {
|
||||||
$raw = [System.Text.Encoding]::UTF8.GetString($buf, 0, $task.Result.Count)
|
$raw = [System.Text.Encoding]::UTF8.GetString($buf, 0, $task.Result.Count)
|
||||||
$msg = $raw | ConvertFrom-Json
|
$msg = $raw | ConvertFrom-Json
|
||||||
if ($msg.event -eq "discord:update") {
|
if ($msg.event -eq "discord:update") {
|
||||||
Set-Activity $msg.data.details $msg.data.state $msg.data.largeImageKey
|
Set-Activity $msg.data.details $msg.data.state $msg.data.largeImageKey $msg.data.startTimestamp $msg.data.endTimestamp $msg.data.largeImageText $msg.data.smallImageKey $msg.data.smallImageText
|
||||||
}
|
}
|
||||||
elseif ($msg.event -eq "discord:clear") { Set-Activity "Idling" "Monochrome" $null }
|
elseif ($msg.event -eq "discord:clear") { Set-Activity "Idling" "Monochrome" $null $null $null $null $null $null }
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ def recv_packet(s):
|
||||||
return json.loads(payload.decode('utf-8'))
|
return json.loads(payload.decode('utf-8'))
|
||||||
except: return None
|
except: return None
|
||||||
|
|
||||||
def set_activity(ds, pid, details, state, img=None):
|
def set_activity(ds, pid, details, state, img=None, start=None, end=None, large_text=None, small_img=None, small_txt=None):
|
||||||
global LAST_STATUS
|
global LAST_STATUS
|
||||||
current = f"{details}-{state}-{img}"
|
current = f"{details}-{state}-{img}-{start}-{end}-{large_text}-{small_img}-{small_txt}"
|
||||||
if current == LAST_STATUS: return
|
if current == LAST_STATUS: return
|
||||||
LAST_STATUS = current
|
LAST_STATUS = current
|
||||||
|
|
||||||
|
|
@ -36,9 +36,18 @@ def set_activity(ds, pid, details, state, img=None):
|
||||||
"type": 2, # Listening
|
"type": 2, # Listening
|
||||||
"assets": {
|
"assets": {
|
||||||
"large_image": img if img and img.startswith('http') else "monochrome",
|
"large_image": img if img and img.startswith('http') else "monochrome",
|
||||||
"large_text": "Monochrome"
|
"large_text": str(large_text or "Monochrome")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if small_img:
|
||||||
|
activity["assets"]["small_image"] = str(small_img)
|
||||||
|
activity["assets"]["small_text"] = str(small_txt or "")
|
||||||
|
|
||||||
|
if start or end:
|
||||||
|
activity["timestamps"] = {}
|
||||||
|
if start: activity["timestamps"]["start"] = int(start)
|
||||||
|
if end: activity["timestamps"]["end"] = int(end)
|
||||||
|
|
||||||
send_packet(ds, 1, {
|
send_packet(ds, 1, {
|
||||||
"cmd": "SET_ACTIVITY",
|
"cmd": "SET_ACTIVITY",
|
||||||
|
|
@ -119,7 +128,7 @@ def main():
|
||||||
msg = json.loads(data.decode('utf-8'))
|
msg = json.loads(data.decode('utf-8'))
|
||||||
if msg['event'] == 'discord:update':
|
if msg['event'] == 'discord:update':
|
||||||
d = msg['data']
|
d = msg['data']
|
||||||
set_activity(ds, ppid, d.get('details'), d.get('state'), d.get('largeImageKey'))
|
set_activity(ds, ppid, d.get('details'), d.get('state'), d.get('largeImageKey'), d.get('startTimestamp'), d.get('endTimestamp'), d.get('largeImageText'), d.get('smallImageKey'), d.get('smallImageText'))
|
||||||
elif msg['event'] == 'discord:clear':
|
elif msg['event'] == 'discord:clear':
|
||||||
set_activity(ds, ppid, "Idling", "Monochrome")
|
set_activity(ds, ppid, "Idling", "Monochrome")
|
||||||
elif msg['event'] == 'windowClose':
|
elif msg['event'] == 'windowClose':
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//js/app.js
|
//js/app.js
|
||||||
console.log('[App] Script loaded');
|
|
||||||
import { LosslessAPI } from './api.js';
|
import { LosslessAPI } from './api.js';
|
||||||
import {
|
import {
|
||||||
apiSettings,
|
apiSettings,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { getTrackTitle, getTrackArtists } from './utils.js';
|
import { getTrackTitle, getTrackArtists } from './utils.js';
|
||||||
|
|
||||||
export function initializeDiscordRPC(player) {
|
export function initializeDiscordRPC(player) {
|
||||||
console.log('[DiscordRPC] Initializing...');
|
|
||||||
|
|
||||||
const EXTENSION_ID = 'js.neutralino.discordrpc';
|
const EXTENSION_ID = 'js.neutralino.discordrpc';
|
||||||
|
|
||||||
function sendUpdate(track, isPaused = false) {
|
function sendUpdate(track, isPaused = false) {
|
||||||
|
|
@ -27,10 +25,12 @@ export function initializeDiscordRPC(player) {
|
||||||
if (!isPaused && track.duration) {
|
if (!isPaused && track.duration) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const elapsed = player.audio.currentTime * 1000;
|
const elapsed = player.audio.currentTime * 1000;
|
||||||
|
const remaining = (track.duration - player.audio.currentTime) * 1000;
|
||||||
|
|
||||||
data.startTimestamp = Math.floor((now - elapsed) / 1000);
|
data.startTimestamp = Math.floor((now - elapsed) / 1000);
|
||||||
|
data.endTimestamp = Math.floor((now + remaining) / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DiscordRPC] Dispatching to', EXTENSION_ID, data);
|
|
||||||
Neutralino.events.broadcast('discord:update', data).catch((e) => console.error('Broadcast failed', e));
|
Neutralino.events.broadcast('discord:update', data).catch((e) => console.error('Broadcast failed', e));
|
||||||
Neutralino.extensions
|
Neutralino.extensions
|
||||||
.dispatch(EXTENSION_ID, 'discord:update', data)
|
.dispatch(EXTENSION_ID, 'discord:update', data)
|
||||||
|
|
@ -42,7 +42,6 @@ export function initializeDiscordRPC(player) {
|
||||||
if (player.currentTrack) {
|
if (player.currentTrack) {
|
||||||
sendUpdate(player.currentTrack, player.audio.paused);
|
sendUpdate(player.currentTrack, player.audio.paused);
|
||||||
} else {
|
} else {
|
||||||
console.log('[DiscordRPC] Sending idling heartbeat...');
|
|
||||||
const idlingData = {
|
const idlingData = {
|
||||||
details: 'Idling',
|
details: 'Idling',
|
||||||
state: 'Monochrome',
|
state: 'Monochrome',
|
||||||
|
|
|
||||||
79
js/neutralino-bridge.js
Normal file
79
js/neutralino-bridge.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// js/neutralino-bridge.js
|
||||||
|
|
||||||
|
const listeners = new Map();
|
||||||
|
|
||||||
|
// Listen for events from the Shell (Parent)
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data?.type === 'NL_EVENT') {
|
||||||
|
const { eventName, detail } = event.data;
|
||||||
|
if (listeners.has(eventName)) {
|
||||||
|
listeners.get(eventName).forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler(detail);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Bridge] Error in event handler:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const init = async () => {
|
||||||
|
// Notify Shell we are ready
|
||||||
|
window.parent.postMessage({ type: 'NL_INIT' }, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const events = {
|
||||||
|
on: (eventName, handler) => {
|
||||||
|
if (!listeners.has(eventName)) {
|
||||||
|
listeners.set(eventName, []);
|
||||||
|
}
|
||||||
|
listeners.get(eventName).push(handler);
|
||||||
|
},
|
||||||
|
off: (eventName, handler) => {
|
||||||
|
if (!listeners.has(eventName)) return;
|
||||||
|
const handlers = listeners.get(eventName);
|
||||||
|
const index = handlers.indexOf(handler);
|
||||||
|
if (index > -1) handlers.splice(index, 1);
|
||||||
|
},
|
||||||
|
broadcast: async (eventName, data) => {
|
||||||
|
window.parent.postMessage({ type: 'NL_BROADCAST', eventName, data }, '*');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extensions = {
|
||||||
|
dispatch: async (extensionId, eventName, data) => {
|
||||||
|
window.parent.postMessage({ type: 'NL_EXTENSION', extensionId, eventName, data }, '*');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const app = {
|
||||||
|
exit: async () => {
|
||||||
|
window.parent.postMessage({ type: 'NL_APP_EXIT' }, '*');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const _window = {
|
||||||
|
minimize: async () => {
|
||||||
|
window.parent.postMessage({ type: 'NL_WINDOW_MIN' }, '*');
|
||||||
|
},
|
||||||
|
maximize: async () => {
|
||||||
|
window.parent.postMessage({ type: 'NL_WINDOW_MAX' }, '*');
|
||||||
|
},
|
||||||
|
isVisible: async () => {
|
||||||
|
return true; // Mock response
|
||||||
|
},
|
||||||
|
setTitle: async (title) => {
|
||||||
|
window.parent.postMessage({ type: 'NL_WINDOW_SET_TITLE', title }, '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose generically for other modules
|
||||||
|
export { _window as window };
|
||||||
|
export default {
|
||||||
|
init,
|
||||||
|
events,
|
||||||
|
extensions,
|
||||||
|
app,
|
||||||
|
window: _window
|
||||||
|
};
|
||||||
147
public/neutralino_loader.html
Normal file
147
public/neutralino_loader.html
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Monochrome Shell</title>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #000;
|
||||||
|
/* Seamless blend */
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="/__neutralino_globals.js"></script>
|
||||||
|
<script src="/neutralino.js"></script>
|
||||||
|
|
||||||
|
<!-- Load the app from the local Neutralino server -->
|
||||||
|
<iframe id="app-frame" allow="autoplay; fullscreen; microphone; clipboard-read; clipboard-write"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// initialize Neutralino in the Shell (Local Context)
|
||||||
|
try {
|
||||||
|
Neutralino.init();
|
||||||
|
console.log('[Shell] Neutralino initialized.');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Shell] Failed to init Neutralino:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point iframe to local server using the port from Neutralino
|
||||||
|
// NL_PORT is available globally after init (or we can parse it/wait for it)
|
||||||
|
// Neutralino.init() usually populates window.NL_PORT or we read it from sessionStorage
|
||||||
|
|
||||||
|
const initFrame = async () => {
|
||||||
|
// Wait a tick for globals
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
|
||||||
|
let port = window.NL_PORT || sessionStorage.getItem('NL_PORT');
|
||||||
|
// Fallback if missing (shouldn't happen after init)
|
||||||
|
if (!port) {
|
||||||
|
// Try reading from window.location if passed (it isn't in shell mode usually)
|
||||||
|
// But Neutralino fills globals.
|
||||||
|
// If not, default to 5050
|
||||||
|
port = '5050';
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.getElementById('app-frame');
|
||||||
|
// Load the local index.html
|
||||||
|
iframe.src = `http://localhost:${port}/?mode=neutralino`;
|
||||||
|
console.log(`[Shell] Loading local app from http://localhost:${port}/?mode=neutralino`);
|
||||||
|
};
|
||||||
|
|
||||||
|
initFrame();
|
||||||
|
|
||||||
|
const iframe = document.getElementById('app-frame');
|
||||||
|
|
||||||
|
// Forward generic Neutralino events to the Iframe
|
||||||
|
const forwardEvent = (eventName, detail) => {
|
||||||
|
if (iframe && iframe.contentWindow) {
|
||||||
|
iframe.contentWindow.postMessage({
|
||||||
|
type: 'NL_EVENT',
|
||||||
|
eventName: eventName,
|
||||||
|
detail: detail
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for specific events to forward
|
||||||
|
// Add more here if the app needs them (e.g., tray events)
|
||||||
|
Neutralino.events.on('windowClose', () => forwardEvent('windowClose'));
|
||||||
|
Neutralino.events.on('windowFocus', () => forwardEvent('windowFocus'));
|
||||||
|
Neutralino.events.on('windowBlur', () => forwardEvent('windowBlur'));
|
||||||
|
|
||||||
|
// Handle commands from the Iframe (via Bridge)
|
||||||
|
window.addEventListener('message', async (event) => {
|
||||||
|
const { type, eventName, data, extensionId } = event.data;
|
||||||
|
|
||||||
|
// Security: In a real scenario, check event.origin if possible.
|
||||||
|
// But since this loads valid HTTPS content, it's generally safe for this context.
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'NL_INIT':
|
||||||
|
console.log('[Shell] Bridge connected.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_BROADCAST':
|
||||||
|
// e.g. Discord RPC updates
|
||||||
|
try {
|
||||||
|
// console.log('[Shell] Broadcasting:', eventName, data);
|
||||||
|
await Neutralino.events.broadcast(eventName, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Shell] Broadcast failed:', e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_EXTENSION':
|
||||||
|
// e.g. specific extension dispatch
|
||||||
|
try {
|
||||||
|
// console.log('[Shell] Dispatching to extension:', extensionId, eventName);
|
||||||
|
await Neutralino.extensions.dispatch(extensionId, eventName, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Shell] Extension dispatch failed:', e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_APP_EXIT':
|
||||||
|
Neutralino.app.exit();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_WINDOW_MIN':
|
||||||
|
Neutralino.window.minimize();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_WINDOW_MAX':
|
||||||
|
try {
|
||||||
|
const isMax = await Neutralino.window.isMaximized();
|
||||||
|
if (isMax) Neutralino.window.unmaximize();
|
||||||
|
else Neutralino.window.maximize();
|
||||||
|
} catch (e) { console.error('[Shell] Window toggle failed:', e); }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NL_WINDOW_SET_TITLE':
|
||||||
|
try {
|
||||||
|
await Neutralino.window.setTitle(event.data.title);
|
||||||
|
} catch (e) { console.error('[Shell] Set title failed:', e); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue