Add stream settings feature with resolution, fps, and quality adjustments; enhance system tray functionality; update dependencies and UI styles

This commit is contained in:
Ethanfly 2026-01-04 18:53:51 +08:00
parent cc6054b3f7
commit 8e6862dcb6
32 changed files with 2086 additions and 503 deletions

View File

@ -118,6 +118,16 @@ pub enum SignalMessage {
key: String, key: String,
event_type: String, // "down", "up" 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,
},
} }
/// 信令客户端 /// 信令客户端

View File

@ -11,7 +11,7 @@ easyremote-common = { path = "../common" }
easyremote-client-core = { path = "../client-core" } easyremote-client-core = { path = "../client-core" }
# Tauri # 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 # Windows single instance
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
@ -21,6 +21,7 @@ windows = { version = "0.58", features = [
"Win32_Security", "Win32_Security",
"Win32_UI_WindowsAndMessaging" "Win32_UI_WindowsAndMessaging"
] } ] }
winreg = "0.52"
# Async # Async
tokio = { workspace = true } tokio = { workspace = true }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View 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

View 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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

589
crates/client-tauri/package-lock.json generated Normal file
View 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
}
}
}

View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"sharp": "^0.34.5"
}
}

View File

@ -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(); 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 { struct ActiveScreenSession {
session_id: String, 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 signal_client_clone = signal_client.clone();
let active_session_clone = active_session.clone(); let active_session_clone = active_session.clone();
let stream_settings = get_stream_settings().await.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
// 创建屏幕捕获器 // 创建屏幕捕获器
let mut capturer = match ScreenCapturer::new(0) { 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(); let start_time = std::time::Instant::now();
loop { 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 should_stop = {
let rt = tokio::runtime::Handle::current(); 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 jpeg_data = Vec::new();
let mut cursor = std::io::Cursor::new(&mut jpeg_data); 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() { if scaled.write_to(&mut cursor, image::ImageFormat::Jpeg).is_ok() {
let jpeg_len = jpeg_data.len(); 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 session_id_send = session_id_capture.clone();
let controller_device_send = controller_device_capture.clone(); let controller_device_send = controller_device_capture.clone();
let signal_client_send = signal_client_clone.clone(); let signal_client_send = signal_client_clone.clone();
let scaled_width = width / 2;
let scaled_height = height / 2;
rt.block_on(async move { rt.block_on(async move {
let client = signal_client_send.read().await; 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; frame_count += 1;
if frame_count % 30 == 0 { if frame_count % 30 == 0 {
let fps = frame_count as f64 / start_time.elapsed().as_secs_f64(); let actual_fps = frame_count as f64 / start_time.elapsed().as_secs_f64();
tracing::debug!("屏幕流帧率: {:.1} fps, 帧大小: {} KB", fps, jpeg_len / 1024); 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>) { fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option<u8>, delta: Option<f64>) {
use easyremote_client_core::InputController; 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_x = (x * scale) as i32;
let actual_y = (y * 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); 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); tracing::debug!("收到信令消息: {:?}", msg);
} }
@ -758,3 +818,125 @@ pub async fn save_config(
config.save().map_err(|e| e.to_string())?; config.save().map_err(|e| e.to_string())?;
Ok(()) 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(());
}
}

View File

@ -8,7 +8,10 @@ mod state;
use state::AppState; use state::AppState;
use std::sync::Arc; use std::sync::Arc;
use tauri::Manager; use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
WindowEvent,
};
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[cfg(windows)] #[cfg(windows)]
@ -140,7 +143,45 @@ fn main() {
// 创建应用状态 // 创建应用状态
let state = Arc::new(RwLock::new(AppState::new())); 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() 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| { .setup(|app| {
// 设置窗口标题以便单例检测 // 设置窗口标题以便单例检测
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
@ -189,6 +230,9 @@ fn main() {
// 配置 // 配置
commands::get_config, commands::get_config,
commands::save_config, commands::save_config,
// 开机启动
commands::get_autostart,
commands::set_autostart,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -23,8 +23,18 @@
}, },
"clipboard": { "clipboard": {
"all": true "all": true
},
"process": {
"all": true
},
"globalShortcut": {
"all": true
} }
}, },
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"bundle": { "bundle": {
"active": true, "active": true,
"category": "Utility", "category": "Utility",

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,68 @@
/* EasyRemote 主题样式 */ /* EasyRemote 主题样式 - 优化版 */
:root {
/* 主色调 - 深蓝科技感 */
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: #60a5fa;
--primary-bg: rgba(59, 130, 246, 0.1);
/* 背景色 */ /* 导入字体 */
--bg-primary: #0a0f1a; @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
--bg-secondary: #111827;
--bg-tertiary: #1f2937; :root {
--bg-card: #161f2d; /* 主色调 - 渐变紫蓝 */
--primary: #6366f1;
--primary-hover: #4f46e5;
--primary-light: #818cf8;
--primary-bg: rgba(99, 102, 241, 0.12);
--primary-glow: rgba(99, 102, 241, 0.4);
/* 强调色 */
--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-primary: #f8fafc;
--text-secondary: #9ca3af; --text-secondary: #94a3b8;
--text-muted: #6b7280; --text-muted: #64748b;
--text-dim: #475569;
/* 边框 */ /* 边框 */
--border-color: #2d3748; --border-color: rgba(255, 255, 255, 0.08);
--border-light: #4b5563; --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: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.15);
--error: #ef4444; --error: #ef4444;
--error-bg: rgba(239, 68, 68, 0.15);
--info: #06b6d4; --info: #06b6d4;
/* 阴影 */ /* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.6); --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6);
--shadow-glow: 0 0 40px var(--primary-glow);
/* 圆角 */ /* 圆角 */
--radius-sm: 6px; --radius-sm: 8px;
--radius-md: 10px; --radius-md: 12px;
--radius-lg: 14px; --radius-lg: 16px;
--radius-xl: 20px;
/* 字体 */ /* 字体 */
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace; --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; --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); color: var(--text-primary);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
} }
/* 滚动条样式 */ /* 滚动条样式 */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 8px;
height: 6px; height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@ -68,12 +92,18 @@ html, body, #app {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--border-color); background: var(--border-light);
border-radius: 3px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-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; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; 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 { .app-header {
padding: 14px 20px; padding: 16px 24px;
background: rgba(22, 31, 45, 0.9); background: rgba(12, 13, 18, 0.8);
backdrop-filter: blur(12px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -100,29 +150,37 @@ html, body, #app {
.app-title { .app-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
display: flex; display: flex;
align-items: center; 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 { .main-content {
flex: 1; flex: 1;
padding: 20px; padding: 24px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
position: relative;
z-index: 1;
} }
.tab-content { .tab-content {
animation: fadeIn 0.25s ease; animation: contentIn 0.35s ease;
} }
@keyframes fadeIn { @keyframes contentIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(12px);
} }
to { to {
opacity: 1; opacity: 1;
@ -133,52 +191,82 @@ html, body, #app {
/* 卡片 */ /* 卡片 */
.card { .card {
background: var(--bg-card); background: var(--bg-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 20px; padding: 24px;
margin-bottom: 16px; margin-bottom: 20px;
transition: all 0.2s ease; 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 { .card:hover {
border-color: var(--border-light); border-color: var(--border-light);
background: var(--bg-card-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
} }
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 24px;
} }
.card-title { .card-title {
font-size: 15px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); 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 { .card-subtitle {
font-size: 12px; font-size: 13px;
color: var(--text-secondary); color: var(--text-secondary);
margin-top: 4px; margin-top: 6px;
line-height: 1.5; line-height: 1.6;
} }
/* 开关 */ /* 开关 */
.switch { .switch {
position: relative; position: relative;
width: 44px; width: 48px;
height: 24px; height: 26px;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-radius: 12px; border-radius: 13px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all var(--transition-normal);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
} }
.switch.active { .switch.active {
background: var(--primary); background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
border-color: var(--primary); border-color: transparent;
box-shadow: 0 0 20px var(--primary-glow);
} }
.switch::after { .switch::after {
@ -186,21 +274,21 @@ html, body, #app {
position: absolute; position: absolute;
top: 2px; top: 2px;
left: 2px; left: 2px;
width: 18px; width: 20px;
height: 18px; height: 20px;
background: var(--text-primary); background: white;
border-radius: 50%; border-radius: 50%;
transition: all 0.2s ease; transition: all var(--transition-normal);
box-shadow: var(--shadow-sm); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
} }
.switch.active::after { .switch.active::after {
left: 22px; left: 24px;
} }
/* 输入框 */ /* 输入框 */
.input-group { .input-group {
margin-bottom: 16px; margin-bottom: 20px;
} }
.input-label { .input-label {
@ -209,58 +297,78 @@ html, body, #app {
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 500; font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.input { .input {
width: 100%; width: 100%;
padding: 12px 14px; padding: 14px 16px;
background: var(--bg-tertiary); background: var(--bg-secondary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-md); border-radius: var(--radius-md);
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
transition: all 0.2s ease; transition: all var(--transition-normal);
} }
.input:focus { .input:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .input::placeholder {
color: var(--text-muted); color: var(--text-dim);
} }
/* 按钮 */ /* 按钮 */
.btn { .btn {
padding: 10px 20px; padding: 12px 24px;
border: none; border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all var(--transition-normal);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; 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 { .btn:disabled {
opacity: 0.6; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
.btn-primary { .btn-primary {
background: var(--primary); background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
color: white; color: white;
box-shadow: 0 4px 16px var(--primary-glow);
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
background: var(--primary-hover); transform: translateY(-2px);
transform: translateY(-1px); box-shadow: 0 8px 24px var(--primary-glow);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
} }
.btn-primary:active:not(:disabled) { .btn-primary:active:not(:disabled) {
@ -274,20 +382,24 @@ html, body, #app {
} }
.btn-secondary:hover:not(:disabled) { .btn-secondary:hover:not(:disabled) {
background: var(--border-color); background: var(--bg-card-hover);
border-color: var(--border-light); border-color: var(--border-light);
} }
.btn-icon { .btn-icon {
padding: 8px; width: 40px;
background: transparent; height: 40px;
padding: 0;
background: var(--bg-glass);
color: var(--text-secondary); color: var(--text-secondary);
border-radius: var(--radius-sm); border-radius: var(--radius-md);
border: 1px solid var(--border-color);
} }
.btn-icon:hover { .btn-icon:hover {
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-primary); color: var(--text-primary);
border-color: var(--border-light);
} }
.btn-block { .btn-block {
@ -295,37 +407,62 @@ html, body, #app {
} }
.btn-sm { .btn-sm {
padding: 6px 12px; padding: 8px 16px;
font-size: 12px; font-size: 13px;
}
.btn-lg {
padding: 16px 32px;
font-size: 15px;
} }
/* 状态指示器 */ /* 状态指示器 */
.status-dot { .status-dot {
width: 8px; width: 10px;
height: 8px; height: 10px;
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
position: relative;
} }
.status-dot.online { .status-dot.online {
background: var(--success); 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 { .status-dot.offline {
background: var(--text-muted); background: var(--text-dim);
} }
/* 历史记录列表 */ /* 历史记录列表 */
.history-item { .history-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 14px; padding: 16px;
background: var(--bg-tertiary); background: var(--bg-glass);
border: 1px solid var(--border-color);
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: 10px; margin-bottom: 12px;
transition: all 0.2s ease; transition: all var(--transition-normal);
} }
.history-item:last-child { .history-item:last-child {
@ -333,19 +470,21 @@ html, body, #app {
} }
.history-item:hover { .history-item:hover {
background: var(--border-color); background: var(--bg-tertiary);
border-color: var(--border-light);
transform: translateX(4px);
} }
.history-icon { .history-icon {
width: 38px; width: 44px;
height: 38px; height: 44px;
background: var(--primary-bg); background: linear-gradient(135deg, var(--primary-bg) 0%, rgba(139, 92, 246, 0.1) 100%);
border-radius: var(--radius-sm); border-radius: var(--radius-md);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 14px; margin-right: 16px;
color: var(--primary); color: var(--primary-light);
flex-shrink: 0; flex-shrink: 0;
} }
@ -355,7 +494,7 @@ html, body, #app {
} }
.history-name { .history-name {
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
@ -364,41 +503,35 @@ html, body, #app {
} }
.history-meta { .history-meta {
font-size: 11px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
margin-top: 3px; margin-top: 4px;
}
.history-action {
color: var(--primary);
cursor: pointer;
font-size: 13px;
background: none;
border: none;
} }
/* 用户菜单 */ /* 用户菜单 */
.user-menu { .user-menu {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
} }
.user-avatar { .user-avatar {
width: 32px; width: 36px;
height: 32px; height: 36px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 13px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: white; color: white;
box-shadow: 0 4px 12px var(--primary-glow);
} }
.user-name { .user-name {
font-size: 13px; font-size: 14px;
font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
max-width: 100px; max-width: 100px;
overflow: hidden; overflow: hidden;
@ -409,47 +542,215 @@ html, body, #app {
/* 空状态 */ /* 空状态 */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 40px 20px; padding: 48px 24px;
color: var(--text-secondary); color: var(--text-secondary);
} }
.empty-icon { .empty-icon {
font-size: 40px; font-size: 48px;
margin-bottom: 12px; margin-bottom: 16px;
opacity: 0.6; opacity: 0.6;
filter: grayscale(0.3);
} }
.empty-text { .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 { .loading-spinner {
width: 20px; width: 20px;
height: 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-top-color: white;
border-radius: 50%; border-radius: 50%;
animation: spin 0.7s linear infinite; animation: spin 0.8s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to { transform: rotate(360deg); }
transform: rotate(360deg);
}
} }
/* 分割线 */ /* 设置列表 */
.divider { .settings-item {
height: 1px; display: flex;
background: var(--border-color); justify-content: space-between;
margin: 20px 0; 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) { @media (max-width: 480px) {
.app-header { .app-header {
padding: 12px 16px; padding: 14px 16px;
} }
.main-content { .main-content {
@ -457,17 +758,28 @@ html, body, #app {
} }
.card { .card {
padding: 16px; padding: 20px;
margin-bottom: 16px;
}
.settings-row {
flex-direction: column;
gap: 12px;
} }
} }
/* 动画 */ /* 动画 */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.2s ease; transition: opacity var(--transition-normal);
} }
.fade-enter-from, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
/* Tooltip */
[title] {
position: relative;
}

View File

@ -135,6 +135,16 @@ pub enum SignalMessage {
key: String, key: String,
event_type: 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处理器 /// 信令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; let _ = state.send_to_device(&to_device, text).await;
} }
// 转发流媒体设置到目标设备(客户端)
SignalMessage::StreamSettings { to_device, .. } => {
let _ = state.send_to_device(&to_device, text).await;
}
_ => {} _ => {}
} }
} }

View File

@ -1752,6 +1752,42 @@
let remoteWs = null; let remoteWs = null;
let selectedDevice = 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() { async function loadRemoteDevices() {
try { try {

View File

@ -447,6 +447,63 @@
font-weight: 500; 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 { .btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
@ -1159,6 +1216,38 @@
</div> </div>
</div> </div>
<div class="device-info" id="selected-device-info"></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>
</div> </div>
@ -1663,6 +1752,42 @@
let remoteWs = null; let remoteWs = null;
let selectedDevice = 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() { async function loadRemoteDevices() {
try { try {
@ -1754,6 +1879,18 @@
remoteStatus.innerHTML = '<span class="status online"><span class="status-dot"></span>已连接</span>'; remoteStatus.innerHTML = '<span class="status online"><span class="status-dot"></span>已连接</span>';
toolbar.classList.remove('hidden'); 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 = ` remoteScreen.innerHTML = `
<div class="remote-placeholder"> <div class="remote-placeholder">
@ -1898,7 +2035,7 @@
// 解码并绘制图像 // 解码并绘制图像
const img = new Image(); const img = new Image();
img.onload = function() { img.onload = function () {
frameCtx.drawImage(img, 0, 0); frameCtx.drawImage(img, 0, 0);
}; };
img.src = 'data:image/jpeg;base64,' + base64Data; img.src = 'data:image/jpeg;base64,' + base64Data;