feat(overlay): in-game voice overlay window, Windows (#25)
While you're in a call and Lotus isn't the focused window, a small always-on-top, click-through window lists everyone in the call, lights up whoever is speaking, and shows your own mute / deafen / PTT state. - A separate window, nothing injected into the game: no anti-cheat surface. Never takes focus (focusable(false), WS_EX_NOACTIVATE, SW_SHOWNOACTIVATE) and ignores the mouse. - Its page is three static files served by the binary over the `lotusoverlay` custom protocol, with a strict CSP (no network) and no IPC (no capability names the window). Rust pushes state in as a JSON literal; the page renders with textContent only. - New `set_overlay_state` command (main window only) takes the call snapshot from cinny's `useTauriOverlay`. Sanitised: bounded list and names, known corner. Placed in the chosen corner of the foreground window's monitor, DPI-scaled. - Windows only; a no-op elsewhere (Linux has no in-app calls yet, #21). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<!-- Lotus Chat in-game voice overlay (cinny-desktop #25). Served by the desktop
|
||||
binary over the `lotusoverlay` custom protocol; no IPC, no network. State
|
||||
arrives via window.lotusOverlay(...) called from Rust (native/overlay.rs). -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Lotus Chat overlay</title>
|
||||
<link rel="stylesheet" href="/overlay.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="panel" class="panel" hidden>
|
||||
<div id="self" class="self"></div>
|
||||
<ul id="people" class="people"></ul>
|
||||
</div>
|
||||
<script src="/overlay.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Lotus Chat in-game voice overlay (cinny-desktop #25).
|
||||
Sizes match native/overlay.rs: width 240, padding 8, header 30, rows 30. */
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
font: 13px/1.2 "Segoe UI", system-ui, sans-serif;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-sizing: border-box;
|
||||
width: 240px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.self {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 15px;
|
||||
background: rgba(18, 18, 24, 0.78);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 1px 7px;
|
||||
border-radius: 9px;
|
||||
font-size: 11px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.badge.off {
|
||||
background: rgba(220, 60, 60, 0.85);
|
||||
}
|
||||
|
||||
.people {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.person {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 30px;
|
||||
padding: 3px 8px 3px 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.person.speaking .avatar {
|
||||
box-shadow: 0 0 0 2px #3ddc84;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(18, 18, 24, 0.7);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.person.speaking .name {
|
||||
color: #fff;
|
||||
background: rgba(18, 18, 24, 0.85);
|
||||
}
|
||||
|
||||
.person.muted .name {
|
||||
color: rgba(242, 242, 242, 0.6);
|
||||
}
|
||||
|
||||
.mic {
|
||||
flex: none;
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(220, 60, 60, 0.85);
|
||||
}
|
||||
|
||||
.more {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding-left: 35px;
|
||||
color: rgba(242, 242, 242, 0.75);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Lotus Chat in-game voice overlay (cinny-desktop #25).
|
||||
//
|
||||
// Rust (native/overlay.rs) calls window.lotusOverlay(state, maxRows) with the
|
||||
// call snapshot from the web app. Everything is rendered with textContent /
|
||||
// createElement, never markup strings, so a display name can't inject HTML.
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
var panel = document.getElementById('panel');
|
||||
var selfRow = document.getElementById('self');
|
||||
var list = document.getElementById('people');
|
||||
|
||||
// Stable colour per user, from the user ID.
|
||||
var COLOURS = ['#7c5cff', '#2f9e8f', '#d9822b', '#c2417c', '#3d7be0', '#5aa02c', '#a35bd6', '#c94f4f'];
|
||||
function colourFor(id) {
|
||||
var h = 0;
|
||||
for (var i = 0; i < id.length; i += 1) h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
||||
return COLOURS[h % COLOURS.length];
|
||||
}
|
||||
|
||||
function initial(name) {
|
||||
var trimmed = (name || '').replace(/^[@#!+]/, '').trim();
|
||||
var first = Array.from(trimmed)[0];
|
||||
return first ? first.toUpperCase() : '?';
|
||||
}
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (cls) node.className = cls;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function badge(text, off) {
|
||||
return el('span', off ? 'badge off' : 'badge', text);
|
||||
}
|
||||
|
||||
function renderSelf(state) {
|
||||
selfRow.replaceChildren();
|
||||
selfRow.appendChild(el('span', '', 'Lotus voice'));
|
||||
if (state.deafened) selfRow.appendChild(badge('Deafened', true));
|
||||
else if (state.muted) selfRow.appendChild(badge('Muted', true));
|
||||
if (state.ptt) selfRow.appendChild(badge('PTT', false));
|
||||
}
|
||||
|
||||
function renderPerson(p) {
|
||||
var row = el('li', 'person');
|
||||
if (p.speaking) row.classList.add('speaking');
|
||||
if (p.muted) row.classList.add('muted');
|
||||
var avatar = el('span', 'avatar', initial(p.name));
|
||||
avatar.style.background = colourFor(p.userId || p.name || '');
|
||||
row.appendChild(avatar);
|
||||
row.appendChild(el('span', 'name', p.isSelf ? p.name + ' (you)' : p.name));
|
||||
if (p.muted) row.appendChild(el('span', 'mic', 'Muted'));
|
||||
return row;
|
||||
}
|
||||
|
||||
window.lotusOverlay = function (state, maxRows) {
|
||||
if (!state || !state.active) {
|
||||
panel.hidden = true;
|
||||
return;
|
||||
}
|
||||
renderSelf(state);
|
||||
list.replaceChildren();
|
||||
var people = Array.isArray(state.people) ? state.people : [];
|
||||
var limit = typeof maxRows === 'number' ? maxRows : 10;
|
||||
people.slice(0, limit).forEach(function (p) {
|
||||
list.appendChild(renderPerson(p));
|
||||
});
|
||||
if (people.length > limit) {
|
||||
list.appendChild(el('li', 'more', '+' + (people.length - limit) + ' more'));
|
||||
}
|
||||
panel.hidden = false;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user