diff --git a/api/package.json b/api/package.json index f71d0a5c..9638712c 100644 --- a/api/package.json +++ b/api/package.json @@ -39,6 +39,7 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", + "ws": "^8.18.2", "youtubei.js": "^13.4.0", "zod": "^3.23.8" }, diff --git a/api/src/core/api.js b/api/src/core/api.js index eb5cf4ff..2cffcca6 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -19,6 +19,7 @@ import { friendlyServiceName } from "../processing/service-alias.js"; import { verifyStream } from "../stream/manage.js"; import { createResponse, normalizeRequest, getIP } from "../processing/request.js"; import { setupTunnelHandler } from "./itunnel.js"; +import { setupSignalingServer } from "./signaling.js"; import * as APIKeys from "../security/api-keys.js"; import * as Cookies from "../processing/cookie/manager.js"; @@ -309,8 +310,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { streamInfo.range = req.headers['range']; } - return stream(res, streamInfo); - }); + return stream(res, streamInfo); }); app.get('/', (_, res) => { res.type('json'); @@ -337,7 +337,12 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { setGlobalDispatcher(new ProxyAgent(env.externalProxy)) } - http.createServer(app).listen({ + const server = http.createServer(app); + + // 设置WebSocket信令服务器 + setupSignalingServer(server); + + server.listen({ port: env.apiPort, host: env.listenAddress, reusePort: env.instanceCount > 1 || undefined diff --git a/api/src/core/signaling.js b/api/src/core/signaling.js new file mode 100644 index 00000000..c5a5ecd4 --- /dev/null +++ b/api/src/core/signaling.js @@ -0,0 +1,180 @@ +import { WebSocketServer } from 'ws'; +import { Green } from '../misc/console-text.js'; + +export const setupSignalingServer = (httpServer) => { + const wss = new WebSocketServer({ + server: httpServer, + path: '/ws' + }); + + const sessions = new Map(); // sessionId -> { creator, joiner, createdAt } + + // 清理过期会话 + setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of sessions.entries()) { + if (now - session.createdAt > 30 * 60 * 1000) { // 30分钟过期 + sessions.delete(sessionId); + console.log(`清理过期会话: ${sessionId}`); + } + } + }, 5 * 60 * 1000); // 每5分钟检查一次 + + wss.on('connection', (ws, req) => { + console.log('WebSocket连接建立:', req.socket.remoteAddress); + + let sessionId = null; + let userRole = null; // 'creator' | 'joiner' + + ws.on('message', (data) => { + try { + const message = JSON.parse(data); + + switch (message.type) { + case 'create_session': + handleCreateSession(ws, message); + break; + case 'join_session': + handleJoinSession(ws, message); + break; + case 'offer': + case 'answer': + case 'ice_candidate': + handleSignaling(ws, message, sessionId, userRole); + break; + case 'disconnect': + handleDisconnect(sessionId, userRole); + break; + default: + ws.send(JSON.stringify({ + type: 'error', + message: '未知消息类型' + })); + } + } catch (error) { + console.error('WebSocket消息处理错误:', error); + ws.send(JSON.stringify({ + type: 'error', + message: '消息格式错误' + })); + } + }); + + ws.on('close', () => { + console.log('WebSocket连接关闭'); + if (sessionId && userRole) { + handleDisconnect(sessionId, userRole); + } + }); + + ws.on('error', (error) => { + console.error('WebSocket错误:', error); + }); + + function handleCreateSession(ws, message) { + sessionId = Math.random().toString(36).substring(2, 10); + userRole = 'creator'; + + sessions.set(sessionId, { + creator: { ws, publicKey: message.publicKey }, + joiner: null, + createdAt: Date.now() + }); + + ws.send(JSON.stringify({ + type: 'session_created', + sessionId: sessionId + })); + + console.log(`会话创建: ${sessionId}`); + } + + function handleJoinSession(ws, message) { + const session = sessions.get(message.sessionId); + + if (!session) { + ws.send(JSON.stringify({ + type: 'error', + message: '会话不存在或已过期' + })); + return; + } + + if (session.joiner) { + ws.send(JSON.stringify({ + type: 'error', + message: '会话已满' + })); + return; + } + + sessionId = message.sessionId; + userRole = 'joiner'; + + session.joiner = { ws, publicKey: message.publicKey }; + + // 通知创建者有人加入,并交换公钥 + if (session.creator && session.creator.ws.readyState === ws.OPEN) { + session.creator.ws.send(JSON.stringify({ + type: 'peer_joined', + publicKey: message.publicKey + })); + } + + // 回复加入者创建者的公钥 + ws.send(JSON.stringify({ + type: 'session_joined', + publicKey: session.creator.publicKey + })); + + console.log(`用户加入会话: ${sessionId}`); + } + + function handleSignaling(ws, message, sessionId, userRole) { + if (!sessionId || !userRole) { + ws.send(JSON.stringify({ + type: 'error', + message: '未加入会话' + })); + return; + } + + const session = sessions.get(sessionId); + if (!session) { + ws.send(JSON.stringify({ + type: 'error', + message: '会话不存在' + })); + return; + } + + // 转发信令消息给对端 + const peer = userRole === 'creator' ? session.joiner : session.creator; + if (peer && peer.ws.readyState === ws.OPEN) { + peer.ws.send(JSON.stringify(message)); + } + } + + function handleDisconnect(sessionId, userRole) { + if (!sessionId) return; + + const session = sessions.get(sessionId); + if (!session) return; + + // 通知对端连接断开 + const peer = userRole === 'creator' ? session.joiner : session.creator; + if (peer && peer.ws.readyState === ws.OPEN) { + peer.ws.send(JSON.stringify({ + type: 'peer_disconnected' + })); + } + + // 清理会话 + sessions.delete(sessionId); + console.log(`会话结束: ${sessionId}`); + } + }); + + console.log(`${Green('[✓]')} WebSocket信令服务器启动成功`); + return wss; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1c0070d..95deb0a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: url-pattern: specifier: 1.0.3 version: 1.0.3 + ws: + specifier: ^8.18.2 + version: 8.18.2 youtubei.js: specifier: ^13.4.0 version: 13.4.0 @@ -90,6 +93,10 @@ importers: packages/version-info: {} web: + dependencies: + qrcode: + specifier: ^1.5.4 + version: 1.5.4 devDependencies: '@eslint/js': specifier: ^9.5.0 @@ -130,6 +137,9 @@ importers: '@types/node': specifier: ^20.14.10 version: 20.14.14 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 '@vitejs/plugin-basic-ssl': specifier: ^1.1.0 version: 1.1.0(vite@5.3.5(@types/node@20.14.14)) @@ -759,6 +769,9 @@ packages: '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -940,6 +953,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -951,6 +968,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -1034,6 +1054,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1064,6 +1088,9 @@ packages: devalue@5.0.0: resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1139,6 +1166,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true esm-env@1.0.0: @@ -1217,6 +1245,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1258,6 +1290,10 @@ packages: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} engines: {node: '>= 4'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -1462,6 +1498,10 @@ packages: locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1620,14 +1660,26 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} @@ -1679,6 +1731,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -1729,6 +1785,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -1761,6 +1822,13 @@ packages: redis@4.7.0: resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1817,6 +1885,9 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} @@ -2185,6 +2256,9 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2194,6 +2268,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2205,9 +2283,32 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2640,6 +2741,10 @@ snapshots: '@types/pug@2.0.10': {} + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 20.14.14 + '@types/unist@2.0.10': {} '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': @@ -2839,6 +2944,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + caseless@0.12.0: {} chalk@4.1.2: @@ -2858,6 +2965,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cluster-key-slot@1.1.2: optional: true @@ -2928,6 +3041,8 @@ snapshots: dependencies: ms: 2.1.2 + decamelize@1.2.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -2948,6 +3063,8 @@ snapshots: devalue@5.0.0: {} + dijkstrajs@1.0.3: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -3212,6 +3329,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3251,6 +3373,8 @@ snapshots: generic-pool@3.9.0: optional: true + get-caller-file@2.0.5: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -3443,6 +3567,10 @@ snapshots: locate-character@3.0.0: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -3567,14 +3695,24 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.0: {} parent-module@1.0.1: @@ -3612,6 +3750,8 @@ snapshots: pirates@4.0.6: {} + pngjs@5.0.0: {} + postcss-load-config@6.0.1(postcss@8.4.40): dependencies: lilconfig: 3.1.2 @@ -3641,6 +3781,12 @@ snapshots: punycode@2.3.1: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.13.0: dependencies: side-channel: 1.0.6 @@ -3681,6 +3827,10 @@ snapshots: '@redis/time-series': 1.1.0(@redis/client@1.6.0) optional: true + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -3765,6 +3915,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + set-cookie-parser@2.6.0: {} set-function-length@1.2.2: @@ -4077,12 +4229,20 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4097,9 +4257,32 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.2: {} + + y18n@4.0.3: {} + yallist@4.0.0: optional: true + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yocto-queue@0.1.0: {} youtubei.js@13.4.0: diff --git a/web/i18n/en/clipboard.json b/web/i18n/en/clipboard.json new file mode 100644 index 00000000..14f231bd --- /dev/null +++ b/web/i18n/en/clipboard.json @@ -0,0 +1,33 @@ +{ + "title": "Clipboard Share", + "description": "Share text and files securely between devices using end-to-end encryption", + "create_session": "Create Session", + "create_description": "Create a new sharing session and get a code", + "create": "Create", + "creating": "Creating...", + "join_session": "Join Session", + "join_description": "Enter a session code to join an existing session", + "join": "Join", + "joining": "Joining...", + "enter_code": "Enter session code", + "session_active": "Session Active", + "session_id": "Session ID", + "copy_link": "Copy sharing link", + "scan_qr": "Scan QR code to join from another device", + "peer_connected": "Device connected", + "waiting_peer": "Waiting for device to connect", + "send_text": "Send Text", + "enter_text": "Enter text to share", + "send": "Send", + "sending": "Sending...", + "download": "Download", + "receiving": "Receiving", + "received_files": "Received Files", + "disconnect": "Disconnect", + "error": { + "session_not_found": "Session not found or expired", + "session_full": "Session is full", + "connection_failed": "Connection failed", + "encryption_failed": "Encryption failed" + } +} diff --git a/web/i18n/en/general.json b/web/i18n/en/general.json index 02b454ee..97df2c84 100644 --- a/web/i18n/en/general.json +++ b/web/i18n/en/general.json @@ -2,6 +2,7 @@ "cobalt": "bamboo download, Download Videos for Free - YouTube, TikTok, Bilibili, Instagram, Facebook, Twitter", "meowbalt": "bamboo download", "beta": "beta", + "or": "or", "embed.description": "Download videos for free from YouTube, TikTok, Bilibili, Instagram, Facebook, and Twitter. No registration needed. Easy and fast video downloads.", "guide": { "title": "Freesavevideo.online Online Video Downloader Guide", diff --git a/web/i18n/en/tabs.json b/web/i18n/en/tabs.json index c0c17cc6..d7dc698f 100644 --- a/web/i18n/en/tabs.json +++ b/web/i18n/en/tabs.json @@ -1,5 +1,6 @@ { "save": "index", + "clipboard": "clipboard", "settings": "settings", "updates": "updates", "donate": "donate", diff --git a/web/i18n/zh/clipboard.json b/web/i18n/zh/clipboard.json new file mode 100644 index 00000000..ce439c15 --- /dev/null +++ b/web/i18n/zh/clipboard.json @@ -0,0 +1,33 @@ +{ + "title": "剪贴板分享", + "description": "使用端到端加密在设备间安全分享文本和文件", + "create_session": "创建会话", + "create_description": "创建新的分享会话并获取分享码", + "create": "创建", + "creating": "创建中...", + "join_session": "加入会话", + "join_description": "输入会话码加入现有会话", + "join": "加入", + "joining": "加入中...", + "enter_code": "输入会话码", + "session_active": "会话已激活", + "session_id": "会话ID", + "copy_link": "复制分享链接", + "scan_qr": "扫描二维码从其他设备加入", + "peer_connected": "设备已连接", + "waiting_peer": "等待设备连接", + "send_text": "发送文本", + "enter_text": "输入要分享的文本", + "send": "发送", + "sending": "发送中...", + "download": "下载", + "receiving": "接收中", + "received_files": "已接收文件", + "disconnect": "断开连接", + "error": { + "session_not_found": "会话不存在或已过期", + "session_full": "会话已满", + "connection_failed": "连接失败", + "encryption_failed": "加密失败" + } +} diff --git a/web/i18n/zh/tabs.json b/web/i18n/zh/tabs.json index e07033ef..b414c997 100644 --- a/web/i18n/zh/tabs.json +++ b/web/i18n/zh/tabs.json @@ -1,5 +1,6 @@ { "save": "首页", + "clipboard": "剪贴板", "settings": "设置", "updates": "更新", "donate": "捐赠", diff --git a/web/package.json b/web/package.json index 69de03ca..8eaa60c1 100644 --- a/web/package.json +++ b/web/package.json @@ -37,6 +37,7 @@ "@types/eslint__js": "^8.42.3", "@types/fluent-ffmpeg": "^2.1.25", "@types/node": "^20.14.10", + "@types/qrcode": "^1.5.5", "@vitejs/plugin-basic-ssl": "^1.1.0", "compare-versions": "^6.1.0", "dotenv": "^16.0.1", @@ -54,5 +55,8 @@ "typescript": "^5.4.5", "typescript-eslint": "^7.13.1", "vite": "^5.0.3" + }, + "dependencies": { + "qrcode": "^1.5.4" } } diff --git a/web/src/components/buttons/ActionButton.svelte b/web/src/components/buttons/ActionButton.svelte index 8eaa5b47..b013595d 100644 --- a/web/src/components/buttons/ActionButton.svelte +++ b/web/src/components/buttons/ActionButton.svelte @@ -3,8 +3,9 @@ export let click = () => { alert("no function assigned"); }; + export let disabled: boolean = false; - diff --git a/web/src/components/sidebar/Sidebar.svelte b/web/src/components/sidebar/Sidebar.svelte index 7f07222d..fe878702 100644 --- a/web/src/components/sidebar/Sidebar.svelte +++ b/web/src/components/sidebar/Sidebar.svelte @@ -3,10 +3,9 @@ import { defaultNavPage } from "$lib/subnav"; import CobaltLogo from "$components/sidebar/CobaltLogo.svelte"; - import SidebarTab from "$components/sidebar/SidebarTab.svelte"; - - import IconDownload from "@tabler/icons-svelte/IconDownload.svelte"; + import SidebarTab from "$components/sidebar/SidebarTab.svelte"; import IconDownload from "@tabler/icons-svelte/IconDownload.svelte"; import IconSettings from "@tabler/icons-svelte/IconSettings.svelte"; + import IconClipboard from "$components/icons/Clipboard.svelte"; import IconRepeat from "@tabler/icons-svelte/IconRepeat.svelte"; @@ -27,11 +26,13 @@