Initial code commit for project
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
release/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
yarn-debug.log*
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
8
.idea/corsair-lcd-control.iml
generated
Normal file
8
.idea/corsair-lcd-control.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/corsair-lcd-control.iml" filepath="$PROJECT_DIR$/.idea/corsair-lcd-control.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
57
README.md
57
README.md
@@ -1,2 +1,59 @@
|
|||||||
# corsair-lcd-control
|
# corsair-lcd-control
|
||||||
|
|
||||||
|
Electron app to control Corsair LCD displays. Monitors system stats (CPU temp, RAM, GPU temp), detects USB devices, and manages preferences.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 18
|
||||||
|
- npm >= 9
|
||||||
|
- **Linux**: `libusb-1.0-0-dev`, `build-essential`, `python3`
|
||||||
|
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
- **Windows**: Build Tools for Visual Studio
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd corsair-lcd-control
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
`postinstall` automatically rebuilds native modules for Electron using `@electron/rebuild`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces platform-specific packages:
|
||||||
|
|
||||||
|
| Platform | Format |
|
||||||
|
|----------|--------|
|
||||||
|
| Linux | `.AppImage`, `.deb` |
|
||||||
|
| macOS | `.dmg` |
|
||||||
|
| Windows | `.exe` (NSIS) |
|
||||||
|
|
||||||
|
## Rebuild Native Modules
|
||||||
|
|
||||||
|
If native modules fail to load after a Node.js or Electron upgrade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- **USB** — `usb` module enumerates and connects to devices
|
||||||
|
- **System Tray** — background process with context menu (Show/Hide/Quit)
|
||||||
|
- **Preferences** — persisted via `electron-store` (JSON)
|
||||||
|
- **File Pickers** — native OS dialogs via Electron's `dialog` API
|
||||||
|
- **CPU Temperature** — `systeminformation.cpuTemperature()`
|
||||||
|
- **RAM Usage** — `systeminformation.mem()`
|
||||||
|
- **GPU Temperature** — `systeminformation.graphics()`
|
||||||
|
|||||||
231
main.js
Normal file
231
main.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const Store = require('electron-store');
|
||||||
|
const si = require('systeminformation');
|
||||||
|
const usb = require('usb');
|
||||||
|
|
||||||
|
const store = new Store({ defaults: { preferences: {} } });
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
|
let tray = null;
|
||||||
|
|
||||||
|
function createTrayIcon() {
|
||||||
|
const size = 16;
|
||||||
|
const canvas = Buffer.alloc(size * size * 4);
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
const idx = (y * size + x) * 4;
|
||||||
|
const cx = x - size / 2, cy = y - size / 2;
|
||||||
|
if (Math.sqrt(cx * cx + cy * cy) < size / 2 - 1) {
|
||||||
|
canvas[idx] = 0;
|
||||||
|
canvas[idx + 1] = 120;
|
||||||
|
canvas[idx + 2] = 255;
|
||||||
|
canvas[idx + 3] = 255;
|
||||||
|
} else {
|
||||||
|
canvas[idx] = 0;
|
||||||
|
canvas[idx + 1] = 0;
|
||||||
|
canvas[idx + 2] = 0;
|
||||||
|
canvas[idx + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nativeImage.createFromBuffer(canvas, { width: size, height: size });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 900,
|
||||||
|
height: 600,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
|
||||||
|
mainWindow.on('close', (e) => {
|
||||||
|
if (!app.isQuitting) {
|
||||||
|
e.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
tray = new Tray(createTrayIcon());
|
||||||
|
tray.setToolTip('Corsair LCD Control');
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{ label: 'Show', click: () => mainWindow.show() },
|
||||||
|
{ label: 'Hide', click: () => mainWindow.hide() },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Quit', click: () => { app.isQuitting = true; app.quit(); } },
|
||||||
|
]);
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
tray.on('double-click', () => mainWindow.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
const VENDORS = {
|
||||||
|
0x1B1C: 'Corsair',
|
||||||
|
0x046D: 'Logitech',
|
||||||
|
0x0A5C: 'Broadcom',
|
||||||
|
0x8087: 'Intel',
|
||||||
|
0x05E3: 'Genesys Logic',
|
||||||
|
0x04F2: 'Chicony',
|
||||||
|
0x0BDA: 'Realtek',
|
||||||
|
0x1D6B: 'Linux Foundation',
|
||||||
|
0x3034: 'Realtek',
|
||||||
|
0x5964: 'Asmedia',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatHexId(vendorId, productId) {
|
||||||
|
const hex = `${vendorId.toString(16).padStart(4, '0')}:${productId.toString(16).padStart(4, '0')}`;
|
||||||
|
const name = VENDORS[vendorId] || '';
|
||||||
|
return name ? `${name} [${hex}]` : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStringDescriptor(device, index) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!index) return resolve('');
|
||||||
|
try {
|
||||||
|
device.getStringDescriptor(index, (err, desc) => {
|
||||||
|
resolve(err ? '' : desc);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
resolve('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- USB ---
|
||||||
|
|
||||||
|
ipcMain.handle('usb:listDevices', async () => {
|
||||||
|
try {
|
||||||
|
const devices = usb.getDeviceList();
|
||||||
|
const results = [];
|
||||||
|
for (const d of devices) {
|
||||||
|
const entry = {
|
||||||
|
vendorId: d.deviceDescriptor.idVendor,
|
||||||
|
productId: d.deviceDescriptor.idProduct,
|
||||||
|
displayName: formatHexId(d.deviceDescriptor.idVendor, d.deviceDescriptor.idProduct),
|
||||||
|
manufacturer: '',
|
||||||
|
product: '',
|
||||||
|
serialNumber: '',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
d.open(true);
|
||||||
|
const [mfg, prod, sn] = await Promise.all([
|
||||||
|
readStringDescriptor(d, d.deviceDescriptor.iManufacturer),
|
||||||
|
readStringDescriptor(d, d.deviceDescriptor.iProduct),
|
||||||
|
readStringDescriptor(d, d.deviceDescriptor.iSerialNumber),
|
||||||
|
]);
|
||||||
|
if (mfg || prod) {
|
||||||
|
entry.manufacturer = mfg;
|
||||||
|
entry.product = prod;
|
||||||
|
entry.displayName = [mfg, prod].filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
entry.serialNumber = sn;
|
||||||
|
d.close();
|
||||||
|
} catch {
|
||||||
|
// no permission — keep the hex-based displayName
|
||||||
|
}
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('usb:connect', async (_e, { vendorId, productId }) => {
|
||||||
|
try {
|
||||||
|
const device = usb.findByIds(vendorId, productId);
|
||||||
|
if (!device) throw new Error('Device not found');
|
||||||
|
device.open();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Preferences ---
|
||||||
|
ipcMain.handle('prefs:get', (_e, key) => {
|
||||||
|
if (key) return store.get(key);
|
||||||
|
return store.store;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('prefs:set', (_e, key, value) => {
|
||||||
|
store.set(key, value);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('prefs:delete', (_e, key) => {
|
||||||
|
store.delete(key);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- File Picker ---
|
||||||
|
ipcMain.handle('dialog:openFile', async (_e, options) => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openFile'],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('dialog:saveFile', async (_e, options) => {
|
||||||
|
const result = await dialog.showSaveDialog(mainWindow, options);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- System Info ---
|
||||||
|
ipcMain.handle('system:cpuTemp', async () => {
|
||||||
|
try {
|
||||||
|
const data = await si.cpuTemperature();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('system:ram', async () => {
|
||||||
|
try {
|
||||||
|
const data = await si.mem();
|
||||||
|
return {
|
||||||
|
total: data.total,
|
||||||
|
free: data.free,
|
||||||
|
used: data.used,
|
||||||
|
usedPercent: ((data.used / data.total) * 100).toFixed(1),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('system:gpu', async () => {
|
||||||
|
try {
|
||||||
|
const data = await si.graphics();
|
||||||
|
const controllers = data.controllers.map((c) => ({
|
||||||
|
vendor: c.vendor,
|
||||||
|
model: c.model,
|
||||||
|
temperatureGpu: c.temperatureGpu,
|
||||||
|
memoryUsed: c.memoryUsed,
|
||||||
|
memoryTotal: c.memoryTotal,
|
||||||
|
}));
|
||||||
|
return controllers;
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
createTray();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
3922
package-lock.json
generated
Normal file
3922
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "corsair-lcd-control",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"author": "xerotacovix <xerotacovix@proton.me>",
|
||||||
|
"homepage": "https://github.com/xerotacovix/corsair-lcd-control",
|
||||||
|
"description": "Electron app to control Corsair LCD displays",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"build": "electron-builder",
|
||||||
|
"postinstall": "CXXFLAGS='-std=c++17' npx @electron/rebuild",
|
||||||
|
"rebuild": "CXXFLAGS='-std=c++17' npx @electron/rebuild"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.corsair-lcd-control.app",
|
||||||
|
"productName": "Corsair LCD Control",
|
||||||
|
"files": [
|
||||||
|
"main.js",
|
||||||
|
"preload.js",
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"target": [
|
||||||
|
"dmg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron-store": "^8.1.0",
|
||||||
|
"systeminformation": "^5.21.0",
|
||||||
|
"usb": "^2.9.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^42.3.3",
|
||||||
|
"electron-builder": "^24.9.0"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"usb@2.18.0": true,
|
||||||
|
"electron@42.3.3": true
|
||||||
|
}
|
||||||
|
}
|
||||||
21
preload.js
Normal file
21
preload.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// USB
|
||||||
|
usbListDevices: () => ipcRenderer.invoke('usb:listDevices'),
|
||||||
|
usbConnect: (vendorId, productId) => ipcRenderer.invoke('usb:connect', { vendorId, productId }),
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
prefsGet: (key) => ipcRenderer.invoke('prefs:get', key),
|
||||||
|
prefsSet: (key, value) => ipcRenderer.invoke('prefs:set', key, value),
|
||||||
|
prefsDelete: (key) => ipcRenderer.invoke('prefs:delete', key),
|
||||||
|
|
||||||
|
// File Picker
|
||||||
|
dialogOpenFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
|
||||||
|
dialogSaveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
|
||||||
|
|
||||||
|
// System Info
|
||||||
|
systemCpuTemp: () => ipcRenderer.invoke('system:cpuTemp'),
|
||||||
|
systemRam: () => ipcRenderer.invoke('system:ram'),
|
||||||
|
systemGpu: () => ipcRenderer.invoke('system:gpu'),
|
||||||
|
});
|
||||||
63
src/index.html
Normal file
63
src/index.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Corsair LCD Control</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header>
|
||||||
|
<h1>Corsair LCD Control</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="system-info">
|
||||||
|
<h2>System Monitoring</h2>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>CPU Temperature</h3>
|
||||||
|
<p id="cpu-temp">--</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>RAM Usage</h3>
|
||||||
|
<p id="ram-usage">--</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>GPU Temperature</h3>
|
||||||
|
<p id="gpu-temp">--</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="refresh-sysinfo">Refresh</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="usb-section">
|
||||||
|
<h2>USB Devices</h2>
|
||||||
|
<button id="scan-usb">Scan USB Devices</button>
|
||||||
|
<ul id="usb-list"></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="preferences-section">
|
||||||
|
<h2>Preferences</h2>
|
||||||
|
<div>
|
||||||
|
<label>Refresh Interval (ms):</label>
|
||||||
|
<input type="number" id="refresh-interval" value="5000" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="save-prefs">Save Preferences</button>
|
||||||
|
<button id="load-prefs">Load Preferences</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="file-section">
|
||||||
|
<h2>File Picker</h2>
|
||||||
|
<button id="open-file">Open File</button>
|
||||||
|
<button id="save-file">Save File</button>
|
||||||
|
<p id="file-result"></p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
78
src/renderer.js
Normal file
78
src/renderer.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const api = window.electronAPI;
|
||||||
|
|
||||||
|
// --- System Info ---
|
||||||
|
async function refreshSystemInfo() {
|
||||||
|
const [cpuTemp, ram, gpu] = await Promise.all([
|
||||||
|
api.systemCpuTemp(),
|
||||||
|
api.systemRam(),
|
||||||
|
api.systemGpu(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.getElementById('cpu-temp').textContent =
|
||||||
|
cpuTemp.error ? `Error: ${cpuTemp.error}` :
|
||||||
|
cpuTemp.main ? `${cpuTemp.main}°C` : 'N/A';
|
||||||
|
|
||||||
|
document.getElementById('ram-usage').textContent =
|
||||||
|
ram.error ? `Error: ${ram.error}` :
|
||||||
|
`${ram.usedPercent}%`;
|
||||||
|
|
||||||
|
document.getElementById('gpu-temp').textContent =
|
||||||
|
gpu.error ? `Error: ${gpu.error}` :
|
||||||
|
gpu.length > 0 && gpu[0].temperatureGpu != null ? `${gpu[0].temperatureGpu}°C` :
|
||||||
|
'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('refresh-sysinfo').addEventListener('click', refreshSystemInfo);
|
||||||
|
|
||||||
|
// --- USB ---
|
||||||
|
document.getElementById('scan-usb').addEventListener('click', async () => {
|
||||||
|
const result = await api.usbListDevices();
|
||||||
|
const list = document.getElementById('usb-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (result.error) {
|
||||||
|
list.innerHTML = `<li>Error: ${result.error}</li>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.length === 0) {
|
||||||
|
list.innerHTML = '<li>No USB devices found</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const dev of result) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = dev.displayName;
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Preferences ---
|
||||||
|
document.getElementById('save-prefs').addEventListener('click', async () => {
|
||||||
|
const interval = document.getElementById('refresh-interval').value;
|
||||||
|
await api.prefsSet('refreshInterval', parseInt(interval, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('load-prefs').addEventListener('click', async () => {
|
||||||
|
const interval = await api.prefsGet('refreshInterval');
|
||||||
|
if (interval != null) {
|
||||||
|
document.getElementById('refresh-interval').value = interval;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- File Picker ---
|
||||||
|
document.getElementById('open-file').addEventListener('click', async () => {
|
||||||
|
const result = await api.dialogOpenFile({
|
||||||
|
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||||
|
});
|
||||||
|
document.getElementById('file-result').textContent =
|
||||||
|
result.canceled ? 'Cancelled' : `Selected: ${result.filePaths.join(', ')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-file').addEventListener('click', async () => {
|
||||||
|
const result = await api.dialogSaveFile({
|
||||||
|
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||||
|
});
|
||||||
|
document.getElementById('file-result').textContent =
|
||||||
|
result.canceled ? 'Cancelled' : `Save path: ${result.filePath}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-refresh on load
|
||||||
|
refreshSystemInfo();
|
||||||
113
src/styles.css
Normal file
113
src/styles.css
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
color: #0078ff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #16213e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #a0a0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #0f3460;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8888aa;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0078ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #0078ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #005fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#usb-list {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#usb-list li {
|
||||||
|
padding: 8px;
|
||||||
|
background: #0f3460;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8888aa;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
background: #0f3460;
|
||||||
|
border: 1px solid #333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user