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
|
||||
# $Log = Join-Path $PSScriptRoot "bridge_final.log"
|
||||
function Log($m) { }
|
||||
# bridge.ps1 - Diagnostic Version
|
||||
$Log = Join-Path $PSScriptRoot "bridge.log"
|
||||
function Log($m) { Add-Content $Log "$(Get-Date -f 'HH:mm:ss') - $m" }
|
||||
|
||||
Log "--- START (DEPTH FIX) ---"
|
||||
Log "--- START (DIAGNOSTIC) ---"
|
||||
|
||||
# 1. PID
|
||||
$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"
|
||||
}
|
||||
|
||||
function Set-Activity($d, $s, $img) {
|
||||
function Set-Activity($d, $s, $img, $start, $end, $large_text, $small_img, $small_txt) {
|
||||
$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"
|
||||
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
|
||||
$payload = @{
|
||||
|
|
@ -59,7 +70,7 @@ function Set-Activity($d, $s, $img) {
|
|||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
Set-Activity "Idling" "Monochrome" $null
|
||||
Set-Activity "Idling" "Monochrome" $null $null $null $null $null $null
|
||||
|
||||
# 4. Config & WS
|
||||
$line = [Console]::In.ReadLine()
|
||||
|
|
@ -83,9 +94,9 @@ while ($ws.State -eq "Open") {
|
|||
$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
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ def recv_packet(s):
|
|||
return json.loads(payload.decode('utf-8'))
|
||||
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
|
||||
current = f"{details}-{state}-{img}"
|
||||
current = f"{details}-{state}-{img}-{start}-{end}-{large_text}-{small_img}-{small_txt}"
|
||||
if current == LAST_STATUS: return
|
||||
LAST_STATUS = current
|
||||
|
||||
|
|
@ -36,9 +36,18 @@ def set_activity(ds, pid, details, state, img=None):
|
|||
"type": 2, # Listening
|
||||
"assets": {
|
||||
"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, {
|
||||
"cmd": "SET_ACTIVITY",
|
||||
|
|
@ -119,7 +128,7 @@ def main():
|
|||
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'))
|
||||
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':
|
||||
set_activity(ds, ppid, "Idling", "Monochrome")
|
||||
elif msg['event'] == 'windowClose':
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//js/app.js
|
||||
console.log('[App] Script loaded');
|
||||
import { LosslessAPI } from './api.js';
|
||||
import {
|
||||
apiSettings,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
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) {
|
||||
|
|
@ -27,10 +25,12 @@ export function initializeDiscordRPC(player) {
|
|||
if (!isPaused && track.duration) {
|
||||
const now = Date.now();
|
||||
const elapsed = player.audio.currentTime * 1000;
|
||||
const remaining = (track.duration - player.audio.currentTime) * 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.extensions
|
||||
.dispatch(EXTENSION_ID, 'discord:update', data)
|
||||
|
|
@ -42,7 +42,6 @@ export function initializeDiscordRPC(player) {
|
|||
if (player.currentTrack) {
|
||||
sendUpdate(player.currentTrack, player.audio.paused);
|
||||
} else {
|
||||
console.log('[DiscordRPC] Sending idling heartbeat...');
|
||||
const idlingData = {
|
||||
details: 'Idling',
|
||||
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