a
All checks were successful
/ upload (release) Successful in 22s

This commit is contained in:
pavel 2026-02-25 01:00:07 +01:00
commit 0273eab753
8 changed files with 127 additions and 8 deletions

View file

@ -71,6 +71,7 @@ const el = {
addGuildBtn: document.getElementById("add-guild-btn"), addGuildBtn: document.getElementById("add-guild-btn"),
guildTitle: document.getElementById("guild-title"), guildTitle: document.getElementById("guild-title"),
createInviteBtn: document.getElementById("create-invite-btn"), createInviteBtn: document.getElementById("create-invite-btn"),
inviteCopiedBadge: document.getElementById("invite-copied-badge"),
// Channels // Channels
channelList: document.getElementById("channel-list"), channelList: document.getElementById("channel-list"),
@ -536,11 +537,27 @@ async function createInviteLink() {
method: "POST", method: "POST",
body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }), body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }),
}); });
const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`; // Use the backend URL if available, otherwise fallback to current origin
const base = API_BASE_URL ? new URL(API_BASE_URL).origin : location.origin;
const link = `${base}/?invite=${encodeURIComponent(invite.code)}`;
// Use Electron's native clipboard API if available (defined in preload.js)
if (window.electronAPI && window.electronAPI.copyToClipboard) {
try {
await window.electronAPI.copyToClipboard(link);
showCopiedBadge();
return;
} catch (e) {
console.error("Electron clipboard write failed:", e);
}
}
// Fallback to navigator.clipboard
try { try {
await navigator.clipboard.writeText(link); await navigator.clipboard.writeText(link);
alert(`Invite link copied:\n${link}`); showCopiedBadge();
} catch { } catch (err) {
console.error("Navigator clipboard write failed:", err);
prompt("Copy invite link:", link); prompt("Copy invite link:", link);
} }
} catch (err) { } catch (err) {
@ -548,6 +565,13 @@ async function createInviteLink() {
} }
} }
function showCopiedBadge() {
el.inviteCopiedBadge.classList.remove("hidden");
setTimeout(() => {
el.inviteCopiedBadge.classList.add("hidden");
}, 2000);
}
// --- Logic --- // --- Logic ---
async function loadGuilds() { async function loadGuilds() {

View file

@ -40,6 +40,7 @@
<header class="sidebar-header clickable" id="guild-header"> <header class="sidebar-header clickable" id="guild-header">
<h2 id="guild-title">No server selected</h2> <h2 id="guild-title">No server selected</h2>
<div class="sidebar-header-actions"> <div class="sidebar-header-actions">
<span id="invite-copied-badge" class="copied-badge hidden">Copied!</span>
<button id="create-invite-btn" class="header-action-btn" title="Create Invite Link"> <button id="create-invite-btn" class="header-action-btn" title="Create Invite Link">
<i data-lucide="link-2"></i> <i data-lucide="link-2"></i>
</button> </button>

5
desktop/preload.js Normal file
View file

@ -0,0 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text)
});

View file

@ -354,6 +354,43 @@ select {
color: var(--text-strong); color: var(--text-strong);
} }
.copied-badge {
background: var(--brand);
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.4px;
animation: fadeInOut 2s ease forwards;
pointer-events: none;
white-space: nowrap;
order: -1;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(4px);
}
15% {
opacity: 1;
transform: translateY(0);
}
85% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-4px);
}
}
.header-action-btn i { .header-action-btn i {
width: 16px; width: 16px;
height: 16px; height: 16px;

14
main.js
View file

@ -1,4 +1,4 @@
const { app, BrowserWindow, session, desktopCapturer } = require('electron'); const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
const path = require('path'); const path = require('path');
const url = require('url'); const url = require('url');
@ -14,20 +14,26 @@ function createWindow() {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
webSecurity: false, // Required for cross-origin fetch/ws from file:// with cookies webSecurity: false, // Required for cross-origin fetch/ws from file:// with cookies
session: sess session: sess,
preload: path.join(__dirname, 'desktop', 'preload.js')
} }
}); });
ipcMain.handle('clipboard-write', (event, text) => {
clipboard.writeText(text);
return true;
});
// Auto-approve media permissions (camera, microphone) // Auto-approve media permissions (camera, microphone)
sess.setPermissionCheckHandler((webContents, permission) => { sess.setPermissionCheckHandler((webContents, permission) => {
if (permission === 'media') { if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') {
return true; return true;
} }
return false; return false;
}); });
sess.setPermissionRequestHandler((webContents, permission, callback) => { sess.setPermissionRequestHandler((webContents, permission, callback) => {
if (permission === 'media') { if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') {
callback(true); callback(true);
} else { } else {
callback(false); callback(false);

View file

@ -45,6 +45,7 @@ const el = {
addGuildBtn: document.getElementById("add-guild-btn"), addGuildBtn: document.getElementById("add-guild-btn"),
guildTitle: document.getElementById("guild-title"), guildTitle: document.getElementById("guild-title"),
createInviteBtn: document.getElementById("create-invite-btn"), createInviteBtn: document.getElementById("create-invite-btn"),
inviteCopiedBadge: document.getElementById("invite-copied-badge"),
// Channels // Channels
channelList: document.getElementById("channel-list"), channelList: document.getElementById("channel-list"),
@ -510,7 +511,7 @@ async function createInviteLink() {
const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`; const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`;
try { try {
await navigator.clipboard.writeText(link); await navigator.clipboard.writeText(link);
alert(`Invite link copied:\n${link}`); showCopiedBadge();
} catch { } catch {
prompt("Copy invite link:", link); prompt("Copy invite link:", link);
} }
@ -519,6 +520,13 @@ async function createInviteLink() {
} }
} }
function showCopiedBadge() {
el.inviteCopiedBadge.classList.remove("hidden");
setTimeout(() => {
el.inviteCopiedBadge.classList.add("hidden");
}, 2000);
}
// --- Logic --- // --- Logic ---
async function loadGuilds() { async function loadGuilds() {

View file

@ -40,6 +40,7 @@
<header class="sidebar-header clickable" id="guild-header"> <header class="sidebar-header clickable" id="guild-header">
<h2 id="guild-title">No server selected</h2> <h2 id="guild-title">No server selected</h2>
<div class="sidebar-header-actions"> <div class="sidebar-header-actions">
<span id="invite-copied-badge" class="copied-badge hidden">Copied!</span>
<button id="create-invite-btn" class="header-action-btn" title="Create Invite Link"> <button id="create-invite-btn" class="header-action-btn" title="Create Invite Link">
<i data-lucide="link-2"></i> <i data-lucide="link-2"></i>
</button> </button>

View file

@ -354,6 +354,43 @@ select {
color: var(--text-strong); color: var(--text-strong);
} }
.copied-badge {
background: var(--brand);
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.4px;
animation: fadeInOut 2s ease forwards;
pointer-events: none;
white-space: nowrap;
order: -1;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(4px);
}
15% {
opacity: 1;
transform: translateY(0);
}
85% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-4px);
}
}
.header-action-btn i { .header-action-btn i {
width: 16px; width: 16px;
height: 16px; height: 16px;