Compare commits
2 commits
3acd082fb0
...
6dbd5e77c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dbd5e77c4 | |||
| afbaa8f7d1 |
2 changed files with 103 additions and 29 deletions
20
main.js
20
main.js
|
|
@ -1,6 +1,7 @@
|
||||||
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
const updateState = {
|
const updateState = {
|
||||||
status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error
|
status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error
|
||||||
|
|
@ -24,6 +25,22 @@ function isNewerVersionAvailable(info) {
|
||||||
|
|
||||||
let updateCheckInProgress = false;
|
let updateCheckInProgress = false;
|
||||||
|
|
||||||
|
function resolveBackendUrl() {
|
||||||
|
const configuredUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || '').trim();
|
||||||
|
if (configuredUrl) {
|
||||||
|
return configuredUrl.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packagedFallback = typeof packageJson.homepage === 'string'
|
||||||
|
? packageJson.homepage.trim()
|
||||||
|
: '';
|
||||||
|
if (app.isPackaged && packagedFallback) {
|
||||||
|
return packagedFallback.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'http://localhost:3000';
|
||||||
|
}
|
||||||
|
|
||||||
async function runUpdateCheck(reason = 'manual') {
|
async function runUpdateCheck(reason = 'manual') {
|
||||||
if (updateCheckInProgress) {
|
if (updateCheckInProgress) {
|
||||||
console.log(`Update check skipped (${reason}): another check is already in progress`);
|
console.log(`Update check skipped (${reason}): another check is already in progress`);
|
||||||
|
|
@ -56,8 +73,9 @@ async function runUpdateCheck(reason = 'manual') {
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const sess = session.fromPartition('persist:chattz');
|
const sess = session.fromPartition('persist:chattz');
|
||||||
const backendUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || 'http://localhost:3000').replace(/\/$/, '');
|
const backendUrl = resolveBackendUrl();
|
||||||
const backendOrigin = new URL(backendUrl).origin;
|
const backendOrigin = new URL(backendUrl).origin;
|
||||||
|
console.log(`Desktop backend URL: ${backendUrl}`);
|
||||||
|
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
|
|
|
||||||
|
|
@ -576,50 +576,103 @@ function renderChannels() {
|
||||||
pList.style.paddingLeft = "24px";
|
pList.style.paddingLeft = "24px";
|
||||||
for (const p of participants) {
|
for (const p of participants) {
|
||||||
const pRow = document.createElement("div");
|
const pRow = document.createElement("div");
|
||||||
pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`;
|
pRow.className = `channel-row voice-participant-row ${p.is_speaking ? 'voice-speaking' : ''}`;
|
||||||
|
pRow.style.display = "block";
|
||||||
pRow.style.padding = "2px 8px";
|
pRow.style.padding = "2px 8px";
|
||||||
pRow.style.flexWrap = "wrap";
|
pRow.style.flexWrap = "wrap";
|
||||||
|
|
||||||
let sliderHtml = '';
|
const topRow = document.createElement("div");
|
||||||
if (p.user_id !== state.me.id) {
|
topRow.style.display = "flex";
|
||||||
const vol = state.userVolumes.get(p.user_id) ?? 1.0;
|
topRow.style.alignItems = "center";
|
||||||
const isVisible = state.voice.visibleVolumeSliders.has(p.user_id);
|
topRow.style.gap = "8px";
|
||||||
sliderHtml = `
|
topRow.style.width = "100%";
|
||||||
<div class="user-volume-control ${isVisible ? 'show-volume' : ''}" data-user-id="${p.user_id}">
|
|
||||||
<i data-lucide="volume-2" style="width: 12px; height: 12px; opacity: 0.6;"></i>
|
const avatar = document.createElement("div");
|
||||||
<input type="range" min="0" max="2" step="0.1" value="${vol}" class="volume-slider">
|
avatar.className = "avatar";
|
||||||
<span class="vol-pct">${Math.round(vol * 100)}%</span>
|
avatar.style.width = "20px";
|
||||||
</div>
|
avatar.style.height = "20px";
|
||||||
`;
|
avatar.style.fontSize = "10px";
|
||||||
|
avatar.textContent = shortName(p.display_name);
|
||||||
|
topRow.appendChild(avatar);
|
||||||
|
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.style.flex = "1";
|
||||||
|
name.style.overflow = "hidden";
|
||||||
|
name.style.textOverflow = "ellipsis";
|
||||||
|
name.textContent = p.display_name;
|
||||||
|
topRow.appendChild(name);
|
||||||
|
|
||||||
|
const isRemoteParticipant = p.user_id !== state.me.id;
|
||||||
|
if (isRemoteParticipant) {
|
||||||
|
const volumeToggle = document.createElement("button");
|
||||||
|
volumeToggle.type = "button";
|
||||||
|
volumeToggle.title = "Toggle volume slider";
|
||||||
|
volumeToggle.style.display = "grid";
|
||||||
|
volumeToggle.style.placeItems = "center";
|
||||||
|
volumeToggle.style.width = "20px";
|
||||||
|
volumeToggle.style.height = "20px";
|
||||||
|
volumeToggle.style.color = "var(--text-muted)";
|
||||||
|
volumeToggle.innerHTML = '<i data-lucide="volume-2" style="width: 14px; height: 14px;"></i>';
|
||||||
|
topRow.appendChild(volumeToggle);
|
||||||
|
|
||||||
|
volumeToggle.addEventListener('click', (e) => {
|
||||||
|
toggleVolumeSlider(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pRow.innerHTML = `
|
if (p.is_muted) {
|
||||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%;">
|
const muteIcon = document.createElement("i");
|
||||||
<div class="avatar" style="width:20px;height:20px;font-size:10px">${shortName(p.display_name)}</div>
|
muteIcon.setAttribute("data-lucide", "mic-off");
|
||||||
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(p.display_name)}</span>
|
muteIcon.className = "voice-muted-icon";
|
||||||
${p.is_muted ? '<i data-lucide="mic-off" class="voice-muted-icon"></i>' : ''}
|
topRow.appendChild(muteIcon);
|
||||||
</div>
|
}
|
||||||
${sliderHtml}
|
|
||||||
`;
|
pRow.appendChild(topRow);
|
||||||
|
|
||||||
|
const volumeControl = document.createElement("div");
|
||||||
|
volumeControl.className = `user-volume-control ${state.voice.visibleVolumeSliders.has(p.user_id) ? 'show-volume' : ''}`;
|
||||||
|
volumeControl.dataset.userId = p.user_id;
|
||||||
|
|
||||||
|
if (isRemoteParticipant) {
|
||||||
|
const volumeIcon = document.createElement("i");
|
||||||
|
volumeIcon.setAttribute("data-lucide", "volume-2");
|
||||||
|
volumeIcon.style.width = "12px";
|
||||||
|
volumeIcon.style.height = "12px";
|
||||||
|
volumeIcon.style.opacity = "0.6";
|
||||||
|
volumeControl.appendChild(volumeIcon);
|
||||||
|
|
||||||
|
const slider = document.createElement("input");
|
||||||
|
slider.type = "range";
|
||||||
|
slider.min = "0";
|
||||||
|
slider.max = "2";
|
||||||
|
slider.step = "0.1";
|
||||||
|
slider.value = String(state.userVolumes.get(p.user_id) ?? 1.0);
|
||||||
|
slider.className = "volume-slider";
|
||||||
|
volumeControl.appendChild(slider);
|
||||||
|
|
||||||
|
const volumePct = document.createElement("span");
|
||||||
|
volumePct.className = "vol-pct";
|
||||||
|
volumePct.textContent = `${Math.round((state.userVolumes.get(p.user_id) ?? 1.0) * 100)}%`;
|
||||||
|
volumeControl.appendChild(volumePct);
|
||||||
|
|
||||||
const slider = pRow.querySelector('.volume-slider');
|
|
||||||
if (slider) {
|
|
||||||
slider.addEventListener('input', (e) => {
|
slider.addEventListener('input', (e) => {
|
||||||
const val = parseFloat(e.target.value);
|
const val = parseFloat(e.target.value);
|
||||||
state.userVolumes.set(p.user_id, val);
|
state.userVolumes.set(p.user_id, val);
|
||||||
pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`;
|
volumePct.textContent = `${Math.round(val * 100)}%`;
|
||||||
|
|
||||||
const gainNode = state.voice.peerGainNodes.get(p.user_id);
|
const gainNode = state.voice.peerGainNodes.get(p.user_id);
|
||||||
if (gainNode) {
|
if (gainNode && state.voice.audioContext) {
|
||||||
gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05);
|
gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Stop propagation to prevent joining channel again when clicking slider
|
|
||||||
slider.addEventListener('click', (e) => e.stopPropagation());
|
volumeControl.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
volumeControl.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||||
|
pRow.appendChild(volumeControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleVolumeSlider = (e) => {
|
const toggleVolumeSlider = (e) => {
|
||||||
if (p.user_id === state.me.id) return;
|
if (!isRemoteParticipant) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (state.voice.visibleVolumeSliders.has(p.user_id)) {
|
if (state.voice.visibleVolumeSliders.has(p.user_id)) {
|
||||||
|
|
@ -633,6 +686,9 @@ function renderChannels() {
|
||||||
// Electron can swallow contextmenu events on some platforms; use
|
// Electron can swallow contextmenu events on some platforms; use
|
||||||
// right-button mousedown as a reliable fallback for slider toggle.
|
// right-button mousedown as a reliable fallback for slider toggle.
|
||||||
pRow.addEventListener('contextmenu', toggleVolumeSlider);
|
pRow.addEventListener('contextmenu', toggleVolumeSlider);
|
||||||
|
pRow.addEventListener('auxclick', (e) => {
|
||||||
|
if (e.button === 2) toggleVolumeSlider(e);
|
||||||
|
});
|
||||||
pRow.addEventListener('mousedown', (e) => {
|
pRow.addEventListener('mousedown', (e) => {
|
||||||
if (e.button === 2) toggleVolumeSlider(e);
|
if (e.button === 2) toggleVolumeSlider(e);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue