This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
================================================
FILE: README.md
================================================
# Deskreen CE (Community Edition)

(Over 2M downloads during 5 years since launch)

## Deskreen turns any device with a web browser into a secondary screen for your computer
## To learn more visit our website: [deskreen.com](https://deskreen.com)
## [Donate to support Deskreen Open-Source](https://deskreen.com/#contribute)
Deskreen is an `electron.js` based application that uses `WebRTC` to make a live stream of your computer screen to a web browser on any device. It is available for MacOS, Windows and Linux operating systems.
The current open-source Community Edition version has limited features. If you need more features please consider upgrading to [Pro](https://deskreen.com/download) version for more features when it is released.
---
### ▶️ [See how people use Deskreen on Youtube](https://www.youtube.com/results?search_query=deskreen) (video tutorials, demos, use cases for Deskreen day to day usage)
---
## [Deskreen Frequently Asked Questions](https://deskreen.com/faq)
---
### Prerequisites
You will need to have `node>=v23` `pnpm>=v10.20.0` installed.
1. git clone this repo
2. `pnpm i`
3. `cd ./src/client-viewer && pnpm i && cd ../..`
4. `pnpm clean && pnpm build && pnpm start` -- run in prod like mode
#### for more pnpm commands look at `package.json`
## Starting with Custom Local IP
You can start Deskreen CE with a custom local IP address using the `--local-ip` or `--ip` CLI flag. This is useful when you want to specify a particular network interface IP address.
### macOS
```bash
# Using open command (recommended)
open -a "Deskreen CE" --args --ip 192.168.1.100
# Or using the executable directly
/Applications/Deskreen\ CE.app/Contents/MacOS/Deskreen\ CE --ip 192.168.1.100
# Get your IP automatically and launch
open -a "Deskreen CE" --args --ip "192.168.1.100"
```
### Windows
```powershell
# Using Start-Process (PowerShell)
Start-Process "Deskreen CE" -ArgumentList "--ip", "192.168.1.100"
# Or using the executable directly
"C:\Program Files\Deskreen CE\Deskreen CE.exe" --ip 192.168.1.100
# Or from Command Prompt
start "" "C:\Program Files\Deskreen CE\Deskreen CE.exe" --ip 192.168.1.100
```
### Linux
```bash
# If installed via AppImage
./Deskreen\ CE-*.AppImage --ip 192.168.1.100
# If installed via .deb/.rpm package (usually in /usr/bin or /opt)
deskreen-ce --ip 192.168.1.100
# Or using full path
/opt/Deskreen\ CE/deskreen-ce --ip 192.168.1.100
```
**Note:** Replace `192.168.1.100` with your actual local IP address. You can find your IP using:
- **macOS/Linux:** `ipconfig getifaddr en0` or `ifconfig | grep "inet "`
- **Windows:** `ipconfig` (look for IPv4 Address)
When using the `--ip` or `--local-ip` flag, the app will use the specified IP for QR codes and connection URLs, while still monitoring the actual network interface status for WiFi connection detection.
## Maintainer
- [Pavlo (Paul) Buidenkov](https://www.linkedin.com/in/pavlobu)
## License
AGPL-3.0 License © [Pavlo (Paul) Buidenkov](https://github.com/pavlobu/deskreen)
## Copyright
Electron-Vite MIT License © [electron-vite](https://github.com/alex8088/electron-vite)
React MIT License © [Facebook, Inc. and its affiliates](https://github.com/facebook/react)
Vite MIT License © [Vite.js](https://github.com/vitejs/vite)
Electron Builder MIT License © [electron-builder contributors](https://github.com/electron-userland/electron-builder)
Apache 2.0 © [blueprintjs](https://github.com/palantir/blueprint)
simple-peer MIT. Copyright © [Feross Aboukhadijeh](http://feross.org/)
tweetnacl ISC License © Dmitry Chestnykh, Devi Mandiri, and contributors (https://github.com/dchest/tweetnacl-js)
darkwire.io MIT License © [darkwire/darkwire.io](https://github.com/darkwire/darkwire.io)
And many many others...
## Thanks
🙏 Many thanks to all 🌍 open source community members and maintainers of libraries used in this project.
================================================
FILE: biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": { "includes": ["**", "!!**/dist"] },
"formatter": { "enabled": true, "indentStyle": "tab" },
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"complexity": {
"noAdjacentSpacesInRegex": "error",
"noExtraBooleanCast": "error",
"noUselessCatch": "error",
"noUselessEscapeInRegex": "error",
"noUselessTypeConstraint": "error"
},
"correctness": {
"noChildrenProp": "error",
"noConstAssign": "error",
"noConstantCondition": "error",
"noEmptyCharacterClassInRegex": "error",
"noEmptyPattern": "error",
"noGlobalObjectCalls": "error",
"noInvalidBuiltinInstantiation": "error",
"noInvalidConstructorSuper": "error",
"noNonoctalDecimalEscape": "error",
"noPrecisionLoss": "error",
"noSelfAssign": "error",
"noSetterReturn": "error",
"noSwitchDeclarations": "error",
"noUndeclaredVariables": "error",
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error",
"noUnusedPrivateClassMembers": "error",
"noUnusedVariables": "error",
"useIsNan": "error",
"useJsxKeyInIterable": "error",
"useValidForDirection": "error",
"useValidTypeof": "error",
"useYield": "error"
},
"security": { "noDangerouslySetInnerHtmlWithChildren": "error" },
"style": {
"noCommonJs": "error",
"noNamespace": "error",
"noNonNullAssertion": "off",
"useArrayLiterals": "error",
"useAsConstAssertion": "error",
"useBlockStatements": "off"
},
"suspicious": {
"noAsyncPromiseExecutor": "error",
"noCatchAssign": "error",
"noClassAssign": "error",
"noCommentText": "error",
"noCompareNegZero": "error",
"noConstantBinaryExpressions": "error",
"noControlCharactersInRegex": "error",
"noDebugger": "error",
"noDuplicateCase": "error",
"noDuplicateClassMembers": "error",
"noDuplicateElseIf": "error",
"noDuplicateJsxProps": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noEmptyBlockStatements": "error",
"noExplicitAny": "error",
"noExtraNonNullAssertion": "error",
"noFallthroughSwitchClause": "error",
"noFunctionAssign": "error",
"noGlobalAssign": "error",
"noImportAssign": "error",
"noIrregularWhitespace": "error",
"noMisleadingCharacterClass": "error",
"noMisleadingInstantiator": "error",
"noNonNullAssertedOptionalChain": "error",
"noPrototypeBuiltins": "error",
"noRedeclare": "error",
"noShadowRestrictedNames": "error",
"noSparseArray": "error",
"noUnsafeDeclarationMerging": "error",
"noUnsafeNegation": "error",
"noUselessRegexBackrefs": "error",
"noWith": "error",
"useGetterReturn": "error",
"useNamespaceKeyword": "error"
}
},
"includes": ["**", "!**/node_modules", "!**/dist", "!**/out"]
},
"javascript": {
"formatter": { "quoteStyle": "single" },
"globals": [
"onanimationend",
"exports",
"ongamepadconnected",
"onlostpointercapture",
"onanimationiteration",
"onkeyup",
"onmousedown",
"onanimationstart",
"onslotchange",
"onprogress",
"ontransitionstart",
"onpause",
"onended",
"onpointerover",
"onscrollend",
"onformdata",
"ontransitionrun",
"onanimationcancel",
"ondrag",
"onchange",
"onbeforeinstallprompt",
"onbeforexrselect",
"onmessage",
"ontransitioncancel",
"onpointerdown",
"onabort",
"onpointerout",
"oncuechange",
"ongotpointercapture",
"onscrollsnapchanging",
"onsearch",
"onsubmit",
"onstalled",
"onsuspend",
"onreset",
"onerror",
"onresize",
"onmouseenter",
"ongamepaddisconnected",
"ondragover",
"onbeforetoggle",
"onmouseover",
"onpagehide",
"onmousemove",
"onratechange",
"oncommand",
"onmessageerror",
"onwheel",
"ondevicemotion",
"onauxclick",
"ontransitionend",
"onpaste",
"onpageswap",
"ononline",
"ondeviceorientationabsolute",
"onkeydown",
"onclose",
"onselect",
"onpageshow",
"onpointercancel",
"onbeforematch",
"onpointerrawupdate",
"ondragleave",
"onscrollsnapchange",
"onseeked",
"onwaiting",
"onbeforeunload",
"onplaying",
"onvolumechange",
"ondragend",
"onstorage",
"onloadeddata",
"onfocus",
"onoffline",
"onplay",
"onafterprint",
"onclick",
"oncut",
"onmouseout",
"ondblclick",
"oncanplay",
"onloadstart",
"onappinstalled",
"onpointermove",
"ontoggle",
"oncontextmenu",
"onblur",
"oncancel",
"onbeforeprint",
"oncontextrestored",
"onloadedmetadata",
"onpointerup",
"onlanguagechange",
"oncopy",
"onselectstart",
"onscroll",
"onload",
"ondragstart",
"onbeforeinput",
"oncanplaythrough",
"oninput",
"oninvalid",
"ontimeupdate",
"ondurationchange",
"onselectionchange",
"onmouseup",
"location",
"onkeypress",
"onpointerleave",
"oncontextlost",
"ondrop",
"onsecuritypolicyviolation",
"oncontentvisibilityautostatechange",
"ondeviceorientation",
"onseeking",
"onrejectionhandled",
"onunload",
"onmouseleave",
"onhashchange",
"onpointerenter",
"onmousewheel",
"onunhandledrejection",
"ondragenter",
"onpopstate",
"onpagereveal",
"onemptied"
]
},
"overrides": [
{
"includes": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
"linter": {
"rules": {
"complexity": { "noArguments": "error" },
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidBuiltinInstantiation": "off",
"noInvalidConstructorSuper": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": { "useConst": "error" },
"suspicious": {
"noClassAssign": "off",
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"noVar": "error",
"noWith": "off",
"useGetterReturn": "off"
}
}
}
},
{
"includes": ["scripts/**/*.js"],
"linter": {
"rules": {
"style": { "noCommonJs": "off" }
}
}
},
{ "includes": ["*.js", "*.mjs"], "linter": { "rules": {} } },
{
"includes": ["**/*.{ts,tsx}"],
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": "warn",
"useHookAtTopLevel": "error"
}
}
}
}
],
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
}
}
================================================
FILE: build/entitlements.mac.plist
================================================
com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
com.apple.security.cs.allow-dyld-environment-variables
com.apple.security.cs.disable-library-validation
================================================
FILE: dev-app-update.yml
================================================
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: deskreen-ce-updater
================================================
FILE: electron-builder.yml
================================================
appId: com.deskreen-ce.app
productName: Deskreen CE
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md,AGENTS.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
extraResources:
- from: out/client-viewer
to: client-viewer
asarUnpack:
- resources/**
win:
artifactName: ${name}-${version}-${arch}.${ext}
executableName: Deskreen CE
target:
- portable
- msi
nsis: null
mac:
artifactName: ${name}-${version}-${arch}.${ext}
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
extendInfo: []
notarize: false
dmg:
artifactName: ${name}-${version}-${arch}.${ext}
linux:
target:
- AppImage
- rpm
- deb
# - snap
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}-${arch}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
================================================
FILE: electron.vite.config.ts
================================================
import { resolve } from 'path';
import {
defineConfig,
externalizeDepsPlugin,
bytecodePlugin,
} from 'electron-vite';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import react from '@vitejs/plugin-react';
import fs from 'fs-extra';
// Custom Vite plugin to copy the 'client-viewer/dist' directory
const copyClientViewerStaticFiles = () => {
return {
name: 'copy-client-viewer-static-files', // A unique name for your plugin
// The 'writeBundle' hook runs after the bundles have been written to disk
async writeBundle() {
const sourceDir = resolve(__dirname, 'src/client-viewer/dist');
const destDir = resolve(__dirname, 'out/client-viewer');
console.log(`Attempting to copy static files from: ${sourceDir}`);
console.log(`To destination: ${destDir}`);
try {
// Ensure the destination directory exists and is empty before copying
await fs.emptyDir(destDir);
// Copy the entire contents of the source directory to the destination
await fs.copy(sourceDir, destDir);
console.log(
'Successfully copied client-viewer/dist to out/client-viewer',
);
} catch (err) {
console.error(`Error copying static files: ${err}`);
}
},
};
};
const copySimplePeerMinJsStaticFiles = () => {
return {
name: 'copy-simple-peer-min-js-static-files',
async writeBundle() {
const sourceFile = resolve(
__dirname,
'node_modules/simple-peer/simplepeer.min.js',
);
const destDir = resolve(__dirname, 'out/renderer/assets');
console.log(`Attempting to copy simple-peer.min.js from: ${sourceFile}`);
console.log(`To destination: ${destDir}`);
try {
// Ensure the destination directory exists
await fs.ensureDir(destDir);
// Copy the file to the destination
await fs.copyFile(sourceFile, resolve(destDir, 'simplepeer.min.js'));
console.log(
'Successfully copied simple-peer.min.js to out/client-viewer/static/js',
);
} catch (err) {
console.error(`Error copying simple-peer.min.js: ${err}`);
}
},
};
};
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin(), bytecodePlugin()],
},
preload: {
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/preload/index.ts'),
helperRenderer: resolve(__dirname, 'src/preload/index.ts'),
// webview: resolve(__dirname, 'src/preload/webview.js')
},
},
},
plugins: [externalizeDepsPlugin(), bytecodePlugin()],
},
renderer: {
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html'),
helperRenderer: resolve(
__dirname,
'src/renderer/peerConnectionHelperRendererWindowIndex.html',
),
},
},
},
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@common': resolve('src/common'),
},
},
plugins: [
react(),
copyClientViewerStaticFiles(),
copySimplePeerMinJsStaticFiles(),
],
},
});
================================================
FILE: package.json
================================================
{
"name": "deskreen-ce",
"version": "3.2.13",
"description": "Screen sharing and present screen: Turn any device into a secondary screen for your computer",
"main": "./out/main/index.js",
"author": "deskreen.com",
"homepage": "https://electron-vite.org",
"scripts": {
"clean": "rm -rf dist out src/client-viewer/dist",
"format": "biome format --write . --max-diagnostics=none",
"lint": "biome lint . --max-diagnostics=none",
"lint:error-only": "biome lint --write . --max-diagnostics=none --diagnostic-level=error",
"typecheck:client-viewer": "tsc --noEmit -p src/client-viewer/tsconfig.app.json --composite false",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:client-viewer && npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "concurrently \"pnpm electron-vite dev\" \"pnpm:devClientViewer\"",
"devClientViewer": "cd src/client-viewer && pnpm dev --host --port=5174",
"buildDev": "npm run buildClientViewer && electron-vite build",
"build": "npm run typecheck && npm run buildClientViewer && electron-vite build",
"buildClientViewer": "cd src/client-viewer && pnpm build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:win:ia32": "npm run build && electron-builder --win --ia32",
"build:win:arm64": "npm run build && electron-builder --win --arm64",
"build:mac": "npm run buildClientViewer && electron-vite build && electron-builder --mac",
"build:mac:arm64": "npm run buildClientViewer && electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "npm run buildClientViewer && electron-vite build && electron-builder --mac --x64",
"build:linux": "npm run buildClientViewer && electron-vite build && electron-builder --linux",
"build:linux:arm64": "npm run buildClientViewer && electron-vite build && electron-builder --linux --arm64",
"build:linux:armv7l": "npm run buildClientViewer && electron-vite build && electron-builder --linux --armv7l"
},
"dependencies": {
"@blueprintjs/core": "^6.3.4",
"@blueprintjs/select": "^6.0.8",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@fortawesome/fontawesome-free": "^7.1.0",
"@material-ui/core": "^4.12.4",
"@roamhq/wrtc": "^0.9.1",
"@types/lodash": "^4.17.20",
"axios": "^1.13.2",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"detect-port": "^2.1.0",
"electron-devtools-installer": "^4.0.0",
"electron-log": "^5.4.3",
"electron-settings": "^4.0.4",
"electron-store": "^10.1.0",
"electron-updater": "^6.6.2",
"fontsource-lexend-peta": "^4.0.0",
"fs-extra": "^11.3.2",
"get-port": "^7.1.0",
"i18next": "^25.6.2",
"i18next-fs-backend": "^2.6.0",
"i18next-sync-fs-backend": "^1.1.1",
"kcors": "^2.2.2",
"koa": "^3.1.1",
"koa-router": "^14.0.0",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"node-forge": "^1.3.1",
"normalize.css": "^8.0.1",
"qrcode.react": "^4.2.0",
"react-flexbox-grid": "^2.1.2",
"react-toast-notifications": "^2.5.1",
"shortid": "^2.2.17",
"simple-peer": "^9.11.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@biomejs/biome": "2.3.6",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/electron-devtools-installer": "^4.0.0",
"@types/i18next-node-fs-backend": "^2.1.5",
"@types/kcors": "^2.2.9",
"@types/koa": "^3.0.1",
"@types/koa-router": "^7.4.9",
"@types/koa-send": "^4.1.6",
"@types/koa-static": "^4.0.4",
"@types/node-forge": "^1.3.14",
"@types/qrcode.react": "^3.0.0",
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.2",
"@types/react-toast-notifications": "^2.4.1",
"@types/simple-peer": "^9.11.9",
"@types/socket.io": "^3.0.2",
"@types/socket.io-client": "^3.0.0",
"@vercel/blob": "^2.0.0",
"@vitejs/plugin-react": "^5.1.0",
"concurrently": "^9.2.1",
"electron": "^37.9.0",
"electron-builder": "^26.7.0",
"electron-vite": "^4.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^15.7.4",
"typescript": "^5.9.3",
"vite": "^7.2.2"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron"
]
}
}
================================================
FILE: scripts/bump-version.js
================================================
#!/usr/bin/env node
'use strict';
const fs = require('node:fs/promises');
const path = require('node:path');
const { execSync, spawnSync } = require('node:child_process');
const rootDir = path.resolve(__dirname, '..');
const rootPackagePath = path.join(rootDir, 'package.json');
const clientPackagePath = path.join(
rootDir,
'src',
'client-viewer',
'package.json',
);
const envPath = path.join(rootDir, 'src', 'client-viewer', '.env');
const bumpFlags = new Map([
['--major', 'major'],
['--minor', 'minor'],
['--patch', 'patch'],
]);
const parsedArgs = parseArgs(process.argv.slice(2));
void main(parsedArgs).catch((error) => {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(error);
}
process.exit(1);
});
function parseArgs(argv) {
const matchedFlags = argv.filter((arg) => bumpFlags.has(arg));
if (matchedFlags.length === 0) {
throw new Error(
'Missing bump flag. Use one of --major, --minor, or --patch.',
);
}
if (matchedFlags.length > 1) {
throw new Error('Multiple bump flags provided. Please supply only one.');
}
return {
type: bumpFlags.get(matchedFlags[0]),
};
}
async function main({ type }) {
process.chdir(rootDir);
await ensureCleanGit();
const currentVersion = await readVersion();
const nextVersion = bumpVersion(currentVersion, type);
await Promise.all([
writeRootPackage(nextVersion),
writeClientPackage(nextVersion),
writeEnv(nextVersion),
]);
await stageFiles();
await commit(nextVersion);
await tag(nextVersion);
console.log(`Version bumped from ${currentVersion} to ${nextVersion}`);
}
async function ensureCleanGit() {
const output = execSync('git status --porcelain', {
encoding: 'utf8',
}).trim();
if (output.length > 0) {
throw new Error(
'Working tree is not clean. Please commit or stash changes before bumping the version.',
);
}
}
async function readVersion() {
const raw = await fs.readFile(rootPackagePath, 'utf8');
const parsed = JSON.parse(raw);
const version = parsed.version;
if (typeof version !== 'string') {
throw new Error('Root package.json does not contain a valid version.');
}
assertValidSemver(version);
return version;
}
async function writeRootPackage(version) {
await updatePackageJSON(rootPackagePath, version);
}
async function writeClientPackage(version) {
await updatePackageJSON(clientPackagePath, version);
}
async function updatePackageJSON(filePath, version) {
const raw = await fs.readFile(filePath, 'utf8');
const parsed = JSON.parse(raw);
parsed.version = version;
const value = `${JSON.stringify(parsed, null, 2)}\n`;
await fs.writeFile(filePath, value, 'utf8');
}
async function writeEnv(version) {
const raw = await fs.readFile(envPath, 'utf8');
const lines = raw.split(/\r?\n/);
let replaced = false;
const nextLines = lines.map((line) => {
if (line.startsWith('VITE_CLIENT_VIEWER_VERSION=')) {
replaced = true;
return `VITE_CLIENT_VIEWER_VERSION=${version}`;
}
return line;
});
if (!replaced) {
nextLines.push(`VITE_CLIENT_VIEWER_VERSION=${version}`);
}
await fs.writeFile(envPath, `${nextLines.join('\n')}\n`, 'utf8');
}
function bumpVersion(current, type) {
const [major, minor, patch] = current.split('.').map(Number);
if ([major, minor, patch].some((part) => Number.isNaN(part))) {
throw new Error(
`Unable to bump version. Invalid semantic version: ${current}`,
);
}
if (type === 'major') {
return `${major + 1}.0.0`;
}
if (type === 'minor') {
return `${major}.${minor + 1}.0`;
}
return `${major}.${minor}.${patch + 1}`;
}
function assertValidSemver(value) {
const semverPattern = /^(\d+)\.(\d+)\.(\d+)$/;
if (!semverPattern.test(value)) {
throw new Error(`Invalid semantic version: ${value}`);
}
}
async function stageFiles() {
execSync(`git add ${quote(rootPackagePath)} ${quote(clientPackagePath)}`, {
stdio: 'inherit',
});
}
async function commit(version) {
const message = version;
execSync(`git commit -m ${quote(message)}`, { stdio: 'inherit' });
}
async function tag(version) {
const tagName = `v${version}`;
const result = spawnSync('git', [
'show-ref',
'--tags',
'--quiet',
`refs/tags/${tagName}`,
]);
if (result.status === 0) {
throw new Error(`Tag ${tagName} already exists.`);
}
if (result.status !== 1) {
throw (
result.error ??
new Error(`git show-ref failed with status ${result.status ?? 'unknown'}`)
);
}
execSync(`git tag ${quote(tagName)}`, { stdio: 'inherit' });
}
function quote(value) {
return `'${value.replace(/'/g, "'\\''")}'`;
}
================================================
FILE: scripts/undo-version-bump.js
================================================
#!/usr/bin/env node
'use strict';
const fs = require('node:fs/promises');
const path = require('node:path');
const { execSync, spawnSync } = require('node:child_process');
const rootDir = path.resolve(__dirname, '..');
const rootPackagePath = path.join(rootDir, 'package.json');
const clientPackagePath = path.join(
rootDir,
'src',
'client-viewer',
'package.json',
);
const envPath = path.join(rootDir, 'src', 'client-viewer', '.env');
void main().catch((error) => {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(error);
}
process.exit(1);
});
async function main() {
process.chdir(rootDir);
await ensureCleanGit();
const latestTag = await getLatestVersionTag();
if (!latestTag) {
throw new Error('No version tag found. Nothing to undo.');
}
const tagVersion = latestTag.replace(/^v/, '');
console.log(
`Found latest version tag: ${latestTag} (version: ${tagVersion})`,
);
const tagCommit = await getTagCommit(latestTag);
const parentCommit = await getParentCommit(tagCommit);
if (!parentCommit) {
throw new Error(
'Cannot find parent commit. The version bump commit might be the first commit.',
);
}
const previousVersion = await getVersionFromCommit(parentCommit);
console.log(`Previous version: ${previousVersion}`);
await Promise.all([
writeRootPackage(previousVersion),
writeClientPackage(previousVersion),
writeEnv(previousVersion),
]);
await deleteRemoteTag(latestTag);
await deleteLocalTag(latestTag);
console.log(`Version reverted from ${tagVersion} to ${previousVersion}`);
console.log(
`Tag ${latestTag} has been deleted locally and remotely (if it existed).`,
);
console.log('Files have been updated. You may want to commit these changes.');
}
async function ensureCleanGit() {
const output = execSync('git status --porcelain', {
encoding: 'utf8',
}).trim();
if (output.length > 0) {
throw new Error(
'Working tree is not clean. Please commit or stash changes before undoing the version bump.',
);
}
}
async function getLatestVersionTag() {
const result = spawnSync(
'git',
['tag', '--sort=-version:refname', '--list', 'v*'],
{
encoding: 'utf8',
},
);
if (result.status !== 0) {
throw new Error('Failed to get git tags.');
}
const tags = result.stdout.trim().split('\n').filter(Boolean);
return tags[0] || null;
}
async function getTagCommit(tagName) {
const result = spawnSync('git', ['rev-parse', tagName], { encoding: 'utf8' });
if (result.status !== 0) {
throw new Error(`Failed to get commit for tag ${tagName}.`);
}
return result.stdout.trim();
}
async function getParentCommit(commitHash) {
const result = spawnSync('git', ['rev-parse', `${commitHash}^`], {
encoding: 'utf8',
});
if (result.status !== 0) {
return null;
}
return result.stdout.trim();
}
async function getVersionFromCommit(commitHash) {
const result = spawnSync('git', ['show', `${commitHash}:package.json`], {
encoding: 'utf8',
});
if (result.status !== 0) {
throw new Error(`Failed to read package.json from commit ${commitHash}.`);
}
const parsed = JSON.parse(result.stdout);
const version = parsed.version;
if (typeof version !== 'string') {
throw new Error(
'Root package.json from parent commit does not contain a valid version.',
);
}
assertValidSemver(version);
return version;
}
async function writeRootPackage(version) {
await updatePackageJSON(rootPackagePath, version);
}
async function writeClientPackage(version) {
await updatePackageJSON(clientPackagePath, version);
}
async function updatePackageJSON(filePath, version) {
const raw = await fs.readFile(filePath, 'utf8');
const parsed = JSON.parse(raw);
parsed.version = version;
const value = `${JSON.stringify(parsed, null, 2)}\n`;
await fs.writeFile(filePath, value, 'utf8');
}
async function writeEnv(version) {
let raw;
try {
raw = await fs.readFile(envPath, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
raw = '';
} else {
throw error;
}
}
const lines = raw.split(/\r?\n/);
let replaced = false;
const nextLines = lines.map((line) => {
if (line.startsWith('VITE_CLIENT_VIEWER_VERSION=')) {
replaced = true;
return `VITE_CLIENT_VIEWER_VERSION=${version}`;
}
return line;
});
if (!replaced) {
nextLines.push(`VITE_CLIENT_VIEWER_VERSION=${version}`);
}
await fs.writeFile(envPath, `${nextLines.join('\n')}\n`, 'utf8');
}
function assertValidSemver(value) {
const semverPattern = /^(\d+)\.(\d+)\.(\d+)$/;
if (!semverPattern.test(value)) {
throw new Error(`Invalid semantic version: ${value}`);
}
}
async function deleteLocalTag(tagName) {
const result = spawnSync('git', [
'show-ref',
'--tags',
'--quiet',
`refs/tags/${tagName}`,
]);
if (result.status === 0) {
execSync(`git tag -d ${quote(tagName)}`, { stdio: 'inherit' });
console.log(`Deleted local tag: ${tagName}`);
} else {
console.log(`Local tag ${tagName} does not exist.`);
}
}
async function deleteRemoteTag(tagName) {
const result = spawnSync('git', ['ls-remote', '--tags', 'origin', tagName], {
encoding: 'utf8',
});
if (result.status === 0 && result.stdout.trim().length > 0) {
execSync(`git push origin :refs/tags/${quote(tagName)}`, {
stdio: 'inherit',
});
console.log(`Deleted remote tag: ${tagName}`);
} else {
console.log(`Remote tag ${tagName} does not exist.`);
}
}
function quote(value) {
return `'${value.replace(/'/g, "'\\''")}'`;
}
================================================
FILE: src/client-viewer/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: src/client-viewer/README.md
================================================
# Deskreen CE Client-Viewer
AGPL-3.0 License © [Pavlo (Paul) Buidenkov](https://github.com/pavlobu/deskreen)
================================================
FILE: src/client-viewer/index.html
================================================
Deskreen CE Viewer
================================================
FILE: src/client-viewer/package.json
================================================
{
"name": "deskreen-ce-client-viewer",
"private": true,
"version": "3.2.13",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@blueprintjs/core": "^6.3.4",
"i18next": "^25.6.2",
"i18next-http-backend": "^3.0.2",
"node-forge": "^1.3.1",
"normalize.css": "^8.0.1",
"pixelmatch": "^7.1.0",
"react": "^19.2.0",
"react-app-polyfill": "^3.0.0",
"react-dom": "^19.2.0",
"react-flexbox-grid": "^2.1.2",
"react-i18next": "^15.7.4",
"react-player": "^3.3.3",
"react-spinners": "^0.17.0",
"screenfull": "^6.0.2",
"shortid": "^2.2.17",
"simple-peer": "^9.11.1",
"socket.io-client": "^4.8.1",
"ua-parser-js": "^2.0.6",
"video.js": "^8.23.4"
},
"devDependencies": {
"@types/node-forge": "^1.3.14",
"@types/pixelmatch": "^5.2.6",
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.2",
"@types/resemblejs": "^4.1.3",
"@types/shortid": "^2.2.0",
"@types/socket.io-client": "^3.0.0",
"@types/ua-parser-js": "^0.7.39",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "^5.1.0",
"typescript": "~5.9.3",
"vite": "^7.2.2",
"vite-plugin-node-polyfills": "^0.24.0"
}
}
================================================
FILE: src/client-viewer/public/img/.gitkeep
================================================
================================================
FILE: src/client-viewer/public/locales/da/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Venter på at brugeren klikker TILLAD knappen på skærmdelingsenheden...",
"Waiting for user to select source to share from screen sharing device...": "Venter på at brugeren vælger kilden, som skal deles fra skærmdelingsenheden...",
"My Device Info": "Min enhedsinfo",
"Device Type": "Enhedstype",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Din Enheds IP burde matche sammen med den Enheds IP, som ses i advarselspopup'en vist på din computer, hvor Deskreen-CE kører",
"Device IP": "Enhedens IP",
"Device Browser": "Enhedens Browser",
"Device OS": "Enhedens Operativsystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Disse detaljer skal matche med dem, som du ser i advarselspopup'en på computerskærmen, hvor Deskreen-CE kører",
"Deskreen-CE Screen Viewer": "Deskreen-CE Skærmviser",
"Connected!": "Forbundet!",
"Error occurred": "Der skete en fejl",
"Deskreen-CE Error Dialog": "Deskreen-CE Fejl Dialog",
"Something went wrong": "Noget gik galt",
"You may close this browser window then try to connect again": "Prøv at lukke dette browservindue og forbind igen",
"An unknown error occurred": "Der opstod en ukendt fejl",
"You were not allowed to connect": "Der blev ikke tilladt forbindelse",
"You were disconnected": "Du blev afbrudt",
"WebRTC error occurred": "Der opstod en WebRTC fejl",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Hvis du er vild med Deskreen-CE, så overvej at bidrage til Deskreen-CE financielt. Deskreen-CE er open-source. Dine donationer hjælper os med at forblive motiverede for at gøre Deskreen-CE endnu bedre.",
"Donate": "Donér",
"get-deskreen-pro": "Hent Deskreen Pro",
"get-deskreen-pro-tooltip": "Hent Deskreen Pro - åbner downloadsiden.",
"Video stream is paused": "Videostream er pauset",
"Video stream is playing": "Videostream kører",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Videostream blev sat på pause efter at have forladt fuldskærmstilstand. Klik på Kør for at fortsætte.",
"Pause": "Pause",
"Play": "Kør",
"Video Settings": "Videoindstillinger",
"Flip": "Vend",
"Video quality has been changed to": "Videokvaliteten er blevet ændret til",
"Click to Open Video Settings": "Klik her for at åbne Videoindstillinger",
"Click to Enter Full Screen Mode": "Klik her for at gå ind i fuldskærmstilstand",
"Click to Play Video": "Klik for at afspille video",
"Click to Pause Video": "Klik for at pause video",
"Default video player has been turned OFF": "Standard videospiller er blevet slået FRA",
"Default video player has been turned ON": "Standard videospiller er blevet slået TIL",
"ON": "TIL",
"OFF": "FRA",
"Default Video Player": "Standard Videospiller",
"Click to visit our website": "Klik jer for at besøge vores hjemmeside",
"Video is flipped horizontally": "Videoen er vendt horisontalt",
"flip-the-screen-is-pro-version-only": "Vende skærmen er kun tilgængelig i Pro-versionen",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klik jer for at se forbindelsesinfo",
"Pair ID": "Par ID",
"Unpair": "Annullér Pardannelse",
"Session ID": "Sessionsid",
"Click to boost video stream if it is lagging": "Klik her for at booste videostreamen, hvis det lagger",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Privatlivsmeddelelse: Analyse i Deskreen CE Viewer",
"Analytics Reference": "Analytik Reference",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Denne app bruger Google Analytics (en gratis tjeneste fra Google) til anonymt at spore grundlæggende brugsdata. Det hjælper os med at forstå, hvordan appen bruges, så vi kan forbedre den for alle.",
"What we collect:": "Det vi indsamler:",
"Page views (which screens you visit)": "Sidevisninger (hvilke skærme du besøger)",
"Time spent on pages": "Tid brugt på sider",
"Basic device info (browser type, screen size)": "Grundlæggende enhedsinfo (browsertype, skærmstørrelse)",
"Your IP address (anonymized — last part removed for privacy)": "Din IP-adresse (anonymiseret — den sidste del fjernes af hensyn til privatlivet)",
"What we DON'T collect:": "Det vi IKKE indsamler:",
"Personal info (names, emails, passwords)": "Personlige oplysninger (navne, e-mails, adgangskoder)",
"Exact location": "Præcis placering",
"Any files or content you interact with": "Filer eller indhold, du interagerer med",
"Why anonymous?": "Hvorfor anonymt?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Din IP bliver automatisk afkortet, og ingen kan identificere dig personligt ud fra disse data.",
"Your options:": "Dine muligheder:",
"Continue:": "Fortsæt:",
"We'll track anonymized usage to help improve the app.": "Vi registrerer anonymiseret brug for at hjælpe os med at forbedre appen.",
"Opt out:": "Fravælg:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik på knappen Afslå nedenfor for at deaktivere sporing. (Vi respekterer dette valg, men du kan gå glip af fremtidige forbedringer baseret på samlet feedback.)",
"Data goes to: Google Analytics. See their privacy policy.": "Data sendes til: Google Analytics. Se deres privatlivspolitik.",
"Accept": "Accepter",
"Allow": "Tillad",
"Deny": "Afslå",
"re-initiate-connection": "Genstart forbindelse",
"Privacy Settings": "Privatindstillinger",
"Change your preference:": "Ændre din præference:",
"Enable analytics:": "Aktivér analyse:",
"Disable analytics:": "Deaktivér analyse:",
"Enable Analytics": "Aktivér analyse",
"Disable Analytics": "Deaktivér analyse",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik på knappen Deaktivér nedenfor for at stoppe sporing. (Vi respekterer dette valg, men du kan gå glip af fremtidige forbedringer baseret på kollektiv feedback.)",
"their privacy policy": "deres privatlivspolitik"
}
================================================
FILE: src/client-viewer/public/locales/de/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Warten bis der Nutzer auf dem Freigabegerät auf ZULASSEN klickt ...",
"Waiting for user to select source to share from screen sharing device...": "Warten bis der Nutzer eine Quelle für die Freigabe auswählt...",
"My Device Info": "Meine Geräteinformationen",
"Device Type": "Gerätetyp",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Deine Geräte-IP sollte mit der \"Geräte-IP\" im Dialog auf dem Computer, auf dem Deskreen-CE läuft, übereinstimmen.",
"Device IP": "Geräte-IP",
"Device Browser": "Geräte-Browser",
"Device OS": "Geräte-Betriebssystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Diese Informationen sollten mit denen im Dialog auf dem Freigabegerät übereinstimmen.",
"Deskreen-CE Screen Viewer": "Deskreen-CE Bildschrimansicht",
"Connected!": "Verbunden!",
"Error occurred": "Ein Fehler ist aufgetreten",
"Deskreen-CE Error Dialog": "Deskreen-CE Fehler Dialog",
"Something went wrong": "Etwas ist schief gegangen",
"You may close this browser window then try to connect again": "Schließe das Browserfenster und probiere es erneut",
"An unknown error occurred": "Ein unbekannter Fehler ist aufgetreten",
"You were not allowed to connect": "Die Verbindung wurde nicht zugelassen",
"You were disconnected": "Die Verbindung wurde getrennt",
"WebRTC error occurred": "WebRTC Fehler aufgetreten",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Wenn dir Deskreen-CE gefällt, denke über eine Spende nach. Deskreen-CE ist Open-Source. Spenden motivieren uns, Deskreen-CE noch besser zu machen.",
"Donate": "Spenden",
"get-deskreen-pro": "Deskreen Pro erhalten",
"get-deskreen-pro-tooltip": "Deskreen Pro erhalten - öffnet die Download-Seite.",
"Video stream is paused": "Videostream ist pausiert",
"Video stream is playing": "Videostream läuft",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Videostream wurde nach dem Verlassen des Vollbildmodus pausiert. Bitte klicken Sie auf Abspielen, um fortzufahren.",
"Pause": "Pause",
"Play": "Abspielen",
"Video Settings": "Video Einstellungen",
"Flip": "Drehen",
"Video quality has been changed to": "Videoqualität wurde geändert zu",
"Click to Open Video Settings": "Klicken um Videoeinstellungen zu öffnen",
"Click to Enter Full Screen Mode": "Klicken für Vollbild",
"Click to Play Video": "Klicken um Video abzuspielen",
"Click to Pause Video": "Klicken um Video anzuhalten",
"Default video player has been turned OFF": "Standard Video-Player wurde ausgeschaltet",
"Default video player has been turned ON": "Standard Video-Player wurde eingeschaltet",
"ON": "AN",
"OFF": "AUS",
"Default Video Player": "Standard Video-Player",
"Click to visit our website": "Klicken um unsere Website zu besuchen",
"Video is flipped horizontally": "Das Video ist horizontal gedreht",
"flip-the-screen-is-pro-version-only": "Bildschirm umdrehen ist nur in der Pro-Version verfügbar",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klicken um Verbindungsinformationen anzuzeigen",
"Pair ID": "Kopplungs-ID",
"Unpair": "Entkoppeln",
"Session ID": "Sitzungs-ID",
"Click to boost video stream if it is lagging": "Klicken um den Videostream zu verbessern, wenn er verzögert ist.",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Datenschutzhinweis: Analyse in Deskreen CE Viewer",
"Analytics Reference": "Analytik-Referenz",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Diese App verwendet Google Analytics (einen kostenlosen Dienst von Google), um anonyme Nutzungsdaten zu erfassen. So verstehen wir, wie die App genutzt wird, und können sie für alle verbessern.",
"What we collect:": "Was wir sammeln:",
"Page views (which screens you visit)": "Seitenaufrufe (welche Ansichten du besuchst)",
"Time spent on pages": "Verweildauer auf Seiten",
"Basic device info (browser type, screen size)": "Grundlegende Geräteinformationen (Browsertyp, Bildschirmgröße)",
"Your IP address (anonymized — last part removed for privacy)": "Deine IP-Adresse (anonymisiert – der letzte Teil wird aus Datenschutzgründen entfernt)",
"What we DON'T collect:": "Was wir NICHT sammeln:",
"Personal info (names, emails, passwords)": "Personenbezogene Daten (Namen, E-Mails, Passwörter)",
"Exact location": "Genauer Standort",
"Any files or content you interact with": "Dateien oder Inhalte, mit denen du interagierst",
"Why anonymous?": "Warum anonym?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Deine IP wird automatisch gekürzt, sodass dich niemand anhand dieser Daten identifizieren kann.",
"Your options:": "Deine Optionen:",
"Continue:": "Weiter:",
"We'll track anonymized usage to help improve the app.": "Wir erfassen anonymisierte Nutzung, um die App zu verbessern.",
"Opt out:": "Ablehnen:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicke auf den Button Ablehnen unten, um das Tracking zu deaktivieren. (Wir respektieren diese Entscheidung, aber du könntest zukünftige Verbesserungen verpassen, die auf kollektivem Feedback basieren.)",
"Data goes to: Google Analytics. See their privacy policy.": "Datenempfänger: Google Analytics. Lies deren Datenschutzerklärung.",
"Accept": "Akzeptieren",
"Allow": "Erlauben",
"Deny": "Ablehnen",
"re-initiate-connection": "Verbindung erneut herstellen",
"Privacy Settings": "Datenschutzeinstellungen",
"Change your preference:": "Deine Präferenz ändern:",
"Enable analytics:": "Analytik aktivieren:",
"Disable analytics:": "Analytik deaktivieren:",
"Enable Analytics": "Analytik aktivieren",
"Disable Analytics": "Analytik deaktivieren",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicke auf den Button Deaktivieren unten, um das Tracking zu stoppen. (Wir respektieren diese Entscheidung, aber du könntest zukünftige Verbesserungen verpassen, die auf kollektivem Feedback basieren.)",
"their privacy policy": "deren Datenschutzerklärung"
}
================================================
FILE: src/client-viewer/public/locales/en/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Waiting for video stream of screen sharing device...",
"Waiting for user to select source to share from screen sharing device...": "Waiting for user to select source to share from screen sharing device...",
"My Device Info": "My Device Info",
"Device Type": "Device Type",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Your Device IP should match with \"Device IP\" in alert popup appeared on your computer, where Deskreen-CE is running.",
"Device IP": "Device IP",
"Device Browser": "Device Browser",
"Device OS": "Device OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "These details should match with the ones that you see in alert popup on screen sharing device.",
"Deskreen-CE Screen Viewer": "Deskreen-CE Screen Viewer",
"Connected!": "Connected!",
"Error occurred": "Error occurred",
"Deskreen-CE Error Dialog": "Deskreen-CE Error Dialog",
"Something went wrong": "Something went wrong",
"You may close this browser window then try to connect again": "You may close this browser window then try to connect again",
"An unknown error occurred": "An unknown error occurred",
"You were not allowed to connect": "You were not allowed to connect",
"You were disconnected": "You were disconnected",
"WebRTC error occurred": "WebRTC error occurred",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "If you like Deskreen-CE, consider contributing financially. Deskreen-CE is open-source. Your donations keep us motivated to make Deskreen-CE even better.",
"Donate": "Donate",
"get-deskreen-pro": "Get Deskreen Pro",
"get-deskreen-pro-tooltip": "Get Deskreen Pro - opens the download page.",
"Video stream is paused": "Video stream is paused",
"Video stream is playing": "Video stream is playing",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Video stream paused after exiting fullscreen. Please click Play to continue.",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Video Settings",
"Flip": "Flip",
"Video quality has been changed to": "Video quality has been changed to",
"Click to Open Video Settings": "Click to Open Video Settings",
"Click to Enter Full Screen Mode": "Click to Enter Full Screen Mode",
"Click to Play Video": "Click to Play Video",
"Click to Pause Video": "Click to Pause Video",
"Default video player has been turned OFF": "Default video player has been turned OFF",
"Default video player has been turned ON": "Default video player has been turned ON",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "Default Video Player",
"Click to visit our website": "Click to visit our website",
"Video is flipped horizontally": "Video is flipped horizontally",
"flip-the-screen-is-pro-version-only": "Flip the screen is pro version only",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"re-initiate-connection": "Re-initiate Connection",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Privacy Notice: Analytics in Deskreen CE Viewer",
"Analytics Reference": "Analytics Reference",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.",
"What we collect:": "What we collect:",
"Page views (which screens you visit)": "Page views (which screens you visit)",
"Time spent on pages": "Time spent on pages",
"Basic device info (browser type, screen size)": "Basic device info (browser type, screen size)",
"Your IP address (anonymized — last part removed for privacy)": "Your IP address (anonymized — last part removed for privacy)",
"What we DON'T collect:": "What we DON'T collect:",
"Personal info (names, emails, passwords)": "Personal info (names, emails, passwords)",
"Exact location": "Exact location",
"Any files or content you interact with": "Any files or content you interact with",
"Why anonymous?": "Why anonymous?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Your IP is automatically shortened, and no one can identify you personally from this data.",
"Your options:": "Your options:",
"Continue:": "Continue:",
"We'll track anonymized usage to help improve the app.": "We'll track anonymized usage to help improve the app.",
"Opt out:": "Opt out:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)",
"Data goes to: Google Analytics. See their privacy policy.": "Data goes to: Google Analytics. See their privacy policy.",
"Accept": "Accept",
"Allow": "Allow",
"Deny": "Deny",
"Privacy Settings": "Privacy Settings",
"Change your preference:": "Change your preference:",
"Enable analytics:": "Enable analytics:",
"Disable analytics:": "Disable analytics:",
"Enable Analytics": "Enable Analytics",
"Disable Analytics": "Disable Analytics",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)",
"their privacy policy": "their privacy policy"
}
================================================
FILE: src/client-viewer/public/locales/es/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Esperando que el usuario haga clic en el botón PERMITIR en el dispositivo para compartir pantalla ...",
"Waiting for user to select source to share from screen sharing device...": "Esperando que el usuario seleccione la fuente para compartir desde el dispositivo para compartir pantalla ...",
"My Device Info": "Información de mi dispositivo",
"Device Type": "Tipo del dispositivo",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "La IP de tu dispositivo debe coincidir con \"IP del dispositivo \" en la ventana emergente de alerta que apareció en la computadora donde se está ejecutando Deskreen-CE.",
"Device IP": "IP del dispositivo",
"Device Browser": "Navegador del dispositivo",
"Device OS": "SO del dispositivo",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Estos detalles deben coincidir con los que ves en la ventana emergente en el dispositivo para compartir pantalla.",
"Deskreen-CE Screen Viewer": "Visor de pantalla de Deskreen-CE",
"Connected!": "¡Conectado!",
"Error occurred": "Ocurrió un error",
"Deskreen-CE Error Dialog": "Cuadro de diálogo de error de Deskreen-CE",
"Something went wrong": "Algo salió mal",
"You may close this browser window then try to connect again": "Puedes cerrar esta ventana del navegador y luego intentar conectarte nuevamente",
"An unknown error occurred": "Ocurrió un error desconocido",
"You were not allowed to connect": "No se te permitió conectarte",
"You were disconnected": "Fuiste desconectado",
"WebRTC error occurred": "Ocurrió un error de WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Si te gusta Deskreen-CE, considera la posibilidad de contribuir económicamente. Deskreen-CE es de código abierto. Tus donaciones nos mantienen motivados para hacer que Deskreen-CE sea aún mejor.",
"Donate": "Donar",
"get-deskreen-pro": "Obtener Deskreen Pro",
"get-deskreen-pro-tooltip": "Obtener Deskreen Pro - abre la página de descarga.",
"Video stream is paused": "La transmisión de video está en pausa",
"Video stream is playing": "La transmisión de video está en reproducción",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "La transmisión de video se pausó después de salir de pantalla completa. Por favor, haz clic en Reproducir para continuar.",
"Pause": "Pausa",
"Play": "Reproducir",
"Video Settings": "Configuraciones de video",
"Flip": "Voltear",
"Video quality has been changed to": "La calidad de video se ha cambiado a",
"Click to Open Video Settings": "Clic para abrir las configuraciones de video",
"Click to Enter Full Screen Mode": "Clic para entrar en el modo de pantalla completa",
"Click to Play Video": "Clic para reproducir el video",
"Click to Pause Video": "Clic para pausar el video",
"Default video player has been turned OFF": "El reproductor de video predeterminado se ha APAGADO",
"Default video player has been turned ON": "El reproductor de video predeterminado se ha ENCENDIDO",
"ON": "ENCENDER",
"OFF": "APAGAR",
"Default Video Player": "Reproductor de video predeterminado",
"Click to visit our website": "Clic para visitar nuestro sitio web",
"Video is flipped horizontally": "El video se ha volteado horizontalmente",
"flip-the-screen-is-pro-version-only": "Voltear la pantalla está disponible solo en la versión Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Clic para ver la información de la conexión",
"Pair ID": "ID del par",
"Unpair": "Desemparejar",
"Session ID": "ID de sesión",
"Click to boost video stream if it is lagging": "Haz clic para mejorar la transmisión de video si se está retrasando",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Aviso de privacidad: Analítica en Deskreen CE Viewer",
"Analytics Reference": "Referencia de Analítica",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Esta aplicación utiliza Google Analytics (un servicio gratuito de Google) para registrar de manera anónima datos básicos de uso. Esto nos ayuda a entender cómo se usa la aplicación para poder mejorarla para todos.",
"What we collect:": "Lo que recopilamos:",
"Page views (which screens you visit)": "Vistas de página (qué pantallas visitas)",
"Time spent on pages": "Tiempo invertido en las páginas",
"Basic device info (browser type, screen size)": "Información básica del dispositivo (tipo de navegador, tamaño de pantalla)",
"Your IP address (anonymized — last part removed for privacy)": "Tu dirección IP (anonimizada: se elimina la última parte por privacidad)",
"What we DON'T collect:": "Lo que NO recopilamos:",
"Personal info (names, emails, passwords)": "Información personal (nombres, correos electrónicos, contraseñas)",
"Exact location": "Ubicación exacta",
"Any files or content you interact with": "Cualquier archivo o contenido con el que interactúes",
"Why anonymous?": "¿Por qué anónimo?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Tu IP se acorta automáticamente, y nadie puede identificarte personalmente con estos datos.",
"Your options:": "Tus opciones:",
"Continue:": "Continuar:",
"We'll track anonymized usage to help improve the app.": "Registraremos uso anonimizado para ayudar a mejorar la aplicación.",
"Opt out:": "Rechazar:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Haz clic en el botón Rechazar a continuación para desactivar el seguimiento. (Respetaremos esta elección, pero podrías perderte mejoras futuras basadas en comentarios colectivos).",
"Data goes to: Google Analytics. See their privacy policy.": "Los datos van a: Google Analytics. Consulta su política de privacidad.",
"Accept": "Aceptar",
"Allow": "Permitir",
"Deny": "Rechazar",
"re-initiate-connection": "Restablecer conexión",
"Privacy Settings": "Configuración de privacidad",
"Change your preference:": "Cambiar tu preferencia:",
"Enable analytics:": "Habilitar analítica:",
"Disable analytics:": "Deshabilitar analítica:",
"Enable Analytics": "Habilitar Analítica",
"Disable Analytics": "Deshabilitar Analítica",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Haz clic en el botón Deshabilitar a continuación para detener el seguimiento. (Respetaremos esta elección, pero podrías perderte mejoras futuras basadas en comentarios colectivos).",
"their privacy policy": "su política de privacidad"
}
================================================
FILE: src/client-viewer/public/locales/fi/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Odotetaan että käyttäjä napsauttaa SALLI-painiketta ruudunjakolaitteessa...",
"Waiting for user to select source to share from screen sharing device...": "Odotetaan että käyttäjä valitsee ruudunjakolaitteesta lähteen joka jaetaan...",
"My Device Info": "Tiedot laitteestani",
"Device Type": "Laitteen malli",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Laitteesi IP:n tulisi täsmätä \"Laitteen IP\" kohdassa joka näkyy ilmoiteikkunassa tietokoneella jossa Deskreen-CE on käynnissä.",
"Device IP": "Laitteen IP",
"Device Browser": "Laiteselain",
"Device OS": "Laitteen käyttöjärjestelmä",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Näiden yksityiskohtien tulisi täsmätä niiden kanssa jotka näet ruudunjakolaitteen ilmoitekehotteessa, Deskreen-CE:in ollessa käynnissä",
"Deskreen-CE Screen Viewer": "Deskreen-CE-ruutukatselin",
"Connected!": "Yhdistetty!",
"Error occurred": "Tapahtui virhe",
"Deskreen-CE Error Dialog": "Deskreen-CE:in virhekooste",
"Something went wrong": "Jokin meni pieleen",
"You may close this browser window then try to connect again": "Voit sulkea tämän selainikkunan koettaaksesi uudelleenyhdistämistä",
"An unknown error occurred": "Ilmeni tuntematon virhe",
"You were not allowed to connect": "Yhdistämistä ei sallittu",
"You were disconnected": "Sinulta katkesi yhteys",
"WebRTC error occurred": "Ilmeni WebRTC-virhe",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Mikäli pidät Deskreen-CE:istä, harkitsethan rahallista lahjoitusta. Deskreen-CE on avoimen lähdekoodin ohjelma. Lahjoituksesi auttavat motivaatiomme säilymisen kannalta tehdäksemme Deskreen-CE:istä vieläkin paremman.",
"Donate": "Lahjoita",
"get-deskreen-pro": "Hanki Deskreen Pro",
"get-deskreen-pro-tooltip": "Hanki Deskreen Pro - avaa lataussivun.",
"Video stream is paused": "Videolähetys on tauolla",
"Video stream is playing": "Videolähetys on käynnissä",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Videolähetys pausattiin kokoruututilasta poistumisen jälkeen. Napsauta Toista jatkaaksesi.",
"Pause": "Tauko",
"Play": "Toista",
"Video Settings": "Asetukset videolle",
"Flip": "Käännä ympäri",
"Video quality has been changed to": "Videon laatu muutettiin määreeseen",
"Click to Open Video Settings": "Napsauta avataksesi videon asetukset",
"Click to Enter Full Screen Mode": "Napsauta siirtyäksesi kokoruututilaan",
"Click to Play Video": "Napsauta toistaaksesi videon",
"Click to Pause Video": "Napsauta pysäyttääksesi videon",
"Default video player has been turned OFF": "Vakiollinen videotoisto-ohjelma on KYTKETTY POIS PÄÄLTÄ",
"Default video player has been turned ON": "Vakiollinen videotoisto-ohjelma on KYTKETTY PÄÄLLE",
"ON": "PÄÄLLÄ",
"OFF": "POIS",
"Default Video Player": "Vakiollinen videontoisto-ohjelma",
"Click to visit our website": "Napsauta vieraillaksesi verkkosivustollamme",
"Video is flipped horizontally": "Video käännetty vaakatasossa",
"flip-the-screen-is-pro-version-only": "Näytön kääntäminen on saatavilla vain Pro-versiossa",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Napsauta katsoaksesi tietoja yhteydestäsi",
"Pair ID": "Lateparin ID-tunniste",
"Unpair": "Poista laiteparitus",
"Session ID": "Istunnon ID-tunniste",
"Click to boost video stream if it is lagging": "Napsauta lisätyöntöapua videovirtaukselle mikäli se hidastelee",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Tietosuojailmoitus: Analytiikka Deskreen CE Viewer -sovelluksessa",
"Analytics Reference": "Analytiikan viite",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Tämä sovellus käyttää Google Analyticsia (Googlelta saatava ilmainen palvelu) seuratakseen nimettömästi perustason käyttötietoja. Se auttaa meitä ymmärtämään, miten sovellusta käytetään, jotta voimme parantaa sitä kaikille.",
"What we collect:": "Mitä keräämme:",
"Page views (which screens you visit)": "Sivunäyttökerrat (mitä näkymiä käyt)",
"Time spent on pages": "Sivuille käytetty aika",
"Basic device info (browser type, screen size)": "Laitteen perustiedot (selaintyyppi, näytön koko)",
"Your IP address (anonymized — last part removed for privacy)": "IP-osoitteesi (anonymisoitu — viimeinen osa poistetaan yksityisyyden suojaamiseksi)",
"What we DON'T collect:": "Mitä emme kerää:",
"Personal info (names, emails, passwords)": "Henkilötietoja (nimiä, sähköposteja, salasanoja)",
"Exact location": "Tarkkaa sijaintia",
"Any files or content you interact with": "Tiedostoja tai sisältöä, joiden kanssa olet vuorovaikutuksessa",
"Why anonymous?": "Miksi anonyymisti?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP-osoitteesi lyhennetään automaattisesti, eikä sinua voi tunnistaa näiden tietojen perusteella.",
"Your options:": "Vaihtoehtosi:",
"Continue:": "Jatka:",
"We'll track anonymized usage to help improve the app.": "Seuraamme anonymisoitua käyttöä sovelluksen parantamiseksi.",
"Opt out:": "Kieltäydy:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Napsauta Hylkää-painiketta alla poistaaksesi seurannan käytöstä. (Kunnioitamme tätä valintaa, mutta saatat jäädä paitsi tulevista parannuksista, jotka perustuvat yhteiseen palautteeseen.)",
"Data goes to: Google Analytics. See their privacy policy.": "Tiedot lähetetään: Google Analytics. Tutustu heidän tietosuojakäytäntöönsä.",
"Accept": "Hyväksy",
"Allow": "Salli",
"Deny": "Hylkää",
"re-initiate-connection": "Käynnistä yhteys uudelleen",
"Privacy Settings": "Tietosuoja-asetukset",
"Change your preference:": "Muuta mieltymystäsi:",
"Enable analytics:": "Ota analytiikka käyttöön:",
"Disable analytics:": "Poista analytiikka käytöstä:",
"Enable Analytics": "Ota analytiikka käyttöön",
"Disable Analytics": "Poista analytiikka käytöstä",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klikkaa alla olevaa Poista käytöstä -painiketta seurannan lopettamiseksi. (Kunnioitamme tätä valintaa, mutta saatat jäädä paitsi tulevista parannuksista, jotka perustuvat kollektiiviseen palautteeseen.)",
"their privacy policy": "heidän tietosuojakäytäntönsä"
}
================================================
FILE: src/client-viewer/public/locales/fr/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "En attente de la validation depuis l'appareil source...",
"Waiting for user to select source to share from screen sharing device...": "En attente de la sélection de la source à partager depuis l'appareil source...",
"My Device Info": "Mes informations d'appareil",
"Device Type": "Type d'appareil",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Votre adresse IP doit correspondre avec l'\"Adresse IP\" affiché dans la pop-up affichée sur l'ordinateur depuis lequel Deskreen-CE est lancé.",
"Device IP": "IP de l'appareil",
"Device Browser": "Navigateur de l'appareil",
"Device OS": "OS de l'appareil",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Ces détails doivent correspondre avec ceux inscrits dans la pop-up affichée sur l'ordinateur depuis lequel Deskreen-CE est lancé..",
"Deskreen-CE Screen Viewer": "Écran de visionnage Deskreen-CE",
"Connected!": "Connecté!",
"Error occurred": "Une erreur est survenue",
"Deskreen-CE Error Dialog": "Boîte de dialogue d'erreur",
"Something went wrong": "Quelque chose s'est mal passé",
"You may close this browser window then try to connect again": "Vous devriez fermer cette fenêtre de navigateur et essayer de vous connecter de nouveau",
"An unknown error occurred": "Une erreur inconnue s'est produite",
"You were not allowed to connect": "Vous n'êtes pas autorisé à vous connecter",
"You were disconnected": "Vous avez été déconnecté",
"WebRTC error occurred": "Une erreur WebRTC s'est produite",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Si vous aimez Deskreen-CE, Vous pouvez contribuer financièrement. Deskreen-CE est open-source. Votre don nous motivera à rendre Deskreen-CE encore meilleur.",
"Donate": "Donner",
"get-deskreen-pro": "Obtenir Deskreen Pro",
"get-deskreen-pro-tooltip": "Obtenir Deskreen Pro - ouvre la page de téléchargement.",
"Video stream is paused": "Le flux vidéo est en pause",
"Video stream is playing": "Lecture du flux vidéo",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Le flux vidéo est en pause après avoir quitté le mode plein écran. Veuillez cliquer sur Lecture pour continuer.",
"Pause": "Pause",
"Play": "Lecture",
"Video Settings": "Paramètres Vidéo",
"Flip": "Tourner",
"Video quality has been changed to": "Qualité de la vidéo changée en",
"Click to Open Video Settings": "Cliquez pour ouvrir les paramètres vidéo",
"Click to Enter Full Screen Mode": "Cliquez pour passer en plein écran",
"Click to Play Video": "Cliquez pour lire la vidéo",
"Click to Pause Video": "Cliquez pour mettre en pause la vidéo",
"Default video player has been turned OFF": "Le lecteur vidéo par défaut a été désactivé",
"Default video player has been turned ON": "Le lecteur vidéo par défaut a été activé",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "Lecteur vidéo par défaut",
"Click to visit our website": "Cliquez ici pour visiter notre site web",
"Video is flipped horizontally": "La vidéo à été tourner horizontallement",
"flip-the-screen-is-pro-version-only": "Retourner l'écran n'est disponible que dans la version Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Cliquez pour voir les informations de connexion",
"Pair ID": "ID d'appairage",
"Unpair": "Desappairer",
"Session ID": "ID de session",
"Click to boost video stream if it is lagging": "Cliquez pour booster le flux vidéo si vous rencontrez des ralentissements",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Avis de confidentialité : Analyses dans Deskreen CE Viewer",
"Analytics Reference": "Référence Analytique",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Cette application utilise Google Analytics (un service gratuit de Google) pour suivre anonymement des données d'utilisation de base. Cela nous aide à comprendre comment l'application est utilisée afin de l'améliorer pour tout le monde.",
"What we collect:": "Ce que nous recueillons :",
"Page views (which screens you visit)": "Pages consultées (les écrans que vous visitez)",
"Time spent on pages": "Temps passé sur les pages",
"Basic device info (browser type, screen size)": "Informations de base sur l'appareil (type de navigateur, taille de l'écran)",
"Your IP address (anonymized — last part removed for privacy)": "Votre adresse IP (anonymisée — la dernière partie est supprimée pour protéger votre vie privée)",
"What we DON'T collect:": "Ce que nous NE collectons PAS :",
"Personal info (names, emails, passwords)": "Informations personnelles (noms, adresses e-mail, mots de passe)",
"Exact location": "Localisation précise",
"Any files or content you interact with": "Les fichiers ou contenus avec lesquels vous interagissez",
"Why anonymous?": "Pourquoi anonyme ?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Votre adresse IP est automatiquement raccourcie, et personne ne peut vous identifier personnellement à partir de ces données.",
"Your options:": "Vos options :",
"Continue:": "Continuer :",
"We'll track anonymized usage to help improve the app.": "Nous suivrons l'utilisation anonymisée pour aider à améliorer l'application.",
"Opt out:": "Refuser :",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Cliquez sur le bouton Refuser ci-dessous pour désactiver le suivi. (Nous respecterons ce choix, mais vous pourriez manquer des améliorations futures basées sur les retours collectifs.)",
"Data goes to: Google Analytics. See their privacy policy.": "Les données sont envoyées à : Google Analytics. Consultez leur politique de confidentialité.",
"Accept": "Accepter",
"Allow": "Autoriser",
"Deny": "Refuser",
"re-initiate-connection": "Réinitialiser la connexion",
"Privacy Settings": "Paramètres de confidentialité",
"Change your preference:": "Modifier votre préférence :",
"Enable analytics:": "Activer les analyses :",
"Disable analytics:": "Désactiver les analyses :",
"Enable Analytics": "Activer les analyses",
"Disable Analytics": "Désactiver les analyses",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Cliquez sur le bouton Désactiver ci-dessous pour arrêter le suivi. (Nous respecterons ce choix, mais vous pourriez manquer des améliorations futures basées sur les retours collectifs.)",
"their privacy policy": "leur politique de confidentialité"
}
================================================
FILE: src/client-viewer/public/locales/it/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "In attesa che l'utente faccia clic sul pulsante CONSENTI sul dispositivo di condivisione...",
"Waiting for user to select source to share from screen sharing device...": "In attesa che l'utente selezioni la sorgente da condividere dal dispositivo di condivisione...",
"My Device Info": "Info del mio Dispositivo",
"Device Type": "Tipologia Dispositivo",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "L'IP del tuo Dispositivo dovrebbe corrispondere a \"IP Dispositivo\" nel popup apparso sul tuo computer, dove Deskreen-CE è in esecuzione.",
"Device IP": "IP Dispositivo",
"Device Browser": "Browser Dispositivo",
"Device OS": "OS Dispositivo",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Questi dettagli dovrebbero corrispondere a quelli che vedi nel popup sul Dispositivo di condivisione.",
"Deskreen-CE Screen Viewer": "Visualizzatore dello schermo di Deskreen-CE",
"Connected!": "Connesso!",
"Error occurred": "Si è verificato un Errore",
"Deskreen-CE Error Dialog": "Finestra di dialogo degli errori di Deskreen-CE",
"Something went wrong": "Qualcosa è andato storto",
"You may close this browser window then try to connect again": "Puoi chiudere questa finestra del browser, quindi provare a connetterti di nuovo",
"An unknown error occurred": "Si è verificato un errore sconosciuto",
"You were not allowed to connect": "Non ti è stato permesso di connetterti",
"You were disconnected": "Sei stato disconnesso",
"WebRTC error occurred": "Si è verificato un errore WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Se ti piace Deskreen-CE, considera di contribuire finanziariamente. Deskreen-CE è open-source. Le tue donazioni ci motivano a rendere Deskreen-CE ancora migliore.",
"Donate": "Dona",
"get-deskreen-pro": "Ottieni Deskreen Pro",
"get-deskreen-pro-tooltip": "Ottieni Deskreen Pro - apre la pagina di download.",
"Video stream is paused": "Trasmissione Video in pausa",
"Video stream is playing": "Trasmissione Video in riproduzione",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Trasmissione video in pausa dopo l'uscita dalla modalità schermo intero. Clicca su Riproduci per continuare.",
"Pause": "Pausa",
"Play": "Riproduci",
"Video Settings": "Impostazioni Video",
"Flip": "Capovolgi",
"Video quality has been changed to": "La qualità Video è stata cambiata a",
"Click to Open Video Settings": "Clicca per aprire le Impostazioni Video",
"Click to Enter Full Screen Mode": "Clicca per entrare in modalità Schermo Intero",
"Click to Play Video": "Clicca per riprodurre il video",
"Click to Pause Video": "Clicca per mettere in pausa il video",
"Default video player has been turned OFF": "il player video predefinito è stato spento",
"Default video player has been turned ON": "il player video predefinito è stato acceso",
"ON": "Acceso",
"OFF": "Spento",
"Default Video Player": "Player Video Predefinito",
"Click to visit our website": "Clicca per visitare il nostro sito",
"Video is flipped horizontally": "Il Video è capovolto orizzontalmente",
"flip-the-screen-is-pro-version-only": "Capovolgere lo schermo è disponibile solo nella versione Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Clicca per vedere le info di connessione",
"Pair ID": "ID Coppia",
"Unpair": "Disaccoppia",
"Session ID": "ID Sessione",
"Click to boost video stream if it is lagging": "Clicca per incrementare il flusso video se sta andando a scatti",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Informativa sulla privacy: Analisi in Deskreen CE Viewer",
"Analytics Reference": "Riferimento Analitico",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Questa app utilizza Google Analytics (un servizio gratuito di Google) per tracciare in modo anonimo i dati di utilizzo di base. Questo ci aiuta a capire come viene usata l'app, così possiamo migliorarla per tutti.",
"What we collect:": "Cosa raccogliamo:",
"Page views (which screens you visit)": "Visualizzazioni di pagina (quali schermate visiti)",
"Time spent on pages": "Tempo trascorso sulle pagine",
"Basic device info (browser type, screen size)": "Informazioni di base sul dispositivo (tipo di browser, dimensioni dello schermo)",
"Your IP address (anonymized — last part removed for privacy)": "Il tuo indirizzo IP (anonimizzato — l'ultima parte viene rimossa per la privacy)",
"What we DON'T collect:": "Cosa NON raccogliamo:",
"Personal info (names, emails, passwords)": "Dati personali (nomi, email, password)",
"Exact location": "Posizione esatta",
"Any files or content you interact with": "Qualsiasi file o contenuto con cui interagisci",
"Why anonymous?": "Perché anonimo?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Il tuo IP viene accorciato automaticamente e nessuno può identificarti personalmente da questi dati.",
"Your options:": "Le tue opzioni:",
"Continue:": "Continua:",
"We'll track anonymized usage to help improve the app.": "Tracceremo l'utilizzo anonimizzato per aiutare a migliorare l'app.",
"Opt out:": "Rifiuta:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Fai clic sul pulsante Rifiuta qui sotto per disattivare il tracciamento. (Rispetteremo questa scelta, ma potresti perdere miglioramenti futuri basati sul feedback collettivo.)",
"Data goes to: Google Analytics. See their privacy policy.": "I dati vengono inviati a: Google Analytics. Consulta la loro informativa sulla privacy.",
"Accept": "Accetta",
"Allow": "Consenti",
"Deny": "Rifiuta",
"re-initiate-connection": "Riavvia la connessione",
"Privacy Settings": "Impostazioni privacy",
"Change your preference:": "Modifica la tua preferenza:",
"Enable analytics:": "Abilita analisi:",
"Disable analytics:": "Disabilita analisi:",
"Enable Analytics": "Abilita analisi",
"Disable Analytics": "Disabilita analisi",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Fai clic sul pulsante Disabilita qui sotto per interrompere il tracciamento. (Rispetteremo questa scelta, ma potresti perdere miglioramenti futuri basati su feedback collettivo.)",
"their privacy policy": "la loro informativa sulla privacy"
}
================================================
FILE: src/client-viewer/public/locales/ja/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "画面共有デバイスでユーザーが「許可」をクリックするのを待っています...",
"Waiting for user to select source to share from screen sharing device...": "画面共有デバイスから共有するソースをユーザーが選択するのを待っています...",
"My Device Info": "このデバイスの情報",
"Device Type": "デバイスの種類",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Deskreen-CEが動作しているパソコンに表示されるアラートポップアップの\"デバイスIP\"と、このデバイスのデバイスIPが一致する必要があります。",
"Device IP": "デバイスのIP",
"Device Browser": "デバイスのブラウザ",
"Device OS": "デバイスのOS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "これらの内容は、画面共有デバイスのアラートポップアップに表示される内容と一致している必要があります。",
"Deskreen-CE Screen Viewer": "Deskreen-CE Screen Viewer",
"Connected!": "接続されました!",
"Error occurred": "エラーが発生しました",
"Deskreen-CE Error Dialog": "Deskreen-CE エラーダイアログ",
"Something went wrong": "何らかの問題が発生しました",
"You may close this browser window then try to connect again": "このブラウザを閉じてから、再度接続を試みてください",
"An unknown error occurred": "不明なエラーが発生しました",
"You were not allowed to connect": "接続が許可されていません",
"You were disconnected": "接続が切断されました",
"WebRTC error occurred": "WebRTCエラーが発生しました",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Deskreen-CEを気に入っていただけたなら、資金面での貢献をご検討ください。Deskreen-CEはオープンソースです。あなたの寄付により、私たちはDeskreen-CEをより良いものにするためのモチベーションを保つことができます。",
"Donate": "寄付",
"get-deskreen-pro": "Deskreen Pro を入手",
"get-deskreen-pro-tooltip": "Deskreen Pro を入手 - ダウンロードページを開きます。",
"Video stream is paused": "ビデオストリームを一時停止しています",
"Video stream is playing": "ビデオストリームを再生中です",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "フルスクリーンモードを終了した後、ビデオストリームが一時停止されました。続けるには再生をクリックしてください。",
"Pause": "一時停止",
"Play": "再生",
"Video Settings": "ビデオ設定",
"Flip": "反転",
"Video quality has been changed to": "ビデオの画質を変更しました。画質:",
"Click to Open Video Settings": "クリックしてビデオ設定を開きます",
"Click to Enter Full Screen Mode": "クリックするとフルスクリーンモードになります",
"Click to Play Video": "クリックしてビデオを再生します",
"Click to Pause Video": "クリックしてビデオを一時停止します",
"Default video player has been turned OFF": "デフォルトのビデオプレーヤーがOFFになっています",
"Default video player has been turned ON": "デフォルトのビデオプレーヤーがONになっています",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "デフォルトのビデオプレーヤー",
"Click to visit our website": "クリックするとウェブサイトが開きます",
"Video is flipped horizontally": "映像が水平方向に反転しています",
"flip-the-screen-is-pro-version-only": "画面の反転はPro版でのみ利用可能です",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "クリックすると接続情報が表示されます",
"Pair ID": "ペアID",
"Unpair": "ペア解除",
"Session ID": "セッションID",
"Click to boost video stream if it is lagging": "クリックすると、ビデオストリームが遅延している場合、ブーストされます",
"Privacy Notice: Analytics in Deskreen CE Viewer": "プライバシーに関するお知らせ:Deskreen CE Viewerでの解析について",
"Analytics Reference": "分析参照",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "このアプリは Google が提供する無料サービスの Google Analytics を使用して、基本的な利用データを匿名で追跡します。アプリの使われ方を理解し、すべてのユーザーのために改善するためです。",
"What we collect:": "収集するデータ:",
"Page views (which screens you visit)": "ページビュー(どの画面を表示したか)",
"Time spent on pages": "ページに滞在した時間",
"Basic device info (browser type, screen size)": "基本的な端末情報(ブラウザーの種類、画面サイズ)",
"Your IP address (anonymized — last part removed for privacy)": "IP アドレス(匿名化 — プライバシー保護のため末尾を削除)",
"What we DON'T collect:": "収集しないもの:",
"Personal info (names, emails, passwords)": "個人情報(氏名、メールアドレス、パスワード)",
"Exact location": "正確な位置情報",
"Any files or content you interact with": "操作したファイルやコンテンツ",
"Why anonymous?": "なぜ匿名なのですか?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP アドレスは自動的に短縮され、このデータから個人を特定することはできません。",
"Your options:": "選択肢:",
"Continue:": "続行:",
"We'll track anonymized usage to help improve the app.": "アプリ改善のために匿名化された利用状況を追跡します。",
"Opt out:": "オプトアウト:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "下の「拒否」ボタンをクリックすると追跡を無効にできます。(この選択は尊重しますが、総合的なフィードバックに基づく将来の改善を逃す可能性があります。)",
"Data goes to: Google Analytics. See their privacy policy.": "データ送信先:Google Analytics。プライバシーポリシーをご確認ください。",
"Accept": "同意する",
"Allow": "許可する",
"Deny": "拒否",
"re-initiate-connection": "再接続",
"Privacy Settings": "プライバシー設定",
"Change your preference:": "設定を変更:",
"Enable analytics:": "分析を有効にする:",
"Disable analytics:": "分析を無効にする:",
"Enable Analytics": "分析を有効にする",
"Disable Analytics": "分析を無効にする",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "下の無効ボタンをクリックして追跡を停止します。(この選択を尊重しますが、集団フィードバックに基づく将来の改善を見逃す可能性があります。)",
"their privacy policy": "プライバシーポリシー"
}
================================================
FILE: src/client-viewer/public/locales/ko/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "공유할 기기의 사용자가 화면 공유 허용 버튼을 클릭하기를 기다리는 중 ...",
"Waiting for user to select source to share from screen sharing device...": "공유할 기기의 어떤 화면을 공유할지 선택을 기다리는 중...",
"My Device Info": "내 기기 정보",
"Device Type": "기기 종류",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "현재 기기의 IP는 Deskreen-CE 이 제공하는 \"Device IP\" 와 같아야 합니다.",
"Device IP": "기기 IP",
"Device Browser": "기기 브라우저",
"Device OS": "기기 OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "세부 사항은 화면 공유 장치에서 팝업에서 표시되는 것과 일치해야합니다.",
"Deskreen-CE Screen Viewer": "스크린 뷰어",
"Connected!": "연결되었습니다.",
"Error occurred": "오류가 발생했습니다",
"Deskreen-CE Error Dialog": "오류 알림",
"Something went wrong": "연결과정에 오류가 발생하였습니다",
"You may close this browser window then try to connect again": "이 브라우저 창을 닫은 다음 다시 연결하십시오.",
"An unknown error occurred": "알 수없는 오류가 발생했습니다",
"You were not allowed to connect": "이 기기는 연결이 허용되지 않았습니다",
"You were disconnected": "연결이 해제되었습니다",
"WebRTC error occurred": "WebRTC 오류가 발생했습니다",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "오픈소스 프로젝트에 재정적으로 기여하는 것은 더 좋은 프로그램 개발 동기를 부여합니다.",
"Donate": "기부하기",
"get-deskreen-pro": "Deskreen Pro 받기",
"get-deskreen-pro-tooltip": "Deskreen Pro 받기 - 다운로드 페이지를 엽니다.",
"Video stream is paused": "비디오 스트림이 일시 중지됩니다",
"Video stream is playing": "비디오 스트림이 재생 중입니다",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "전체 화면 종료 후 비디오 스트림이 일시 중지되었습니다. 계속하려면 재생을 클릭하세요.",
"Pause": "중지",
"Play": "재생",
"Video Settings": "비디오 설정",
"Flip": "화면 좌우 반전",
"Video quality has been changed to": "비디오 품질이 변경되었습니다",
"Click to Open Video Settings": "비디오 설정 열기",
"Click to Enter Full Screen Mode": "전체 화면 모드로 들어가려면 클릭하십시오",
"Click to Play Video": "비디오 재생을 클릭하십시오",
"Click to Pause Video": "비디오 일시 정지를 클릭하십시오",
"Default video player has been turned OFF": "기본 비디오 플레이어가 꺼져 있습니다",
"Default video player has been turned ON": "기본 비디오 플레이어가 켜져 있습니다",
"ON": "켜짐",
"OFF": "꺼짐",
"Default Video Player": "기본 비디오 플레이어",
"Click to visit our website": "클릭하면 웹사이트를 방문합니다",
"Video is flipped horizontally": "비디오를 수평으로 뒤집습니다",
"flip-the-screen-is-pro-version-only": "화면 뒤집기는 Pro 버전에서만 사용할 수 있습니다",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "연결 정보를 보려면 클릭하십시오",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "클릭하면 비디오 스트림을 향상시킬 수 있습니다",
"Privacy Notice: Analytics in Deskreen CE Viewer": "개인정보 안내: Deskreen CE Viewer의 분석",
"Analytics Reference": "분석 참조",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "이 앱은 Google Analytics(구글에서 제공하는 무료 서비스)를 사용하여 기본 사용 데이터를 익명으로 추적합니다. 이를 통해 사람들이 앱을 어떻게 사용하는지 이해하고 모두를 위해 개선할 수 있습니다.",
"What we collect:": "수집하는 정보:",
"Page views (which screens you visit)": "페이지 조회수(어떤 화면을 방문하는지)",
"Time spent on pages": "페이지에 머문 시간",
"Basic device info (browser type, screen size)": "기본 기기 정보(브라우저 종류, 화면 크기)",
"Your IP address (anonymized — last part removed for privacy)": "IP 주소(익명 처리됨 — 개인정보 보호를 위해 마지막 부분이 제거됩니다)",
"What we DON'T collect:": "수집하지 않는 정보:",
"Personal info (names, emails, passwords)": "개인 정보(이름, 이메일, 비밀번호)",
"Exact location": "정확한 위치",
"Any files or content you interact with": "사용자가 상호작용하는 파일이나 콘텐츠",
"Why anonymous?": "왜 익명으로 수집하나요?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP 주소는 자동으로 축약되며, 이 데이터로 개인을 식별할 수 없습니다.",
"Your options:": "선택 사항:",
"Continue:": "계속:",
"We'll track anonymized usage to help improve the app.": "앱을 개선하기 위해 익명화된 사용 데이터를 추적합니다.",
"Opt out:": "거부:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "아래의 거부 버튼을 클릭하여 추적을 비활성화하세요. (이 선택을 존중하지만, 공동 피드백에 기반한 향후 개선 사항을 놓칠 수 있습니다.)",
"Data goes to: Google Analytics. See their privacy policy.": "데이터가 전송되는 곳: Google Analytics. 개인정보 보호정책을 확인하세요.",
"Accept": "동의",
"Allow": "허용",
"Deny": "거부",
"re-initiate-connection": "연결 재시작",
"Privacy Settings": "개인정보 설정",
"Change your preference:": "선호도 변경:",
"Enable analytics:": "분석 활성화:",
"Disable analytics:": "분석 비활성화:",
"Enable Analytics": "분석 활성화",
"Disable Analytics": "분석 비활성화",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "아래의 비활성화 버튼을 클릭하여 추적을 중지하세요. (이 선택을 존중하지만, 집단 피드백을 기반으로 한 향후 개선 사항을 놓칠 수 있습니다.)",
"their privacy policy": "개인정보 보호정책"
}
================================================
FILE: src/client-viewer/public/locales/nl/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Wachtend op de gebruiker om de TOESTAAN knop in te drukken op het scherm-delen-apparaat...",
"Waiting for user to select source to share from screen sharing device...": "Wachtend op de gebruiker om de bron te selecteren om te delen vanuit het scherm-delen-apparaat...",
"My Device Info": "Mijn Apparaat Info",
"Device Type": "Apparaat Type",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Uw Apparaat IP zou identiek moeten zijn met het Apparaat IP in de verschenen alert pop-up op uw computer, waar Deskreen-CE actief is",
"Device IP": "Apparaat IP",
"Device Browser": "Apparaat Browser",
"Device OS": "Apparaat OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Deze details zouden identiek moeten zijn met diegene die u ziet in de alert pop-up op uw computer, waar Deskreen-CE actief is",
"Deskreen-CE Screen Viewer": "Deskreen-CE Scherm Viewer",
"Connected!": "Verbonden!",
"Error occurred": "Fout opgetreden",
"Deskreen-CE Error Dialog": "Deskreen-CE Error Dialoog",
"Something went wrong": "Er is iets misgegaan",
"You may close this browser window then try to connect again": "U mag dit browser venster sluiten en opnieuw proberen te verbinden",
"An unknown error occurred": "Een onbekende fout is opgetreden",
"You were not allowed to connect": "Uw verbinding werd niet toegestaan",
"You were disconnected": "Uw verbinding werd verbroken",
"WebRTC error occurred": "WebRTC fout opgetreden",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Als u Deskreen-CE waardeert, overweeg dan een financiële bijdrage. Deskreen-CE is open-source. Uw donaties houden ons gemotiveerd om Deskreen-CE te blijven verbeteren.",
"Donate": "Doneer",
"get-deskreen-pro": "Ontvang Deskreen Pro",
"get-deskreen-pro-tooltip": "Ontvang Deskreen Pro - opent de downloadpagina.",
"Video stream is paused": "Video stream is gepauzeerd",
"Video stream is playing": "Video stream wordt afgespeeld",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Video stream is gepauzeerd na het verlaten van de volledig scherm modus. Klik op Afspelen om door te gaan.",
"Pause": "Pauze",
"Play": "Afspelen",
"Video Settings": "Video Instellingen",
"Flip": "Flip",
"Video quality has been changed to": "Video kwaliteit is aangepast naar",
"Click to Open Video Settings": "Klik om Video Instellingen te openen",
"Click to Enter Full Screen Mode": "Klik om Volledig Scherm modus te activeren",
"Click to Play Video": "Klik om video af te spelen",
"Click to Pause Video": "Klik om video te pauzeren",
"Default video player has been turned OFF": "Standaard video speler staat nu UIT",
"Default video player has been turned ON": "Standaard video speler staat nu AAN",
"ON": "AAN",
"OFF": "UIT",
"Default Video Player": "Standaard Video Speler",
"Click to visit our website": "Klik om onze website te bezoeken",
"Video is flipped horizontally": "Video is horizontaal geflipt",
"flip-the-screen-is-pro-version-only": "Scherm omdraaien is alleen beschikbaar in de Pro-versie",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klik om verbindings informatie te zien",
"Pair ID": "Koppel ID",
"Unpair": "Ontkoppelen",
"Session ID": "Sessie ID",
"Click to boost video stream if it is lagging": "Klik om de video stream te versterken als het traag is",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Privacyverklaring: Analyse in Deskreen CE Viewer",
"Analytics Reference": "Analytiek Referentie",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Deze app gebruikt Google Analytics (een gratis dienst van Google) om anoniem basisgebruikgegevens bij te houden. Zo begrijpen we hoe de app wordt gebruikt en kunnen we haar voor iedereen verbeteren.",
"What we collect:": "Wat we verzamelen:",
"Page views (which screens you visit)": "Paginaweergaven (welke schermen je bezoekt)",
"Time spent on pages": "Tijd doorgebracht op pagina's",
"Basic device info (browser type, screen size)": "Basisapparaatinformatie (browsertype, schermgrootte)",
"Your IP address (anonymized — last part removed for privacy)": "Je IP-adres (geanonimiseerd — het laatste deel wordt verwijderd voor je privacy)",
"What we DON'T collect:": "Wat we NIET verzamelen:",
"Personal info (names, emails, passwords)": "Persoonlijke info (namen, e-mails, wachtwoorden)",
"Exact location": "Exacte locatie",
"Any files or content you interact with": "Bestanden of inhoud waarmee je interacteert",
"Why anonymous?": "Waarom anoniem?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Je IP wordt automatisch ingekort, zodat niemand je persoonlijk kan identificeren met deze gegevens.",
"Your options:": "Je opties:",
"Continue:": "Doorgaan:",
"We'll track anonymized usage to help improve the app.": "We volgen geanonimiseerd gebruik om de app te verbeteren.",
"Opt out:": "Afmelden:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik op de knop Weigeren hieronder om tracking uit te schakelen. (We respecteren die keuze, maar je kunt toekomstige verbeteringen missen die op collectieve feedback zijn gebaseerd.)",
"Data goes to: Google Analytics. See their privacy policy.": "Gegevens gaan naar: Google Analytics. Bekijk hun privacybeleid.",
"Accept": "Accepteren",
"Allow": "Toestaan",
"Deny": "Weigeren",
"re-initiate-connection": "Verbinding opnieuw starten",
"Privacy Settings": "Privacy-instellingen",
"Change your preference:": "Wijzig uw voorkeur:",
"Enable analytics:": "Analytics inschakelen:",
"Disable analytics:": "Analytics uitschakelen:",
"Enable Analytics": "Analytics inschakelen",
"Disable Analytics": "Analytics uitschakelen",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik op de knop Uitschakelen hieronder om tracking te stoppen. (We respecteren deze keuze, maar u kunt toekomstige verbeteringen missen op basis van collectieve feedback.)",
"their privacy policy": "hun privacybeleid"
}
================================================
FILE: src/client-viewer/public/locales/ru/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Ждем когда пользователь нажмет кнопку РАЗРЕШИТЬ для доступа к экрану компьютера...",
"Waiting for user to select source to share from screen sharing device...": "Ждем когда пользователь выберет Весь экран или Окно приложения для отображения его здесь...",
"My Device Info": "Информация о моем устройстве",
"Device Type": "Тип устройства",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "IP-aдрес вашего устройства должен совпадать с «IP-адресом устройства» во всплывающем окне с предупреждением на компьютере, где работает Deskreen-CE.",
"Device IP": "IP-aдрес устройства",
"Device Browser": "Веб-браузер устройства",
"Device OS": "ОС устройства",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Эти данные должны совпадать с теми, которые вы видите во всплывающем окне предупреждения на экране компьютера, на котором работает Deskreen-CE.",
"Deskreen-CE Screen Viewer": "Просмотрщик экрана Deskreen-CE",
"Connected!": "Подключено!",
"Error occurred": "Произошла ошибка",
"Deskreen-CE Error Dialog": "Диалог ошибки Deskreen-CE",
"Something went wrong": "Произошло что-то не так",
"You may close this browser window then try to connect again": "Вы можете закрыть это окно браузера и попытаться подключиться снова",
"An unknown error occurred": "Произошла неизвестная ошибка",
"You were not allowed to connect": "Вам не разрешили подключиться",
"You were disconnected": "Вы были отключены",
"WebRTC error occurred": "Произошла ошибка WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Если вам нравится Deskreen-CE, подумайте о том, чтобы внести финансовый вклад. Deskreen-CE - это оупенсорсный проэкт. Ваши пожертвования позволяют нам делать Deskreen-CE еще лучше.",
"Donate": "Пожертвовать",
"get-deskreen-pro": "Получить Deskreen Pro",
"get-deskreen-pro-tooltip": "Получить Deskreen Pro - открывает страницу загрузки.",
"Video stream is paused": "Видеопоток приостановлен",
"Video stream is playing": "Видеопоток воспроизводится",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Видеопоток приостановлен после выхода из полноэкранного режима. Пожалуйста, нажмите Воспроизвести, чтобы продолжить.",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Настройки видео",
"Flip": "Отзеркалить",
"Video quality has been changed to": "Качество видео изменено на",
"Click to Open Video Settings": "Нажмите, чтобы открыть настройки видео",
"Click to Enter Full Screen Mode": "Нажмите, чтобы перейти в полноэкранный режим",
"Click to Play Video": "Нажмите, чтобы воспроизвести видео",
"Click to Pause Video": "Нажмите, чтобы приостановить видео",
"Default video player has been turned OFF": "Видеоплеер по умолчанию отключен",
"Default video player has been turned ON": "Видеопроигрыватель по умолчанию включен",
"ON": "ВКЛ",
"OFF": "ВЫКЛ",
"Default Video Player": "Видеоплеер по умолчанию",
"Click to visit our website": "Нажмите, чтобы посетить наш сайт",
"Video is flipped horizontally": "Видео отзеркалено",
"flip-the-screen-is-pro-version-only": "Отзеркаливание экрана доступно только в Pro версии",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Уведомление о конфиденциальности: аналитика в Deskreen CE Viewer",
"Analytics Reference": "Справочник по аналитике",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Это приложение использует Google Analytics (бесплатный сервис Google), чтобы анонимно отслеживать базовые данные использования. Это помогает нам понимать, как люди пользуются приложением, чтобы улучшать его для всех.",
"What we collect:": "Что мы собираем:",
"Page views (which screens you visit)": "Просмотры страниц (какие экраны вы посещаете)",
"Time spent on pages": "Время, проведённое на страницах",
"Basic device info (browser type, screen size)": "Базовую информацию об устройстве (тип браузера, размер экрана)",
"Your IP address (anonymized — last part removed for privacy)": "Ваш IP-адрес (анонимизированный — последняя часть удалена для защиты приватности)",
"What we DON'T collect:": "Чего мы НЕ собираем:",
"Personal info (names, emails, passwords)": "Персональные данные (имена, электронные адреса, пароли)",
"Exact location": "Точное местоположение",
"Any files or content you interact with": "Любые файлы или контент, с которыми вы взаимодействуете",
"Why anonymous?": "Почему анонимно?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Ваш IP автоматически сокращается, и никто не сможет идентифицировать вас лично по этим данным.",
"Your options:": "Ваши варианты:",
"Continue:": "Продолжить:",
"We'll track anonymized usage to help improve the app.": "Мы будем отслеживать анонимизированное использование, чтобы улучшать приложение.",
"Opt out:": "Отказаться:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Нажмите кнопку Отклонить ниже, чтобы отключить отслеживание. (Мы уважим этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)",
"Data goes to: Google Analytics. See their privacy policy.": "Данные отправляются в: Google Analytics. Ознакомьтесь с их политикой конфиденциальности.",
"Accept": "Принять",
"Allow": "Разрешить",
"Deny": "Отклонить",
"re-initiate-connection": "Повторить подключение",
"Privacy Settings": "Настройки конфиденциальности",
"Change your preference:": "Изменить ваше предпочтение:",
"Enable analytics:": "Включить аналитику:",
"Disable analytics:": "Отключить аналитику:",
"Enable Analytics": "Включить аналитику",
"Disable Analytics": "Отключить аналитику",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Нажмите кнопку Отключить ниже, чтобы остановить отслеживание. (Мы уважаем этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)",
"their privacy policy": "их политику конфиденциальности"
}
================================================
FILE: src/client-viewer/public/locales/sv/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Väntar på att användaren ska klicka på 'TILLÅT' på skärmdelningsenheten...",
"Waiting for user to select source to share from screen sharing device...": "Väntar på att användaren ska välja källa att dela från skärmdelningsenhet...",
"My Device Info": "Min enhetsinformation",
"Device Type": "Enhetens typ",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Din enhets IP-adress bör matcha med 'Enhetens IP' i den varnings-popup som dyker upp på din dator där Deskreen-CE körs",
"Device IP": "Enhetens IP",
"Device Browser": "Enhetens webbläsare",
"Device OS": "Enhetens operativsystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Dessa uppgifter ska matcha de som du ser i popup-fönstret på skärmdelningsenheten.",
"Deskreen-CE Screen Viewer": "Deskreen-CE skärmvisare",
"Connected!": "Ansluten!",
"Error occurred": "Ett fel inträffade",
"Deskreen-CE Error Dialog": "Deskreen-CE felhanterare",
"Something went wrong": "Något blev fel",
"You may close this browser window then try to connect again": "Stäng det här webbläsarfönstret och försök sedan ansluta igen",
"An unknown error occurred": "Ett okänt fel inträffade",
"You were not allowed to connect": "Du fick inte ansluta",
"You were disconnected": "Du blev nedkopplad",
"WebRTC error occurred": "Ett WebRTC-fel error inträffade",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Om du gillar Deskreen-CE, överväg i så fall att ge oss ett ekonomiskt bidrag. Deskreen-CE är open-source. Era donationer motiverar oss att göra Deskreen-CE ännu bättre.",
"Donate": "Donera",
"get-deskreen-pro": "Hämta Deskreen Pro",
"get-deskreen-pro-tooltip": "Hämta Deskreen Pro - öppnar nedladdningssidan.",
"Video stream is paused": "Videoströmmen är pausad",
"Video stream is playing": "Videoströmmen spelas",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Videoströmmen pausades efter att ha lämnat helskärmsläge. Klicka på Kör för att fortsätta.",
"Pause": "Paus",
"Play": "Kör",
"Video Settings": "Videoinställningar",
"Flip": "Omvänd",
"Video quality has been changed to": "Videokvaliteten har ändrats till",
"Click to Open Video Settings": "Klicka här för att öppna videoinställningarna",
"Click to Enter Full Screen Mode": "Klicka här för att gå in i helskärmsläge",
"Click to Play Video": "Klicka för att spela upp video",
"Click to Pause Video": "Klicka för att pausa video",
"Default video player has been turned OFF": "Standardvideospelaren har stängts av",
"Default video player has been turned ON": "Standardvideospelaren har aktiverats",
"ON": "PÅ",
"OFF": "AV",
"Default Video Player": "Standardvideospelare",
"Click to visit our website": "Klicka här för att besöka vår webbplats",
"Video is flipped horizontally": "Videon är vänd horisontellt",
"flip-the-screen-is-pro-version-only": "Vända skärmen är endast tillgängligt i Pro-versionen",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klicka här för att visa anslutningsinformationen",
"Pair ID": "ID för sammankopplingen",
"Unpair": "Ta bort sammankopplingen",
"Session ID": "ID för sessionen",
"Click to boost video stream if it is lagging": "Klicka för att öka videoströmmen om den släpar efter",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Integritetsmeddelande: Analys i Deskreen CE Viewer",
"Analytics Reference": "Analysreferens",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Den här appen använder Google Analytics (en kostnadsfri tjänst från Google) för att anonymt spåra grundläggande användningsdata. Det hjälper oss att förstå hur appen används så att vi kan förbättra den för alla.",
"What we collect:": "Det vi samlar in:",
"Page views (which screens you visit)": "Sidvisningar (vilka vyer du besöker)",
"Time spent on pages": "Tid som spenderas på sidor",
"Basic device info (browser type, screen size)": "Grundläggande enhetsinformation (webbläsartyp, skärmstorlek)",
"Your IP address (anonymized — last part removed for privacy)": "Din IP-adress (anonymiserad — den sista delen tas bort av integritetsskäl)",
"What we DON'T collect:": "Det vi INTE samlar in:",
"Personal info (names, emails, passwords)": "Personlig information (namn, e-postadresser, lösenord)",
"Exact location": "Exakt plats",
"Any files or content you interact with": "Filer eller innehåll du interagerar med",
"Why anonymous?": "Varför anonymt?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Din IP-adress förkortas automatiskt, och ingen kan identifiera dig personligen utifrån dessa data.",
"Your options:": "Dina alternativ:",
"Continue:": "Fortsätt:",
"We'll track anonymized usage to help improve the app.": "Vi spårar anonymiserad användning för att hjälpa oss förbättra appen.",
"Opt out:": "Avslå:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicka på knappen Avböj nedan för att inaktivera spårning. (Vi respekterar detta val, men du kan gå miste om framtida förbättringar som bygger på samlad feedback.)",
"Data goes to: Google Analytics. See their privacy policy.": "Data skickas till: Google Analytics. Se deras integritetspolicy.",
"Accept": "Acceptera",
"Allow": "Tillåt",
"Deny": "Avböj",
"re-initiate-connection": "Återanslut",
"Privacy Settings": "Integritetsinställningar",
"Change your preference:": "Ändra din preferens:",
"Enable analytics:": "Aktivera analys:",
"Disable analytics:": "Inaktivera analys:",
"Enable Analytics": "Aktivera analys",
"Disable Analytics": "Inaktivera analys",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicka på knappen Inaktivera nedan för att stoppa spårning. (Vi respekterar detta val, men du kan missa framtida förbättringar baserade på kollektiv feedback.)",
"their privacy policy": "deras integritetspolicy"
}
================================================
FILE: src/client-viewer/public/locales/ua/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Чекаємо коли користувач натисне кнопку ДОЗВОЛИТИ для доступу до екрану комп'ютера...",
"Waiting for user to select source to share from screen sharing device...": "Чекаємо коли користувач вибере Весь екран або Вікно додатка для відображення його тут...",
"My Device Info": "Інформація про мій пристрій",
"Device Type": "Тип пристрою",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "IP-aдрес пристрою вашого пристрою має збігатися з «IP-адресою пристрою» у спливаючому вікні сповіщення, що з’явилося на комп’ютері, де працює Deskreen-CE.",
"Device IP": "IP-aдрес пристрою",
"Device Browser": "Веб-браузер пристрою",
"Device OS": "ОС пристрою",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Ці деталі повинні збігатися з тими, які ви бачите у спливаючому вікні сповіщень на екрані комп’ютера, де запущений Deskreen-CE.",
"Deskreen-CE Screen Viewer": "Переглядач екрану Deskreen-CE",
"Connected!": "Підключено!",
"Error occurred": "Виникла помилка",
"Deskreen-CE Error Dialog": "Діалог помилки Deskreen-CE",
"Something went wrong": "Щось не так сталося",
"You may close this browser window then try to connect again": "Ви можете закрити це вікно браузера та спробувати підключитися знову",
"An unknown error occurred": "Виникла невідома помилка",
"You were not allowed to connect": "Вам не дозволили підключитися",
"You were disconnected": "Ви були відключені",
"WebRTC error occurred": "Сталася помилка WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Якщо вам подобається Deskreen-CE, подумайте про те, щоб внести фінансовий внесок. Deskreen-CE - це оупенсорсний проект. Ваші пожертвування дозволяють нам робити Deskreen-CE ще краще.",
"Donate": "Пожертвувати",
"get-deskreen-pro": "Отримати Deskreen Pro",
"get-deskreen-pro-tooltip": "Отримати Deskreen Pro - відкриває сторінку завантаження.",
"Video stream is paused": "Відеопотік призупинено",
"Video stream is playing": "Відеопотік продовжується",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "Відеопотік призупинено після виходу з повноекранного режиму. Будь ласка, натисніть Відтворення, щоб продовжити.",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Настройки видео",
"Flip": "Віддзеркалити",
"Video quality has been changed to": "Якість відео змінено на",
"Click to Open Video Settings": "Натисніть, щоб відкрити настройки відео",
"Click to Enter Full Screen Mode": "Натисніть для входу в повноекранноий режим",
"Click to Play Video": "Натисніть, щоб відтворити відео",
"Click to Pause Video": "Натисніть, щоб призупинити відео",
"Default video player has been turned OFF": "Стандартний відеоплеєр браузера вимкнено",
"Default video player has been turned ON": "Стандартний відеоплеєр браузера включений",
"ON": "ВКЛ",
"OFF": "ВИМК",
"Default Video Player": "Стандартний відеоплеєр браузера",
"Click to visit our website": "Клацніть, щоб відвідати наш веб-сайт",
"Video is flipped horizontally": "Відео віддзеркалено",
"flip-the-screen-is-pro-version-only": "Віддзеркалення екрану доступне лише в Pro версії",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"Privacy Notice: Analytics in Deskreen CE Viewer": "Повідомлення про конфіденційність: аналітика в Deskreen CE Viewer",
"Analytics Reference": "Довідник з аналітики",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Цей застосунок використовує Google Analytics (безкоштовний сервіс Google), щоб анонімно відстежувати базові дані використання. Це допомагає нам розуміти, як люди користуються застосунком, аби покращувати його для всіх.",
"What we collect:": "Що ми збираємо:",
"Page views (which screens you visit)": "Перегляди сторінок (які екрани ви відвідуєте)",
"Time spent on pages": "Час, проведений на сторінках",
"Basic device info (browser type, screen size)": "Базову інформацію про пристрій (тип браузера, розмір екрана)",
"Your IP address (anonymized — last part removed for privacy)": "Вашу IP-адресу (анонімізовану — остання частина вилучається для приватності)",
"What we DON'T collect:": "Що ми НЕ збираємо:",
"Personal info (names, emails, passwords)": "Особисту інформацію (імена, електронні адреси, паролі)",
"Exact location": "Точне місцезнаходження",
"Any files or content you interact with": "Будь-які файли чи вміст, з якими ви взаємодієте",
"Why anonymous?": "Чому анонімно?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Вашу IP автоматично скорочують, тому ніхто не може ідентифікувати вас особисто за цими даними.",
"Your options:": "Ваші варіанти:",
"Continue:": "Продовжити:",
"We'll track anonymized usage to help improve the app.": "Ми відстежуватимемо анонімізоване використання, щоб допомогти покращити застосунок.",
"Opt out:": "Відмовитися:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Натисніть кнопку Відхилити нижче, щоб вимкнути відстеження. (Ми поважаємо цей вибір, але ви можете пропустити майбутні покращення, що базуються на спільному зворотному зв'язку.)",
"Data goes to: Google Analytics. See their privacy policy.": "Дані надсилаються до: Google Analytics. Перегляньте їхню політику конфіденційності.",
"Accept": "Погодитися",
"Allow": "Дозволити",
"Deny": "Відхилити",
"re-initiate-connection": "Повторно підключитися",
"Privacy Settings": "Налаштування конфіденційності",
"Change your preference:": "Змінити вашу перевагу:",
"Enable analytics:": "Увімкнути аналітику:",
"Disable analytics:": "Вимкнути аналітику:",
"Enable Analytics": "Увімкнути аналітику",
"Disable Analytics": "Вимкнути аналітику",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Натисніть кнопку Вимкнути нижче, щоб зупинити відстеження. (Ми поважатимемо цей вибір, але ви можете пропустити майбутні покращення, засновані на колективному відгуку.)",
"their privacy policy": "їхню політику конфіденційності"
}
================================================
FILE: src/client-viewer/public/locales/zh_CN/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "正在等待用户单击屏幕共享设备上的允许按钮...",
"Waiting for user to select source to share from screen sharing device...": "正在等待用户从屏幕共享设备选择要共享的源...",
"My Device Info": "我的设备信息",
"Device Type": "设备类型",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "您的设备 IP 应该与运行 Deskreen-CE 的计算机上出现的警报弹出窗口中的 '设备 IP' 相匹配。",
"Device IP": "设备 IP",
"Device Browser": "设备浏览器",
"Device OS": "设备操作系统",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "这些详细信息应与您在屏幕共享设备上的警报弹出窗口中看到的信息相匹配。",
"Deskreen-CE Screen Viewer": "Deskreen-CE 屏幕查看器",
"Connected!": "已连接!",
"Error occurred": "出现错误",
"Deskreen-CE Error Dialog": "Deskreen-CE 错误对话框",
"Something went wrong": "出问题了",
"You may close this browser window then try to connect again": "您可以关闭此浏览器窗口,然后尝试重新连接",
"An unknown error occurred": "出现未知错误",
"You were not allowed to connect": "您不能连接",
"You were disconnected": "您将断开连接",
"WebRTC error occurred": "出现 WebRTC 错误",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "如果你喜欢 Deskreen-CE,可以考虑出钱。Deskreen-CE 是开源的。您的捐赠使我们有动力让 Deskreen-CE 变得更好。",
"Donate": "捐赠",
"get-deskreen-pro": "获取 Deskreen Pro",
"get-deskreen-pro-tooltip": "获取 Deskreen Pro - 打开下载页面。",
"Video stream is paused": "视频流暂停",
"Video stream is playing": "正在播放视频流",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "退出全屏后视频流已暂停。请点击播放以继续。",
"Pause": "暂停",
"Play": "播放",
"Video Settings": "视频设置",
"Flip": "翻转",
"Video quality has been changed to": "视频质量已更改为",
"Click to Open Video Settings": "单击以打开视频设置",
"Click to Enter Full Screen Mode": "单击以进入全屏模式",
"Click to Play Video": "单击以播放视频",
"Click to Pause Video": "单击以暂停视频",
"Default video player has been turned OFF": "默认视频播放器已关闭",
"Default video player has been turned ON": "默认视频播放器已开启",
"ON": "开启",
"OFF": "关闭",
"Default Video Player": "默认视频播放器",
"Click to visit our website": "点击访问我们的网站",
"Video is flipped horizontally": "视频水平翻转",
"flip-the-screen-is-pro-version-only": "翻转屏幕仅在 Pro 版本中可用",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻译尚未添加到 UI,但欢迎您的翻译!功能将很快添加,因此需要您的翻译",
"Click to see connection info": "单击以查看连接信息",
"Pair ID": "配对 ID",
"Unpair": "取消配对",
"Session ID": "会话 ID",
"Click to boost video stream if it is lagging": "如果视频流滞后,请单击以提高视频流",
"Privacy Notice: Analytics in Deskreen CE Viewer": "隐私提示:Deskreen CE Viewer 中的分析",
"Analytics Reference": "分析参考",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "本应用使用 Google Analytics(Google 提供的免费服务)匿名跟踪基本的使用数据。这有助于我们了解用户如何使用应用,从而为所有人改进。",
"What we collect:": "我们收集:",
"Page views (which screens you visit)": "页面浏览量(你访问的界面)",
"Time spent on pages": "在页面停留的时间",
"Basic device info (browser type, screen size)": "设备基本信息(浏览器类型、屏幕尺寸)",
"Your IP address (anonymized — last part removed for privacy)": "你的 IP 地址(已匿名化——出于隐私保护会移除最后一部分)",
"What we DON'T collect:": "我们不会收集:",
"Personal info (names, emails, passwords)": "个人信息(姓名、邮箱、密码)",
"Exact location": "精确位置",
"Any files or content you interact with": "你接触的任何文件或内容",
"Why anonymous?": "为什么是匿名的?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "你的 IP 会自动被截短,任何人都无法通过这些数据识别你的身份。",
"Your options:": "你的选择:",
"Continue:": "继续:",
"We'll track anonymized usage to help improve the app.": "我们会跟踪匿名化的使用情况,以帮助改进应用。",
"Opt out:": "拒绝:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "点击下面的\"拒绝\"按钮以关闭跟踪。(我们会尊重这个选择,但你可能会错过基于集体反馈的未来改进。)",
"Data goes to: Google Analytics. See their privacy policy.": "数据将发送至:Google Analytics。查看其隐私政策。",
"Accept": "接受",
"Allow": "允许",
"Deny": "拒绝",
"re-initiate-connection": "重新连接",
"Privacy Settings": "隐私设置",
"Change your preference:": "更改您的偏好:",
"Enable analytics:": "启用分析:",
"Disable analytics:": "禁用分析:",
"Enable Analytics": "启用分析",
"Disable Analytics": "禁用分析",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "点击下面的禁用按钮以停止跟踪。(我们会尊重您的选择,但您可能会错过基于集体反馈的未来改进。)",
"their privacy policy": "其隐私政策"
}
================================================
FILE: src/client-viewer/public/locales/zh_TW/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "正在等待使用者單擊螢幕共享裝置上的允許按鈕...",
"Waiting for user to select source to share from screen sharing device...": "正在等待使用者從螢幕共享裝置選擇要共享的源...",
"My Device Info": "我的裝置資訊",
"Device Type": "裝置型別",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "您的裝置 IP 應該與執行 Deskreen-CE 的計算機上出現的警報彈出視窗中的 '裝置 IP' 相匹配。",
"Device IP": "裝置 IP",
"Device Browser": "裝置瀏覽器",
"Device OS": "裝置作業系統",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "這些詳細資訊應與您在螢幕共享裝置上的警報彈出視窗中看到的資訊相匹配。",
"Deskreen-CE Screen Viewer": "Deskreen-CE 螢幕檢視器",
"Connected!": "已連線!",
"Error occurred": "出現錯誤",
"Deskreen-CE Error Dialog": "Deskreen-CE 錯誤對話方塊",
"Something went wrong": "出問題了",
"You may close this browser window then try to connect again": "您可以關閉此瀏覽器視窗,然後嘗試重新連線",
"An unknown error occurred": "出現未知錯誤",
"You were not allowed to connect": "您不能連線",
"You were disconnected": "您將斷開連線",
"WebRTC error occurred": "出現 WebRTC 錯誤",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "如果你喜歡 Deskreen-CE,可以考慮出錢。Deskreen-CE 是開源的。您的捐贈使我們有動力讓 Deskreen-CE 變得更好。",
"Donate": "捐贈",
"get-deskreen-pro": "取得 Deskreen Pro",
"get-deskreen-pro-tooltip": "取得 Deskreen Pro - 開啟下載頁面。",
"Video stream is paused": "影片流暫停",
"Video stream is playing": "正在播放影片流",
"Video stream paused after exiting fullscreen. Please click Play to continue.": "退出全螢幕後影片流已暫停。請點擊播放以繼續。",
"Pause": "暫停",
"Play": "播放",
"Video Settings": "影片設定",
"Flip": "翻轉",
"Video quality has been changed to": "影片質量已更改為",
"Click to Open Video Settings": "單擊以開啟影片設定",
"Click to Enter Full Screen Mode": "單擊以進入全屏模式",
"Click to Play Video": "單擊以播放影片",
"Click to Pause Video": "單擊以暫停影片",
"Default video player has been turned OFF": "預設影片播放器已關閉",
"Default video player has been turned ON": "預設影片播放器已開啟",
"ON": "開啟",
"OFF": "關閉",
"Default Video Player": "預設影片播放器",
"Click to visit our website": "點選訪問我們的網站",
"Video is flipped horizontally": "影片水平翻轉",
"flip-the-screen-is-pro-version-only": "翻轉螢幕僅在 Pro 版本中可用",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻譯尚未新增到 UI,但歡迎您的翻譯!功能將很快新增,因此需要您的翻譯",
"Click to see connection info": "單擊以檢視連線資訊",
"Pair ID": "配對 ID",
"Unpair": "取消配對",
"Session ID": "會話 ID",
"Click to boost video stream if it is lagging": "如果影片流滯後,請單擊以提高影片流",
"Privacy Notice: Analytics in Deskreen CE Viewer": "隱私提示:Deskreen CE Viewer 的分析",
"Analytics Reference": "分析參考",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "此應用程式使用 Google Analytics(Google 提供的免費服務)以匿名方式追蹤基本使用資料。這有助於我們了解使用者如何使用應用程式,從而為所有人帶來改進。",
"What we collect:": "我們收集:",
"Page views (which screens you visit)": "頁面瀏覽量(你造訪的畫面)",
"Time spent on pages": "在頁面停留的時間",
"Basic device info (browser type, screen size)": "裝置基本資訊(瀏覽器類型、螢幕尺寸)",
"Your IP address (anonymized — last part removed for privacy)": "你的 IP 位址(已匿名化 — 為保護隱私會移除最後一段)",
"What we DON'T collect:": "我們不會收集:",
"Personal info (names, emails, passwords)": "個人資訊(姓名、電子郵件、密碼)",
"Exact location": "精確位置",
"Any files or content you interact with": "你互動的任何檔案或內容",
"Why anonymous?": "為什麼要匿名?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "你的 IP 會自動被截短,沒有人能根據這些資料識別你的身份。",
"Your options:": "你的選項:",
"Continue:": "繼續:",
"We'll track anonymized usage to help improve the app.": "我們會追蹤匿名化的使用情況,協助改進應用程式。",
"Opt out:": "拒絕:",
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "點擊下方的「拒絕」按鈕以停用追蹤。(我們會尊重此選擇,但你可能會錯過基於集體回饋的未來改善。)",
"Data goes to: Google Analytics. See their privacy policy.": "資料傳送至:Google Analytics。查看其隱私權政策。",
"Accept": "接受",
"Allow": "允許",
"Deny": "拒絕",
"re-initiate-connection": "重新連線",
"Privacy Settings": "隱私設定",
"Change your preference:": "更改您的偏好:",
"Enable analytics:": "啟用分析:",
"Disable analytics:": "停用分析:",
"Enable Analytics": "啟用分析",
"Disable Analytics": "停用分析",
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "點擊下面的停用按鈕以停止追蹤。(我們會尊重您的選擇,但您可能會錯過基於集體反饋的未來改進。)",
"their privacy policy": "其隱私權政策"
}
================================================
FILE: src/client-viewer/public/manifest.json
================================================
{
"short_name": "Deskreen CE",
"name": "Deskreen CE Makes Any Device a Second Screen For Your Computer",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: src/client-viewer/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: src/client-viewer/scripts/ga-interceptor.js
================================================
(function () {
const CONSENT_KEY = 'deskreen_ga_consent';
const GA_DOMAINS = [
'google-analytics.com',
'googletagmanager.com',
'google-analytics.co',
'analytics.google.com',
];
for (let i = 1; i <= 20; i++) {
GA_DOMAINS.push('region' + i + '.google-analytics.com');
}
function getConsentStatus() {
try {
const stored = localStorage.getItem(CONSENT_KEY);
return stored === 'accepted' ? 'accepted' : null;
} catch {
return null;
}
}
function isGoogleAnalyticsUrl(url) {
try {
const urlObj = new URL(url, window.location.href);
const hostname = urlObj.hostname.toLowerCase();
return GA_DOMAINS.some(function (domain) {
return hostname === domain || hostname.endsWith('.' + domain);
});
} catch {
return false;
}
}
function shouldBlockRequest() {
return getConsentStatus() !== 'accepted';
}
function isLocalIP(ip) {
const parts = ip.split('.').map(Number);
if (parts.length !== 4 || parts.some(isNaN)) {
return false;
}
// 127.0.0.0/8
if (parts[0] === 127) return true;
// 10.0.0.0/8
if (parts[0] === 10) return true;
// 172.16.0.0/12
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
// 192.168.0.0/16
if (parts[0] === 192 && parts[1] === 168) return true;
return false;
}
function sanitizeGAUrl(url) {
try {
const urlObj = new URL(url);
// only sanitize /g/collect requests
if (!urlObj.pathname.includes('/g/collect')) {
return url;
}
const dlParam = urlObj.searchParams.get('dl');
if (!dlParam) {
return url;
}
try {
const dlUrl = new URL(decodeURIComponent(dlParam));
const hostname = dlUrl.hostname;
if (isLocalIP(hostname)) {
urlObj.searchParams.set('dl', encodeURIComponent('http://localhost'));
return urlObj.toString();
}
} catch {
// if dl parameter is not a valid URL, leave it as is
}
return url;
} catch {
return url;
}
}
// intercept fetch
if (window.fetch) {
const originalFetch = window.fetch;
window.fetch = function (input, init) {
let url =
typeof input === 'string'
? input
: input instanceof Request
? input.url
: '';
if (isGoogleAnalyticsUrl(url)) {
if (shouldBlockRequest()) {
return Promise.reject(
new Error(
'Google Analytics request blocked: user consent not granted',
),
);
}
url = sanitizeGAUrl(url);
if (input instanceof Request) {
input = new Request(url, init || input);
} else {
input = url;
}
}
return originalFetch.apply(this, arguments);
};
}
// intercept XMLHttpRequest
if (window.XMLHttpRequest) {
const XHR = window.XMLHttpRequest;
const originalOpen = XHR.prototype.open;
const originalSend = XHR.prototype.send;
XHR.prototype.open = function (method, url, async, username, password) {
let urlString = typeof url === 'string' ? url : url.toString();
if (isGoogleAnalyticsUrl(urlString)) {
if (shouldBlockRequest()) {
throw new Error(
'Google Analytics request blocked: user consent not granted',
);
}
urlString = sanitizeGAUrl(urlString);
url = urlString;
}
this._interceptedUrl = urlString;
return originalOpen.apply(this, arguments);
};
XHR.prototype.send = function () {
const url = this._interceptedUrl || '';
if (isGoogleAnalyticsUrl(url) && shouldBlockRequest()) {
return;
}
return originalSend.apply(this, arguments);
};
}
// intercept sendBeacon
if (navigator.sendBeacon) {
const originalSendBeacon = navigator.sendBeacon;
navigator.sendBeacon = function (url, data) {
let urlString = typeof url === 'string' ? url : url.toString();
if (isGoogleAnalyticsUrl(urlString)) {
if (shouldBlockRequest()) {
return false;
}
urlString = sanitizeGAUrl(urlString);
url = urlString;
}
return originalSendBeacon.call(this, url, data);
};
}
})();
================================================
FILE: src/client-viewer/src/App.css
================================================
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
================================================
FILE: src/client-viewer/src/App.tsx
================================================
import React, { useEffect, useState } from 'react';
import MainView from './containers/MainView';
import PrivacyConsentDialog from './components/PrivacyConsentDialog';
import LoadingScreen from './components/LoadingScreen';
import {
getConsentStatus,
setConsentStatus,
loadGoogleAnalytics,
getGaTagIdFromMeta,
updateAnalyticsConsent,
} from './utils/analytics';
const App: React.FC = () => {
// Helper function to check for prerendering safely
const isCurrentlyPrerendering = () => {
// Check if 'document' and 'prerendering' property exist
return typeof document !== 'undefined' &&
typeof document.prerendering === 'boolean'
? document.prerendering
: false; // Default to false if the property doesn't exist
};
const [isTrulyVisible, setIsTrulyVisible] = useState(
!isCurrentlyPrerendering(),
);
const [showConsentDialog, setShowConsentDialog] = useState(false);
const [hasConsent, setHasConsent] = useState(false);
useEffect(() => {
// Only set up listeners if document.prerendering is supported
if (
typeof document !== 'undefined' &&
typeof document.prerendering === 'boolean'
) {
const handlePrerenderChange = () => {
// When the prerendering state changes, update isTrulyVisible
// It becomes true when document.prerendering is false (i.e., page is activated)
setIsTrulyVisible(!document.prerendering);
};
// If it was initially prerendering, listen for the change to activate.
// The { once: true } option is useful if you only care about the first activation.
// However, if a page could theoretically go back into a prerender state (less common for user navigation),
// you might remove { once: true } but then also need more complex logic.
// For the typical "prerender then activate" flow, { once: true } is fine.
if (document.prerendering) {
document.addEventListener('prerenderingchange', handlePrerenderChange, {
once: true,
});
}
return () => {
// Cleanup the event listener
document.removeEventListener(
'prerenderingchange',
handlePrerenderChange,
);
};
}
// If document.prerendering is not supported, isTrulyVisible is already true
// (due to the initial useState value and isCurrentlyPrerendering fallback),
// so no specific effect logic is needed for that case here.
}, []); // Empty dependency array means this effect runs once on mount and cleans up on unmount.
useEffect(() => {
if (!isTrulyVisible) {
return;
}
// check consent status first
const consentStatus = getConsentStatus();
// load GA immediately when page is visible (before consent)
const gaTagId = getGaTagIdFromMeta();
if (gaTagId && gaTagId !== '%VITE_CLIENT_VIEWER_GA_TAG%') {
loadGoogleAnalytics(gaTagId);
// if user previously accepted consent, ensure page_view is sent
if (consentStatus === 'accepted') {
// wait a bit for GA to load, then update consent and send page_view
setTimeout(() => {
updateAnalyticsConsent('accepted');
}, 500);
}
}
if (consentStatus === 'accepted') {
setHasConsent(true);
} else if (consentStatus === 'opted-out') {
// user previously opted out - allow app usage without analytics
setHasConsent(true);
// ensure analytics consent is set to denied
updateAnalyticsConsent('opted-out');
} else {
// no consent yet - show dialog
setShowConsentDialog(true);
}
}, [isTrulyVisible]);
const handleAccept = () => {
setConsentStatus('accepted');
setShowConsentDialog(false);
setHasConsent(true);
// update GA consent to granted and send page_view
updateAnalyticsConsent('accepted');
};
const handleOptOut = () => {
// set consent status to opted-out so user can continue using app without analytics
setConsentStatus('opted-out');
setShowConsentDialog(false);
setHasConsent(true);
// update GA consent to denied and ensure no analytics are sent
updateAnalyticsConsent('opted-out');
};
if (!isTrulyVisible) {
// Render a minimal version or nothing while the browser is prerendering.
// This prevents heavy computations or API calls during the browser's prerender phase.
// console.log("Page is being prerendered by the browser (or support for detection is present). Waiting for activation.");
return ;
}
if (!hasConsent) {
return (
<>
>
);
}
return ;
};
export default App;
================================================
FILE: src/client-viewer/src/api/config.ts
================================================
let host;
let protocol;
let port;
if (!host && !protocol && !port) {
host = window.location.host.split(':')[0];
protocol = 'http';
port = 3131;
}
export default {
host,
port,
protocol,
};
================================================
FILE: src/client-viewer/src/api/generator.ts
================================================
import config from './config';
export const generateUrl = (resourceName = '') => {
const { port, protocol, host } = config;
const resourcePath = resourceName;
if (!host) {
return `/localhost`;
}
return `${protocol}://${host}:${port}/${resourcePath}`;
};
export default {
generateUrl,
};
================================================
FILE: src/client-viewer/src/assets/index.html
================================================
Deskreen CE Viewer
You need to enable JavaScript to run this app.
================================================
FILE: src/client-viewer/src/assets/locales/da/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Venter på at brugeren klikker TILLAD knappen på skærmdelingsenheden...",
"Waiting for user to select source to share from screen sharing device...": "Venter på at brugeren vælger kilden, som skal deles fra skærmdelingsenheden...",
"My Device Info": "Min enhedsinfo",
"Device Type": "Enhedstype",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Din Enheds IP burde matche sammen med den Enheds IP, som ses i advarselspopup'en vist på din computer, hvor Deskreen-CE kører",
"Device IP": "Enhedens IP",
"Device Browser": "Enhedens Browser",
"Device OS": "Enhedens Operativsystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Disse detaljer skal matche med dem, som du ser i advarselspopup'en på computerskærmen, hvor Deskreen-CE kører",
"Deskreen-CE Screen Viewer": "Deskreen-CE Skærmviser",
"Connected!": "Forbundet!",
"Error occurred": "Der skete en fejl",
"Deskreen-CE Error Dialog": "Deskreen-CE Fejl Dialog",
"Something went wrong": "Noget gik galt",
"You may close this browser window then try to connect again": "Prøv at lukke dette browservindue og forbind igen",
"An unknown error occurred": "Der opstod en ukendt fejl",
"You were not allowed to connect": "Der blev ikke tilladt forbindelse",
"You were disconnected": "Du blev afbrudt",
"WebRTC error occurred": "Der opstod en WebRTC fejl",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Hvis du er vild med Deskreen-CE, så overvej at bidrage til Deskreen-CE financielt. Deskreen-CE er open-source. Dine donationer hjælper os med at forblive motiverede for at gøre Deskreen-CE endnu bedre.",
"Donate": "Donér",
"get-deskreen-pro": "Hent Deskreen Pro",
"get-deskreen-pro-tooltip": "Hent Deskreen Pro - åbner downloadsiden.",
"Video stream is paused": "Videostream er pauset",
"Video stream is playing": "Videostream kører",
"Pause": "Pause",
"Play": "Kør",
"Video Settings": "Videoindstillinger",
"Flip": "Vend",
"Video quality has been changed to": "Videokvaliteten er blevet ændret til",
"Click to Open Video Settings": "Klik her for at åbne Videoindstillinger",
"Click to Enter Full Screen Mode": "Klik her for at gå ind i fuldskærmstilstand",
"Default video player has been turned OFF": "Standard videospiller er blevet slået FRA",
"Default video player has been turned ON": "Standard videospiller er blevet slået TIL",
"ON": "TIL",
"OFF": "FRA",
"Default Video Player": "Standard Videospiller",
"Click to visit our website": "Klik jer for at besøge vores hjemmeside",
"Video is flipped horizontally": "Videoen er vendt horisontalt",
"flip-the-screen-is-pro-version-only": "Vende skærmen er kun tilgængelig i Pro-versionen",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klik jer for at se forbindelsesinfo",
"Pair ID": "Par ID",
"Unpair": "Annullér Pardannelse",
"Session ID": "Sessionsid",
"Click to boost video stream if it is lagging": "Klik her for at booste videostreamen, hvis det lagger",
"Privacy Notice: Analytics in This App": "Privatlivsmeddelelse: Analyse i denne app",
"Analytics Reference": "Analytik Reference",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Denne app bruger Google Analytics (en gratis tjeneste fra Google) til anonymt at spore grundlæggende brugsdata. Det hjælper os med at forstå, hvordan appen bruges, så vi kan forbedre den for alle.",
"What we collect:": "Det vi indsamler:",
"Page views (which screens you visit)": "Sidevisninger (hvilke skærme du besøger)",
"Time spent on pages": "Tid brugt på sider",
"Basic device info (browser type, screen size)": "Grundlæggende enhedsinfo (browsertype, skærmstørrelse)",
"Your IP address (anonymized — last part removed for privacy)": "Din IP-adresse (anonymiseret — den sidste del fjernes af hensyn til privatlivet)",
"What we DON'T collect:": "Det vi IKKE indsamler:",
"Personal info (names, emails, passwords)": "Personlige oplysninger (navne, e-mails, adgangskoder)",
"Exact location": "Præcis placering",
"Any files or content you interact with": "Filer eller indhold, du interagerer med",
"Why anonymous?": "Hvorfor anonymt?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Din IP bliver automatisk afkortet, og ingen kan identificere dig personligt ud fra disse data.",
"Your options:": "Dine muligheder:",
"Continue:": "Fortsæt:",
"We'll track anonymized usage to help improve the app.": "Vi registrerer anonymiseret brug for at hjælpe os med at forbedre appen.",
"Opt out:": "Fravælg:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik på knappen Afslå nedenfor for at deaktivere sporing. (Vi respekterer dette valg, men du kan gå glip af fremtidige forbedringer baseret på samlet feedback.)",
"Data goes to: Google Analytics. See their privacy policy.": "Data sendes til: Google Analytics. Se deres privatlivspolitik.",
"Accept": "Accepter",
"Disagree": "Afslå",
"re-initiate-connection": "Genstart forbindelse"
}
================================================
FILE: src/client-viewer/src/assets/locales/de/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Warten bis der Nutzer auf dem Freigabegerät auf ZULASSEN klickt ...",
"Waiting for user to select source to share from screen sharing device...": "Warten bis der Nutzer eine Quelle für die Freigabe auswählt...",
"My Device Info": "Meine Geräteinformationen",
"Device Type": "Gerätetyp",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Deine Geräte-IP sollte mit der \"Geräte-IP\" im Dialog auf dem Computer, auf dem Deskreen-CE läuft, übereinstimmen.",
"Device IP": "Geräte-IP",
"Device Browser": "Geräte-Browser",
"Device OS": "Geräte-Betriebssystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Diese Informationen sollten mit denen im Dialog auf dem Freigabegerät übereinstimmen.",
"Deskreen-CE Screen Viewer": "Deskreen-CE Bildschrimansicht",
"Connected!": "Verbunden!",
"Error occurred": "Ein Fehler ist aufgetreten",
"Deskreen-CE Error Dialog": "Deskreen-CE Fehler Dialog",
"Something went wrong": "Etwas ist schief gegangen",
"You may close this browser window then try to connect again": "Schließe das Browserfenster und probiere es erneut",
"An unknown error occurred": "Ein unbekannter Fehler ist aufgetreten",
"You were not allowed to connect": "Die Verbindung wurde nicht zugelassen",
"You were disconnected": "Die Verbindung wurde getrennt",
"WebRTC error occurred": "WebRTC Fehler aufgetreten",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Wenn dir Deskreen-CE gefällt, denke über eine Spende nach. Deskreen-CE ist Open-Source. Spenden motivieren uns, Deskreen-CE noch besser zu machen.",
"Donate": "Spenden",
"get-deskreen-pro": "Deskreen Pro erhalten",
"get-deskreen-pro-tooltip": "Deskreen Pro erhalten - öffnet die Download-Seite.",
"Video stream is paused": "Videostream ist pausiert",
"Video stream is playing": "Videostream läuft",
"Pause": "Pause",
"Play": "Abspielen",
"Video Settings": "Video Einstellungen",
"Flip": "Drehen",
"Video quality has been changed to": "Videoqualität wurde geändert zu",
"Click to Open Video Settings": "Klicken um Videoeinstellungen zu öffnen",
"Click to Enter Full Screen Mode": "Klicken für Vollbild",
"Default video player has been turned OFF": "Standard Video-Player wurde ausgeschaltet",
"Default video player has been turned ON": "Standard Video-Player wurde eingeschaltet",
"ON": "AN",
"OFF": "AUS",
"Default Video Player": "Standard Video-Player",
"Click to visit our website": "Klicken um unsere Website zu besuchen",
"Video is flipped horizontally": "Das Video ist horizontal gedreht",
"flip-the-screen-is-pro-version-only": "Bildschirm umdrehen ist nur in der Pro-Version verfügbar",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klicken um Verbindungsinformationen anzuzeigen",
"Pair ID": "Kopplungs-ID",
"Unpair": "Entkoppeln",
"Session ID": "Sitzungs-ID",
"Click to boost video stream if it is lagging": "Klicken um den Videostream zu verbessern, wenn er verzögert ist.",
"Privacy Notice: Analytics in This App": "Datenschutzhinweis: Analyse in dieser App",
"Analytics Reference": "Analytik-Referenz",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Diese App verwendet Google Analytics (einen kostenlosen Dienst von Google), um anonyme Nutzungsdaten zu erfassen. So verstehen wir, wie die App genutzt wird, und können sie für alle verbessern.",
"What we collect:": "Was wir sammeln:",
"Page views (which screens you visit)": "Seitenaufrufe (welche Ansichten du besuchst)",
"Time spent on pages": "Verweildauer auf Seiten",
"Basic device info (browser type, screen size)": "Grundlegende Geräteinformationen (Browsertyp, Bildschirmgröße)",
"Your IP address (anonymized — last part removed for privacy)": "Deine IP-Adresse (anonymisiert – der letzte Teil wird aus Datenschutzgründen entfernt)",
"What we DON'T collect:": "Was wir NICHT sammeln:",
"Personal info (names, emails, passwords)": "Personenbezogene Daten (Namen, E-Mails, Passwörter)",
"Exact location": "Genauer Standort",
"Any files or content you interact with": "Dateien oder Inhalte, mit denen du interagierst",
"Why anonymous?": "Warum anonym?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Deine IP wird automatisch gekürzt, sodass dich niemand anhand dieser Daten identifizieren kann.",
"Your options:": "Deine Optionen:",
"Continue:": "Weiter:",
"We'll track anonymized usage to help improve the app.": "Wir erfassen anonymisierte Nutzung, um die App zu verbessern.",
"Opt out:": "Ablehnen:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicke auf den Button Ablehnen unten, um das Tracking zu deaktivieren. (Wir respektieren diese Entscheidung, aber du könntest zukünftige Verbesserungen verpassen, die auf kollektivem Feedback basieren.)",
"Data goes to: Google Analytics. See their privacy policy.": "Datenempfänger: Google Analytics. Lies deren Datenschutzerklärung.",
"Accept": "Akzeptieren",
"Disagree": "Ablehnen",
"re-initiate-connection": "Verbindung erneut herstellen"
}
================================================
FILE: src/client-viewer/src/assets/locales/en/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Waiting for video stream of screen sharing device...",
"Waiting for user to select source to share from screen sharing device...": "Waiting for user to select source to share from screen sharing device...",
"My Device Info": "My Device Info",
"Device Type": "Device Type",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Your Device IP should match with \"Device IP\" in alert popup appeared on your computer, where Deskreen-CE is running.",
"Device IP": "Device IP",
"Device Browser": "Device Browser",
"Device OS": "Device OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "These details should match with the ones that you see in alert popup on screen sharing device.",
"Deskreen-CE Screen Viewer": "Deskreen-CE Screen Viewer",
"Connected!": "Connected!",
"Error occurred": "Error occurred",
"Deskreen-CE Error Dialog": "Deskreen-CE Error Dialog",
"Something went wrong": "Something went wrong",
"You may close this browser window then try to connect again": "You may close this browser window then try to connect again",
"An unknown error occurred": "An unknown error occurred",
"You were not allowed to connect": "You were not allowed to connect",
"You were disconnected": "You were disconnected",
"WebRTC error occurred": "WebRTC error occurred",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "If you like Deskreen-CE, consider contributing financially. Deskreen-CE is open-source. Your donations keep us motivated to make Deskreen-CE even better.",
"Donate": "Donate",
"get-deskreen-pro": "Get Deskreen Pro",
"get-deskreen-pro-tooltip": "Get Deskreen Pro - opens the download page.",
"Video stream is paused": "Video stream is paused",
"Video stream is playing": "Video stream is playing",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Video Settings",
"Flip": "Flip",
"Video quality has been changed to": "Video quality has been changed to",
"Click to Open Video Settings": "Click to Open Video Settings",
"Click to Enter Full Screen Mode": "Click to Enter Full Screen Mode",
"Default video player has been turned OFF": "Default video player has been turned OFF",
"Default video player has been turned ON": "Default video player has been turned ON",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "Default Video Player",
"Click to visit our website": "Click to visit our website",
"Video is flipped horizontally": "Video is flipped horizontally",
"flip-the-screen-is-pro-version-only": "Flip the screen is pro version only",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"re-initiate-connection": "Re-initiate Connection"
}
================================================
FILE: src/client-viewer/src/assets/locales/es/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Esperando que el usuario haga clic en el botón PERMITIR en el dispositivo para compartir pantalla ...",
"Waiting for user to select source to share from screen sharing device...": "Esperando que el usuario seleccione la fuente para compartir desde el dispositivo para compartir pantalla ...",
"My Device Info": "Información de mi dispositivo",
"Device Type": "Tipo del dispositivo",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "La IP de tu dispositivo debe coincidir con \"IP del dispositivo \" en la ventana emergente de alerta que apareció en la computadora donde se está ejecutando Deskreen-CE.",
"Device IP": "IP del dispositivo",
"Device Browser": "Navegador del dispositivo",
"Device OS": "SO del dispositivo",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Estos detalles deben coincidir con los que ves en la ventana emergente en el dispositivo para compartir pantalla.",
"Deskreen-CE Screen Viewer": "Visor de pantalla de Deskreen-CE",
"Connected!": "¡Conectado!",
"Error occurred": "Ocurrió un error",
"Deskreen-CE Error Dialog": "Cuadro de diálogo de error de Deskreen-CE",
"Something went wrong": "Algo salió mal",
"You may close this browser window then try to connect again": "Puedes cerrar esta ventana del navegador y luego intentar conectarte nuevamente",
"An unknown error occurred": "Ocurrió un error desconocido",
"You were not allowed to connect": "No se te permitió conectarte",
"You were disconnected": "Fuiste desconectado",
"WebRTC error occurred": "Ocurrió un error de WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Si te gusta Deskreen-CE, considera la posibilidad de contribuir económicamente. Deskreen-CE es de código abierto. Tus donaciones nos mantienen motivados para hacer que Deskreen-CE sea aún mejor.",
"Donate": "Donar",
"get-deskreen-pro": "Obtener Deskreen Pro",
"get-deskreen-pro-tooltip": "Obtener Deskreen Pro - abre la página de descarga.",
"Video stream is paused": "La transmisión de video está en pausa",
"Video stream is playing": "La transmisión de video está en reproducción",
"Pause": "Pausa",
"Play": "Reproducir",
"Video Settings": "Configuraciones de video",
"Flip": "Voltear",
"Video quality has been changed to": "La calidad de video se ha cambiado a",
"Click to Open Video Settings": "Clic para abrir las configuraciones de video",
"Click to Enter Full Screen Mode": "Clic para entrar en el modo de pantalla completa",
"Default video player has been turned OFF": "El reproductor de video predeterminado se ha APAGADO",
"Default video player has been turned ON": "El reproductor de video predeterminado se ha ENCENDIDO",
"ON": "ENCENDER",
"OFF": "APAGAR",
"Default Video Player": "Reproductor de video predeterminado",
"Click to visit our website": "Clic para visitar nuestro sitio web",
"Video is flipped horizontally": "El video se ha volteado horizontalmente",
"flip-the-screen-is-pro-version-only": "Voltear la pantalla está disponible solo en la versión Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Clic para ver la información de la conexión",
"Pair ID": "ID del par",
"Unpair": "Desemparejar",
"Session ID": "ID de sesión",
"Click to boost video stream if it is lagging": "Haz clic para mejorar la transmisión de video si se está retrasando",
"Privacy Notice: Analytics in This App": "Aviso de privacidad: Analítica en esta aplicación",
"Analytics Reference": "Referencia de Analítica",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Esta aplicación utiliza Google Analytics (un servicio gratuito de Google) para registrar de manera anónima datos básicos de uso. Esto nos ayuda a entender cómo se usa la aplicación para poder mejorarla para todos.",
"What we collect:": "Lo que recopilamos:",
"Page views (which screens you visit)": "Vistas de página (qué pantallas visitas)",
"Time spent on pages": "Tiempo invertido en las páginas",
"Basic device info (browser type, screen size)": "Información básica del dispositivo (tipo de navegador, tamaño de pantalla)",
"Your IP address (anonymized — last part removed for privacy)": "Tu dirección IP (anonimizada: se elimina la última parte por privacidad)",
"What we DON'T collect:": "Lo que NO recopilamos:",
"Personal info (names, emails, passwords)": "Información personal (nombres, correos electrónicos, contraseñas)",
"Exact location": "Ubicación exacta",
"Any files or content you interact with": "Cualquier archivo o contenido con el que interactúes",
"Why anonymous?": "¿Por qué anónimo?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Tu IP se acorta automáticamente, y nadie puede identificarte personalmente con estos datos.",
"Your options:": "Tus opciones:",
"Continue:": "Continuar:",
"We'll track anonymized usage to help improve the app.": "Registraremos uso anonimizado para ayudar a mejorar la aplicación.",
"Opt out:": "Rechazar:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Haz clic en el botón Rechazar a continuación para desactivar el seguimiento. (Respetaremos esta elección, pero podrías perderte mejoras futuras basadas en comentarios colectivos).",
"Data goes to: Google Analytics. See their privacy policy.": "Los datos van a: Google Analytics. Consulta su política de privacidad.",
"Accept": "Aceptar",
"Disagree": "Rechazar",
"re-initiate-connection": "Restablecer conexión"
}
================================================
FILE: src/client-viewer/src/assets/locales/fi/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Odotetaan että käyttäjä napsauttaa SALLI-painiketta ruudunjakolaitteessa...",
"Waiting for user to select source to share from screen sharing device...": "Odotetaan että käyttäjä valitsee ruudunjakolaitteesta lähteen joka jaetaan...",
"My Device Info": "Tiedot laitteestani",
"Device Type": "Laitteen malli",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Laitteesi IP:n tulisi täsmätä \"Laitteen IP\" kohdassa joka näkyy ilmoiteikkunassa tietokoneella jossa Deskreen-CE on käynnissä.",
"Device IP": "Laitteen IP",
"Device Browser": "Laiteselain",
"Device OS": "Laitteen käyttöjärjestelmä",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Näiden yksityiskohtien tulisi täsmätä niiden kanssa jotka näet ruudunjakolaitteen ilmoitekehotteessa, Deskreen-CE:in ollessa käynnissä",
"Deskreen-CE Screen Viewer": "Deskreen-CE-ruutukatselin",
"Connected!": "Yhdistetty!",
"Error occurred": "Tapahtui virhe",
"Deskreen-CE Error Dialog": "Deskreen-CE:in virhekooste",
"Something went wrong": "Jokin meni pieleen",
"You may close this browser window then try to connect again": "Voit sulkea tämän selainikkunan koettaaksesi uudelleenyhdistämistä",
"An unknown error occurred": "Ilmeni tuntematon virhe",
"You were not allowed to connect": "Yhdistämistä ei sallittu",
"You were disconnected": "Sinulta katkesi yhteys",
"WebRTC error occurred": "Ilmeni WebRTC-virhe",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Mikäli pidät Deskreen-CE:istä, harkitsethan rahallista lahjoitusta. Deskreen-CE on avoimen lähdekoodin ohjelma. Lahjoituksesi auttavat motivaatiomme säilymisen kannalta tehdäksemme Deskreen-CE:istä vieläkin paremman.",
"Donate": "Lahjoita",
"get-deskreen-pro": "Hanki Deskreen Pro",
"get-deskreen-pro-tooltip": "Hanki Deskreen Pro - avaa lataussivun.",
"Video stream is paused": "Videolähetys on tauolla",
"Video stream is playing": "Videolähetys on käynnissä",
"Pause": "Tauko",
"Play": "Toista",
"Video Settings": "Asetukset videolle",
"Flip": "Käännä ympäri",
"Video quality has been changed to": "Videon laatu muutettiin määreeseen",
"Click to Open Video Settings": "Napsauta avataksesi videon asetukset",
"Click to Enter Full Screen Mode": "Napsauta siirtyäksesi kokoruututilaan",
"Default video player has been turned OFF": "Vakiollinen videotoisto-ohjelma on KYTKETTY POIS PÄÄLTÄ",
"Default video player has been turned ON": "Vakiollinen videotoisto-ohjelma on KYTKETTY PÄÄLLE",
"ON": "PÄÄLLÄ",
"OFF": "POIS",
"Default Video Player": "Vakiollinen videontoisto-ohjelma",
"Click to visit our website": "Napsauta vieraillaksesi verkkosivustollamme",
"Video is flipped horizontally": "Video käännetty vaakatasossa",
"flip-the-screen-is-pro-version-only": "Näytön kääntäminen on saatavilla vain Pro-versiossa",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Napsauta katsoaksesi tietoja yhteydestäsi",
"Pair ID": "Lateparin ID-tunniste",
"Unpair": "Poista laiteparitus",
"Session ID": "Istunnon ID-tunniste",
"Click to boost video stream if it is lagging": "Napsauta lisätyöntöapua videovirtaukselle mikäli se hidastelee",
"Privacy Notice: Analytics in This App": "Tietosuojailmoitus: Analytiikka tässä sovelluksessa",
"Analytics Reference": "Analytiikan viite",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Tämä sovellus käyttää Google Analyticsia (Googlelta saatava ilmainen palvelu) seuratakseen nimettömästi perustason käyttötietoja. Se auttaa meitä ymmärtämään, miten sovellusta käytetään, jotta voimme parantaa sitä kaikille.",
"What we collect:": "Mitä keräämme:",
"Page views (which screens you visit)": "Sivunäyttökerrat (mitä näkymiä käyt)",
"Time spent on pages": "Sivuille käytetty aika",
"Basic device info (browser type, screen size)": "Laitteen perustiedot (selaintyyppi, näytön koko)",
"Your IP address (anonymized — last part removed for privacy)": "IP-osoitteesi (anonymisoitu — viimeinen osa poistetaan yksityisyyden suojaamiseksi)",
"What we DON'T collect:": "Mitä emme kerää:",
"Personal info (names, emails, passwords)": "Henkilötietoja (nimiä, sähköposteja, salasanoja)",
"Exact location": "Tarkkaa sijaintia",
"Any files or content you interact with": "Tiedostoja tai sisältöä, joiden kanssa olet vuorovaikutuksessa",
"Why anonymous?": "Miksi anonyymisti?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP-osoitteesi lyhennetään automaattisesti, eikä sinua voi tunnistaa näiden tietojen perusteella.",
"Your options:": "Vaihtoehtosi:",
"Continue:": "Jatka:",
"We'll track anonymized usage to help improve the app.": "Seuraamme anonymisoitua käyttöä sovelluksen parantamiseksi.",
"Opt out:": "Kieltäydy:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Napsauta Hylkää-painiketta alla poistaaksesi seurannan käytöstä. (Kunnioitamme tätä valintaa, mutta saatat jäädä paitsi tulevista parannuksista, jotka perustuvat yhteiseen palautteeseen.)",
"Data goes to: Google Analytics. See their privacy policy.": "Tiedot lähetetään: Google Analytics. Tutustu heidän tietosuojakäytäntöönsä.",
"Accept": "Hyväksy",
"Disagree": "Hylkää",
"re-initiate-connection": "Käynnistä yhteys uudelleen"
}
================================================
FILE: src/client-viewer/src/assets/locales/fr/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "En attente de la validation depuis l'appareil source...",
"Waiting for user to select source to share from screen sharing device...": "En attente de la sélection de la source à partager depuis l'appareil source...",
"My Device Info": "Mes informations d'appareil",
"Device Type": "Type d'appareil",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Votre adresse IP doit correspondre avec l'\"Adresse IP\" affiché dans la pop-up affichée sur l'ordinateur depuis lequel Deskreen-CE est lancé.",
"Device IP": "IP de l'appareil",
"Device Browser": "Navigateur de l'appareil",
"Device OS": "OS de l'appareil",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Ces détails doivent correspondre avec ceux inscrits dans la pop-up affichée sur l'ordinateur depuis lequel Deskreen-CE est lancé..",
"Deskreen-CE Screen Viewer": "Écran de visionnage Deskreen-CE",
"Connected!": "Connecté!",
"Error occurred": "Une erreur est survenue",
"Deskreen-CE Error Dialog": "Boîte de dialogue d'erreur",
"Something went wrong": "Quelque chose s'est mal passé",
"You may close this browser window then try to connect again": "Vous devriez fermer cette fenêtre de navigateur et essayer de vous connecter de nouveau",
"An unknown error occurred": "Une erreur inconnue s'est produite",
"You were not allowed to connect": "Vous n'êtes pas autorisé à vous connecter",
"You were disconnected": "Vous avez été déconnecté",
"WebRTC error occurred": "Une erreur WebRTC s'est produite",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Si vous aimez Deskreen-CE, Vous pouvez contribuer financièrement. Deskreen-CE est open-source. Votre don nous motivera à rendre Deskreen-CE encore meilleur.",
"Donate": "Donner",
"get-deskreen-pro": "Obtenir Deskreen Pro",
"get-deskreen-pro-tooltip": "Obtenir Deskreen Pro - ouvre la page de téléchargement.",
"Video stream is paused": "Le flux vidéo est en pause",
"Video stream is playing": "Lecture du flux vidéo",
"Pause": "Pause",
"Play": "Lecture",
"Video Settings": "Paramètres Vidéo",
"Flip": "Tourner",
"Video quality has been changed to": "Qualité de la vidéo changée en",
"Click to Open Video Settings": "Cliquez pour ouvrir les paramètres vidéo",
"Click to Enter Full Screen Mode": "Cliquez pour passer en plein écran",
"Default video player has been turned OFF": "Le lecteur vidéo par défaut a été désactivé",
"Default video player has been turned ON": "Le lecteur vidéo par défaut a été activé",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "Lecteur vidéo par défaut",
"Click to visit our website": "Cliquez ici pour visiter notre site web",
"Video is flipped horizontally": "La vidéo à été tourner horizontallement",
"flip-the-screen-is-pro-version-only": "Retourner l'écran n'est disponible que dans la version Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Cliquez pour voir les informations de connexion",
"Pair ID": "ID d'appairage",
"Unpair": "Desappairer",
"Session ID": "ID de session",
"Click to boost video stream if it is lagging": "Cliquez pour booster le flux vidéo si vous rencontrez des ralentissements",
"Privacy Notice: Analytics in This App": "Avis de confidentialité : Analyses dans cette application",
"Analytics Reference": "Référence Analytique",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Cette application utilise Google Analytics (un service gratuit de Google) pour suivre anonymement des données d'utilisation de base. Cela nous aide à comprendre comment l'application est utilisée afin de l'améliorer pour tout le monde.",
"What we collect:": "Ce que nous recueillons :",
"Page views (which screens you visit)": "Pages consultées (les écrans que vous visitez)",
"Time spent on pages": "Temps passé sur les pages",
"Basic device info (browser type, screen size)": "Informations de base sur l'appareil (type de navigateur, taille de l'écran)",
"Your IP address (anonymized — last part removed for privacy)": "Votre adresse IP (anonymisée — la dernière partie est supprimée pour protéger votre vie privée)",
"What we DON'T collect:": "Ce que nous NE collectons PAS :",
"Personal info (names, emails, passwords)": "Informations personnelles (noms, adresses e-mail, mots de passe)",
"Exact location": "Localisation précise",
"Any files or content you interact with": "Les fichiers ou contenus avec lesquels vous interagissez",
"Why anonymous?": "Pourquoi anonyme ?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Votre adresse IP est automatiquement raccourcie, et personne ne peut vous identifier personnellement à partir de ces données.",
"Your options:": "Vos options :",
"Continue:": "Continuer :",
"We'll track anonymized usage to help improve the app.": "Nous suivrons l'utilisation anonymisée pour aider à améliorer l'application.",
"Opt out:": "Refuser :",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Cliquez sur le bouton Refuser ci-dessous pour désactiver le suivi. (Nous respecterons ce choix, mais vous pourriez manquer des améliorations futures basées sur les retours collectifs.)",
"Data goes to: Google Analytics. See their privacy policy.": "Les données sont envoyées à : Google Analytics. Consultez leur politique de confidentialité.",
"Accept": "Accepter",
"Disagree": "Refuser",
"re-initiate-connection": "Réinitialiser la connexion"
}
================================================
FILE: src/client-viewer/src/assets/locales/it/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "In attesa che l'utente faccia clic sul pulsante CONSENTI sul dispositivo di condivisione...",
"Waiting for user to select source to share from screen sharing device...": "In attesa che l'utente selezioni la sorgente da condividere dal dispositivo di condivisione...",
"My Device Info": "Info del mio Dispositivo",
"Device Type": "Tipologia Dispositivo",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "L'IP del tuo Dispositivo dovrebbe corrispondere a \"IP Dispositivo\" nel popup apparso sul tuo computer, dove Deskreen-CE è in esecuzione.",
"Device IP": "IP Dispositivo",
"Device Browser": "Browser Dispositivo",
"Device OS": "OS Dispositivo",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Questi dettagli dovrebbero corrispondere a quelli che vedi nel popup sul Dispositivo di condivisione.",
"Deskreen-CE Screen Viewer": "Visualizzatore dello schermo di Deskreen-CE",
"Connected!": "Connesso!",
"Error occurred": "Si è verificato un Errore",
"Deskreen-CE Error Dialog": "Finestra di dialogo degli errori di Deskreen-CE",
"Something went wrong": "Qualcosa è andato storto",
"You may close this browser window then try to connect again": "Puoi chiudere questa finestra del browser, quindi provare a connetterti di nuovo",
"An unknown error occurred": "Si è verificato un errore sconosciuto",
"You were not allowed to connect": "Non ti è stato permesso di connetterti",
"You were disconnected": "Sei stato disconnesso",
"WebRTC error occurred": "Si è verificato un errore WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Se ti piace Deskreen-CE, considera di contribuire finanziariamente. Deskreen-CE è open-source. Le tue donazioni ci motivano a rendere Deskreen-CE ancora migliore.",
"Donate": "Dona",
"get-deskreen-pro": "Ottieni Deskreen Pro",
"get-deskreen-pro-tooltip": "Ottieni Deskreen Pro - apre la pagina di download.",
"Video stream is paused": "Trasmissione Video in pausa",
"Video stream is playing": "Trasmissione Video in riproduzione",
"Pause": "Pausa",
"Play": "Riproduci",
"Video Settings": "Impostazioni Video",
"Flip": "Capovolgi",
"Video quality has been changed to": "La qualità Video è stata cambiata a",
"Click to Open Video Settings": "Clicca per aprire le Impostazioni Video",
"Click to Enter Full Screen Mode": "Clicca per entrare in modalità Schermo Intero",
"Default video player has been turned OFF": "il player video predefinito è stato spento",
"Default video player has been turned ON": "il player video predefinito è stato acceso",
"ON": "Acceso",
"OFF": "Spento",
"Default Video Player": "Player Video Predefinito",
"Click to visit our website": "Clicca per visitare il nostro sito",
"Video is flipped horizontally": "Il Video è capovolto orizzontalmente",
"flip-the-screen-is-pro-version-only": "Capovolgere lo schermo è disponibile solo nella versione Pro",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Clicca per vedere le info di connessione",
"Pair ID": "ID Coppia",
"Unpair": "Disaccoppia",
"Session ID": "ID Sessione",
"Click to boost video stream if it is lagging": "Clicca per incrementare il flusso video se sta andando a scatti",
"Privacy Notice: Analytics in This App": "Informativa sulla privacy: Analisi in questa app",
"Analytics Reference": "Riferimento Analitico",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Questa app utilizza Google Analytics (un servizio gratuito di Google) per tracciare in modo anonimo i dati di utilizzo di base. Questo ci aiuta a capire come viene usata l'app, così possiamo migliorarla per tutti.",
"What we collect:": "Cosa raccogliamo:",
"Page views (which screens you visit)": "Visualizzazioni di pagina (quali schermate visiti)",
"Time spent on pages": "Tempo trascorso sulle pagine",
"Basic device info (browser type, screen size)": "Informazioni di base sul dispositivo (tipo di browser, dimensioni dello schermo)",
"Your IP address (anonymized — last part removed for privacy)": "Il tuo indirizzo IP (anonimizzato — l'ultima parte viene rimossa per la privacy)",
"What we DON'T collect:": "Cosa NON raccogliamo:",
"Personal info (names, emails, passwords)": "Dati personali (nomi, email, password)",
"Exact location": "Posizione esatta",
"Any files or content you interact with": "Qualsiasi file o contenuto con cui interagisci",
"Why anonymous?": "Perché anonimo?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Il tuo IP viene accorciato automaticamente e nessuno può identificarti personalmente da questi dati.",
"Your options:": "Le tue opzioni:",
"Continue:": "Continua:",
"We'll track anonymized usage to help improve the app.": "Tracceremo l'utilizzo anonimizzato per aiutare a migliorare l'app.",
"Opt out:": "Rifiuta:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Fai clic sul pulsante Rifiuta qui sotto per disattivare il tracciamento. (Rispetteremo questa scelta, ma potresti perdere miglioramenti futuri basati sul feedback collettivo.)",
"Data goes to: Google Analytics. See their privacy policy.": "I dati vengono inviati a: Google Analytics. Consulta la loro informativa sulla privacy.",
"Accept": "Accetta",
"Disagree": "Rifiuta",
"re-initiate-connection": "Riavvia la connessione"
}
================================================
FILE: src/client-viewer/src/assets/locales/ja/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "画面共有デバイスでユーザーが「許可」をクリックするのを待っています...",
"Waiting for user to select source to share from screen sharing device...": "画面共有デバイスから共有するソースをユーザーが選択するのを待っています...",
"My Device Info": "このデバイスの情報",
"Device Type": "デバイスの種類",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Deskreen-CEが動作しているパソコンに表示されるアラートポップアップの\"デバイスIP\"と、このデバイスのデバイスIPが一致する必要があります。",
"Device IP": "デバイスのIP",
"Device Browser": "デバイスのブラウザ",
"Device OS": "デバイスのOS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "これらの内容は、画面共有デバイスのアラートポップアップに表示される内容と一致している必要があります。",
"Deskreen-CE Screen Viewer": "Deskreen-CE Screen Viewer",
"Connected!": "接続されました!",
"Error occurred": "エラーが発生しました",
"Deskreen-CE Error Dialog": "Deskreen-CE エラーダイアログ",
"Something went wrong": "何らかの問題が発生しました",
"You may close this browser window then try to connect again": "このブラウザを閉じてから、再度接続を試みてください",
"An unknown error occurred": "不明なエラーが発生しました",
"You were not allowed to connect": "接続が許可されていません",
"You were disconnected": "接続が切断されました",
"WebRTC error occurred": "WebRTCエラーが発生しました",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Deskreen-CEを気に入っていただけたなら、資金面での貢献をご検討ください。Deskreen-CEはオープンソースです。あなたの寄付により、私たちはDeskreen-CEをより良いものにするためのモチベーションを保つことができます。",
"Donate": "寄付",
"get-deskreen-pro": "Deskreen Pro を入手",
"get-deskreen-pro-tooltip": "Deskreen Pro を入手 - ダウンロードページを開きます。",
"Video stream is paused": "ビデオストリームを一時停止しています",
"Video stream is playing": "ビデオストリームを再生中です",
"Pause": "一時停止",
"Play": "再生",
"Video Settings": "ビデオ設定",
"Flip": "反転",
"Video quality has been changed to": "ビデオの画質を変更しました。画質:",
"Click to Open Video Settings": "クリックしてビデオ設定を開きます",
"Click to Enter Full Screen Mode": "クリックするとフルスクリーンモードになります",
"Default video player has been turned OFF": "デフォルトのビデオプレーヤーがOFFになっています",
"Default video player has been turned ON": "デフォルトのビデオプレーヤーがONになっています",
"ON": "ON",
"OFF": "OFF",
"Default Video Player": "デフォルトのビデオプレーヤー",
"Click to visit our website": "クリックするとウェブサイトが開きます",
"Video is flipped horizontally": "映像が水平方向に反転しています",
"flip-the-screen-is-pro-version-only": "画面の反転はPro版でのみ利用可能です",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "クリックすると接続情報が表示されます",
"Pair ID": "ペアID",
"Unpair": "ペア解除",
"Session ID": "セッションID",
"Click to boost video stream if it is lagging": "クリックすると、ビデオストリームが遅延している場合、ブーストされます",
"Privacy Notice: Analytics in This App": "プライバシーに関するお知らせ:このアプリでの解析について",
"Analytics Reference": "分析参照",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "このアプリは Google が提供する無料サービスの Google Analytics を使用して、基本的な利用データを匿名で追跡します。アプリの使われ方を理解し、すべてのユーザーのために改善するためです。",
"What we collect:": "収集するデータ:",
"Page views (which screens you visit)": "ページビュー(どの画面を表示したか)",
"Time spent on pages": "ページに滞在した時間",
"Basic device info (browser type, screen size)": "基本的な端末情報(ブラウザーの種類、画面サイズ)",
"Your IP address (anonymized — last part removed for privacy)": "IP アドレス(匿名化 — プライバシー保護のため末尾を削除)",
"What we DON'T collect:": "収集しないもの:",
"Personal info (names, emails, passwords)": "個人情報(氏名、メールアドレス、パスワード)",
"Exact location": "正確な位置情報",
"Any files or content you interact with": "操作したファイルやコンテンツ",
"Why anonymous?": "なぜ匿名なのですか?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP アドレスは自動的に短縮され、このデータから個人を特定することはできません。",
"Your options:": "選択肢:",
"Continue:": "続行:",
"We'll track anonymized usage to help improve the app.": "アプリ改善のために匿名化された利用状況を追跡します。",
"Opt out:": "オプトアウト:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "下の「同意しない」ボタンをクリックすると追跡を無効にできます。(この選択は尊重しますが、総合的なフィードバックに基づく将来の改善を逃す可能性があります。)",
"Data goes to: Google Analytics. See their privacy policy.": "データ送信先:Google Analytics。プライバシーポリシーをご確認ください。",
"Accept": "同意する",
"Disagree": "同意しない",
"re-initiate-connection": "再接続"
}
================================================
FILE: src/client-viewer/src/assets/locales/ko/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "공유할 기기의 사용자가 화면 공유 허용 버튼을 클릭하기를 기다리는 중 ...",
"Waiting for user to select source to share from screen sharing device...": "공유할 기기의 어떤 화면을 공유할지 선택을 기다리는 중...",
"My Device Info": "내 기기 정보",
"Device Type": "기기 종류",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "현재 기기의 IP는 Deskreen-CE 이 제공하는 \"Device IP\" 와 같아야 합니다.",
"Device IP": "기기 IP",
"Device Browser": "기기 브라우저",
"Device OS": "기기 OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "세부 사항은 화면 공유 장치에서 팝업에서 표시되는 것과 일치해야합니다.",
"Deskreen-CE Screen Viewer": "스크린 뷰어",
"Connected!": "연결되었습니다.",
"Error occurred": "오류가 발생했습니다",
"Deskreen-CE Error Dialog": "오류 알림",
"Something went wrong": "연결과정에 오류가 발생하였습니다",
"You may close this browser window then try to connect again": "이 브라우저 창을 닫은 다음 다시 연결하십시오.",
"An unknown error occurred": "알 수없는 오류가 발생했습니다",
"You were not allowed to connect": "이 기기는 연결이 허용되지 않았습니다",
"You were disconnected": "연결이 해제되었습니다",
"WebRTC error occurred": "WebRTC 오류가 발생했습니다",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "오픈소스 프로젝트에 재정적으로 기여하는 것은 더 좋은 프로그램 개발 동기를 부여합니다.",
"Donate": "기부하기",
"get-deskreen-pro": "Deskreen Pro 받기",
"get-deskreen-pro-tooltip": "Deskreen Pro 받기 - 다운로드 페이지를 엽니다.",
"Video stream is paused": "비디오 스트림이 일시 중지됩니다",
"Video stream is playing": "비디오 스트림이 재생 중입니다",
"Pause": "중지",
"Play": "재생",
"Video Settings": "비디오 설정",
"Flip": "화면 좌우 반전",
"Video quality has been changed to": "비디오 품질이 변경되었습니다",
"Click to Open Video Settings": "비디오 설정 열기",
"Click to Enter Full Screen Mode": "전체 화면 모드로 들어가려면 클릭하십시오",
"Default video player has been turned OFF": "기본 비디오 플레이어가 꺼져 있습니다",
"Default video player has been turned ON": "기본 비디오 플레이어가 켜져 있습니다",
"ON": "켜짐",
"OFF": "꺼짐",
"Default Video Player": "기본 비디오 플레이어",
"Click to visit our website": "클릭하면 웹사이트를 방문합니다",
"Video is flipped horizontally": "비디오를 수평으로 뒤집습니다",
"flip-the-screen-is-pro-version-only": "화면 뒤집기는 Pro 버전에서만 사용할 수 있습니다",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "연결 정보를 보려면 클릭하십시오",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "클릭하면 비디오 스트림을 향상시킬 수 있습니다",
"Privacy Notice: Analytics in This App": "개인정보 안내: 이 앱의 분석",
"Analytics Reference": "분석 참조",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "이 앱은 Google Analytics(구글에서 제공하는 무료 서비스)를 사용하여 기본 사용 데이터를 익명으로 추적합니다. 이를 통해 사람들이 앱을 어떻게 사용하는지 이해하고 모두를 위해 개선할 수 있습니다.",
"What we collect:": "수집하는 정보:",
"Page views (which screens you visit)": "페이지 조회수(어떤 화면을 방문하는지)",
"Time spent on pages": "페이지에 머문 시간",
"Basic device info (browser type, screen size)": "기본 기기 정보(브라우저 종류, 화면 크기)",
"Your IP address (anonymized — last part removed for privacy)": "IP 주소(익명 처리됨 — 개인정보 보호를 위해 마지막 부분이 제거됩니다)",
"What we DON'T collect:": "수집하지 않는 정보:",
"Personal info (names, emails, passwords)": "개인 정보(이름, 이메일, 비밀번호)",
"Exact location": "정확한 위치",
"Any files or content you interact with": "사용자가 상호작용하는 파일이나 콘텐츠",
"Why anonymous?": "왜 익명으로 수집하나요?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "IP 주소는 자동으로 축약되며, 이 데이터로 개인을 식별할 수 없습니다.",
"Your options:": "선택 사항:",
"Continue:": "계속:",
"We'll track anonymized usage to help improve the app.": "앱을 개선하기 위해 익명화된 사용 데이터를 추적합니다.",
"Opt out:": "거부:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "아래의 동의하지 않음 버튼을 클릭하여 추적을 비활성화하세요. (이 선택을 존중하지만, 공동 피드백에 기반한 향후 개선 사항을 놓칠 수 있습니다.)",
"Data goes to: Google Analytics. See their privacy policy.": "데이터가 전송되는 곳: Google Analytics. 개인정보 보호정책을 확인하세요.",
"Accept": "동의",
"Disagree": "동의하지 않음",
"re-initiate-connection": "연결 재시작"
}
================================================
FILE: src/client-viewer/src/assets/locales/nl/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Wachtend op de gebruiker om de TOESTAAN knop in te drukken op het scherm-delen-apparaat...",
"Waiting for user to select source to share from screen sharing device...": "Wachtend op de gebruiker om de bron te selecteren om te delen vanuit het scherm-delen-apparaat...",
"My Device Info": "Mijn Apparaat Info",
"Device Type": "Apparaat Type",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Uw Apparaat IP zou identiek moeten zijn met het Apparaat IP in de verschenen alert pop-up op uw computer, waar Deskreen-CE actief is",
"Device IP": "Apparaat IP",
"Device Browser": "Apparaat Browser",
"Device OS": "Apparaat OS",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Deze details zouden identiek moeten zijn met diegene die u ziet in de alert pop-up op uw computer, waar Deskreen-CE actief is",
"Deskreen-CE Screen Viewer": "Deskreen-CE Scherm Viewer",
"Connected!": "Verbonden!",
"Error occurred": "Fout opgetreden",
"Deskreen-CE Error Dialog": "Deskreen-CE Error Dialoog",
"Something went wrong": "Er is iets misgegaan",
"You may close this browser window then try to connect again": "U mag dit browser venster sluiten en opnieuw proberen te verbinden",
"An unknown error occurred": "Een onbekende fout is opgetreden",
"You were not allowed to connect": "Uw verbinding werd niet toegestaan",
"You were disconnected": "Uw verbinding werd verbroken",
"WebRTC error occurred": "WebRTC fout opgetreden",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Als u Deskreen-CE waardeert, overweeg dan een financiële bijdrage. Deskreen-CE is open-source. Uw donaties houden ons gemotiveerd om Deskreen-CE te blijven verbeteren.",
"Donate": "Doneer",
"get-deskreen-pro": "Ontvang Deskreen Pro",
"get-deskreen-pro-tooltip": "Ontvang Deskreen Pro - opent de downloadpagina.",
"Video stream is paused": "Video stream is gepauzeerd",
"Video stream is playing": "Video stream wordt afgespeeld",
"Pause": "Pauze",
"Play": "Afspelen",
"Video Settings": "Video Instellingen",
"Flip": "Flip",
"Video quality has been changed to": "Video kwaliteit is aangepast naar",
"Click to Open Video Settings": "Klik om Video Instellingen te openen",
"Click to Enter Full Screen Mode": "Klik om Volledig Scherm modus te activeren",
"Default video player has been turned OFF": "Standaard video speler staat nu UIT",
"Default video player has been turned ON": "Standaard video speler staat nu AAN",
"ON": "AAN",
"OFF": "UIT",
"Default Video Player": "Standaard Video Speler",
"Click to visit our website": "Klik om onze website te bezoeken",
"Video is flipped horizontally": "Video is horizontaal geflipt",
"flip-the-screen-is-pro-version-only": "Scherm omdraaien is alleen beschikbaar in de Pro-versie",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klik om verbindings informatie te zien",
"Pair ID": "Koppel ID",
"Unpair": "Ontkoppelen",
"Session ID": "Sessie ID",
"Click to boost video stream if it is lagging": "Klik om de video stream te versterken als het traag is",
"Privacy Notice: Analytics in This App": "Privacyverklaring: Analyse in deze app",
"Analytics Reference": "Analytiek Referentie",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Deze app gebruikt Google Analytics (een gratis dienst van Google) om anoniem basisgebruikgegevens bij te houden. Zo begrijpen we hoe de app wordt gebruikt en kunnen we haar voor iedereen verbeteren.",
"What we collect:": "Wat we verzamelen:",
"Page views (which screens you visit)": "Paginaweergaven (welke schermen je bezoekt)",
"Time spent on pages": "Tijd doorgebracht op pagina's",
"Basic device info (browser type, screen size)": "Basisapparaatinformatie (browsertype, schermgrootte)",
"Your IP address (anonymized — last part removed for privacy)": "Je IP-adres (geanonimiseerd — het laatste deel wordt verwijderd voor je privacy)",
"What we DON'T collect:": "Wat we NIET verzamelen:",
"Personal info (names, emails, passwords)": "Persoonlijke info (namen, e-mails, wachtwoorden)",
"Exact location": "Exacte locatie",
"Any files or content you interact with": "Bestanden of inhoud waarmee je interacteert",
"Why anonymous?": "Waarom anoniem?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Je IP wordt automatisch ingekort, zodat niemand je persoonlijk kan identificeren met deze gegevens.",
"Your options:": "Je opties:",
"Continue:": "Doorgaan:",
"We'll track anonymized usage to help improve the app.": "We volgen geanonimiseerd gebruik om de app te verbeteren.",
"Opt out:": "Afmelden:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klik op de knop Weigeren hieronder om tracking uit te schakelen. (We respecteren die keuze, maar je kunt toekomstige verbeteringen missen die op collectieve feedback zijn gebaseerd.)",
"Data goes to: Google Analytics. See their privacy policy.": "Gegevens gaan naar: Google Analytics. Bekijk hun privacybeleid.",
"Accept": "Accepteren",
"Disagree": "Weigeren",
"re-initiate-connection": "Verbinding opnieuw starten"
}
================================================
FILE: src/client-viewer/src/assets/locales/ru/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Ждем когда пользователь нажмет кнопку РАЗРЕШИТЬ для доступа к экрану компьютера...",
"Waiting for user to select source to share from screen sharing device...": "Ждем когда пользователь выберет Весь экран или Окно приложения для отображения его здесь...",
"My Device Info": "Информация о моем устройстве",
"Device Type": "Тип устройства",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "IP-aдрес вашего устройства должен совпадать с «IP-адресом устройства» во всплывающем окне с предупреждением на компьютере, где работает Deskreen-CE.",
"Device IP": "IP-aдрес устройства",
"Device Browser": "Веб-браузер устройства",
"Device OS": "ОС устройства",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Эти данные должны совпадать с теми, которые вы видите во всплывающем окне предупреждения на экране компьютера, на котором работает Deskreen-CE.",
"Deskreen-CE Screen Viewer": "Просмотрщик экрана Deskreen-CE",
"Connected!": "Подключено!",
"Error occurred": "Произошла ошибка",
"Deskreen-CE Error Dialog": "Диалог ошибки Deskreen-CE",
"Something went wrong": "Произошло что-то не так",
"You may close this browser window then try to connect again": "Вы можете закрыть это окно браузера и попытаться подключиться снова",
"An unknown error occurred": "Произошла неизвестная ошибка",
"You were not allowed to connect": "Вам не разрешили подключиться",
"You were disconnected": "Вы были отключены",
"WebRTC error occurred": "Произошла ошибка WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Если вам нравится Deskreen-CE, подумайте о том, чтобы внести финансовый вклад. Deskreen-CE - это оупенсорсный проэкт. Ваши пожертвования позволяют нам делать Deskreen-CE еще лучше.",
"Donate": "Пожертвовать",
"get-deskreen-pro": "Получить Deskreen Pro",
"get-deskreen-pro-tooltip": "Получить Deskreen Pro - открывает страницу загрузки.",
"Video stream is paused": "Видеопоток приостановлен",
"Video stream is playing": "Видеопоток воспроизводится",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Настройки видео",
"Flip": "Отзеркалить",
"Video quality has been changed to": "Качество видео изменено на",
"Click to Open Video Settings": "Нажмите, чтобы открыть настройки видео",
"Click to Enter Full Screen Mode": "Нажмите, чтобы перейти в полноэкранный режим",
"Default video player has been turned OFF": "Видеоплеер по умолчанию отключен",
"Default video player has been turned ON": "Видеопроигрыватель по умолчанию включен",
"ON": "ВКЛ",
"OFF": "ВЫКЛ",
"Default Video Player": "Видеоплеер по умолчанию",
"Click to visit our website": "Нажмите, чтобы посетить наш сайт",
"Video is flipped horizontally": "Видео отзеркалено",
"flip-the-screen-is-pro-version-only": "Отзеркаливание экрана доступно только в Pro версии",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"Privacy Notice: Analytics in This App": "Уведомление о конфиденциальности: аналитика в этом приложении",
"Analytics Reference": "Справочник по аналитике",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Это приложение использует Google Analytics (бесплатный сервис Google), чтобы анонимно отслеживать базовые данные использования. Это помогает нам понимать, как люди пользуются приложением, чтобы улучшать его для всех.",
"What we collect:": "Что мы собираем:",
"Page views (which screens you visit)": "Просмотры страниц (какие экраны вы посещаете)",
"Time spent on pages": "Время, проведённое на страницах",
"Basic device info (browser type, screen size)": "Базовую информацию об устройстве (тип браузера, размер экрана)",
"Your IP address (anonymized — last part removed for privacy)": "Ваш IP-адрес (анонимизированный — последняя часть удалена для защиты приватности)",
"What we DON'T collect:": "Чего мы НЕ собираем:",
"Personal info (names, emails, passwords)": "Персональные данные (имена, электронные адреса, пароли)",
"Exact location": "Точное местоположение",
"Any files or content you interact with": "Любые файлы или контент, с которыми вы взаимодействуете",
"Why anonymous?": "Почему анонимно?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Ваш IP автоматически сокращается, и никто не сможет идентифицировать вас лично по этим данным.",
"Your options:": "Ваши варианты:",
"Continue:": "Продолжить:",
"We'll track anonymized usage to help improve the app.": "Мы будем отслеживать анонимизированное использование, чтобы улучшать приложение.",
"Opt out:": "Отказаться:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Нажмите кнопку Отклонить ниже, чтобы отключить отслеживание. (Мы уважим этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)",
"Data goes to: Google Analytics. See their privacy policy.": "Данные отправляются в: Google Analytics. Ознакомьтесь с их политикой конфиденциальности.",
"Accept": "Принять",
"Disagree": "Отклонить",
"re-initiate-connection": "Повторить подключение"
}
================================================
FILE: src/client-viewer/src/assets/locales/sv/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Väntar på att användaren ska klicka på 'TILLÅT' på skärmdelningsenheten...",
"Waiting for user to select source to share from screen sharing device...": "Väntar på att användaren ska välja källa att dela från skärmdelningsenhet...",
"My Device Info": "Min enhetsinformation",
"Device Type": "Enhetens typ",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "Din enhets IP-adress bör matcha med 'Enhetens IP' i den varnings-popup som dyker upp på din dator där Deskreen-CE körs",
"Device IP": "Enhetens IP",
"Device Browser": "Enhetens webbläsare",
"Device OS": "Enhetens operativsystem",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Dessa uppgifter ska matcha de som du ser i popup-fönstret på skärmdelningsenheten.",
"Deskreen-CE Screen Viewer": "Deskreen-CE skärmvisare",
"Connected!": "Ansluten!",
"Error occurred": "Ett fel inträffade",
"Deskreen-CE Error Dialog": "Deskreen-CE felhanterare",
"Something went wrong": "Något blev fel",
"You may close this browser window then try to connect again": "Stäng det här webbläsarfönstret och försök sedan ansluta igen",
"An unknown error occurred": "Ett okänt fel inträffade",
"You were not allowed to connect": "Du fick inte ansluta",
"You were disconnected": "Du blev nedkopplad",
"WebRTC error occurred": "Ett WebRTC-fel error inträffade",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Om du gillar Deskreen-CE, överväg i så fall att ge oss ett ekonomiskt bidrag. Deskreen-CE är open-source. Era donationer motiverar oss att göra Deskreen-CE ännu bättre.",
"Donate": "Donera",
"get-deskreen-pro": "Hämta Deskreen Pro",
"get-deskreen-pro-tooltip": "Hämta Deskreen Pro - öppnar nedladdningssidan.",
"Video stream is paused": "Videoströmmen är pausad",
"Video stream is playing": "Videoströmmen spelas",
"Pause": "Paus",
"Play": "Kör",
"Video Settings": "Videoinställningar",
"Flip": "Omvänd",
"Video quality has been changed to": "Videokvaliteten har ändrats till",
"Click to Open Video Settings": "Klicka här för att öppna videoinställningarna",
"Click to Enter Full Screen Mode": "Klicka här för att gå in i helskärmsläge",
"Default video player has been turned OFF": "Standardvideospelaren har stängts av",
"Default video player has been turned ON": "Standardvideospelaren har aktiverats",
"ON": "PÅ",
"OFF": "AV",
"Default Video Player": "Standardvideospelare",
"Click to visit our website": "Klicka här för att besöka vår webbplats",
"Video is flipped horizontally": "Videon är vänd horisontellt",
"flip-the-screen-is-pro-version-only": "Vända skärmen är endast tillgängligt i Pro-versionen",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Klicka här för att visa anslutningsinformationen",
"Pair ID": "ID för sammankopplingen",
"Unpair": "Ta bort sammankopplingen",
"Session ID": "ID för sessionen",
"Click to boost video stream if it is lagging": "Klicka för att öka videoströmmen om den släpar efter",
"Privacy Notice: Analytics in This App": "Integritetsmeddelande: Analys i den här appen",
"Analytics Reference": "Analysreferens",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Den här appen använder Google Analytics (en kostnadsfri tjänst från Google) för att anonymt spåra grundläggande användningsdata. Det hjälper oss att förstå hur appen används så att vi kan förbättra den för alla.",
"What we collect:": "Det vi samlar in:",
"Page views (which screens you visit)": "Sidvisningar (vilka vyer du besöker)",
"Time spent on pages": "Tid som spenderas på sidor",
"Basic device info (browser type, screen size)": "Grundläggande enhetsinformation (webbläsartyp, skärmstorlek)",
"Your IP address (anonymized — last part removed for privacy)": "Din IP-adress (anonymiserad — den sista delen tas bort av integritetsskäl)",
"What we DON'T collect:": "Det vi INTE samlar in:",
"Personal info (names, emails, passwords)": "Personlig information (namn, e-postadresser, lösenord)",
"Exact location": "Exakt plats",
"Any files or content you interact with": "Filer eller innehåll du interagerar med",
"Why anonymous?": "Varför anonymt?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Din IP-adress förkortas automatiskt, och ingen kan identifiera dig personligen utifrån dessa data.",
"Your options:": "Dina alternativ:",
"Continue:": "Fortsätt:",
"We'll track anonymized usage to help improve the app.": "Vi spårar anonymiserad användning för att hjälpa oss förbättra appen.",
"Opt out:": "Avslå:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Klicka på knappen Avböj nedan för att inaktivera spårning. (Vi respekterar detta val, men du kan gå miste om framtida förbättringar som bygger på samlad feedback.)",
"Data goes to: Google Analytics. See their privacy policy.": "Data skickas till: Google Analytics. Se deras integritetspolicy.",
"Accept": "Acceptera",
"Disagree": "Avböj",
"re-initiate-connection": "Återanslut"
}
================================================
FILE: src/client-viewer/src/assets/locales/ua/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "Чекаємо коли користувач натисне кнопку ДОЗВОЛИТИ для доступу до екрану комп'ютера...",
"Waiting for user to select source to share from screen sharing device...": "Чекаємо коли користувач вибере Весь екран або Вікно додатка для відображення його тут...",
"My Device Info": "Інформація про мій пристрій",
"Device Type": "Тип пристрою",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "IP-aдрес пристрою вашого пристрою має збігатися з «IP-адресою пристрою» у спливаючому вікні сповіщення, що з’явилося на комп’ютері, де працює Deskreen-CE.",
"Device IP": "IP-aдрес пристрою",
"Device Browser": "Веб-браузер пристрою",
"Device OS": "ОС пристрою",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "Ці деталі повинні збігатися з тими, які ви бачите у спливаючому вікні сповіщень на екрані комп’ютера, де запущений Deskreen-CE.",
"Deskreen-CE Screen Viewer": "Переглядач екрану Deskreen-CE",
"Connected!": "Підключено!",
"Error occurred": "Виникла помилка",
"Deskreen-CE Error Dialog": "Діалог помилки Deskreen-CE",
"Something went wrong": "Щось не так сталося",
"You may close this browser window then try to connect again": "Ви можете закрити це вікно браузера та спробувати підключитися знову",
"An unknown error occurred": "Виникла невідома помилка",
"You were not allowed to connect": "Вам не дозволили підключитися",
"You were disconnected": "Ви були відключені",
"WebRTC error occurred": "Сталася помилка WebRTC",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "Якщо вам подобається Deskreen-CE, подумайте про те, щоб внести фінансовий внесок. Deskreen-CE - це оупенсорсний проект. Ваші пожертвування дозволяють нам робити Deskreen-CE ще краще.",
"Donate": "Пожертвувати",
"get-deskreen-pro": "Отримати Deskreen Pro",
"get-deskreen-pro-tooltip": "Отримати Deskreen Pro - відкриває сторінку завантаження.",
"Video stream is paused": "Відеопотік призупинено",
"Video stream is playing": "Відеопотік продовжується",
"Pause": "Pause",
"Play": "Play",
"Video Settings": "Настройки видео",
"Flip": "Віддзеркалити",
"Video quality has been changed to": "Якість відео змінено на",
"Click to Open Video Settings": "Натисніть, щоб відкрити настройки відео",
"Click to Enter Full Screen Mode": "Натисніть для входу в повноекранноий режим",
"Default video player has been turned OFF": "Стандартний відеоплеєр браузера вимкнено",
"Default video player has been turned ON": "Стандартний відеоплеєр браузера включений",
"ON": "ВКЛ",
"OFF": "ВИМК",
"Default Video Player": "Стандартний відеоплеєр браузера",
"Click to visit our website": "Клацніть, щоб відвідати наш веб-сайт",
"Video is flipped horizontally": "Відео віддзеркалено",
"flip-the-screen-is-pro-version-only": "Віддзеркалення екрану доступне лише в Pro версії",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "",
"Click to see connection info": "Click to see connection info",
"Pair ID": "Pair ID",
"Unpair": "Unpair",
"Session ID": "Session ID",
"Click to boost video stream if it is lagging": "Click to boost video stream if it is lagging",
"Privacy Notice: Analytics in This App": "Повідомлення про конфіденційність: аналітика в цьому застосунку",
"Analytics Reference": "Довідник з аналітики",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "Цей застосунок використовує Google Analytics (безкоштовний сервіс Google), щоб анонімно відстежувати базові дані використання. Це допомагає нам розуміти, як люди користуються застосунком, аби покращувати його для всіх.",
"What we collect:": "Що ми збираємо:",
"Page views (which screens you visit)": "Перегляди сторінок (які екрани ви відвідуєте)",
"Time spent on pages": "Час, проведений на сторінках",
"Basic device info (browser type, screen size)": "Базову інформацію про пристрій (тип браузера, розмір екрана)",
"Your IP address (anonymized — last part removed for privacy)": "Вашу IP-адресу (анонімізовану — остання частина вилучається для приватності)",
"What we DON'T collect:": "Що ми НЕ збираємо:",
"Personal info (names, emails, passwords)": "Особисту інформацію (імена, електронні адреси, паролі)",
"Exact location": "Точне місцезнаходження",
"Any files or content you interact with": "Будь-які файли чи вміст, з якими ви взаємодієте",
"Why anonymous?": "Чому анонімно?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "Вашу IP автоматично скорочують, тому ніхто не може ідентифікувати вас особисто за цими даними.",
"Your options:": "Ваші варіанти:",
"Continue:": "Продовжити:",
"We'll track anonymized usage to help improve the app.": "Ми відстежуватимемо анонімізоване використання, щоб допомогти покращити застосунок.",
"Opt out:": "Відмовитися:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "Натисніть кнопку Відхилити нижче, щоб вимкнути відстеження. (Ми поважаємо цей вибір, але ви можете пропустити майбутні покращення, що базуються на спільному зворотному зв'язку.)",
"Data goes to: Google Analytics. See their privacy policy.": "Дані надсилаються до: Google Analytics. Перегляньте їхню політику конфіденційності.",
"Accept": "Погодитися",
"Disagree": "Відхилити",
"re-initiate-connection": "Повторно підключитися"
}
================================================
FILE: src/client-viewer/src/assets/locales/zh_CN/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "正在等待用户单击屏幕共享设备上的允许按钮...",
"Waiting for user to select source to share from screen sharing device...": "正在等待用户从屏幕共享设备选择要共享的源...",
"My Device Info": "我的设备信息",
"Device Type": "设备类型",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "您的设备 IP 应该与运行 Deskreen-CE 的计算机上出现的警报弹出窗口中的 '设备 IP' 相匹配。",
"Device IP": "设备 IP",
"Device Browser": "设备浏览器",
"Device OS": "设备操作系统",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "这些详细信息应与您在屏幕共享设备上的警报弹出窗口中看到的信息相匹配。",
"Deskreen-CE Screen Viewer": "Deskreen-CE 屏幕查看器",
"Connected!": "已连接!",
"Error occurred": "出现错误",
"Deskreen-CE Error Dialog": "Deskreen-CE 错误对话框",
"Something went wrong": "出问题了",
"You may close this browser window then try to connect again": "您可以关闭此浏览器窗口,然后尝试重新连接",
"An unknown error occurred": "出现未知错误",
"You were not allowed to connect": "您不能连接",
"You were disconnected": "您将断开连接",
"WebRTC error occurred": "出现 WebRTC 错误",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "如果你喜欢 Deskreen-CE,可以考虑出钱。Deskreen-CE 是开源的。您的捐赠使我们有动力让 Deskreen-CE 变得更好。",
"Donate": "捐赠",
"get-deskreen-pro": "获取 Deskreen Pro",
"get-deskreen-pro-tooltip": "获取 Deskreen Pro - 打开下载页面。",
"Video stream is paused": "视频流暂停",
"Video stream is playing": "正在播放视频流",
"Pause": "暂停",
"Play": "播放",
"Video Settings": "视频设置",
"Flip": "翻转",
"Video quality has been changed to": "视频质量已更改为",
"Click to Open Video Settings": "单击以打开视频设置",
"Click to Enter Full Screen Mode": "单击以进入全屏模式",
"Default video player has been turned OFF": "默认视频播放器已关闭",
"Default video player has been turned ON": "默认视频播放器已开启",
"ON": "开启",
"OFF": "关闭",
"Default Video Player": "默认视频播放器",
"Click to visit our website": "点击访问我们的网站",
"Video is flipped horizontally": "视频水平翻转",
"flip-the-screen-is-pro-version-only": "翻转屏幕仅在 Pro 版本中可用",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻译尚未添加到 UI,但欢迎您的翻译!功能将很快添加,因此需要您的翻译",
"Click to see connection info": "单击以查看连接信息",
"Pair ID": "配对 ID",
"Unpair": "取消配对",
"Session ID": "会话 ID",
"Click to boost video stream if it is lagging": "如果视频流滞后,请单击以提高视频流",
"Privacy Notice: Analytics in This App": "隐私提示:本应用中的分析",
"Analytics Reference": "分析参考",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "本应用使用 Google Analytics(Google 提供的免费服务)匿名跟踪基本的使用数据。这有助于我们了解用户如何使用应用,从而为所有人改进。",
"What we collect:": "我们收集:",
"Page views (which screens you visit)": "页面浏览量(你访问的界面)",
"Time spent on pages": "在页面停留的时间",
"Basic device info (browser type, screen size)": "设备基本信息(浏览器类型、屏幕尺寸)",
"Your IP address (anonymized — last part removed for privacy)": "你的 IP 地址(已匿名化——出于隐私保护会移除最后一部分)",
"What we DON'T collect:": "我们不会收集:",
"Personal info (names, emails, passwords)": "个人信息(姓名、邮箱、密码)",
"Exact location": "精确位置",
"Any files or content you interact with": "你接触的任何文件或内容",
"Why anonymous?": "为什么是匿名的?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "你的 IP 会自动被截短,任何人都无法通过这些数据识别你的身份。",
"Your options:": "你的选择:",
"Continue:": "继续:",
"We'll track anonymized usage to help improve the app.": "我们会跟踪匿名化的使用情况,以帮助改进应用。",
"Opt out:": "拒绝:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "点击下面的\"拒绝\"按钮以关闭跟踪。(我们会尊重这个选择,但你可能会错过基于集体反馈的未来改进。)",
"Data goes to: Google Analytics. See their privacy policy.": "数据将发送至:Google Analytics。查看其隐私政策。",
"Accept": "接受",
"Disagree": "拒绝",
"re-initiate-connection": "重新连接"
}
================================================
FILE: src/client-viewer/src/assets/locales/zh_TW/translation.json
================================================
{
"Waiting for user to click ALLOW button on screen sharing device...": "正在等待使用者單擊螢幕共享裝置上的允許按鈕...",
"Waiting for user to select source to share from screen sharing device...": "正在等待使用者從螢幕共享裝置選擇要共享的源...",
"My Device Info": "我的裝置資訊",
"Device Type": "裝置型別",
"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running": "您的裝置 IP 應該與執行 Deskreen-CE 的計算機上出現的警報彈出視窗中的 '裝置 IP' 相匹配。",
"Device IP": "裝置 IP",
"Device Browser": "裝置瀏覽器",
"Device OS": "裝置作業系統",
"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running": "這些詳細資訊應與您在螢幕共享裝置上的警報彈出視窗中看到的資訊相匹配。",
"Deskreen-CE Screen Viewer": "Deskreen-CE 螢幕檢視器",
"Connected!": "已連線!",
"Error occurred": "出現錯誤",
"Deskreen-CE Error Dialog": "Deskreen-CE 錯誤對話方塊",
"Something went wrong": "出問題了",
"You may close this browser window then try to connect again": "您可以關閉此瀏覽器視窗,然後嘗試重新連線",
"An unknown error occurred": "出現未知錯誤",
"You were not allowed to connect": "您不能連線",
"You were disconnected": "您將斷開連線",
"WebRTC error occurred": "出現 WebRTC 錯誤",
"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better": "如果你喜歡 Deskreen-CE,可以考慮出錢。Deskreen-CE 是開源的。您的捐贈使我們有動力讓 Deskreen-CE 變得更好。",
"Donate": "捐贈",
"get-deskreen-pro": "取得 Deskreen Pro",
"get-deskreen-pro-tooltip": "取得 Deskreen Pro - 開啟下載頁面。",
"Video stream is paused": "影片流暫停",
"Video stream is playing": "正在播放影片流",
"Pause": "暫停",
"Play": "播放",
"Video Settings": "影片設定",
"Flip": "翻轉",
"Video quality has been changed to": "影片質量已更改為",
"Click to Open Video Settings": "單擊以開啟影片設定",
"Click to Enter Full Screen Mode": "單擊以進入全屏模式",
"Default video player has been turned OFF": "預設影片播放器已關閉",
"Default video player has been turned ON": "預設影片播放器已開啟",
"ON": "開啟",
"OFF": "關閉",
"Default Video Player": "預設影片播放器",
"Click to visit our website": "點選訪問我們的網站",
"Video is flipped horizontally": "影片水平翻轉",
"flip-the-screen-is-pro-version-only": "翻轉螢幕僅在 Pro 版本中可用",
"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻譯尚未新增到 UI,但歡迎您的翻譯!功能將很快新增,因此需要您的翻譯",
"Click to see connection info": "單擊以檢視連線資訊",
"Pair ID": "配對 ID",
"Unpair": "取消配對",
"Session ID": "會話 ID",
"Click to boost video stream if it is lagging": "如果影片流滯後,請單擊以提高影片流",
"Privacy Notice: Analytics in This App": "隱私提示:此應用程式的分析",
"Analytics Reference": "分析參考",
"This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.": "此應用程式使用 Google Analytics(Google 提供的免費服務)以匿名方式追蹤基本使用資料。這有助於我們了解使用者如何使用應用程式,從而為所有人帶來改進。",
"What we collect:": "我們收集:",
"Page views (which screens you visit)": "頁面瀏覽量(你造訪的畫面)",
"Time spent on pages": "在頁面停留的時間",
"Basic device info (browser type, screen size)": "裝置基本資訊(瀏覽器類型、螢幕尺寸)",
"Your IP address (anonymized — last part removed for privacy)": "你的 IP 位址(已匿名化 — 為保護隱私會移除最後一段)",
"What we DON'T collect:": "我們不會收集:",
"Personal info (names, emails, passwords)": "個人資訊(姓名、電子郵件、密碼)",
"Exact location": "精確位置",
"Any files or content you interact with": "你互動的任何檔案或內容",
"Why anonymous?": "為什麼要匿名?",
"Your IP is automatically shortened, and no one can identify you personally from this data.": "你的 IP 會自動被截短,沒有人能根據這些資料識別你的身份。",
"Your options:": "你的選項:",
"Continue:": "繼續:",
"We'll track anonymized usage to help improve the app.": "我們會追蹤匿名化的使用情況,協助改進應用程式。",
"Opt out:": "拒絕:",
"Click the Disagree button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)": "點擊下方的「不同意」按鈕以停用追蹤。(我們會尊重此選擇,但你可能會錯過基於集體回饋的未來改善。)",
"Data goes to: Google Analytics. See their privacy policy.": "資料傳送至:Google Analytics。查看其隱私權政策。",
"Accept": "接受",
"Disagree": "不同意",
"re-initiate-connection": "重新連線"
}
================================================
FILE: src/client-viewer/src/assets/manifest.json
================================================
{
"short_name": "Deskreen CE",
"name": "Deskreen CE Makes Any Device a Second Screen For Your Computer",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: src/client-viewer/src/assets/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: src/client-viewer/src/components/ConnectingIndicator/ConnectingIndicatorIcon.tsx
================================================
import { Icon } from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
interface ConnectingIndicatorIconProps {
connectionIconType: 'feed' | 'feed-subscribed';
}
function ConnectingIndicatorIcon(props: ConnectingIndicatorIconProps) {
const { connectionIconType } = props;
return (
);
}
export default ConnectingIndicatorIcon;
================================================
FILE: src/client-viewer/src/components/ConnectingIndicator/LoadingSharingIcon.tsx
================================================
import { Icon } from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import PropagateLoader from 'react-spinners/PropagateLoader';
interface SelectSharingIconProps {
loadingSharingIconType: LoadingSharingIconType;
isShownLoadingSharingIcon: boolean;
}
function LoadingSharingIcon(props: SelectSharingIconProps) {
const {
loadingSharingIconType: selectingSharingIconType,
isShownLoadingSharingIcon: isShownSelectingSharingIcon,
} = props;
return (
{isShownSelectingSharingIcon && (
)}
);
}
export default LoadingSharingIcon;
================================================
FILE: src/client-viewer/src/components/ConnectingIndicator/index.tsx
================================================
import React from 'react';
import { Text } from '@blueprintjs/core';
import { Row } from 'react-flexbox-grid';
import ConnectingIndicatorIcon from './ConnectingIndicatorIcon';
import LoadingSharingIcon from './LoadingSharingIcon';
const basePulsingCircleStyles = {
borderRadius: '100%',
marginLeft: 'auto',
marginRight: 'auto',
left: '0',
right: '0',
textAlign: 'center',
position: 'absolute',
width: '100px',
height: '100px',
};
function getConnectingStepContent(
currentStep: number,
connectionIconType: ConnectionIconType,
loadingSharingIconType: LoadingSharingIconType,
isShownLoadingSharingIcon: boolean,
) {
const pulsingCircle1Styles = {
...basePulsingCircleStyles,
zIndex: 1,
backgroundColor: 'rgba(43, 149, 214, 0.7)',
} as React.CSSProperties;
const pulsingCircle2Styles = {
...basePulsingCircleStyles,
zIndex: 2,
backgroundColor: '#2B95D6',
} as React.CSSProperties;
const pulsingCircle3Styles = {
...basePulsingCircleStyles,
backgroundColor: '#15B371',
} as React.CSSProperties;
switch (currentStep) {
case 1:
return (
);
case 2:
return (
);
case 3:
return (
);
default:
return Error occurred :( ;
}
}
interface ConnectingIndicatorProps {
currentStep: number;
connectionIconType: ConnectionIconType;
isShownSelectingSharingIcon: boolean;
selectingSharingIconType: LoadingSharingIconType;
}
function ConnectingIndicator(props: ConnectingIndicatorProps) {
const {
currentStep,
connectionIconType,
isShownSelectingSharingIcon,
selectingSharingIconType,
} = props;
return (
<>
{getConnectingStepContent(
currentStep,
connectionIconType,
selectingSharingIconType,
isShownSelectingSharingIcon,
)}
>
);
}
export default ConnectingIndicator;
================================================
FILE: src/client-viewer/src/components/ErrorDialog/ErrorMessageEnum.ts
================================================
export const ErrorMessage = {
UNKNOWN_ERROR: 'An unknown error occurred',
DENY_TO_CONNECT: 'You were not allowed to connect',
DISCONNECTED: 'You were disconnected',
NOT_ALLOWED: 'You were not allowed to connect',
WEBRTC_ERROR: 'WebRTC error occurred',
} as const;
export type ErrorMessageType = (typeof ErrorMessage)[keyof typeof ErrorMessage];
================================================
FILE: src/client-viewer/src/components/ErrorDialog/index.css
================================================
.error-dialog-backdrop {
backdrop-filter: blur(2px);
}
================================================
FILE: src/client-viewer/src/components/ErrorDialog/index.tsx
================================================
import { Classes, Dialog, Divider, H1, H2, H3, Icon } from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import { useTranslation } from 'react-i18next';
import './index.css';
import { type ErrorMessageType } from './ErrorMessageEnum';
interface ErrorDialogProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
errorMessage: ErrorMessageType;
}
function ErrorDialog(props: ErrorDialogProps) {
const { t } = useTranslation();
const { errorMessage, isOpen, setIsOpen } = props;
return (
{
setIsOpen(false);
}}
>
{t('Deskreen CE Error Dialog')}
{`${t('Something went wrong')} :(`}
{t(`${errorMessage}`)}
{`${t('You may close this browser window then try to connect again')}.`}
);
}
export default ErrorDialog;
================================================
FILE: src/client-viewer/src/components/LoadingScreen/LoadingScreen.css
================================================
.loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
z-index: 9999;
}
.css-spinner {
width: 40px;
height: 40px;
border: 4px solid #e1e8ed;
border-top-color: #5c7080;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
================================================
FILE: src/client-viewer/src/components/LoadingScreen/index.tsx
================================================
import React from 'react';
import './LoadingScreen.css';
const LoadingScreen: React.FC = () => {
return (
);
};
export default LoadingScreen;
================================================
FILE: src/client-viewer/src/components/MyDeviceInfoCard/DeviceDetails.d.ts
================================================
interface DeviceDetails {
myIP: string;
myOS: string;
myDeviceType: string;
myBrowser: string;
myRoomId: string;
}
================================================
FILE: src/client-viewer/src/components/MyDeviceInfoCard/index.tsx
================================================
import { Callout, Card, H3, Text, Tooltip, Position } from '@blueprintjs/core';
import { useTranslation } from 'react-i18next';
import { LIGHT_UI_BACKGROUND } from '../../constants/styleConstants';
interface MyDeviceDetailsCardProps {
deviceDetails: DeviceDetails;
}
function MyDeviceInfoCard(props: MyDeviceDetailsCardProps) {
const { t } = useTranslation();
const { deviceDetails } = props;
const { myIP, myOS, myDeviceType, myBrowser, myRoomId } = deviceDetails;
return (
{`${t('My Device Info')}:`}
{`${t('Device Type')}: ${myDeviceType}`}
{`${t('Device IP')}: ${myIP}`}
{`${t('Device Browser')}: ${myBrowser}`}
{`${t('Device OS')}: ${myOS}`}
{`${t('My Current Connection ID')}: ${myRoomId}`}
{t(
'These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running',
)}
);
}
export default MyDeviceInfoCard;
================================================
FILE: src/client-viewer/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts
================================================
import { togglePlayerFullscreen } from '../../utils/playerFullscreen';
export const handlePlayerToggleFullscreen = () => {
return togglePlayerFullscreen();
};
================================================
FILE: src/client-viewer/src/components/PlayerControlPanel/index.css
================================================
.play-pause-text {
width: max-content;
}
.play-pause-button,
.play-pause-button:focus,
.play-pause-button:active {
border: none !important;
outline: none !important;
overflow: hidden !important;
}
.play-pause-button:hover {
border: none !important;
outline: none !important;
background: rgba(255, 255, 255, 0.1) !important;
overflow: hidden !important;
}
.play-pause-button::before,
.play-pause-button::after {
display: none !important;
}
.play-pause-button {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: center !important;
width: 120px !important;
min-width: 120px !important;
max-width: 120px !important;
}
.play-pause-button-glow {
box-shadow: 0 0 20px rgba(19, 124, 189, 0.8), 0 0 40px rgba(19, 124, 189, 0.6) !important;
}
.play-pause-button .bp3-button-text {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: center !important;
border: none !important;
outline: none !important;
margin: 0 !important;
}
.bp3-button-group {
border: none !important;
overflow: visible !important;
}
.bp3-button-group .bp3-button {
position: relative !important;
}
.settings-button-separator {
border: none !important;
position: relative !important;
}
.settings-button-separator::before {
content: '' !important;
position: absolute !important;
left: 0 !important;
top: 25% !important;
bottom: 25% !important;
width: 1px !important;
background-color: rgba(255, 255, 255, 0.5) !important;
z-index: 10 !important;
pointer-events: none !important;
}
.settings-button-separator::after {
content: '' !important;
position: absolute !important;
right: 0 !important;
top: 25% !important;
bottom: 25% !important;
width: 1px !important;
background-color: rgba(255, 255, 255, 0.5) !important;
z-index: 10 !important;
pointer-events: none !important;
}
.bp3-button-group .bp3-button:not(.play-pause-button) {
box-shadow: none !important;
}
.bp3-button-group .bp3-button:not(.play-pause-button):hover {
box-shadow: none !important;
}
.bp3-button-group .bp3-button:first-child {
border-left: none !important;
border-top-left-radius: 20px !important;
border-bottom-left-radius: 20px !important;
padding-left: 20px !important;
}
.bp3-button-group .bp3-button:last-child {
border-top-right-radius: 20px !important;
border-bottom-right-radius: 20px !important;
padding-right: 20px !important;
}
.bp3-button-group .bp3-button:hover,
.bp3-button-group .bp3-button:focus,
.bp3-button-group .bp3-button:active {
overflow: hidden !important;
}
.bp3-button-group .bp3-button:first-child:hover,
.bp3-button-group .bp3-button:first-child:focus,
.bp3-button-group .bp3-button:first-child:active {
border-top-left-radius: 20px !important;
border-bottom-left-radius: 20px !important;
}
.bp3-button-group .bp3-button:last-child:hover,
.bp3-button-group .bp3-button:last-child:focus,
.bp3-button-group .bp3-button:last-child:active {
border-top-right-radius: 20px !important;
border-bottom-right-radius: 20px !important;
}
================================================
FILE: src/client-viewer/src/components/PlayerControlPanel/index.tsx
================================================
import React, { useEffect, useState, useCallback } from 'react';
import {
Alignment,
Button,
ButtonGroup,
Card,
H5,
Switch,
Divider,
Text,
Icon,
Tooltip,
Position,
Popover,
Classes,
H3,
} from '@blueprintjs/core';
import screenfull from 'screenfull';
import { useTranslation } from 'react-i18next';
import FullScreenEnter from '../../images/fullscreen_24px.svg';
import FullScreenExit from '../../images/fullscreen_exit-24px.svg';
import { Col, Row } from 'react-flexbox-grid';
import {
VideoQuality,
type VideoQualityType,
} from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
import { handlePlayerToggleFullscreen } from './handlePlayerToggleFullscreen';
import initScreenfullOnChange from './initScreenfullOnChange';
import { ScreenSharingSource } from '../../features/PeerConnection/ScreenSharingSourceEnum';
import {
trackAnalyticsEvent,
setConsentStatus,
updateAnalyticsConsent,
} from '../../utils/analytics';
import PrivacyControlDialog from '../PrivacyControlDialog';
import './index.css';
const videoQualityButtonStyle: React.CSSProperties = {
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
};
interface PlayerControlPanelProps {
onSwitchChangedCallback: (isEnabled: boolean) => void;
isPlaying: boolean;
isDefaultPlayerTurnedOn: boolean;
handleClickFullscreen: () => 'entered' | 'exited' | 'failed';
handleClickPlayPause: () => void;
setVideoQuality: (q: VideoQualityType) => void;
selectedVideoQuality: VideoQualityType;
screenSharingSourceType: ScreenSharingSourceType;
// toaster: undefined | HTMLDivElement;
}
function PlayerControlPanel(props: PlayerControlPanelProps) {
const { t } = useTranslation();
const {
onSwitchChangedCallback,
isPlaying,
isDefaultPlayerTurnedOn,
handleClickPlayPause,
handleClickFullscreen,
selectedVideoQuality,
setVideoQuality,
screenSharingSourceType,
} = props;
const isFullScreenAPIAvailable = screenfull.isEnabled;
const [isFullScreenOn, setIsFullScreenOn] = useState(false);
const [isPrivacyDialogOpen, setIsPrivacyDialogOpen] = useState(false);
useEffect(() => {
const cleanup = initScreenfullOnChange(setIsFullScreenOn);
return cleanup;
}, []);
const handleClickFullscreenWhenDefaultPlayerIsOn = useCallback(() => {
const result = handlePlayerToggleFullscreen();
if (result === 'failed') {
console.warn('Unable to toggle fullscreen');
return result;
}
setIsFullScreenOn(result === 'entered');
return result;
}, [setIsFullScreenOn]);
const handleLogoClick = useCallback(() => {
trackAnalyticsEvent('logo_clicked', {
destination: 'https://deskreen.com',
});
window.open('https://deskreen.com', '_blank');
}, []);
const handleContributeClick = useCallback(() => {
trackAnalyticsEvent('contribute_clicked', {
destination: 'https://deskreen.com/download',
});
window.open('https://deskreen.com/download', '_blank');
}, []);
const handlePlayPauseClick = useCallback(() => {
const nextAction = isPlaying ? 'pause' : 'play';
trackAnalyticsEvent(
nextAction === 'play' ? 'play_button_clicked' : 'pause_button_clicked',
{
target_state: nextAction === 'play' ? 'playing' : 'paused',
},
);
handleClickPlayPause();
}, [handleClickPlayPause, isPlaying]);
const handleVideoQualitySelect = useCallback(
(quality: VideoQualityType) => {
if (selectedVideoQuality !== quality) {
trackAnalyticsEvent('video_quality_selected', {
quality,
});
}
setVideoQuality(quality);
},
[selectedVideoQuality, setVideoQuality],
);
const handleDefaultPlayerToggle = useCallback(() => {
const nextState = !isDefaultPlayerTurnedOn;
trackAnalyticsEvent('default_player_toggled', {
state: nextState ? 'on' : 'off',
});
onSwitchChangedCallback(nextState);
}, [isDefaultPlayerTurnedOn, onSwitchChangedCallback]);
const handleFullscreenClick = useCallback(() => {
const result = isDefaultPlayerTurnedOn
? handleClickFullscreenWhenDefaultPlayerIsOn()
: handleClickFullscreen();
if (result === 'failed') {
trackAnalyticsEvent('fullscreen_toggle_failed', {
player_mode: isDefaultPlayerTurnedOn ? 'default' : 'custom',
});
return;
}
trackAnalyticsEvent('fullscreen_toggled', {
state: result === 'entered' ? 'on' : 'off',
player_mode: isDefaultPlayerTurnedOn ? 'default' : 'custom',
});
}, [
handleClickFullscreen,
handleClickFullscreenWhenDefaultPlayerIsOn,
isDefaultPlayerTurnedOn,
]);
const handlePrivacyControlClick = useCallback(() => {
setIsPrivacyDialogOpen(true);
}, []);
const handlePrivacyDialogClose = useCallback(() => {
setIsPrivacyDialogOpen(false);
}, []);
const handlePrivacyAccept = useCallback(() => {
setConsentStatus('accepted');
updateAnalyticsConsent('accepted');
setIsPrivacyDialogOpen(false);
}, []);
const handlePrivacyOptOut = useCallback(() => {
setConsentStatus('opted-out');
updateAnalyticsConsent('opted-out');
setIsPrivacyDialogOpen(false);
}, []);
return (
<>
Deskreen CE Viewer
{
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow =
'0 6px 16px rgba(102, 51, 204, 0.5), 0 3px 6px rgba(102, 51, 204, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow =
'0 4px 12px rgba(102, 51, 204, 0.4), 0 2px 4px rgba(102, 51, 204, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)';
}}
>
{t('get-deskreen-pro')}
{isPlaying ? t('Pause') : t('Play')}
{`${t('Video Settings')}:`}
{t('Flip')}
{Object.values(VideoQuality).map(
(q: VideoQualityType) => {
return (
{
handleVideoQualitySelect(q);
// toaster?.show({
// icon: 'clean',
// intent: Intent.PRIMARY,
// message: `${t(
// 'Video quality has been changed to'
// )} ${q}`,
// });
}}
>
{q}
);
},
)}
>
}
position={Position.BOTTOM}
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
>
{t('Privacy Settings')}
>
);
}
export default PlayerControlPanel;
================================================
FILE: src/client-viewer/src/components/PlayerControlPanel/initScreenfullOnChange.ts
================================================
import { subscribeToPlayerFullscreenChange } from '../../utils/playerFullscreen';
export default (setIsFullScreenOn: (_: boolean) => void) => {
const unsubscribe = subscribeToPlayerFullscreenChange(setIsFullScreenOn);
return () => {
unsubscribe();
};
};
================================================
FILE: src/client-viewer/src/components/PrivacyConsentDialog/index.css
================================================
.privacy-consent-dialog-backdrop {
backdrop-filter: blur(2px);
}
.privacy-consent-dialog {
display: flex;
flex-direction: column;
}
.privacy-consent-buttons-container {
flex-shrink: 0;
background: white;
border-top: 1px solid rgba(16, 22, 26, 0.15);
}
.privacy-consent-buttons-container .row {
display: flex;
}
.bp4-dark .privacy-consent-buttons-container {
background: #30404d;
border-top-color: rgba(255, 255, 255, 0.2);
}
.privacy-consent-dialog a {
color: #137cbd;
text-decoration: none;
}
.privacy-consent-dialog a:hover {
text-decoration: underline;
color: #106ba3;
}
.allow-analytics-button {
box-shadow:
0 4px 16px rgba(15, 153, 96, 0.5),
0 0 20px rgba(15, 153, 96, 0.3) !important;
transition: all 0.2s ease-in-out !important;
background: linear-gradient(135deg, #0f9960 0%, #0a7a4a 100%) !important;
border: none !important;
color: white !important;
}
.allow-analytics-button:hover {
box-shadow:
0 6px 24px rgba(15, 153, 96, 0.6),
0 0 30px rgba(15, 153, 96, 0.4) !important;
transform: translateY(-2px);
background: linear-gradient(135deg, #15b371 0%, #0f9960 100%) !important;
}
.allow-analytics-button:active {
transform: translateY(0);
box-shadow: 0 2px 12px rgba(15, 153, 96, 0.5) !important;
}
.disagree-analytics-button {
transition: all 0.2s ease-in-out;
color: #5c7080 !important;
background: none !important;
border: none !important;
padding: 8px 12px !important;
min-height: auto !important;
text-decoration: none !important;
}
.disagree-analytics-button:hover {
color: #394b59 !important;
background: none !important;
text-decoration: underline !important;
}
.bp4-dark .disagree-analytics-button {
color: #8a9ba8 !important;
}
.bp4-dark .disagree-analytics-button:hover {
color: #bfccd6 !important;
background: none !important;
}
/* reorder buttons on mobile: disagree first, allow second */
@media (max-width: 575px) {
.disagree-button-col {
order: 1;
}
.allow-button-col {
order: 2;
}
}
================================================
FILE: src/client-viewer/src/components/PrivacyConsentDialog/index.tsx
================================================
import { useEffect, useState } from 'react';
import {
Button,
Classes,
Dialog,
Divider,
H2,
H3,
HTMLSelect,
Icon,
} from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import { useTranslation } from 'react-i18next';
import i18nInstance from '../../config/i18n';
import './index.css';
interface PrivacyConsentDialogProps {
isOpen: boolean;
onAccept: () => void;
onOptOut: () => void;
}
const AVAILABLE_LANGUAGES = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'de', name: 'Deutsch' },
{ code: 'fr', name: 'Français' },
{ code: 'ko', name: '한국어' },
{ code: 'fi', name: 'Suomi' },
{ code: 'it', name: 'Italiano' },
{ code: 'da', name: 'Dansk' },
{ code: 'sv', name: 'Svenska' },
{ code: 'nl', name: 'Nederlands' },
{ code: 'ua', name: 'Українська' },
{ code: 'ru', name: 'Русский' },
{ code: 'zh_CN', name: '简体中文' },
{ code: 'zh_TW', name: '繁體中文' },
{ code: 'ja', name: '日本語' },
];
function TranslatedContent() {
const { t } = useTranslation();
return (
<>
{t('Analytics Reference')}
{t(
'This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.',
)}
{t('What we collect:')}
{t('Page views (which screens you visit)')}
{t('Time spent on pages')}
{t('Basic device info (browser type, screen size)')}
{t('Your IP address (anonymized — last part removed for privacy)')}
{t("What we DON'T collect:")}
{t('Personal info (names, emails, passwords)')}
{t('Exact location')}
{t('Any files or content you interact with')}
{t('Why anonymous?')}
{t(
'Your IP is automatically shortened, and no one can identify you personally from this data.',
)}
{t('Your options:')}
{t('Continue:')} {' '}
{t("We'll track anonymized usage to help improve the app.")}
{t('Opt out:')} {' '}
{t(
"Click the Deny button below to disable tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)",
)}
{t('Data goes to: Google Analytics. See')}{' '}
{t('their privacy policy')}
.
>
);
}
function TranslatedButtons({
onAccept,
onOptOut,
}: {
onAccept: () => void;
onOptOut: () => void;
}) {
const { t } = useTranslation();
return (
{t('Allow')}
{t('Deny')}
);
}
function PrivacyConsentDialog(props: PrivacyConsentDialogProps) {
const { t, i18n } = useTranslation();
const { isOpen, onAccept, onOptOut } = props;
const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');
const [forceUpdate, setForceUpdate] = useState(0);
useEffect(() => {
const updateLanguage = () => {
const newLang = i18nInstance.language || 'en';
setCurrentLanguage(newLang);
setForceUpdate((prev) => prev + 1);
};
const handleLanguageChanged = () => {
updateLanguage();
};
i18nInstance.on('languageChanged', handleLanguageChanged);
i18nInstance.on('loaded', handleLanguageChanged);
return () => {
i18nInstance.off('languageChanged', handleLanguageChanged);
i18nInstance.off('loaded', handleLanguageChanged);
};
}, []);
const handleLanguageChange = (
event: React.ChangeEvent,
) => {
const newLang = event.target.value;
i18nInstance
.changeLanguage(newLang)
.then(() => {
setCurrentLanguage(newLang);
setForceUpdate((prev) => prev + 1);
})
.catch((err) => {
console.error('Error changing language:', err);
});
};
const langKey = `${currentLanguage}-${forceUpdate}`;
return (
({
value: lang.code,
label: lang.name,
}))}
minimal
style={{ minWidth: '120px' }}
/>
{t('Privacy Notice: Analytics in Deskreen CE Viewer')}
);
}
export default PrivacyConsentDialog;
================================================
FILE: src/client-viewer/src/components/PrivacyControlDialog/index.css
================================================
.privacy-control-dialog-backdrop {
backdrop-filter: blur(2px);
}
.privacy-control-dialog {
display: flex;
flex-direction: column;
}
.privacy-control-buttons-container {
flex-shrink: 0;
background: white;
border-top: 1px solid rgba(16, 22, 26, 0.15);
}
.bp4-dark .privacy-control-buttons-container {
background: #30404d;
border-top-color: rgba(255, 255, 255, 0.2);
}
.privacy-control-dialog a {
color: #137cbd;
text-decoration: none;
}
.privacy-control-dialog a:hover {
text-decoration: underline;
color: #106ba3;
}
.allow-analytics-button {
box-shadow:
0 4px 16px rgba(15, 153, 96, 0.5),
0 0 20px rgba(15, 153, 96, 0.3) !important;
transition: all 0.2s ease-in-out !important;
background: linear-gradient(135deg, #0f9960 0%, #0a7a4a 100%) !important;
border: none !important;
color: white !important;
}
.allow-analytics-button:hover {
box-shadow:
0 6px 24px rgba(15, 153, 96, 0.6),
0 0 30px rgba(15, 153, 96, 0.4) !important;
transform: translateY(-2px);
background: linear-gradient(135deg, #15b371 0%, #0f9960 100%) !important;
}
.allow-analytics-button:active {
transform: translateY(0);
box-shadow: 0 2px 12px rgba(15, 153, 96, 0.5) !important;
}
.disagree-analytics-button {
opacity: 0.7;
transition: opacity 0.2s ease-in-out;
color: #5c7080 !important;
border-color: #ced9e0 !important;
}
.disagree-analytics-button:hover {
opacity: 0.9;
background-color: #f5f8fa !important;
}
.bp4-dark .disagree-analytics-button {
color: #8a9ba8 !important;
border-color: #5c7080 !important;
}
.bp4-dark .disagree-analytics-button:hover {
background-color: #394b59 !important;
}
================================================
FILE: src/client-viewer/src/components/PrivacyControlDialog/index.tsx
================================================
import { useEffect, useState } from 'react';
import {
Button,
Classes,
Dialog,
Divider,
H2,
H3,
HTMLSelect,
Icon,
} from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import { useTranslation } from 'react-i18next';
import i18nInstance from '../../config/i18n';
import { getConsentStatus, type ConsentStatus } from '../../utils/analytics';
import './index.css';
interface PrivacyControlDialogProps {
isOpen: boolean;
onClose: () => void;
onAccept: () => void;
onOptOut: () => void;
}
const AVAILABLE_LANGUAGES = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'de', name: 'Deutsch' },
{ code: 'fr', name: 'Français' },
{ code: 'ko', name: '한국어' },
{ code: 'fi', name: 'Suomi' },
{ code: 'it', name: 'Italiano' },
{ code: 'da', name: 'Dansk' },
{ code: 'sv', name: 'Svenska' },
{ code: 'nl', name: 'Nederlands' },
{ code: 'ua', name: 'Українська' },
{ code: 'ru', name: 'Русский' },
{ code: 'zh_CN', name: '简体中文' },
{ code: 'zh_TW', name: '繁體中文' },
{ code: 'ja', name: '日本語' },
];
function TranslatedContent() {
const { t } = useTranslation();
return (
<>
{t('Analytics Reference')}
{t(
'This app uses Google Analytics (a free service by Google) to anonymously track basic usage data. This helps us understand how people use the app so we can improve it for everyone.',
)}
{t('What we collect:')}
{t('Page views (which screens you visit)')}
{t('Time spent on pages')}
{t('Basic device info (browser type, screen size)')}
{t('Your IP address (anonymized — last part removed for privacy)')}
{t("What we DON'T collect:")}
{t('Personal info (names, emails, passwords)')}
{t('Exact location')}
{t('Any files or content you interact with')}
{t('Why anonymous?')}
{t(
'Your IP is automatically shortened, and no one can identify you personally from this data.',
)}
{t('Change your preference:')}
{t('Enable analytics:')} {' '}
{t("We'll track anonymized usage to help improve the app.")}
{t('Disable analytics:')} {' '}
{t(
"Click the Disable button below to stop tracking. (We'll respect this choice, but you might miss out on future improvements based on collective feedback.)",
)}
{t('Data goes to: Google Analytics. See')}{' '}
{t('their privacy policy')}
.
>
);
}
function TranslatedButtons({
onAccept,
onOptOut,
currentStatus,
}: {
onAccept: () => void;
onOptOut: () => void;
currentStatus: ConsentStatus;
}) {
const { t } = useTranslation();
return (
{t('Enable Analytics')}
{currentStatus === 'accepted' && (
{t('Disable Analytics')}
)}
);
}
function PrivacyControlDialog(props: PrivacyControlDialogProps) {
const { t, i18n } = useTranslation();
const { isOpen, onClose, onAccept, onOptOut } = props;
const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');
const [forceUpdate, setForceUpdate] = useState(0);
const [currentStatus, setCurrentStatus] = useState(null);
useEffect(() => {
if (isOpen) {
setCurrentStatus(getConsentStatus());
}
}, [isOpen]);
useEffect(() => {
const updateLanguage = () => {
const newLang = i18nInstance.language || 'en';
setCurrentLanguage(newLang);
setForceUpdate((prev) => prev + 1);
};
const handleLanguageChanged = () => {
updateLanguage();
};
i18nInstance.on('languageChanged', handleLanguageChanged);
i18nInstance.on('loaded', handleLanguageChanged);
return () => {
i18nInstance.off('languageChanged', handleLanguageChanged);
i18nInstance.off('loaded', handleLanguageChanged);
};
}, []);
const handleLanguageChange = (
event: React.ChangeEvent,
) => {
const newLang = event.target.value;
i18nInstance
.changeLanguage(newLang)
.then(() => {
setCurrentLanguage(newLang);
setForceUpdate((prev) => prev + 1);
})
.catch((err) => {
console.error('Error changing language:', err);
});
};
const handleAcceptClick = () => {
onAccept();
setCurrentStatus('accepted');
};
const handleOptOutClick = () => {
onOptOut();
setCurrentStatus('opted-out');
};
const langKey = `${currentLanguage}-${forceUpdate}`;
return (
({
value: lang.code,
label: lang.name,
}))}
minimal
style={{ minWidth: '120px' }}
/>
{t('Privacy Settings')}
);
}
export default PrivacyControlDialog;
================================================
FILE: src/client-viewer/src/components/VideoJSPlayer/index.tsx
================================================
import { useEffect, useRef } from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types provided by video.js in this setup
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import './videojs-contain.css';
interface VideoJSPlayerProps {
stream: MediaStream | null;
playing: boolean;
containerEl: HTMLElement | null;
}
type VideoJsPlayerInstance = {
play?: () => void | Promise;
pause?: () => void;
dispose?: () => void;
};
function VideoJSPlayer(props: VideoJSPlayerProps) {
const { stream, playing, containerEl } = props;
const videoElRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
if (!containerEl || !videojs) return;
if (playerRef.current) return;
const videoEl = document.createElement('video');
videoEl.className = 'video-js vjs-default-skin';
videoEl.setAttribute('playsinline', 'true');
videoEl.setAttribute('webkit-playsinline', 'true');
videoEl.muted = true; // allow autoplay on mobile/safari
videoEl.style.width = '100%';
videoEl.style.height = '100%';
videoEl.style.objectFit = 'contain';
videoEl.style.backgroundColor = 'black';
// set container background to black to show letterboxing
try {
containerEl.style.backgroundColor = 'black';
} catch {
// ignore styling errors
}
containerEl.appendChild(videoEl);
videoElRef.current = videoEl;
playerRef.current = videojs(videoEl, {
controls: false,
autoplay: false,
preload: 'auto',
fluid: false,
fill: false,
responsive: false,
inactivityTimeout: 0,
});
return () => {
try {
if (playerRef.current) {
const instance = playerRef.current;
instance.dispose?.();
playerRef.current = null;
}
} finally {
if (videoElRef.current && videoElRef.current.parentNode) {
videoElRef.current.parentNode.removeChild(videoElRef.current);
}
videoElRef.current = null;
}
};
}, [containerEl]);
useEffect(() => {
const el = videoElRef.current;
if (!el) return;
try {
if (stream instanceof MediaStream) {
// @ts-ignore srcObject exists on HTMLMediaElement
el.srcObject = stream;
el.style.objectFit = 'contain';
el.style.backgroundColor = 'black';
if (playing) {
// attempt play after attaching
const p = el.play();
if (p && typeof p.catch === 'function') {
p.catch(() => {
// ignore autoplay failures
});
}
}
} else {
// @ts-ignore
el.srcObject = null;
el.removeAttribute('src');
el.load();
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to attach MediaStream to element', e);
}
}, [stream, playing]);
useEffect(() => {
const player = playerRef.current;
if (!player) return;
if (playing) {
player.play && player.play();
} else {
player.pause && player.pause();
}
}, [playing]);
return null;
}
export default VideoJSPlayer;
================================================
FILE: src/client-viewer/src/components/VideoJSPlayer/videojs-contain.css
================================================
.video-js,
.video-js .vjs-tech {
width: 100% !important;
height: 100% !important;
background-color: #000 !important;
}
.video-js .vjs-tech,
.video-js .vjs-tech[style] {
object-fit: contain !important;
}
:fullscreen .video-js,
:fullscreen .video-js .vjs-tech,
:-webkit-full-screen .video-js,
:-webkit-full-screen .video-js .vjs-tech {
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
background-color: #000 !important;
}
================================================
FILE: src/client-viewer/src/config/i18n.ts
================================================
/* istanbul ignore file */
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
// don't want to use this?
// have a look at the Quick start guide
// for passing in lng and translations on init
i18n
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
lng: 'en',
saveMissing: true,
saveMissingTo: 'all',
fallbackLng: 'en', // TODO: to generate missing keys use false as value here, will be useful when custom nodejs server is created to store missing values
debug: false, // change to true to see debug message logs in browser console
// whitelist: ['en', 'es', 'ru', 'ua', 'zh_CN', 'zh_TW', 'da', 'de', 'fi', 'ko', 'it', 'ja', 'nl', 'fr', 'sv'],
backend: {
// path where resources get loaded from
loadPath: '/locales/{{lng}}/{{ns}}.json',
// TODO: in future implement custom nodejs server that accepts missing translations POST requests and updates .missing.json files accordingly. Here is how to do so: https://www.robinwieruch.de/react-internationalization . it can be simple nodejs server that can be started when 'yarn dev' is running, need to ckagne package.json file then
// path to post missing resources
addPath: '/locales/{{lng}}/{{ns}}.json',
// jsonIndent to use when storing json files
jsonIndent: 2,
},
keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;
================================================
FILE: src/client-viewer/src/constants/appConstants.ts
================================================
export const PLAYER_WRAPPER_ID = 'player-wrapper-id';
export const VIDEO_QUALITY_TO_DECIMAL = {
'25%': 0.25, // Q_25_PERCENT
'40%': 0.4, // Q_40_PERCENT
'60%': 0.6, // Q_60_PERCENT
'80%': 0.8, // Q_80_PERCENT
'100%': 1, // Q_100_PERCENT
};
export const COMPARISON_CANVAS_ID = 'comparison-canvas';
export const DUMMY_MY_DEVICE_DETAILS = {
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
myRoomId: '',
};
================================================
FILE: src/client-viewer/src/constants/styleConstants.ts
================================================
export const LIGHT_UI_BACKGROUND = 'rgba(240, 248, 250, 1)';
================================================
FILE: src/client-viewer/src/containers/ConnectionPrompts/index.tsx
================================================
import { Row, Col } from 'react-flexbox-grid';
import { useTranslation } from 'react-i18next';
import { LIGHT_UI_BACKGROUND } from '../../constants/styleConstants';
import MyDeviceInfoCard from '../../components/MyDeviceInfoCard';
import type { TFunction } from 'i18next';
import { Button, H3 } from '@blueprintjs/core';
import ConnectingIndicator from '../../components/ConnectingIndicator';
import DeskreenLogo from '../../images/deskreen_logo_128x128.png';
interface ConnectionPropmptsProps {
myDeviceDetails: DeviceDetails;
isShownTextPrompt: boolean;
promptStep: number;
connectionIconType: ConnectionIconType;
isShownSpinnerIcon: boolean;
spinnerIconType: LoadingSharingIconType;
}
function getPromptContent(t: TFunction, step: number) {
switch (step) {
case 1:
return (
{
t(
'Waiting for user to click ALLOW button on screen sharing device...',
) as string
}
);
case 2:
return {t('Connected!') as string} ;
case 3:
return (
{
t(
'Waiting for user to select source to share from screen sharing device...',
) as string
}
);
default:
return {`${t('Error occurred')} :(`} ;
}
}
function ConnectionPropmpts(props: ConnectionPropmptsProps) {
const {
myDeviceDetails,
promptStep,
connectionIconType,
isShownSpinnerIcon,
spinnerIconType,
} = props;
const { t } = useTranslation();
const handleReinitiateConnection = () => {
window.location.reload();
};
return (
Deskreen CE Viewer
{getPromptContent(t, promptStep)}
{t('re-initiate-connection') as string}
);
}
export default ConnectionPropmpts;
================================================
FILE: src/client-viewer/src/containers/MainView/ConnectionIconEnum.ts
================================================
const ConnectionIcon = {
FEED: 'feed',
FEED_SUBSCRIBED: 'feed-subscribed',
} as const;
export default ConnectionIcon;
================================================
FILE: src/client-viewer/src/containers/MainView/LoadingSharingIconEnum.ts
================================================
export const LoadingSharingIconEnum = {
DESKTOP: 'desktop',
APPLICATION: 'application',
} as const;
export type LoadingSharingIconType =
(typeof LoadingSharingIconEnum)[keyof typeof LoadingSharingIconEnum];
================================================
FILE: src/client-viewer/src/containers/MainView/changeLanguage.ts
================================================
import i18n from '../../config/i18n';
export default (lng: string) => {
i18n.changeLanguage(lng);
};
================================================
FILE: src/client-viewer/src/containers/MainView/handleCreatePeerConnection.ts
================================================
import PeerConnection from '../../features/PeerConnection';
import PeerConnectionUIHandler from '../../features/PeerConnection/PeerConnectionUIHandler';
import VideoAutoQualityOptimizer from '../../features/VideoAutoQualityOptimizer';
import changeLanguage from './changeLanguage';
import ConnectionIcon from './ConnectionIconEnum';
export default (params: CreatePeerConnectionUseEffectParams) => {
const {
peer,
connectionRoomId,
setMyDeviceDetails,
setConnectionIconType,
setIsShownTextPrompt,
setPromptStep,
setScreenSharingSourceType,
setDialogErrorMessage,
setIsErrorDialogOpen,
setUrl,
setPeer,
} = params;
// return the effect function
return () => {
if (!peer) {
if (connectionRoomId === '') {
return;
}
const UIHandler = new PeerConnectionUIHandler(
setMyDeviceDetails,
() => {
setConnectionIconType(ConnectionIcon.FEED_SUBSCRIBED);
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(2);
setTimeout(() => {
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(3);
}, 2000);
},
setScreenSharingSourceType,
changeLanguage,
setDialogErrorMessage,
setIsErrorDialogOpen,
);
const _peer = new PeerConnection(
connectionRoomId,
setUrl,
new VideoAutoQualityOptimizer(),
UIHandler,
);
setPeer(_peer);
setTimeout(() => {
setIsShownTextPrompt(true);
}, 100);
}
// return cleanup function - cleanup when connectionRoomId changes or component unmounts
return () => {
if (peer) {
peer.destroy();
setPeer(undefined);
}
};
};
};
================================================
FILE: src/client-viewer/src/containers/MainView/handleDisplayingLoadingSharingIconLoop.ts
================================================
import { LoadingSharingIconEnum } from './LoadingSharingIconEnum';
export default (params: handleDisplayingLoadingSharingIconLoopParams) => {
const {
promptStep,
url,
setIsShownLoadingSharingIcon,
loadingSharingIconType,
isShownLoadingSharingIcon,
setLoadingSharingIconType,
} = params;
return () => {
let interval: NodeJS.Timeout;
if (promptStep === 3 && url === null) {
setIsShownLoadingSharingIcon(true);
let currentIcon = loadingSharingIconType;
let isShownIcon = isShownLoadingSharingIcon;
let isShownWithFadingUIEffect = false;
interval = setInterval(() => {
isShownIcon = !isShownIcon;
setIsShownLoadingSharingIcon(isShownIcon);
if (isShownWithFadingUIEffect) {
currentIcon =
currentIcon === LoadingSharingIconEnum.DESKTOP
? LoadingSharingIconEnum.APPLICATION
: LoadingSharingIconEnum.DESKTOP;
setLoadingSharingIconType(currentIcon);
isShownWithFadingUIEffect = false;
} else {
isShownWithFadingUIEffect = true;
}
}, 1500);
}
return () => {
clearInterval(interval);
};
};
};
================================================
FILE: src/client-viewer/src/containers/MainView/handleNoConnectionTimeout.ts
================================================
import { DUMMY_MY_DEVICE_DETAILS } from '../../constants/appConstants';
export default (
myDeviceDetails: DeviceDetails,
setIsErrorDialogOpen: (_: boolean) => void,
) => {
return () => {
const timeout = setTimeout(() => {
if (myDeviceDetails === DUMMY_MY_DEVICE_DETAILS) {
setIsErrorDialogOpen(true);
}
}, 10000);
return () => {
clearTimeout(timeout);
};
};
};
================================================
FILE: src/client-viewer/src/containers/MainView/handleRemoveDanglingReactRevealContainer.ts
================================================
export default (url: MediaStream | null) => {
return () => {
if (url !== null) {
setTimeout(() => {
// @ts-ignore
document.querySelector('.container > div:nth-child(1)').style.display =
'none';
}, 1000);
}
};
};
================================================
FILE: src/client-viewer/src/containers/MainView/handleSetVideoQuality.ts
================================================
import PeerConnection from '../../features/PeerConnection';
import { type VideoQualityType } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
export default (
videoQuality: VideoQualityType,
peer: PeerConnection | undefined,
) => {
return () => {
if (!peer) return;
if (!peer.isStreamStarted) return;
peer.setVideoQuality(videoQuality);
};
};
================================================
FILE: src/client-viewer/src/containers/MainView/index.css
================================================
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
#pulsing-circle-1 {
animation: pulse1 infinite 6s linear;
}
#pulsing-circle-2 {
animation: pulse2 infinite 6s linear;
}
.pulse-3-once {
animation: pulse3twice 2 750ms linear;
}
}
@keyframes pulse1 {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(72, 175, 240, 0.7);
}
80% {
transform: scale(1);
box-shadow: 0 0 0 15px rgba(72, 175, 240, 0.4);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(72, 175, 240, 0);
}
}
@keyframes pulse2 {
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(72, 175, 240, 0);
}
80% {
transform: scale(1);
box-shadow: 0 0 0 33px rgba(72, 175, 240, 0.3);
}
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(72, 175, 240, 1);
}
}
@keyframes pulse3twice {
0% {
box-shadow: 0 0 0 0 rgba(21, 179, 113, 0.7);
}
50% {
box-shadow: 0 0 0 30px rgba(21, 179, 113, 0.3);
}
100% {
box-shadow: 0 0 0 0 rgba(21, 179, 113, 0);
}
}
.container > .react-reveal {
overflow: hidden;
}
================================================
FILE: src/client-viewer/src/containers/MainView/index.tsx
================================================
import { useEffect, useState, useCallback } from 'react';
import { Grid } from 'react-flexbox-grid';
import screenfull from 'screenfull';
import './index.css';
import PeerConnection from '../../features/PeerConnection';
import {
VideoQuality,
type VideoQualityType,
} from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
import ErrorDialog from '../../components/ErrorDialog';
import {
ErrorMessage,
type ErrorMessageType,
} from '../../components/ErrorDialog/ErrorMessageEnum';
import ConnectionPropmpts from '../../containers/ConnectionPrompts';
import PlayerView from '../../containers/PlayerView';
import handleSetVideoQuality from './handleSetVideoQuality';
import { DUMMY_MY_DEVICE_DETAILS } from '../../constants/appConstants';
import handleNoConnectionTimeout from './handleNoConnectionTimeout';
import handleCreatePeerConnection from './handleCreatePeerConnection';
import handleRemoveDanglingReactRevealContainer from './handleRemoveDanglingReactRevealContainer';
import handleDisplayingLoadingSharingIconLoop from './handleDisplayingLoadingSharingIconLoop';
import { ScreenSharingSource } from '../../features/PeerConnection/ScreenSharingSourceEnum';
import ConnectionIcon from './ConnectionIconEnum';
import { LoadingSharingIconEnum } from './LoadingSharingIconEnum';
import { useScreenViewingTracker } from './useScreenViewingTracker';
function MainView() {
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState(false);
const [promptStep, setPromptStep] = useState(1);
const [dialogErrorMessage, setDialogErrorMessage] =
useState(ErrorMessage.UNKNOWN_ERROR);
const [connectionIconType, setConnectionIconType] =
useState(ConnectionIcon.FEED);
const [myDeviceDetails, setMyDeviceDetails] = useState(
DUMMY_MY_DEVICE_DETAILS,
);
const [playing, setPlaying] = useState(true);
const [url, setUrl] = useState(null);
const [screenSharingSourceType, setScreenSharingSourceType] =
useState(ScreenSharingSource.SCREEN);
const [isWithControls, setIsWithControls] = useState(!screenfull.isEnabled);
const [isShownTextPrompt, setIsShownTextPrompt] = useState(false);
const [isShownLoadingSharingIcon, setIsShownLoadingSharingIcon] =
useState(false);
const [loadingSharingIconType, setLoadingSharingIconType] =
useState(LoadingSharingIconEnum.DESKTOP);
const [videoQuality, setVideoQuality] = useState(
VideoQuality.Q_100_PERCENT,
);
const [peer, setPeer] = useState();
const [connectionRoomId, setConnectionRoomId] = useState('');
useEffect(() => {
const { pathname } = window.location;
const normalizedPath = pathname.startsWith('/')
? pathname.slice(1)
: pathname;
const extractedRoomId = normalizedPath.split('/').filter(Boolean)[0] || '';
if (extractedRoomId !== '') {
setConnectionRoomId(extractedRoomId);
return;
}
const fallbackRoomId = Math.random().toString(36).substring(2, 10);
setConnectionRoomId(fallbackRoomId);
}, []);
useEffect(handleSetVideoQuality(videoQuality, peer), [videoQuality, peer]);
useEffect(handleNoConnectionTimeout(myDeviceDetails, setIsErrorDialogOpen), [
myDeviceDetails,
]);
useEffect(
handleCreatePeerConnection({
peer,
connectionRoomId,
setMyDeviceDetails,
setConnectionIconType,
setIsShownTextPrompt,
setPromptStep,
setScreenSharingSourceType,
setDialogErrorMessage,
setIsErrorDialogOpen,
setUrl,
setPeer,
}),
[connectionRoomId],
);
const handlePlayPause = useCallback(() => {
setPlaying(!playing);
}, [playing]);
useEffect(handleRemoveDanglingReactRevealContainer(url), [url]);
useEffect(
handleDisplayingLoadingSharingIconLoop({
promptStep,
url,
setIsShownLoadingSharingIcon,
loadingSharingIconType,
isShownLoadingSharingIcon,
setLoadingSharingIconType,
}),
[promptStep, url],
);
useScreenViewingTracker({
streamUrl: url,
isPlaying: playing,
isErrorDialogOpen,
dialogErrorMessage,
});
return (
);
}
export default MainView;
================================================
FILE: src/client-viewer/src/containers/MainView/useScreenViewingTracker.ts
================================================
import { useEffect, useRef } from 'react';
import { trackAnalyticsEvent } from '../../utils/analytics';
import { type ErrorMessageType } from '../../components/ErrorDialog/ErrorMessageEnum';
interface UseScreenViewingTrackerParams {
streamUrl: MediaStream | null;
isPlaying: boolean;
isErrorDialogOpen: boolean;
dialogErrorMessage: ErrorMessageType;
}
function formatErrorMessageForEvent(errorMessage: ErrorMessageType): string {
// convert error message to event-friendly format
// e.g., "An unknown error occurred" -> "an_unknown_error_occurred"
return errorMessage
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_');
}
export function useScreenViewingTracker(
params: UseScreenViewingTrackerParams,
): void {
const { streamUrl, isPlaying, isErrorDialogOpen, dialogErrorMessage } =
params;
const startTimeRef = useRef(null);
const intervalRef = useRef(null);
const lastMinuteTrackedRef = useRef(0);
const errorEventSentRef = useRef(false);
const previousErrorDialogOpenRef = useRef(false);
const isErrorDialogOpenRef = useRef(false);
// determine if stream is currently visible (without error dialog check)
const isStreamVisible = streamUrl !== null && isPlaying;
// update error dialog ref
isErrorDialogOpenRef.current = isErrorDialogOpen;
// handle error dialog appearance
useEffect(() => {
// if error dialog just appeared and we were tracking
if (
isErrorDialogOpen &&
!previousErrorDialogOpenRef.current &&
startTimeRef.current !== null &&
!errorEventSentRef.current
) {
// clear interval
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// calculate total minutes spent viewing
const elapsedMs = Date.now() - startTimeRef.current;
const elapsedMinutes = Math.floor(elapsedMs / 60000);
const errorReason = formatErrorMessageForEvent(dialogErrorMessage);
// send error event
trackAnalyticsEvent(
`error_dialog_reason_${errorReason}_spent_screen_viewing_${elapsedMinutes}_minutes`,
{},
);
errorEventSentRef.current = true;
}
previousErrorDialogOpenRef.current = isErrorDialogOpen;
}, [isErrorDialogOpen, dialogErrorMessage]);
// handle stream visibility tracking
useEffect(() => {
// if stream becomes visible and no error dialog, start tracking
if (
isStreamVisible &&
!isErrorDialogOpen &&
startTimeRef.current === null
) {
startTimeRef.current = Date.now();
lastMinuteTrackedRef.current = 0;
errorEventSentRef.current = false;
// set up interval to check every minute
intervalRef.current = setInterval(() => {
if (startTimeRef.current === null || isErrorDialogOpenRef.current) {
return;
}
const elapsedMs = Date.now() - startTimeRef.current;
const elapsedMinutes = Math.floor(elapsedMs / 60000);
// send event for each new minute
if (elapsedMinutes > lastMinuteTrackedRef.current) {
lastMinuteTrackedRef.current = elapsedMinutes;
trackAnalyticsEvent(`screen_viewing_${elapsedMinutes}_minutes`, {});
}
}, 60000); // check every minute
}
// if stream is not visible (and no error dialog), stop tracking and reset
if (
(!isStreamVisible || isErrorDialogOpen) &&
intervalRef.current !== null
) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// reset tracking state when stream is not visible (only if no error dialog)
if (!isStreamVisible && !isErrorDialogOpen) {
startTimeRef.current = null;
lastMinuteTrackedRef.current = 0;
errorEventSentRef.current = false;
}
// if error dialog closes and stream is still visible, restart tracking
if (
!isErrorDialogOpen &&
previousErrorDialogOpenRef.current &&
isStreamVisible &&
errorEventSentRef.current
) {
// reset error event flag and restart tracking
errorEventSentRef.current = false;
startTimeRef.current = Date.now();
lastMinuteTrackedRef.current = 0;
// restart interval
if (intervalRef.current === null) {
intervalRef.current = setInterval(() => {
if (startTimeRef.current === null || isErrorDialogOpenRef.current) {
return;
}
const elapsedMs = Date.now() - startTimeRef.current;
const elapsedMinutes = Math.floor(elapsedMs / 60000);
// send event for each new minute
if (elapsedMinutes > lastMinuteTrackedRef.current) {
lastMinuteTrackedRef.current = elapsedMinutes;
trackAnalyticsEvent(`screen_viewing_${elapsedMinutes}_minutes`, {});
}
}, 60000);
}
}
// cleanup on unmount
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [isStreamVisible, isErrorDialogOpen]);
}
================================================
FILE: src/client-viewer/src/containers/PlayerView/index.tsx
================================================
import { useEffect, useRef, useCallback } from 'react';
import { OverlayToaster, Position } from '@blueprintjs/core';
import { useTranslation } from 'react-i18next';
import VideoJSPlayer from '../../components/VideoJSPlayer';
import PlayerControlPanel from '../../components/PlayerControlPanel';
import {
COMPARISON_CANVAS_ID,
PLAYER_WRAPPER_ID,
} from '../../constants/appConstants';
import { type VideoQualityType } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
import { togglePlayerFullscreen } from '../../utils/playerFullscreen';
interface PlayerViewProps {
isWithControls: boolean;
setIsWithControls: (_: boolean) => void;
handlePlayPause: () => void;
isPlaying: boolean;
setPlaying: (playing: boolean) => void;
setVideoQuality: (_: VideoQualityType) => void;
videoQuality: VideoQualityType;
screenSharingSourceType: ScreenSharingSourceType;
streamUrl: MediaStream | null;
}
type IOSVideoElement = HTMLVideoElement & {
webkitEnterFullscreen?: () => void;
webkitExitFullscreen?: () => void;
webkitSupportsFullscreen?: boolean;
webkitDisplayingFullscreen?: boolean;
};
function PlayerView(props: PlayerViewProps) {
const { t } = useTranslation();
const {
screenSharingSourceType,
setIsWithControls,
isWithControls,
handlePlayPause,
isPlaying,
setPlaying,
setVideoQuality,
videoQuality,
streamUrl,
} = props;
// const player = useRef(null);
const videoRef = useRef(null);
const toasterRef = useRef> | null>(null);
// no external player ref needed for video.js variant
useEffect(() => {
if (!streamUrl) return;
// html5 video mode
if (isWithControls && videoRef.current) {
if (streamUrl instanceof MediaStream) {
videoRef.current.srcObject = streamUrl;
} else {
videoRef.current.src = streamUrl;
}
videoRef.current.play().catch((error) => {
console.error('Error playing video:', error);
});
return;
}
// video.js mode (default) doesn't need imperative src assignment here
}, [streamUrl, isWithControls]);
useEffect(() => {
if (isWithControls) {
if (!videoRef.current) return;
if (isPlaying) {
videoRef.current.play().catch((error) => {
console.error('Error playing video:', error);
});
} else {
videoRef.current.pause();
}
}
// react-player play/pause is handled via its `playing` prop
}, [isPlaying, isWithControls]);
// initialize toaster
useEffect(() => {
const initToaster = async () => {
if (!toasterRef.current) {
toasterRef.current = await OverlayToaster.create({
position: Position.BOTTOM,
});
}
};
initToaster();
}, []);
// wrap handlePlayPause to show toaster notifications
const handlePlayPauseWithNotification = useCallback(() => {
const nextPlaying = !isPlaying;
handlePlayPause();
// show notification after a small delay to ensure state is updated
setTimeout(() => {
if (toasterRef.current) {
toasterRef.current.show({
message: nextPlaying ? t('Video stream is playing') : t('Video stream is paused'),
intent: nextPlaying ? 'success' : 'warning',
timeout: 2000,
});
}
}, 50);
}, [handlePlayPause, isPlaying, t]);
// handle iPhone fullscreen exit - detect when video stops and auto-resume
useEffect(() => {
if (!streamUrl) return;
const getVideoElement = (): IOSVideoElement | null => {
if (isWithControls && videoRef.current) {
return videoRef.current as IOSVideoElement;
}
const container = document.getElementById(PLAYER_WRAPPER_ID);
if (!container) return null;
const maybeVideo = container.querySelector('video');
if (!(maybeVideo instanceof HTMLVideoElement)) return null;
return maybeVideo as IOSVideoElement;
};
const handleFullscreenEnd = () => {
// small delay to ensure video state is updated after fullscreen exit
setTimeout(() => {
const video = getVideoElement();
if (!video) return;
// check if video is paused after exiting fullscreen
if (video.paused) {
// sync play state - ensure button shows "Play" instead of "Pause"
setPlaying(false);
// show warning notification that video stopped and user needs to click play
if (toasterRef.current) {
toasterRef.current.show({
message: t('Video stream paused after exiting fullscreen. Please click Play to continue.'),
intent: 'warning',
timeout: 5000,
});
}
} else {
// video is playing, but state might be wrong - sync it
if (!isPlaying) {
setPlaying(true);
}
}
}, 150);
};
const attachListener = (video: IOSVideoElement | null) => {
if (video) {
video.addEventListener('webkitendfullscreen', handleFullscreenEnd);
}
};
const detachListener = (video: IOSVideoElement | null) => {
if (video) {
video.removeEventListener('webkitendfullscreen', handleFullscreenEnd);
}
};
let currentVideo: IOSVideoElement | null = getVideoElement();
attachListener(currentVideo);
// watch for video element changes (especially for VideoJSPlayer)
const container = document.getElementById(PLAYER_WRAPPER_ID);
let observer: MutationObserver | null = null;
if (container) {
observer = new MutationObserver(() => {
const newVideo = getVideoElement();
if (newVideo !== currentVideo) {
detachListener(currentVideo);
currentVideo = newVideo;
attachListener(currentVideo);
}
});
observer.observe(container, { childList: true, subtree: true });
}
return () => {
detachListener(currentVideo);
if (observer) {
observer.disconnect();
}
};
}, [streamUrl, isWithControls, isPlaying, setPlaying, t]);
// @ts-ignore
return (
setIsWithControls(isEnabled)}
isDefaultPlayerTurnedOn={isWithControls}
handleClickFullscreen={() => {
const result = togglePlayerFullscreen();
if (result === 'failed') {
console.warn('Unable to toggle fullscreen');
}
return result;
}}
handleClickPlayPause={handlePlayPauseWithNotification}
isPlaying={isPlaying}
setVideoQuality={setVideoQuality}
selectedVideoQuality={videoQuality}
screenSharingSourceType={screenSharingSourceType}
/>
{isWithControls ? (
) : (
)}
);
}
export default PlayerView;
================================================
FILE: src/client-viewer/src/features/PeerConnection/NullUser.ts
================================================
export default { username: '', id: '' };
================================================
FILE: src/client-viewer/src/features/PeerConnection/PartnerPeerUser.d.ts
================================================
interface PartnerPeerUser {
username: string;
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/PeerConnection.d.ts
================================================
type PeerConnection = import('.').default;
================================================
FILE: src/client-viewer/src/features/PeerConnection/PeerConnectionUIHandler.ts
================================================
import {
ErrorMessage,
type ErrorMessageType,
} from '../../components/ErrorDialog/ErrorMessageEnum';
export default class PeerConnectionUIHandler {
setMyDeviceDetails: (details: DeviceDetails) => void;
hostAllowedToConnectCallback: () => void;
setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void;
setAppLanguageCallback: (newLang: string) => void;
setDialogErrorMessageCallback: (message: ErrorMessageType) => void;
setIsErrorDialogOpen: (val: boolean) => void;
errorDialogMessage: ErrorMessageType = ErrorMessage.UNKNOWN_ERROR;
constructor(
setMyDeviceDetails: (details: DeviceDetails) => void,
hostAllowedToConnectCallback: () => void,
setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void,
setAppLanguageCallback: (newLang: string) => void,
setDialogErrorMessageCallback: (message: ErrorMessageType) => void,
setIsErrorDialogOpen: (val: boolean) => void,
) {
this.hostAllowedToConnectCallback = hostAllowedToConnectCallback;
this.setMyDeviceDetails = setMyDeviceDetails;
this.setScreenSharingSourceTypeCallback =
setScreenSharingSourceTypeCallback;
this.setAppLanguageCallback = setAppLanguageCallback;
this.setDialogErrorMessageCallback = setDialogErrorMessageCallback;
this.setIsErrorDialogOpen = setIsErrorDialogOpen;
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts
================================================
interface ReceiveEncryptedMessagePayload {
fromSocketID: string;
type: string;
payload: Record;
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/ScreenSharingSourceEnum.ts
================================================
export const ScreenSharingSource = {
WINDOW: 'window',
SCREEN: 'screen',
} as const;
export type ScreenSharingSourceType =
(typeof ScreenSharingSource)[keyof typeof ScreenSharingSource];
================================================
FILE: src/client-viewer/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts
================================================
export default class PeerConnectionPartnerIsNotDefinedError extends Error {
constructor() {
super('partner should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(
this,
PeerConnectionPartnerIsNotDefinedError.prototype,
);
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts
================================================
export default class PeerConnectionPeerIsNullError extends Error {
constructor() {
super('peer of PeerConnection should not be null!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionPeerIsNullError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts
================================================
export default class PeerConnectionSocketNotDefined extends Error {
constructor() {
super('socket should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionSocketNotDefined.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts
================================================
export default class PeerConnectionUserIsNotDefinedError extends Error {
constructor() {
super('user should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionUserIsNotDefinedError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/index.ts
================================================
import shortId from 'shortid';
import SimplePeer from 'simple-peer';
import { UAParser } from 'ua-parser-js';
import type { Socket } from 'socket.io-client';
import type { LocalPeerUser } from '../../../../common/LocalPeerUser';
import type { SendEncryptedMessagePayload } from '../../../../common/SendEncryptedMessagePayload';
import { connect as connectSocket } from '../../utils/socket';
import {
prepare as prepareMessage,
type ProcessedPayload,
} from '../../utils/message';
import setSdpMediaBitrate from './setSdpMediaBitrate';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import {
VideoQuality,
type VideoQualityType,
} from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import { prepareDataMessageToChangeQuality } from './simplePeerDataMessages';
import { VIDEO_QUALITY_TO_DECIMAL } from './../../constants/appConstants';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import peerConnectionHandleSocket from './peerConnectionHandleSocket';
import peerConnectionHandlePeer from './peerConnectionHandlePeer';
import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage';
import startSocketConnectedCheckingLoop from './startSocketConnectedCheckingLoop';
import NullUser from './NullUser';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError';
export default class PeerConnection {
roomId: string;
socket: Socket | null = null;
user: LocalPeerUser = NullUser;
partner: PartnerPeerUser = NullUser;
peer: null | SimplePeer.Instance = null;
myDeviceDetails: DeviceDetails = {
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
myRoomId: '',
};
setUrlCallback: (url: MediaStream | null) => void;
uaParser: UAParser;
screenSharingSourceType: string | undefined = undefined;
videoQuality: VideoQualityType = VideoQuality.Q_100_PERCENT;
videoAutoQualityOptimizer: VideoAutoQualityOptimizer;
isStreamStarted: boolean = false;
UIHandler: PeerConnectionUIHandler;
beforeunloadHandler: (() => void) | null = null;
connectionCheckInterval: NodeJS.Timeout | null = null;
reconnectTimeout: NodeJS.Timeout | null = null;
getMyIPTimeout: NodeJS.Timeout | null = null;
setMyDeviceDetailsTimeout: NodeJS.Timeout | null = null;
constructor(
roomId: string,
setUrlCallback: (url: MediaStream | null) => void,
videoAutoQualityOptimizer: VideoAutoQualityOptimizer,
UIHandler: PeerConnectionUIHandler,
) {
this.setUrlCallback = setUrlCallback;
this.videoAutoQualityOptimizer = videoAutoQualityOptimizer;
this.UIHandler = UIHandler;
this.roomId = roomId;
this.socket = connectSocket(this.roomId);
this.uaParser = new UAParser();
this.createUserAndInitSocket();
this.createPeer();
if (!this.roomId || this.roomId === '') {
setAndShowErrorDialogMessage(this, ErrorMessage.NOT_ALLOWED);
}
this.connectionCheckInterval = startSocketConnectedCheckingLoop(this);
}
setVideoQuality(videoQuality: VideoQualityType) {
this.videoQuality = videoQuality;
this.videoQualityChangedCallback();
}
videoQualityChangedCallback() {
if (!this.peer) return;
if (this.videoQuality === VideoQuality.Q_AUTO) {
this.peer.send(prepareDataMessageToChangeQuality(1));
} else {
this.peer.send(
prepareDataMessageToChangeQuality(
VIDEO_QUALITY_TO_DECIMAL[this.videoQuality],
),
);
}
}
stopStream() {
// stop the video stream by clearing the stream URL
this.setUrlCallback(null);
this.isStreamStarted = false;
// destroy the peer connection
if (this.peer) {
try {
this.peer.removeAllListeners();
this.peer.destroy();
} catch (error) {
console.error('Error destroying peer connection:', error);
}
this.peer = null;
}
}
destroy() {
// remove window event listener
if (this.beforeunloadHandler) {
window.removeEventListener('beforeunload', this.beforeunloadHandler);
this.beforeunloadHandler = null;
}
// clear connection check interval
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
this.connectionCheckInterval = null;
}
// clear all timeouts
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
if (this.getMyIPTimeout) {
clearTimeout(this.getMyIPTimeout);
this.getMyIPTimeout = null;
}
if (this.setMyDeviceDetailsTimeout) {
clearTimeout(this.setMyDeviceDetailsTimeout);
this.setMyDeviceDetailsTimeout = null;
}
// stop stream if started
if (this.isStreamStarted) {
this.stopStream();
}
// cleanup peer connection
if (this.peer) {
try {
this.peer.removeAllListeners();
this.peer.destroy();
} catch (error) {
console.error('Error destroying peer:', error);
}
this.peer = null;
}
// cleanup socket
if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect();
}
}
createPeer() {
// cleanup existing peer before creating new one
if (this.peer) {
try {
this.peer.removeAllListeners();
this.peer.destroy();
} catch (error) {
console.error('Error cleaning up existing peer:', error);
}
this.peer = null;
}
// When we are testing with jest, SimplePeer() can not be created, so we just return
const peer = new SimplePeer({
initiator: false,
config: { iceServers: [] },
sdpTransform: (sdp) => {
let newSDP = sdp;
newSDP = setSdpMediaBitrate(
newSDP as unknown as string,
'video',
500000,
) as unknown as typeof sdp;
return newSDP;
},
});
this.peer = peer;
this.peer.on('error', (e) => {
console.error('error in simple peer happened!');
console.error(e);
setAndShowErrorDialogMessage(this, ErrorMessage.WEBRTC_ERROR);
});
peerConnectionHandlePeer(this);
}
initApp(user: LocalPeerUser, myIP: string) {
if (!this.socket) {
throw new PeerConnectionSocketNotDefined();
}
this.socket.emit('USER_ENTER', {
username: user.username,
ip: myIP, // TODO: remove as it is not used
});
}
createUser() {
return new Promise((resolve) => {
const username = shortId.generate();
const id = shortId.generate();
resolve({
username,
id,
});
});
}
sendEncryptedMessage(payload: SendEncryptedMessagePayload) {
const socket = this.socket;
if (!socket) {
throw new PeerConnectionSocketNotDefined();
}
if (!this.user || this.user === NullUser) {
throw new PeerConnectionUserIsNotDefinedError();
}
if (!this.partner || this.partner === NullUser) {
throw new PeerConnectionPartnerIsNotDefinedError();
}
if (!this.partner.username) return;
prepareMessage(payload, this.user).then((msg: ProcessedPayload) => {
socket.emit('MESSAGE', msg.toSend);
});
}
receiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) {
peerConnectionReceiveEncryptedMessage(this, payload);
}
createUserAndInitSocket() {
const socket = this.socket;
if (!socket) {
throw new PeerConnectionSocketNotDefined();
}
socket.removeAllListeners();
const userCreatedCallback = (createdUser: LocalPeerUser) => {
this.user = createdUser;
peerConnectionHandleSocket(this);
this.beforeunloadHandler = () => {
socket.emit('USER_DISCONNECT');
};
window.addEventListener('beforeunload', this.beforeunloadHandler);
};
this.createUser().then((newUser: LocalPeerUser) =>
userCreatedCallback(newUser),
);
}
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts
================================================
export const INPUTtestWindowNavigatorUserAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36';
================================================
FILE: src/client-viewer/src/features/PeerConnection/mocks/INPUTvideo500000testSdpMediaBitrate.ts
================================================
export const INPUTtestSdpMediaBitrate = `
v=0
o=- 5730467698688819135 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
b=AS:30
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:1
a=sctp-port:5000
a=max-message-size:262144
`;
================================================
FILE: src/client-viewer/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts
================================================
export const OUTPUTDeviceDetailsFromUAParsed: DeviceDetails = {
myBrowser: 'Chrome 87.0.4280.88',
myDeviceType: 'computer',
myIP: '123.123.123.123',
myOS: 'Mac OS 10.15.6',
myRoomId: 'asdf2314',
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/mocks/OUTPUTvideo500000testSdpMediaBitrate.ts
================================================
export const OUTPUTtestSdpMediaBitrate = `
v=0
o=- 5730467698688819135 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116
c=IN IP4 0.0.0.0
b=AS:500000
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
b=AS:30
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:1
a=sctp-port:5000
a=max-message-size:262144
`;
================================================
FILE: src/client-viewer/src/features/PeerConnection/peerConnectionHandlePeer.ts
================================================
import {
prepareDataMessageToChangeQuality,
prepareDataMessageToGetSharingSourceType,
} from './simplePeerDataMessages';
import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError';
import { ScreenSharingSource } from './ScreenSharingSourceEnum';
export function getSharingShourceType(peerConnection: PeerConnection) {
try {
peerConnection.peer?.send(prepareDataMessageToGetSharingSourceType());
} catch (e) {
console.log(e);
}
}
export default (peerConnection: PeerConnection) => {
if (peerConnection.peer === null) {
throw new PeerConnectionPeerIsNullError();
}
peerConnection.peer.on('stream', (stream) => {
peerConnection.setUrlCallback(stream);
setTimeout(() => {
peerConnection.videoAutoQualityOptimizer.setGoodQualityCallback(() => {
if (peerConnection.videoQuality === VideoQuality.Q_AUTO) {
try {
peerConnection.peer?.send(prepareDataMessageToChangeQuality(1));
} catch (e) {
console.log(e);
}
}
});
peerConnection.videoAutoQualityOptimizer.setHalfQualityCallbak(() => {
if (peerConnection.videoQuality === VideoQuality.Q_AUTO) {
try {
peerConnection.peer?.send(prepareDataMessageToChangeQuality(0.5));
} catch (e) {
console.log(e);
}
}
});
}, 1000);
peerConnection.videoAutoQualityOptimizer.startOptimizationLoop();
setTimeout(getSharingShourceType, 1000, peerConnection);
peerConnection.isStreamStarted = true;
// if any transient error dialog was shown earlier, close it now
try {
peerConnection.UIHandler.setIsErrorDialogOpen(false);
peerConnection.UIHandler.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;
} catch (_) {
// ignore
}
});
peerConnection.peer.on('signal', (data) => {
// fired when webrtc done preparation to start call on peerConnection machine
peerConnection.sendEncryptedMessage({
type: 'CALL_ACCEPTED',
payload: {
signalData: data,
},
});
});
peerConnection.peer.on('data', (data) => {
const dataJSON = JSON.parse(data);
if (dataJSON.type === 'screen_sharing_source_type') {
peerConnection.screenSharingSourceType = dataJSON.payload.value;
if (
peerConnection.screenSharingSourceType === ScreenSharingSource.SCREEN ||
peerConnection.screenSharingSourceType === ScreenSharingSource.WINDOW
) {
peerConnection.UIHandler.setScreenSharingSourceTypeCallback(
peerConnection.screenSharingSourceType,
);
}
}
});
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/peerConnectionHandleSocket.ts
================================================
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import {
getBrowserFromUAParser,
getDeviceTypeFromUAParser,
getOSFromUAParser,
} from '../../utils/userAgentParserHelpers';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
export function getMyIPCallback(
peerConnection: PeerConnection,
ip: string,
userAgent: string,
) {
peerConnection.myDeviceDetails.myIP = ip;
peerConnection.uaParser.setUA(userAgent);
peerConnection.myDeviceDetails.myOS = getOSFromUAParser(
peerConnection.uaParser,
);
peerConnection.myDeviceDetails.myDeviceType = getDeviceTypeFromUAParser(
peerConnection.uaParser,
);
peerConnection.myDeviceDetails.myBrowser = getBrowserFromUAParser(
peerConnection.uaParser,
);
peerConnection.initApp(peerConnection.user, ip);
}
export default (peerConnection: PeerConnection) => {
let disconnectCount = 0;
let isAllowed = true;
const socket = peerConnection.socket;
if (!socket) {
throw new PeerConnectionSocketNotDefined();
}
socket.on('disconnect', () => {
disconnectCount++;
// handle disconnect even when stream is started - stop stream and show error
if (peerConnection.isStreamStarted && disconnectCount >= 1) {
peerConnection.stopStream();
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
return;
}
// for pre-stream disconnects, wait for sustained disconnection before showing error
if (disconnectCount > 6 && isAllowed) {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
}
});
socket.on('connect', () => {
let ipCallbackReceived = false;
// clear any existing reconnect timeout
if (peerConnection.reconnectTimeout) {
clearTimeout(peerConnection.reconnectTimeout);
}
peerConnection.reconnectTimeout = setTimeout(() => {
if (!ipCallbackReceived && isAllowed) {
console.log('GET_MY_IP callback not received, reconnecting socket');
socket.disconnect();
socket.connect();
}
}, 2500); // 2 seconds timeout to wait for callback
// clear any existing getMyIP timeout
if (peerConnection.getMyIPTimeout) {
clearTimeout(peerConnection.getMyIPTimeout);
}
peerConnection.getMyIPTimeout = setTimeout(() => {
if (!isAllowed) return;
socket.emit('GET_MY_IP', (ip: string) => {
console.log('GET_MY_IP', ip);
ipCallbackReceived = true;
if (peerConnection.reconnectTimeout) {
clearTimeout(peerConnection.reconnectTimeout);
peerConnection.reconnectTimeout = null;
}
getMyIPCallback(peerConnection, ip, window.navigator.userAgent);
});
}, 500);
});
socket.on('NOT_ALLOWED', () => {
isAllowed = false;
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.NOT_ALLOWED);
});
socket.on('USER_ENTER', (payload: { users: PartnerPeerUser[] }) => {
if (!isAllowed) return;
const filteredPartner = payload.users.filter((v) => {
return peerConnection.user.username !== v.username;
});
peerConnection.partner = filteredPartner[0];
if (!peerConnection.partner) return;
peerConnection.sendEncryptedMessage({
type: 'DEVICE_DETAILS',
// TODO: add deviceIP in this payload
payload: {
os: peerConnection.myDeviceDetails.myOS,
deviceType: peerConnection.myDeviceDetails.myDeviceType,
browser: peerConnection.myDeviceDetails.myBrowser,
deviceScreenWidth: window.screen.width,
deviceScreenHeight: window.screen.height,
},
});
peerConnection.sendEncryptedMessage({
type: 'GET_APP_LANGUAGE',
payload: {},
});
// clear any existing timeout
if (peerConnection.setMyDeviceDetailsTimeout) {
clearTimeout(peerConnection.setMyDeviceDetailsTimeout);
}
peerConnection.setMyDeviceDetailsTimeout = setTimeout(() => {
peerConnection.UIHandler.setMyDeviceDetails({
myIP: peerConnection.myDeviceDetails.myIP,
myOS: peerConnection.myDeviceDetails.myOS,
myBrowser: peerConnection.myDeviceDetails.myBrowser,
myDeviceType: peerConnection.myDeviceDetails.myDeviceType,
myRoomId: peerConnection.roomId,
});
}, 100);
});
// peerConnection.socket.on('USER_EXIT', (payload: any) => {
// // peerConnection.props.receiveUnencryptedMessage('USER_EXIT', payload);
// });
socket.on('MESSAGE', (payload: ReceiveEncryptedMessagePayload) => {
if (!isAllowed) return;
peerConnection.receiveEncryptedMessage(payload);
});
socket.on('ROOM_LOCKED', () => {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);
});
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts
================================================
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import { process as processMessage } from '../../utils/message';
import NullUser from './NullUser';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
export default async (
peerConnection: PeerConnection,
payload: ReceiveEncryptedMessagePayload,
) => {
if (peerConnection.user === NullUser) {
throw new PeerConnectionUserIsNotDefinedError();
}
const message = await processMessage(payload);
// const message = payload as any;
if (message.type === 'CALL_USER') {
peerConnection.peer?.signal(message.payload.signalData);
}
if (message.type === 'DENY_TO_CONNECT') {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);
}
if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
}
if (message.type === 'ALLOWED_TO_CONNECT') {
peerConnection.UIHandler.hostAllowedToConnectCallback();
}
if (message.type === 'APP_LANGUAGE') {
peerConnection.UIHandler.setAppLanguageCallback(message.payload.value);
}
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/setAndShowErrorDialogMessage.ts
================================================
import {
ErrorMessage,
type ErrorMessageType,
} from '../../components/ErrorDialog/ErrorMessageEnum';
export default (
peerConnection: PeerConnection,
errorMessage: ErrorMessageType,
) => {
// allow showing disconnect errors even when stream is started
const isDisconnectError = errorMessage === ErrorMessage.DISCONNECTED;
if (peerConnection.isStreamStarted && !isDisconnectError) {
// avoid flashing an error if the stream already started (except for disconnect errors)
return;
}
if (
peerConnection.UIHandler.errorDialogMessage ===
ErrorMessage.UNKNOWN_ERROR ||
isDisconnectError
) {
peerConnection.UIHandler.setDialogErrorMessageCallback(errorMessage);
peerConnection.UIHandler.setIsErrorDialogOpen(true);
peerConnection.UIHandler.errorDialogMessage = errorMessage;
}
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/setSdpMediaBitrate.ts
================================================
export default (sdp: string, mediaType: string, bitrate: number) => {
const sdpLines = sdp.split('\n');
let mediaLineIndex = -1;
const mediaLine = `m=${mediaType}`;
let bitrateLineIndex = -1;
const bitrateLine = `b=AS:${bitrate}`;
mediaLineIndex = sdpLines.findIndex((line) => line.startsWith(mediaLine));
// If we find a line matching “m={mediaType}”
if (mediaLineIndex && mediaLineIndex < sdpLines.length) {
// Skip the media line
bitrateLineIndex = mediaLineIndex + 1;
// Skip both i=* and c=* lines (bandwidths limiters have to come afterwards)
while (
sdpLines[bitrateLineIndex].startsWith('i=') ||
sdpLines[bitrateLineIndex].startsWith('c=')
) {
bitrateLineIndex += 1;
}
if (sdpLines[bitrateLineIndex].startsWith('b=')) {
// If the next line is a b=* line, replace it with our new bandwidth
sdpLines[bitrateLineIndex] = bitrateLine;
} else {
// Otherwise insert a new bitrate line.
sdpLines.splice(bitrateLineIndex, 0, bitrateLine);
}
}
// Then return the updated sdp content as a string
return sdpLines.join('\n');
};
================================================
FILE: src/client-viewer/src/features/PeerConnection/simplePeerDataMessages.ts
================================================
export function prepareDataMessageToChangeQuality(q: number) {
return `
{
"type": "set_video_quality",
"payload": {
"value": ${q}
}
}
`;
}
export function prepareDataMessageToGetSharingSourceType() {
return `
{
"type": "get_sharing_source_type",
"payload": {
}
}
`;
}
================================================
FILE: src/client-viewer/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts
================================================
import PeerConnection from '..';
import { ErrorMessage } from '../../../components/ErrorDialog/ErrorMessageEnum';
import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage';
export default (peerConnection: PeerConnection): NodeJS.Timeout => {
let disconnectedStreak = 0;
let pingTimeout: NodeJS.Timeout | null = null;
const checkConnection = () => {
const socket = peerConnection.socket;
if (!socket) {
disconnectedStreak++;
handleDisconnection();
return;
}
const isSocketConnected = !!socket.connected;
if (isSocketConnected) {
// perform explicit ping/pong check to verify server is alive
try {
if (pingTimeout) {
clearTimeout(pingTimeout);
}
const timeout = setTimeout(() => {
// ping timeout - server didn't respond
disconnectedStreak++;
handleDisconnection();
}, 3000);
pingTimeout = timeout;
socket.emit('PING', (response: string) => {
if (pingTimeout) {
clearTimeout(pingTimeout);
pingTimeout = null;
}
if (response === 'PONG') {
disconnectedStreak = 0;
} else {
disconnectedStreak++;
handleDisconnection();
}
});
} catch {
// socket error during ping
disconnectedStreak++;
handleDisconnection();
}
} else {
// socket is not connected
disconnectedStreak++;
handleDisconnection();
}
};
const handleDisconnection = () => {
// show error and stop stream after sustained disconnection
if (disconnectedStreak >= 3) {
// stop the video stream
if (peerConnection.isStreamStarted) {
peerConnection.stopStream();
}
// show error dialog (now allows showing even when stream was started)
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
}
};
return setInterval(checkConnection, 5000);
};
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/VideoQualityEnum.ts
================================================
export const VideoQuality = {
Q_AUTO: 'Auto',
Q_25_PERCENT: '25%',
Q_40_PERCENT: '40%',
Q_60_PERCENT: '60%',
Q_80_PERCENT: '80%',
Q_100_PERCENT: '100%',
} as const;
export type VideoQualityType = (typeof VideoQuality)[keyof typeof VideoQuality];
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts
================================================
export default class CanvasNotDefinedError extends Error {
constructor() {
super('internal variable of canvas DOM element should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, CanvasNotDefinedError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts
================================================
export default class ImageDataIsUndefinedError extends Error {
constructor() {
super('imageData retrieved is undefined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, ImageDataIsUndefinedError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts
================================================
export default class VideoDimensionsAreWrongError extends Error {
constructor() {
super('video dimensions are wrong, neither width nor height can be zero!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, VideoDimensionsAreWrongError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts
================================================
export default class VideoNotDefinedError extends Error {
constructor() {
super('internal variable of video DOM element should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, VideoNotDefinedError.prototype);
}
}
================================================
FILE: src/client-viewer/src/features/VideoAutoQualityOptimizer/index.ts
================================================
import { COMPARISON_CANVAS_ID } from './../../constants/appConstants';
import pixelmatch from 'pixelmatch';
import { PLAYER_WRAPPER_ID } from '../../constants/appConstants';
import CanvasNotDefinedError from './errors/CanvasNotDefinedError';
import VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError';
import VideoNotDefinedError from './errors/VideoNotDefinedError';
import ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError';
export const CANVAS_SCALE_MULTIPLIER = 0.125; // 1/8 of original canvas size, to speed up calculations
export const MISMATCH_PERCENT_THRESHOLD = 0.1;
export default class VideoAutoQualityOptimizer {
video: undefined | HTMLVideoElement;
canvas: undefined | HTMLCanvasElement;
prevFrame: undefined | ImageData;
largeMismatchFramesCount = 0;
isRequestedHalfQuality = false;
goodQualityCallback = () => {
// noop until callbacks are provided
};
halfQualityCallbak = () => {
// noop until callbacks are provided
};
setGoodQualityCallback(callback: () => void) {
this.goodQualityCallback = callback;
}
setHalfQualityCallbak(callback: () => void) {
this.halfQualityCallbak = callback;
}
startOptimizationLoop() {
this.prepareCanvasAndVideo();
setInterval(() => {
try {
this.doFrameComparisonAndQualityOptimization();
} catch (e) {
// some errors may be thrown here, better ignore them in production
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
}
}, 1000);
}
doFrameComparisonAndQualityOptimization() {
this.validateBeforeCalculations();
this.clearCanvas();
this.scaleCanvas();
this.drawVideoFrameToCanvas();
const imageData = this.getImageDataFromCanvas();
if (!imageData) {
throw new ImageDataIsUndefinedError();
}
if (!this.prevFrame) {
this.prevFrame = imageData;
return;
}
try {
const mismatchInPercent =
this.getPreviousAndCurrentFrameMismatchInPercent(imageData);
this.handleFramesMismatch(mismatchInPercent);
} catch (_e) {
// usually frames size mismatch thrown here, so can be ignored as it happens
// often when changing sharing window size
// so logging this error may be not necessary
}
this.prevFrame = imageData;
}
findAndSetVideoInternalVariable(document: Document) {
this.video = document.querySelector(
`#${PLAYER_WRAPPER_ID} > video`,
) as HTMLVideoElement;
}
findAndSetCanvasInternalVariable(document: Document) {
this.canvas = document.querySelector(
`#${COMPARISON_CANVAS_ID}`,
) as HTMLCanvasElement;
}
prepareCanvasAndVideo() {
setTimeout(() => {
this.findAndSetVideoInternalVariable(document);
this.findAndSetCanvasInternalVariable(document);
}, 1000);
}
clearCanvas() {
this.canvas
?.getContext('2d')
?.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
validateVideoWidthAndHeight() {
if (this.video?.videoWidth === 0 || this.video?.videoHeight === 0) {
throw new VideoDimensionsAreWrongError();
}
}
validateBeforeCalculations() {
this.validateVideoWidthAndHeight();
this.validateVideoIsDefined();
this.validateCanvasIsDefined();
}
validateVideoIsDefined() {
if (!this.video) {
throw new VideoNotDefinedError();
}
}
validateCanvasIsDefined() {
if (!this.canvas) {
throw new CanvasNotDefinedError();
}
}
scaleCanvas() {
if (!this.canvas || !this.video) return;
this.canvas.width = this.video.videoWidth * CANVAS_SCALE_MULTIPLIER;
this.canvas.height = this.video.videoHeight * CANVAS_SCALE_MULTIPLIER;
}
drawVideoFrameToCanvas() {
if (!this.video) return;
this.canvas
?.getContext('2d')
?.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
}
getImageDataFromCanvas() {
return this.canvas
?.getContext('2d')
?.getImageData(0, 0, this.canvas.width, this.canvas.height);
}
getNumberOfMismatchedPixels(imageData: ImageData) {
if (!this.canvas || !this.canvas.width || !this.prevFrame) return 0;
return pixelmatch(
this.prevFrame.data,
imageData.data,
undefined,
this.canvas.width,
this.canvas.height,
{ threshold: 0.1 },
);
}
getPreviousAndCurrentFrameMismatchInPercent(imageData: ImageData) {
if (!this.canvas) return 0;
return (
this.getNumberOfMismatchedPixels(imageData) /
(this.canvas.width * this.canvas.height)
);
}
handleFramesMismatch(mismatchInPercent: number) {
if (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) {
this.largeMismatchFramesCount -= 1;
} else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) {
this.largeMismatchFramesCount = 0;
this.isRequestedHalfQuality = false;
this.goodQualityCallback();
} else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) {
if (this.largeMismatchFramesCount < 3) {
this.largeMismatchFramesCount += 1;
} else {
this.halfQualityCallbak();
this.isRequestedHalfQuality = true;
}
}
}
isLowMismatchPercent(mismatchInPercent: number) {
return mismatchInPercent < MISMATCH_PERCENT_THRESHOLD;
}
isHighMismatchPercent(mismatchInPercent: number) {
return mismatchInPercent >= MISMATCH_PERCENT_THRESHOLD;
}
}
================================================
FILE: src/client-viewer/src/index.css
================================================
@import "normalize.css";
@import "@blueprintjs/core/lib/css/blueprint.css";
/*@import "@blueprintjs/icons/lib/css/blueprint-icons.css";*/
body {
margin: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family:
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
/*:root {*/
/* font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;*/
/* line-height: 1.5;*/
/* font-weight: 400;*/
/* color-scheme: light dark;*/
/* color: rgba(255, 255, 255, 0.87);*/
/* background-color: #242424;*/
/* font-synthesis: none;*/
/* text-rendering: optimizeLegibility;*/
/* -webkit-font-smoothing: antialiased;*/
/* -moz-osx-font-smoothing: grayscale;*/
/*}*/
/*a {*/
/* font-weight: 500;*/
/* color: #646cff;*/
/* text-decoration: inherit;*/
/*}*/
/*a:hover {*/
/* color: #535bf2;*/
/*}*/
/*body {*/
/* margin: 0;*/
/* display: flex;*/
/* place-items: center;*/
/* min-width: 320px;*/
/* min-height: 100vh;*/
/*}*/
/*h1 {*/
/* font-size: 3.2em;*/
/* line-height: 1.1;*/
/*}*/
/*button {*/
/* border-radius: 8px;*/
/* border: 1px solid transparent;*/
/* padding: 0.6em 1.2em;*/
/* font-size: 1em;*/
/* font-weight: 500;*/
/* font-family: inherit;*/
/* background-color: #1a1a1a;*/
/* cursor: pointer;*/
/* transition: border-color 0.25s;*/
/*}*/
/*button:hover {*/
/* border-color: #646cff;*/
/*}*/
/*button:focus,*/
/*button:focus-visible {*/
/* outline: 4px auto -webkit-focus-ring-color;*/
/*}*/
/*@media (prefers-color-scheme: light) {*/
/* :root {*/
/* color: #213547;*/
/* background-color: #ffffff;*/
/* }*/
/* a:hover {*/
/* color: #747bff;*/
/* }*/
/* button {*/
/* background-color: #f9f9f9;*/
/* }*/
/*}*/
.rounded-pill-button {
border-radius: 9999px;
}
================================================
FILE: src/client-viewer/src/main.tsx
================================================
import { initializeGARequestInterceptor } from './utils/gaRequestInterceptor';
// initialize GA request interceptor immediately to block requests before consent
initializeGARequestInterceptor();
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import './config/i18n';
import App from './App.tsx';
import { AppContextProvider } from './providers/AppContextProvider';
import LoadingScreen from './components/LoadingScreen';
createRoot(document.getElementById('root')!).render(
}>
,
);
================================================
FILE: src/client-viewer/src/providers/AppContextProvider/index.tsx
================================================
import React, { useState } from 'react';
interface AppContextInterface {
appLanguage: string;
setAppLanguageHook: (val: string) => void;
}
const defaultAppContextValue = {
appLanguage: 'en',
setAppLanguageHook: () => {
// noop default
},
};
// eslint-disable-next-line react-refresh/only-export-components
export const AppContext = React.createContext(
defaultAppContextValue,
);
export const AppContextProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [appLanguage, setAppLanguage] = useState('en');
const setAppLanguageHook = (newLang: string) => {
setAppLanguage(newLang);
};
const value = {
appLanguage,
setAppLanguageHook,
};
return {children} ;
};
================================================
FILE: src/client-viewer/src/utils/ProcessedMessage.d.ts
================================================
type CallUserMessageWithPayload = {
type: 'CALL_USER';
payload: {
signalData: string;
};
};
type DeviceDetailsMessageWithPayload = {
type: 'DEVICE_DETAILS';
payload: {
socketID: string;
deviceType: { type: string };
os: { name: string; version: string };
browser: { name: string; version: string; major: string };
deviceScreenWidth: number;
deviceScreenHeight: number;
};
};
type DenyToConnectMessageWithPayload = {
type: 'DENY_TO_CONNECT';
payload: {};
};
type DisconnectByHostMachineUserMessageWithPayload = {
type: 'DISCONNECT_BY_HOST_MACHINE_USER';
payload: {};
};
type AllowedToConnectMessageWithPayload = {
type: 'ALLOWED_TO_CONNECT';
payload: {};
};
type AppLanguageMessageWithPayload = {
type: 'APP_LANGUAGE';
payload: {
value: string;
};
};
type ProcessedMessage =
| CallUserMessageWithPayload
| DeviceDetailsMessageWithPayload
| DenyToConnectMessageWithPayload
| DisconnectByHostMachineUserMessageWithPayload
| AllowedToConnectMessageWithPayload
| AppLanguageMessageWithPayload;
================================================
FILE: src/client-viewer/src/utils/analytics.ts
================================================
// google analytics type declarations
declare global {
interface Window {
dataLayer: unknown[];
gtag: (...args: unknown[]) => void;
}
}
const CONSENT_KEY = 'deskreen_ga_consent';
const GA_TAG_PLACEHOLDER = '%VITE_CLIENT_VIEWER_GA_TAG%';
const CLIENT_VIEWER_VERSION_PLACEHOLDER = '%VITE_CLIENT_VIEWER_VERSION%';
let versionEventSent = false;
type AnalyticsEventParams = Record;
export type ConsentStatus = 'accepted' | 'opted-out' | null;
export function getConsentStatus(): ConsentStatus {
if (typeof window === 'undefined') {
return null;
}
const stored = localStorage.getItem(CONSENT_KEY);
if (stored === 'accepted' || stored === 'opted-out') {
return stored;
}
return null;
}
export function setConsentStatus(status: 'accepted' | 'opted-out'): void {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(CONSENT_KEY, status);
}
export function clearConsentStatus(): void {
if (typeof window === 'undefined') {
return;
}
localStorage.removeItem(CONSENT_KEY);
}
export function loadGoogleAnalytics(gaTagId: string): void {
if (
typeof window === 'undefined' ||
!gaTagId ||
gaTagId === GA_TAG_PLACEHOLDER
) {
return;
}
// check if GA script is already loaded in DOM (from HTML)
const existingScript = document.querySelector(
'script[src*="googletagmanager.com/gtag/js"]',
);
if (existingScript && window.dataLayer && typeof window.gtag === 'function') {
// GA is already loaded from HTML, just update consent and send page_view
const consentStatus = getConsentStatus();
const analyticsConsent =
consentStatus === 'accepted' ? 'granted' : 'denied';
// update consent mode
window.gtag('consent', 'update', {
analytics_storage: analyticsConsent,
ad_storage: 'denied',
});
// if user has consent, wait for GA to be ready and send page_view
if (analyticsConsent === 'granted') {
waitForGAReady(() => {
sendPageView();
});
}
return;
}
// initialize dataLayer BEFORE gtag.js loads (required for consent mode)
window.dataLayer = window.dataLayer || [];
function gtag(...args: unknown[]) {
window.dataLayer.push(args);
}
window.gtag = gtag;
gtag('js', new Date());
// set default consent mode to denied (will be updated when user accepts)
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
});
// load gtag.js script
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${gaTagId}`;
script.onload = () => {
// configure GA after script loads
const consentStatus = getConsentStatus();
const analyticsConsent =
consentStatus === 'accepted' ? 'granted' : 'denied';
window.gtag('config', gaTagId, {
send_page_view: true,
anonymize_ip: true,
});
// update consent mode based on current status
window.gtag('consent', 'update', {
analytics_storage: analyticsConsent,
ad_storage: 'denied',
});
// send page_view event after script is ready
if (analyticsConsent === 'granted') {
waitForGAReady(() => {
sendPageView();
});
}
};
document.head.appendChild(script);
}
function sendPageView(): void {
if (typeof window === 'undefined' || !window.gtag) {
return;
}
// send page_view event with proper GA4 format for real-time tracking
trackAnalyticsEvent('page_view', {
page_title: document.title,
page_location: window.location.href,
page_path: window.location.pathname,
});
sendClientViewerVersionEvent();
}
function waitForGAReady(callback: () => void): void {
if (typeof window === 'undefined') {
return;
}
// check if GA script exists
const script = document.querySelector(
'script[src*="googletagmanager.com/gtag/js"]',
);
if (!script || typeof window.gtag !== 'function' || !window.dataLayer) {
// if script not loaded yet, wait for window load event
if (document.readyState === 'loading') {
window.addEventListener('load', () => {
setTimeout(callback, 300);
});
} else {
// document already loaded, wait a bit for GA to initialize
setTimeout(() => {
if (typeof window.gtag === 'function' && window.dataLayer) {
callback();
}
}, 300);
}
return;
}
// script exists, wait for GA to fully initialize
// if document is already loaded, GA should be ready soon
if (document.readyState === 'complete') {
setTimeout(callback, 200);
} else {
// wait for document to finish loading first
window.addEventListener('load', () => {
setTimeout(callback, 200);
});
}
}
export function updateAnalyticsConsent(consentStatus: ConsentStatus): void {
if (typeof window === 'undefined' || !window.dataLayer) {
return;
}
const analyticsConsent = consentStatus === 'accepted' ? 'granted' : 'denied';
// update consent mode
if (window.gtag) {
window.gtag('consent', 'update', {
analytics_storage: analyticsConsent,
ad_storage: 'denied',
});
// if consent granted, send page_view event after GA is ready
if (analyticsConsent === 'granted') {
waitForGAReady(() => {
sendPageView();
});
}
} else {
// queue consent update if gtag not ready yet
window.dataLayer.push([
'consent',
'update',
{
analytics_storage: analyticsConsent,
ad_storage: 'denied',
},
]);
// if consent granted, queue page_view for when GA loads
if (analyticsConsent === 'granted') {
waitForGAReady(() => {
sendPageView();
});
}
}
}
export function getGaTagIdFromMeta(): string | null {
const metaTag = document.querySelector('meta[name="ga-tag-id"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
// fallback: try to extract from any existing script tags
const scripts = document.querySelectorAll(
'script[src*="googletagmanager.com/gtag/js"]',
);
for (const script of scripts) {
const src = script.getAttribute('src');
if (src) {
const match = src.match(/id=([^&]+)/);
if (match && match[1] !== GA_TAG_PLACEHOLDER) {
return match[1];
}
}
}
return null;
}
function getClientViewerVersion(): string | null {
if (typeof window === 'undefined') {
return null;
}
const metaTag = document.querySelector('meta[name="client-viewer-version"]');
if (!metaTag) {
return null;
}
const version = metaTag.getAttribute('content');
if (!version || version === CLIENT_VIEWER_VERSION_PLACEHOLDER) {
return null;
}
return version;
}
function sendClientViewerVersionEvent(): void {
if (versionEventSent || typeof window === 'undefined' || !window.gtag) {
return;
}
const version = getClientViewerVersion();
if (!version) {
return;
}
versionEventSent = true;
trackAnalyticsEvent('client_viewer_version', {
client_viewer_version: version,
});
}
export function trackAnalyticsEvent(
eventName: string,
params: AnalyticsEventParams = {},
): void {
if (typeof window === 'undefined') {
return;
}
if (typeof window.gtag === 'function') {
window.gtag('event', eventName, params);
return;
}
if (
window.dataLayer &&
typeof (window.dataLayer as unknown[]).push === 'function'
) {
(window.dataLayer as unknown[]).push(['event', eventName, params]);
}
}
================================================
FILE: src/client-viewer/src/utils/gaRequestInterceptor.ts
================================================
import { getConsentStatus } from './analytics';
const GA_DOMAINS = [
'google-analytics.com',
'googletagmanager.com',
'google-analytics.co',
'analytics.google.com',
'region1.google-analytics.com',
'region2.google-analytics.com',
'region3.google-analytics.com',
'region4.google-analytics.com',
'region5.google-analytics.com',
'region6.google-analytics.com',
'region7.google-analytics.com',
'region8.google-analytics.com',
'region9.google-analytics.com',
'region10.google-analytics.com',
'region11.google-analytics.com',
'region12.google-analytics.com',
'region13.google-analytics.com',
'region14.google-analytics.com',
'region15.google-analytics.com',
'region16.google-analytics.com',
'region17.google-analytics.com',
'region18.google-analytics.com',
'region19.google-analytics.com',
'region20.google-analytics.com',
];
function isGoogleAnalyticsUrl(url: string): boolean {
try {
const urlObj = new URL(url, window.location.href);
const hostname = urlObj.hostname.toLowerCase();
return GA_DOMAINS.some(
(domain) => hostname === domain || hostname.endsWith('.' + domain),
);
} catch {
return false;
}
}
function shouldBlockRequest(): boolean {
const consentStatus = getConsentStatus();
return consentStatus !== 'accepted';
}
function isLocalIP(ip: string): boolean {
const parts = ip.split('.').map(Number);
if (parts.length !== 4 || parts.some(isNaN)) {
return false;
}
// 127.0.0.0/8
if (parts[0] === 127) {
return true;
}
// 10.0.0.0/8
if (parts[0] === 10) {
return true;
}
// 172.16.0.0/12
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {
return true;
}
// 192.168.0.0/16
if (parts[0] === 192 && parts[1] === 168) {
return true;
}
return false;
}
function sanitizeGAUrl(url: string): string {
try {
const urlObj = new URL(url);
// only sanitize /g/collect requests
if (!urlObj.pathname.includes('/g/collect')) {
return url;
}
const dlParam = urlObj.searchParams.get('dl');
if (!dlParam) {
return url;
}
try {
const dlUrl = new URL(decodeURIComponent(dlParam));
const hostname = dlUrl.hostname;
// check if hostname is a local IP address
if (isLocalIP(hostname)) {
urlObj.searchParams.set('dl', encodeURIComponent('http://localhost'));
return urlObj.toString();
}
} catch {
// if dl parameter is not a valid URL, leave it as is
}
return url;
} catch {
return url;
}
}
let originalFetch: typeof fetch;
let originalXHROpen: typeof XMLHttpRequest.prototype.open;
let originalXHRSend: typeof XMLHttpRequest.prototype.send;
let originalSendBeacon: typeof navigator.sendBeacon;
function interceptFetch(): void {
if (typeof window === 'undefined' || window.fetch === originalFetch) {
return;
}
originalFetch = window.fetch;
window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
let url =
typeof input === 'string'
? input
: input instanceof Request
? input.url
: '';
if (isGoogleAnalyticsUrl(url)) {
if (shouldBlockRequest()) {
return Promise.reject(
new Error(
'Google Analytics request blocked: user consent not granted',
),
);
}
// sanitize URL before making request
url = sanitizeGAUrl(url);
// if input was a Request object, we need to create a new one with sanitized URL
if (input instanceof Request) {
input = new Request(url, init || input);
} else {
input = url;
}
}
return originalFetch.call(this, input, init);
};
}
function interceptXMLHttpRequest(): void {
if (typeof window === 'undefined' || !window.XMLHttpRequest) {
return;
}
const XHR = window.XMLHttpRequest;
if (XHR.prototype.open === originalXHROpen) {
return;
}
originalXHROpen = XHR.prototype.open;
originalXHRSend = XHR.prototype.send;
XHR.prototype.open = function (...args: unknown[]) {
let url = args[1] as string | URL;
let urlString = typeof url === 'string' ? url : url.toString();
if (isGoogleAnalyticsUrl(urlString)) {
if (shouldBlockRequest()) {
throw new Error(
'Google Analytics request blocked: user consent not granted',
);
}
// sanitize URL before making request
urlString = sanitizeGAUrl(urlString);
url = urlString;
args[1] = url;
}
(this as XMLHttpRequest & { _interceptedUrl?: string })._interceptedUrl =
urlString;
return (originalXHROpen as (...args: unknown[]) => void).apply(this, args);
};
XHR.prototype.send = function (...args) {
const url =
(this as XMLHttpRequest & { _interceptedUrl?: string })._interceptedUrl ||
'';
if (isGoogleAnalyticsUrl(url) && shouldBlockRequest()) {
return;
}
return originalXHRSend.apply(this, args);
};
}
function interceptSendBeacon(): void {
if (
typeof window === 'undefined' ||
!navigator.sendBeacon ||
navigator.sendBeacon === originalSendBeacon
) {
return;
}
originalSendBeacon = navigator.sendBeacon;
navigator.sendBeacon = function (
url: string | URL,
data?: BodyInit | null,
): boolean {
let urlString = typeof url === 'string' ? url : url.toString();
if (isGoogleAnalyticsUrl(urlString)) {
if (shouldBlockRequest()) {
return false;
}
// sanitize URL before making request
urlString = sanitizeGAUrl(urlString);
url = urlString;
}
return originalSendBeacon.call(this, url, data);
};
}
export function initializeGARequestInterceptor(): void {
if (typeof window === 'undefined') {
return;
}
interceptFetch();
interceptXMLHttpRequest();
interceptSendBeacon();
}
================================================
FILE: src/client-viewer/src/utils/message.ts
================================================
import type { LocalPeerUser } from '../../../common/LocalPeerUser';
import type { SendEncryptedMessagePayload } from '../../../common/SendEncryptedMessagePayload';
export interface ProcessedPayload {
toSend: ProcessedMessage;
original: ProcessedMessage;
}
export const process = (
payload: ReceiveEncryptedMessagePayload,
): Promise => Promise.resolve(payload as ProcessedMessage);
export const prepare = (
payload: SendEncryptedMessagePayload,
user: LocalPeerUser,
): Promise =>
new Promise((resolve) => {
const myUsername = user.username;
const myId = user.id;
const innerPayload = { ...payload.payload } as Record;
if (typeof (innerPayload as { text?: unknown }).text === 'string') {
(innerPayload as { text?: string }).text = encodeURI(
(innerPayload as { text?: string }).text as string,
);
}
const jsonToSend = {
...payload,
payload: {
...innerPayload,
sender: myId,
username: myUsername,
},
} as ProcessedMessage;
resolve({
toSend: jsonToSend,
original: jsonToSend,
});
});
================================================
FILE: src/client-viewer/src/utils/playerFullscreen.ts
================================================
import screenfull from 'screenfull';
import { PLAYER_WRAPPER_ID } from '../constants/appConstants';
type LegacyFullscreenElement = HTMLElement & {
webkitRequestFullscreen?: () => Promise | void;
webkitRequestFullScreen?: () => Promise | void;
mozRequestFullScreen?: () => Promise | void;
msRequestFullscreen?: () => Promise | void;
};
type LegacyFullscreenDocument = Document & {
webkitExitFullscreen?: () => Promise | void;
mozCancelFullScreen?: () => Promise | void;
msExitFullscreen?: () => Promise | void;
webkitFullscreenElement?: Element | null;
mozFullScreenElement?: Element | null;
msFullscreenElement?: Element | null;
};
type IOSVideoElement = HTMLVideoElement & {
webkitEnterFullscreen?: () => void;
webkitExitFullscreen?: () => void;
webkitSupportsFullscreen?: boolean;
webkitDisplayingFullscreen?: boolean;
};
type Unsubscribe = () => void;
const fullscreenEventNames = [
'fullscreenchange',
'webkitfullscreenchange',
'MSFullscreenChange',
'mozfullscreenchange',
];
const getPlayerContainer = (): HTMLElement | null => {
return document.getElementById(PLAYER_WRAPPER_ID);
};
const getPlayerVideo = (): IOSVideoElement | null => {
const container = getPlayerContainer();
if (!container) return null;
const maybeVideo = container.querySelector('video');
if (!(maybeVideo instanceof HTMLVideoElement)) return null;
return maybeVideo as IOSVideoElement;
};
const requestStandardFullscreen = (element: HTMLElement | null): boolean => {
if (!element) return false;
const target = element as LegacyFullscreenElement;
const request =
target.requestFullscreen ||
target.webkitRequestFullscreen ||
target.webkitRequestFullScreen ||
target.mozRequestFullScreen ||
target.msRequestFullscreen;
if (typeof request !== 'function') return false;
request.call(target);
return true;
};
const exitStandardFullscreen = (): boolean => {
const doc = document as LegacyFullscreenDocument;
const exit =
doc.exitFullscreen ||
doc.webkitExitFullscreen ||
doc.mozCancelFullScreen ||
doc.msExitFullscreen;
if (typeof exit !== 'function') return false;
exit.call(doc);
return true;
};
export const isPlayerFullscreen = (): boolean => {
const doc = document as LegacyFullscreenDocument;
if (
doc.fullscreenElement ||
doc.webkitFullscreenElement ||
doc.mozFullScreenElement ||
doc.msFullscreenElement
) {
return true;
}
const video = getPlayerVideo();
if (!video) return false;
if (typeof video.webkitDisplayingFullscreen === 'boolean') {
return video.webkitDisplayingFullscreen;
}
return false;
};
export const enterPlayerFullscreen = (): boolean => {
const container = getPlayerContainer();
if (container && screenfull.isEnabled) {
screenfull.request(container);
return true;
}
if (requestStandardFullscreen(container)) return true;
const video = getPlayerVideo();
if (requestStandardFullscreen(video)) return true;
if (video && typeof video.webkitEnterFullscreen === 'function') {
if (
typeof video.webkitSupportsFullscreen === 'boolean' &&
!video.webkitSupportsFullscreen
) {
return false;
}
video.webkitEnterFullscreen();
return true;
}
return false;
};
export const exitPlayerFullscreen = (): boolean => {
if (screenfull.isEnabled && screenfull.isFullscreen) {
screenfull.exit();
return true;
}
if (exitStandardFullscreen()) return true;
const video = getPlayerVideo();
if (video && typeof video.webkitExitFullscreen === 'function') {
if (
typeof video.webkitDisplayingFullscreen === 'boolean' &&
!video.webkitDisplayingFullscreen
) {
return false;
}
video.webkitExitFullscreen();
return true;
}
return false;
};
export const togglePlayerFullscreen = (): 'entered' | 'exited' | 'failed' => {
if (isPlayerFullscreen()) {
return exitPlayerFullscreen() ? 'exited' : 'failed';
}
return enterPlayerFullscreen() ? 'entered' : 'failed';
};
export const subscribeToPlayerFullscreenChange = (
listener: (isFullscreen: boolean) => void,
): Unsubscribe => {
const handleChange = () => listener(isPlayerFullscreen());
const handleVideoBegin = () => listener(true);
const handleVideoEnd = () => listener(false);
if (screenfull.isEnabled) {
screenfull.on('change', handleChange);
}
fullscreenEventNames.forEach((eventName) => {
document.addEventListener(eventName, handleChange);
});
let currentVideo: IOSVideoElement | null = null;
const attachVideoListeners = (video: IOSVideoElement | null) => {
if (currentVideo === video) return;
if (currentVideo) {
currentVideo.removeEventListener(
'webkitbeginfullscreen',
handleVideoBegin,
);
currentVideo.removeEventListener('webkitendfullscreen', handleVideoEnd);
}
currentVideo = video;
if (currentVideo) {
currentVideo.addEventListener('webkitbeginfullscreen', handleVideoBegin);
currentVideo.addEventListener('webkitendfullscreen', handleVideoEnd);
}
};
attachVideoListeners(getPlayerVideo());
const container = getPlayerContainer();
let observer: MutationObserver | null = null;
if (container) {
observer = new MutationObserver(() => {
attachVideoListeners(getPlayerVideo());
});
observer.observe(container, { childList: true, subtree: true });
}
listener(isPlayerFullscreen());
return () => {
if (screenfull.isEnabled) {
screenfull.off('change', handleChange);
}
fullscreenEventNames.forEach((eventName) => {
document.removeEventListener(eventName, handleChange);
});
if (currentVideo) {
currentVideo.removeEventListener(
'webkitbeginfullscreen',
handleVideoBegin,
);
currentVideo.removeEventListener('webkitendfullscreen', handleVideoEnd);
}
if (observer) {
observer.disconnect();
}
};
};
================================================
FILE: src/client-viewer/src/utils/socket.ts
================================================
import socketIO, { Socket } from 'socket.io-client';
import { generateUrl } from '../api/generator';
let socket: Socket;
export const connect = (roomId: string) => {
socket = socketIO(generateUrl(), {
query: {
roomId,
},
forceNew: true,
});
return socket;
};
export const getSocket = () => socket;
================================================
FILE: src/client-viewer/src/utils/userAgentParserHelpers.ts
================================================
// @ts-ignore
import { UAParser } from 'ua-parser-js';
export function getOSFromUAParser(uaParser: UAParser) {
const osFromUAParser = uaParser.getResult().os;
return `${osFromUAParser.name ? osFromUAParser.name : ''} ${
osFromUAParser.version ? osFromUAParser.version : ''
}`;
}
export function getDeviceTypeFromUAParser(uaParser: UAParser) {
const deviceTypeFromUAParser = uaParser.getResult().device;
return deviceTypeFromUAParser.type
? deviceTypeFromUAParser.type.toString()
: 'computer';
}
export function getBrowserFromUAParser(uaParser: UAParser) {
const browserFromUAParser = uaParser.getResult().browser;
return `${browserFromUAParser.name ? browserFromUAParser.name : ''} ${
browserFromUAParser.version ? browserFromUAParser.version : ''
}`;
}
================================================
FILE: src/client-viewer/src/vite-env.d.ts
================================================
///
type ConnectionIconType =
| ConnectionIconEnum.FEED
| ConnectionIconEnum.FEED_SUBSCRIBED;
type LoadingSharingIconType =
| LoadingSharingIconEnum.DESKTOP
| LoadingSharingIconEnum.APPLICATION;
type ScreenSharingSourceType =
| ScreenSharingSourceEnum.SCREEN
| ScreenSharingSourceEnum.WINDOW;
type CreatePeerConnectionUseEffectParams = {
connectionRoomId: string;
peer: undefined | PeerConnection;
setMyDeviceDetails: (_: DeviceDetails) => void;
setConnectionIconType: (_: ConnectionIconType) => void;
setIsShownTextPrompt: (_: boolean) => void;
setPromptStep: (_: number) => void;
setScreenSharingSourceType: (_: ScreenSharingSourceType) => void;
setDialogErrorMessage: (_: ErrorMessage) => void;
setIsErrorDialogOpen: (_: boolean) => void;
setUrl: (_: MediaStream | null) => void;
setPeer: (_: undefined | PeerConnection) => void;
};
type handleDisplayingLoadingSharingIconLoopParams = {
promptStep: number;
url: MediaStream | null;
setIsShownLoadingSharingIcon: (_: boolean) => void;
loadingSharingIconType: LoadingSharingIconType;
isShownLoadingSharingIcon: boolean;
setLoadingSharingIconType: (_: LoadingSharingIconType) => void;
};
interface Document {
/**
* Indicates whether the document is currently in the process of prerendering.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering
* @see https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering
*/
prerendering?: boolean;
/**
* An event handler for the prerenderingchange event, which is fired when
* a prerendered document is activated.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/prerenderingchange_event
*/
onprerenderingchange?: ((this: Document, ev: Event) => void) | null;
}
================================================
FILE: src/client-viewer/tsconfig.app.json
================================================
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
================================================
FILE: src/client-viewer/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: src/client-viewer/tsconfig.node.json
================================================
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: src/client-viewer/vite.config.ts
================================================
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import type { Plugin } from 'vite';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
interface PackageJson {
version?: string;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = JSON.parse(
readFileSync(new URL('./package.json', import.meta.url), 'utf-8'),
) as PackageJson;
const clientViewerVersion =
process.env.VITE_CLIENT_VIEWER_VERSION || packageJson.version || '';
// load GA interceptor script from separate file
const gaInterceptorScript = readFileSync(
join(__dirname, 'scripts', 'ga-interceptor.js'),
'utf-8',
);
// plugin to replace html placeholders with env variables and inject GA interceptor
const replaceHtmlEnvPlugin = (): Plugin => {
return {
name: 'replace-html-env',
transformIndexHtml(html) {
const gaTagId = process.env.VITE_CLIENT_VIEWER_GA_TAG || '';
let transformed = html
.replace(/%VITE_CLIENT_VIEWER_GA_TAG%/g, gaTagId)
.replace(/%VITE_CLIENT_VIEWER_VERSION%/g, clientViewerVersion);
// inject GA interceptor script before GA script loads
if (
transformed.includes(
'\n