Add stream settings feature with resolution, fps, and quality adjustments; enhance system tray functionality; update dependencies and UI styles
@ -118,6 +118,16 @@ pub enum SignalMessage {
|
||||
key: String,
|
||||
event_type: String, // "down", "up"
|
||||
},
|
||||
/// 流媒体设置
|
||||
#[serde(rename = "stream_settings")]
|
||||
StreamSettings {
|
||||
session_id: String,
|
||||
from_device: String,
|
||||
to_device: String,
|
||||
resolution: f64,
|
||||
fps: u32,
|
||||
quality: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// 信令客户端
|
||||
|
||||
@ -11,7 +11,7 @@ easyremote-common = { path = "../common" }
|
||||
easyremote-client-core = { path = "../client-core" }
|
||||
|
||||
# Tauri
|
||||
tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all"] }
|
||||
tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all", "global-shortcut-all", "process-all", "system-tray"] }
|
||||
|
||||
# Windows single instance
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@ -21,6 +21,7 @@ windows = { version = "0.58", features = [
|
||||
"Win32_Security",
|
||||
"Win32_UI_WindowsAndMessaging"
|
||||
] }
|
||||
winreg = "0.52"
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true }
|
||||
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 20 KiB |
45
crates/client-tauri/app-icon.svg
Normal file
@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea"/>
|
||||
<stop offset="100%" style="stop-color:#764ba2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screen" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1a1a2e"/>
|
||||
<stop offset="100%" style="stop-color:#16213e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景圆角矩形 -->
|
||||
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="url(#bg)"/>
|
||||
|
||||
<!-- 主显示器 -->
|
||||
<rect x="100" y="140" width="200" height="150" rx="12" ry="12" fill="#fff" opacity="0.95"/>
|
||||
<rect x="112" y="152" width="176" height="110" rx="4" ry="4" fill="url(#screen)"/>
|
||||
|
||||
<!-- 主显示器支架 -->
|
||||
<rect x="175" y="290" width="50" height="20" rx="4" ry="4" fill="#fff" opacity="0.9"/>
|
||||
<rect x="155" y="305" width="90" height="12" rx="6" ry="6" fill="#fff" opacity="0.85"/>
|
||||
|
||||
<!-- 副显示器 (远程) -->
|
||||
<rect x="260" y="200" width="160" height="120" rx="10" ry="10" fill="#fff" opacity="0.85"/>
|
||||
<rect x="270" y="210" width="140" height="84" rx="3" ry="3" fill="url(#screen)"/>
|
||||
|
||||
<!-- 副显示器支架 -->
|
||||
<rect x="320" y="320" width="40" height="16" rx="3" ry="3" fill="#fff" opacity="0.8"/>
|
||||
<rect x="305" y="332" width="70" height="10" rx="5" ry="5" fill="#fff" opacity="0.75"/>
|
||||
|
||||
<!-- 连接线/信号 -->
|
||||
<path d="M 288 215 Q 250 180 212 215" stroke="#4ade80" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.9"/>
|
||||
<path d="M 283 225 Q 250 195 217 225" stroke="#4ade80" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M 278 235 Q 250 210 222 235" stroke="#4ade80" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
|
||||
<!-- 主屏幕内容线条 -->
|
||||
<rect x="130" y="170" width="80" height="6" rx="3" fill="#4ade80" opacity="0.8"/>
|
||||
<rect x="130" y="185" width="60" height="6" rx="3" fill="#fff" opacity="0.3"/>
|
||||
<rect x="130" y="200" width="100" height="6" rx="3" fill="#fff" opacity="0.3"/>
|
||||
<rect x="130" y="215" width="45" height="6" rx="3" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<!-- 光标箭头 -->
|
||||
<path d="M 340 250 L 340 285 L 352 275 L 365 295 L 375 290 L 362 270 L 375 265 Z" fill="#fff" opacity="0.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
79
crates/client-tauri/generate-icons.js
Normal file
@ -0,0 +1,79 @@
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const svgPath = path.join(__dirname, 'app-icon.svg');
|
||||
const iconsDir = path.join(__dirname, 'icons');
|
||||
|
||||
// 确保icons目录存在
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const sizes = [
|
||||
{ name: '32x32.png', size: 32 },
|
||||
{ name: '128x128.png', size: 128 },
|
||||
{ name: '128x128@2x.png', size: 256 },
|
||||
{ name: 'icon.png', size: 512 },
|
||||
{ name: 'Square30x30Logo.png', size: 30 },
|
||||
{ name: 'Square44x44Logo.png', size: 44 },
|
||||
{ name: 'Square71x71Logo.png', size: 71 },
|
||||
{ name: 'Square89x89Logo.png', size: 89 },
|
||||
{ name: 'Square107x107Logo.png', size: 107 },
|
||||
{ name: 'Square142x142Logo.png', size: 142 },
|
||||
{ name: 'Square150x150Logo.png', size: 150 },
|
||||
{ name: 'Square284x284Logo.png', size: 284 },
|
||||
{ name: 'Square310x310Logo.png', size: 310 },
|
||||
{ name: 'StoreLogo.png', size: 50 },
|
||||
];
|
||||
|
||||
async function generateIcons() {
|
||||
const svgBuffer = fs.readFileSync(svgPath);
|
||||
|
||||
for (const { name, size } of sizes) {
|
||||
const outputPath = path.join(iconsDir, name);
|
||||
await sharp(svgBuffer)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(outputPath);
|
||||
console.log(`Generated: ${name} (${size}x${size})`);
|
||||
}
|
||||
|
||||
// 生成 ICO 文件 (Windows)
|
||||
// 为了简单起见,我们用256x256的PNG作为ICO的基础
|
||||
const icoSizes = [16, 32, 48, 64, 128, 256];
|
||||
const icoPngs = [];
|
||||
|
||||
for (const size of icoSizes) {
|
||||
const buffer = await sharp(svgBuffer)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toBuffer();
|
||||
icoPngs.push({ size, buffer });
|
||||
}
|
||||
|
||||
// 生成简单的ICO (使用256x256作为主图标)
|
||||
const ico256 = await sharp(svgBuffer)
|
||||
.resize(256, 256)
|
||||
.png()
|
||||
.toFile(path.join(iconsDir, 'icon.ico.png'));
|
||||
|
||||
// 复制一个PNG作为ICO的替代(Tauri会处理)
|
||||
await sharp(svgBuffer)
|
||||
.resize(256, 256)
|
||||
.png()
|
||||
.toFile(path.join(iconsDir, 'icon.ico'));
|
||||
|
||||
console.log('Generated: icon.ico');
|
||||
|
||||
// 复制到根目录作为app-icon.png
|
||||
await sharp(svgBuffer)
|
||||
.resize(512, 512)
|
||||
.png()
|
||||
.toFile(path.join(__dirname, 'app-icon.png'));
|
||||
console.log('Generated: app-icon.png');
|
||||
|
||||
console.log('\nAll icons generated successfully!');
|
||||
}
|
||||
|
||||
generateIcons().catch(console.error);
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 29 KiB |
BIN
crates/client-tauri/icons/icon.ico.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 22 KiB |
589
crates/client-tauri/package-lock.json
generated
Normal file
@ -0,0 +1,589 @@
|
||||
{
|
||||
"name": "client-tauri",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz",
|
||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/client-tauri/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,33 @@ static FORCE_OFFLINE_FLAG: AtomicBool = AtomicBool::new(false);
|
||||
/// 当前活跃的屏幕流会话
|
||||
static ACTIVE_SESSION: tokio::sync::OnceCell<Arc<RwLock<Option<ActiveScreenSession>>>> = tokio::sync::OnceCell::const_new();
|
||||
|
||||
/// 流媒体设置
|
||||
#[derive(Clone)]
|
||||
struct StreamSettings {
|
||||
resolution: f64,
|
||||
fps: u32,
|
||||
quality: u32,
|
||||
}
|
||||
|
||||
impl Default for StreamSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resolution: 0.5,
|
||||
fps: 15,
|
||||
quality: 70,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局流媒体设置
|
||||
static STREAM_SETTINGS: tokio::sync::OnceCell<Arc<RwLock<StreamSettings>>> = tokio::sync::OnceCell::const_new();
|
||||
|
||||
async fn get_stream_settings() -> &'static Arc<RwLock<StreamSettings>> {
|
||||
STREAM_SETTINGS.get_or_init(|| async {
|
||||
Arc::new(RwLock::new(StreamSettings::default()))
|
||||
}).await
|
||||
}
|
||||
|
||||
/// 活跃屏幕会话
|
||||
struct ActiveScreenSession {
|
||||
session_id: String,
|
||||
@ -89,6 +116,8 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
let signal_client_clone = signal_client.clone();
|
||||
let active_session_clone = active_session.clone();
|
||||
|
||||
let stream_settings = get_stream_settings().await.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// 创建屏幕捕获器
|
||||
let mut capturer = match ScreenCapturer::new(0) {
|
||||
@ -107,6 +136,18 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
// 获取当前设置
|
||||
let (resolution, fps, _quality) = {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(async {
|
||||
let s = stream_settings.read().await;
|
||||
(s.resolution, s.fps, s.quality)
|
||||
})
|
||||
};
|
||||
|
||||
// 计算帧间隔
|
||||
let frame_interval = std::time::Duration::from_millis(1000 / fps as u64);
|
||||
|
||||
// 检查是否需要停止
|
||||
let should_stop = {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
@ -130,8 +171,10 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
let mut jpeg_data = Vec::new();
|
||||
let mut cursor = std::io::Cursor::new(&mut jpeg_data);
|
||||
|
||||
// 降低分辨率和质量以减少带宽
|
||||
let scaled = image::imageops::resize(&img, width / 2, height / 2, image::imageops::FilterType::Nearest);
|
||||
// 根据设置调整分辨率
|
||||
let scaled_width = ((width as f64) * resolution) as u32;
|
||||
let scaled_height = ((height as f64) * resolution) as u32;
|
||||
let scaled = image::imageops::resize(&img, scaled_width, scaled_height, image::imageops::FilterType::Nearest);
|
||||
|
||||
if scaled.write_to(&mut cursor, image::ImageFormat::Jpeg).is_ok() {
|
||||
let jpeg_len = jpeg_data.len();
|
||||
@ -141,8 +184,6 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
let session_id_send = session_id_capture.clone();
|
||||
let controller_device_send = controller_device_capture.clone();
|
||||
let signal_client_send = signal_client_clone.clone();
|
||||
let scaled_width = width / 2;
|
||||
let scaled_height = height / 2;
|
||||
|
||||
rt.block_on(async move {
|
||||
let client = signal_client_send.read().await;
|
||||
@ -159,8 +200,9 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
|
||||
frame_count += 1;
|
||||
if frame_count % 30 == 0 {
|
||||
let fps = frame_count as f64 / start_time.elapsed().as_secs_f64();
|
||||
tracing::debug!("屏幕流帧率: {:.1} fps, 帧大小: {} KB", fps, jpeg_len / 1024);
|
||||
let actual_fps = frame_count as f64 / start_time.elapsed().as_secs_f64();
|
||||
tracing::debug!("屏幕流: {:.1} fps, 分辨率: {}x{}, 帧大小: {} KB",
|
||||
actual_fps, scaled_width, scaled_height, jpeg_len / 1024);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -174,8 +216,8 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
}
|
||||
}
|
||||
|
||||
// 控制帧率 (约 15 fps)
|
||||
std::thread::sleep(std::time::Duration::from_millis(66));
|
||||
// 根据设置控制帧率
|
||||
std::thread::sleep(frame_interval);
|
||||
}
|
||||
|
||||
// 清理会话
|
||||
@ -191,8 +233,15 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m
|
||||
fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option<u8>, delta: Option<f64>) {
|
||||
use easyremote_client_core::InputController;
|
||||
|
||||
// 获取屏幕缩放比例 (因为我们发送的是缩放后的帧)
|
||||
let scale = 2.0;
|
||||
// 获取当前分辨率设置以计算缩放比例
|
||||
let scale = {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(async {
|
||||
let settings = get_stream_settings().await;
|
||||
let s = settings.read().await;
|
||||
1.0 / s.resolution
|
||||
})
|
||||
};
|
||||
let actual_x = (x * scale) as i32;
|
||||
let actual_y = (y * scale) as i32;
|
||||
|
||||
@ -351,6 +400,17 @@ async fn connect_to_signal_server(device_id: String, server_url: String) -> Resu
|
||||
// 处理键盘事件
|
||||
handle_keyboard_input(&key, &event_type);
|
||||
}
|
||||
SignalMessage::StreamSettings { resolution, fps, quality, .. } => {
|
||||
// 更新流媒体设置
|
||||
tracing::info!("更新流媒体设置: resolution={}, fps={}, quality={}", resolution, fps, quality);
|
||||
tokio::spawn(async move {
|
||||
let settings = get_stream_settings().await;
|
||||
let mut s = settings.write().await;
|
||||
s.resolution = resolution;
|
||||
s.fps = fps;
|
||||
s.quality = quality;
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("收到信令消息: {:?}", msg);
|
||||
}
|
||||
@ -758,3 +818,125 @@ pub async fn save_config(
|
||||
config.save().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取开机启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_autostart() -> Result<bool, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
if let Ok(run_key) = hkcu.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run") {
|
||||
return Ok(run_key.get_value::<String, _>("EasyRemote").is_ok());
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let plist_path = dirs::home_dir()
|
||||
.map(|p| p.join("Library/LaunchAgents/com.easyremote.app.plist"))
|
||||
.unwrap_or_default();
|
||||
Ok(plist_path.exists())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let autostart_path = dirs::config_dir()
|
||||
.map(|p| p.join("autostart/easyremote.desktop"))
|
||||
.unwrap_or_default();
|
||||
Ok(autostart_path.exists())
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置开机启动
|
||||
#[tauri::command]
|
||||
pub fn set_autostart(enable: bool) -> Result<(), String> {
|
||||
let exe_path = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let run_key = hkcu
|
||||
.open_subkey_with_flags("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", KEY_WRITE)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if enable {
|
||||
run_key
|
||||
.set_value("EasyRemote", &exe_path.to_string_lossy().to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
let _ = run_key.delete_value("EasyRemote");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let plist_path = dirs::home_dir()
|
||||
.ok_or("找不到用户目录")?
|
||||
.join("Library/LaunchAgents/com.easyremote.app.plist");
|
||||
|
||||
if enable {
|
||||
let plist_content = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.easyremote.app</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>"#,
|
||||
exe_path.to_string_lossy()
|
||||
);
|
||||
|
||||
if let Some(parent) = plist_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
std::fs::write(&plist_path, plist_content).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&plist_path);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let autostart_dir = dirs::config_dir()
|
||||
.ok_or("找不到配置目录")?
|
||||
.join("autostart");
|
||||
let desktop_path = autostart_dir.join("easyremote.desktop");
|
||||
|
||||
if enable {
|
||||
std::fs::create_dir_all(&autostart_dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let desktop_content = format!(
|
||||
r#"[Desktop Entry]
|
||||
Type=Application
|
||||
Name=EasyRemote
|
||||
Exec={}
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
"#,
|
||||
exe_path.to_string_lossy()
|
||||
);
|
||||
|
||||
std::fs::write(&desktop_path, desktop_content).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&desktop_path);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,10 @@ mod state;
|
||||
|
||||
use state::AppState;
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
use tauri::{
|
||||
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
WindowEvent,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[cfg(windows)]
|
||||
@ -140,7 +143,45 @@ fn main() {
|
||||
// 创建应用状态
|
||||
let state = Arc::new(RwLock::new(AppState::new()));
|
||||
|
||||
// 创建系统托盘菜单
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("show", "显示主窗口"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "退出"));
|
||||
|
||||
let system_tray = SystemTray::new().with_menu(tray_menu);
|
||||
|
||||
tauri::Builder::default()
|
||||
.system_tray(system_tray)
|
||||
.on_system_tray_event(|app, event| match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
// 左键点击显示窗口
|
||||
if let Some(window) = app.get_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
})
|
||||
.on_window_event(|event| {
|
||||
// 关闭窗口时最小化到托盘而不是退出
|
||||
if let WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||
event.window().hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.setup(|app| {
|
||||
// 设置窗口标题以便单例检测
|
||||
if let Some(window) = app.get_window("main") {
|
||||
@ -189,6 +230,9 @@ fn main() {
|
||||
// 配置
|
||||
commands::get_config,
|
||||
commands::save_config,
|
||||
// 开机启动
|
||||
commands::get_autostart,
|
||||
commands::set_autostart,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@ -23,8 +23,18 @@
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"process": {
|
||||
"all": true
|
||||
},
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Utility",
|
||||
|
||||
@ -1,45 +1,68 @@
|
||||
/* EasyRemote 主题样式 */
|
||||
/* EasyRemote 主题样式 - 优化版 */
|
||||
|
||||
/* 导入字体 */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
/* 主色调 - 深蓝科技感 */
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-bg: rgba(59, 130, 246, 0.1);
|
||||
/* 主色调 - 渐变紫蓝 */
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--primary-light: #818cf8;
|
||||
--primary-bg: rgba(99, 102, 241, 0.12);
|
||||
--primary-glow: rgba(99, 102, 241, 0.4);
|
||||
|
||||
/* 背景色 */
|
||||
--bg-primary: #0a0f1a;
|
||||
--bg-secondary: #111827;
|
||||
--bg-tertiary: #1f2937;
|
||||
--bg-card: #161f2d;
|
||||
/* 强调色 */
|
||||
--accent: #8b5cf6;
|
||||
--accent-light: #a78bfa;
|
||||
|
||||
/* 背景色 - 深邃空间感 */
|
||||
--bg-primary: #0c0d12;
|
||||
--bg-secondary: #12141c;
|
||||
--bg-tertiary: #1a1d28;
|
||||
--bg-card: rgba(26, 29, 40, 0.8);
|
||||
--bg-card-hover: rgba(32, 36, 50, 0.9);
|
||||
--bg-glass: rgba(255, 255, 255, 0.03);
|
||||
|
||||
/* 文字色 */
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--text-dim: #475569;
|
||||
|
||||
/* 边框 */
|
||||
--border-color: #2d3748;
|
||||
--border-light: #4b5563;
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--border-light: rgba(255, 255, 255, 0.12);
|
||||
--border-glow: rgba(99, 102, 241, 0.3);
|
||||
|
||||
/* 状态色 */
|
||||
--success: #10b981;
|
||||
--success: #22c55e;
|
||||
--success-bg: rgba(34, 197, 94, 0.15);
|
||||
--warning: #f59e0b;
|
||||
--warning-bg: rgba(245, 158, 11, 0.15);
|
||||
--error: #ef4444;
|
||||
--error-bg: rgba(239, 68, 68, 0.15);
|
||||
--info: #06b6d4;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.6);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6);
|
||||
--shadow-glow: 0 0 40px var(--primary-glow);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
|
||||
/* 字体 */
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
|
||||
/* 动画 */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.25s ease;
|
||||
--transition-slow: 0.4s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
@ -55,12 +78,13 @@ html, body, #app {
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@ -68,12 +92,18 @@ html, body, #app {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
background: var(--border-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-light);
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 选择文本样式 */
|
||||
::selection {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
@ -81,14 +111,34 @@ html, body, #app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, var(--bg-primary) 0%, #050810 100%);
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(139, 92, 246, 0.1) 0%, transparent 40%),
|
||||
linear-gradient(180deg, var(--bg-primary) 0%, #08090d 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.app-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.015'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.app-header {
|
||||
padding: 14px 20px;
|
||||
background: rgba(22, 31, 45, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 16px 24px;
|
||||
background: rgba(12, 13, 18, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -100,29 +150,37 @@ html, body, #app {
|
||||
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.app-title .logo-icon {
|
||||
font-size: 22px;
|
||||
filter: drop-shadow(0 0 8px var(--primary-glow));
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
animation: fadeIn 0.25s ease;
|
||||
animation: contentIn 0.35s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@keyframes contentIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@ -133,52 +191,82 @@ html, body, #app {
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
transition: all var(--transition-normal);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--border-light);
|
||||
background: var(--bg-card-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.01em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(180deg, var(--primary) 0%, var(--accent) 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
margin-top: 6px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 开关 */
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 12px;
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--transition-normal);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.switch.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.switch::after {
|
||||
@ -186,21 +274,21 @@ html, body, #app {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--text-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--transition-normal);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.switch.active::after {
|
||||
left: 22px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.input-group {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@ -209,58 +297,78 @@ html, body, #app {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 14px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-bg);
|
||||
background: var(--bg-tertiary);
|
||||
box-shadow: 0 0 0 4px var(--primary-bg), 0 0 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--transition-normal);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px var(--primary-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px var(--primary-glow);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
@ -274,20 +382,24 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--border-color);
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
background: var(--bg-glass);
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
@ -295,37 +407,62 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 16px 32px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 8px var(--success);
|
||||
box-shadow: 0 0 12px var(--success);
|
||||
}
|
||||
|
||||
.status-dot.online::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
opacity: 0.3;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.3; }
|
||||
50% { transform: scale(1.5); opacity: 0; }
|
||||
}
|
||||
|
||||
.status-dot.offline {
|
||||
background: var(--text-muted);
|
||||
background: var(--text-dim);
|
||||
}
|
||||
|
||||
/* 历史记录列表 */
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 16px;
|
||||
background: var(--bg-glass);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 12px;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
@ -333,19 +470,21 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border-light);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.history-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
background: var(--primary-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, var(--primary-bg) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 14px;
|
||||
color: var(--primary);
|
||||
margin-right: 16px;
|
||||
color: var(--primary-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -355,7 +494,7 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.history-name {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
@ -364,41 +503,35 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.history-meta {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.history-action {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
background: none;
|
||||
border: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
@ -409,47 +542,215 @@ html, body, #app {
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
padding: 48px 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 20px 0;
|
||||
/* 设置列表 */
|
||||
.settings-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-item.clickable {
|
||||
cursor: pointer;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-md);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-item.clickable:hover {
|
||||
background: var(--bg-glass);
|
||||
}
|
||||
|
||||
.settings-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settings-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.settings-value {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-value.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* 流媒体设置面板 */
|
||||
.stream-settings-panel {
|
||||
background: var(--bg-glass);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stream-settings-panel .panel-title {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-item .input-label {
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 下拉选择框 */
|
||||
.select {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 32px 0 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.select:hover {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-bg);
|
||||
}
|
||||
|
||||
.select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 徽章 */
|
||||
.badge {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px var(--primary-glow);
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.connected {
|
||||
background: var(--success-bg);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-badge.connected::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--success);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--success);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-badge.disconnected {
|
||||
background: rgba(100, 116, 139, 0.15);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 480px) {
|
||||
.app-header {
|
||||
padding: 12px 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@ -457,17 +758,28 @@ html, body, #app {
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
[title] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -135,6 +135,16 @@ pub enum SignalMessage {
|
||||
key: String,
|
||||
event_type: String,
|
||||
},
|
||||
/// 流媒体设置
|
||||
#[serde(rename = "stream_settings")]
|
||||
StreamSettings {
|
||||
session_id: String,
|
||||
from_device: String,
|
||||
to_device: String,
|
||||
resolution: f64,
|
||||
fps: u32,
|
||||
quality: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// 信令WebSocket处理器
|
||||
@ -352,6 +362,11 @@ async fn handle_signal_message(state: &Arc<AppState>, device_id: &str, text: &st
|
||||
let _ = state.send_to_device(&to_device, text).await;
|
||||
}
|
||||
|
||||
// 转发流媒体设置到目标设备(客户端)
|
||||
SignalMessage::StreamSettings { to_device, .. } => {
|
||||
let _ = state.send_to_device(&to_device, text).await;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1752,6 +1752,42 @@
|
||||
let remoteWs = null;
|
||||
let selectedDevice = null;
|
||||
|
||||
// 获取流媒体设置
|
||||
function getStreamSettings() {
|
||||
return {
|
||||
resolution: parseFloat(document.getElementById('resolution-select').value),
|
||||
fps: parseInt(document.getElementById('fps-select').value),
|
||||
quality: parseInt(document.getElementById('quality-select').value)
|
||||
};
|
||||
}
|
||||
|
||||
// 发送设置更新
|
||||
function sendSettingsUpdate() {
|
||||
if (remoteWs && remoteWs.readyState === WebSocket.OPEN && selectedDevice) {
|
||||
const settings = getStreamSettings();
|
||||
remoteWs.send(JSON.stringify({
|
||||
type: 'stream_settings',
|
||||
session_id: '',
|
||||
from_device: 'browser',
|
||||
to_device: selectedDevice.id,
|
||||
resolution: settings.resolution,
|
||||
fps: settings.fps,
|
||||
quality: settings.quality
|
||||
}));
|
||||
showToast('设置已更新', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听设置变化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
['resolution-select', 'fps-select', 'quality-select'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', sendSettingsUpdate);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载在线设备(用于远程控制)
|
||||
async function loadRemoteDevices() {
|
||||
try {
|
||||
|
||||
@ -447,6 +447,63 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 流媒体设置 */
|
||||
.stream-settings {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex: 1;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.setting-item .form-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 32px 0 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.setting-select:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.setting-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.setting-select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
@ -1159,6 +1216,38 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-info" id="selected-device-info"></div>
|
||||
|
||||
<!-- 画质设置 -->
|
||||
<div class="stream-settings">
|
||||
<div class="setting-item">
|
||||
<label class="form-label">分辨率</label>
|
||||
<select class="setting-select" id="resolution-select">
|
||||
<option value="0.25">25% (流畅)</option>
|
||||
<option value="0.5" selected>50% (平衡)</option>
|
||||
<option value="0.75">75% (高清)</option>
|
||||
<option value="1">100% (原画)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="form-label">帧率</label>
|
||||
<select class="setting-select" id="fps-select">
|
||||
<option value="5">5 FPS (省流)</option>
|
||||
<option value="10">10 FPS (流畅)</option>
|
||||
<option value="15" selected>15 FPS (平衡)</option>
|
||||
<option value="24">24 FPS (高帧)</option>
|
||||
<option value="30">30 FPS (极限)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="form-label">画质</label>
|
||||
<select class="setting-select" id="quality-select">
|
||||
<option value="30">低 (省流)</option>
|
||||
<option value="50">中 (平衡)</option>
|
||||
<option value="70" selected>高 (清晰)</option>
|
||||
<option value="85">极高 (高清)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1663,6 +1752,42 @@
|
||||
let remoteWs = null;
|
||||
let selectedDevice = null;
|
||||
|
||||
// 获取流媒体设置
|
||||
function getStreamSettings() {
|
||||
return {
|
||||
resolution: parseFloat(document.getElementById('resolution-select').value),
|
||||
fps: parseInt(document.getElementById('fps-select').value),
|
||||
quality: parseInt(document.getElementById('quality-select').value)
|
||||
};
|
||||
}
|
||||
|
||||
// 发送设置更新
|
||||
function sendSettingsUpdate() {
|
||||
if (remoteWs && remoteWs.readyState === WebSocket.OPEN && selectedDevice) {
|
||||
const settings = getStreamSettings();
|
||||
remoteWs.send(JSON.stringify({
|
||||
type: 'stream_settings',
|
||||
session_id: '',
|
||||
from_device: 'browser',
|
||||
to_device: selectedDevice.id,
|
||||
resolution: settings.resolution,
|
||||
fps: settings.fps,
|
||||
quality: settings.quality
|
||||
}));
|
||||
showToast('设置已更新', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听设置变化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
['resolution-select', 'fps-select', 'quality-select'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', sendSettingsUpdate);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载在线设备(用于远程控制)
|
||||
async function loadRemoteDevices() {
|
||||
try {
|
||||
@ -1754,6 +1879,18 @@
|
||||
remoteStatus.innerHTML = '<span class="status online"><span class="status-dot"></span>已连接</span>';
|
||||
toolbar.classList.remove('hidden');
|
||||
|
||||
// 发送流媒体设置
|
||||
const settings = getStreamSettings();
|
||||
remoteWs.send(JSON.stringify({
|
||||
type: 'stream_settings',
|
||||
session_id: '',
|
||||
from_device: 'browser',
|
||||
to_device: selectedDevice.id,
|
||||
resolution: settings.resolution,
|
||||
fps: settings.fps,
|
||||
quality: settings.quality
|
||||
}));
|
||||
|
||||
// 显示连接成功信息
|
||||
remoteScreen.innerHTML = `
|
||||
<div class="remote-placeholder">
|
||||
@ -1842,7 +1979,7 @@
|
||||
function handleRemoteFrame(data) {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
|
||||
if (msg.type === 'screen_frame') {
|
||||
displayFrame(msg.width, msg.height, msg.data);
|
||||
} else if (msg.type === 'error') {
|
||||
@ -1865,7 +2002,7 @@
|
||||
let frameCtx = null;
|
||||
function displayFrame(width, height, base64Data) {
|
||||
const remoteScreen = document.getElementById('remote-screen');
|
||||
|
||||
|
||||
// 初始化 canvas
|
||||
if (!frameCanvas) {
|
||||
remoteScreen.innerHTML = '';
|
||||
@ -1875,7 +2012,7 @@
|
||||
frameCanvas.style.objectFit = 'contain';
|
||||
remoteScreen.appendChild(frameCanvas);
|
||||
frameCtx = frameCanvas.getContext('2d');
|
||||
|
||||
|
||||
// 添加鼠标事件监听
|
||||
frameCanvas.addEventListener('mousemove', handleMouseMove);
|
||||
frameCanvas.addEventListener('mousedown', handleMouseDown);
|
||||
@ -1883,22 +2020,22 @@
|
||||
frameCanvas.addEventListener('click', handleMouseClick);
|
||||
frameCanvas.addEventListener('wheel', handleMouseWheel);
|
||||
frameCanvas.addEventListener('contextmenu', e => e.preventDefault());
|
||||
|
||||
|
||||
// 添加键盘事件监听
|
||||
frameCanvas.tabIndex = 1;
|
||||
frameCanvas.addEventListener('keydown', handleKeyDown);
|
||||
frameCanvas.addEventListener('keyup', handleKeyUp);
|
||||
}
|
||||
|
||||
|
||||
// 设置 canvas 尺寸
|
||||
if (frameCanvas.width !== width || frameCanvas.height !== height) {
|
||||
frameCanvas.width = width;
|
||||
frameCanvas.height = height;
|
||||
}
|
||||
|
||||
|
||||
// 解码并绘制图像
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
img.onload = function () {
|
||||
frameCtx.drawImage(img, 0, 0);
|
||||
};
|
||||
img.src = 'data:image/jpeg;base64,' + base64Data;
|
||||
|
||||