mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-03 11:58:28 +00:00
文件传输1
This commit is contained in:
parent
cddf6867de
commit
7b83692845
@ -39,6 +39,7 @@
|
|||||||
"set-cookie-parser": "2.6.0",
|
"set-cookie-parser": "2.6.0",
|
||||||
"undici": "^5.19.1",
|
"undici": "^5.19.1",
|
||||||
"url-pattern": "1.0.3",
|
"url-pattern": "1.0.3",
|
||||||
|
"ws": "^8.18.2",
|
||||||
"youtubei.js": "^13.4.0",
|
"youtubei.js": "^13.4.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@ import { friendlyServiceName } from "../processing/service-alias.js";
|
|||||||
import { verifyStream } from "../stream/manage.js";
|
import { verifyStream } from "../stream/manage.js";
|
||||||
import { createResponse, normalizeRequest, getIP } from "../processing/request.js";
|
import { createResponse, normalizeRequest, getIP } from "../processing/request.js";
|
||||||
import { setupTunnelHandler } from "./itunnel.js";
|
import { setupTunnelHandler } from "./itunnel.js";
|
||||||
|
import { setupSignalingServer } from "./signaling.js";
|
||||||
|
|
||||||
import * as APIKeys from "../security/api-keys.js";
|
import * as APIKeys from "../security/api-keys.js";
|
||||||
import * as Cookies from "../processing/cookie/manager.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'];
|
streamInfo.range = req.headers['range'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream(res, streamInfo);
|
return stream(res, streamInfo); });
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/', (_, res) => {
|
app.get('/', (_, res) => {
|
||||||
res.type('json');
|
res.type('json');
|
||||||
@ -337,7 +337,12 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
|
|||||||
setGlobalDispatcher(new ProxyAgent(env.externalProxy))
|
setGlobalDispatcher(new ProxyAgent(env.externalProxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
http.createServer(app).listen({
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
// 设置WebSocket信令服务器
|
||||||
|
setupSignalingServer(server);
|
||||||
|
|
||||||
|
server.listen({
|
||||||
port: env.apiPort,
|
port: env.apiPort,
|
||||||
host: env.listenAddress,
|
host: env.listenAddress,
|
||||||
reusePort: env.instanceCount > 1 || undefined
|
reusePort: env.instanceCount > 1 || undefined
|
||||||
|
180
api/src/core/signaling.js
Normal file
180
api/src/core/signaling.js
Normal file
@ -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;
|
||||||
|
};
|
183
pnpm-lock.yaml
183
pnpm-lock.yaml
@ -58,6 +58,9 @@ importers:
|
|||||||
url-pattern:
|
url-pattern:
|
||||||
specifier: 1.0.3
|
specifier: 1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
ws:
|
||||||
|
specifier: ^8.18.2
|
||||||
|
version: 8.18.2
|
||||||
youtubei.js:
|
youtubei.js:
|
||||||
specifier: ^13.4.0
|
specifier: ^13.4.0
|
||||||
version: 13.4.0
|
version: 13.4.0
|
||||||
@ -90,6 +93,10 @@ importers:
|
|||||||
packages/version-info: {}
|
packages/version-info: {}
|
||||||
|
|
||||||
web:
|
web:
|
||||||
|
dependencies:
|
||||||
|
qrcode:
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.5.0
|
specifier: ^9.5.0
|
||||||
@ -130,6 +137,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.14.10
|
specifier: ^20.14.10
|
||||||
version: 20.14.14
|
version: 20.14.14
|
||||||
|
'@types/qrcode':
|
||||||
|
specifier: ^1.5.5
|
||||||
|
version: 1.5.5
|
||||||
'@vitejs/plugin-basic-ssl':
|
'@vitejs/plugin-basic-ssl':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(vite@5.3.5(@types/node@20.14.14))
|
version: 1.1.0(vite@5.3.5(@types/node@20.14.14))
|
||||||
@ -759,6 +769,9 @@ packages:
|
|||||||
'@types/pug@2.0.10':
|
'@types/pug@2.0.10':
|
||||||
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
||||||
|
|
||||||
|
'@types/qrcode@1.5.5':
|
||||||
|
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
|
||||||
|
|
||||||
'@types/unist@2.0.10':
|
'@types/unist@2.0.10':
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
|
|
||||||
@ -940,6 +953,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
camelcase@5.3.1:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
caseless@0.12.0:
|
caseless@0.12.0:
|
||||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||||
|
|
||||||
@ -951,6 +968,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||||
|
|
||||||
cluster-key-slot@1.1.2:
|
cluster-key-slot@1.1.2:
|
||||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -1034,6 +1054,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decamelize@1.2.0:
|
||||||
|
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
@ -1064,6 +1088,9 @@ packages:
|
|||||||
devalue@5.0.0:
|
devalue@5.0.0:
|
||||||
resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==}
|
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:
|
dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1139,6 +1166,7 @@ packages:
|
|||||||
eslint@8.57.0:
|
eslint@8.57.0:
|
||||||
resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
|
resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
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
|
hasBin: true
|
||||||
|
|
||||||
esm-env@1.0.0:
|
esm-env@1.0.0:
|
||||||
@ -1217,6 +1245,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1258,6 +1290,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
||||||
engines: {node: '>= 4'}
|
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:
|
get-intrinsic@1.2.4:
|
||||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1462,6 +1498,10 @@ packages:
|
|||||||
locate-character@3.0.0:
|
locate-character@3.0.0:
|
||||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1620,14 +1660,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
p-limit@3.1.0:
|
p-limit@3.1.0:
|
||||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
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:
|
package-json-from-dist@1.0.0:
|
||||||
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
||||||
|
|
||||||
@ -1679,6 +1731,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
postcss-load-config@6.0.1:
|
postcss-load-config@6.0.1:
|
||||||
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@ -1729,6 +1785,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@ -1761,6 +1822,13 @@ packages:
|
|||||||
redis@4.7.0:
|
redis@4.7.0:
|
||||||
resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==}
|
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:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -1817,6 +1885,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
set-blocking@2.0.0:
|
||||||
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
set-cookie-parser@2.6.0:
|
set-cookie-parser@2.6.0:
|
||||||
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
|
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
|
||||||
|
|
||||||
@ -2185,6 +2256,9 @@ packages:
|
|||||||
whatwg-url@7.1.0:
|
whatwg-url@7.1.0:
|
||||||
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
||||||
|
|
||||||
|
which-module@2.0.1:
|
||||||
|
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2194,6 +2268,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
wrap-ansi@7.0.0:
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2205,9 +2283,32 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
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:
|
yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
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:
|
yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2640,6 +2741,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pug@2.0.10': {}
|
'@types/pug@2.0.10': {}
|
||||||
|
|
||||||
|
'@types/qrcode@1.5.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.14.14
|
||||||
|
|
||||||
'@types/unist@2.0.10': {}
|
'@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)':
|
'@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: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
|
camelcase@5.3.1: {}
|
||||||
|
|
||||||
caseless@0.12.0: {}
|
caseless@0.12.0: {}
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
@ -2858,6 +2965,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
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:
|
cluster-key-slot@1.1.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -2928,6 +3041,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
|
decamelize@1.2.0: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
@ -2948,6 +3063,8 @@ snapshots:
|
|||||||
|
|
||||||
devalue@5.0.0: {}
|
devalue@5.0.0: {}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
@ -3212,6 +3329,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 5.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path: 6.0.0
|
locate-path: 6.0.0
|
||||||
@ -3251,6 +3373,8 @@ snapshots:
|
|||||||
generic-pool@3.9.0:
|
generic-pool@3.9.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@ -3443,6 +3567,10 @@ snapshots:
|
|||||||
|
|
||||||
locate-character@3.0.0: {}
|
locate-character@3.0.0: {}
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 4.1.0
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
@ -3567,14 +3695,24 @@ snapshots:
|
|||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
word-wrap: 1.2.5
|
word-wrap: 1.2.5
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
p-try: 2.2.0
|
||||||
|
|
||||||
p-limit@3.1.0:
|
p-limit@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 0.1.0
|
yocto-queue: 0.1.0
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 2.3.0
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
package-json-from-dist@1.0.0: {}
|
package-json-from-dist@1.0.0: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
@ -3612,6 +3750,8 @@ snapshots:
|
|||||||
|
|
||||||
pirates@4.0.6: {}
|
pirates@4.0.6: {}
|
||||||
|
|
||||||
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
postcss-load-config@6.0.1(postcss@8.4.40):
|
postcss-load-config@6.0.1(postcss@8.4.40):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.2
|
lilconfig: 3.1.2
|
||||||
@ -3641,6 +3781,12 @@ snapshots:
|
|||||||
|
|
||||||
punycode@2.3.1: {}
|
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:
|
qs@6.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.0.6
|
side-channel: 1.0.6
|
||||||
@ -3681,6 +3827,10 @@ snapshots:
|
|||||||
'@redis/time-series': 1.1.0(@redis/client@1.6.0)
|
'@redis/time-series': 1.1.0(@redis/client@1.6.0)
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
|
require-main-filename@2.0.0: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-from@5.0.0: {}
|
resolve-from@5.0.0: {}
|
||||||
@ -3765,6 +3915,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-cookie-parser@2.6.0: {}
|
set-cookie-parser@2.6.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
@ -4077,12 +4229,20 @@ snapshots:
|
|||||||
tr46: 1.0.1
|
tr46: 1.0.1
|
||||||
webidl-conversions: 4.0.2
|
webidl-conversions: 4.0.2
|
||||||
|
|
||||||
|
which-module@2.0.1: {}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
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:
|
wrap-ansi@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
@ -4097,9 +4257,32 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
ws@8.18.2: {}
|
||||||
|
|
||||||
|
y18n@4.0.3: {}
|
||||||
|
|
||||||
yallist@4.0.0:
|
yallist@4.0.0:
|
||||||
optional: true
|
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: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
youtubei.js@13.4.0:
|
youtubei.js@13.4.0:
|
||||||
|
33
web/i18n/en/clipboard.json
Normal file
33
web/i18n/en/clipboard.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
"cobalt": "bamboo download, Download Videos for Free - YouTube, TikTok, Bilibili, Instagram, Facebook, Twitter",
|
"cobalt": "bamboo download, Download Videos for Free - YouTube, TikTok, Bilibili, Instagram, Facebook, Twitter",
|
||||||
"meowbalt": "bamboo download",
|
"meowbalt": "bamboo download",
|
||||||
"beta": "beta",
|
"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.",
|
"embed.description": "Download videos for free from YouTube, TikTok, Bilibili, Instagram, Facebook, and Twitter. No registration needed. Easy and fast video downloads.",
|
||||||
"guide": {
|
"guide": {
|
||||||
"title": "Freesavevideo.online Online Video Downloader Guide",
|
"title": "Freesavevideo.online Online Video Downloader Guide",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"save": "index",
|
"save": "index",
|
||||||
|
"clipboard": "clipboard",
|
||||||
"settings": "settings",
|
"settings": "settings",
|
||||||
"updates": "updates",
|
"updates": "updates",
|
||||||
"donate": "donate",
|
"donate": "donate",
|
||||||
|
33
web/i18n/zh/clipboard.json
Normal file
33
web/i18n/zh/clipboard.json
Normal file
@ -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": "加密失败"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"save": "首页",
|
"save": "首页",
|
||||||
|
"clipboard": "剪贴板",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"updates": "更新",
|
"updates": "更新",
|
||||||
"donate": "捐赠",
|
"donate": "捐赠",
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.25",
|
"@types/fluent-ffmpeg": "^2.1.25",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
@ -54,5 +55,8 @@
|
|||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^7.13.1",
|
"typescript-eslint": "^7.13.1",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
export let click = () => {
|
export let click = () => {
|
||||||
alert("no function assigned");
|
alert("no function assigned");
|
||||||
};
|
};
|
||||||
|
export let disabled: boolean = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button id="button-{id}" class="button" on:click={click}>
|
<button id="button-{id}" class="button" {disabled} on:click={click}>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
import { defaultNavPage } from "$lib/subnav";
|
import { defaultNavPage } from "$lib/subnav";
|
||||||
|
|
||||||
import CobaltLogo from "$components/sidebar/CobaltLogo.svelte";
|
import CobaltLogo from "$components/sidebar/CobaltLogo.svelte";
|
||||||
import SidebarTab from "$components/sidebar/SidebarTab.svelte";
|
import SidebarTab from "$components/sidebar/SidebarTab.svelte"; import IconDownload from "@tabler/icons-svelte/IconDownload.svelte";
|
||||||
|
|
||||||
import IconDownload from "@tabler/icons-svelte/IconDownload.svelte";
|
|
||||||
import IconSettings from "@tabler/icons-svelte/IconSettings.svelte";
|
import IconSettings from "@tabler/icons-svelte/IconSettings.svelte";
|
||||||
|
import IconClipboard from "$components/icons/Clipboard.svelte";
|
||||||
|
|
||||||
import IconRepeat from "@tabler/icons-svelte/IconRepeat.svelte";
|
import IconRepeat from "@tabler/icons-svelte/IconRepeat.svelte";
|
||||||
|
|
||||||
@ -27,11 +26,13 @@
|
|||||||
|
|
||||||
<nav id="sidebar" aria-label={$t("a11y.tabs.tab_panel")}>
|
<nav id="sidebar" aria-label={$t("a11y.tabs.tab_panel")}>
|
||||||
<CobaltLogo />
|
<CobaltLogo />
|
||||||
<div id="sidebar-tabs" role="tablist">
|
<div id="sidebar-tabs" role="tablist"> <div id="sidebar-actions" class="sidebar-inner-container">
|
||||||
<div id="sidebar-actions" class="sidebar-inner-container">
|
|
||||||
<SidebarTab tabName="save" tabLink="/">
|
<SidebarTab tabName="save" tabLink="/">
|
||||||
<IconDownload />
|
<IconDownload />
|
||||||
</SidebarTab>
|
</SidebarTab>
|
||||||
|
<SidebarTab tabName="clipboard" tabLink="/clipboard">
|
||||||
|
<IconClipboard />
|
||||||
|
</SidebarTab>
|
||||||
<SidebarTab tabName="remux" tabLink="/remux" beta>
|
<SidebarTab tabName="remux" tabLink="/remux" beta>
|
||||||
<IconRepeat />
|
<IconRepeat />
|
||||||
</SidebarTab>
|
</SidebarTab>
|
||||||
|
@ -55,8 +55,8 @@ const docs = {
|
|||||||
apiLicense: "https://github.com/imputnet/cobalt/blob/main/api/LICENSE",
|
apiLicense: "https://github.com/imputnet/cobalt/blob/main/api/LICENSE",
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiURL = "https://api.freesavevideo.online/";
|
//const apiURL = "https://api.freesavevideo.online/";
|
||||||
//const apiURL = "http://localhost:9000";
|
const apiURL = "http://localhost:9000";
|
||||||
|
|
||||||
export { donate, apiURL, contacts, partners, siriShortcuts, docs };
|
export { donate, apiURL, contacts, partners, siriShortcuts, docs };
|
||||||
export default variables;
|
export default variables;
|
||||||
|
978
web/src/routes/clipboard/+page.svelte
Normal file
978
web/src/routes/clipboard/+page.svelte
Normal file
@ -0,0 +1,978 @@
|
|||||||
|
<script lang="ts"> import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { t } from '$lib/i18n/translations';
|
||||||
|
import SettingsCategory from '$components/settings/SettingsCategory.svelte';
|
||||||
|
import ActionButton from '$components/buttons/ActionButton.svelte';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import { currentApiURL } from '$lib/api/api-url';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
interface FileItem {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
blob: Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReceivingFile {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
chunks: Uint8Array[];
|
||||||
|
receivedSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
let sessionId = '';
|
||||||
|
let joinCode = '';
|
||||||
|
let isConnected = false;
|
||||||
|
let isCreating = false;
|
||||||
|
let isJoining = false;
|
||||||
|
let isCreator = false;
|
||||||
|
let peerConnected = false;
|
||||||
|
let qrCodeUrl = '';
|
||||||
|
|
||||||
|
// WebSocket and WebRTC
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
let peerConnection: RTCPeerConnection | null = null;
|
||||||
|
let dataChannel: RTCDataChannel | null = null;
|
||||||
|
|
||||||
|
// Encryption
|
||||||
|
let keyPair: CryptoKeyPair | null = null;
|
||||||
|
let remotePublicKey: CryptoKey | null = null;
|
||||||
|
let sharedKey: CryptoKey | null = null;
|
||||||
|
|
||||||
|
// File transfer
|
||||||
|
let files: File[] = [];
|
||||||
|
let receivedFiles: FileItem[] = [];
|
||||||
|
let textContent = '';
|
||||||
|
let dragover = false;
|
||||||
|
let sendingFiles = false;
|
||||||
|
let receivingFiles = false;
|
||||||
|
let transferProgress = 0;
|
||||||
|
let currentReceivingFile: ReceivingFile | null = null;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const CHUNK_SIZE = 64 * 1024; // 64KB chunks
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// Check for session parameter in URL
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const sessionParam = urlParams.get('session');
|
||||||
|
if (sessionParam) {
|
||||||
|
joinCode = sessionParam;
|
||||||
|
await joinSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
cleanup();
|
||||||
|
}); function getWebSocketURL(): string {
|
||||||
|
if (typeof window === 'undefined') return 'ws://localhost:9000/ws';
|
||||||
|
|
||||||
|
const apiUrl = currentApiURL();
|
||||||
|
const url = new URL(apiUrl);
|
||||||
|
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|
||||||
|
return `${protocol}//${url.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateKeyPair(): Promise<void> {
|
||||||
|
keyPair = await window.crypto.subtle.generateKey(
|
||||||
|
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPublicKey(): Promise<ArrayBuffer> {
|
||||||
|
if (!keyPair) throw new Error('Key pair not generated');
|
||||||
|
return await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
|
||||||
|
} async function importRemotePublicKey(publicKeyArray: number[]): Promise<void> {
|
||||||
|
const publicKeyBuffer = new Uint8Array(publicKeyArray).buffer;
|
||||||
|
remotePublicKey = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
publicKeyBuffer,
|
||||||
|
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||||
|
false,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deriveSharedKey(): Promise<void> {
|
||||||
|
if (!keyPair || !remotePublicKey) throw new Error('Keys not available');
|
||||||
|
|
||||||
|
sharedKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{ name: 'ECDH', public: remotePublicKey },
|
||||||
|
keyPair.privateKey,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptData(data: string): Promise<ArrayBuffer> {
|
||||||
|
if (!sharedKey) throw new Error('Shared key not available');
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const dataBuffer = encoder.encode(data);
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv },
|
||||||
|
sharedKey,
|
||||||
|
dataBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine IV and encrypted data
|
||||||
|
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
||||||
|
result.set(iv);
|
||||||
|
result.set(new Uint8Array(encrypted), iv.length);
|
||||||
|
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptData(encryptedBuffer: ArrayBuffer): Promise<string> {
|
||||||
|
if (!sharedKey) throw new Error('Shared key not available');
|
||||||
|
|
||||||
|
const encryptedArray = new Uint8Array(encryptedBuffer);
|
||||||
|
const iv = encryptedArray.slice(0, 12);
|
||||||
|
const encrypted = encryptedArray.slice(12);
|
||||||
|
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv },
|
||||||
|
sharedKey,
|
||||||
|
encrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWebSocket(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const wsUrl = getWebSocketURL();
|
||||||
|
console.log('Connecting to WebSocket:', wsUrl);
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
isConnected = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = async (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
console.log('WebSocket message:', message);
|
||||||
|
await handleWebSocketMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
isConnected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
isConnected = false;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} async function handleWebSocketMessage(message: any): Promise<void> {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'session_created':
|
||||||
|
sessionId = message.sessionId;
|
||||||
|
isCreating = false;
|
||||||
|
isCreator = true;
|
||||||
|
console.log('Session created:', sessionId);
|
||||||
|
await generateQRCode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'session_joined':
|
||||||
|
await importRemotePublicKey(message.publicKey);
|
||||||
|
await deriveSharedKey();
|
||||||
|
isJoining = false;
|
||||||
|
await setupWebRTC(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'peer_joined':
|
||||||
|
await importRemotePublicKey(message.publicKey);
|
||||||
|
await deriveSharedKey();
|
||||||
|
if (isCreator) {
|
||||||
|
await setupWebRTC(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'offer':
|
||||||
|
await handleOffer(message.offer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'answer':
|
||||||
|
await handleAnswer(message.answer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ice_candidate':
|
||||||
|
await handleIceCandidate(message.candidate);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
console.error('Server error:', message.error);
|
||||||
|
isCreating = false;
|
||||||
|
isJoining = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} async function createSession(): Promise<void> {
|
||||||
|
try {
|
||||||
|
isCreating = true;
|
||||||
|
await generateKeyPair();
|
||||||
|
await connectWebSocket();
|
||||||
|
|
||||||
|
const publicKeyBuffer = await exportPublicKey();
|
||||||
|
const publicKeyArray = Array.from(new Uint8Array(publicKeyBuffer));
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'create_session',
|
||||||
|
publicKey: publicKeyArray
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating session:', error);
|
||||||
|
isCreating = false;
|
||||||
|
}
|
||||||
|
} async function joinSession(): Promise<void> {
|
||||||
|
try {
|
||||||
|
isJoining = true;
|
||||||
|
await generateKeyPair();
|
||||||
|
await connectWebSocket();
|
||||||
|
|
||||||
|
const publicKeyBuffer = await exportPublicKey();
|
||||||
|
const publicKeyArray = Array.from(new Uint8Array(publicKeyBuffer));
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'join_session',
|
||||||
|
sessionId: joinCode,
|
||||||
|
publicKey: publicKeyArray
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error joining session:', error);
|
||||||
|
isJoining = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupWebRTC(shouldCreateOffer: boolean): Promise<void> {
|
||||||
|
peerConnection = new RTCPeerConnection({
|
||||||
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||||
|
if (event.candidate && ws) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'ice_candidate',
|
||||||
|
candidate: event.candidate
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldCreateOffer) {
|
||||||
|
dataChannel = peerConnection.createDataChannel('fileTransfer', { ordered: true });
|
||||||
|
setupDataChannel(dataChannel);
|
||||||
|
|
||||||
|
peerConnection.createOffer().then((offer: RTCSessionDescriptionInit) => {
|
||||||
|
if (peerConnection) {
|
||||||
|
peerConnection.setLocalDescription(offer);
|
||||||
|
if (ws) {
|
||||||
|
ws.send(JSON.stringify({ type: 'offer', offer: offer }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
peerConnection.ondatachannel = (event: RTCDataChannelEvent) => {
|
||||||
|
dataChannel = event.channel;
|
||||||
|
setupDataChannel(dataChannel);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDataChannel(channel: RTCDataChannel): void {
|
||||||
|
channel.onopen = () => {
|
||||||
|
console.log('Data channel opened');
|
||||||
|
peerConnected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.onclose = () => {
|
||||||
|
console.log('Data channel closed');
|
||||||
|
peerConnected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.onmessage = async (event) => {
|
||||||
|
try {
|
||||||
|
const encryptedData = event.data;
|
||||||
|
const decryptedMessage = await decryptData(encryptedData);
|
||||||
|
const message = JSON.parse(decryptedMessage);
|
||||||
|
|
||||||
|
if (message.type === 'text') {
|
||||||
|
textContent = message.data;
|
||||||
|
} else if (message.type === 'file_info') {
|
||||||
|
currentReceivingFile = {
|
||||||
|
name: message.name,
|
||||||
|
size: message.size,
|
||||||
|
type: message.type,
|
||||||
|
chunks: [],
|
||||||
|
receivedSize: 0
|
||||||
|
};
|
||||||
|
receivingFiles = true;
|
||||||
|
} else if (message.type === 'file_chunk' && currentReceivingFile) {
|
||||||
|
const chunkData = new Uint8Array(message.data);
|
||||||
|
currentReceivingFile.chunks.push(chunkData);
|
||||||
|
currentReceivingFile.receivedSize += chunkData.length;
|
||||||
|
transferProgress = (currentReceivingFile.receivedSize / currentReceivingFile.size) * 100;
|
||||||
|
|
||||||
|
if (currentReceivingFile.receivedSize >= currentReceivingFile.size) {
|
||||||
|
const completeFile = new Blob(currentReceivingFile.chunks, { type: currentReceivingFile.type });
|
||||||
|
receivedFiles = [...receivedFiles, {
|
||||||
|
name: currentReceivingFile.name,
|
||||||
|
size: currentReceivingFile.size,
|
||||||
|
type: currentReceivingFile.type,
|
||||||
|
blob: completeFile
|
||||||
|
}];
|
||||||
|
|
||||||
|
currentReceivingFile = null;
|
||||||
|
receivingFiles = false;
|
||||||
|
transferProgress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling data channel message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||||
|
if (!peerConnection) return;
|
||||||
|
|
||||||
|
await peerConnection.setRemoteDescription(offer);
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
ws.send(JSON.stringify({ type: 'answer', answer: answer }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
||||||
|
if (!peerConnection) return;
|
||||||
|
await peerConnection.setRemoteDescription(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
||||||
|
if (!peerConnection) return;
|
||||||
|
await peerConnection.addIceCandidate(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendText(): Promise<void> {
|
||||||
|
if (!dataChannel || !peerConnected || !textContent.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = { type: 'text', data: textContent.trim() };
|
||||||
|
const encryptedData = await encryptData(JSON.stringify(message));
|
||||||
|
|
||||||
|
if (dataChannel) {
|
||||||
|
dataChannel.send(encryptedData);
|
||||||
|
textContent = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending text:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendFiles(): Promise<void> {
|
||||||
|
if (!dataChannel || !peerConnected || files.length === 0) return;
|
||||||
|
|
||||||
|
sendingFiles = true;
|
||||||
|
transferProgress = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
|
||||||
|
// Send file info
|
||||||
|
const fileInfo = { type: 'file_info', name: file.name, size: file.size, mimeType: file.type };
|
||||||
|
const encryptedInfo = await encryptData(JSON.stringify(fileInfo));
|
||||||
|
dataChannel.send(encryptedInfo);
|
||||||
|
|
||||||
|
// Send file chunks
|
||||||
|
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||||
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||||
|
const start = chunkIndex * CHUNK_SIZE;
|
||||||
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
const chunkArray = new Uint8Array(await chunk.arrayBuffer());
|
||||||
|
|
||||||
|
const chunkMessage = { type: 'file_chunk', data: Array.from(chunkArray) };
|
||||||
|
const encryptedChunk = await encryptData(JSON.stringify(chunkMessage));
|
||||||
|
dataChannel.send(encryptedChunk);
|
||||||
|
|
||||||
|
const fileProgress = (chunkIndex + 1) / totalChunks;
|
||||||
|
const totalProgress = ((i + fileProgress) / files.length) * 100;
|
||||||
|
transferProgress = totalProgress;
|
||||||
|
|
||||||
|
// Small delay to prevent overwhelming the connection
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files = [];
|
||||||
|
transferProgress = 0;
|
||||||
|
sendingFiles = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending files:', error);
|
||||||
|
sendingFiles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(event: Event): void {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (target.files) {
|
||||||
|
files = [...files, ...Array.from(target.files)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event: DragEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
dragover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(): void {
|
||||||
|
dragover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
dragover = false;
|
||||||
|
if (event.dataTransfer?.files) {
|
||||||
|
files = [...files, ...Array.from(event.dataTransfer.files)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile(index: number): void {
|
||||||
|
files = files.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadReceivedFile(file: FileItem): void {
|
||||||
|
const url = URL.createObjectURL(file.blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReceivedFile(index: number): void {
|
||||||
|
const file = receivedFiles[index];
|
||||||
|
URL.revokeObjectURL(URL.createObjectURL(file.blob));
|
||||||
|
receivedFiles = receivedFiles.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateQRCode(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && sessionId) {
|
||||||
|
const url = `${window.location.origin}/clipboard?session=${sessionId}`;
|
||||||
|
qrCodeUrl = await QRCode.toDataURL(url, {
|
||||||
|
width: 200,
|
||||||
|
margin: 2,
|
||||||
|
color: { dark: '#000000', light: '#ffffff' }
|
||||||
|
});
|
||||||
|
console.log('QR Code generated:', { sessionId, url, qrCodeUrl: 'data URL created' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('QR Code generation failed:', error);
|
||||||
|
console.log('QR Code generation failed:', { hasWindow: typeof window !== 'undefined', sessionId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareSession(): void {
|
||||||
|
if (typeof window !== 'undefined' && sessionId) {
|
||||||
|
const url = `${window.location.origin}/clipboard?session=${sessionId}`;
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(): void {
|
||||||
|
if (dataChannel) {
|
||||||
|
dataChannel.close();
|
||||||
|
dataChannel = null;
|
||||||
|
}
|
||||||
|
if (peerConnection) {
|
||||||
|
peerConnection.close();
|
||||||
|
peerConnection = null;
|
||||||
|
}
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId = '';
|
||||||
|
isConnected = false;
|
||||||
|
peerConnected = false;
|
||||||
|
sharedKey = null;
|
||||||
|
remotePublicKey = null;
|
||||||
|
qrCodeUrl = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>cobalt | {$t("clipboard.title")}</title>
|
||||||
|
<meta property="og:title" content="cobalt | {$t("clipboard.title")}" />
|
||||||
|
<meta property="og:description" content={$t("clipboard.description")} />
|
||||||
|
<meta property="description" content={$t("clipboard.description")} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="clipboard-container">
|
||||||
|
<div class="clipboard-header">
|
||||||
|
<h1>{$t("clipboard.title")}</h1>
|
||||||
|
<p>{$t("clipboard.description")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isConnected}
|
||||||
|
<SettingsCategory title={$t("clipboard.title")} sectionId="connection-setup">
|
||||||
|
<div class="connection-setup">
|
||||||
|
<div class="setup-option">
|
||||||
|
<h3>{$t("clipboard.create_session")}</h3>
|
||||||
|
<p>{$t("clipboard.create_description")}</p> <ActionButton
|
||||||
|
id="create-session"
|
||||||
|
click={createSession} disabled={isCreating}
|
||||||
|
>
|
||||||
|
{isCreating ? $t("clipboard.creating") : $t("clipboard.create")}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>{$t("general.or")}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setup-option">
|
||||||
|
<h3>{$t("clipboard.join_session")}</h3>
|
||||||
|
<p>{$t("clipboard.join_description")}</p>
|
||||||
|
<div class="join-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={joinCode}
|
||||||
|
placeholder={$t("clipboard.enter_code")}
|
||||||
|
disabled={isJoining}
|
||||||
|
/> <ActionButton
|
||||||
|
id="join-session"
|
||||||
|
click={joinSession}
|
||||||
|
disabled={isJoining || !joinCode.trim()}
|
||||||
|
>
|
||||||
|
{isJoining ? $t("clipboard.joining") : $t("clipboard.join")}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
{:else}
|
||||||
|
<SettingsCategory title={$t("clipboard.session_active")} sectionId="session-info">
|
||||||
|
<div class="session-info">
|
||||||
|
<h3>{$t("clipboard.session_active")}</h3>
|
||||||
|
<div class="session-details">
|
||||||
|
<div class="session-id">
|
||||||
|
<strong>{$t("clipboard.session_id")}:</strong>
|
||||||
|
<code>{sessionId}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isCreator && sessionId && !peerConnected && qrCodeUrl}
|
||||||
|
<div class="qr-code">
|
||||||
|
<p>{$t("clipboard.scan_qr")}</p>
|
||||||
|
<img src={qrCodeUrl} alt="QR Code" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="connection-status">
|
||||||
|
<span class="status-indicator" class:connected={peerConnected}></span>
|
||||||
|
{peerConnected ? $t("clipboard.peer_connected") : $t("clipboard.waiting_peer")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
|
||||||
|
{#if peerConnected}
|
||||||
|
<SettingsCategory title={$t("clipboard.send_text")} sectionId="text-transfer">
|
||||||
|
<div class="text-transfer">
|
||||||
|
<h3>{$t("clipboard.send_text")}</h3>
|
||||||
|
<textarea
|
||||||
|
bind:value={textContent}
|
||||||
|
placeholder={$t("clipboard.enter_text")}
|
||||||
|
rows="4"
|
||||||
|
></textarea> <ActionButton id="send-text" click={sendText} disabled={!textContent.trim()}>
|
||||||
|
{$t("clipboard.send")}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
|
||||||
|
<SettingsCategory title={$t("clipboard.send_files")} sectionId="file-transfer">
|
||||||
|
<div class="file-transfer">
|
||||||
|
<h3>{$t("clipboard.send_files")}</h3>
|
||||||
|
<div
|
||||||
|
class="drop-zone"
|
||||||
|
on:dragover|preventDefault={handleDragOver}
|
||||||
|
on:dragleave={handleDragLeave}
|
||||||
|
on:drop={handleDrop}
|
||||||
|
class:dragover
|
||||||
|
>
|
||||||
|
<div class="drop-content">
|
||||||
|
<p>{$t("clipboard.drop_files")}</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
on:change={handleFileSelect}
|
||||||
|
id="file-input"
|
||||||
|
style="display: none;"
|
||||||
|
/> <ActionButton id="select-files" click={() => document.getElementById('file-input')?.click()}>
|
||||||
|
{$t("clipboard.select_files")}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if files.length > 0}
|
||||||
|
<div class="file-list">
|
||||||
|
{#each files as file, index}
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="file-name">{file.name}</span>
|
||||||
|
<span class="file-size">({formatFileSize(file.size)})</span>
|
||||||
|
<button class="remove-btn" on:click={() => removeFile(index)}>×</button>
|
||||||
|
</div>
|
||||||
|
{/each} <ActionButton id="send-files" click={sendFiles} disabled={sendingFiles}>
|
||||||
|
{sendingFiles ? $t("clipboard.sending") : $t("clipboard.send")}
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
{#if sendingFiles || transferProgress > 0}
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {transferProgress}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">{Math.round(transferProgress)}%</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
|
||||||
|
{#if receivedFiles.length > 0}
|
||||||
|
<SettingsCategory title={$t("clipboard.received_files")} sectionId="received-files">
|
||||||
|
<div class="received-files">
|
||||||
|
<h3>{$t("clipboard.received_files")}</h3>
|
||||||
|
<div class="file-list">
|
||||||
|
{#each receivedFiles as file, index}
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="file-name">{file.name}</span>
|
||||||
|
<span class="file-size">({formatFileSize(file.size)})</span>
|
||||||
|
<div class="file-actions">
|
||||||
|
<button class="download-btn" on:click={() => downloadReceivedFile(file)}>
|
||||||
|
⬇ {$t("clipboard.download")}
|
||||||
|
</button>
|
||||||
|
<button class="remove-btn" on:click={() => removeReceivedFile(index)}>×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if receivingFiles && currentReceivingFile}
|
||||||
|
<SettingsCategory title={$t("clipboard.receiving")} sectionId="receiving-progress">
|
||||||
|
<div class="receiving-progress">
|
||||||
|
<h3>{$t("clipboard.receiving")}: {currentReceivingFile.name}</h3>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {transferProgress}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">{Math.round(transferProgress)}% ({formatFileSize(currentReceivingFile.receivedSize)} / {formatFileSize(currentReceivingFile.size)})</span>
|
||||||
|
</div>
|
||||||
|
</SettingsCategory>
|
||||||
|
{/if}
|
||||||
|
{/if} <div class="disconnect-section">
|
||||||
|
<ActionButton id="cleanup" click={cleanup}>
|
||||||
|
{$t("clipboard.disconnect")}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clipboard-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-header h1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-setup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider span {
|
||||||
|
background-color: var(--background);
|
||||||
|
padding: 0 1rem;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: var(--input-background);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-id {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-id code {
|
||||||
|
background-color: var(--border);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code img {
|
||||||
|
max-width: 200px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected {
|
||||||
|
background-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-transfer textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: var(--input-background);
|
||||||
|
color: var(--text);
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.dragover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background-color: var(--accent-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
color: var(--secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn, .remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
background-color: var(--accent-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background-color: var(--red-background);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--accent);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.connection-setup {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
align-self: center;
|
||||||
|
margin: 0 2rem;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
width: 1px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-details {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1112
web/src/routes/clipboard/+page.svelte.backup
Normal file
1112
web/src/routes/clipboard/+page.svelte.backup
Normal file
File diff suppressed because it is too large
Load Diff
@ -80,8 +80,9 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, server: {
|
||||||
server: {
|
host: '0.0.0.0', // 允许外部访问
|
||||||
|
port: 5173,
|
||||||
headers: {
|
headers: {
|
||||||
"Cross-Origin-Opener-Policy": "same-origin",
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||||
|
Loading…
Reference in New Issue
Block a user