feat: real-time messaging, Electron desktop app, admin drive portal
Messaging: - Optimistic UI: message appears instantly in sender's view on send - Skip WS relay-back of own messages to avoid duplicates - Fix sent_at field name mismatch (sentAt → sent_at fallback) Admin portal: - /admin route: list all users, storage used vs quota, file count - Inline-edit per-user storage quota (MB) and admin flag - Stats header: total users, total storage, total files - is_admin + storage_quota_mb columns added to users table - Login response now includes isAdmin; sidebar shows Admin link for admins - requireAdmin middleware on all /admin/* routes Electron desktop app (desktop/): - BrowserWindow with native title bar, system tray, hide-to-tray on close - First-run setup dialog to configure self-hosted server URL - Persists server URL via electron-store - Tray menu: Open, Change server URL, Quit - IPC: showNotification for future native push notifications - electron-builder config for macOS (DMG), Windows (NSIS), Linux (AppImage) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
46f0a4b71e
commit
9b57dd9b94
15 files changed
+6104
-14
No files matched your search
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<rect width="512" height="512" rx="100" fill="#1e1b4b"/>
|
||||
<path d="M256 120 C180 120 120 180 120 256 C120 332 180 392 256 392 C332 392 392 332 392 256 C392 180 332 120 256 120Z" fill="none" stroke="#6366f1" stroke-width="24"/>
|
||||
<path d="M200 200 L256 160 L312 200 L312 280 L256 320 L200 280Z" fill="#6366f1" opacity="0.9"/>
|
||||
<circle cx="256" cy="240" r="30" fill="#e0e7ff"/>
|
||||
<path d="M180 320 Q256 360 332 320" fill="none" stroke="#a5b4fc" stroke-width="16" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
+197
@@ -0,0 +1,197 @@
|
||||
'use strict';
|
||||
|
||||
const { app, BrowserWindow, Tray, Menu, ipcMain, Notification, nativeImage, shell } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// electron-store v10 uses ESM — load dynamically
|
||||
let Store;
|
||||
let store;
|
||||
|
||||
async function loadStore() {
|
||||
const mod = await import('electron-store');
|
||||
Store = mod.default;
|
||||
store = new Store({
|
||||
defaults: { serverUrl: '' },
|
||||
});
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
let tray = null;
|
||||
let setupWindow = null;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function getIconPath() {
|
||||
if (process.platform === 'darwin') return path.join(__dirname, 'assets', 'icon.icns');
|
||||
if (process.platform === 'win32') return path.join(__dirname, 'assets', 'icon.ico');
|
||||
return path.join(__dirname, 'assets', 'icon.png');
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
const trayImg = nativeImage.createFromPath(
|
||||
path.join(__dirname, 'assets', 'tray.png')
|
||||
);
|
||||
tray = new Tray(trayImg.isEmpty() ? nativeImage.createEmpty() : trayImg);
|
||||
tray.setToolTip('CloudLink');
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: 'Open CloudLink', click: () => { mainWindow?.show(); mainWindow?.focus(); } },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Change server URL…',
|
||||
click: () => openSetup(true),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
]);
|
||||
|
||||
tray.setContextMenu(menu);
|
||||
tray.on('click', () => { mainWindow?.show(); mainWindow?.focus(); });
|
||||
}
|
||||
|
||||
function openSetup(fromTray = false) {
|
||||
if (setupWindow) { setupWindow.focus(); return; }
|
||||
|
||||
setupWindow = new BrowserWindow({
|
||||
width: 520,
|
||||
height: 340,
|
||||
resizable: false,
|
||||
title: 'CloudLink — Setup',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
const current = store.get('serverUrl');
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #0f172a; color: #e2e8f0; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; height: 100vh; gap: 24px; padding: 32px; }
|
||||
h1 { font-size: 1.25rem; font-weight: 700; }
|
||||
p { font-size: 0.875rem; color: #94a3b8; text-align: center; }
|
||||
input { width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid #334155;
|
||||
background: #1e293b; color: #e2e8f0; font-size: 0.9rem; outline: none; }
|
||||
input:focus { border-color: #6366f1; }
|
||||
button { padding: 10px 24px; background: #6366f1; color: white; border: none;
|
||||
border-radius: 8px; font-size: 0.9rem; cursor: pointer; }
|
||||
button:hover { background: #4f46e5; }
|
||||
.row { display: flex; gap: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔗 CloudLink</h1>
|
||||
<p>Enter the URL of your CloudLink server.<br>
|
||||
For local development use <code style="color:#818cf8">http://localhost:5173</code></p>
|
||||
<input id="url" type="url" placeholder="https://cloudlink.yourserver.com"
|
||||
value="${current}" />
|
||||
<div class="row">
|
||||
<button onclick="save()">Connect</button>
|
||||
</div>
|
||||
<script>
|
||||
function save() {
|
||||
const v = document.getElementById('url').value.trim().replace(/\\/$/, '');
|
||||
if (!v) return;
|
||||
window.cloudlink.setServerUrl(v);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
setupWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
|
||||
setupWindow.on('closed', () => { setupWindow = null; });
|
||||
}
|
||||
|
||||
function createMainWindow(serverUrl) {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 820,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
title: 'CloudLink',
|
||||
icon: getIconPath(),
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
// Allow the web app to use the Web Crypto API and WebSockets
|
||||
webSecurity: true,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadURL(serverUrl);
|
||||
|
||||
// Open external links in the default browser
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
// Hide to tray instead of quitting on close (macOS/Windows convention)
|
||||
mainWindow.on('close', (e) => {
|
||||
if (process.platform !== 'darwin' && !app.isQuitting) {
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => { mainWindow = null; });
|
||||
}
|
||||
|
||||
// ── IPC ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
ipcMain.handle('get-server-url', () => store.get('serverUrl'));
|
||||
|
||||
ipcMain.handle('set-server-url', (_e, url) => {
|
||||
store.set('serverUrl', url);
|
||||
setupWindow?.close();
|
||||
if (mainWindow) {
|
||||
mainWindow.loadURL(url);
|
||||
} else {
|
||||
createMainWindow(url);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('show-notification', (_e, { title, body }) => {
|
||||
if (Notification.isSupported()) {
|
||||
new Notification({ title, body }).show();
|
||||
}
|
||||
});
|
||||
|
||||
// ── App lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await loadStore();
|
||||
createTray();
|
||||
|
||||
const serverUrl = store.get('serverUrl');
|
||||
if (!serverUrl) {
|
||||
openSetup();
|
||||
} else {
|
||||
createMainWindow(serverUrl);
|
||||
}
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow && store.get('serverUrl')) {
|
||||
createMainWindow(store.get('serverUrl'));
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => { app.isQuitting = true; });
|
||||
|
||||
// Keep running when all windows are closed (show via tray)
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
// don't quit — user can reopen from tray
|
||||
}
|
||||
});
|
||||
Generated
+5541
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "cloudlink-desktop",
|
||||
"version": "1.0.0",
|
||||
"description": "CloudLink — encrypted messaging and cloud drive",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder --mac --win --linux",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:win": "electron-builder --win",
|
||||
"build:linux": "electron-builder --linux"
|
||||
},
|
||||
"build": {
|
||||
"appId": "us.necloud.cloudlink",
|
||||
"productName": "CloudLink",
|
||||
"copyright": "Copyright © 2026",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"assets/**"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"icon": "assets/icon.icns",
|
||||
"target": [{ "target": "dmg", "arch": ["x64", "arm64"] }]
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/icon.ico",
|
||||
"target": [{ "target": "nsis", "arch": ["x64"] }]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/icon.png",
|
||||
"category": "Network",
|
||||
"target": [{ "target": "AppImage", "arch": ["x64"] }]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.0.0",
|
||||
"electron-builder": "^25.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('cloudlink', {
|
||||
getServerUrl: () => ipcRenderer.invoke('get-server-url'),
|
||||
setServerUrl: (url) => ipcRenderer.invoke('set-server-url', url),
|
||||
showNotification: (title, body) =>
|
||||
ipcRenderer.invoke('show-notification', { title, body }),
|
||||
// Platform detection for the web app
|
||||
platform: process.platform,
|
||||
isDesktop: true,
|
||||
});
|
||||
Reference in new issue
Block a user