[
  {
    "path": ".github/FUNDING.yml",
    "content": "# TODO: edit funding when ready\n\n# These are supported funding model platforms\n\n# github: [deskreen, pavlobu]\npatreon: deskreen\nopen_collective: deskreen\ngithub: pavlobu\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-Bug_report.md",
    "content": "---\nname: Bug report\nabout: You're having technical issues. 🐞 And willing to share more details. If you don't know details, write here https://github.com/pavlobu/deskreen/discussions/68\nlabels: 'bug'\n---\n\n<!-- Please use the following issue template or your issue will be closed -->\n\n## Prerequisites\n\n<!-- If the following boxes are not ALL checked, your issue is likely to be closed -->\n\n- [ ] Using yarn\n- [ ] Using an up-to-date [`master` branch](https://github.com/pavlobu/deskreen/tree/master)\n- [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/)\n- [ ] For issue in production release, add devtools output of `DEBUG_PROD=true yarn build && yarn start`\n\n## Expected Behavior\n\n<!--- What should have happened? -->\n\n## Current Behavior\n\n<!--- What went wrong? -->\n\n## Steps to Reproduce\n\n<!-- Add relevant code and/or a live example -->\n<!-- Add stack traces -->\n\n1.\n\n2.\n\n3.\n\n4.\n\n## Possible Solution (Not obligatory)\n\n<!--- Suggest a reason for the bug or how to fix it. -->\n\n## Context\n\n<!--- How has this issue affected you? What are you trying to accomplish? -->\n<!--- Did you make any changes to the boilerplate after cloning it? -->\n<!--- Providing context helps us come up with a solution that is most useful in the real world -->\n\n## Your Environment\n\n<!--- Include as many relevant details about the environment you experienced the bug in -->\n\n- Node version :\n- Deskreen version or branch :\n- Operating System and version :\n- Link to your project :\n\n<!---\n❗️❗️ Also, please consider donating (https://opencollective.com/deskreen) ❗️❗️\n\nDonations will ensure the following:\n\n🔨 Long term maintenance of the project\n🛣 Progress on the roadmap\n🐛 Quick responses to bug reports and help requests\n -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3-Feature_request.md",
    "content": "---\nname: Feature request\nabout: You want something small added to Deskreen and with concrete code and solution. 🎉 If it is a big enhancement drop it here https://github.com/pavlobu/deskreen/discussions/50\nlabels: 'enhancement'\n---\n\n# Here you post only concrete examples of enhancements with code and solutions that you have. Other BIG enhancements and general ideas of features you would like to see in Deskreen, you post here: https://github.com/pavlobu/deskreen/discussions/50\n\n# Otherwise this issue will be closed.\n\n<!---\n❗️❗️ Also, please consider donating (https://opencollective.com/deskreen) ❗️❗️\n\nDonations will ensure the following:\n\n🔨 Long term maintenance of the project\n🛣 Progress on the roadmap\n🐛 Quick responses to bug reports and help requests\n -->\n"
  },
  {
    "path": ".github/config.yml",
    "content": "requiredHeaders:\n  - Prerequisites\n  - Expected Behavior\n  - Current Behavior\n  - Possible Solution\n  - Your Environment\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pr\n  - discussion\n  - e2e\n  - enhancement\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "on:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\nname: release all os — mac signed & notarized, windows, linux\n\npermissions:\n  contents: write # Required to create releases and upload assets\n\njobs:\n  create-release:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Create GitHub Release (draft)\n        id: create_release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          tag=\"${{ github.ref_name }}\"\n          echo \"Ensuring draft release for tag: $tag\"\n          if gh release view \"$tag\" >/dev/null 2>&1; then\n            echo \"Release $tag already exists, skipping creation\"\n          else\n            gh release create \"$tag\" \\\n              --title \"$tag\" \\\n              --draft \\\n              --notes \"\"\n          fi\n\n  release:\n    name: Release (${{ matrix.build_name }})\n    needs: create-release\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            platform: linux\n            build_name: linux-x64\n            build_command: pnpm build:linux\n            artifact_name: 'dist/{*.AppImage,*.rpm,*.deb,*.yml}'\n          - os: ubuntu-latest\n            platform: linux\n            build_name: linux-arm64\n            build_command: pnpm build:linux:arm64\n            artifact_name: 'dist/{*.AppImage,*.rpm,*.deb,*.yml}'\n          - os: ubuntu-latest\n            platform: linux\n            build_name: linux-arm32\n            build_command: pnpm build:linux:armv7l\n            artifact_name: 'dist/{*.AppImage,*.rpm,*.deb,*.yml}'\n          - os: windows-latest\n            platform: windows\n            build_name: windows-amd64\n            build_command: pnpm build:win\n            artifact_name: 'dist/{*.msi,*.exe,*.blockmap,*.yml}'\n          - os: windows-latest\n            platform: windows\n            build_name: windows-amd32\n            build_command: pnpm build:win:ia32\n            artifact_name: 'dist/{*.msi,*.exe,*.blockmap,*.yml}'\n          - os: windows-latest\n            platform: windows\n            build_name: windows-arm64\n            build_command: pnpm build:win:arm64\n            artifact_name: 'dist/{*.msi,*.exe,*.blockmap,*.yml}'\n          - os: macos-latest\n            platform: macos\n            build_name: macos-arm64\n            build_command: pnpm build:mac:arm64\n            artifact_name: 'dist/{*.dmg,*.blockmap,*.yml}'\n          - os: macos-15-intel\n            platform: macos\n            build_name: macos-intel\n            build_command: pnpm build:mac:x64\n            artifact_name: 'dist/{*.dmg,*.blockmap,*.yml}'\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '23'\n\n      - name: Install pnpm\n        run: npm install -g pnpm\n\n      - name: pnpm install in ./src/client-viewer\n        run: |\n          cd ./src/client-viewer\n          pnpm install\n\n      - name: pnpm install in ./\n        run: pnpm install\n\n      - name: Install Linux build dependencies\n        if: ${{ matrix.platform == 'linux' }}\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libfuse2\n\n      - name: Create .env file for client-viewer with Google Analytics tag (macOS)\n        if: ${{ matrix.platform == 'macos' }}\n        run: |\n          echo \"VITE_CLIENT_VIEWER_GA_TAG=${{ secrets.CLIENT_VIEWER_GA_TAG_MACOS }}\" > ./src/client-viewer/.env\n\n      - name: Create .env file for client-viewer with Google Analytics tag (Windows)\n        if: ${{ matrix.platform == 'windows' }}\n        run: |\n          echo \"VITE_CLIENT_VIEWER_GA_TAG=${{ secrets.CLIENT_VIEWER_GA_TAG_WINDOWS }}\" > ./src/client-viewer/.env\n\n      - name: Create .env file for client-viewer with Google Analytics tag (Linux)\n        if: ${{ matrix.platform == 'linux' }}\n        run: |\n          echo \"VITE_CLIENT_VIEWER_GA_TAG=${{ secrets.CLIENT_VIEWER_GA_TAG_LINUX }}\" > ./src/client-viewer/.env\n\n      - name: pnpm build on Windows\n        if: ${{ matrix.platform == 'windows' }}\n        shell: pwsh\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}\n          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}\n          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}\n        run: |\n          # 1. Pre-install the Azure Trusted Signing module\n          # This prevents electron-builder from running the buggy 'install' sub-routine\n          Set-PSRepository -Name PSGallery -InstallationPolicy Trusted\n          Install-Module -Name TrustedSigning -AllowPrerelease -Force -Scope CurrentUser\n          \n          $ErrorActionPreference = 'Stop'\n          $attempts = 3\n          for ($i = 1; $i -le $attempts; $i++) {\n            Write-Host \"Build attempt $($i)/$($attempts)\"\n            \n            # Execute the pnpm script directly in the shell\n            # We use 'pnpm run' instead of 'cmd /c' to avoid extra shell escaping layers\n            pnpm run ${{ matrix.build_command == 'pnpm build:win' && 'build:win' || (matrix.build_command == 'pnpm build:win:ia32' && 'build:win:ia32' || 'build:win:arm64') }}\n            \n            if ($LASTEXITCODE -eq 0) { exit 0 }\n            if ($i -lt $attempts) {\n              Write-Host \"Build failed. Retrying in 20s...\"\n              Start-Sleep -Seconds 20\n            }\n          }\n          exit $LASTEXITCODE\n\n      - name: pnpm build on macOS\n        if: ${{ matrix.platform == 'macos' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          # certificate for signing (base64-encoded .p12) and its password\n          CSC_LINK: ${{ secrets.MAC_CERTS }}\n          CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}\n        run: ${{ matrix.build_command }}\n\n      - name: pnpm build on Linux\n        if: ${{ matrix.platform == 'linux' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: ${{ matrix.build_command }}\n\n      - name: Rename Linux update manifest for non-x64\n        if: ${{ matrix.platform == 'linux' }}\n        run: |\n          if [ -f dist/latest-linux.yml ]; then\n            mv dist/latest-linux.yml dist/latest-${{ matrix.build_name }}.yml\n          fi\n\n      - name: Rename Windows update manifest for non-x64\n        if: ${{ matrix.platform == 'windows' }}\n        shell: pwsh\n        run: |\n          $manifest = Join-Path dist 'latest.yml'\n          if (Test-Path $manifest) {\n            Rename-Item $manifest \"latest-${{ matrix.build_name }}.yml\"\n          }\n\n      - name: Rename macOS update manifest for arm builds\n        if: ${{ matrix.platform == 'macos' }}\n        run: |\n          if [ -f dist/latest-mac.yml ]; then\n            mv dist/latest-mac.yml dist/latest-${{ matrix.build_name }}.yml\n          fi\n\n      - name: Notarize and staple macOS DMG\n        if: ${{ matrix.platform == 'macos' }}\n        env:\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n        run: |\n          shopt -s nullglob\n          for dmg in dist/*.dmg; do\n            echo \"Submitting $dmg for notarization\"\n            xcrun notarytool submit \"$dmg\" \\\n              --apple-id \"$APPLE_ID\" \\\n              --team-id \"$APPLE_TEAM_ID\" \\\n              --password \"$APPLE_APP_SPECIFIC_PASSWORD\" \\\n              --wait\n            echo \"Stapling ticket to $dmg\"\n            xcrun stapler staple \"$dmg\"\n          done\n\n      - name: Upload binaries to release\n        uses: xresloader/upload-to-github-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          file: ${{ matrix.artifact_name }}\n          tags: true\n          draft: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".history\nnode_modules\ndist\nout\n.DS_Store\n.pnpm-store\n.eslintcache\n*.log*\nsrc/client-viewer/dist\nsrc/client-viewer/node_modules\n\n.idea\n.env\ntsconfig.web.tsbuildinfo\nsrc/client-viewer/.env\n\nAGENTS.md"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 1.0.11 (8 Mar 2021)\n\nChanges:\n\n- add Danish language translations. Special thanks to @LauritsLL\n- add German language translations. Special thanks to @Aceto1\n\n<br/>\n<br/>\n\n# 1.0.10 (25 Feb 2021)\n\nChanges:\n\n- add Spanish language translations. Special thanks to @schiappa\n\n<br/>\n<br/>\n\n# 1.0.9 (24 Feb 2021)\n\nChanges:\n\n- remove severe Windows UI lags.\n- Increase performance on weak Windows machines.\n\n<br/>\n<br/>\n\n# 1.0.8 (23 Feb 2021)\n\nChanges:\n\n- added locales for Traditional Chinese language, special thanks to @taotieren\n- minor UI improvements\n\n<br/>\n<br/>\n\n# 1.0.7 (21 Feb 2021)\n\nChanges:\n\n- added locales for Chinese language, special thanks to @taotieren\n- minor UI improvements\n\n<br/>\n<br/>\n\n# 1.0.6 (20 Feb 2021)\n\nChanges:\n\n- added locales for Russian and Ukrainian languages\n- minor UI improvements\n\n<br/>\n<br/>\n\n# 1.0.5 (7 Feb 2021)\n\nChanges:\n\n- Fix quality is not set to 100% when sharing started: https://github.com/pavlobu/deskreen/issues/100\n\n<br/>\n<br/>\n\n# 1.0.4 (4 Feb 2021)\n\nChanges:\n\n- add Flip button in client web view (gear icon button, next to play button) for use a tablet as a teleprompter\n- set 100% video quality by default when screen sharing starts\n- add polyfills to remove issue of blank pages on old browsers. Thanks @klarkc\n- display error message in client viewer if there is WebRTC error\n\n<br/>\n<br/>\n\n# 1.0.3 (29 Jan 2021)\n\nChanges:\n\n- remove pulsing animation on orange step bubbles\n- portable version on windows\n- release draft before publishing release\n- remove non-working macOS zip from release\n\n<br/>\n<br/>\n\n# 1.0.2 (27 Jan 2021)\n\nChanges:\n\n- Security patches\n- Updated electron version to 11.2.1\n- fix for https://github.com/pavlobu/deskreen/issues/45\n- ? fix for https://github.com/pavlobu/deskreen/issues/56\n- ? fix for https://github.com/pavlobu/deskreen/issues/17\n\n<br/>\n<br/>\n\n# 1.0.1 (25 Jan 2021)\n\nChanges:\n\n- Fix typos in Deskreen. Special thanks to @EdwardBetts\n\n<br/>\n<br/>\n\n# 1.0.0 (18 Jan 2021)\n\nFeatures:\n\n- works with WiFi or LAN\n- use any device with web browser as second screen for your computer (using Display Dummy Plug)\n- use any device web browser to mirror your computer's screen\n- use any device web browser to view a single application window from your computer's screen\n- supports multiple screen sharing sessions to as many devices as you want\n- supports changing picture quality while sharing a screen.\n- Picture auto quality change supported. (for performance boost while watching youtube video for example)\n- End-to-end security\n- dark mode UI support\n- available for Win / Mac / Linux\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n- Using welcoming and inclusive language\n- Being respectful of differing viewpoints and experiences\n- Gracefully accepting constructive criticism\n- Focusing on what is best for the community\n- Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n- The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n- Trolling, insulting/derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at pavlobu@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "# Deskreen CE (Community Edition)\n\n![platform](https://img.shields.io/badge/platform-Windows%20%7C%20MacOS%20%7C%20Linux-lightgrey)\n(Over 2M downloads during 5 years since launch)\n\n![Deskreen Logo](https://raw.githubusercontent.com/pavlobu/deskreen/master/resources/icon.png)\n\n## Deskreen turns any device with a web browser into a secondary screen for your computer\n\n## To learn more visit our website: [deskreen.com](https://deskreen.com)\n\n## [Donate to support Deskreen Open-Source](https://deskreen.com/#contribute)\n\nDeskreen 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.\nThe 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.\n\n---\n\n### ▶️ [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)\n\n---\n\n## [Deskreen Frequently Asked Questions](https://deskreen.com/faq)\n\n---\n\n### Prerequisites\n\nYou will need to have `node>=v23` `pnpm>=v10.20.0` installed.\n\n\n1. git clone this repo\n2. `pnpm i`\n3. `cd ./src/client-viewer && pnpm i && cd ../..`\n4. `pnpm clean && pnpm build && pnpm start` -- run in prod like mode\n\n#### for more pnpm commands look at `package.json`\n\n## Starting with Custom Local IP\n\nYou 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.\n\n### macOS\n\n```bash\n# Using open command (recommended)\nopen -a \"Deskreen CE\" --args --ip 192.168.1.100\n\n# Or using the executable directly\n/Applications/Deskreen\\ CE.app/Contents/MacOS/Deskreen\\ CE --ip 192.168.1.100\n\n# Get your IP automatically and launch\nopen -a \"Deskreen CE\" --args --ip \"192.168.1.100\"\n```\n\n### Windows\n\n```powershell\n# Using Start-Process (PowerShell)\nStart-Process \"Deskreen CE\" -ArgumentList \"--ip\", \"192.168.1.100\"\n\n# Or using the executable directly\n\"C:\\Program Files\\Deskreen CE\\Deskreen CE.exe\" --ip 192.168.1.100\n\n# Or from Command Prompt\nstart \"\" \"C:\\Program Files\\Deskreen CE\\Deskreen CE.exe\" --ip 192.168.1.100\n```\n\n### Linux\n\n```bash\n# If installed via AppImage\n./Deskreen\\ CE-*.AppImage --ip 192.168.1.100\n\n# If installed via .deb/.rpm package (usually in /usr/bin or /opt)\ndeskreen-ce --ip 192.168.1.100\n\n# Or using full path\n/opt/Deskreen\\ CE/deskreen-ce --ip 192.168.1.100\n```\n\n**Note:** Replace `192.168.1.100` with your actual local IP address. You can find your IP using:\n- **macOS/Linux:** `ipconfig getifaddr en0` or `ifconfig | grep \"inet \"`\n- **Windows:** `ipconfig` (look for IPv4 Address)\n\nWhen 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.\n\n## Maintainer\n\n- [Pavlo (Paul) Buidenkov](https://www.linkedin.com/in/pavlobu)\n\n## License\n\nAGPL-3.0 License © [Pavlo (Paul) Buidenkov](https://github.com/pavlobu/deskreen)\n\n## Copyright\n\nElectron-Vite MIT License © [electron-vite](https://github.com/alex8088/electron-vite)\n\nReact MIT License © [Facebook, Inc. and its affiliates](https://github.com/facebook/react)\n\nVite MIT License © [Vite.js](https://github.com/vitejs/vite)\n\nElectron Builder MIT License © [electron-builder contributors](https://github.com/electron-userland/electron-builder)\n\nApache 2.0 © [blueprintjs](https://github.com/palantir/blueprint)\n\nsimple-peer MIT. Copyright © [Feross Aboukhadijeh](http://feross.org/)\n\ntweetnacl ISC License © Dmitry Chestnykh, Devi Mandiri, and contributors (https://github.com/dchest/tweetnacl-js)\n\ndarkwire.io MIT License © [darkwire/darkwire.io](https://github.com/darkwire/darkwire.io)\n\nAnd many many others...\n\n## Thanks\n\n🙏 Many thanks to all 🌍 open source community members and maintainers of libraries used in this project.\n"
  },
  {
    "path": "biome.json",
    "content": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.3.6/schema.json\",\n\t\"vcs\": { \"enabled\": true, \"clientKind\": \"git\", \"useIgnoreFile\": true },\n\t\"files\": { \"includes\": [\"**\", \"!!**/dist\"] },\n\t\"formatter\": { \"enabled\": true, \"indentStyle\": \"tab\" },\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": false,\n\t\t\t\"complexity\": {\n\t\t\t\t\"noAdjacentSpacesInRegex\": \"error\",\n\t\t\t\t\"noExtraBooleanCast\": \"error\",\n\t\t\t\t\"noUselessCatch\": \"error\",\n\t\t\t\t\"noUselessEscapeInRegex\": \"error\",\n\t\t\t\t\"noUselessTypeConstraint\": \"error\"\n\t\t\t},\n\t\t\t\"correctness\": {\n\t\t\t\t\"noChildrenProp\": \"error\",\n\t\t\t\t\"noConstAssign\": \"error\",\n\t\t\t\t\"noConstantCondition\": \"error\",\n\t\t\t\t\"noEmptyCharacterClassInRegex\": \"error\",\n\t\t\t\t\"noEmptyPattern\": \"error\",\n\t\t\t\t\"noGlobalObjectCalls\": \"error\",\n\t\t\t\t\"noInvalidBuiltinInstantiation\": \"error\",\n\t\t\t\t\"noInvalidConstructorSuper\": \"error\",\n\t\t\t\t\"noNonoctalDecimalEscape\": \"error\",\n\t\t\t\t\"noPrecisionLoss\": \"error\",\n\t\t\t\t\"noSelfAssign\": \"error\",\n\t\t\t\t\"noSetterReturn\": \"error\",\n\t\t\t\t\"noSwitchDeclarations\": \"error\",\n\t\t\t\t\"noUndeclaredVariables\": \"error\",\n\t\t\t\t\"noUnreachable\": \"error\",\n\t\t\t\t\"noUnreachableSuper\": \"error\",\n\t\t\t\t\"noUnsafeFinally\": \"error\",\n\t\t\t\t\"noUnsafeOptionalChaining\": \"error\",\n\t\t\t\t\"noUnusedLabels\": \"error\",\n\t\t\t\t\"noUnusedPrivateClassMembers\": \"error\",\n\t\t\t\t\"noUnusedVariables\": \"error\",\n\t\t\t\t\"useIsNan\": \"error\",\n\t\t\t\t\"useJsxKeyInIterable\": \"error\",\n\t\t\t\t\"useValidForDirection\": \"error\",\n\t\t\t\t\"useValidTypeof\": \"error\",\n\t\t\t\t\"useYield\": \"error\"\n\t\t\t},\n\t\t\t\"security\": { \"noDangerouslySetInnerHtmlWithChildren\": \"error\" },\n\t\t\t\"style\": {\n\t\t\t\t\"noCommonJs\": \"error\",\n\t\t\t\t\"noNamespace\": \"error\",\n\t\t\t\t\"noNonNullAssertion\": \"off\",\n\t\t\t\t\"useArrayLiterals\": \"error\",\n\t\t\t\t\"useAsConstAssertion\": \"error\",\n\t\t\t\t\"useBlockStatements\": \"off\"\n\t\t\t},\n\t\t\t\"suspicious\": {\n\t\t\t\t\"noAsyncPromiseExecutor\": \"error\",\n\t\t\t\t\"noCatchAssign\": \"error\",\n\t\t\t\t\"noClassAssign\": \"error\",\n\t\t\t\t\"noCommentText\": \"error\",\n\t\t\t\t\"noCompareNegZero\": \"error\",\n\t\t\t\t\"noConstantBinaryExpressions\": \"error\",\n\t\t\t\t\"noControlCharactersInRegex\": \"error\",\n\t\t\t\t\"noDebugger\": \"error\",\n\t\t\t\t\"noDuplicateCase\": \"error\",\n\t\t\t\t\"noDuplicateClassMembers\": \"error\",\n\t\t\t\t\"noDuplicateElseIf\": \"error\",\n\t\t\t\t\"noDuplicateJsxProps\": \"error\",\n\t\t\t\t\"noDuplicateObjectKeys\": \"error\",\n\t\t\t\t\"noDuplicateParameters\": \"error\",\n\t\t\t\t\"noEmptyBlockStatements\": \"error\",\n\t\t\t\t\"noExplicitAny\": \"error\",\n\t\t\t\t\"noExtraNonNullAssertion\": \"error\",\n\t\t\t\t\"noFallthroughSwitchClause\": \"error\",\n\t\t\t\t\"noFunctionAssign\": \"error\",\n\t\t\t\t\"noGlobalAssign\": \"error\",\n\t\t\t\t\"noImportAssign\": \"error\",\n\t\t\t\t\"noIrregularWhitespace\": \"error\",\n\t\t\t\t\"noMisleadingCharacterClass\": \"error\",\n\t\t\t\t\"noMisleadingInstantiator\": \"error\",\n\t\t\t\t\"noNonNullAssertedOptionalChain\": \"error\",\n\t\t\t\t\"noPrototypeBuiltins\": \"error\",\n\t\t\t\t\"noRedeclare\": \"error\",\n\t\t\t\t\"noShadowRestrictedNames\": \"error\",\n\t\t\t\t\"noSparseArray\": \"error\",\n\t\t\t\t\"noUnsafeDeclarationMerging\": \"error\",\n\t\t\t\t\"noUnsafeNegation\": \"error\",\n\t\t\t\t\"noUselessRegexBackrefs\": \"error\",\n\t\t\t\t\"noWith\": \"error\",\n\t\t\t\t\"useGetterReturn\": \"error\",\n\t\t\t\t\"useNamespaceKeyword\": \"error\"\n\t\t\t}\n\t\t},\n\t\t\"includes\": [\"**\", \"!**/node_modules\", \"!**/dist\", \"!**/out\"]\n\t},\n\t\"javascript\": {\n\t\t\"formatter\": { \"quoteStyle\": \"single\" },\n\t\t\"globals\": [\n\t\t\t\"onanimationend\",\n\t\t\t\"exports\",\n\t\t\t\"ongamepadconnected\",\n\t\t\t\"onlostpointercapture\",\n\t\t\t\"onanimationiteration\",\n\t\t\t\"onkeyup\",\n\t\t\t\"onmousedown\",\n\t\t\t\"onanimationstart\",\n\t\t\t\"onslotchange\",\n\t\t\t\"onprogress\",\n\t\t\t\"ontransitionstart\",\n\t\t\t\"onpause\",\n\t\t\t\"onended\",\n\t\t\t\"onpointerover\",\n\t\t\t\"onscrollend\",\n\t\t\t\"onformdata\",\n\t\t\t\"ontransitionrun\",\n\t\t\t\"onanimationcancel\",\n\t\t\t\"ondrag\",\n\t\t\t\"onchange\",\n\t\t\t\"onbeforeinstallprompt\",\n\t\t\t\"onbeforexrselect\",\n\t\t\t\"onmessage\",\n\t\t\t\"ontransitioncancel\",\n\t\t\t\"onpointerdown\",\n\t\t\t\"onabort\",\n\t\t\t\"onpointerout\",\n\t\t\t\"oncuechange\",\n\t\t\t\"ongotpointercapture\",\n\t\t\t\"onscrollsnapchanging\",\n\t\t\t\"onsearch\",\n\t\t\t\"onsubmit\",\n\t\t\t\"onstalled\",\n\t\t\t\"onsuspend\",\n\t\t\t\"onreset\",\n\t\t\t\"onerror\",\n\t\t\t\"onresize\",\n\t\t\t\"onmouseenter\",\n\t\t\t\"ongamepaddisconnected\",\n\t\t\t\"ondragover\",\n\t\t\t\"onbeforetoggle\",\n\t\t\t\"onmouseover\",\n\t\t\t\"onpagehide\",\n\t\t\t\"onmousemove\",\n\t\t\t\"onratechange\",\n\t\t\t\"oncommand\",\n\t\t\t\"onmessageerror\",\n\t\t\t\"onwheel\",\n\t\t\t\"ondevicemotion\",\n\t\t\t\"onauxclick\",\n\t\t\t\"ontransitionend\",\n\t\t\t\"onpaste\",\n\t\t\t\"onpageswap\",\n\t\t\t\"ononline\",\n\t\t\t\"ondeviceorientationabsolute\",\n\t\t\t\"onkeydown\",\n\t\t\t\"onclose\",\n\t\t\t\"onselect\",\n\t\t\t\"onpageshow\",\n\t\t\t\"onpointercancel\",\n\t\t\t\"onbeforematch\",\n\t\t\t\"onpointerrawupdate\",\n\t\t\t\"ondragleave\",\n\t\t\t\"onscrollsnapchange\",\n\t\t\t\"onseeked\",\n\t\t\t\"onwaiting\",\n\t\t\t\"onbeforeunload\",\n\t\t\t\"onplaying\",\n\t\t\t\"onvolumechange\",\n\t\t\t\"ondragend\",\n\t\t\t\"onstorage\",\n\t\t\t\"onloadeddata\",\n\t\t\t\"onfocus\",\n\t\t\t\"onoffline\",\n\t\t\t\"onplay\",\n\t\t\t\"onafterprint\",\n\t\t\t\"onclick\",\n\t\t\t\"oncut\",\n\t\t\t\"onmouseout\",\n\t\t\t\"ondblclick\",\n\t\t\t\"oncanplay\",\n\t\t\t\"onloadstart\",\n\t\t\t\"onappinstalled\",\n\t\t\t\"onpointermove\",\n\t\t\t\"ontoggle\",\n\t\t\t\"oncontextmenu\",\n\t\t\t\"onblur\",\n\t\t\t\"oncancel\",\n\t\t\t\"onbeforeprint\",\n\t\t\t\"oncontextrestored\",\n\t\t\t\"onloadedmetadata\",\n\t\t\t\"onpointerup\",\n\t\t\t\"onlanguagechange\",\n\t\t\t\"oncopy\",\n\t\t\t\"onselectstart\",\n\t\t\t\"onscroll\",\n\t\t\t\"onload\",\n\t\t\t\"ondragstart\",\n\t\t\t\"onbeforeinput\",\n\t\t\t\"oncanplaythrough\",\n\t\t\t\"oninput\",\n\t\t\t\"oninvalid\",\n\t\t\t\"ontimeupdate\",\n\t\t\t\"ondurationchange\",\n\t\t\t\"onselectionchange\",\n\t\t\t\"onmouseup\",\n\t\t\t\"location\",\n\t\t\t\"onkeypress\",\n\t\t\t\"onpointerleave\",\n\t\t\t\"oncontextlost\",\n\t\t\t\"ondrop\",\n\t\t\t\"onsecuritypolicyviolation\",\n\t\t\t\"oncontentvisibilityautostatechange\",\n\t\t\t\"ondeviceorientation\",\n\t\t\t\"onseeking\",\n\t\t\t\"onrejectionhandled\",\n\t\t\t\"onunload\",\n\t\t\t\"onmouseleave\",\n\t\t\t\"onhashchange\",\n\t\t\t\"onpointerenter\",\n\t\t\t\"onmousewheel\",\n\t\t\t\"onunhandledrejection\",\n\t\t\t\"ondragenter\",\n\t\t\t\"onpopstate\",\n\t\t\t\"onpagereveal\",\n\t\t\t\"onemptied\"\n\t\t]\n\t},\n\t\"overrides\": [\n\t\t{\n\t\t\t\"includes\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mts\", \"**/*.cts\"],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"complexity\": { \"noArguments\": \"error\" },\n\t\t\t\t\t\"correctness\": {\n\t\t\t\t\t\t\"noConstAssign\": \"off\",\n\t\t\t\t\t\t\"noGlobalObjectCalls\": \"off\",\n\t\t\t\t\t\t\"noInvalidBuiltinInstantiation\": \"off\",\n\t\t\t\t\t\t\"noInvalidConstructorSuper\": \"off\",\n\t\t\t\t\t\t\"noSetterReturn\": \"off\",\n\t\t\t\t\t\t\"noUndeclaredVariables\": \"off\",\n\t\t\t\t\t\t\"noUnreachable\": \"off\",\n\t\t\t\t\t\t\"noUnreachableSuper\": \"off\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": { \"useConst\": \"error\" },\n\t\t\t\t\t\"suspicious\": {\n\t\t\t\t\t\t\"noClassAssign\": \"off\",\n\t\t\t\t\t\t\"noDuplicateClassMembers\": \"off\",\n\t\t\t\t\t\t\"noDuplicateObjectKeys\": \"off\",\n\t\t\t\t\t\t\"noDuplicateParameters\": \"off\",\n\t\t\t\t\t\t\"noFunctionAssign\": \"off\",\n\t\t\t\t\t\t\"noImportAssign\": \"off\",\n\t\t\t\t\t\t\"noRedeclare\": \"off\",\n\t\t\t\t\t\t\"noUnsafeNegation\": \"off\",\n\t\t\t\t\t\t\"noVar\": \"error\",\n\t\t\t\t\t\t\"noWith\": \"off\",\n\t\t\t\t\t\t\"useGetterReturn\": \"off\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"includes\": [\"scripts/**/*.js\"],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"style\": { \"noCommonJs\": \"off\" }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{ \"includes\": [\"*.js\", \"*.mjs\"], \"linter\": { \"rules\": {} } },\n\t\t{\n\t\t\t\"includes\": [\"**/*.{ts,tsx}\"],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"correctness\": {\n\t\t\t\t\t\t\"useExhaustiveDependencies\": \"warn\",\n\t\t\t\t\t\t\"useHookAtTopLevel\": \"error\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t],\n\t\"assist\": {\n\t\t\"enabled\": true,\n\t\t\"actions\": { \"source\": { \"organizeImports\": \"on\" } }\n\t}\n}\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "dev-app-update.yml",
    "content": "provider: generic\nurl: https://example.com/auto-updates\nupdaterCacheDirName: deskreen-ce-updater\n"
  },
  {
    "path": "electron-builder.yml",
    "content": "appId: com.deskreen-ce.app\nproductName: Deskreen CE\ndirectories:\n  buildResources: build\nfiles:\n  - '!**/.vscode/*'\n  - '!src/*'\n  - '!electron.vite.config.{js,ts,mjs,cjs}'\n  - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md,AGENTS.md}'\n  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'\n  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'\nextraResources:\n  - from: out/client-viewer\n    to: client-viewer\nasarUnpack:\n  - resources/**\nwin:\n  artifactName: ${name}-${version}-${arch}.${ext}\n  executableName: Deskreen CE\n  target:\n    - portable\n    - msi\nnsis: null\nmac:\n  artifactName: ${name}-${version}-${arch}.${ext}\n  hardenedRuntime: true\n  entitlements: build/entitlements.mac.plist\n  entitlementsInherit: build/entitlements.mac.plist\n  extendInfo: []\n  notarize: false\ndmg:\n  artifactName: ${name}-${version}-${arch}.${ext}\nlinux:\n  target:\n    - AppImage\n    - rpm\n    - deb\n    # - snap\n  maintainer: electronjs.org\n  category: Utility\nappImage:\n  artifactName: ${name}-${version}-${arch}.${ext}\nnpmRebuild: false\npublish:\n  provider: generic\n  url: https://example.com/auto-updates\nelectronDownload:\n  mirror: https://npmmirror.com/mirrors/electron/\n"
  },
  {
    "path": "electron.vite.config.ts",
    "content": "import { resolve } from 'path';\nimport {\n\tdefineConfig,\n\texternalizeDepsPlugin,\n\tbytecodePlugin,\n} from 'electron-vite';\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nimport react from '@vitejs/plugin-react';\nimport fs from 'fs-extra';\n\n// Custom Vite plugin to copy the 'client-viewer/dist' directory\nconst copyClientViewerStaticFiles = () => {\n\treturn {\n\t\tname: 'copy-client-viewer-static-files', // A unique name for your plugin\n\t\t// The 'writeBundle' hook runs after the bundles have been written to disk\n\t\tasync writeBundle() {\n\t\t\tconst sourceDir = resolve(__dirname, 'src/client-viewer/dist');\n\t\t\tconst destDir = resolve(__dirname, 'out/client-viewer');\n\n\t\t\tconsole.log(`Attempting to copy static files from: ${sourceDir}`);\n\t\t\tconsole.log(`To destination: ${destDir}`);\n\n\t\t\ttry {\n\t\t\t\t// Ensure the destination directory exists and is empty before copying\n\t\t\t\tawait fs.emptyDir(destDir);\n\t\t\t\t// Copy the entire contents of the source directory to the destination\n\t\t\t\tawait fs.copy(sourceDir, destDir);\n\t\t\t\tconsole.log(\n\t\t\t\t\t'Successfully copied client-viewer/dist to out/client-viewer',\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(`Error copying static files: ${err}`);\n\t\t\t}\n\t\t},\n\t};\n};\n\nconst copySimplePeerMinJsStaticFiles = () => {\n\treturn {\n\t\tname: 'copy-simple-peer-min-js-static-files',\n\t\tasync writeBundle() {\n\t\t\tconst sourceFile = resolve(\n\t\t\t\t__dirname,\n\t\t\t\t'node_modules/simple-peer/simplepeer.min.js',\n\t\t\t);\n\t\t\tconst destDir = resolve(__dirname, 'out/renderer/assets');\n\n\t\t\tconsole.log(`Attempting to copy simple-peer.min.js from: ${sourceFile}`);\n\t\t\tconsole.log(`To destination: ${destDir}`);\n\n\t\t\ttry {\n\t\t\t\t// Ensure the destination directory exists\n\t\t\t\tawait fs.ensureDir(destDir);\n\t\t\t\t// Copy the file to the destination\n\t\t\t\tawait fs.copyFile(sourceFile, resolve(destDir, 'simplepeer.min.js'));\n\t\t\t\tconsole.log(\n\t\t\t\t\t'Successfully copied simple-peer.min.js to out/client-viewer/static/js',\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(`Error copying simple-peer.min.js: ${err}`);\n\t\t\t}\n\t\t},\n\t};\n};\n\nexport default defineConfig({\n\tmain: {\n\t\tplugins: [externalizeDepsPlugin(), bytecodePlugin()],\n\t},\n\tpreload: {\n\t\tbuild: {\n\t\t\trollupOptions: {\n\t\t\t\tinput: {\n\t\t\t\t\tindex: resolve(__dirname, 'src/preload/index.ts'),\n\t\t\t\t\thelperRenderer: resolve(__dirname, 'src/preload/index.ts'),\n\t\t\t\t\t// webview: resolve(__dirname, 'src/preload/webview.js')\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tplugins: [externalizeDepsPlugin(), bytecodePlugin()],\n\t},\n\trenderer: {\n\t\tbuild: {\n\t\t\trollupOptions: {\n\t\t\t\tinput: {\n\t\t\t\t\tindex: resolve(__dirname, 'src/renderer/index.html'),\n\t\t\t\t\thelperRenderer: resolve(\n\t\t\t\t\t\t__dirname,\n\t\t\t\t\t\t'src/renderer/peerConnectionHelperRendererWindowIndex.html',\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tresolve: {\n\t\t\talias: {\n\t\t\t\t'@renderer': resolve('src/renderer/src'),\n\t\t\t\t'@common': resolve('src/common'),\n\t\t\t},\n\t\t},\n\t\tplugins: [\n\t\t\treact(),\n\t\t\tcopyClientViewerStaticFiles(),\n\t\t\tcopySimplePeerMinJsStaticFiles(),\n\t\t],\n\t},\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"deskreen-ce\",\n  \"version\": \"3.2.13\",\n  \"description\": \"Screen sharing and present screen: Turn any device into a secondary screen for your computer\",\n  \"main\": \"./out/main/index.js\",\n  \"author\": \"deskreen.com\",\n  \"homepage\": \"https://electron-vite.org\",\n  \"scripts\": {\n    \"clean\": \"rm -rf dist out src/client-viewer/dist\",\n    \"format\": \"biome format --write . --max-diagnostics=none\",\n    \"lint\": \"biome lint . --max-diagnostics=none\",\n    \"lint:error-only\": \"biome lint --write . --max-diagnostics=none --diagnostic-level=error\",\n    \"typecheck:client-viewer\": \"tsc --noEmit -p src/client-viewer/tsconfig.app.json --composite false\",\n    \"typecheck:node\": \"tsc --noEmit -p tsconfig.node.json --composite false\",\n    \"typecheck:web\": \"tsc --noEmit -p tsconfig.web.json --composite false\",\n    \"typecheck\": \"npm run typecheck:client-viewer && npm run typecheck:node && npm run typecheck:web\",\n    \"start\": \"electron-vite preview\",\n    \"dev\": \"concurrently \\\"pnpm electron-vite dev\\\" \\\"pnpm:devClientViewer\\\"\",\n    \"devClientViewer\": \"cd src/client-viewer && pnpm dev --host --port=5174\",\n    \"buildDev\": \"npm run buildClientViewer && electron-vite build\",\n    \"build\": \"npm run typecheck && npm run buildClientViewer && electron-vite build\",\n    \"buildClientViewer\": \"cd src/client-viewer && pnpm build\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"build:unpack\": \"npm run build && electron-builder --dir\",\n    \"build:win\": \"npm run build && electron-builder --win\",\n    \"build:win:ia32\": \"npm run build && electron-builder --win --ia32\",\n    \"build:win:arm64\": \"npm run build && electron-builder --win --arm64\",\n    \"build:mac\": \"npm run buildClientViewer && electron-vite build && electron-builder --mac\",\n    \"build:mac:arm64\": \"npm run buildClientViewer && electron-vite build && electron-builder --mac --arm64\",\n    \"build:mac:x64\": \"npm run buildClientViewer && electron-vite build && electron-builder --mac --x64\",\n    \"build:linux\": \"npm run buildClientViewer && electron-vite build && electron-builder --linux\",\n    \"build:linux:arm64\": \"npm run buildClientViewer && electron-vite build && electron-builder --linux --arm64\",\n    \"build:linux:armv7l\": \"npm run buildClientViewer && electron-vite build && electron-builder --linux --armv7l\"\n  },\n  \"dependencies\": {\n    \"@blueprintjs/core\": \"^6.3.4\",\n    \"@blueprintjs/select\": \"^6.0.8\",\n    \"@electron-toolkit/preload\": \"^3.0.2\",\n    \"@electron-toolkit/utils\": \"^4.0.0\",\n    \"@fortawesome/fontawesome-free\": \"^7.1.0\",\n    \"@material-ui/core\": \"^4.12.4\",\n    \"@roamhq/wrtc\": \"^0.9.1\",\n    \"@types/lodash\": \"^4.17.20\",\n    \"axios\": \"^1.13.2\",\n    \"classnames\": \"^2.5.1\",\n    \"clsx\": \"^2.1.1\",\n    \"detect-port\": \"^2.1.0\",\n    \"electron-devtools-installer\": \"^4.0.0\",\n    \"electron-log\": \"^5.4.3\",\n    \"electron-settings\": \"^4.0.4\",\n    \"electron-store\": \"^10.1.0\",\n    \"electron-updater\": \"^6.6.2\",\n    \"fontsource-lexend-peta\": \"^4.0.0\",\n    \"fs-extra\": \"^11.3.2\",\n    \"get-port\": \"^7.1.0\",\n    \"i18next\": \"^25.6.2\",\n    \"i18next-fs-backend\": \"^2.6.0\",\n    \"i18next-sync-fs-backend\": \"^1.1.1\",\n    \"kcors\": \"^2.2.2\",\n    \"koa\": \"^3.1.1\",\n    \"koa-router\": \"^14.0.0\",\n    \"koa-send\": \"^5.0.1\",\n    \"koa-static\": \"^5.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"node-forge\": \"^1.3.1\",\n    \"normalize.css\": \"^8.0.1\",\n    \"qrcode.react\": \"^4.2.0\",\n    \"react-flexbox-grid\": \"^2.1.2\",\n    \"react-toast-notifications\": \"^2.5.1\",\n    \"shortid\": \"^2.2.17\",\n    \"simple-peer\": \"^9.11.1\",\n    \"socket.io\": \"^4.8.1\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"uuid\": \"^11.1.0\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"2.3.6\",\n    \"@electron-toolkit/tsconfig\": \"^1.0.1\",\n    \"@types/electron-devtools-installer\": \"^4.0.0\",\n    \"@types/i18next-node-fs-backend\": \"^2.1.5\",\n    \"@types/kcors\": \"^2.2.9\",\n    \"@types/koa\": \"^3.0.1\",\n    \"@types/koa-router\": \"^7.4.9\",\n    \"@types/koa-send\": \"^4.1.6\",\n    \"@types/koa-static\": \"^4.0.4\",\n    \"@types/node-forge\": \"^1.3.14\",\n    \"@types/qrcode.react\": \"^3.0.0\",\n    \"@types/react\": \"^19.2.3\",\n    \"@types/react-dom\": \"^19.2.2\",\n    \"@types/react-toast-notifications\": \"^2.4.1\",\n    \"@types/simple-peer\": \"^9.11.9\",\n    \"@types/socket.io\": \"^3.0.2\",\n    \"@types/socket.io-client\": \"^3.0.0\",\n    \"@vercel/blob\": \"^2.0.0\",\n    \"@vitejs/plugin-react\": \"^5.1.0\",\n    \"concurrently\": \"^9.2.1\",\n    \"electron\": \"^37.9.0\",\n    \"electron-builder\": \"^26.7.0\",\n    \"electron-vite\": \"^4.0.1\",\n    \"react\": \"^19.2.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"react-i18next\": \"^15.7.4\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.2.2\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"electron\"\n    ]\n  }\n}\n"
  },
  {
    "path": "scripts/bump-version.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('node:fs/promises');\nconst path = require('node:path');\nconst { execSync, spawnSync } = require('node:child_process');\n\nconst rootDir = path.resolve(__dirname, '..');\nconst rootPackagePath = path.join(rootDir, 'package.json');\nconst clientPackagePath = path.join(\n\trootDir,\n\t'src',\n\t'client-viewer',\n\t'package.json',\n);\nconst envPath = path.join(rootDir, 'src', 'client-viewer', '.env');\n\nconst bumpFlags = new Map([\n\t['--major', 'major'],\n\t['--minor', 'minor'],\n\t['--patch', 'patch'],\n]);\n\nconst parsedArgs = parseArgs(process.argv.slice(2));\n\nvoid main(parsedArgs).catch((error) => {\n\tif (error instanceof Error) {\n\t\tconsole.error(error.message);\n\t} else {\n\t\tconsole.error(error);\n\t}\n\tprocess.exit(1);\n});\n\nfunction parseArgs(argv) {\n\tconst matchedFlags = argv.filter((arg) => bumpFlags.has(arg));\n\tif (matchedFlags.length === 0) {\n\t\tthrow new Error(\n\t\t\t'Missing bump flag. Use one of --major, --minor, or --patch.',\n\t\t);\n\t}\n\tif (matchedFlags.length > 1) {\n\t\tthrow new Error('Multiple bump flags provided. Please supply only one.');\n\t}\n\treturn {\n\t\ttype: bumpFlags.get(matchedFlags[0]),\n\t};\n}\n\nasync function main({ type }) {\n\tprocess.chdir(rootDir);\n\tawait ensureCleanGit();\n\tconst currentVersion = await readVersion();\n\tconst nextVersion = bumpVersion(currentVersion, type);\n\tawait Promise.all([\n\t\twriteRootPackage(nextVersion),\n\t\twriteClientPackage(nextVersion),\n\t\twriteEnv(nextVersion),\n\t]);\n\tawait stageFiles();\n\tawait commit(nextVersion);\n\tawait tag(nextVersion);\n\tconsole.log(`Version bumped from ${currentVersion} to ${nextVersion}`);\n}\n\nasync function ensureCleanGit() {\n\tconst output = execSync('git status --porcelain', {\n\t\tencoding: 'utf8',\n\t}).trim();\n\tif (output.length > 0) {\n\t\tthrow new Error(\n\t\t\t'Working tree is not clean. Please commit or stash changes before bumping the version.',\n\t\t);\n\t}\n}\n\nasync function readVersion() {\n\tconst raw = await fs.readFile(rootPackagePath, 'utf8');\n\tconst parsed = JSON.parse(raw);\n\tconst version = parsed.version;\n\tif (typeof version !== 'string') {\n\t\tthrow new Error('Root package.json does not contain a valid version.');\n\t}\n\tassertValidSemver(version);\n\treturn version;\n}\n\nasync function writeRootPackage(version) {\n\tawait updatePackageJSON(rootPackagePath, version);\n}\n\nasync function writeClientPackage(version) {\n\tawait updatePackageJSON(clientPackagePath, version);\n}\n\nasync function updatePackageJSON(filePath, version) {\n\tconst raw = await fs.readFile(filePath, 'utf8');\n\tconst parsed = JSON.parse(raw);\n\tparsed.version = version;\n\tconst value = `${JSON.stringify(parsed, null, 2)}\\n`;\n\tawait fs.writeFile(filePath, value, 'utf8');\n}\n\nasync function writeEnv(version) {\n\tconst raw = await fs.readFile(envPath, 'utf8');\n\tconst lines = raw.split(/\\r?\\n/);\n\tlet replaced = false;\n\tconst nextLines = lines.map((line) => {\n\t\tif (line.startsWith('VITE_CLIENT_VIEWER_VERSION=')) {\n\t\t\treplaced = true;\n\t\t\treturn `VITE_CLIENT_VIEWER_VERSION=${version}`;\n\t\t}\n\t\treturn line;\n\t});\n\tif (!replaced) {\n\t\tnextLines.push(`VITE_CLIENT_VIEWER_VERSION=${version}`);\n\t}\n\tawait fs.writeFile(envPath, `${nextLines.join('\\n')}\\n`, 'utf8');\n}\n\nfunction bumpVersion(current, type) {\n\tconst [major, minor, patch] = current.split('.').map(Number);\n\tif ([major, minor, patch].some((part) => Number.isNaN(part))) {\n\t\tthrow new Error(\n\t\t\t`Unable to bump version. Invalid semantic version: ${current}`,\n\t\t);\n\t}\n\tif (type === 'major') {\n\t\treturn `${major + 1}.0.0`;\n\t}\n\tif (type === 'minor') {\n\t\treturn `${major}.${minor + 1}.0`;\n\t}\n\treturn `${major}.${minor}.${patch + 1}`;\n}\n\nfunction assertValidSemver(value) {\n\tconst semverPattern = /^(\\d+)\\.(\\d+)\\.(\\d+)$/;\n\tif (!semverPattern.test(value)) {\n\t\tthrow new Error(`Invalid semantic version: ${value}`);\n\t}\n}\n\nasync function stageFiles() {\n\texecSync(`git add ${quote(rootPackagePath)} ${quote(clientPackagePath)}`, {\n\t\tstdio: 'inherit',\n\t});\n}\n\nasync function commit(version) {\n\tconst message = version;\n\texecSync(`git commit -m ${quote(message)}`, { stdio: 'inherit' });\n}\n\nasync function tag(version) {\n\tconst tagName = `v${version}`;\n\tconst result = spawnSync('git', [\n\t\t'show-ref',\n\t\t'--tags',\n\t\t'--quiet',\n\t\t`refs/tags/${tagName}`,\n\t]);\n\tif (result.status === 0) {\n\t\tthrow new Error(`Tag ${tagName} already exists.`);\n\t}\n\tif (result.status !== 1) {\n\t\tthrow (\n\t\t\tresult.error ??\n\t\t\tnew Error(`git show-ref failed with status ${result.status ?? 'unknown'}`)\n\t\t);\n\t}\n\texecSync(`git tag ${quote(tagName)}`, { stdio: 'inherit' });\n}\n\nfunction quote(value) {\n\treturn `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n"
  },
  {
    "path": "scripts/undo-version-bump.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('node:fs/promises');\nconst path = require('node:path');\nconst { execSync, spawnSync } = require('node:child_process');\n\nconst rootDir = path.resolve(__dirname, '..');\nconst rootPackagePath = path.join(rootDir, 'package.json');\nconst clientPackagePath = path.join(\n\trootDir,\n\t'src',\n\t'client-viewer',\n\t'package.json',\n);\nconst envPath = path.join(rootDir, 'src', 'client-viewer', '.env');\n\nvoid main().catch((error) => {\n\tif (error instanceof Error) {\n\t\tconsole.error(error.message);\n\t} else {\n\t\tconsole.error(error);\n\t}\n\tprocess.exit(1);\n});\n\nasync function main() {\n\tprocess.chdir(rootDir);\n\tawait ensureCleanGit();\n\n\tconst latestTag = await getLatestVersionTag();\n\tif (!latestTag) {\n\t\tthrow new Error('No version tag found. Nothing to undo.');\n\t}\n\n\tconst tagVersion = latestTag.replace(/^v/, '');\n\tconsole.log(\n\t\t`Found latest version tag: ${latestTag} (version: ${tagVersion})`,\n\t);\n\n\tconst tagCommit = await getTagCommit(latestTag);\n\tconst parentCommit = await getParentCommit(tagCommit);\n\n\tif (!parentCommit) {\n\t\tthrow new Error(\n\t\t\t'Cannot find parent commit. The version bump commit might be the first commit.',\n\t\t);\n\t}\n\n\tconst previousVersion = await getVersionFromCommit(parentCommit);\n\tconsole.log(`Previous version: ${previousVersion}`);\n\n\tawait Promise.all([\n\t\twriteRootPackage(previousVersion),\n\t\twriteClientPackage(previousVersion),\n\t\twriteEnv(previousVersion),\n\t]);\n\n\tawait deleteRemoteTag(latestTag);\n\tawait deleteLocalTag(latestTag);\n\n\tconsole.log(`Version reverted from ${tagVersion} to ${previousVersion}`);\n\tconsole.log(\n\t\t`Tag ${latestTag} has been deleted locally and remotely (if it existed).`,\n\t);\n\tconsole.log('Files have been updated. You may want to commit these changes.');\n}\n\nasync function ensureCleanGit() {\n\tconst output = execSync('git status --porcelain', {\n\t\tencoding: 'utf8',\n\t}).trim();\n\tif (output.length > 0) {\n\t\tthrow new Error(\n\t\t\t'Working tree is not clean. Please commit or stash changes before undoing the version bump.',\n\t\t);\n\t}\n}\n\nasync function getLatestVersionTag() {\n\tconst result = spawnSync(\n\t\t'git',\n\t\t['tag', '--sort=-version:refname', '--list', 'v*'],\n\t\t{\n\t\t\tencoding: 'utf8',\n\t\t},\n\t);\n\n\tif (result.status !== 0) {\n\t\tthrow new Error('Failed to get git tags.');\n\t}\n\n\tconst tags = result.stdout.trim().split('\\n').filter(Boolean);\n\treturn tags[0] || null;\n}\n\nasync function getTagCommit(tagName) {\n\tconst result = spawnSync('git', ['rev-parse', tagName], { encoding: 'utf8' });\n\n\tif (result.status !== 0) {\n\t\tthrow new Error(`Failed to get commit for tag ${tagName}.`);\n\t}\n\n\treturn result.stdout.trim();\n}\n\nasync function getParentCommit(commitHash) {\n\tconst result = spawnSync('git', ['rev-parse', `${commitHash}^`], {\n\t\tencoding: 'utf8',\n\t});\n\n\tif (result.status !== 0) {\n\t\treturn null;\n\t}\n\n\treturn result.stdout.trim();\n}\n\nasync function getVersionFromCommit(commitHash) {\n\tconst result = spawnSync('git', ['show', `${commitHash}:package.json`], {\n\t\tencoding: 'utf8',\n\t});\n\n\tif (result.status !== 0) {\n\t\tthrow new Error(`Failed to read package.json from commit ${commitHash}.`);\n\t}\n\n\tconst parsed = JSON.parse(result.stdout);\n\tconst version = parsed.version;\n\n\tif (typeof version !== 'string') {\n\t\tthrow new Error(\n\t\t\t'Root package.json from parent commit does not contain a valid version.',\n\t\t);\n\t}\n\n\tassertValidSemver(version);\n\treturn version;\n}\n\nasync function writeRootPackage(version) {\n\tawait updatePackageJSON(rootPackagePath, version);\n}\n\nasync function writeClientPackage(version) {\n\tawait updatePackageJSON(clientPackagePath, version);\n}\n\nasync function updatePackageJSON(filePath, version) {\n\tconst raw = await fs.readFile(filePath, 'utf8');\n\tconst parsed = JSON.parse(raw);\n\tparsed.version = version;\n\tconst value = `${JSON.stringify(parsed, null, 2)}\\n`;\n\tawait fs.writeFile(filePath, value, 'utf8');\n}\n\nasync function writeEnv(version) {\n\tlet raw;\n\ttry {\n\t\traw = await fs.readFile(envPath, 'utf8');\n\t} catch (error) {\n\t\tif (error.code === 'ENOENT') {\n\t\t\traw = '';\n\t\t} else {\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tconst lines = raw.split(/\\r?\\n/);\n\tlet replaced = false;\n\tconst nextLines = lines.map((line) => {\n\t\tif (line.startsWith('VITE_CLIENT_VIEWER_VERSION=')) {\n\t\t\treplaced = true;\n\t\t\treturn `VITE_CLIENT_VIEWER_VERSION=${version}`;\n\t\t}\n\t\treturn line;\n\t});\n\n\tif (!replaced) {\n\t\tnextLines.push(`VITE_CLIENT_VIEWER_VERSION=${version}`);\n\t}\n\n\tawait fs.writeFile(envPath, `${nextLines.join('\\n')}\\n`, 'utf8');\n}\n\nfunction assertValidSemver(value) {\n\tconst semverPattern = /^(\\d+)\\.(\\d+)\\.(\\d+)$/;\n\tif (!semverPattern.test(value)) {\n\t\tthrow new Error(`Invalid semantic version: ${value}`);\n\t}\n}\n\nasync function deleteLocalTag(tagName) {\n\tconst result = spawnSync('git', [\n\t\t'show-ref',\n\t\t'--tags',\n\t\t'--quiet',\n\t\t`refs/tags/${tagName}`,\n\t]);\n\n\tif (result.status === 0) {\n\t\texecSync(`git tag -d ${quote(tagName)}`, { stdio: 'inherit' });\n\t\tconsole.log(`Deleted local tag: ${tagName}`);\n\t} else {\n\t\tconsole.log(`Local tag ${tagName} does not exist.`);\n\t}\n}\n\nasync function deleteRemoteTag(tagName) {\n\tconst result = spawnSync('git', ['ls-remote', '--tags', 'origin', tagName], {\n\t\tencoding: 'utf8',\n\t});\n\n\tif (result.status === 0 && result.stdout.trim().length > 0) {\n\t\texecSync(`git push origin :refs/tags/${quote(tagName)}`, {\n\t\t\tstdio: 'inherit',\n\t\t});\n\t\tconsole.log(`Deleted remote tag: ${tagName}`);\n\t} else {\n\t\tconsole.log(`Remote tag ${tagName} does not exist.`);\n\t}\n}\n\nfunction quote(value) {\n\treturn `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n"
  },
  {
    "path": "src/client-viewer/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "src/client-viewer/README.md",
    "content": "# Deskreen CE Client-Viewer\n\nAGPL-3.0 License © [Pavlo (Paul) Buidenkov](https://github.com/pavlobu/deskreen)\n"
  },
  {
    "path": "src/client-viewer/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Deskreen CE Viewer</title>\n    <!-- GA tag ID will be loaded dynamically after user consent -->\n    <meta name=\"ga-tag-id\" content=\"%VITE_CLIENT_VIEWER_GA_TAG%\" />\n    <meta name=\"client-viewer-version\" content=\"%VITE_CLIENT_VIEWER_VERSION%\" />\n    <!-- GA request interceptor - blocks requests until user consent -->\n    <script>\n      (function() {\n        const CONSENT_KEY = 'deskreen_ga_consent';\n        const GA_DOMAINS = ['google-analytics.com', 'googletagmanager.com', 'google-analytics.co', 'analytics.google.com'];\n        \n        for (let i = 1; i <= 20; i++) {\n          GA_DOMAINS.push('region' + i + '.google-analytics.com');\n        }\n        \n        function getConsentStatus() {\n          try {\n            const stored = localStorage.getItem(CONSENT_KEY);\n            return stored === 'accepted' ? 'accepted' : null;\n          } catch {\n            return null;\n          }\n        }\n        \n        function isGoogleAnalyticsUrl(url) {\n          try {\n            const urlObj = new URL(url, window.location.href);\n            const hostname = urlObj.hostname.toLowerCase();\n            return GA_DOMAINS.some(function(domain) {\n              return hostname === domain || hostname.endsWith('.' + domain);\n            });\n          } catch {\n            return false;\n          }\n        }\n        \n        function shouldBlockRequest() {\n          return getConsentStatus() !== 'accepted';\n        }\n        \n        function isLocalIP(ip) {\n          const parts = ip.split('.').map(Number);\n          if (parts.length !== 4 || parts.some(isNaN)) {\n            return false;\n          }\n          // 127.0.0.0/8\n          if (parts[0] === 127) return true;\n          // 10.0.0.0/8\n          if (parts[0] === 10) return true;\n          // 172.16.0.0/12\n          if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;\n          // 192.168.0.0/16\n          if (parts[0] === 192 && parts[1] === 168) return true;\n          return false;\n        }\n        \n        function sanitizeGAUrl(url) {\n          try {\n            const urlObj = new URL(url);\n            // only sanitize /g/collect requests\n            if (!urlObj.pathname.includes('/g/collect')) {\n              return url;\n            }\n            const dlParam = urlObj.searchParams.get('dl');\n            if (!dlParam) {\n              return url;\n            }\n            try {\n              const dlUrl = new URL(decodeURIComponent(dlParam));\n              const hostname = dlUrl.hostname;\n              if (isLocalIP(hostname)) {\n                urlObj.searchParams.set('dl', encodeURIComponent('http://localhost'));\n                return urlObj.toString();\n              }\n            } catch {\n              // if dl parameter is not a valid URL, leave it as is\n            }\n            return url;\n          } catch {\n            return url;\n          }\n        }\n        \n        // intercept fetch\n        if (window.fetch) {\n          const originalFetch = window.fetch;\n          window.fetch = function(input, init) {\n            let url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');\n            if (isGoogleAnalyticsUrl(url)) {\n              if (shouldBlockRequest()) {\n                return Promise.reject(new Error('Google Analytics request blocked: user consent not granted'));\n              }\n              url = sanitizeGAUrl(url);\n              if (input instanceof Request) {\n                input = new Request(url, init || input);\n              } else {\n                input = url;\n              }\n            }\n            return originalFetch.apply(this, arguments);\n          };\n        }\n        \n        // intercept XMLHttpRequest\n        if (window.XMLHttpRequest) {\n          const XHR = window.XMLHttpRequest;\n          const originalOpen = XHR.prototype.open;\n          const originalSend = XHR.prototype.send;\n          \n          XHR.prototype.open = function(method, url, async, username, password) {\n            let urlString = typeof url === 'string' ? url : url.toString();\n            if (isGoogleAnalyticsUrl(urlString)) {\n              if (shouldBlockRequest()) {\n                throw new Error('Google Analytics request blocked: user consent not granted');\n              }\n              urlString = sanitizeGAUrl(urlString);\n              url = urlString;\n            }\n            this._interceptedUrl = urlString;\n            return originalOpen.call(this, method, url, async, username, password);\n          };\n          \n          XHR.prototype.send = function() {\n            const url = this._interceptedUrl || '';\n            if (isGoogleAnalyticsUrl(url) && shouldBlockRequest()) {\n              return;\n            }\n            return originalSend.apply(this, arguments);\n          };\n        }\n        \n        // intercept sendBeacon\n        if (navigator.sendBeacon) {\n          const originalSendBeacon = navigator.sendBeacon;\n          navigator.sendBeacon = function(url, data) {\n            let urlString = typeof url === 'string' ? url : url.toString();\n            if (isGoogleAnalyticsUrl(urlString)) {\n              if (shouldBlockRequest()) {\n                return false;\n              }\n              urlString = sanitizeGAUrl(urlString);\n              url = urlString;\n            }\n            return originalSendBeacon.call(this, url, data);\n          };\n        }\n      })();\n    </script>\n    <!-- Google tag (gtag.js) -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=%VITE_CLIENT_VIEWER_GA_TAG%\"></script>\n    <script>\n      window.dataLayer = window.dataLayer || [];\n      function gtag(){window.dataLayer.push(arguments);}\n      gtag('js', new Date());\n      // set default consent to denied (will be updated when user accepts)\n      gtag('consent', 'default', {\n        analytics_storage: 'denied',\n        ad_storage: 'denied'\n      });\n      gtag('config', '%VITE_CLIENT_VIEWER_GA_TAG%');\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/client-viewer/package.json",
    "content": "{\n  \"name\": \"deskreen-ce-client-viewer\",\n  \"private\": true,\n  \"version\": \"3.2.13\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@blueprintjs/core\": \"^6.3.4\",\n    \"i18next\": \"^25.6.2\",\n    \"i18next-http-backend\": \"^3.0.2\",\n    \"node-forge\": \"^1.3.1\",\n    \"normalize.css\": \"^8.0.1\",\n    \"pixelmatch\": \"^7.1.0\",\n    \"react\": \"^19.2.0\",\n    \"react-app-polyfill\": \"^3.0.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"react-flexbox-grid\": \"^2.1.2\",\n    \"react-i18next\": \"^15.7.4\",\n    \"react-player\": \"^3.3.3\",\n    \"react-spinners\": \"^0.17.0\",\n    \"screenfull\": \"^6.0.2\",\n    \"shortid\": \"^2.2.17\",\n    \"simple-peer\": \"^9.11.1\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"ua-parser-js\": \"^2.0.6\",\n    \"video.js\": \"^8.23.4\"\n  },\n  \"devDependencies\": {\n    \"@types/node-forge\": \"^1.3.14\",\n    \"@types/pixelmatch\": \"^5.2.6\",\n    \"@types/react\": \"^19.2.3\",\n    \"@types/react-dom\": \"^19.2.2\",\n    \"@types/resemblejs\": \"^4.1.3\",\n    \"@types/shortid\": \"^2.2.0\",\n    \"@types/socket.io-client\": \"^3.0.0\",\n    \"@types/ua-parser-js\": \"^0.7.39\",\n    \"@vitejs/plugin-legacy\": \"^7.2.1\",\n    \"@vitejs/plugin-react\": \"^5.1.0\",\n    \"typescript\": \"~5.9.3\",\n    \"vite\": \"^7.2.2\",\n    \"vite-plugin-node-polyfills\": \"^0.24.0\"\n  }\n}\n"
  },
  {
    "path": "src/client-viewer/public/img/.gitkeep",
    "content": ""
  },
  {
    "path": "src/client-viewer/public/locales/da/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Venter på at brugeren klikker TILLAD knappen på skærmdelingsenheden...\",\n\t\"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...\",\n\t\"My Device Info\": \"Min enhedsinfo\",\n\t\"Device Type\": \"Enhedstype\",\n\t\"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\",\n\t\"Device IP\": \"Enhedens IP\",\n\t\"Device Browser\": \"Enhedens Browser\",\n\t\"Device OS\": \"Enhedens Operativsystem\",\n\t\"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\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Skærmviser\",\n\t\"Connected!\": \"Forbundet!\",\n\t\"Error occurred\": \"Der skete en fejl\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Fejl Dialog\",\n\t\"Something went wrong\": \"Noget gik galt\",\n\t\"You may close this browser window then try to connect again\": \"Prøv at lukke dette browservindue og forbind igen\",\n\t\"An unknown error occurred\": \"Der opstod en ukendt fejl\",\n\t\"You were not allowed to connect\": \"Der blev ikke tilladt forbindelse\",\n\t\"You were disconnected\": \"Du blev afbrudt\",\n\t\"WebRTC error occurred\": \"Der opstod en WebRTC fejl\",\n\t\"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.\",\n\t\"Donate\": \"Donér\",\n\t\"get-deskreen-pro\": \"Hent Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hent Deskreen Pro - åbner downloadsiden.\",\n\t\"Video stream is paused\": \"Videostream er pauset\",\n\t\"Video stream is playing\": \"Videostream kører\",\n\t\"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.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Kør\",\n\t\"Video Settings\": \"Videoindstillinger\",\n\t\"Flip\": \"Vend\",\n\t\"Video quality has been changed to\": \"Videokvaliteten er blevet ændret til\",\n\t\"Click to Open Video Settings\": \"Klik her for at åbne Videoindstillinger\",\n\t\"Click to Enter Full Screen Mode\": \"Klik her for at gå ind i fuldskærmstilstand\",\n\t\"Click to Play Video\": \"Klik for at afspille video\",\n\t\"Click to Pause Video\": \"Klik for at pause video\",\n\t\"Default video player has been turned OFF\": \"Standard videospiller er blevet slået FRA\",\n\t\"Default video player has been turned ON\": \"Standard videospiller er blevet slået TIL\",\n\t\"ON\": \"TIL\",\n\t\"OFF\": \"FRA\",\n\t\"Default Video Player\": \"Standard Videospiller\",\n\t\"Click to visit our website\": \"Klik jer for at besøge vores hjemmeside\",\n\t\"Video is flipped horizontally\": \"Videoen er vendt horisontalt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Vende skærmen er kun tilgængelig i Pro-versionen\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klik jer for at se forbindelsesinfo\",\n\t\"Pair ID\": \"Par ID\",\n\t\"Unpair\": \"Annullér Pardannelse\",\n\t\"Session ID\": \"Sessionsid\",\n\t\"Click to boost video stream if it is lagging\": \"Klik her for at booste videostreamen, hvis det lagger\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Privatlivsmeddelelse: Analyse i Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Analytik Reference\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Det vi indsamler:\",\n\t\"Page views (which screens you visit)\": \"Sidevisninger (hvilke skærme du besøger)\",\n\t\"Time spent on pages\": \"Tid brugt på sider\",\n\t\"Basic device info (browser type, screen size)\": \"Grundlæggende enhedsinfo (browsertype, skærmstørrelse)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Din IP-adresse (anonymiseret — den sidste del fjernes af hensyn til privatlivet)\",\n\t\"What we DON'T collect:\": \"Det vi IKKE indsamler:\",\n\t\"Personal info (names, emails, passwords)\": \"Personlige oplysninger (navne, e-mails, adgangskoder)\",\n\t\"Exact location\": \"Præcis placering\",\n\t\"Any files or content you interact with\": \"Filer eller indhold, du interagerer med\",\n\t\"Why anonymous?\": \"Hvorfor anonymt?\",\n\t\"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.\",\n\t\"Your options:\": \"Dine muligheder:\",\n\t\"Continue:\": \"Fortsæt:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Vi registrerer anonymiseret brug for at hjælpe os med at forbedre appen.\",\n\t\"Opt out:\": \"Fravælg:\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Data sendes til: Google Analytics. Se deres privatlivspolitik.\",\n\t\"Accept\": \"Accepter\",\n\t\"Allow\": \"Tillad\",\n\t\"Deny\": \"Afslå\",\n\t\"re-initiate-connection\": \"Genstart forbindelse\",\n\t\"Privacy Settings\": \"Privatindstillinger\",\n\t\"Change your preference:\": \"Ændre din præference:\",\n\t\"Enable analytics:\": \"Aktivér analyse:\",\n\t\"Disable analytics:\": \"Deaktivér analyse:\",\n\t\"Enable Analytics\": \"Aktivér analyse\",\n\t\"Disable Analytics\": \"Deaktivér analyse\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"deres privatlivspolitik\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/de/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Warten bis der Nutzer auf dem Freigabegerät auf ZULASSEN klickt ...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Warten bis der Nutzer eine Quelle für die Freigabe auswählt...\",\n\t\"My Device Info\": \"Meine Geräteinformationen\",\n\t\"Device Type\": \"Gerätetyp\",\n\t\"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.\",\n\t\"Device IP\": \"Geräte-IP\",\n\t\"Device Browser\": \"Geräte-Browser\",\n\t\"Device OS\": \"Geräte-Betriebssystem\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Bildschrimansicht\",\n\t\"Connected!\": \"Verbunden!\",\n\t\"Error occurred\": \"Ein Fehler ist aufgetreten\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Fehler Dialog\",\n\t\"Something went wrong\": \"Etwas ist schief gegangen\",\n\t\"You may close this browser window then try to connect again\": \"Schließe das Browserfenster und probiere es erneut\",\n\t\"An unknown error occurred\": \"Ein unbekannter Fehler ist aufgetreten\",\n\t\"You were not allowed to connect\": \"Die Verbindung wurde nicht zugelassen\",\n\t\"You were disconnected\": \"Die Verbindung wurde getrennt\",\n\t\"WebRTC error occurred\": \"WebRTC Fehler aufgetreten\",\n\t\"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.\",\n\t\"Donate\": \"Spenden\",\n\t\"get-deskreen-pro\": \"Deskreen Pro erhalten\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro erhalten - öffnet die Download-Seite.\",\n\t\"Video stream is paused\": \"Videostream ist pausiert\",\n\t\"Video stream is playing\": \"Videostream läuft\",\n\t\"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.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Abspielen\",\n\t\"Video Settings\": \"Video Einstellungen\",\n\t\"Flip\": \"Drehen\",\n\t\"Video quality has been changed to\": \"Videoqualität wurde geändert zu\",\n\t\"Click to Open Video Settings\": \"Klicken um Videoeinstellungen zu öffnen\",\n\t\"Click to Enter Full Screen Mode\": \"Klicken für Vollbild\",\n\t\"Click to Play Video\": \"Klicken um Video abzuspielen\",\n\t\"Click to Pause Video\": \"Klicken um Video anzuhalten\",\n\t\"Default video player has been turned OFF\": \"Standard Video-Player wurde ausgeschaltet\",\n\t\"Default video player has been turned ON\": \"Standard Video-Player wurde eingeschaltet\",\n\t\"ON\": \"AN\",\n\t\"OFF\": \"AUS\",\n\t\"Default Video Player\": \"Standard Video-Player\",\n\t\"Click to visit our website\": \"Klicken um unsere Website zu besuchen\",\n\t\"Video is flipped horizontally\": \"Das Video ist horizontal gedreht\",\n\t\"flip-the-screen-is-pro-version-only\": \"Bildschirm umdrehen ist nur in der Pro-Version verfügbar\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klicken um Verbindungsinformationen anzuzeigen\",\n\t\"Pair ID\": \"Kopplungs-ID\",\n\t\"Unpair\": \"Entkoppeln\",\n\t\"Session ID\": \"Sitzungs-ID\",\n\t\"Click to boost video stream if it is lagging\": \"Klicken um den Videostream zu verbessern, wenn er verzögert ist.\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Datenschutzhinweis: Analyse in Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Analytik-Referenz\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Was wir sammeln:\",\n\t\"Page views (which screens you visit)\": \"Seitenaufrufe (welche Ansichten du besuchst)\",\n\t\"Time spent on pages\": \"Verweildauer auf Seiten\",\n\t\"Basic device info (browser type, screen size)\": \"Grundlegende Geräteinformationen (Browsertyp, Bildschirmgröße)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Deine IP-Adresse (anonymisiert – der letzte Teil wird aus Datenschutzgründen entfernt)\",\n\t\"What we DON'T collect:\": \"Was wir NICHT sammeln:\",\n\t\"Personal info (names, emails, passwords)\": \"Personenbezogene Daten (Namen, E-Mails, Passwörter)\",\n\t\"Exact location\": \"Genauer Standort\",\n\t\"Any files or content you interact with\": \"Dateien oder Inhalte, mit denen du interagierst\",\n\t\"Why anonymous?\": \"Warum anonym?\",\n\t\"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.\",\n\t\"Your options:\": \"Deine Optionen:\",\n\t\"Continue:\": \"Weiter:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Wir erfassen anonymisierte Nutzung, um die App zu verbessern.\",\n\t\"Opt out:\": \"Ablehnen:\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Datenempfänger: Google Analytics. Lies deren Datenschutzerklärung.\",\n\t\"Accept\": \"Akzeptieren\",\n\t\"Allow\": \"Erlauben\",\n\t\"Deny\": \"Ablehnen\",\n\t\"re-initiate-connection\": \"Verbindung erneut herstellen\",\n\t\"Privacy Settings\": \"Datenschutzeinstellungen\",\n\t\"Change your preference:\": \"Deine Präferenz ändern:\",\n\t\"Enable analytics:\": \"Analytik aktivieren:\",\n\t\"Disable analytics:\": \"Analytik deaktivieren:\",\n\t\"Enable Analytics\": \"Analytik aktivieren\",\n\t\"Disable Analytics\": \"Analytik deaktivieren\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"deren Datenschutzerklärung\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/en/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Waiting for video stream of screen sharing device...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Waiting for user to select source to share from screen sharing device...\",\n\t\"My Device Info\": \"My Device Info\",\n\t\"Device Type\": \"Device Type\",\n\t\"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.\",\n\t\"Device IP\": \"Device IP\",\n\t\"Device Browser\": \"Device Browser\",\n\t\"Device OS\": \"Device OS\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Screen Viewer\",\n\t\"Connected!\": \"Connected!\",\n\t\"Error occurred\": \"Error occurred\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Error Dialog\",\n\t\"Something went wrong\": \"Something went wrong\",\n\t\"You may close this browser window then try to connect again\": \"You may close this browser window then try to connect again\",\n\t\"An unknown error occurred\": \"An unknown error occurred\",\n\t\"You were not allowed to connect\": \"You were not allowed to connect\",\n\t\"You were disconnected\": \"You were disconnected\",\n\t\"WebRTC error occurred\": \"WebRTC error occurred\",\n\t\"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.\",\n\t\"Donate\": \"Donate\",\n\t\"get-deskreen-pro\": \"Get Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Get Deskreen Pro - opens the download page.\",\n\t\"Video stream is paused\": \"Video stream is paused\",\n\t\"Video stream is playing\": \"Video stream is playing\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"Video stream paused after exiting fullscreen. Please click Play to continue.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Video Settings\",\n\t\"Flip\": \"Flip\",\n\t\"Video quality has been changed to\": \"Video quality has been changed to\",\n\t\"Click to Open Video Settings\": \"Click to Open Video Settings\",\n\t\"Click to Enter Full Screen Mode\": \"Click to Enter Full Screen Mode\",\n\t\"Click to Play Video\": \"Click to Play Video\",\n\t\"Click to Pause Video\": \"Click to Pause Video\",\n\t\"Default video player has been turned OFF\": \"Default video player has been turned OFF\",\n\t\"Default video player has been turned ON\": \"Default video player has been turned ON\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"Default Video Player\",\n\t\"Click to visit our website\": \"Click to visit our website\",\n\t\"Video is flipped horizontally\": \"Video is flipped horizontally\",\n\t\"flip-the-screen-is-pro-version-only\": \"Flip the screen is pro version only\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Privacy Notice: Analytics in Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Analytics Reference\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"What we collect:\",\n\t\"Page views (which screens you visit)\": \"Page views (which screens you visit)\",\n\t\"Time spent on pages\": \"Time spent on pages\",\n\t\"Basic device info (browser type, screen size)\": \"Basic device info (browser type, screen size)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Your IP address (anonymized — last part removed for privacy)\",\n\t\"What we DON'T collect:\": \"What we DON'T collect:\",\n\t\"Personal info (names, emails, passwords)\": \"Personal info (names, emails, passwords)\",\n\t\"Exact location\": \"Exact location\",\n\t\"Any files or content you interact with\": \"Any files or content you interact with\",\n\t\"Why anonymous?\": \"Why anonymous?\",\n\t\"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.\",\n\t\"Your options:\": \"Your options:\",\n\t\"Continue:\": \"Continue:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"We'll track anonymized usage to help improve the app.\",\n\t\"Opt out:\": \"Opt out:\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Data goes to: Google Analytics. See their privacy policy.\",\n\t\"Accept\": \"Accept\",\n\t\"Allow\": \"Allow\",\n\t\"Deny\": \"Deny\",\n\t\"Privacy Settings\": \"Privacy Settings\",\n\t\"Change your preference:\": \"Change your preference:\",\n\t\"Enable analytics:\": \"Enable analytics:\",\n\t\"Disable analytics:\": \"Disable analytics:\",\n\t\"Enable Analytics\": \"Enable Analytics\",\n\t\"Disable Analytics\": \"Disable Analytics\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"their privacy policy\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/es/translation.json",
    "content": "{\n\t\"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 ...\",\n\t\"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 ...\",\n\t\"My Device Info\": \"Información de mi dispositivo\",\n\t\"Device Type\": \"Tipo del dispositivo\",\n\t\"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.\",\n\t\"Device IP\": \"IP del dispositivo\",\n\t\"Device Browser\": \"Navegador del dispositivo\",\n\t\"Device OS\": \"SO del dispositivo\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Visor de pantalla de Deskreen-CE\",\n\t\"Connected!\": \"¡Conectado!\",\n\t\"Error occurred\": \"Ocurrió un error\",\n\t\"Deskreen-CE Error Dialog\": \"Cuadro de diálogo de error de Deskreen-CE\",\n\t\"Something went wrong\": \"Algo salió mal\",\n\t\"You may close this browser window then try to connect again\": \"Puedes cerrar esta ventana del navegador y luego intentar conectarte nuevamente\",\n\t\"An unknown error occurred\": \"Ocurrió un error desconocido\",\n\t\"You were not allowed to connect\": \"No se te permitió conectarte\",\n\t\"You were disconnected\": \"Fuiste desconectado\",\n\t\"WebRTC error occurred\": \"Ocurrió un error de WebRTC\",\n\t\"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.\",\n\t\"Donate\": \"Donar\",\n\t\"get-deskreen-pro\": \"Obtener Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtener Deskreen Pro - abre la página de descarga.\",\n\t\"Video stream is paused\": \"La transmisión de video está en pausa\",\n\t\"Video stream is playing\": \"La transmisión de video está en reproducción\",\n\t\"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.\",\n\t\"Pause\": \"Pausa\",\n\t\"Play\": \"Reproducir\",\n\t\"Video Settings\": \"Configuraciones de video\",\n\t\"Flip\": \"Voltear\",\n\t\"Video quality has been changed to\": \"La calidad de video se ha cambiado a\",\n\t\"Click to Open Video Settings\": \"Clic para abrir las configuraciones de video\",\n\t\"Click to Enter Full Screen Mode\": \"Clic para entrar en el modo de pantalla completa\",\n\t\"Click to Play Video\": \"Clic para reproducir el video\",\n\t\"Click to Pause Video\": \"Clic para pausar el video\",\n\t\"Default video player has been turned OFF\": \"El reproductor de video predeterminado se ha APAGADO\",\n\t\"Default video player has been turned ON\": \"El reproductor de video predeterminado se ha ENCENDIDO\",\n\t\"ON\": \"ENCENDER\",\n\t\"OFF\": \"APAGAR\",\n\t\"Default Video Player\": \"Reproductor de video predeterminado\",\n\t\"Click to visit our website\": \"Clic para visitar nuestro sitio web\",\n\t\"Video is flipped horizontally\": \"El video se ha volteado horizontalmente\",\n\t\"flip-the-screen-is-pro-version-only\": \"Voltear la pantalla está disponible solo en la versión Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Clic para ver la información de la conexión\",\n\t\"Pair ID\": \"ID del par\",\n\t\"Unpair\": \"Desemparejar\",\n\t\"Session ID\": \"ID de sesión\",\n\t\"Click to boost video stream if it is lagging\": \"Haz clic para mejorar la transmisión de video si se está retrasando\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Aviso de privacidad: Analítica en Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Referencia de Analítica\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Lo que recopilamos:\",\n\t\"Page views (which screens you visit)\": \"Vistas de página (qué pantallas visitas)\",\n\t\"Time spent on pages\": \"Tiempo invertido en las páginas\",\n\t\"Basic device info (browser type, screen size)\": \"Información básica del dispositivo (tipo de navegador, tamaño de pantalla)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Tu dirección IP (anonimizada: se elimina la última parte por privacidad)\",\n\t\"What we DON'T collect:\": \"Lo que NO recopilamos:\",\n\t\"Personal info (names, emails, passwords)\": \"Información personal (nombres, correos electrónicos, contraseñas)\",\n\t\"Exact location\": \"Ubicación exacta\",\n\t\"Any files or content you interact with\": \"Cualquier archivo o contenido con el que interactúes\",\n\t\"Why anonymous?\": \"¿Por qué anónimo?\",\n\t\"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.\",\n\t\"Your options:\": \"Tus opciones:\",\n\t\"Continue:\": \"Continuar:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Registraremos uso anonimizado para ayudar a mejorar la aplicación.\",\n\t\"Opt out:\": \"Rechazar:\",\n\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.)\": \"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).\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Los datos van a: Google Analytics. Consulta su política de privacidad.\",\n\t\"Accept\": \"Aceptar\",\n\t\"Allow\": \"Permitir\",\n\t\"Deny\": \"Rechazar\",\n\t\"re-initiate-connection\": \"Restablecer conexión\",\n\t\"Privacy Settings\": \"Configuración de privacidad\",\n\t\"Change your preference:\": \"Cambiar tu preferencia:\",\n\t\"Enable analytics:\": \"Habilitar analítica:\",\n\t\"Disable analytics:\": \"Deshabilitar analítica:\",\n\t\"Enable Analytics\": \"Habilitar Analítica\",\n\t\"Disable Analytics\": \"Deshabilitar Analítica\",\n\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.)\": \"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).\",\n\t\"their privacy policy\": \"su política de privacidad\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/fi/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Odotetaan että käyttäjä napsauttaa SALLI-painiketta ruudunjakolaitteessa...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Odotetaan että käyttäjä valitsee ruudunjakolaitteesta lähteen joka jaetaan...\",\n\t\"My Device Info\": \"Tiedot laitteestani\",\n\t\"Device Type\": \"Laitteen malli\",\n\t\"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ä.\",\n\t\"Device IP\": \"Laitteen IP\",\n\t\"Device Browser\": \"Laiteselain\",\n\t\"Device OS\": \"Laitteen käyttöjärjestelmä\",\n\t\"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ä\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE-ruutukatselin\",\n\t\"Connected!\": \"Yhdistetty!\",\n\t\"Error occurred\": \"Tapahtui virhe\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE:in virhekooste\",\n\t\"Something went wrong\": \"Jokin meni pieleen\",\n\t\"You may close this browser window then try to connect again\": \"Voit sulkea tämän selainikkunan koettaaksesi uudelleenyhdistämistä\",\n\t\"An unknown error occurred\": \"Ilmeni tuntematon virhe\",\n\t\"You were not allowed to connect\": \"Yhdistämistä ei sallittu\",\n\t\"You were disconnected\": \"Sinulta katkesi yhteys\",\n\t\"WebRTC error occurred\": \"Ilmeni WebRTC-virhe\",\n\t\"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.\",\n\t\"Donate\": \"Lahjoita\",\n\t\"get-deskreen-pro\": \"Hanki Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hanki Deskreen Pro - avaa lataussivun.\",\n\t\"Video stream is paused\": \"Videolähetys on tauolla\",\n\t\"Video stream is playing\": \"Videolähetys on käynnissä\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"Videolähetys pausattiin kokoruututilasta poistumisen jälkeen. Napsauta Toista jatkaaksesi.\",\n\t\"Pause\": \"Tauko\",\n\t\"Play\": \"Toista\",\n\t\"Video Settings\": \"Asetukset videolle\",\n\t\"Flip\": \"Käännä ympäri\",\n\t\"Video quality has been changed to\": \"Videon laatu muutettiin määreeseen\",\n\t\"Click to Open Video Settings\": \"Napsauta avataksesi videon asetukset\",\n\t\"Click to Enter Full Screen Mode\": \"Napsauta siirtyäksesi kokoruututilaan\",\n\t\"Click to Play Video\": \"Napsauta toistaaksesi videon\",\n\t\"Click to Pause Video\": \"Napsauta pysäyttääksesi videon\",\n\t\"Default video player has been turned OFF\": \"Vakiollinen videotoisto-ohjelma on KYTKETTY POIS PÄÄLTÄ\",\n\t\"Default video player has been turned ON\": \"Vakiollinen videotoisto-ohjelma on KYTKETTY PÄÄLLE\",\n\t\"ON\": \"PÄÄLLÄ\",\n\t\"OFF\": \"POIS\",\n\t\"Default Video Player\": \"Vakiollinen videontoisto-ohjelma\",\n\t\"Click to visit our website\": \"Napsauta vieraillaksesi verkkosivustollamme\",\n\t\"Video is flipped horizontally\": \"Video käännetty vaakatasossa\",\n\t\"flip-the-screen-is-pro-version-only\": \"Näytön kääntäminen on saatavilla vain Pro-versiossa\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Napsauta katsoaksesi tietoja yhteydestäsi\",\n\t\"Pair ID\": \"Lateparin ID-tunniste\",\n\t\"Unpair\": \"Poista laiteparitus\",\n\t\"Session ID\": \"Istunnon ID-tunniste\",\n\t\"Click to boost video stream if it is lagging\": \"Napsauta lisätyöntöapua videovirtaukselle mikäli se hidastelee\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Tietosuojailmoitus: Analytiikka Deskreen CE Viewer -sovelluksessa\",\n\t\"Analytics Reference\": \"Analytiikan viite\",\n\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ä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.\",\n\t\"What we collect:\": \"Mitä keräämme:\",\n\t\"Page views (which screens you visit)\": \"Sivunäyttökerrat (mitä näkymiä käyt)\",\n\t\"Time spent on pages\": \"Sivuille käytetty aika\",\n\t\"Basic device info (browser type, screen size)\": \"Laitteen perustiedot (selaintyyppi, näytön koko)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP-osoitteesi (anonymisoitu — viimeinen osa poistetaan yksityisyyden suojaamiseksi)\",\n\t\"What we DON'T collect:\": \"Mitä emme kerää:\",\n\t\"Personal info (names, emails, passwords)\": \"Henkilötietoja (nimiä, sähköposteja, salasanoja)\",\n\t\"Exact location\": \"Tarkkaa sijaintia\",\n\t\"Any files or content you interact with\": \"Tiedostoja tai sisältöä, joiden kanssa olet vuorovaikutuksessa\",\n\t\"Why anonymous?\": \"Miksi anonyymisti?\",\n\t\"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.\",\n\t\"Your options:\": \"Vaihtoehtosi:\",\n\t\"Continue:\": \"Jatka:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Seuraamme anonymisoitua käyttöä sovelluksen parantamiseksi.\",\n\t\"Opt out:\": \"Kieltäydy:\",\n\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.)\": \"Napsauta Hylkää-painiketta alla poistaaksesi seurannan käytöstä. (Kunnioitamme tätä valintaa, mutta saatat jäädä paitsi tulevista parannuksista, jotka perustuvat yhteiseen palautteeseen.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Tiedot lähetetään: Google Analytics. Tutustu heidän tietosuojakäytäntöönsä.\",\n\t\"Accept\": \"Hyväksy\",\n\t\"Allow\": \"Salli\",\n\t\"Deny\": \"Hylkää\",\n\t\"re-initiate-connection\": \"Käynnistä yhteys uudelleen\",\n\t\"Privacy Settings\": \"Tietosuoja-asetukset\",\n\t\"Change your preference:\": \"Muuta mieltymystäsi:\",\n\t\"Enable analytics:\": \"Ota analytiikka käyttöön:\",\n\t\"Disable analytics:\": \"Poista analytiikka käytöstä:\",\n\t\"Enable Analytics\": \"Ota analytiikka käyttöön\",\n\t\"Disable Analytics\": \"Poista analytiikka käytöstä\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"heidän tietosuojakäytäntönsä\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/fr/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"En attente de la validation depuis l'appareil source...\",\n\t\"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...\",\n\t\"My Device Info\": \"Mes informations d'appareil\",\n\t\"Device Type\": \"Type d'appareil\",\n\t\"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é.\",\n\t\"Device IP\": \"IP de l'appareil\",\n\t\"Device Browser\": \"Navigateur de l'appareil\",\n\t\"Device OS\": \"OS de l'appareil\",\n\t\"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é..\",\n\t\"Deskreen-CE Screen Viewer\": \"Écran de visionnage Deskreen-CE\",\n\t\"Connected!\": \"Connecté!\",\n\t\"Error occurred\": \"Une erreur est survenue\",\n\t\"Deskreen-CE Error Dialog\": \"Boîte de dialogue d'erreur\",\n\t\"Something went wrong\": \"Quelque chose s'est mal passé\",\n\t\"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\",\n\t\"An unknown error occurred\": \"Une erreur inconnue s'est produite\",\n\t\"You were not allowed to connect\": \"Vous n'êtes pas autorisé à vous connecter\",\n\t\"You were disconnected\": \"Vous avez été déconnecté\",\n\t\"WebRTC error occurred\": \"Une erreur WebRTC s'est produite\",\n\t\"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.\",\n\t\"Donate\": \"Donner\",\n\t\"get-deskreen-pro\": \"Obtenir Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtenir Deskreen Pro - ouvre la page de téléchargement.\",\n\t\"Video stream is paused\": \"Le flux vidéo est en pause\",\n\t\"Video stream is playing\": \"Lecture du flux vidéo\",\n\t\"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.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Lecture\",\n\t\"Video Settings\": \"Paramètres Vidéo\",\n\t\"Flip\": \"Tourner\",\n\t\"Video quality has been changed to\": \"Qualité de la vidéo changée en\",\n\t\"Click to Open Video Settings\": \"Cliquez pour ouvrir les paramètres vidéo\",\n\t\"Click to Enter Full Screen Mode\": \"Cliquez pour passer en plein écran\",\n\t\"Click to Play Video\": \"Cliquez pour lire la vidéo\",\n\t\"Click to Pause Video\": \"Cliquez pour mettre en pause la vidéo\",\n\t\"Default video player has been turned OFF\": \"Le lecteur vidéo par défaut a été désactivé\",\n\t\"Default video player has been turned ON\": \"Le lecteur vidéo par défaut a été activé\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"Lecteur vidéo par défaut\",\n\t\"Click to visit our website\": \"Cliquez ici pour visiter notre site web\",\n\t\"Video is flipped horizontally\": \"La vidéo à été tourner horizontallement\",\n\t\"flip-the-screen-is-pro-version-only\": \"Retourner l'écran n'est disponible que dans la version Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Cliquez pour voir les informations de connexion\",\n\t\"Pair ID\": \"ID d'appairage\",\n\t\"Unpair\": \"Desappairer\",\n\t\"Session ID\": \"ID de session\",\n\t\"Click to boost video stream if it is lagging\": \"Cliquez pour booster le flux vidéo si vous rencontrez des ralentissements\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Avis de confidentialité : Analyses dans Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Référence Analytique\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Ce que nous recueillons :\",\n\t\"Page views (which screens you visit)\": \"Pages consultées (les écrans que vous visitez)\",\n\t\"Time spent on pages\": \"Temps passé sur les pages\",\n\t\"Basic device info (browser type, screen size)\": \"Informations de base sur l'appareil (type de navigateur, taille de l'écran)\",\n\t\"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)\",\n\t\"What we DON'T collect:\": \"Ce que nous NE collectons PAS :\",\n\t\"Personal info (names, emails, passwords)\": \"Informations personnelles (noms, adresses e-mail, mots de passe)\",\n\t\"Exact location\": \"Localisation précise\",\n\t\"Any files or content you interact with\": \"Les fichiers ou contenus avec lesquels vous interagissez\",\n\t\"Why anonymous?\": \"Pourquoi anonyme ?\",\n\t\"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.\",\n\t\"Your options:\": \"Vos options :\",\n\t\"Continue:\": \"Continuer :\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Nous suivrons l'utilisation anonymisée pour aider à améliorer l'application.\",\n\t\"Opt out:\": \"Refuser :\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Les données sont envoyées à : Google Analytics. Consultez leur politique de confidentialité.\",\n\t\"Accept\": \"Accepter\",\n\t\"Allow\": \"Autoriser\",\n\t\"Deny\": \"Refuser\",\n\t\"re-initiate-connection\": \"Réinitialiser la connexion\",\n\t\"Privacy Settings\": \"Paramètres de confidentialité\",\n\t\"Change your preference:\": \"Modifier votre préférence :\",\n\t\"Enable analytics:\": \"Activer les analyses :\",\n\t\"Disable analytics:\": \"Désactiver les analyses :\",\n\t\"Enable Analytics\": \"Activer les analyses\",\n\t\"Disable Analytics\": \"Désactiver les analyses\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"leur politique de confidentialité\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/it/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Info del mio Dispositivo\",\n\t\"Device Type\": \"Tipologia Dispositivo\",\n\t\"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.\",\n\t\"Device IP\": \"IP Dispositivo\",\n\t\"Device Browser\": \"Browser Dispositivo\",\n\t\"Device OS\": \"OS Dispositivo\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Visualizzatore dello schermo di Deskreen-CE\",\n\t\"Connected!\": \"Connesso!\",\n\t\"Error occurred\": \"Si è verificato un Errore\",\n\t\"Deskreen-CE Error Dialog\": \"Finestra di dialogo degli errori di Deskreen-CE\",\n\t\"Something went wrong\": \"Qualcosa è andato storto\",\n\t\"You may close this browser window then try to connect again\": \"Puoi chiudere questa finestra del browser, quindi provare a connetterti di nuovo\",\n\t\"An unknown error occurred\": \"Si è verificato un errore sconosciuto\",\n\t\"You were not allowed to connect\": \"Non ti è stato permesso di connetterti\",\n\t\"You were disconnected\": \"Sei stato disconnesso\",\n\t\"WebRTC error occurred\": \"Si è verificato un errore WebRTC\",\n\t\"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.\",\n\t\"Donate\": \"Dona\",\n\t\"get-deskreen-pro\": \"Ottieni Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ottieni Deskreen Pro - apre la pagina di download.\",\n\t\"Video stream is paused\": \"Trasmissione Video in pausa\",\n\t\"Video stream is playing\": \"Trasmissione Video in riproduzione\",\n\t\"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.\",\n\t\"Pause\": \"Pausa\",\n\t\"Play\": \"Riproduci\",\n\t\"Video Settings\": \"Impostazioni Video\",\n\t\"Flip\": \"Capovolgi\",\n\t\"Video quality has been changed to\": \"La qualità Video è stata cambiata a\",\n\t\"Click to Open Video Settings\": \"Clicca per aprire le Impostazioni Video\",\n\t\"Click to Enter Full Screen Mode\": \"Clicca per entrare in modalità Schermo Intero\",\n\t\"Click to Play Video\": \"Clicca per riprodurre il video\",\n\t\"Click to Pause Video\": \"Clicca per mettere in pausa il video\",\n\t\"Default video player has been turned OFF\": \"il player video predefinito è stato spento\",\n\t\"Default video player has been turned ON\": \"il player video predefinito è stato acceso\",\n\t\"ON\": \"Acceso\",\n\t\"OFF\": \"Spento\",\n\t\"Default Video Player\": \"Player Video Predefinito\",\n\t\"Click to visit our website\": \"Clicca per visitare il nostro sito\",\n\t\"Video is flipped horizontally\": \"Il Video è capovolto orizzontalmente\",\n\t\"flip-the-screen-is-pro-version-only\": \"Capovolgere lo schermo è disponibile solo nella versione Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Clicca per vedere le info di connessione\",\n\t\"Pair ID\": \"ID Coppia\",\n\t\"Unpair\": \"Disaccoppia\",\n\t\"Session ID\": \"ID Sessione\",\n\t\"Click to boost video stream if it is lagging\": \"Clicca per incrementare il flusso video se sta andando a scatti\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Informativa sulla privacy: Analisi in Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Riferimento Analitico\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Cosa raccogliamo:\",\n\t\"Page views (which screens you visit)\": \"Visualizzazioni di pagina (quali schermate visiti)\",\n\t\"Time spent on pages\": \"Tempo trascorso sulle pagine\",\n\t\"Basic device info (browser type, screen size)\": \"Informazioni di base sul dispositivo (tipo di browser, dimensioni dello schermo)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Il tuo indirizzo IP (anonimizzato — l'ultima parte viene rimossa per la privacy)\",\n\t\"What we DON'T collect:\": \"Cosa NON raccogliamo:\",\n\t\"Personal info (names, emails, passwords)\": \"Dati personali (nomi, email, password)\",\n\t\"Exact location\": \"Posizione esatta\",\n\t\"Any files or content you interact with\": \"Qualsiasi file o contenuto con cui interagisci\",\n\t\"Why anonymous?\": \"Perché anonimo?\",\n\t\"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.\",\n\t\"Your options:\": \"Le tue opzioni:\",\n\t\"Continue:\": \"Continua:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Tracceremo l'utilizzo anonimizzato per aiutare a migliorare l'app.\",\n\t\"Opt out:\": \"Rifiuta:\",\n\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.)\": \"Fai clic sul pulsante Rifiuta qui sotto per disattivare il tracciamento. (Rispetteremo questa scelta, ma potresti perdere miglioramenti futuri basati sul feedback collettivo.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"I dati vengono inviati a: Google Analytics. Consulta la loro informativa sulla privacy.\",\n\t\"Accept\": \"Accetta\",\n\t\"Allow\": \"Consenti\",\n\t\"Deny\": \"Rifiuta\",\n\t\"re-initiate-connection\": \"Riavvia la connessione\",\n\t\"Privacy Settings\": \"Impostazioni privacy\",\n\t\"Change your preference:\": \"Modifica la tua preferenza:\",\n\t\"Enable analytics:\": \"Abilita analisi:\",\n\t\"Disable analytics:\": \"Disabilita analisi:\",\n\t\"Enable Analytics\": \"Abilita analisi\",\n\t\"Disable Analytics\": \"Disabilita analisi\",\n\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.)\": \"Fai clic sul pulsante Disabilita qui sotto per interrompere il tracciamento. (Rispetteremo questa scelta, ma potresti perdere miglioramenti futuri basati su feedback collettivo.)\",\n\t\"their privacy policy\": \"la loro informativa sulla privacy\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/ja/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"画面共有デバイスでユーザーが「許可」をクリックするのを待っています...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"画面共有デバイスから共有するソースをユーザーが選択するのを待っています...\",\n\t\"My Device Info\": \"このデバイスの情報\",\n\t\"Device Type\": \"デバイスの種類\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"Deskreen-CEが動作しているパソコンに表示されるアラートポップアップの\\\"デバイスIP\\\"と、このデバイスのデバイスIPが一致する必要があります。\",\n\t\"Device IP\": \"デバイスのIP\",\n\t\"Device Browser\": \"デバイスのブラウザ\",\n\t\"Device OS\": \"デバイスのOS\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"これらの内容は、画面共有デバイスのアラートポップアップに表示される内容と一致している必要があります。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Screen Viewer\",\n\t\"Connected!\": \"接続されました！\",\n\t\"Error occurred\": \"エラーが発生しました\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE エラーダイアログ\",\n\t\"Something went wrong\": \"何らかの問題が発生しました\",\n\t\"You may close this browser window then try to connect again\": \"このブラウザを閉じてから、再度接続を試みてください\",\n\t\"An unknown error occurred\": \"不明なエラーが発生しました\",\n\t\"You were not allowed to connect\": \"接続が許可されていません\",\n\t\"You were disconnected\": \"接続が切断されました\",\n\t\"WebRTC error occurred\": \"WebRTCエラーが発生しました\",\n\t\"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をより良いものにするためのモチベーションを保つことができます。\",\n\t\"Donate\": \"寄付\",\n\t\"get-deskreen-pro\": \"Deskreen Pro を入手\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro を入手 - ダウンロードページを開きます。\",\n\t\"Video stream is paused\": \"ビデオストリームを一時停止しています\",\n\t\"Video stream is playing\": \"ビデオストリームを再生中です\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"フルスクリーンモードを終了した後、ビデオストリームが一時停止されました。続けるには再生をクリックしてください。\",\n\t\"Pause\": \"一時停止\",\n\t\"Play\": \"再生\",\n\t\"Video Settings\": \"ビデオ設定\",\n\t\"Flip\": \"反転\",\n\t\"Video quality has been changed to\": \"ビデオの画質を変更しました。画質：\",\n\t\"Click to Open Video Settings\": \"クリックしてビデオ設定を開きます\",\n\t\"Click to Enter Full Screen Mode\": \"クリックするとフルスクリーンモードになります\",\n\t\"Click to Play Video\": \"クリックしてビデオを再生します\",\n\t\"Click to Pause Video\": \"クリックしてビデオを一時停止します\",\n\t\"Default video player has been turned OFF\": \"デフォルトのビデオプレーヤーがOFFになっています\",\n\t\"Default video player has been turned ON\": \"デフォルトのビデオプレーヤーがONになっています\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"デフォルトのビデオプレーヤー\",\n\t\"Click to visit our website\": \"クリックするとウェブサイトが開きます\",\n\t\"Video is flipped horizontally\": \"映像が水平方向に反転しています\",\n\t\"flip-the-screen-is-pro-version-only\": \"画面の反転はPro版でのみ利用可能です\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"クリックすると接続情報が表示されます\",\n\t\"Pair ID\": \"ペアID\",\n\t\"Unpair\": \"ペア解除\",\n\t\"Session ID\": \"セッションID\",\n\t\"Click to boost video stream if it is lagging\": \"クリックすると、ビデオストリームが遅延している場合、ブーストされます\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"プライバシーに関するお知らせ：Deskreen CE Viewerでの解析について\",\n\t\"Analytics Reference\": \"分析参照\",\n\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.\": \"このアプリは Google が提供する無料サービスの Google Analytics を使用して、基本的な利用データを匿名で追跡します。アプリの使われ方を理解し、すべてのユーザーのために改善するためです。\",\n\t\"What we collect:\": \"収集するデータ:\",\n\t\"Page views (which screens you visit)\": \"ページビュー（どの画面を表示したか）\",\n\t\"Time spent on pages\": \"ページに滞在した時間\",\n\t\"Basic device info (browser type, screen size)\": \"基本的な端末情報（ブラウザーの種類、画面サイズ）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP アドレス（匿名化 — プライバシー保護のため末尾を削除）\",\n\t\"What we DON'T collect:\": \"収集しないもの:\",\n\t\"Personal info (names, emails, passwords)\": \"個人情報（氏名、メールアドレス、パスワード）\",\n\t\"Exact location\": \"正確な位置情報\",\n\t\"Any files or content you interact with\": \"操作したファイルやコンテンツ\",\n\t\"Why anonymous?\": \"なぜ匿名なのですか？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"IP アドレスは自動的に短縮され、このデータから個人を特定することはできません。\",\n\t\"Your options:\": \"選択肢:\",\n\t\"Continue:\": \"続行:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"アプリ改善のために匿名化された利用状況を追跡します。\",\n\t\"Opt out:\": \"オプトアウト:\",\n\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.)\": \"下の「拒否」ボタンをクリックすると追跡を無効にできます。（この選択は尊重しますが、総合的なフィードバックに基づく将来の改善を逃す可能性があります。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"データ送信先：Google Analytics。プライバシーポリシーをご確認ください。\",\n\t\"Accept\": \"同意する\",\n\t\"Allow\": \"許可する\",\n\t\"Deny\": \"拒否\",\n\t\"re-initiate-connection\": \"再接続\",\n\t\"Privacy Settings\": \"プライバシー設定\",\n\t\"Change your preference:\": \"設定を変更：\",\n\t\"Enable analytics:\": \"分析を有効にする：\",\n\t\"Disable analytics:\": \"分析を無効にする：\",\n\t\"Enable Analytics\": \"分析を有効にする\",\n\t\"Disable Analytics\": \"分析を無効にする\",\n\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.)\": \"下の無効ボタンをクリックして追跡を停止します。（この選択を尊重しますが、集団フィードバックに基づく将来の改善を見逃す可能性があります。）\",\n\t\"their privacy policy\": \"プライバシーポリシー\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/ko/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"공유할 기기의 사용자가 화면 공유 허용 버튼을 클릭하기를 기다리는 중 ...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"공유할 기기의 어떤 화면을 공유할지 선택을 기다리는 중...\",\n\t\"My Device Info\": \"내 기기 정보\",\n\t\"Device Type\": \"기기 종류\",\n\t\"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\\\" 와 같아야 합니다.\",\n\t\"Device IP\": \"기기 IP\",\n\t\"Device Browser\": \"기기 브라우저\",\n\t\"Device OS\": \"기기 OS\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"세부 사항은 화면 공유 장치에서 팝업에서 표시되는 것과 일치해야합니다.\",\n\t\"Deskreen-CE Screen Viewer\": \"스크린 뷰어\",\n\t\"Connected!\": \"연결되었습니다.\",\n\t\"Error occurred\": \"오류가 발생했습니다\",\n\t\"Deskreen-CE Error Dialog\": \"오류 알림\",\n\t\"Something went wrong\": \"연결과정에 오류가 발생하였습니다\",\n\t\"You may close this browser window then try to connect again\": \"이 브라우저 창을 닫은 다음 다시 연결하십시오.\",\n\t\"An unknown error occurred\": \"알 수없는 오류가 발생했습니다\",\n\t\"You were not allowed to connect\": \"이 기기는 연결이 허용되지 않았습니다\",\n\t\"You were disconnected\": \"연결이 해제되었습니다\",\n\t\"WebRTC error occurred\": \"WebRTC 오류가 발생했습니다\",\n\t\"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better\": \"오픈소스 프로젝트에 재정적으로 기여하는 것은 더 좋은 프로그램 개발 동기를 부여합니다.\",\n\t\"Donate\": \"기부하기\",\n\t\"get-deskreen-pro\": \"Deskreen Pro 받기\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro 받기 - 다운로드 페이지를 엽니다.\",\n\t\"Video stream is paused\": \"비디오 스트림이 일시 중지됩니다\",\n\t\"Video stream is playing\": \"비디오 스트림이 재생 중입니다\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"전체 화면 종료 후 비디오 스트림이 일시 중지되었습니다. 계속하려면 재생을 클릭하세요.\",\n\t\"Pause\": \"중지\",\n\t\"Play\": \"재생\",\n\t\"Video Settings\": \"비디오 설정\",\n\t\"Flip\": \"화면 좌우 반전\",\n\t\"Video quality has been changed to\": \"비디오 품질이 변경되었습니다\",\n\t\"Click to Open Video Settings\": \"비디오 설정 열기\",\n\t\"Click to Enter Full Screen Mode\": \"전체 화면 모드로 들어가려면 클릭하십시오\",\n\t\"Click to Play Video\": \"비디오 재생을 클릭하십시오\",\n\t\"Click to Pause Video\": \"비디오 일시 정지를 클릭하십시오\",\n\t\"Default video player has been turned OFF\": \"기본 비디오 플레이어가 꺼져 있습니다\",\n\t\"Default video player has been turned ON\": \"기본 비디오 플레이어가 켜져 있습니다\",\n\t\"ON\": \"켜짐\",\n\t\"OFF\": \"꺼짐\",\n\t\"Default Video Player\": \"기본 비디오 플레이어\",\n\t\"Click to visit our website\": \"클릭하면 웹사이트를 방문합니다\",\n\t\"Video is flipped horizontally\": \"비디오를 수평으로 뒤집습니다\",\n\t\"flip-the-screen-is-pro-version-only\": \"화면 뒤집기는 Pro 버전에서만 사용할 수 있습니다\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"연결 정보를 보려면 클릭하십시오\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"클릭하면 비디오 스트림을 향상시킬 수 있습니다\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"개인정보 안내: Deskreen CE Viewer의 분석\",\n\t\"Analytics Reference\": \"분석 참조\",\n\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.\": \"이 앱은 Google Analytics(구글에서 제공하는 무료 서비스)를 사용하여 기본 사용 데이터를 익명으로 추적합니다. 이를 통해 사람들이 앱을 어떻게 사용하는지 이해하고 모두를 위해 개선할 수 있습니다.\",\n\t\"What we collect:\": \"수집하는 정보:\",\n\t\"Page views (which screens you visit)\": \"페이지 조회수(어떤 화면을 방문하는지)\",\n\t\"Time spent on pages\": \"페이지에 머문 시간\",\n\t\"Basic device info (browser type, screen size)\": \"기본 기기 정보(브라우저 종류, 화면 크기)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP 주소(익명 처리됨 — 개인정보 보호를 위해 마지막 부분이 제거됩니다)\",\n\t\"What we DON'T collect:\": \"수집하지 않는 정보:\",\n\t\"Personal info (names, emails, passwords)\": \"개인 정보(이름, 이메일, 비밀번호)\",\n\t\"Exact location\": \"정확한 위치\",\n\t\"Any files or content you interact with\": \"사용자가 상호작용하는 파일이나 콘텐츠\",\n\t\"Why anonymous?\": \"왜 익명으로 수집하나요?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"IP 주소는 자동으로 축약되며, 이 데이터로 개인을 식별할 수 없습니다.\",\n\t\"Your options:\": \"선택 사항:\",\n\t\"Continue:\": \"계속:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"앱을 개선하기 위해 익명화된 사용 데이터를 추적합니다.\",\n\t\"Opt out:\": \"거부:\",\n\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.)\": \"아래의 거부 버튼을 클릭하여 추적을 비활성화하세요. (이 선택을 존중하지만, 공동 피드백에 기반한 향후 개선 사항을 놓칠 수 있습니다.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"데이터가 전송되는 곳: Google Analytics. 개인정보 보호정책을 확인하세요.\",\n\t\"Accept\": \"동의\",\n\t\"Allow\": \"허용\",\n\t\"Deny\": \"거부\",\n\t\"re-initiate-connection\": \"연결 재시작\",\n\t\"Privacy Settings\": \"개인정보 설정\",\n\t\"Change your preference:\": \"선호도 변경:\",\n\t\"Enable analytics:\": \"분석 활성화:\",\n\t\"Disable analytics:\": \"분석 비활성화:\",\n\t\"Enable Analytics\": \"분석 활성화\",\n\t\"Disable Analytics\": \"분석 비활성화\",\n\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.)\": \"아래의 비활성화 버튼을 클릭하여 추적을 중지하세요. (이 선택을 존중하지만, 집단 피드백을 기반으로 한 향후 개선 사항을 놓칠 수 있습니다.)\",\n\t\"their privacy policy\": \"개인정보 보호정책\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/nl/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Mijn Apparaat Info\",\n\t\"Device Type\": \"Apparaat Type\",\n\t\"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\",\n\t\"Device IP\": \"Apparaat IP\",\n\t\"Device Browser\": \"Apparaat Browser\",\n\t\"Device OS\": \"Apparaat OS\",\n\t\"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\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Scherm Viewer\",\n\t\"Connected!\": \"Verbonden!\",\n\t\"Error occurred\": \"Fout opgetreden\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Error Dialoog\",\n\t\"Something went wrong\": \"Er is iets misgegaan\",\n\t\"You may close this browser window then try to connect again\": \"U mag dit browser venster sluiten en opnieuw proberen te verbinden\",\n\t\"An unknown error occurred\": \"Een onbekende fout is opgetreden\",\n\t\"You were not allowed to connect\": \"Uw verbinding werd niet toegestaan\",\n\t\"You were disconnected\": \"Uw verbinding werd verbroken\",\n\t\"WebRTC error occurred\": \"WebRTC fout opgetreden\",\n\t\"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.\",\n\t\"Donate\": \"Doneer\",\n\t\"get-deskreen-pro\": \"Ontvang Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ontvang Deskreen Pro - opent de downloadpagina.\",\n\t\"Video stream is paused\": \"Video stream is gepauzeerd\",\n\t\"Video stream is playing\": \"Video stream wordt afgespeeld\",\n\t\"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.\",\n\t\"Pause\": \"Pauze\",\n\t\"Play\": \"Afspelen\",\n\t\"Video Settings\": \"Video Instellingen\",\n\t\"Flip\": \"Flip\",\n\t\"Video quality has been changed to\": \"Video kwaliteit is aangepast naar\",\n\t\"Click to Open Video Settings\": \"Klik om Video Instellingen te openen\",\n\t\"Click to Enter Full Screen Mode\": \"Klik om Volledig Scherm modus te activeren\",\n\t\"Click to Play Video\": \"Klik om video af te spelen\",\n\t\"Click to Pause Video\": \"Klik om video te pauzeren\",\n\t\"Default video player has been turned OFF\": \"Standaard video speler staat nu UIT\",\n\t\"Default video player has been turned ON\": \"Standaard video speler staat nu AAN\",\n\t\"ON\": \"AAN\",\n\t\"OFF\": \"UIT\",\n\t\"Default Video Player\": \"Standaard Video Speler\",\n\t\"Click to visit our website\": \"Klik om onze website te bezoeken\",\n\t\"Video is flipped horizontally\": \"Video is horizontaal geflipt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Scherm omdraaien is alleen beschikbaar in de Pro-versie\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klik om verbindings informatie te zien\",\n\t\"Pair ID\": \"Koppel ID\",\n\t\"Unpair\": \"Ontkoppelen\",\n\t\"Session ID\": \"Sessie ID\",\n\t\"Click to boost video stream if it is lagging\": \"Klik om de video stream te versterken als het traag is\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Privacyverklaring: Analyse in Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Analytiek Referentie\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Wat we verzamelen:\",\n\t\"Page views (which screens you visit)\": \"Paginaweergaven (welke schermen je bezoekt)\",\n\t\"Time spent on pages\": \"Tijd doorgebracht op pagina's\",\n\t\"Basic device info (browser type, screen size)\": \"Basisapparaatinformatie (browsertype, schermgrootte)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Je IP-adres (geanonimiseerd — het laatste deel wordt verwijderd voor je privacy)\",\n\t\"What we DON'T collect:\": \"Wat we NIET verzamelen:\",\n\t\"Personal info (names, emails, passwords)\": \"Persoonlijke info (namen, e-mails, wachtwoorden)\",\n\t\"Exact location\": \"Exacte locatie\",\n\t\"Any files or content you interact with\": \"Bestanden of inhoud waarmee je interacteert\",\n\t\"Why anonymous?\": \"Waarom anoniem?\",\n\t\"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.\",\n\t\"Your options:\": \"Je opties:\",\n\t\"Continue:\": \"Doorgaan:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"We volgen geanonimiseerd gebruik om de app te verbeteren.\",\n\t\"Opt out:\": \"Afmelden:\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Gegevens gaan naar: Google Analytics. Bekijk hun privacybeleid.\",\n\t\"Accept\": \"Accepteren\",\n\t\"Allow\": \"Toestaan\",\n\t\"Deny\": \"Weigeren\",\n\t\"re-initiate-connection\": \"Verbinding opnieuw starten\",\n\t\"Privacy Settings\": \"Privacy-instellingen\",\n\t\"Change your preference:\": \"Wijzig uw voorkeur:\",\n\t\"Enable analytics:\": \"Analytics inschakelen:\",\n\t\"Disable analytics:\": \"Analytics uitschakelen:\",\n\t\"Enable Analytics\": \"Analytics inschakelen\",\n\t\"Disable Analytics\": \"Analytics uitschakelen\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"hun privacybeleid\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/ru/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Ждем когда пользователь нажмет кнопку РАЗРЕШИТЬ для доступа к экрану компьютера...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Ждем когда пользователь выберет Весь экран или Окно приложения для отображения его здесь...\",\n\t\"My Device Info\": \"Информация о моем устройстве\",\n\t\"Device Type\": \"Тип устройства\",\n\t\"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.\",\n\t\"Device IP\": \"IP-aдрес устройства\",\n\t\"Device Browser\": \"Веб-браузер устройства\",\n\t\"Device OS\": \"ОС устройства\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"Эти данные должны совпадать с теми, которые вы видите во всплывающем окне предупреждения на экране компьютера, на котором работает Deskreen-CE.\",\n\t\"Deskreen-CE Screen Viewer\": \"Просмотрщик экрана Deskreen-CE\",\n\t\"Connected!\": \"Подключено!\",\n\t\"Error occurred\": \"Произошла ошибка\",\n\t\"Deskreen-CE Error Dialog\": \"Диалог ошибки Deskreen-CE\",\n\t\"Something went wrong\": \"Произошло что-то не так\",\n\t\"You may close this browser window then try to connect again\": \"Вы можете закрыть это окно браузера и попытаться подключиться снова\",\n\t\"An unknown error occurred\": \"Произошла неизвестная ошибка\",\n\t\"You were not allowed to connect\": \"Вам не разрешили подключиться\",\n\t\"You were disconnected\": \"Вы были отключены\",\n\t\"WebRTC error occurred\": \"Произошла ошибка WebRTC\",\n\t\"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 еще лучше.\",\n\t\"Donate\": \"Пожертвовать\",\n\t\"get-deskreen-pro\": \"Получить Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Получить Deskreen Pro - открывает страницу загрузки.\",\n\t\"Video stream is paused\": \"Видеопоток приостановлен\",\n\t\"Video stream is playing\": \"Видеопоток воспроизводится\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"Видеопоток приостановлен после выхода из полноэкранного режима. Пожалуйста, нажмите Воспроизвести, чтобы продолжить.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Настройки видео\",\n\t\"Flip\": \"Отзеркалить\",\n\t\"Video quality has been changed to\": \"Качество видео изменено на\",\n\t\"Click to Open Video Settings\": \"Нажмите, чтобы открыть настройки видео\",\n\t\"Click to Enter Full Screen Mode\": \"Нажмите, чтобы перейти в полноэкранный режим\",\n\t\"Click to Play Video\": \"Нажмите, чтобы воспроизвести видео\",\n\t\"Click to Pause Video\": \"Нажмите, чтобы приостановить видео\",\n\t\"Default video player has been turned OFF\": \"Видеоплеер по умолчанию отключен\",\n\t\"Default video player has been turned ON\": \"Видеопроигрыватель по умолчанию включен\",\n\t\"ON\": \"ВКЛ\",\n\t\"OFF\": \"ВЫКЛ\",\n\t\"Default Video Player\": \"Видеоплеер по умолчанию\",\n\t\"Click to visit our website\": \"Нажмите, чтобы посетить наш сайт\",\n\t\"Video is flipped horizontally\": \"Видео отзеркалено\",\n\t\"flip-the-screen-is-pro-version-only\": \"Отзеркаливание экрана доступно только в Pro версии\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Уведомление о конфиденциальности: аналитика в Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Справочник по аналитике\",\n\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.\": \"Это приложение использует Google Analytics (бесплатный сервис Google), чтобы анонимно отслеживать базовые данные использования. Это помогает нам понимать, как люди пользуются приложением, чтобы улучшать его для всех.\",\n\t\"What we collect:\": \"Что мы собираем:\",\n\t\"Page views (which screens you visit)\": \"Просмотры страниц (какие экраны вы посещаете)\",\n\t\"Time spent on pages\": \"Время, проведённое на страницах\",\n\t\"Basic device info (browser type, screen size)\": \"Базовую информацию об устройстве (тип браузера, размер экрана)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Ваш IP-адрес (анонимизированный — последняя часть удалена для защиты приватности)\",\n\t\"What we DON'T collect:\": \"Чего мы НЕ собираем:\",\n\t\"Personal info (names, emails, passwords)\": \"Персональные данные (имена, электронные адреса, пароли)\",\n\t\"Exact location\": \"Точное местоположение\",\n\t\"Any files or content you interact with\": \"Любые файлы или контент, с которыми вы взаимодействуете\",\n\t\"Why anonymous?\": \"Почему анонимно?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"Ваш IP автоматически сокращается, и никто не сможет идентифицировать вас лично по этим данным.\",\n\t\"Your options:\": \"Ваши варианты:\",\n\t\"Continue:\": \"Продолжить:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Мы будем отслеживать анонимизированное использование, чтобы улучшать приложение.\",\n\t\"Opt out:\": \"Отказаться:\",\n\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.)\": \"Нажмите кнопку Отклонить ниже, чтобы отключить отслеживание. (Мы уважим этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Данные отправляются в: Google Analytics. Ознакомьтесь с их политикой конфиденциальности.\",\n\t\"Accept\": \"Принять\",\n\t\"Allow\": \"Разрешить\",\n\t\"Deny\": \"Отклонить\",\n\t\"re-initiate-connection\": \"Повторить подключение\",\n\t\"Privacy Settings\": \"Настройки конфиденциальности\",\n\t\"Change your preference:\": \"Изменить ваше предпочтение:\",\n\t\"Enable analytics:\": \"Включить аналитику:\",\n\t\"Disable analytics:\": \"Отключить аналитику:\",\n\t\"Enable Analytics\": \"Включить аналитику\",\n\t\"Disable Analytics\": \"Отключить аналитику\",\n\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.)\": \"Нажмите кнопку Отключить ниже, чтобы остановить отслеживание. (Мы уважаем этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)\",\n\t\"their privacy policy\": \"их политику конфиденциальности\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/sv/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Min enhetsinformation\",\n\t\"Device Type\": \"Enhetens typ\",\n\t\"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\",\n\t\"Device IP\": \"Enhetens IP\",\n\t\"Device Browser\": \"Enhetens webbläsare\",\n\t\"Device OS\": \"Enhetens operativsystem\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE skärmvisare\",\n\t\"Connected!\": \"Ansluten!\",\n\t\"Error occurred\": \"Ett fel inträffade\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE felhanterare\",\n\t\"Something went wrong\": \"Något blev fel\",\n\t\"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\",\n\t\"An unknown error occurred\": \"Ett okänt fel inträffade\",\n\t\"You were not allowed to connect\": \"Du fick inte ansluta\",\n\t\"You were disconnected\": \"Du blev nedkopplad\",\n\t\"WebRTC error occurred\": \"Ett WebRTC-fel error inträffade\",\n\t\"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.\",\n\t\"Donate\": \"Donera\",\n\t\"get-deskreen-pro\": \"Hämta Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hämta Deskreen Pro - öppnar nedladdningssidan.\",\n\t\"Video stream is paused\": \"Videoströmmen är pausad\",\n\t\"Video stream is playing\": \"Videoströmmen spelas\",\n\t\"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.\",\n\t\"Pause\": \"Paus\",\n\t\"Play\": \"Kör\",\n\t\"Video Settings\": \"Videoinställningar\",\n\t\"Flip\": \"Omvänd\",\n\t\"Video quality has been changed to\": \"Videokvaliteten har ändrats till\",\n\t\"Click to Open Video Settings\": \"Klicka här för att öppna videoinställningarna\",\n\t\"Click to Enter Full Screen Mode\": \"Klicka här för att gå in i helskärmsläge\",\n\t\"Click to Play Video\": \"Klicka för att spela upp video\",\n\t\"Click to Pause Video\": \"Klicka för att pausa video\",\n\t\"Default video player has been turned OFF\": \"Standardvideospelaren har stängts av\",\n\t\"Default video player has been turned ON\": \"Standardvideospelaren har aktiverats\",\n\t\"ON\": \"PÅ\",\n\t\"OFF\": \"AV\",\n\t\"Default Video Player\": \"Standardvideospelare\",\n\t\"Click to visit our website\": \"Klicka här för att besöka vår webbplats\",\n\t\"Video is flipped horizontally\": \"Videon är vänd horisontellt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Vända skärmen är endast tillgängligt i Pro-versionen\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klicka här för att visa anslutningsinformationen\",\n\t\"Pair ID\": \"ID för sammankopplingen\",\n\t\"Unpair\": \"Ta bort sammankopplingen\",\n\t\"Session ID\": \"ID för sessionen\",\n\t\"Click to boost video stream if it is lagging\": \"Klicka för att öka videoströmmen om den släpar efter\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Integritetsmeddelande: Analys i Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Analysreferens\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Det vi samlar in:\",\n\t\"Page views (which screens you visit)\": \"Sidvisningar (vilka vyer du besöker)\",\n\t\"Time spent on pages\": \"Tid som spenderas på sidor\",\n\t\"Basic device info (browser type, screen size)\": \"Grundläggande enhetsinformation (webbläsartyp, skärmstorlek)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Din IP-adress (anonymiserad — den sista delen tas bort av integritetsskäl)\",\n\t\"What we DON'T collect:\": \"Det vi INTE samlar in:\",\n\t\"Personal info (names, emails, passwords)\": \"Personlig information (namn, e-postadresser, lösenord)\",\n\t\"Exact location\": \"Exakt plats\",\n\t\"Any files or content you interact with\": \"Filer eller innehåll du interagerar med\",\n\t\"Why anonymous?\": \"Varför anonymt?\",\n\t\"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.\",\n\t\"Your options:\": \"Dina alternativ:\",\n\t\"Continue:\": \"Fortsätt:\",\n\t\"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.\",\n\t\"Opt out:\": \"Avslå:\",\n\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.)\": \"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Data skickas till: Google Analytics. Se deras integritetspolicy.\",\n\t\"Accept\": \"Acceptera\",\n\t\"Allow\": \"Tillåt\",\n\t\"Deny\": \"Avböj\",\n\t\"re-initiate-connection\": \"Återanslut\",\n\t\"Privacy Settings\": \"Integritetsinställningar\",\n\t\"Change your preference:\": \"Ändra din preferens:\",\n\t\"Enable analytics:\": \"Aktivera analys:\",\n\t\"Disable analytics:\": \"Inaktivera analys:\",\n\t\"Enable Analytics\": \"Aktivera analys\",\n\t\"Disable Analytics\": \"Inaktivera analys\",\n\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.)\": \"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.)\",\n\t\"their privacy policy\": \"deras integritetspolicy\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/ua/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Чекаємо коли користувач натисне кнопку ДОЗВОЛИТИ для доступу до екрану комп'ютера...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Чекаємо коли користувач вибере Весь екран або Вікно додатка для відображення його тут...\",\n\t\"My Device Info\": \"Інформація про мій пристрій\",\n\t\"Device Type\": \"Тип пристрою\",\n\t\"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.\",\n\t\"Device IP\": \"IP-aдрес пристрою\",\n\t\"Device Browser\": \"Веб-браузер пристрою\",\n\t\"Device OS\": \"ОС пристрою\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"Ці деталі повинні збігатися з тими, які ви бачите у спливаючому вікні сповіщень на екрані комп’ютера, де запущений Deskreen-CE.\",\n\t\"Deskreen-CE Screen Viewer\": \"Переглядач екрану Deskreen-CE\",\n\t\"Connected!\": \"Підключено!\",\n\t\"Error occurred\": \"Виникла помилка\",\n\t\"Deskreen-CE Error Dialog\": \"Діалог помилки Deskreen-CE\",\n\t\"Something went wrong\": \"Щось не так сталося\",\n\t\"You may close this browser window then try to connect again\": \"Ви можете закрити це вікно браузера та спробувати підключитися знову\",\n\t\"An unknown error occurred\": \"Виникла невідома помилка\",\n\t\"You were not allowed to connect\": \"Вам не дозволили підключитися\",\n\t\"You were disconnected\": \"Ви були відключені\",\n\t\"WebRTC error occurred\": \"Сталася помилка WebRTC\",\n\t\"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 ще краще.\",\n\t\"Donate\": \"Пожертвувати\",\n\t\"get-deskreen-pro\": \"Отримати Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Отримати Deskreen Pro - відкриває сторінку завантаження.\",\n\t\"Video stream is paused\": \"Відеопотік призупинено\",\n\t\"Video stream is playing\": \"Відеопотік продовжується\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"Відеопотік призупинено після виходу з повноекранного режиму. Будь ласка, натисніть Відтворення, щоб продовжити.\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Настройки видео\",\n\t\"Flip\": \"Віддзеркалити\",\n\t\"Video quality has been changed to\": \"Якість відео змінено на\",\n\t\"Click to Open Video Settings\": \"Натисніть, щоб відкрити настройки відео\",\n\t\"Click to Enter Full Screen Mode\": \"Натисніть для входу в повноекранноий режим\",\n\t\"Click to Play Video\": \"Натисніть, щоб відтворити відео\",\n\t\"Click to Pause Video\": \"Натисніть, щоб призупинити відео\",\n\t\"Default video player has been turned OFF\": \"Стандартний відеоплеєр браузера вимкнено\",\n\t\"Default video player has been turned ON\": \"Стандартний відеоплеєр браузера включений\",\n\t\"ON\": \"ВКЛ\",\n\t\"OFF\": \"ВИМК\",\n\t\"Default Video Player\": \"Стандартний відеоплеєр браузера\",\n\t\"Click to visit our website\": \"Клацніть, щоб відвідати наш веб-сайт\",\n\t\"Video is flipped horizontally\": \"Відео віддзеркалено\",\n\t\"flip-the-screen-is-pro-version-only\": \"Віддзеркалення екрану доступне лише в Pro версії\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"Повідомлення про конфіденційність: аналітика в Deskreen CE Viewer\",\n\t\"Analytics Reference\": \"Довідник з аналітики\",\n\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.\": \"Цей застосунок використовує Google Analytics (безкоштовний сервіс Google), щоб анонімно відстежувати базові дані використання. Це допомагає нам розуміти, як люди користуються застосунком, аби покращувати його для всіх.\",\n\t\"What we collect:\": \"Що ми збираємо:\",\n\t\"Page views (which screens you visit)\": \"Перегляди сторінок (які екрани ви відвідуєте)\",\n\t\"Time spent on pages\": \"Час, проведений на сторінках\",\n\t\"Basic device info (browser type, screen size)\": \"Базову інформацію про пристрій (тип браузера, розмір екрана)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Вашу IP-адресу (анонімізовану — остання частина вилучається для приватності)\",\n\t\"What we DON'T collect:\": \"Що ми НЕ збираємо:\",\n\t\"Personal info (names, emails, passwords)\": \"Особисту інформацію (імена, електронні адреси, паролі)\",\n\t\"Exact location\": \"Точне місцезнаходження\",\n\t\"Any files or content you interact with\": \"Будь-які файли чи вміст, з якими ви взаємодієте\",\n\t\"Why anonymous?\": \"Чому анонімно?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"Вашу IP автоматично скорочують, тому ніхто не може ідентифікувати вас особисто за цими даними.\",\n\t\"Your options:\": \"Ваші варіанти:\",\n\t\"Continue:\": \"Продовжити:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Ми відстежуватимемо анонімізоване використання, щоб допомогти покращити застосунок.\",\n\t\"Opt out:\": \"Відмовитися:\",\n\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.)\": \"Натисніть кнопку Відхилити нижче, щоб вимкнути відстеження. (Ми поважаємо цей вибір, але ви можете пропустити майбутні покращення, що базуються на спільному зворотному зв'язку.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Дані надсилаються до: Google Analytics. Перегляньте їхню політику конфіденційності.\",\n\t\"Accept\": \"Погодитися\",\n\t\"Allow\": \"Дозволити\",\n\t\"Deny\": \"Відхилити\",\n\t\"re-initiate-connection\": \"Повторно підключитися\",\n\t\"Privacy Settings\": \"Налаштування конфіденційності\",\n\t\"Change your preference:\": \"Змінити вашу перевагу:\",\n\t\"Enable analytics:\": \"Увімкнути аналітику:\",\n\t\"Disable analytics:\": \"Вимкнути аналітику:\",\n\t\"Enable Analytics\": \"Увімкнути аналітику\",\n\t\"Disable Analytics\": \"Вимкнути аналітику\",\n\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.)\": \"Натисніть кнопку Вимкнути нижче, щоб зупинити відстеження. (Ми поважатимемо цей вибір, але ви можете пропустити майбутні покращення, засновані на колективному відгуку.)\",\n\t\"their privacy policy\": \"їхню політику конфіденційності\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/zh_CN/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"正在等待用户单击屏幕共享设备上的允许按钮...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"正在等待用户从屏幕共享设备选择要共享的源...\",\n\t\"My Device Info\": \"我的设备信息\",\n\t\"Device Type\": \"设备类型\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"您的设备 IP 应该与运行 Deskreen-CE 的计算机上出现的警报弹出窗口中的 '设备 IP' 相匹配。\",\n\t\"Device IP\": \"设备 IP\",\n\t\"Device Browser\": \"设备浏览器\",\n\t\"Device OS\": \"设备操作系统\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"这些详细信息应与您在屏幕共享设备上的警报弹出窗口中看到的信息相匹配。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE 屏幕查看器\",\n\t\"Connected!\": \"已连接!\",\n\t\"Error occurred\": \"出现错误\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE 错误对话框\",\n\t\"Something went wrong\": \"出问题了\",\n\t\"You may close this browser window then try to connect again\": \"您可以关闭此浏览器窗口，然后尝试重新连接\",\n\t\"An unknown error occurred\": \"出现未知错误\",\n\t\"You were not allowed to connect\": \"您不能连接\",\n\t\"You were disconnected\": \"您将断开连接\",\n\t\"WebRTC error occurred\": \"出现 WebRTC 错误\",\n\t\"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 变得更好。\",\n\t\"Donate\": \"捐赠\",\n\t\"get-deskreen-pro\": \"获取 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"获取 Deskreen Pro - 打开下载页面。\",\n\t\"Video stream is paused\": \"视频流暂停\",\n\t\"Video stream is playing\": \"正在播放视频流\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"退出全屏后视频流已暂停。请点击播放以继续。\",\n\t\"Pause\": \"暂停\",\n\t\"Play\": \"播放\",\n\t\"Video Settings\": \"视频设置\",\n\t\"Flip\": \"翻转\",\n\t\"Video quality has been changed to\": \"视频质量已更改为\",\n\t\"Click to Open Video Settings\": \"单击以打开视频设置\",\n\t\"Click to Enter Full Screen Mode\": \"单击以进入全屏模式\",\n\t\"Click to Play Video\": \"单击以播放视频\",\n\t\"Click to Pause Video\": \"单击以暂停视频\",\n\t\"Default video player has been turned OFF\": \"默认视频播放器已关闭\",\n\t\"Default video player has been turned ON\": \"默认视频播放器已开启\",\n\t\"ON\": \"开启\",\n\t\"OFF\": \"关闭\",\n\t\"Default Video Player\": \"默认视频播放器\",\n\t\"Click to visit our website\": \"点击访问我们的网站\",\n\t\"Video is flipped horizontally\": \"视频水平翻转\",\n\t\"flip-the-screen-is-pro-version-only\": \"翻转屏幕仅在 Pro 版本中可用\",\n\t\"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，但欢迎您的翻译！功能将很快添加，因此需要您的翻译\",\n\t\"Click to see connection info\": \"单击以查看连接信息\",\n\t\"Pair ID\": \"配对 ID\",\n\t\"Unpair\": \"取消配对\",\n\t\"Session ID\": \"会话 ID\",\n\t\"Click to boost video stream if it is lagging\": \"如果视频流滞后，请单击以提高视频流\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"隐私提示：Deskreen CE Viewer 中的分析\",\n\t\"Analytics Reference\": \"分析参考\",\n\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.\": \"本应用使用 Google Analytics（Google 提供的免费服务）匿名跟踪基本的使用数据。这有助于我们了解用户如何使用应用，从而为所有人改进。\",\n\t\"What we collect:\": \"我们收集：\",\n\t\"Page views (which screens you visit)\": \"页面浏览量（你访问的界面）\",\n\t\"Time spent on pages\": \"在页面停留的时间\",\n\t\"Basic device info (browser type, screen size)\": \"设备基本信息（浏览器类型、屏幕尺寸）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"你的 IP 地址（已匿名化——出于隐私保护会移除最后一部分）\",\n\t\"What we DON'T collect:\": \"我们不会收集：\",\n\t\"Personal info (names, emails, passwords)\": \"个人信息（姓名、邮箱、密码）\",\n\t\"Exact location\": \"精确位置\",\n\t\"Any files or content you interact with\": \"你接触的任何文件或内容\",\n\t\"Why anonymous?\": \"为什么是匿名的？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"你的 IP 会自动被截短，任何人都无法通过这些数据识别你的身份。\",\n\t\"Your options:\": \"你的选择：\",\n\t\"Continue:\": \"继续：\",\n\t\"We'll track anonymized usage to help improve the app.\": \"我们会跟踪匿名化的使用情况，以帮助改进应用。\",\n\t\"Opt out:\": \"拒绝：\",\n\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.)\": \"点击下面的\\\"拒绝\\\"按钮以关闭跟踪。（我们会尊重这个选择，但你可能会错过基于集体反馈的未来改进。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"数据将发送至：Google Analytics。查看其隐私政策。\",\n\t\"Accept\": \"接受\",\n\t\"Allow\": \"允许\",\n\t\"Deny\": \"拒绝\",\n\t\"re-initiate-connection\": \"重新连接\",\n\t\"Privacy Settings\": \"隐私设置\",\n\t\"Change your preference:\": \"更改您的偏好：\",\n\t\"Enable analytics:\": \"启用分析：\",\n\t\"Disable analytics:\": \"禁用分析：\",\n\t\"Enable Analytics\": \"启用分析\",\n\t\"Disable Analytics\": \"禁用分析\",\n\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.)\": \"点击下面的禁用按钮以停止跟踪。（我们会尊重您的选择，但您可能会错过基于集体反馈的未来改进。）\",\n\t\"their privacy policy\": \"其隐私政策\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/locales/zh_TW/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"正在等待使用者單擊螢幕共享裝置上的允許按鈕...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"正在等待使用者從螢幕共享裝置選擇要共享的源...\",\n\t\"My Device Info\": \"我的裝置資訊\",\n\t\"Device Type\": \"裝置型別\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"您的裝置 IP 應該與執行 Deskreen-CE 的計算機上出現的警報彈出視窗中的 '裝置 IP' 相匹配。\",\n\t\"Device IP\": \"裝置 IP\",\n\t\"Device Browser\": \"裝置瀏覽器\",\n\t\"Device OS\": \"裝置作業系統\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"這些詳細資訊應與您在螢幕共享裝置上的警報彈出視窗中看到的資訊相匹配。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE 螢幕檢視器\",\n\t\"Connected!\": \"已連線!\",\n\t\"Error occurred\": \"出現錯誤\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE 錯誤對話方塊\",\n\t\"Something went wrong\": \"出問題了\",\n\t\"You may close this browser window then try to connect again\": \"您可以關閉此瀏覽器視窗，然後嘗試重新連線\",\n\t\"An unknown error occurred\": \"出現未知錯誤\",\n\t\"You were not allowed to connect\": \"您不能連線\",\n\t\"You were disconnected\": \"您將斷開連線\",\n\t\"WebRTC error occurred\": \"出現 WebRTC 錯誤\",\n\t\"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 變得更好。\",\n\t\"Donate\": \"捐贈\",\n\t\"get-deskreen-pro\": \"取得 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"取得 Deskreen Pro - 開啟下載頁面。\",\n\t\"Video stream is paused\": \"影片流暫停\",\n\t\"Video stream is playing\": \"正在播放影片流\",\n\t\"Video stream paused after exiting fullscreen. Please click Play to continue.\": \"退出全螢幕後影片流已暫停。請點擊播放以繼續。\",\n\t\"Pause\": \"暫停\",\n\t\"Play\": \"播放\",\n\t\"Video Settings\": \"影片設定\",\n\t\"Flip\": \"翻轉\",\n\t\"Video quality has been changed to\": \"影片質量已更改為\",\n\t\"Click to Open Video Settings\": \"單擊以開啟影片設定\",\n\t\"Click to Enter Full Screen Mode\": \"單擊以進入全屏模式\",\n\t\"Click to Play Video\": \"單擊以播放影片\",\n\t\"Click to Pause Video\": \"單擊以暫停影片\",\n\t\"Default video player has been turned OFF\": \"預設影片播放器已關閉\",\n\t\"Default video player has been turned ON\": \"預設影片播放器已開啟\",\n\t\"ON\": \"開啟\",\n\t\"OFF\": \"關閉\",\n\t\"Default Video Player\": \"預設影片播放器\",\n\t\"Click to visit our website\": \"點選訪問我們的網站\",\n\t\"Video is flipped horizontally\": \"影片水平翻轉\",\n\t\"flip-the-screen-is-pro-version-only\": \"翻轉螢幕僅在 Pro 版本中可用\",\n\t\"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，但歡迎您的翻譯！功能將很快新增，因此需要您的翻譯\",\n\t\"Click to see connection info\": \"單擊以檢視連線資訊\",\n\t\"Pair ID\": \"配對 ID\",\n\t\"Unpair\": \"取消配對\",\n\t\"Session ID\": \"會話 ID\",\n\t\"Click to boost video stream if it is lagging\": \"如果影片流滯後，請單擊以提高影片流\",\n\t\"Privacy Notice: Analytics in Deskreen CE Viewer\": \"隱私提示：Deskreen CE Viewer 的分析\",\n\t\"Analytics Reference\": \"分析參考\",\n\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.\": \"此應用程式使用 Google Analytics（Google 提供的免費服務）以匿名方式追蹤基本使用資料。這有助於我們了解使用者如何使用應用程式，從而為所有人帶來改進。\",\n\t\"What we collect:\": \"我們收集：\",\n\t\"Page views (which screens you visit)\": \"頁面瀏覽量（你造訪的畫面）\",\n\t\"Time spent on pages\": \"在頁面停留的時間\",\n\t\"Basic device info (browser type, screen size)\": \"裝置基本資訊（瀏覽器類型、螢幕尺寸）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"你的 IP 位址（已匿名化 — 為保護隱私會移除最後一段）\",\n\t\"What we DON'T collect:\": \"我們不會收集：\",\n\t\"Personal info (names, emails, passwords)\": \"個人資訊（姓名、電子郵件、密碼）\",\n\t\"Exact location\": \"精確位置\",\n\t\"Any files or content you interact with\": \"你互動的任何檔案或內容\",\n\t\"Why anonymous?\": \"為什麼要匿名？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"你的 IP 會自動被截短，沒有人能根據這些資料識別你的身份。\",\n\t\"Your options:\": \"你的選項：\",\n\t\"Continue:\": \"繼續：\",\n\t\"We'll track anonymized usage to help improve the app.\": \"我們會追蹤匿名化的使用情況，協助改進應用程式。\",\n\t\"Opt out:\": \"拒絕：\",\n\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.)\": \"點擊下方的「拒絕」按鈕以停用追蹤。（我們會尊重此選擇，但你可能會錯過基於集體回饋的未來改善。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"資料傳送至：Google Analytics。查看其隱私權政策。\",\n\t\"Accept\": \"接受\",\n\t\"Allow\": \"允許\",\n\t\"Deny\": \"拒絕\",\n\t\"re-initiate-connection\": \"重新連線\",\n\t\"Privacy Settings\": \"隱私設定\",\n\t\"Change your preference:\": \"更改您的偏好：\",\n\t\"Enable analytics:\": \"啟用分析：\",\n\t\"Disable analytics:\": \"停用分析：\",\n\t\"Enable Analytics\": \"啟用分析\",\n\t\"Disable Analytics\": \"停用分析\",\n\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.)\": \"點擊下面的停用按鈕以停止追蹤。（我們會尊重您的選擇，但您可能會錯過基於集體反饋的未來改進。）\",\n\t\"their privacy policy\": \"其隱私權政策\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/manifest.json",
    "content": "{\n\t\"short_name\": \"Deskreen CE\",\n\t\"name\": \"Deskreen CE Makes Any Device a Second Screen For Your Computer\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"favicon.ico\",\n\t\t\t\"sizes\": \"64x64 32x32 24x24 16x16\",\n\t\t\t\"type\": \"image/x-icon\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"logo192.png\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"sizes\": \"192x192\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"logo512.png\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"sizes\": \"512x512\"\n\t\t}\n\t],\n\t\"start_url\": \".\",\n\t\"display\": \"standalone\",\n\t\"theme_color\": \"#000000\",\n\t\"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "src/client-viewer/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/client-viewer/scripts/ga-interceptor.js",
    "content": "(function () {\n\tconst CONSENT_KEY = 'deskreen_ga_consent';\n\tconst GA_DOMAINS = [\n\t\t'google-analytics.com',\n\t\t'googletagmanager.com',\n\t\t'google-analytics.co',\n\t\t'analytics.google.com',\n\t];\n\n\tfor (let i = 1; i <= 20; i++) {\n\t\tGA_DOMAINS.push('region' + i + '.google-analytics.com');\n\t}\n\n\tfunction getConsentStatus() {\n\t\ttry {\n\t\t\tconst stored = localStorage.getItem(CONSENT_KEY);\n\t\t\treturn stored === 'accepted' ? 'accepted' : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tfunction isGoogleAnalyticsUrl(url) {\n\t\ttry {\n\t\t\tconst urlObj = new URL(url, window.location.href);\n\t\t\tconst hostname = urlObj.hostname.toLowerCase();\n\t\t\treturn GA_DOMAINS.some(function (domain) {\n\t\t\t\treturn hostname === domain || hostname.endsWith('.' + domain);\n\t\t\t});\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tfunction shouldBlockRequest() {\n\t\treturn getConsentStatus() !== 'accepted';\n\t}\n\n\tfunction isLocalIP(ip) {\n\t\tconst parts = ip.split('.').map(Number);\n\t\tif (parts.length !== 4 || parts.some(isNaN)) {\n\t\t\treturn false;\n\t\t}\n\t\t// 127.0.0.0/8\n\t\tif (parts[0] === 127) return true;\n\t\t// 10.0.0.0/8\n\t\tif (parts[0] === 10) return true;\n\t\t// 172.16.0.0/12\n\t\tif (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;\n\t\t// 192.168.0.0/16\n\t\tif (parts[0] === 192 && parts[1] === 168) return true;\n\t\treturn false;\n\t}\n\n\tfunction sanitizeGAUrl(url) {\n\t\ttry {\n\t\t\tconst urlObj = new URL(url);\n\t\t\t// only sanitize /g/collect requests\n\t\t\tif (!urlObj.pathname.includes('/g/collect')) {\n\t\t\t\treturn url;\n\t\t\t}\n\t\t\tconst dlParam = urlObj.searchParams.get('dl');\n\t\t\tif (!dlParam) {\n\t\t\t\treturn url;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst dlUrl = new URL(decodeURIComponent(dlParam));\n\t\t\t\tconst hostname = dlUrl.hostname;\n\t\t\t\tif (isLocalIP(hostname)) {\n\t\t\t\t\turlObj.searchParams.set('dl', encodeURIComponent('http://localhost'));\n\t\t\t\t\treturn urlObj.toString();\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// if dl parameter is not a valid URL, leave it as is\n\t\t\t}\n\t\t\treturn url;\n\t\t} catch {\n\t\t\treturn url;\n\t\t}\n\t}\n\n\t// intercept fetch\n\tif (window.fetch) {\n\t\tconst originalFetch = window.fetch;\n\t\twindow.fetch = function (input, init) {\n\t\t\tlet url =\n\t\t\t\ttypeof input === 'string'\n\t\t\t\t\t? input\n\t\t\t\t\t: input instanceof Request\n\t\t\t\t\t\t? input.url\n\t\t\t\t\t\t: '';\n\t\t\tif (isGoogleAnalyticsUrl(url)) {\n\t\t\t\tif (shouldBlockRequest()) {\n\t\t\t\t\treturn Promise.reject(\n\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t'Google Analytics request blocked: user consent not granted',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\turl = sanitizeGAUrl(url);\n\t\t\t\tif (input instanceof Request) {\n\t\t\t\t\tinput = new Request(url, init || input);\n\t\t\t\t} else {\n\t\t\t\t\tinput = url;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn originalFetch.apply(this, arguments);\n\t\t};\n\t}\n\n\t// intercept XMLHttpRequest\n\tif (window.XMLHttpRequest) {\n\t\tconst XHR = window.XMLHttpRequest;\n\t\tconst originalOpen = XHR.prototype.open;\n\t\tconst originalSend = XHR.prototype.send;\n\n\t\tXHR.prototype.open = function (method, url, async, username, password) {\n\t\t\tlet urlString = typeof url === 'string' ? url : url.toString();\n\t\t\tif (isGoogleAnalyticsUrl(urlString)) {\n\t\t\t\tif (shouldBlockRequest()) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'Google Analytics request blocked: user consent not granted',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\turlString = sanitizeGAUrl(urlString);\n\t\t\t\turl = urlString;\n\t\t\t}\n\t\t\tthis._interceptedUrl = urlString;\n\t\t\treturn originalOpen.apply(this, arguments);\n\t\t};\n\n\t\tXHR.prototype.send = function () {\n\t\t\tconst url = this._interceptedUrl || '';\n\t\t\tif (isGoogleAnalyticsUrl(url) && shouldBlockRequest()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn originalSend.apply(this, arguments);\n\t\t};\n\t}\n\n\t// intercept sendBeacon\n\tif (navigator.sendBeacon) {\n\t\tconst originalSendBeacon = navigator.sendBeacon;\n\t\tnavigator.sendBeacon = function (url, data) {\n\t\t\tlet urlString = typeof url === 'string' ? url : url.toString();\n\t\t\tif (isGoogleAnalyticsUrl(urlString)) {\n\t\t\t\tif (shouldBlockRequest()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\turlString = sanitizeGAUrl(urlString);\n\t\t\t\turl = urlString;\n\t\t\t}\n\t\t\treturn originalSendBeacon.call(this, url, data);\n\t\t};\n\t}\n})();\n"
  },
  {
    "path": "src/client-viewer/src/App.css",
    "content": "#root {\n\tmax-width: 1280px;\n\tmargin: 0 auto;\n\tpadding: 2rem;\n\ttext-align: center;\n}\n\n.logo {\n\theight: 6em;\n\tpadding: 1.5em;\n\twill-change: filter;\n\ttransition: filter 300ms;\n}\n.logo:hover {\n\tfilter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n\tfilter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n\tfrom {\n\t\ttransform: rotate(0deg);\n\t}\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\ta:nth-of-type(2) .logo {\n\t\tanimation: logo-spin infinite 20s linear;\n\t}\n}\n\n.card {\n\tpadding: 2em;\n}\n\n.read-the-docs {\n\tcolor: #888;\n}\n"
  },
  {
    "path": "src/client-viewer/src/App.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport MainView from './containers/MainView';\nimport PrivacyConsentDialog from './components/PrivacyConsentDialog';\nimport LoadingScreen from './components/LoadingScreen';\nimport {\n\tgetConsentStatus,\n\tsetConsentStatus,\n\tloadGoogleAnalytics,\n\tgetGaTagIdFromMeta,\n\tupdateAnalyticsConsent,\n} from './utils/analytics';\n\nconst App: React.FC = () => {\n\t// Helper function to check for prerendering safely\n\tconst isCurrentlyPrerendering = () => {\n\t\t// Check if 'document' and 'prerendering' property exist\n\t\treturn typeof document !== 'undefined' &&\n\t\t\ttypeof document.prerendering === 'boolean'\n\t\t\t? document.prerendering\n\t\t\t: false; // Default to false if the property doesn't exist\n\t};\n\n\tconst [isTrulyVisible, setIsTrulyVisible] = useState(\n\t\t!isCurrentlyPrerendering(),\n\t);\n\tconst [showConsentDialog, setShowConsentDialog] = useState(false);\n\tconst [hasConsent, setHasConsent] = useState(false);\n\n\tuseEffect(() => {\n\t\t// Only set up listeners if document.prerendering is supported\n\t\tif (\n\t\t\ttypeof document !== 'undefined' &&\n\t\t\ttypeof document.prerendering === 'boolean'\n\t\t) {\n\t\t\tconst handlePrerenderChange = () => {\n\t\t\t\t// When the prerendering state changes, update isTrulyVisible\n\t\t\t\t// It becomes true when document.prerendering is false (i.e., page is activated)\n\t\t\t\tsetIsTrulyVisible(!document.prerendering);\n\t\t\t};\n\n\t\t\t// If it was initially prerendering, listen for the change to activate.\n\t\t\t// The { once: true } option is useful if you only care about the first activation.\n\t\t\t// However, if a page could theoretically go back into a prerender state (less common for user navigation),\n\t\t\t// you might remove { once: true } but then also need more complex logic.\n\t\t\t// For the typical \"prerender then activate\" flow, { once: true } is fine.\n\t\t\tif (document.prerendering) {\n\t\t\t\tdocument.addEventListener('prerenderingchange', handlePrerenderChange, {\n\t\t\t\t\tonce: true,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn () => {\n\t\t\t\t// Cleanup the event listener\n\t\t\t\tdocument.removeEventListener(\n\t\t\t\t\t'prerenderingchange',\n\t\t\t\t\thandlePrerenderChange,\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\t\t// If document.prerendering is not supported, isTrulyVisible is already true\n\t\t// (due to the initial useState value and isCurrentlyPrerendering fallback),\n\t\t// so no specific effect logic is needed for that case here.\n\t}, []); // Empty dependency array means this effect runs once on mount and cleans up on unmount.\n\n\tuseEffect(() => {\n\t\tif (!isTrulyVisible) {\n\t\t\treturn;\n\t\t}\n\n\t\t// check consent status first\n\t\tconst consentStatus = getConsentStatus();\n\n\t\t// load GA immediately when page is visible (before consent)\n\t\tconst gaTagId = getGaTagIdFromMeta();\n\t\tif (gaTagId && gaTagId !== '%VITE_CLIENT_VIEWER_GA_TAG%') {\n\t\t\tloadGoogleAnalytics(gaTagId);\n\n\t\t\t// if user previously accepted consent, ensure page_view is sent\n\t\t\tif (consentStatus === 'accepted') {\n\t\t\t\t// wait a bit for GA to load, then update consent and send page_view\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tupdateAnalyticsConsent('accepted');\n\t\t\t\t}, 500);\n\t\t\t}\n\t\t}\n\n\t\tif (consentStatus === 'accepted') {\n\t\t\tsetHasConsent(true);\n\t\t} else if (consentStatus === 'opted-out') {\n\t\t\t// user previously opted out - allow app usage without analytics\n\t\t\tsetHasConsent(true);\n\t\t\t// ensure analytics consent is set to denied\n\t\t\tupdateAnalyticsConsent('opted-out');\n\t\t} else {\n\t\t\t// no consent yet - show dialog\n\t\t\tsetShowConsentDialog(true);\n\t\t}\n\t}, [isTrulyVisible]);\n\n\tconst handleAccept = () => {\n\t\tsetConsentStatus('accepted');\n\t\tsetShowConsentDialog(false);\n\t\tsetHasConsent(true);\n\n\t\t// update GA consent to granted and send page_view\n\t\tupdateAnalyticsConsent('accepted');\n\t};\n\n\tconst handleOptOut = () => {\n\t\t// set consent status to opted-out so user can continue using app without analytics\n\t\tsetConsentStatus('opted-out');\n\t\tsetShowConsentDialog(false);\n\t\tsetHasConsent(true);\n\n\t\t// update GA consent to denied and ensure no analytics are sent\n\t\tupdateAnalyticsConsent('opted-out');\n\t};\n\n\tif (!isTrulyVisible) {\n\t\t// Render a minimal version or nothing while the browser is prerendering.\n\t\t// This prevents heavy computations or API calls during the browser's prerender phase.\n\t\t// console.log(\"Page is being prerendered by the browser (or support for detection is present). Waiting for activation.\");\n\t\treturn <LoadingScreen />;\n\t}\n\n\tif (!hasConsent) {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<PrivacyConsentDialog\n\t\t\t\t\tisOpen={showConsentDialog}\n\t\t\t\t\tonAccept={handleAccept}\n\t\t\t\t\tonOptOut={handleOptOut}\n\t\t\t\t/>\n\t\t\t</>\n\t\t);\n\t}\n\n\treturn <MainView />;\n};\n\nexport default App;\n"
  },
  {
    "path": "src/client-viewer/src/api/config.ts",
    "content": "let host;\nlet protocol;\nlet port;\n\nif (!host && !protocol && !port) {\n\thost = window.location.host.split(':')[0];\n\tprotocol = 'http';\n\tport = 3131;\n}\n\nexport default {\n\thost,\n\tport,\n\tprotocol,\n};\n"
  },
  {
    "path": "src/client-viewer/src/api/generator.ts",
    "content": "import config from './config';\n\nexport const generateUrl = (resourceName = '') => {\n\tconst { port, protocol, host } = config;\n\n\tconst resourcePath = resourceName;\n\n\tif (!host) {\n\t\treturn `/localhost`;\n\t}\n\n\treturn `${protocol}://${host}:${port}/${resourcePath}`;\n};\n\nexport default {\n\tgenerateUrl,\n};\n"
  },
  {
    "path": "src/client-viewer/src/assets/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>Deskreen CE Viewer</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/da/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Venter på at brugeren klikker TILLAD knappen på skærmdelingsenheden...\",\n\t\"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...\",\n\t\"My Device Info\": \"Min enhedsinfo\",\n\t\"Device Type\": \"Enhedstype\",\n\t\"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\",\n\t\"Device IP\": \"Enhedens IP\",\n\t\"Device Browser\": \"Enhedens Browser\",\n\t\"Device OS\": \"Enhedens Operativsystem\",\n\t\"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\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Skærmviser\",\n\t\"Connected!\": \"Forbundet!\",\n\t\"Error occurred\": \"Der skete en fejl\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Fejl Dialog\",\n\t\"Something went wrong\": \"Noget gik galt\",\n\t\"You may close this browser window then try to connect again\": \"Prøv at lukke dette browservindue og forbind igen\",\n\t\"An unknown error occurred\": \"Der opstod en ukendt fejl\",\n\t\"You were not allowed to connect\": \"Der blev ikke tilladt forbindelse\",\n\t\"You were disconnected\": \"Du blev afbrudt\",\n\t\"WebRTC error occurred\": \"Der opstod en WebRTC fejl\",\n\t\"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.\",\n\t\"Donate\": \"Donér\",\n\t\"get-deskreen-pro\": \"Hent Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hent Deskreen Pro - åbner downloadsiden.\",\n\t\"Video stream is paused\": \"Videostream er pauset\",\n\t\"Video stream is playing\": \"Videostream kører\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Kør\",\n\t\"Video Settings\": \"Videoindstillinger\",\n\t\"Flip\": \"Vend\",\n\t\"Video quality has been changed to\": \"Videokvaliteten er blevet ændret til\",\n\t\"Click to Open Video Settings\": \"Klik her for at åbne Videoindstillinger\",\n\t\"Click to Enter Full Screen Mode\": \"Klik her for at gå ind i fuldskærmstilstand\",\n\t\"Default video player has been turned OFF\": \"Standard videospiller er blevet slået FRA\",\n\t\"Default video player has been turned ON\": \"Standard videospiller er blevet slået TIL\",\n\t\"ON\": \"TIL\",\n\t\"OFF\": \"FRA\",\n\t\"Default Video Player\": \"Standard Videospiller\",\n\t\"Click to visit our website\": \"Klik jer for at besøge vores hjemmeside\",\n\t\"Video is flipped horizontally\": \"Videoen er vendt horisontalt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Vende skærmen er kun tilgængelig i Pro-versionen\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klik jer for at se forbindelsesinfo\",\n\t\"Pair ID\": \"Par ID\",\n\t\"Unpair\": \"Annullér Pardannelse\",\n\t\"Session ID\": \"Sessionsid\",\n\t\"Click to boost video stream if it is lagging\": \"Klik her for at booste videostreamen, hvis det lagger\",\n\t\"Privacy Notice: Analytics in This App\": \"Privatlivsmeddelelse: Analyse i denne app\",\n\t\"Analytics Reference\": \"Analytik Reference\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Det vi indsamler:\",\n\t\"Page views (which screens you visit)\": \"Sidevisninger (hvilke skærme du besøger)\",\n\t\"Time spent on pages\": \"Tid brugt på sider\",\n\t\"Basic device info (browser type, screen size)\": \"Grundlæggende enhedsinfo (browsertype, skærmstørrelse)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Din IP-adresse (anonymiseret — den sidste del fjernes af hensyn til privatlivet)\",\n\t\"What we DON'T collect:\": \"Det vi IKKE indsamler:\",\n\t\"Personal info (names, emails, passwords)\": \"Personlige oplysninger (navne, e-mails, adgangskoder)\",\n\t\"Exact location\": \"Præcis placering\",\n\t\"Any files or content you interact with\": \"Filer eller indhold, du interagerer med\",\n\t\"Why anonymous?\": \"Hvorfor anonymt?\",\n\t\"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.\",\n\t\"Your options:\": \"Dine muligheder:\",\n\t\"Continue:\": \"Fortsæt:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Vi registrerer anonymiseret brug for at hjælpe os med at forbedre appen.\",\n\t\"Opt out:\": \"Fravælg:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Data sendes til: Google Analytics. Se deres privatlivspolitik.\",\n\t\"Accept\": \"Accepter\",\n\t\"Disagree\": \"Afslå\",\n\t\"re-initiate-connection\": \"Genstart forbindelse\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/de/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Warten bis der Nutzer auf dem Freigabegerät auf ZULASSEN klickt ...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Warten bis der Nutzer eine Quelle für die Freigabe auswählt...\",\n\t\"My Device Info\": \"Meine Geräteinformationen\",\n\t\"Device Type\": \"Gerätetyp\",\n\t\"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.\",\n\t\"Device IP\": \"Geräte-IP\",\n\t\"Device Browser\": \"Geräte-Browser\",\n\t\"Device OS\": \"Geräte-Betriebssystem\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Bildschrimansicht\",\n\t\"Connected!\": \"Verbunden!\",\n\t\"Error occurred\": \"Ein Fehler ist aufgetreten\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Fehler Dialog\",\n\t\"Something went wrong\": \"Etwas ist schief gegangen\",\n\t\"You may close this browser window then try to connect again\": \"Schließe das Browserfenster und probiere es erneut\",\n\t\"An unknown error occurred\": \"Ein unbekannter Fehler ist aufgetreten\",\n\t\"You were not allowed to connect\": \"Die Verbindung wurde nicht zugelassen\",\n\t\"You were disconnected\": \"Die Verbindung wurde getrennt\",\n\t\"WebRTC error occurred\": \"WebRTC Fehler aufgetreten\",\n\t\"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.\",\n\t\"Donate\": \"Spenden\",\n\t\"get-deskreen-pro\": \"Deskreen Pro erhalten\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro erhalten - öffnet die Download-Seite.\",\n\t\"Video stream is paused\": \"Videostream ist pausiert\",\n\t\"Video stream is playing\": \"Videostream läuft\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Abspielen\",\n\t\"Video Settings\": \"Video Einstellungen\",\n\t\"Flip\": \"Drehen\",\n\t\"Video quality has been changed to\": \"Videoqualität wurde geändert zu\",\n\t\"Click to Open Video Settings\": \"Klicken um Videoeinstellungen zu öffnen\",\n\t\"Click to Enter Full Screen Mode\": \"Klicken für Vollbild\",\n\t\"Default video player has been turned OFF\": \"Standard Video-Player wurde ausgeschaltet\",\n\t\"Default video player has been turned ON\": \"Standard Video-Player wurde eingeschaltet\",\n\t\"ON\": \"AN\",\n\t\"OFF\": \"AUS\",\n\t\"Default Video Player\": \"Standard Video-Player\",\n\t\"Click to visit our website\": \"Klicken um unsere Website zu besuchen\",\n\t\"Video is flipped horizontally\": \"Das Video ist horizontal gedreht\",\n\t\"flip-the-screen-is-pro-version-only\": \"Bildschirm umdrehen ist nur in der Pro-Version verfügbar\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klicken um Verbindungsinformationen anzuzeigen\",\n\t\"Pair ID\": \"Kopplungs-ID\",\n\t\"Unpair\": \"Entkoppeln\",\n\t\"Session ID\": \"Sitzungs-ID\",\n\t\"Click to boost video stream if it is lagging\": \"Klicken um den Videostream zu verbessern, wenn er verzögert ist.\",\n\t\"Privacy Notice: Analytics in This App\": \"Datenschutzhinweis: Analyse in dieser App\",\n\t\"Analytics Reference\": \"Analytik-Referenz\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Was wir sammeln:\",\n\t\"Page views (which screens you visit)\": \"Seitenaufrufe (welche Ansichten du besuchst)\",\n\t\"Time spent on pages\": \"Verweildauer auf Seiten\",\n\t\"Basic device info (browser type, screen size)\": \"Grundlegende Geräteinformationen (Browsertyp, Bildschirmgröße)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Deine IP-Adresse (anonymisiert – der letzte Teil wird aus Datenschutzgründen entfernt)\",\n\t\"What we DON'T collect:\": \"Was wir NICHT sammeln:\",\n\t\"Personal info (names, emails, passwords)\": \"Personenbezogene Daten (Namen, E-Mails, Passwörter)\",\n\t\"Exact location\": \"Genauer Standort\",\n\t\"Any files or content you interact with\": \"Dateien oder Inhalte, mit denen du interagierst\",\n\t\"Why anonymous?\": \"Warum anonym?\",\n\t\"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.\",\n\t\"Your options:\": \"Deine Optionen:\",\n\t\"Continue:\": \"Weiter:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Wir erfassen anonymisierte Nutzung, um die App zu verbessern.\",\n\t\"Opt out:\": \"Ablehnen:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Datenempfänger: Google Analytics. Lies deren Datenschutzerklärung.\",\n\t\"Accept\": \"Akzeptieren\",\n\t\"Disagree\": \"Ablehnen\",\n\t\"re-initiate-connection\": \"Verbindung erneut herstellen\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/en/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Waiting for video stream of screen sharing device...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Waiting for user to select source to share from screen sharing device...\",\n\t\"My Device Info\": \"My Device Info\",\n\t\"Device Type\": \"Device Type\",\n\t\"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.\",\n\t\"Device IP\": \"Device IP\",\n\t\"Device Browser\": \"Device Browser\",\n\t\"Device OS\": \"Device OS\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Screen Viewer\",\n\t\"Connected!\": \"Connected!\",\n\t\"Error occurred\": \"Error occurred\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Error Dialog\",\n\t\"Something went wrong\": \"Something went wrong\",\n\t\"You may close this browser window then try to connect again\": \"You may close this browser window then try to connect again\",\n\t\"An unknown error occurred\": \"An unknown error occurred\",\n\t\"You were not allowed to connect\": \"You were not allowed to connect\",\n\t\"You were disconnected\": \"You were disconnected\",\n\t\"WebRTC error occurred\": \"WebRTC error occurred\",\n\t\"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.\",\n\t\"Donate\": \"Donate\",\n\t\"get-deskreen-pro\": \"Get Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Get Deskreen Pro - opens the download page.\",\n\t\"Video stream is paused\": \"Video stream is paused\",\n\t\"Video stream is playing\": \"Video stream is playing\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Video Settings\",\n\t\"Flip\": \"Flip\",\n\t\"Video quality has been changed to\": \"Video quality has been changed to\",\n\t\"Click to Open Video Settings\": \"Click to Open Video Settings\",\n\t\"Click to Enter Full Screen Mode\": \"Click to Enter Full Screen Mode\",\n\t\"Default video player has been turned OFF\": \"Default video player has been turned OFF\",\n\t\"Default video player has been turned ON\": \"Default video player has been turned ON\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"Default Video Player\",\n\t\"Click to visit our website\": \"Click to visit our website\",\n\t\"Video is flipped horizontally\": \"Video is flipped horizontally\",\n\t\"flip-the-screen-is-pro-version-only\": \"Flip the screen is pro version only\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/es/translation.json",
    "content": "{\n\t\"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 ...\",\n\t\"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 ...\",\n\t\"My Device Info\": \"Información de mi dispositivo\",\n\t\"Device Type\": \"Tipo del dispositivo\",\n\t\"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.\",\n\t\"Device IP\": \"IP del dispositivo\",\n\t\"Device Browser\": \"Navegador del dispositivo\",\n\t\"Device OS\": \"SO del dispositivo\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Visor de pantalla de Deskreen-CE\",\n\t\"Connected!\": \"¡Conectado!\",\n\t\"Error occurred\": \"Ocurrió un error\",\n\t\"Deskreen-CE Error Dialog\": \"Cuadro de diálogo de error de Deskreen-CE\",\n\t\"Something went wrong\": \"Algo salió mal\",\n\t\"You may close this browser window then try to connect again\": \"Puedes cerrar esta ventana del navegador y luego intentar conectarte nuevamente\",\n\t\"An unknown error occurred\": \"Ocurrió un error desconocido\",\n\t\"You were not allowed to connect\": \"No se te permitió conectarte\",\n\t\"You were disconnected\": \"Fuiste desconectado\",\n\t\"WebRTC error occurred\": \"Ocurrió un error de WebRTC\",\n\t\"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.\",\n\t\"Donate\": \"Donar\",\n\t\"get-deskreen-pro\": \"Obtener Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtener Deskreen Pro - abre la página de descarga.\",\n\t\"Video stream is paused\": \"La transmisión de video está en pausa\",\n\t\"Video stream is playing\": \"La transmisión de video está en reproducción\",\n\t\"Pause\": \"Pausa\",\n\t\"Play\": \"Reproducir\",\n\t\"Video Settings\": \"Configuraciones de video\",\n\t\"Flip\": \"Voltear\",\n\t\"Video quality has been changed to\": \"La calidad de video se ha cambiado a\",\n\t\"Click to Open Video Settings\": \"Clic para abrir las configuraciones de video\",\n\t\"Click to Enter Full Screen Mode\": \"Clic para entrar en el modo de pantalla completa\",\n\t\"Default video player has been turned OFF\": \"El reproductor de video predeterminado se ha APAGADO\",\n\t\"Default video player has been turned ON\": \"El reproductor de video predeterminado se ha ENCENDIDO\",\n\t\"ON\": \"ENCENDER\",\n\t\"OFF\": \"APAGAR\",\n\t\"Default Video Player\": \"Reproductor de video predeterminado\",\n\t\"Click to visit our website\": \"Clic para visitar nuestro sitio web\",\n\t\"Video is flipped horizontally\": \"El video se ha volteado horizontalmente\",\n\t\"flip-the-screen-is-pro-version-only\": \"Voltear la pantalla está disponible solo en la versión Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Clic para ver la información de la conexión\",\n\t\"Pair ID\": \"ID del par\",\n\t\"Unpair\": \"Desemparejar\",\n\t\"Session ID\": \"ID de sesión\",\n\t\"Click to boost video stream if it is lagging\": \"Haz clic para mejorar la transmisión de video si se está retrasando\",\n\t\"Privacy Notice: Analytics in This App\": \"Aviso de privacidad: Analítica en esta aplicación\",\n\t\"Analytics Reference\": \"Referencia de Analítica\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Lo que recopilamos:\",\n\t\"Page views (which screens you visit)\": \"Vistas de página (qué pantallas visitas)\",\n\t\"Time spent on pages\": \"Tiempo invertido en las páginas\",\n\t\"Basic device info (browser type, screen size)\": \"Información básica del dispositivo (tipo de navegador, tamaño de pantalla)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Tu dirección IP (anonimizada: se elimina la última parte por privacidad)\",\n\t\"What we DON'T collect:\": \"Lo que NO recopilamos:\",\n\t\"Personal info (names, emails, passwords)\": \"Información personal (nombres, correos electrónicos, contraseñas)\",\n\t\"Exact location\": \"Ubicación exacta\",\n\t\"Any files or content you interact with\": \"Cualquier archivo o contenido con el que interactúes\",\n\t\"Why anonymous?\": \"¿Por qué anónimo?\",\n\t\"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.\",\n\t\"Your options:\": \"Tus opciones:\",\n\t\"Continue:\": \"Continuar:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Registraremos uso anonimizado para ayudar a mejorar la aplicación.\",\n\t\"Opt out:\": \"Rechazar:\",\n\t\"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).\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Los datos van a: Google Analytics. Consulta su política de privacidad.\",\n\t\"Accept\": \"Aceptar\",\n\t\"Disagree\": \"Rechazar\",\n\t\"re-initiate-connection\": \"Restablecer conexión\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/fi/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Odotetaan että käyttäjä napsauttaa SALLI-painiketta ruudunjakolaitteessa...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Odotetaan että käyttäjä valitsee ruudunjakolaitteesta lähteen joka jaetaan...\",\n\t\"My Device Info\": \"Tiedot laitteestani\",\n\t\"Device Type\": \"Laitteen malli\",\n\t\"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ä.\",\n\t\"Device IP\": \"Laitteen IP\",\n\t\"Device Browser\": \"Laiteselain\",\n\t\"Device OS\": \"Laitteen käyttöjärjestelmä\",\n\t\"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ä\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE-ruutukatselin\",\n\t\"Connected!\": \"Yhdistetty!\",\n\t\"Error occurred\": \"Tapahtui virhe\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE:in virhekooste\",\n\t\"Something went wrong\": \"Jokin meni pieleen\",\n\t\"You may close this browser window then try to connect again\": \"Voit sulkea tämän selainikkunan koettaaksesi uudelleenyhdistämistä\",\n\t\"An unknown error occurred\": \"Ilmeni tuntematon virhe\",\n\t\"You were not allowed to connect\": \"Yhdistämistä ei sallittu\",\n\t\"You were disconnected\": \"Sinulta katkesi yhteys\",\n\t\"WebRTC error occurred\": \"Ilmeni WebRTC-virhe\",\n\t\"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.\",\n\t\"Donate\": \"Lahjoita\",\n\t\"get-deskreen-pro\": \"Hanki Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hanki Deskreen Pro - avaa lataussivun.\",\n\t\"Video stream is paused\": \"Videolähetys on tauolla\",\n\t\"Video stream is playing\": \"Videolähetys on käynnissä\",\n\t\"Pause\": \"Tauko\",\n\t\"Play\": \"Toista\",\n\t\"Video Settings\": \"Asetukset videolle\",\n\t\"Flip\": \"Käännä ympäri\",\n\t\"Video quality has been changed to\": \"Videon laatu muutettiin määreeseen\",\n\t\"Click to Open Video Settings\": \"Napsauta avataksesi videon asetukset\",\n\t\"Click to Enter Full Screen Mode\": \"Napsauta siirtyäksesi kokoruututilaan\",\n\t\"Default video player has been turned OFF\": \"Vakiollinen videotoisto-ohjelma on KYTKETTY POIS PÄÄLTÄ\",\n\t\"Default video player has been turned ON\": \"Vakiollinen videotoisto-ohjelma on KYTKETTY PÄÄLLE\",\n\t\"ON\": \"PÄÄLLÄ\",\n\t\"OFF\": \"POIS\",\n\t\"Default Video Player\": \"Vakiollinen videontoisto-ohjelma\",\n\t\"Click to visit our website\": \"Napsauta vieraillaksesi verkkosivustollamme\",\n\t\"Video is flipped horizontally\": \"Video käännetty vaakatasossa\",\n\t\"flip-the-screen-is-pro-version-only\": \"Näytön kääntäminen on saatavilla vain Pro-versiossa\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Napsauta katsoaksesi tietoja yhteydestäsi\",\n\t\"Pair ID\": \"Lateparin ID-tunniste\",\n\t\"Unpair\": \"Poista laiteparitus\",\n\t\"Session ID\": \"Istunnon ID-tunniste\",\n\t\"Click to boost video stream if it is lagging\": \"Napsauta lisätyöntöapua videovirtaukselle mikäli se hidastelee\",\n\t\"Privacy Notice: Analytics in This App\": \"Tietosuojailmoitus: Analytiikka tässä sovelluksessa\",\n\t\"Analytics Reference\": \"Analytiikan viite\",\n\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ä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.\",\n\t\"What we collect:\": \"Mitä keräämme:\",\n\t\"Page views (which screens you visit)\": \"Sivunäyttökerrat (mitä näkymiä käyt)\",\n\t\"Time spent on pages\": \"Sivuille käytetty aika\",\n\t\"Basic device info (browser type, screen size)\": \"Laitteen perustiedot (selaintyyppi, näytön koko)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP-osoitteesi (anonymisoitu — viimeinen osa poistetaan yksityisyyden suojaamiseksi)\",\n\t\"What we DON'T collect:\": \"Mitä emme kerää:\",\n\t\"Personal info (names, emails, passwords)\": \"Henkilötietoja (nimiä, sähköposteja, salasanoja)\",\n\t\"Exact location\": \"Tarkkaa sijaintia\",\n\t\"Any files or content you interact with\": \"Tiedostoja tai sisältöä, joiden kanssa olet vuorovaikutuksessa\",\n\t\"Why anonymous?\": \"Miksi anonyymisti?\",\n\t\"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.\",\n\t\"Your options:\": \"Vaihtoehtosi:\",\n\t\"Continue:\": \"Jatka:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Seuraamme anonymisoitua käyttöä sovelluksen parantamiseksi.\",\n\t\"Opt out:\": \"Kieltäydy:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Tiedot lähetetään: Google Analytics. Tutustu heidän tietosuojakäytäntöönsä.\",\n\t\"Accept\": \"Hyväksy\",\n\t\"Disagree\": \"Hylkää\",\n\t\"re-initiate-connection\": \"Käynnistä yhteys uudelleen\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/fr/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"En attente de la validation depuis l'appareil source...\",\n\t\"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...\",\n\t\"My Device Info\": \"Mes informations d'appareil\",\n\t\"Device Type\": \"Type d'appareil\",\n\t\"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é.\",\n\t\"Device IP\": \"IP de l'appareil\",\n\t\"Device Browser\": \"Navigateur de l'appareil\",\n\t\"Device OS\": \"OS de l'appareil\",\n\t\"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é..\",\n\t\"Deskreen-CE Screen Viewer\": \"Écran de visionnage Deskreen-CE\",\n\t\"Connected!\": \"Connecté!\",\n\t\"Error occurred\": \"Une erreur est survenue\",\n\t\"Deskreen-CE Error Dialog\": \"Boîte de dialogue d'erreur\",\n\t\"Something went wrong\": \"Quelque chose s'est mal passé\",\n\t\"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\",\n\t\"An unknown error occurred\": \"Une erreur inconnue s'est produite\",\n\t\"You were not allowed to connect\": \"Vous n'êtes pas autorisé à vous connecter\",\n\t\"You were disconnected\": \"Vous avez été déconnecté\",\n\t\"WebRTC error occurred\": \"Une erreur WebRTC s'est produite\",\n\t\"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.\",\n\t\"Donate\": \"Donner\",\n\t\"get-deskreen-pro\": \"Obtenir Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtenir Deskreen Pro - ouvre la page de téléchargement.\",\n\t\"Video stream is paused\": \"Le flux vidéo est en pause\",\n\t\"Video stream is playing\": \"Lecture du flux vidéo\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Lecture\",\n\t\"Video Settings\": \"Paramètres Vidéo\",\n\t\"Flip\": \"Tourner\",\n\t\"Video quality has been changed to\": \"Qualité de la vidéo changée en\",\n\t\"Click to Open Video Settings\": \"Cliquez pour ouvrir les paramètres vidéo\",\n\t\"Click to Enter Full Screen Mode\": \"Cliquez pour passer en plein écran\",\n\t\"Default video player has been turned OFF\": \"Le lecteur vidéo par défaut a été désactivé\",\n\t\"Default video player has been turned ON\": \"Le lecteur vidéo par défaut a été activé\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"Lecteur vidéo par défaut\",\n\t\"Click to visit our website\": \"Cliquez ici pour visiter notre site web\",\n\t\"Video is flipped horizontally\": \"La vidéo à été tourner horizontallement\",\n\t\"flip-the-screen-is-pro-version-only\": \"Retourner l'écran n'est disponible que dans la version Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Cliquez pour voir les informations de connexion\",\n\t\"Pair ID\": \"ID d'appairage\",\n\t\"Unpair\": \"Desappairer\",\n\t\"Session ID\": \"ID de session\",\n\t\"Click to boost video stream if it is lagging\": \"Cliquez pour booster le flux vidéo si vous rencontrez des ralentissements\",\n\t\"Privacy Notice: Analytics in This App\": \"Avis de confidentialité : Analyses dans cette application\",\n\t\"Analytics Reference\": \"Référence Analytique\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Ce que nous recueillons :\",\n\t\"Page views (which screens you visit)\": \"Pages consultées (les écrans que vous visitez)\",\n\t\"Time spent on pages\": \"Temps passé sur les pages\",\n\t\"Basic device info (browser type, screen size)\": \"Informations de base sur l'appareil (type de navigateur, taille de l'écran)\",\n\t\"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)\",\n\t\"What we DON'T collect:\": \"Ce que nous NE collectons PAS :\",\n\t\"Personal info (names, emails, passwords)\": \"Informations personnelles (noms, adresses e-mail, mots de passe)\",\n\t\"Exact location\": \"Localisation précise\",\n\t\"Any files or content you interact with\": \"Les fichiers ou contenus avec lesquels vous interagissez\",\n\t\"Why anonymous?\": \"Pourquoi anonyme ?\",\n\t\"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.\",\n\t\"Your options:\": \"Vos options :\",\n\t\"Continue:\": \"Continuer :\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Nous suivrons l'utilisation anonymisée pour aider à améliorer l'application.\",\n\t\"Opt out:\": \"Refuser :\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Les données sont envoyées à : Google Analytics. Consultez leur politique de confidentialité.\",\n\t\"Accept\": \"Accepter\",\n\t\"Disagree\": \"Refuser\",\n\t\"re-initiate-connection\": \"Réinitialiser la connexion\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/it/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Info del mio Dispositivo\",\n\t\"Device Type\": \"Tipologia Dispositivo\",\n\t\"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.\",\n\t\"Device IP\": \"IP Dispositivo\",\n\t\"Device Browser\": \"Browser Dispositivo\",\n\t\"Device OS\": \"OS Dispositivo\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Visualizzatore dello schermo di Deskreen-CE\",\n\t\"Connected!\": \"Connesso!\",\n\t\"Error occurred\": \"Si è verificato un Errore\",\n\t\"Deskreen-CE Error Dialog\": \"Finestra di dialogo degli errori di Deskreen-CE\",\n\t\"Something went wrong\": \"Qualcosa è andato storto\",\n\t\"You may close this browser window then try to connect again\": \"Puoi chiudere questa finestra del browser, quindi provare a connetterti di nuovo\",\n\t\"An unknown error occurred\": \"Si è verificato un errore sconosciuto\",\n\t\"You were not allowed to connect\": \"Non ti è stato permesso di connetterti\",\n\t\"You were disconnected\": \"Sei stato disconnesso\",\n\t\"WebRTC error occurred\": \"Si è verificato un errore WebRTC\",\n\t\"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.\",\n\t\"Donate\": \"Dona\",\n\t\"get-deskreen-pro\": \"Ottieni Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ottieni Deskreen Pro - apre la pagina di download.\",\n\t\"Video stream is paused\": \"Trasmissione Video in pausa\",\n\t\"Video stream is playing\": \"Trasmissione Video in riproduzione\",\n\t\"Pause\": \"Pausa\",\n\t\"Play\": \"Riproduci\",\n\t\"Video Settings\": \"Impostazioni Video\",\n\t\"Flip\": \"Capovolgi\",\n\t\"Video quality has been changed to\": \"La qualità Video è stata cambiata a\",\n\t\"Click to Open Video Settings\": \"Clicca per aprire le Impostazioni Video\",\n\t\"Click to Enter Full Screen Mode\": \"Clicca per entrare in modalità Schermo Intero\",\n\t\"Default video player has been turned OFF\": \"il player video predefinito è stato spento\",\n\t\"Default video player has been turned ON\": \"il player video predefinito è stato acceso\",\n\t\"ON\": \"Acceso\",\n\t\"OFF\": \"Spento\",\n\t\"Default Video Player\": \"Player Video Predefinito\",\n\t\"Click to visit our website\": \"Clicca per visitare il nostro sito\",\n\t\"Video is flipped horizontally\": \"Il Video è capovolto orizzontalmente\",\n\t\"flip-the-screen-is-pro-version-only\": \"Capovolgere lo schermo è disponibile solo nella versione Pro\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Clicca per vedere le info di connessione\",\n\t\"Pair ID\": \"ID Coppia\",\n\t\"Unpair\": \"Disaccoppia\",\n\t\"Session ID\": \"ID Sessione\",\n\t\"Click to boost video stream if it is lagging\": \"Clicca per incrementare il flusso video se sta andando a scatti\",\n\t\"Privacy Notice: Analytics in This App\": \"Informativa sulla privacy: Analisi in questa app\",\n\t\"Analytics Reference\": \"Riferimento Analitico\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Cosa raccogliamo:\",\n\t\"Page views (which screens you visit)\": \"Visualizzazioni di pagina (quali schermate visiti)\",\n\t\"Time spent on pages\": \"Tempo trascorso sulle pagine\",\n\t\"Basic device info (browser type, screen size)\": \"Informazioni di base sul dispositivo (tipo di browser, dimensioni dello schermo)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Il tuo indirizzo IP (anonimizzato — l'ultima parte viene rimossa per la privacy)\",\n\t\"What we DON'T collect:\": \"Cosa NON raccogliamo:\",\n\t\"Personal info (names, emails, passwords)\": \"Dati personali (nomi, email, password)\",\n\t\"Exact location\": \"Posizione esatta\",\n\t\"Any files or content you interact with\": \"Qualsiasi file o contenuto con cui interagisci\",\n\t\"Why anonymous?\": \"Perché anonimo?\",\n\t\"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.\",\n\t\"Your options:\": \"Le tue opzioni:\",\n\t\"Continue:\": \"Continua:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Tracceremo l'utilizzo anonimizzato per aiutare a migliorare l'app.\",\n\t\"Opt out:\": \"Rifiuta:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"I dati vengono inviati a: Google Analytics. Consulta la loro informativa sulla privacy.\",\n\t\"Accept\": \"Accetta\",\n\t\"Disagree\": \"Rifiuta\",\n\t\"re-initiate-connection\": \"Riavvia la connessione\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/ja/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"画面共有デバイスでユーザーが「許可」をクリックするのを待っています...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"画面共有デバイスから共有するソースをユーザーが選択するのを待っています...\",\n\t\"My Device Info\": \"このデバイスの情報\",\n\t\"Device Type\": \"デバイスの種類\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"Deskreen-CEが動作しているパソコンに表示されるアラートポップアップの\\\"デバイスIP\\\"と、このデバイスのデバイスIPが一致する必要があります。\",\n\t\"Device IP\": \"デバイスのIP\",\n\t\"Device Browser\": \"デバイスのブラウザ\",\n\t\"Device OS\": \"デバイスのOS\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"これらの内容は、画面共有デバイスのアラートポップアップに表示される内容と一致している必要があります。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Screen Viewer\",\n\t\"Connected!\": \"接続されました！\",\n\t\"Error occurred\": \"エラーが発生しました\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE エラーダイアログ\",\n\t\"Something went wrong\": \"何らかの問題が発生しました\",\n\t\"You may close this browser window then try to connect again\": \"このブラウザを閉じてから、再度接続を試みてください\",\n\t\"An unknown error occurred\": \"不明なエラーが発生しました\",\n\t\"You were not allowed to connect\": \"接続が許可されていません\",\n\t\"You were disconnected\": \"接続が切断されました\",\n\t\"WebRTC error occurred\": \"WebRTCエラーが発生しました\",\n\t\"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をより良いものにするためのモチベーションを保つことができます。\",\n\t\"Donate\": \"寄付\",\n\t\"get-deskreen-pro\": \"Deskreen Pro を入手\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro を入手 - ダウンロードページを開きます。\",\n\t\"Video stream is paused\": \"ビデオストリームを一時停止しています\",\n\t\"Video stream is playing\": \"ビデオストリームを再生中です\",\n\t\"Pause\": \"一時停止\",\n\t\"Play\": \"再生\",\n\t\"Video Settings\": \"ビデオ設定\",\n\t\"Flip\": \"反転\",\n\t\"Video quality has been changed to\": \"ビデオの画質を変更しました。画質：\",\n\t\"Click to Open Video Settings\": \"クリックしてビデオ設定を開きます\",\n\t\"Click to Enter Full Screen Mode\": \"クリックするとフルスクリーンモードになります\",\n\t\"Default video player has been turned OFF\": \"デフォルトのビデオプレーヤーがOFFになっています\",\n\t\"Default video player has been turned ON\": \"デフォルトのビデオプレーヤーがONになっています\",\n\t\"ON\": \"ON\",\n\t\"OFF\": \"OFF\",\n\t\"Default Video Player\": \"デフォルトのビデオプレーヤー\",\n\t\"Click to visit our website\": \"クリックするとウェブサイトが開きます\",\n\t\"Video is flipped horizontally\": \"映像が水平方向に反転しています\",\n\t\"flip-the-screen-is-pro-version-only\": \"画面の反転はPro版でのみ利用可能です\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"クリックすると接続情報が表示されます\",\n\t\"Pair ID\": \"ペアID\",\n\t\"Unpair\": \"ペア解除\",\n\t\"Session ID\": \"セッションID\",\n\t\"Click to boost video stream if it is lagging\": \"クリックすると、ビデオストリームが遅延している場合、ブーストされます\",\n\t\"Privacy Notice: Analytics in This App\": \"プライバシーに関するお知らせ：このアプリでの解析について\",\n\t\"Analytics Reference\": \"分析参照\",\n\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.\": \"このアプリは Google が提供する無料サービスの Google Analytics を使用して、基本的な利用データを匿名で追跡します。アプリの使われ方を理解し、すべてのユーザーのために改善するためです。\",\n\t\"What we collect:\": \"収集するデータ:\",\n\t\"Page views (which screens you visit)\": \"ページビュー（どの画面を表示したか）\",\n\t\"Time spent on pages\": \"ページに滞在した時間\",\n\t\"Basic device info (browser type, screen size)\": \"基本的な端末情報（ブラウザーの種類、画面サイズ）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP アドレス（匿名化 — プライバシー保護のため末尾を削除）\",\n\t\"What we DON'T collect:\": \"収集しないもの:\",\n\t\"Personal info (names, emails, passwords)\": \"個人情報（氏名、メールアドレス、パスワード）\",\n\t\"Exact location\": \"正確な位置情報\",\n\t\"Any files or content you interact with\": \"操作したファイルやコンテンツ\",\n\t\"Why anonymous?\": \"なぜ匿名なのですか？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"IP アドレスは自動的に短縮され、このデータから個人を特定することはできません。\",\n\t\"Your options:\": \"選択肢:\",\n\t\"Continue:\": \"続行:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"アプリ改善のために匿名化された利用状況を追跡します。\",\n\t\"Opt out:\": \"オプトアウト:\",\n\t\"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.)\": \"下の「同意しない」ボタンをクリックすると追跡を無効にできます。（この選択は尊重しますが、総合的なフィードバックに基づく将来の改善を逃す可能性があります。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"データ送信先：Google Analytics。プライバシーポリシーをご確認ください。\",\n\t\"Accept\": \"同意する\",\n\t\"Disagree\": \"同意しない\",\n\t\"re-initiate-connection\": \"再接続\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/ko/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"공유할 기기의 사용자가 화면 공유 허용 버튼을 클릭하기를 기다리는 중 ...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"공유할 기기의 어떤 화면을 공유할지 선택을 기다리는 중...\",\n\t\"My Device Info\": \"내 기기 정보\",\n\t\"Device Type\": \"기기 종류\",\n\t\"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\\\" 와 같아야 합니다.\",\n\t\"Device IP\": \"기기 IP\",\n\t\"Device Browser\": \"기기 브라우저\",\n\t\"Device OS\": \"기기 OS\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"세부 사항은 화면 공유 장치에서 팝업에서 표시되는 것과 일치해야합니다.\",\n\t\"Deskreen-CE Screen Viewer\": \"스크린 뷰어\",\n\t\"Connected!\": \"연결되었습니다.\",\n\t\"Error occurred\": \"오류가 발생했습니다\",\n\t\"Deskreen-CE Error Dialog\": \"오류 알림\",\n\t\"Something went wrong\": \"연결과정에 오류가 발생하였습니다\",\n\t\"You may close this browser window then try to connect again\": \"이 브라우저 창을 닫은 다음 다시 연결하십시오.\",\n\t\"An unknown error occurred\": \"알 수없는 오류가 발생했습니다\",\n\t\"You were not allowed to connect\": \"이 기기는 연결이 허용되지 않았습니다\",\n\t\"You were disconnected\": \"연결이 해제되었습니다\",\n\t\"WebRTC error occurred\": \"WebRTC 오류가 발생했습니다\",\n\t\"If you like Deskreen-CE consider contributing financially Deskreen-CE is open-source Your donations keep us motivated to make Deskreen-CE even better\": \"오픈소스 프로젝트에 재정적으로 기여하는 것은 더 좋은 프로그램 개발 동기를 부여합니다.\",\n\t\"Donate\": \"기부하기\",\n\t\"get-deskreen-pro\": \"Deskreen Pro 받기\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro 받기 - 다운로드 페이지를 엽니다.\",\n\t\"Video stream is paused\": \"비디오 스트림이 일시 중지됩니다\",\n\t\"Video stream is playing\": \"비디오 스트림이 재생 중입니다\",\n\t\"Pause\": \"중지\",\n\t\"Play\": \"재생\",\n\t\"Video Settings\": \"비디오 설정\",\n\t\"Flip\": \"화면 좌우 반전\",\n\t\"Video quality has been changed to\": \"비디오 품질이 변경되었습니다\",\n\t\"Click to Open Video Settings\": \"비디오 설정 열기\",\n\t\"Click to Enter Full Screen Mode\": \"전체 화면 모드로 들어가려면 클릭하십시오\",\n\t\"Default video player has been turned OFF\": \"기본 비디오 플레이어가 꺼져 있습니다\",\n\t\"Default video player has been turned ON\": \"기본 비디오 플레이어가 켜져 있습니다\",\n\t\"ON\": \"켜짐\",\n\t\"OFF\": \"꺼짐\",\n\t\"Default Video Player\": \"기본 비디오 플레이어\",\n\t\"Click to visit our website\": \"클릭하면 웹사이트를 방문합니다\",\n\t\"Video is flipped horizontally\": \"비디오를 수평으로 뒤집습니다\",\n\t\"flip-the-screen-is-pro-version-only\": \"화면 뒤집기는 Pro 버전에서만 사용할 수 있습니다\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"연결 정보를 보려면 클릭하십시오\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"클릭하면 비디오 스트림을 향상시킬 수 있습니다\",\n\t\"Privacy Notice: Analytics in This App\": \"개인정보 안내: 이 앱의 분석\",\n\t\"Analytics Reference\": \"분석 참조\",\n\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.\": \"이 앱은 Google Analytics(구글에서 제공하는 무료 서비스)를 사용하여 기본 사용 데이터를 익명으로 추적합니다. 이를 통해 사람들이 앱을 어떻게 사용하는지 이해하고 모두를 위해 개선할 수 있습니다.\",\n\t\"What we collect:\": \"수집하는 정보:\",\n\t\"Page views (which screens you visit)\": \"페이지 조회수(어떤 화면을 방문하는지)\",\n\t\"Time spent on pages\": \"페이지에 머문 시간\",\n\t\"Basic device info (browser type, screen size)\": \"기본 기기 정보(브라우저 종류, 화면 크기)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"IP 주소(익명 처리됨 — 개인정보 보호를 위해 마지막 부분이 제거됩니다)\",\n\t\"What we DON'T collect:\": \"수집하지 않는 정보:\",\n\t\"Personal info (names, emails, passwords)\": \"개인 정보(이름, 이메일, 비밀번호)\",\n\t\"Exact location\": \"정확한 위치\",\n\t\"Any files or content you interact with\": \"사용자가 상호작용하는 파일이나 콘텐츠\",\n\t\"Why anonymous?\": \"왜 익명으로 수집하나요?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"IP 주소는 자동으로 축약되며, 이 데이터로 개인을 식별할 수 없습니다.\",\n\t\"Your options:\": \"선택 사항:\",\n\t\"Continue:\": \"계속:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"앱을 개선하기 위해 익명화된 사용 데이터를 추적합니다.\",\n\t\"Opt out:\": \"거부:\",\n\t\"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.)\": \"아래의 동의하지 않음 버튼을 클릭하여 추적을 비활성화하세요. (이 선택을 존중하지만, 공동 피드백에 기반한 향후 개선 사항을 놓칠 수 있습니다.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"데이터가 전송되는 곳: Google Analytics. 개인정보 보호정책을 확인하세요.\",\n\t\"Accept\": \"동의\",\n\t\"Disagree\": \"동의하지 않음\",\n\t\"re-initiate-connection\": \"연결 재시작\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/nl/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Mijn Apparaat Info\",\n\t\"Device Type\": \"Apparaat Type\",\n\t\"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\",\n\t\"Device IP\": \"Apparaat IP\",\n\t\"Device Browser\": \"Apparaat Browser\",\n\t\"Device OS\": \"Apparaat OS\",\n\t\"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\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE Scherm Viewer\",\n\t\"Connected!\": \"Verbonden!\",\n\t\"Error occurred\": \"Fout opgetreden\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE Error Dialoog\",\n\t\"Something went wrong\": \"Er is iets misgegaan\",\n\t\"You may close this browser window then try to connect again\": \"U mag dit browser venster sluiten en opnieuw proberen te verbinden\",\n\t\"An unknown error occurred\": \"Een onbekende fout is opgetreden\",\n\t\"You were not allowed to connect\": \"Uw verbinding werd niet toegestaan\",\n\t\"You were disconnected\": \"Uw verbinding werd verbroken\",\n\t\"WebRTC error occurred\": \"WebRTC fout opgetreden\",\n\t\"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.\",\n\t\"Donate\": \"Doneer\",\n\t\"get-deskreen-pro\": \"Ontvang Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ontvang Deskreen Pro - opent de downloadpagina.\",\n\t\"Video stream is paused\": \"Video stream is gepauzeerd\",\n\t\"Video stream is playing\": \"Video stream wordt afgespeeld\",\n\t\"Pause\": \"Pauze\",\n\t\"Play\": \"Afspelen\",\n\t\"Video Settings\": \"Video Instellingen\",\n\t\"Flip\": \"Flip\",\n\t\"Video quality has been changed to\": \"Video kwaliteit is aangepast naar\",\n\t\"Click to Open Video Settings\": \"Klik om Video Instellingen te openen\",\n\t\"Click to Enter Full Screen Mode\": \"Klik om Volledig Scherm modus te activeren\",\n\t\"Default video player has been turned OFF\": \"Standaard video speler staat nu UIT\",\n\t\"Default video player has been turned ON\": \"Standaard video speler staat nu AAN\",\n\t\"ON\": \"AAN\",\n\t\"OFF\": \"UIT\",\n\t\"Default Video Player\": \"Standaard Video Speler\",\n\t\"Click to visit our website\": \"Klik om onze website te bezoeken\",\n\t\"Video is flipped horizontally\": \"Video is horizontaal geflipt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Scherm omdraaien is alleen beschikbaar in de Pro-versie\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klik om verbindings informatie te zien\",\n\t\"Pair ID\": \"Koppel ID\",\n\t\"Unpair\": \"Ontkoppelen\",\n\t\"Session ID\": \"Sessie ID\",\n\t\"Click to boost video stream if it is lagging\": \"Klik om de video stream te versterken als het traag is\",\n\t\"Privacy Notice: Analytics in This App\": \"Privacyverklaring: Analyse in deze app\",\n\t\"Analytics Reference\": \"Analytiek Referentie\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Wat we verzamelen:\",\n\t\"Page views (which screens you visit)\": \"Paginaweergaven (welke schermen je bezoekt)\",\n\t\"Time spent on pages\": \"Tijd doorgebracht op pagina's\",\n\t\"Basic device info (browser type, screen size)\": \"Basisapparaatinformatie (browsertype, schermgrootte)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Je IP-adres (geanonimiseerd — het laatste deel wordt verwijderd voor je privacy)\",\n\t\"What we DON'T collect:\": \"Wat we NIET verzamelen:\",\n\t\"Personal info (names, emails, passwords)\": \"Persoonlijke info (namen, e-mails, wachtwoorden)\",\n\t\"Exact location\": \"Exacte locatie\",\n\t\"Any files or content you interact with\": \"Bestanden of inhoud waarmee je interacteert\",\n\t\"Why anonymous?\": \"Waarom anoniem?\",\n\t\"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.\",\n\t\"Your options:\": \"Je opties:\",\n\t\"Continue:\": \"Doorgaan:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"We volgen geanonimiseerd gebruik om de app te verbeteren.\",\n\t\"Opt out:\": \"Afmelden:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Gegevens gaan naar: Google Analytics. Bekijk hun privacybeleid.\",\n\t\"Accept\": \"Accepteren\",\n\t\"Disagree\": \"Weigeren\",\n\t\"re-initiate-connection\": \"Verbinding opnieuw starten\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/ru/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Ждем когда пользователь нажмет кнопку РАЗРЕШИТЬ для доступа к экрану компьютера...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Ждем когда пользователь выберет Весь экран или Окно приложения для отображения его здесь...\",\n\t\"My Device Info\": \"Информация о моем устройстве\",\n\t\"Device Type\": \"Тип устройства\",\n\t\"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.\",\n\t\"Device IP\": \"IP-aдрес устройства\",\n\t\"Device Browser\": \"Веб-браузер устройства\",\n\t\"Device OS\": \"ОС устройства\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"Эти данные должны совпадать с теми, которые вы видите во всплывающем окне предупреждения на экране компьютера, на котором работает Deskreen-CE.\",\n\t\"Deskreen-CE Screen Viewer\": \"Просмотрщик экрана Deskreen-CE\",\n\t\"Connected!\": \"Подключено!\",\n\t\"Error occurred\": \"Произошла ошибка\",\n\t\"Deskreen-CE Error Dialog\": \"Диалог ошибки Deskreen-CE\",\n\t\"Something went wrong\": \"Произошло что-то не так\",\n\t\"You may close this browser window then try to connect again\": \"Вы можете закрыть это окно браузера и попытаться подключиться снова\",\n\t\"An unknown error occurred\": \"Произошла неизвестная ошибка\",\n\t\"You were not allowed to connect\": \"Вам не разрешили подключиться\",\n\t\"You were disconnected\": \"Вы были отключены\",\n\t\"WebRTC error occurred\": \"Произошла ошибка WebRTC\",\n\t\"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 еще лучше.\",\n\t\"Donate\": \"Пожертвовать\",\n\t\"get-deskreen-pro\": \"Получить Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Получить Deskreen Pro - открывает страницу загрузки.\",\n\t\"Video stream is paused\": \"Видеопоток приостановлен\",\n\t\"Video stream is playing\": \"Видеопоток воспроизводится\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Настройки видео\",\n\t\"Flip\": \"Отзеркалить\",\n\t\"Video quality has been changed to\": \"Качество видео изменено на\",\n\t\"Click to Open Video Settings\": \"Нажмите, чтобы открыть настройки видео\",\n\t\"Click to Enter Full Screen Mode\": \"Нажмите, чтобы перейти в полноэкранный режим\",\n\t\"Default video player has been turned OFF\": \"Видеоплеер по умолчанию отключен\",\n\t\"Default video player has been turned ON\": \"Видеопроигрыватель по умолчанию включен\",\n\t\"ON\": \"ВКЛ\",\n\t\"OFF\": \"ВЫКЛ\",\n\t\"Default Video Player\": \"Видеоплеер по умолчанию\",\n\t\"Click to visit our website\": \"Нажмите, чтобы посетить наш сайт\",\n\t\"Video is flipped horizontally\": \"Видео отзеркалено\",\n\t\"flip-the-screen-is-pro-version-only\": \"Отзеркаливание экрана доступно только в Pro версии\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"Privacy Notice: Analytics in This App\": \"Уведомление о конфиденциальности: аналитика в этом приложении\",\n\t\"Analytics Reference\": \"Справочник по аналитике\",\n\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.\": \"Это приложение использует Google Analytics (бесплатный сервис Google), чтобы анонимно отслеживать базовые данные использования. Это помогает нам понимать, как люди пользуются приложением, чтобы улучшать его для всех.\",\n\t\"What we collect:\": \"Что мы собираем:\",\n\t\"Page views (which screens you visit)\": \"Просмотры страниц (какие экраны вы посещаете)\",\n\t\"Time spent on pages\": \"Время, проведённое на страницах\",\n\t\"Basic device info (browser type, screen size)\": \"Базовую информацию об устройстве (тип браузера, размер экрана)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Ваш IP-адрес (анонимизированный — последняя часть удалена для защиты приватности)\",\n\t\"What we DON'T collect:\": \"Чего мы НЕ собираем:\",\n\t\"Personal info (names, emails, passwords)\": \"Персональные данные (имена, электронные адреса, пароли)\",\n\t\"Exact location\": \"Точное местоположение\",\n\t\"Any files or content you interact with\": \"Любые файлы или контент, с которыми вы взаимодействуете\",\n\t\"Why anonymous?\": \"Почему анонимно?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"Ваш IP автоматически сокращается, и никто не сможет идентифицировать вас лично по этим данным.\",\n\t\"Your options:\": \"Ваши варианты:\",\n\t\"Continue:\": \"Продолжить:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Мы будем отслеживать анонимизированное использование, чтобы улучшать приложение.\",\n\t\"Opt out:\": \"Отказаться:\",\n\t\"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.)\": \"Нажмите кнопку Отклонить ниже, чтобы отключить отслеживание. (Мы уважим этот выбор, но вы можете пропустить будущие улучшения, основанные на коллективной обратной связи.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Данные отправляются в: Google Analytics. Ознакомьтесь с их политикой конфиденциальности.\",\n\t\"Accept\": \"Принять\",\n\t\"Disagree\": \"Отклонить\",\n\t\"re-initiate-connection\": \"Повторить подключение\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/sv/translation.json",
    "content": "{\n\t\"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...\",\n\t\"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...\",\n\t\"My Device Info\": \"Min enhetsinformation\",\n\t\"Device Type\": \"Enhetens typ\",\n\t\"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\",\n\t\"Device IP\": \"Enhetens IP\",\n\t\"Device Browser\": \"Enhetens webbläsare\",\n\t\"Device OS\": \"Enhetens operativsystem\",\n\t\"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.\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE skärmvisare\",\n\t\"Connected!\": \"Ansluten!\",\n\t\"Error occurred\": \"Ett fel inträffade\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE felhanterare\",\n\t\"Something went wrong\": \"Något blev fel\",\n\t\"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\",\n\t\"An unknown error occurred\": \"Ett okänt fel inträffade\",\n\t\"You were not allowed to connect\": \"Du fick inte ansluta\",\n\t\"You were disconnected\": \"Du blev nedkopplad\",\n\t\"WebRTC error occurred\": \"Ett WebRTC-fel error inträffade\",\n\t\"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.\",\n\t\"Donate\": \"Donera\",\n\t\"get-deskreen-pro\": \"Hämta Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hämta Deskreen Pro - öppnar nedladdningssidan.\",\n\t\"Video stream is paused\": \"Videoströmmen är pausad\",\n\t\"Video stream is playing\": \"Videoströmmen spelas\",\n\t\"Pause\": \"Paus\",\n\t\"Play\": \"Kör\",\n\t\"Video Settings\": \"Videoinställningar\",\n\t\"Flip\": \"Omvänd\",\n\t\"Video quality has been changed to\": \"Videokvaliteten har ändrats till\",\n\t\"Click to Open Video Settings\": \"Klicka här för att öppna videoinställningarna\",\n\t\"Click to Enter Full Screen Mode\": \"Klicka här för att gå in i helskärmsläge\",\n\t\"Default video player has been turned OFF\": \"Standardvideospelaren har stängts av\",\n\t\"Default video player has been turned ON\": \"Standardvideospelaren har aktiverats\",\n\t\"ON\": \"PÅ\",\n\t\"OFF\": \"AV\",\n\t\"Default Video Player\": \"Standardvideospelare\",\n\t\"Click to visit our website\": \"Klicka här för att besöka vår webbplats\",\n\t\"Video is flipped horizontally\": \"Videon är vänd horisontellt\",\n\t\"flip-the-screen-is-pro-version-only\": \"Vända skärmen är endast tillgängligt i Pro-versionen\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Klicka här för att visa anslutningsinformationen\",\n\t\"Pair ID\": \"ID för sammankopplingen\",\n\t\"Unpair\": \"Ta bort sammankopplingen\",\n\t\"Session ID\": \"ID för sessionen\",\n\t\"Click to boost video stream if it is lagging\": \"Klicka för att öka videoströmmen om den släpar efter\",\n\t\"Privacy Notice: Analytics in This App\": \"Integritetsmeddelande: Analys i den här appen\",\n\t\"Analytics Reference\": \"Analysreferens\",\n\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.\": \"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.\",\n\t\"What we collect:\": \"Det vi samlar in:\",\n\t\"Page views (which screens you visit)\": \"Sidvisningar (vilka vyer du besöker)\",\n\t\"Time spent on pages\": \"Tid som spenderas på sidor\",\n\t\"Basic device info (browser type, screen size)\": \"Grundläggande enhetsinformation (webbläsartyp, skärmstorlek)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Din IP-adress (anonymiserad — den sista delen tas bort av integritetsskäl)\",\n\t\"What we DON'T collect:\": \"Det vi INTE samlar in:\",\n\t\"Personal info (names, emails, passwords)\": \"Personlig information (namn, e-postadresser, lösenord)\",\n\t\"Exact location\": \"Exakt plats\",\n\t\"Any files or content you interact with\": \"Filer eller innehåll du interagerar med\",\n\t\"Why anonymous?\": \"Varför anonymt?\",\n\t\"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.\",\n\t\"Your options:\": \"Dina alternativ:\",\n\t\"Continue:\": \"Fortsätt:\",\n\t\"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.\",\n\t\"Opt out:\": \"Avslå:\",\n\t\"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.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Data skickas till: Google Analytics. Se deras integritetspolicy.\",\n\t\"Accept\": \"Acceptera\",\n\t\"Disagree\": \"Avböj\",\n\t\"re-initiate-connection\": \"Återanslut\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/ua/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"Чекаємо коли користувач натисне кнопку ДОЗВОЛИТИ для доступу до екрану комп'ютера...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"Чекаємо коли користувач вибере Весь екран або Вікно додатка для відображення його тут...\",\n\t\"My Device Info\": \"Інформація про мій пристрій\",\n\t\"Device Type\": \"Тип пристрою\",\n\t\"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.\",\n\t\"Device IP\": \"IP-aдрес пристрою\",\n\t\"Device Browser\": \"Веб-браузер пристрою\",\n\t\"Device OS\": \"ОС пристрою\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"Ці деталі повинні збігатися з тими, які ви бачите у спливаючому вікні сповіщень на екрані комп’ютера, де запущений Deskreen-CE.\",\n\t\"Deskreen-CE Screen Viewer\": \"Переглядач екрану Deskreen-CE\",\n\t\"Connected!\": \"Підключено!\",\n\t\"Error occurred\": \"Виникла помилка\",\n\t\"Deskreen-CE Error Dialog\": \"Діалог помилки Deskreen-CE\",\n\t\"Something went wrong\": \"Щось не так сталося\",\n\t\"You may close this browser window then try to connect again\": \"Ви можете закрити це вікно браузера та спробувати підключитися знову\",\n\t\"An unknown error occurred\": \"Виникла невідома помилка\",\n\t\"You were not allowed to connect\": \"Вам не дозволили підключитися\",\n\t\"You were disconnected\": \"Ви були відключені\",\n\t\"WebRTC error occurred\": \"Сталася помилка WebRTC\",\n\t\"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 ще краще.\",\n\t\"Donate\": \"Пожертвувати\",\n\t\"get-deskreen-pro\": \"Отримати Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Отримати Deskreen Pro - відкриває сторінку завантаження.\",\n\t\"Video stream is paused\": \"Відеопотік призупинено\",\n\t\"Video stream is playing\": \"Відеопотік продовжується\",\n\t\"Pause\": \"Pause\",\n\t\"Play\": \"Play\",\n\t\"Video Settings\": \"Настройки видео\",\n\t\"Flip\": \"Віддзеркалити\",\n\t\"Video quality has been changed to\": \"Якість відео змінено на\",\n\t\"Click to Open Video Settings\": \"Натисніть, щоб відкрити настройки відео\",\n\t\"Click to Enter Full Screen Mode\": \"Натисніть для входу в повноекранноий режим\",\n\t\"Default video player has been turned OFF\": \"Стандартний відеоплеєр браузера вимкнено\",\n\t\"Default video player has been turned ON\": \"Стандартний відеоплеєр браузера включений\",\n\t\"ON\": \"ВКЛ\",\n\t\"OFF\": \"ВИМК\",\n\t\"Default Video Player\": \"Стандартний відеоплеєр браузера\",\n\t\"Click to visit our website\": \"Клацніть, щоб відвідати наш веб-сайт\",\n\t\"Video is flipped horizontally\": \"Відео віддзеркалено\",\n\t\"flip-the-screen-is-pro-version-only\": \"Віддзеркалення екрану доступне лише в Pro версії\",\n\t\"TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED\": \"\",\n\t\"Click to see connection info\": \"Click to see connection info\",\n\t\"Pair ID\": \"Pair ID\",\n\t\"Unpair\": \"Unpair\",\n\t\"Session ID\": \"Session ID\",\n\t\"Click to boost video stream if it is lagging\": \"Click to boost video stream if it is lagging\",\n\t\"Privacy Notice: Analytics in This App\": \"Повідомлення про конфіденційність: аналітика в цьому застосунку\",\n\t\"Analytics Reference\": \"Довідник з аналітики\",\n\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.\": \"Цей застосунок використовує Google Analytics (безкоштовний сервіс Google), щоб анонімно відстежувати базові дані використання. Це допомагає нам розуміти, як люди користуються застосунком, аби покращувати його для всіх.\",\n\t\"What we collect:\": \"Що ми збираємо:\",\n\t\"Page views (which screens you visit)\": \"Перегляди сторінок (які екрани ви відвідуєте)\",\n\t\"Time spent on pages\": \"Час, проведений на сторінках\",\n\t\"Basic device info (browser type, screen size)\": \"Базову інформацію про пристрій (тип браузера, розмір екрана)\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"Вашу IP-адресу (анонімізовану — остання частина вилучається для приватності)\",\n\t\"What we DON'T collect:\": \"Що ми НЕ збираємо:\",\n\t\"Personal info (names, emails, passwords)\": \"Особисту інформацію (імена, електронні адреси, паролі)\",\n\t\"Exact location\": \"Точне місцезнаходження\",\n\t\"Any files or content you interact with\": \"Будь-які файли чи вміст, з якими ви взаємодієте\",\n\t\"Why anonymous?\": \"Чому анонімно?\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"Вашу IP автоматично скорочують, тому ніхто не може ідентифікувати вас особисто за цими даними.\",\n\t\"Your options:\": \"Ваші варіанти:\",\n\t\"Continue:\": \"Продовжити:\",\n\t\"We'll track anonymized usage to help improve the app.\": \"Ми відстежуватимемо анонімізоване використання, щоб допомогти покращити застосунок.\",\n\t\"Opt out:\": \"Відмовитися:\",\n\t\"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.)\": \"Натисніть кнопку Відхилити нижче, щоб вимкнути відстеження. (Ми поважаємо цей вибір, але ви можете пропустити майбутні покращення, що базуються на спільному зворотному зв'язку.)\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"Дані надсилаються до: Google Analytics. Перегляньте їхню політику конфіденційності.\",\n\t\"Accept\": \"Погодитися\",\n\t\"Disagree\": \"Відхилити\",\n\t\"re-initiate-connection\": \"Повторно підключитися\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/zh_CN/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"正在等待用户单击屏幕共享设备上的允许按钮...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"正在等待用户从屏幕共享设备选择要共享的源...\",\n\t\"My Device Info\": \"我的设备信息\",\n\t\"Device Type\": \"设备类型\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"您的设备 IP 应该与运行 Deskreen-CE 的计算机上出现的警报弹出窗口中的 '设备 IP' 相匹配。\",\n\t\"Device IP\": \"设备 IP\",\n\t\"Device Browser\": \"设备浏览器\",\n\t\"Device OS\": \"设备操作系统\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"这些详细信息应与您在屏幕共享设备上的警报弹出窗口中看到的信息相匹配。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE 屏幕查看器\",\n\t\"Connected!\": \"已连接!\",\n\t\"Error occurred\": \"出现错误\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE 错误对话框\",\n\t\"Something went wrong\": \"出问题了\",\n\t\"You may close this browser window then try to connect again\": \"您可以关闭此浏览器窗口，然后尝试重新连接\",\n\t\"An unknown error occurred\": \"出现未知错误\",\n\t\"You were not allowed to connect\": \"您不能连接\",\n\t\"You were disconnected\": \"您将断开连接\",\n\t\"WebRTC error occurred\": \"出现 WebRTC 错误\",\n\t\"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 变得更好。\",\n\t\"Donate\": \"捐赠\",\n\t\"get-deskreen-pro\": \"获取 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"获取 Deskreen Pro - 打开下载页面。\",\n\t\"Video stream is paused\": \"视频流暂停\",\n\t\"Video stream is playing\": \"正在播放视频流\",\n\t\"Pause\": \"暂停\",\n\t\"Play\": \"播放\",\n\t\"Video Settings\": \"视频设置\",\n\t\"Flip\": \"翻转\",\n\t\"Video quality has been changed to\": \"视频质量已更改为\",\n\t\"Click to Open Video Settings\": \"单击以打开视频设置\",\n\t\"Click to Enter Full Screen Mode\": \"单击以进入全屏模式\",\n\t\"Default video player has been turned OFF\": \"默认视频播放器已关闭\",\n\t\"Default video player has been turned ON\": \"默认视频播放器已开启\",\n\t\"ON\": \"开启\",\n\t\"OFF\": \"关闭\",\n\t\"Default Video Player\": \"默认视频播放器\",\n\t\"Click to visit our website\": \"点击访问我们的网站\",\n\t\"Video is flipped horizontally\": \"视频水平翻转\",\n\t\"flip-the-screen-is-pro-version-only\": \"翻转屏幕仅在 Pro 版本中可用\",\n\t\"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，但欢迎您的翻译！功能将很快添加，因此需要您的翻译\",\n\t\"Click to see connection info\": \"单击以查看连接信息\",\n\t\"Pair ID\": \"配对 ID\",\n\t\"Unpair\": \"取消配对\",\n\t\"Session ID\": \"会话 ID\",\n\t\"Click to boost video stream if it is lagging\": \"如果视频流滞后，请单击以提高视频流\",\n\t\"Privacy Notice: Analytics in This App\": \"隐私提示：本应用中的分析\",\n\t\"Analytics Reference\": \"分析参考\",\n\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.\": \"本应用使用 Google Analytics（Google 提供的免费服务）匿名跟踪基本的使用数据。这有助于我们了解用户如何使用应用，从而为所有人改进。\",\n\t\"What we collect:\": \"我们收集：\",\n\t\"Page views (which screens you visit)\": \"页面浏览量（你访问的界面）\",\n\t\"Time spent on pages\": \"在页面停留的时间\",\n\t\"Basic device info (browser type, screen size)\": \"设备基本信息（浏览器类型、屏幕尺寸）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"你的 IP 地址（已匿名化——出于隐私保护会移除最后一部分）\",\n\t\"What we DON'T collect:\": \"我们不会收集：\",\n\t\"Personal info (names, emails, passwords)\": \"个人信息（姓名、邮箱、密码）\",\n\t\"Exact location\": \"精确位置\",\n\t\"Any files or content you interact with\": \"你接触的任何文件或内容\",\n\t\"Why anonymous?\": \"为什么是匿名的？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"你的 IP 会自动被截短，任何人都无法通过这些数据识别你的身份。\",\n\t\"Your options:\": \"你的选择：\",\n\t\"Continue:\": \"继续：\",\n\t\"We'll track anonymized usage to help improve the app.\": \"我们会跟踪匿名化的使用情况，以帮助改进应用。\",\n\t\"Opt out:\": \"拒绝：\",\n\t\"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.)\": \"点击下面的\\\"拒绝\\\"按钮以关闭跟踪。（我们会尊重这个选择，但你可能会错过基于集体反馈的未来改进。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"数据将发送至：Google Analytics。查看其隐私政策。\",\n\t\"Accept\": \"接受\",\n\t\"Disagree\": \"拒绝\",\n\t\"re-initiate-connection\": \"重新连接\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/locales/zh_TW/translation.json",
    "content": "{\n\t\"Waiting for user to click ALLOW button on screen sharing device...\": \"正在等待使用者單擊螢幕共享裝置上的允許按鈕...\",\n\t\"Waiting for user to select source to share from screen sharing device...\": \"正在等待使用者從螢幕共享裝置選擇要共享的源...\",\n\t\"My Device Info\": \"我的裝置資訊\",\n\t\"Device Type\": \"裝置型別\",\n\t\"Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running\": \"您的裝置 IP 應該與執行 Deskreen-CE 的計算機上出現的警報彈出視窗中的 '裝置 IP' 相匹配。\",\n\t\"Device IP\": \"裝置 IP\",\n\t\"Device Browser\": \"裝置瀏覽器\",\n\t\"Device OS\": \"裝置作業系統\",\n\t\"These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running\": \"這些詳細資訊應與您在螢幕共享裝置上的警報彈出視窗中看到的資訊相匹配。\",\n\t\"Deskreen-CE Screen Viewer\": \"Deskreen-CE 螢幕檢視器\",\n\t\"Connected!\": \"已連線!\",\n\t\"Error occurred\": \"出現錯誤\",\n\t\"Deskreen-CE Error Dialog\": \"Deskreen-CE 錯誤對話方塊\",\n\t\"Something went wrong\": \"出問題了\",\n\t\"You may close this browser window then try to connect again\": \"您可以關閉此瀏覽器視窗，然後嘗試重新連線\",\n\t\"An unknown error occurred\": \"出現未知錯誤\",\n\t\"You were not allowed to connect\": \"您不能連線\",\n\t\"You were disconnected\": \"您將斷開連線\",\n\t\"WebRTC error occurred\": \"出現 WebRTC 錯誤\",\n\t\"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 變得更好。\",\n\t\"Donate\": \"捐贈\",\n\t\"get-deskreen-pro\": \"取得 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"取得 Deskreen Pro - 開啟下載頁面。\",\n\t\"Video stream is paused\": \"影片流暫停\",\n\t\"Video stream is playing\": \"正在播放影片流\",\n\t\"Pause\": \"暫停\",\n\t\"Play\": \"播放\",\n\t\"Video Settings\": \"影片設定\",\n\t\"Flip\": \"翻轉\",\n\t\"Video quality has been changed to\": \"影片質量已更改為\",\n\t\"Click to Open Video Settings\": \"單擊以開啟影片設定\",\n\t\"Click to Enter Full Screen Mode\": \"單擊以進入全屏模式\",\n\t\"Default video player has been turned OFF\": \"預設影片播放器已關閉\",\n\t\"Default video player has been turned ON\": \"預設影片播放器已開啟\",\n\t\"ON\": \"開啟\",\n\t\"OFF\": \"關閉\",\n\t\"Default Video Player\": \"預設影片播放器\",\n\t\"Click to visit our website\": \"點選訪問我們的網站\",\n\t\"Video is flipped horizontally\": \"影片水平翻轉\",\n\t\"flip-the-screen-is-pro-version-only\": \"翻轉螢幕僅在 Pro 版本中可用\",\n\t\"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，但歡迎您的翻譯！功能將很快新增，因此需要您的翻譯\",\n\t\"Click to see connection info\": \"單擊以檢視連線資訊\",\n\t\"Pair ID\": \"配對 ID\",\n\t\"Unpair\": \"取消配對\",\n\t\"Session ID\": \"會話 ID\",\n\t\"Click to boost video stream if it is lagging\": \"如果影片流滯後，請單擊以提高影片流\",\n\t\"Privacy Notice: Analytics in This App\": \"隱私提示：此應用程式的分析\",\n\t\"Analytics Reference\": \"分析參考\",\n\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.\": \"此應用程式使用 Google Analytics（Google 提供的免費服務）以匿名方式追蹤基本使用資料。這有助於我們了解使用者如何使用應用程式，從而為所有人帶來改進。\",\n\t\"What we collect:\": \"我們收集：\",\n\t\"Page views (which screens you visit)\": \"頁面瀏覽量（你造訪的畫面）\",\n\t\"Time spent on pages\": \"在頁面停留的時間\",\n\t\"Basic device info (browser type, screen size)\": \"裝置基本資訊（瀏覽器類型、螢幕尺寸）\",\n\t\"Your IP address (anonymized — last part removed for privacy)\": \"你的 IP 位址（已匿名化 — 為保護隱私會移除最後一段）\",\n\t\"What we DON'T collect:\": \"我們不會收集：\",\n\t\"Personal info (names, emails, passwords)\": \"個人資訊（姓名、電子郵件、密碼）\",\n\t\"Exact location\": \"精確位置\",\n\t\"Any files or content you interact with\": \"你互動的任何檔案或內容\",\n\t\"Why anonymous?\": \"為什麼要匿名？\",\n\t\"Your IP is automatically shortened, and no one can identify you personally from this data.\": \"你的 IP 會自動被截短，沒有人能根據這些資料識別你的身份。\",\n\t\"Your options:\": \"你的選項：\",\n\t\"Continue:\": \"繼續：\",\n\t\"We'll track anonymized usage to help improve the app.\": \"我們會追蹤匿名化的使用情況，協助改進應用程式。\",\n\t\"Opt out:\": \"拒絕：\",\n\t\"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.)\": \"點擊下方的「不同意」按鈕以停用追蹤。（我們會尊重此選擇，但你可能會錯過基於集體回饋的未來改善。）\",\n\t\"Data goes to: Google Analytics. See their privacy policy.\": \"資料傳送至：Google Analytics。查看其隱私權政策。\",\n\t\"Accept\": \"接受\",\n\t\"Disagree\": \"不同意\",\n\t\"re-initiate-connection\": \"重新連線\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/manifest.json",
    "content": "{\n\t\"short_name\": \"Deskreen CE\",\n\t\"name\": \"Deskreen CE Makes Any Device a Second Screen For Your Computer\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"favicon.ico\",\n\t\t\t\"sizes\": \"64x64 32x32 24x24 16x16\",\n\t\t\t\"type\": \"image/x-icon\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"logo192.png\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"sizes\": \"192x192\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"logo512.png\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"sizes\": \"512x512\"\n\t\t}\n\t],\n\t\"start_url\": \".\",\n\t\"display\": \"standalone\",\n\t\"theme_color\": \"#000000\",\n\t\"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "src/client-viewer/src/assets/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/client-viewer/src/components/ConnectingIndicator/ConnectingIndicatorIcon.tsx",
    "content": "import { Icon } from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\n\ninterface ConnectingIndicatorIconProps {\n\tconnectionIconType: 'feed' | 'feed-subscribed';\n}\n\nfunction ConnectingIndicatorIcon(props: ConnectingIndicatorIconProps) {\n\tconst { connectionIconType } = props;\n\n\treturn (\n\t\t<Row\n\t\t\tmiddle=\"xs\"\n\t\t\tcenter=\"xs\"\n\t\t\tstyle={{\n\t\t\t\theight: '100%',\n\t\t\t\twidth: '100%',\n\t\t\t}}\n\t\t>\n\t\t\t<Col>\n\t\t\t\t<Icon\n\t\t\t\t\ticon={connectionIconType}\n\t\t\t\t\tsize={50}\n\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\ttransform: 'translateX(10px)',\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Col>\n\t\t</Row>\n\t);\n}\n\nexport default ConnectingIndicatorIcon;\n"
  },
  {
    "path": "src/client-viewer/src/components/ConnectingIndicator/LoadingSharingIcon.tsx",
    "content": "import { Icon } from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport PropagateLoader from 'react-spinners/PropagateLoader';\n\ninterface SelectSharingIconProps {\n\tloadingSharingIconType: LoadingSharingIconType;\n\tisShownLoadingSharingIcon: boolean;\n}\n\nfunction LoadingSharingIcon(props: SelectSharingIconProps) {\n\tconst {\n\t\tloadingSharingIconType: selectingSharingIconType,\n\t\tisShownLoadingSharingIcon: isShownSelectingSharingIcon,\n\t} = props;\n\n\treturn (\n\t\t<Row\n\t\t\tcenter=\"xs\"\n\t\t\ttop=\"xs\"\n\t\t\tstyle={{\n\t\t\t\theight: '100%',\n\t\t\t\twidth: '100%',\n\t\t\t\tmarginRight: '0px',\n\t\t\t\tmarginLeft: '0px',\n\t\t\t}}\n\t\t>\n\t\t\t<Col>\n\t\t\t\t<Row\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\theight: '50px',\n\t\t\t\t\t}}\n\t\t\t\t\tcenter=\"xs\"\n\t\t\t\t>\n\t\t\t\t\t<Col xs={8} md={4}>\n\t\t\t\t\t\t<PropagateLoader loading size={18} color=\"#5C7080\" />\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t<Col>\n\t\t\t\t\t\t{isShownSelectingSharingIcon && (\n\t\t\t\t\t\t\t<Icon icon={selectingSharingIconType} size={60} color=\"#5C7080\" />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Col>\n\t\t</Row>\n\t);\n}\n\nexport default LoadingSharingIcon;\n"
  },
  {
    "path": "src/client-viewer/src/components/ConnectingIndicator/index.tsx",
    "content": "import React from 'react';\nimport { Text } from '@blueprintjs/core';\nimport { Row } from 'react-flexbox-grid';\nimport ConnectingIndicatorIcon from './ConnectingIndicatorIcon';\nimport LoadingSharingIcon from './LoadingSharingIcon';\n\nconst basePulsingCircleStyles = {\n\tborderRadius: '100%',\n\tmarginLeft: 'auto',\n\tmarginRight: 'auto',\n\tleft: '0',\n\tright: '0',\n\ttextAlign: 'center',\n\tposition: 'absolute',\n\twidth: '100px',\n\theight: '100px',\n};\n\nfunction getConnectingStepContent(\n\tcurrentStep: number,\n\tconnectionIconType: ConnectionIconType,\n\tloadingSharingIconType: LoadingSharingIconType,\n\tisShownLoadingSharingIcon: boolean,\n) {\n\tconst pulsingCircle1Styles = {\n\t\t...basePulsingCircleStyles,\n\t\tzIndex: 1,\n\t\tbackgroundColor: 'rgba(43, 149, 214, 0.7)',\n\t} as React.CSSProperties;\n\n\tconst pulsingCircle2Styles = {\n\t\t...basePulsingCircleStyles,\n\t\tzIndex: 2,\n\t\tbackgroundColor: '#2B95D6',\n\t} as React.CSSProperties;\n\n\tconst pulsingCircle3Styles = {\n\t\t...basePulsingCircleStyles,\n\t\tbackgroundColor: '#15B371',\n\t} as React.CSSProperties;\n\n\tswitch (currentStep) {\n\t\tcase 1:\n\t\t\treturn (\n\t\t\t\t<div style={{ marginTop: '200px' }}>\n\t\t\t\t\t<div id=\"pulsing-circle-1\" style={pulsingCircle1Styles}></div>\n\t\t\t\t\t<div id=\"pulsing-circle-2\" style={pulsingCircle2Styles}>\n\t\t\t\t\t\t<ConnectingIndicatorIcon connectionIconType={connectionIconType} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\tcase 2:\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginTop: '200px',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tid=\"pulsing-circle-3\"\n\t\t\t\t\t\tclassName=\"pulse-3-once\"\n\t\t\t\t\t\tstyle={pulsingCircle3Styles}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ConnectingIndicatorIcon connectionIconType={connectionIconType} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\tcase 3:\n\t\t\treturn (\n\t\t\t\t<div style={{ width: '100%', margin: '200px auto 0 auto' }}>\n\t\t\t\t\t<LoadingSharingIcon\n\t\t\t\t\t\tisShownLoadingSharingIcon={isShownLoadingSharingIcon}\n\t\t\t\t\t\tloadingSharingIconType={loadingSharingIconType}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t);\n\t\tdefault:\n\t\t\treturn <Text>Error occurred :(</Text>;\n\t}\n}\n\ninterface ConnectingIndicatorProps {\n\tcurrentStep: number;\n\tconnectionIconType: ConnectionIconType;\n\tisShownSelectingSharingIcon: boolean;\n\tselectingSharingIconType: LoadingSharingIconType;\n}\n\nfunction ConnectingIndicator(props: ConnectingIndicatorProps) {\n\tconst {\n\t\tcurrentStep,\n\t\tconnectionIconType,\n\t\tisShownSelectingSharingIcon,\n\t\tselectingSharingIconType,\n\t} = props;\n\n\treturn (\n\t\t<>\n\t\t\t<Row\n\t\t\t\tid=\"connecting-screen\"\n\t\t\t\ttop=\"xs\"\n\t\t\t\tstyle={{\n\t\t\t\t\theight: '50vh',\n\t\t\t\t\twidth: '100%',\n\t\t\t\t\tmarginRight: '0px',\n\t\t\t\t\tmarginLeft: '0px',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{getConnectingStepContent(\n\t\t\t\t\tcurrentStep,\n\t\t\t\t\tconnectionIconType,\n\t\t\t\t\tselectingSharingIconType,\n\t\t\t\t\tisShownSelectingSharingIcon,\n\t\t\t\t)}\n\t\t\t</Row>\n\t\t</>\n\t);\n}\n\nexport default ConnectingIndicator;\n"
  },
  {
    "path": "src/client-viewer/src/components/ErrorDialog/ErrorMessageEnum.ts",
    "content": "export const ErrorMessage = {\n\tUNKNOWN_ERROR: 'An unknown error occurred',\n\tDENY_TO_CONNECT: 'You were not allowed to connect',\n\tDISCONNECTED: 'You were disconnected',\n\tNOT_ALLOWED: 'You were not allowed to connect',\n\tWEBRTC_ERROR: 'WebRTC error occurred',\n} as const;\n\nexport type ErrorMessageType = (typeof ErrorMessage)[keyof typeof ErrorMessage];\n"
  },
  {
    "path": "src/client-viewer/src/components/ErrorDialog/index.css",
    "content": ".error-dialog-backdrop {\n\tbackdrop-filter: blur(2px);\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/ErrorDialog/index.tsx",
    "content": "import { Classes, Dialog, Divider, H1, H2, H3, Icon } from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport './index.css';\nimport { type ErrorMessageType } from './ErrorMessageEnum';\n\ninterface ErrorDialogProps {\n\tisOpen: boolean;\n\tsetIsOpen: (isOpen: boolean) => void;\n\terrorMessage: ErrorMessageType;\n}\n\nfunction ErrorDialog(props: ErrorDialogProps) {\n\tconst { t } = useTranslation();\n\tconst { errorMessage, isOpen, setIsOpen } = props;\n\n\treturn (\n\t\t<Dialog\n\t\t\tclassName=\"error-dialog\"\n\t\t\tautoFocus\n\t\t\tcanEscapeKeyClose\n\t\t\tcanOutsideClickClose={false}\n\t\t\tenforceFocus\n\t\t\tisOpen={isOpen}\n\t\t\t// usePortal\n\t\t\tstyle={{\n\t\t\t\twidth: '90%',\n\t\t\t\tmaxWidth: '1200px',\n\t\t\t}}\n\t\t\tbackdropClassName=\"error-dialog-backdrop\"\n\t\t\tonClose={() => {\n\t\t\t\tsetIsOpen(false);\n\t\t\t}}\n\t\t>\n\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t<Col xs={12}>\n\t\t\t\t\t<H3 className={Classes.TEXT_MUTED}>\n\t\t\t\t\t\t{t('Deskreen CE Error Dialog')}\n\t\t\t\t\t</H3>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Row middle=\"xs\" center=\"xs\" style={{ padding: '20px', width: '100%' }}>\n\t\t\t\t<Col xs={12} md={10} lg={6}>\n\t\t\t\t\t<Row middle=\"xs\" center=\"xs\">\n\t\t\t\t\t\t<Col xs={1}>\n\t\t\t\t\t\t\t<Icon icon=\"error\" size={52} color=\"#8A9BA8\" />\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t<Col xs={11}>\n\t\t\t\t\t\t\t<H1\n\t\t\t\t\t\t\t\tclassName={Classes.TEXT_DISABLED}\n\t\t\t\t\t\t\t\tstyle={{ marginBottom: '0px' }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{`${t('Something went wrong')} :(`}\n\t\t\t\t\t\t\t</H1>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t</Row>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Divider />\n\t\t\t<div className={Classes.DIALOG_BODY}>\n\t\t\t\t<H3 className={Classes.TEXT_MUTED}>{t(`${errorMessage}`)}</H3>\n\t\t\t\t<Divider />\n\t\t\t\t<H2>{`${t('You may close this browser window then try to connect again')}.`}</H2>\n\t\t\t</div>\n\t\t</Dialog>\n\t);\n}\n\nexport default ErrorDialog;\n"
  },
  {
    "path": "src/client-viewer/src/components/LoadingScreen/LoadingScreen.css",
    "content": ".loading-screen {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground-color: #ffffff;\n\tz-index: 9999;\n}\n\n.css-spinner {\n\twidth: 40px;\n\theight: 40px;\n\tborder: 4px solid #e1e8ed;\n\tborder-top-color: #5c7080;\n\tborder-radius: 50%;\n\tanimation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/LoadingScreen/index.tsx",
    "content": "import React from 'react';\nimport './LoadingScreen.css';\n\nconst LoadingScreen: React.FC = () => {\n\treturn (\n\t\t<div className=\"loading-screen\">\n\t\t\t<div className=\"css-spinner\" />\n\t\t</div>\n\t);\n};\n\nexport default LoadingScreen;\n"
  },
  {
    "path": "src/client-viewer/src/components/MyDeviceInfoCard/DeviceDetails.d.ts",
    "content": "interface DeviceDetails {\n\tmyIP: string;\n\tmyOS: string;\n\tmyDeviceType: string;\n\tmyBrowser: string;\n\tmyRoomId: string;\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/MyDeviceInfoCard/index.tsx",
    "content": "import { Callout, Card, H3, Text, Tooltip, Position } from '@blueprintjs/core';\nimport { useTranslation } from 'react-i18next';\nimport { LIGHT_UI_BACKGROUND } from '../../constants/styleConstants';\n\ninterface MyDeviceDetailsCardProps {\n\tdeviceDetails: DeviceDetails;\n}\n\nfunction MyDeviceInfoCard(props: MyDeviceDetailsCardProps) {\n\tconst { t } = useTranslation();\n\n\tconst { deviceDetails } = props;\n\tconst { myIP, myOS, myDeviceType, myBrowser, myRoomId } = deviceDetails;\n\n\treturn (\n\t\t<Card\n\t\t\televation={3}\n\t\t\tstyle={{\n\t\t\t\tbackgroundColor: LIGHT_UI_BACKGROUND,\n\t\t\t\tmarginBottom: '30px',\n\t\t\t}}\n\t\t>\n\t\t\t<H3>{`${t('My Device Info')}:`}</H3>\n\t\t\t<Callout>\n\t\t\t\t<Text>{`${t('Device Type')}: ${myDeviceType}`}</Text>\n\t\t\t\t<Tooltip\n\t\t\t\t\tcontent={t(\n\t\t\t\t\t\t'Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen-CE is running',\n\t\t\t\t\t)}\n\t\t\t\t\tposition={Position.TOP}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tfontWeight: 900,\n\t\t\t\t\t\t\tbackgroundColor: '#00f99273',\n\t\t\t\t\t\t\tpaddingLeft: '10px',\n\t\t\t\t\t\t\tpaddingRight: '10px',\n\t\t\t\t\t\t\tborderRadius: '20px',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text>{`${t('Device IP')}: ${myIP}`}</Text>\n\t\t\t\t\t</div>\n\t\t\t\t</Tooltip>\n\t\t\t\t<Text>{`${t('Device Browser')}: ${myBrowser}`}</Text>\n\t\t\t\t<Text>{`${t('Device OS')}: ${myOS}`}</Text>\n\t\t\t\t<Text>{`${t('My Current Connection ID')}: ${myRoomId}`}</Text>\n\t\t\t</Callout>\n\t\t\t<Text className=\"bp3-text-muted\">\n\t\t\t\t{t(\n\t\t\t\t\t'These details should match with the ones that you see in alert popup on computer screen, where Deskreen-CE is running',\n\t\t\t\t)}\n\t\t\t</Text>\n\t\t</Card>\n\t);\n}\n\nexport default MyDeviceInfoCard;\n"
  },
  {
    "path": "src/client-viewer/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts",
    "content": "import { togglePlayerFullscreen } from '../../utils/playerFullscreen';\n\nexport const handlePlayerToggleFullscreen = () => {\n\treturn togglePlayerFullscreen();\n};\n"
  },
  {
    "path": "src/client-viewer/src/components/PlayerControlPanel/index.css",
    "content": ".play-pause-text {\n\twidth: max-content;\n}\n\n.play-pause-button,\n.play-pause-button:focus,\n.play-pause-button:active {\n\tborder: none !important;\n\toutline: none !important;\n\toverflow: hidden !important;\n}\n\n.play-pause-button:hover {\n\tborder: none !important;\n\toutline: none !important;\n\tbackground: rgba(255, 255, 255, 0.1) !important;\n\toverflow: hidden !important;\n}\n\n.play-pause-button::before,\n.play-pause-button::after {\n\tdisplay: none !important;\n}\n\n.play-pause-button {\n\tdisplay: flex !important;\n\tflex-direction: row !important;\n\talign-items: center !important;\n\tjustify-content: center !important;\n\twidth: 120px !important;\n\tmin-width: 120px !important;\n\tmax-width: 120px !important;\n}\n\n.play-pause-button-glow {\n\tbox-shadow: 0 0 20px rgba(19, 124, 189, 0.8), 0 0 40px rgba(19, 124, 189, 0.6) !important;\n}\n\n.play-pause-button .bp3-button-text {\n\tdisplay: flex !important;\n\tflex-direction: row !important;\n\talign-items: center !important;\n\tjustify-content: center !important;\n\tborder: none !important;\n\toutline: none !important;\n\tmargin: 0 !important;\n}\n\n.bp3-button-group {\n\tborder: none !important;\n\toverflow: visible !important;\n}\n\n.bp3-button-group .bp3-button {\n\tposition: relative !important;\n}\n\n.settings-button-separator {\n\tborder: none !important;\n\tposition: relative !important;\n}\n\n.settings-button-separator::before {\n\tcontent: '' !important;\n\tposition: absolute !important;\n\tleft: 0 !important;\n\ttop: 25% !important;\n\tbottom: 25% !important;\n\twidth: 1px !important;\n\tbackground-color: rgba(255, 255, 255, 0.5) !important;\n\tz-index: 10 !important;\n\tpointer-events: none !important;\n}\n\n.settings-button-separator::after {\n\tcontent: '' !important;\n\tposition: absolute !important;\n\tright: 0 !important;\n\ttop: 25% !important;\n\tbottom: 25% !important;\n\twidth: 1px !important;\n\tbackground-color: rgba(255, 255, 255, 0.5) !important;\n\tz-index: 10 !important;\n\tpointer-events: none !important;\n}\n\n.bp3-button-group .bp3-button:not(.play-pause-button) {\n\tbox-shadow: none !important;\n}\n\n.bp3-button-group .bp3-button:not(.play-pause-button):hover {\n\tbox-shadow: none !important;\n}\n\n.bp3-button-group .bp3-button:first-child {\n\tborder-left: none !important;\n\tborder-top-left-radius: 20px !important;\n\tborder-bottom-left-radius: 20px !important;\n\tpadding-left: 20px !important;\n}\n\n.bp3-button-group .bp3-button:last-child {\n\tborder-top-right-radius: 20px !important;\n\tborder-bottom-right-radius: 20px !important;\n\tpadding-right: 20px !important;\n}\n\n.bp3-button-group .bp3-button:hover,\n.bp3-button-group .bp3-button:focus,\n.bp3-button-group .bp3-button:active {\n\toverflow: hidden !important;\n}\n\n.bp3-button-group .bp3-button:first-child:hover,\n.bp3-button-group .bp3-button:first-child:focus,\n.bp3-button-group .bp3-button:first-child:active {\n\tborder-top-left-radius: 20px !important;\n\tborder-bottom-left-radius: 20px !important;\n}\n\n.bp3-button-group .bp3-button:last-child:hover,\n.bp3-button-group .bp3-button:last-child:focus,\n.bp3-button-group .bp3-button:last-child:active {\n\tborder-top-right-radius: 20px !important;\n\tborder-bottom-right-radius: 20px !important;\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/PlayerControlPanel/index.tsx",
    "content": "import React, { useEffect, useState, useCallback } from 'react';\nimport {\n\tAlignment,\n\tButton,\n\tButtonGroup,\n\tCard,\n\tH5,\n\tSwitch,\n\tDivider,\n\tText,\n\tIcon,\n\tTooltip,\n\tPosition,\n\tPopover,\n\tClasses,\n\tH3,\n} from '@blueprintjs/core';\nimport screenfull from 'screenfull';\nimport { useTranslation } from 'react-i18next';\nimport FullScreenEnter from '../../images/fullscreen_24px.svg';\nimport FullScreenExit from '../../images/fullscreen_exit-24px.svg';\nimport { Col, Row } from 'react-flexbox-grid';\nimport {\n\tVideoQuality,\n\ttype VideoQualityType,\n} from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';\nimport { handlePlayerToggleFullscreen } from './handlePlayerToggleFullscreen';\nimport initScreenfullOnChange from './initScreenfullOnChange';\nimport { ScreenSharingSource } from '../../features/PeerConnection/ScreenSharingSourceEnum';\nimport {\n\ttrackAnalyticsEvent,\n\tsetConsentStatus,\n\tupdateAnalyticsConsent,\n} from '../../utils/analytics';\nimport PrivacyControlDialog from '../PrivacyControlDialog';\nimport './index.css';\n\nconst videoQualityButtonStyle: React.CSSProperties = {\n\twidth: '100%',\n\tdisplay: 'flex',\n\tjustifyContent: 'center',\n\talignItems: 'center',\n\ttextAlign: 'center',\n};\n\ninterface PlayerControlPanelProps {\n\tonSwitchChangedCallback: (isEnabled: boolean) => void;\n\tisPlaying: boolean;\n\tisDefaultPlayerTurnedOn: boolean;\n\thandleClickFullscreen: () => 'entered' | 'exited' | 'failed';\n\thandleClickPlayPause: () => void;\n\tsetVideoQuality: (q: VideoQualityType) => void;\n\tselectedVideoQuality: VideoQualityType;\n\tscreenSharingSourceType: ScreenSharingSourceType;\n\t// toaster: undefined | HTMLDivElement;\n}\n\nfunction PlayerControlPanel(props: PlayerControlPanelProps) {\n\tconst { t } = useTranslation();\n\tconst {\n\t\tonSwitchChangedCallback,\n\t\tisPlaying,\n\t\tisDefaultPlayerTurnedOn,\n\t\thandleClickPlayPause,\n\t\thandleClickFullscreen,\n\t\tselectedVideoQuality,\n\t\tsetVideoQuality,\n\t\tscreenSharingSourceType,\n\t} = props;\n\n\tconst isFullScreenAPIAvailable = screenfull.isEnabled;\n\n\tconst [isFullScreenOn, setIsFullScreenOn] = useState(false);\n\tconst [isPrivacyDialogOpen, setIsPrivacyDialogOpen] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst cleanup = initScreenfullOnChange(setIsFullScreenOn);\n\t\treturn cleanup;\n\t}, []);\n\n\tconst handleClickFullscreenWhenDefaultPlayerIsOn = useCallback(() => {\n\t\tconst result = handlePlayerToggleFullscreen();\n\t\tif (result === 'failed') {\n\t\t\tconsole.warn('Unable to toggle fullscreen');\n\t\t\treturn result;\n\t\t}\n\t\tsetIsFullScreenOn(result === 'entered');\n\t\treturn result;\n\t}, [setIsFullScreenOn]);\n\n\tconst handleLogoClick = useCallback(() => {\n\t\ttrackAnalyticsEvent('logo_clicked', {\n\t\t\tdestination: 'https://deskreen.com',\n\t\t});\n\t\twindow.open('https://deskreen.com', '_blank');\n\t}, []);\n\n\tconst handleContributeClick = useCallback(() => {\n\t\ttrackAnalyticsEvent('contribute_clicked', {\n\t\t\tdestination: 'https://deskreen.com/download',\n\t\t});\n\t\twindow.open('https://deskreen.com/download', '_blank');\n\t}, []);\n\n\tconst handlePlayPauseClick = useCallback(() => {\n\t\tconst nextAction = isPlaying ? 'pause' : 'play';\n\t\ttrackAnalyticsEvent(\n\t\t\tnextAction === 'play' ? 'play_button_clicked' : 'pause_button_clicked',\n\t\t\t{\n\t\t\t\ttarget_state: nextAction === 'play' ? 'playing' : 'paused',\n\t\t\t},\n\t\t);\n\t\thandleClickPlayPause();\n\t}, [handleClickPlayPause, isPlaying]);\n\n\tconst handleVideoQualitySelect = useCallback(\n\t\t(quality: VideoQualityType) => {\n\t\t\tif (selectedVideoQuality !== quality) {\n\t\t\t\ttrackAnalyticsEvent('video_quality_selected', {\n\t\t\t\t\tquality,\n\t\t\t\t});\n\t\t\t}\n\t\t\tsetVideoQuality(quality);\n\t\t},\n\t\t[selectedVideoQuality, setVideoQuality],\n\t);\n\n\tconst handleDefaultPlayerToggle = useCallback(() => {\n\t\tconst nextState = !isDefaultPlayerTurnedOn;\n\t\ttrackAnalyticsEvent('default_player_toggled', {\n\t\t\tstate: nextState ? 'on' : 'off',\n\t\t});\n\t\tonSwitchChangedCallback(nextState);\n\t}, [isDefaultPlayerTurnedOn, onSwitchChangedCallback]);\n\n\tconst handleFullscreenClick = useCallback(() => {\n\t\tconst result = isDefaultPlayerTurnedOn\n\t\t\t? handleClickFullscreenWhenDefaultPlayerIsOn()\n\t\t\t: handleClickFullscreen();\n\t\tif (result === 'failed') {\n\t\t\ttrackAnalyticsEvent('fullscreen_toggle_failed', {\n\t\t\t\tplayer_mode: isDefaultPlayerTurnedOn ? 'default' : 'custom',\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\ttrackAnalyticsEvent('fullscreen_toggled', {\n\t\t\tstate: result === 'entered' ? 'on' : 'off',\n\t\t\tplayer_mode: isDefaultPlayerTurnedOn ? 'default' : 'custom',\n\t\t});\n\t}, [\n\t\thandleClickFullscreen,\n\t\thandleClickFullscreenWhenDefaultPlayerIsOn,\n\t\tisDefaultPlayerTurnedOn,\n\t]);\n\n\tconst handlePrivacyControlClick = useCallback(() => {\n\t\tsetIsPrivacyDialogOpen(true);\n\t}, []);\n\n\tconst handlePrivacyDialogClose = useCallback(() => {\n\t\tsetIsPrivacyDialogOpen(false);\n\t}, []);\n\n\tconst handlePrivacyAccept = useCallback(() => {\n\t\tsetConsentStatus('accepted');\n\t\tupdateAnalyticsConsent('accepted');\n\t\tsetIsPrivacyDialogOpen(false);\n\t}, []);\n\n\tconst handlePrivacyOptOut = useCallback(() => {\n\t\tsetConsentStatus('opted-out');\n\t\tupdateAnalyticsConsent('opted-out');\n\t\tsetIsPrivacyDialogOpen(false);\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<PrivacyControlDialog\n\t\t\t\tisOpen={isPrivacyDialogOpen}\n\t\t\t\tonClose={handlePrivacyDialogClose}\n\t\t\t\tonAccept={handlePrivacyAccept}\n\t\t\t\tonOptOut={handlePrivacyOptOut}\n\t\t\t/>\n\t\t\t<Card elevation={4}>\n\t\t\t\t<Row between=\"xs\" middle=\"xs\">\n\t\t\t\t\t<Col xs={12} md={3}>\n\t\t\t\t\t\t<Row middle=\"xs\" start=\"xs\">\n\t\t\t\t\t\t\t<Col xs>\n\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\tcontent={t('Click to visit our website')}\n\t\t\t\t\t\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Button minimal onClick={handleLogoClick}>\n\t\t\t\t\t\t\t\t\t\t<Row middle=\"xs\">\n\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\tsrc=\"/img/logo512.png\"\n\t\t\t\t\t\t\t\t\t\t\t\talt=\"logo\"\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ height: '72px', marginRight: '12px' }}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<H3 style={{ margin: 0 }}>Deskreen CE Viewer</H3>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t<Col xs>\n\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\tcontent={t('get-deskreen-pro-tooltip')}\n\t\t\t\t\t\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\t\t\t\tmarginLeft: '8px',\n\t\t\t\t\t\t\t\t\t\t\tpadding: '8px 18px',\n\t\t\t\t\t\t\t\t\t\t\tminHeight: '36px',\n\t\t\t\t\t\t\t\t\t\t\tbackground:\n\t\t\t\t\t\t\t\t\t\t\t\t'linear-gradient(135deg, hsl(258, 90%, 66%) 0%, hsl(210, 96%, 62%) 30%, hsl(192, 94%, 44%) 70%, hsl(28, 96%, 58%) 100%)',\n\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\tboxShadow:\n\t\t\t\t\t\t\t\t\t\t\t\t'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)',\n\t\t\t\t\t\t\t\t\t\t\ttransition: 'all 0.2s ease',\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tonClick={handleContributeClick}\n\t\t\t\t\t\t\t\t\t\tonMouseEnter={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.currentTarget.style.transform = 'translateY(-1px)';\n\t\t\t\t\t\t\t\t\t\t\te.currentTarget.style.boxShadow =\n\t\t\t\t\t\t\t\t\t\t\t\t'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)';\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tonMouseLeave={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.currentTarget.style.transform = 'translateY(0)';\n\t\t\t\t\t\t\t\t\t\t\te.currentTarget.style.boxShadow =\n\t\t\t\t\t\t\t\t\t\t\t\t'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)';\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\t\t\t\t\t\tgap: '8px',\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Icon\n\t\t\t\t\t\t\t\t\t\t\t\ticon=\"clean\"\n\t\t\t\t\t\t\t\t\t\t\t\tsize={20}\n\t\t\t\t\t\t\t\t\t\t\t\tcolor=\"#D4AF37\"\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tflexShrink: 0,\n\t\t\t\t\t\t\t\t\t\t\t\t\tfilter:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'brightness(1.1) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))',\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<Text\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tlineHeight: '1',\n\t\t\t\t\t\t\t\t\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\t\t\t\t\t\t\t\t\tfontSize: '14px',\n\t\t\t\t\t\t\t\t\t\t\t\t\tfontWeight: '600',\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#ffffff',\n\t\t\t\t\t\t\t\t\t\t\t\t\ttextShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{t('get-deskreen-pro')}\n\t\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col xs={12} md={5}>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ height: '42px' }}>\n\t\t\t\t\t\t\t<ButtonGroup\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tborderRadius: '20px',\n\t\t\t\t\t\t\t\t\tbackgroundColor: '#137CBD',\n\t\t\t\t\t\t\t\t\theight: '42px',\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Tooltip content={isPlaying ? t('Click to Pause Video') : t('Click to Play Video')} position={Position.BOTTOM}>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\t\tonClick={handlePlayPauseClick}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tcolor: 'white',\n\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: !isPlaying ? 'rgba(255, 255, 255, 0.2)' : 'transparent',\n\t\t\t\t\t\t\t\t\t\t\tboxShadow: !isPlaying ? '0 0 20px rgba(19, 124, 189, 0.8), 0 0 40px rgba(19, 124, 189, 0.6)' : 'none',\n\t\t\t\t\t\t\t\t\t\t\ttransition: 'all 0.3s ease-in-out',\n\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\toutline: 'none',\n\t\t\t\t\t\t\t\t\t\t\tboxSizing: 'border-box',\n\t\t\t\t\t\t\t\t\t\t\tpadding: '0 20px',\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: '20px 0 0 20px',\n\t\t\t\t\t\t\t\t\t\t\twidth: '120px',\n\t\t\t\t\t\t\t\t\t\t\tminWidth: '120px',\n\t\t\t\t\t\t\t\t\t\t\tmaxWidth: '120px',\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tclassName={!isPlaying ? 'play-pause-button play-pause-button-glow' : 'play-pause-button'}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>\n\t\t\t\t\t\t\t\t\t\t\t<Icon icon={isPlaying ? 'pause' : 'play'} color=\"white\" />\n\t\t\t\t\t\t\t\t\t\t\t<Text className=\"bp3-text-large play-pause-text\" style={{ color: 'white' }}>\n\t\t\t\t\t\t\t\t\t\t\t\t{isPlaying ? t('Pause') : t('Play')}\n\t\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t<Popover\n\t\t\t\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t<H5>{`${t('Video Settings')}:`}</H5>\n\t\t\t\t\t\t\t\t\t\t\t<Divider />\n\t\t\t\t\t\t\t\t\t\t\t<Row\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontent={t('flip-the-screen-is-pro-version-only')}\n\t\t\t\t\t\t\t\t\t\t\t\t\tposition={Position.TOP}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: 'block',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ticon=\"key-tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={videoQualityButtonStyle}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={true}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{t('Flip')}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<Divider />\n\t\t\t\t\t\t\t\t\t\t\t{Object.values(VideoQuality).map(\n\t\t\t\t\t\t\t\t\t\t\t\t(q: VideoQualityType) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row key={q}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactive={selectedVideoQuality === q}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={videoQualityButtonStyle}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tscreenSharingSourceType ===\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tScreenSharingSource.WINDOW\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thandleVideoQualitySelect(q);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// toaster?.show({\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//   icon: 'clean',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//   intent: Intent.PRIMARY,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//   message: `${t(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//     'Video quality has been changed to'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//   )} ${q}`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{q}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\t\t\t\t\t\tpopoverClassName={Classes.POPOVER_CONTENT_SIZING}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\t\tcontent={t('Click to Open Video Settings')}\n\t\t\t\t\t\t\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white',\n\t\t\t\t\t\t\t\t\t\t\t\toutline: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tboxShadow: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: '0',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '0 20px',\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"settings-button-separator\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Icon icon=\"cog\" color=\"white\" />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t</Popover>\n\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\tcontent={t('Click to Enter Full Screen Mode')}\n\t\t\t\t\t\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\t\tonClick={handleFullscreenClick}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tcolor: 'white',\n\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\toutline: 'none',\n\t\t\t\t\t\t\t\t\t\t\tboxShadow: 'none',\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: '0 20px 20px 0',\n\t\t\t\t\t\t\t\t\t\t\tpadding: '0 20px',\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\tsrc={isFullScreenOn ? FullScreenExit : FullScreenEnter}\n\t\t\t\t\t\t\t\t\t\t\twidth={16}\n\t\t\t\t\t\t\t\t\t\t\theight={16}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\ttransform: 'scale(1.5) translateY(1px)',\n\t\t\t\t\t\t\t\t\t\t\t\tfilter:\n\t\t\t\t\t\t\t\t\t\t\t\t\t'invert(100%) sepia(100%) saturate(0%) hue-rotate(127deg) brightness(107%) contrast(102%)',\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\talt=\"fullscreen-toggle\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</ButtonGroup>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col xs={12} md={3}>\n\t\t\t\t\t\t<Row end=\"xs\">\n\t\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\tonChange={handleDefaultPlayerToggle}\n\t\t\t\t\t\t\t\t\tinnerLabel={isDefaultPlayerTurnedOn ? t('ON') : t('OFF')}\n\t\t\t\t\t\t\t\t\tinline\n\t\t\t\t\t\t\t\t\tlabel={t('Default Video Player')}\n\t\t\t\t\t\t\t\t\talignIndicator={Alignment.RIGHT}\n\t\t\t\t\t\t\t\t\tchecked={isDefaultPlayerTurnedOn}\n\t\t\t\t\t\t\t\t\tdisabled={!isFullScreenAPIAvailable}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tmarginBottom: '12px',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\t\ticon=\"shield\"\n\t\t\t\t\t\t\t\t\tonClick={handlePrivacyControlClick}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\twidth: 'fit-content',\n\t\t\t\t\t\t\t\t\t\tmarginLeft: 'auto',\n\t\t\t\t\t\t\t\t\t\tcolor: '#5C7080',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{t('Privacy Settings')}\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Card>\n\t\t</>\n\t);\n}\n\nexport default PlayerControlPanel;\n"
  },
  {
    "path": "src/client-viewer/src/components/PlayerControlPanel/initScreenfullOnChange.ts",
    "content": "import { subscribeToPlayerFullscreenChange } from '../../utils/playerFullscreen';\n\nexport default (setIsFullScreenOn: (_: boolean) => void) => {\n\tconst unsubscribe = subscribeToPlayerFullscreenChange(setIsFullScreenOn);\n\treturn () => {\n\t\tunsubscribe();\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/components/PrivacyConsentDialog/index.css",
    "content": ".privacy-consent-dialog-backdrop {\n\tbackdrop-filter: blur(2px);\n}\n\n.privacy-consent-dialog {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.privacy-consent-buttons-container {\n\tflex-shrink: 0;\n\tbackground: white;\n\tborder-top: 1px solid rgba(16, 22, 26, 0.15);\n}\n\n.privacy-consent-buttons-container .row {\n\tdisplay: flex;\n}\n\n.bp4-dark .privacy-consent-buttons-container {\n\tbackground: #30404d;\n\tborder-top-color: rgba(255, 255, 255, 0.2);\n}\n\n.privacy-consent-dialog a {\n\tcolor: #137cbd;\n\ttext-decoration: none;\n}\n\n.privacy-consent-dialog a:hover {\n\ttext-decoration: underline;\n\tcolor: #106ba3;\n}\n\n.allow-analytics-button {\n\tbox-shadow:\n\t\t0 4px 16px rgba(15, 153, 96, 0.5),\n\t\t0 0 20px rgba(15, 153, 96, 0.3) !important;\n\ttransition: all 0.2s ease-in-out !important;\n\tbackground: linear-gradient(135deg, #0f9960 0%, #0a7a4a 100%) !important;\n\tborder: none !important;\n\tcolor: white !important;\n}\n\n.allow-analytics-button:hover {\n\tbox-shadow:\n\t\t0 6px 24px rgba(15, 153, 96, 0.6),\n\t\t0 0 30px rgba(15, 153, 96, 0.4) !important;\n\ttransform: translateY(-2px);\n\tbackground: linear-gradient(135deg, #15b371 0%, #0f9960 100%) !important;\n}\n\n.allow-analytics-button:active {\n\ttransform: translateY(0);\n\tbox-shadow: 0 2px 12px rgba(15, 153, 96, 0.5) !important;\n}\n\n.disagree-analytics-button {\n\ttransition: all 0.2s ease-in-out;\n\tcolor: #5c7080 !important;\n\tbackground: none !important;\n\tborder: none !important;\n\tpadding: 8px 12px !important;\n\tmin-height: auto !important;\n\ttext-decoration: none !important;\n}\n\n.disagree-analytics-button:hover {\n\tcolor: #394b59 !important;\n\tbackground: none !important;\n\ttext-decoration: underline !important;\n}\n\n.bp4-dark .disagree-analytics-button {\n\tcolor: #8a9ba8 !important;\n}\n\n.bp4-dark .disagree-analytics-button:hover {\n\tcolor: #bfccd6 !important;\n\tbackground: none !important;\n}\n\n/* reorder buttons on mobile: disagree first, allow second */\n@media (max-width: 575px) {\n\t.disagree-button-col {\n\t\torder: 1;\n\t}\n\n\t.allow-button-col {\n\t\torder: 2;\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/PrivacyConsentDialog/index.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport {\n\tButton,\n\tClasses,\n\tDialog,\n\tDivider,\n\tH2,\n\tH3,\n\tHTMLSelect,\n\tIcon,\n} from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport i18nInstance from '../../config/i18n';\nimport './index.css';\n\ninterface PrivacyConsentDialogProps {\n\tisOpen: boolean;\n\tonAccept: () => void;\n\tonOptOut: () => void;\n}\n\nconst AVAILABLE_LANGUAGES = [\n\t{ code: 'en', name: 'English' },\n\t{ code: 'es', name: 'Español' },\n\t{ code: 'de', name: 'Deutsch' },\n\t{ code: 'fr', name: 'Français' },\n\t{ code: 'ko', name: '한국어' },\n\t{ code: 'fi', name: 'Suomi' },\n\t{ code: 'it', name: 'Italiano' },\n\t{ code: 'da', name: 'Dansk' },\n\t{ code: 'sv', name: 'Svenska' },\n\t{ code: 'nl', name: 'Nederlands' },\n\t{ code: 'ua', name: 'Українська' },\n\t{ code: 'ru', name: 'Русский' },\n\t{ code: 'zh_CN', name: '简体中文' },\n\t{ code: 'zh_TW', name: '繁體中文' },\n\t{ code: 'ja', name: '日本語' },\n];\n\nfunction TranslatedContent() {\n\tconst { t } = useTranslation();\n\n\treturn (\n\t\t<>\n\t\t\t<H2 style={{ marginBottom: '16px' }}>{t('Analytics Reference')}</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t{t(\n\t\t\t\t\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.',\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('What we collect:')}\n\t\t\t</H2>\n\t\t\t<ul style={{ marginBottom: '16px', paddingLeft: '20px' }}>\n\t\t\t\t<li>{t('Page views (which screens you visit)')}</li>\n\t\t\t\t<li>{t('Time spent on pages')}</li>\n\t\t\t\t<li>{t('Basic device info (browser type, screen size)')}</li>\n\t\t\t\t<li>\n\t\t\t\t\t{t('Your IP address (anonymized — last part removed for privacy)')}\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t(\"What we DON'T collect:\")}\n\t\t\t</H2>\n\t\t\t<ul style={{ marginBottom: '16px', paddingLeft: '20px' }}>\n\t\t\t\t<li>{t('Personal info (names, emails, passwords)')}</li>\n\t\t\t\t<li>{t('Exact location')}</li>\n\t\t\t\t<li>{t('Any files or content you interact with')}</li>\n\t\t\t</ul>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('Why anonymous?')}\n\t\t\t</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t{t(\n\t\t\t\t\t'Your IP is automatically shortened, and no one can identify you personally from this data.',\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('Your options:')}\n\t\t\t</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t<strong>{t('Continue:')}</strong>{' '}\n\t\t\t\t{t(\"We'll track anonymized usage to help improve the app.\")}\n\t\t\t</p>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t<strong>{t('Opt out:')}</strong>{' '}\n\t\t\t\t{t(\n\t\t\t\t\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.)\",\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<p style={{ marginBottom: '24px', fontSize: '14px', color: '#5C7080' }}>\n\t\t\t\t{t('Data goes to: Google Analytics. See')}{' '}\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://policies.google.com/privacy\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t>\n\t\t\t\t\t{t('their privacy policy')}\n\t\t\t\t</a>\n\t\t\t\t.\n\t\t\t</p>\n\t\t</>\n\t);\n}\n\nfunction TranslatedButtons({\n\tonAccept,\n\tonOptOut,\n}: {\n\tonAccept: () => void;\n\tonOptOut: () => void;\n}) {\n\tconst { t } = useTranslation();\n\n\treturn (\n\t\t<Row center=\"xs\" style={{ padding: '20px', gap: '16px' }}>\n\t\t\t<Col xs={12} sm={7} className=\"allow-button-col\">\n\t\t\t\t<Button\n\t\t\t\t\tintent=\"success\"\n\t\t\t\t\tlarge\n\t\t\t\t\tfill\n\t\t\t\t\tclassName=\"allow-analytics-button\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: '60px',\n\t\t\t\t\t\tfontSize: '18px',\n\t\t\t\t\t\tfontWeight: '600',\n\t\t\t\t\t}}\n\t\t\t\t\tonClick={onAccept}\n\t\t\t\t>\n\t\t\t\t\t{t('Allow')}\n\t\t\t\t</Button>\n\t\t\t</Col>\n\t\t\t<Col xs={12} sm={5} className=\"disagree-button-col\">\n\t\t\t\t<Button\n\t\t\t\t\tminimal\n\t\t\t\t\tclassName=\"disagree-analytics-button\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: '12px',\n\t\t\t\t\t}}\n\t\t\t\t\tonClick={onOptOut}\n\t\t\t\t>\n\t\t\t\t\t{t('Deny')}\n\t\t\t\t</Button>\n\t\t\t</Col>\n\t\t</Row>\n\t);\n}\n\nfunction PrivacyConsentDialog(props: PrivacyConsentDialogProps) {\n\tconst { t, i18n } = useTranslation();\n\tconst { isOpen, onAccept, onOptOut } = props;\n\tconst [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');\n\tconst [forceUpdate, setForceUpdate] = useState(0);\n\n\tuseEffect(() => {\n\t\tconst updateLanguage = () => {\n\t\t\tconst newLang = i18nInstance.language || 'en';\n\t\t\tsetCurrentLanguage(newLang);\n\t\t\tsetForceUpdate((prev) => prev + 1);\n\t\t};\n\n\t\tconst handleLanguageChanged = () => {\n\t\t\tupdateLanguage();\n\t\t};\n\n\t\ti18nInstance.on('languageChanged', handleLanguageChanged);\n\t\ti18nInstance.on('loaded', handleLanguageChanged);\n\n\t\treturn () => {\n\t\t\ti18nInstance.off('languageChanged', handleLanguageChanged);\n\t\t\ti18nInstance.off('loaded', handleLanguageChanged);\n\t\t};\n\t}, []);\n\n\tconst handleLanguageChange = (\n\t\tevent: React.ChangeEvent<HTMLSelectElement>,\n\t) => {\n\t\tconst newLang = event.target.value;\n\t\ti18nInstance\n\t\t\t.changeLanguage(newLang)\n\t\t\t.then(() => {\n\t\t\t\tsetCurrentLanguage(newLang);\n\t\t\t\tsetForceUpdate((prev) => prev + 1);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconsole.error('Error changing language:', err);\n\t\t\t});\n\t};\n\n\tconst langKey = `${currentLanguage}-${forceUpdate}`;\n\n\treturn (\n\t\t<Dialog\n\t\t\tclassName=\"privacy-consent-dialog\"\n\t\t\tautoFocus\n\t\t\tcanEscapeKeyClose={false}\n\t\t\tcanOutsideClickClose={false}\n\t\t\tenforceFocus\n\t\t\tisOpen={isOpen}\n\t\t\tstyle={{\n\t\t\t\twidth: '90%',\n\t\t\t\tmaxWidth: '800px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\tmaxHeight: '90vh',\n\t\t\t}}\n\t\t\tbackdropClassName=\"privacy-consent-dialog-backdrop\"\n\t\t>\n\t\t\t<Row\n\t\t\t\tstyle={{\n\t\t\t\t\tpadding: '20px 20px 16px 20px',\n\t\t\t\t\tflexShrink: 0,\n\t\t\t\t\talignItems: 'center',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Col xs={6}>\n\t\t\t\t\t<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>\n\t\t\t\t\t\t<Icon icon=\"translate\" style={{ color: '#5C7080' }} />\n\t\t\t\t\t\t<HTMLSelect\n\t\t\t\t\t\t\tvalue={currentLanguage}\n\t\t\t\t\t\t\tonChange={handleLanguageChange}\n\t\t\t\t\t\t\toptions={AVAILABLE_LANGUAGES.map((lang) => ({\n\t\t\t\t\t\t\t\tvalue: lang.code,\n\t\t\t\t\t\t\t\tlabel: lang.name,\n\t\t\t\t\t\t\t}))}\n\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\tstyle={{ minWidth: '120px' }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</Col>\n\t\t\t\t<Col xs={6}>\n\t\t\t\t\t<H3\n\t\t\t\t\t\tkey={langKey}\n\t\t\t\t\t\tclassName={Classes.TEXT_MUTED}\n\t\t\t\t\t\tstyle={{ textAlign: 'right', margin: 0 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{t('Privacy Notice: Analytics in Deskreen CE Viewer')}\n\t\t\t\t\t</H3>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Divider style={{ flexShrink: 0 }} />\n\t\t\t<div\n\t\t\t\tclassName={Classes.DIALOG_BODY}\n\t\t\t\tstyle={{ padding: '20px', overflowY: 'auto', flex: '1 1 auto' }}\n\t\t\t>\n\t\t\t\t<TranslatedContent key={langKey} />\n\t\t\t</div>\n\t\t\t<Divider style={{ flexShrink: 0 }} />\n\t\t\t<div className=\"privacy-consent-buttons-container\">\n\t\t\t\t<TranslatedButtons\n\t\t\t\t\tkey={langKey}\n\t\t\t\t\tonAccept={onAccept}\n\t\t\t\t\tonOptOut={onOptOut}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</Dialog>\n\t);\n}\n\nexport default PrivacyConsentDialog;\n"
  },
  {
    "path": "src/client-viewer/src/components/PrivacyControlDialog/index.css",
    "content": ".privacy-control-dialog-backdrop {\n\tbackdrop-filter: blur(2px);\n}\n\n.privacy-control-dialog {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.privacy-control-buttons-container {\n\tflex-shrink: 0;\n\tbackground: white;\n\tborder-top: 1px solid rgba(16, 22, 26, 0.15);\n}\n\n.bp4-dark .privacy-control-buttons-container {\n\tbackground: #30404d;\n\tborder-top-color: rgba(255, 255, 255, 0.2);\n}\n\n.privacy-control-dialog a {\n\tcolor: #137cbd;\n\ttext-decoration: none;\n}\n\n.privacy-control-dialog a:hover {\n\ttext-decoration: underline;\n\tcolor: #106ba3;\n}\n\n.allow-analytics-button {\n\tbox-shadow:\n\t\t0 4px 16px rgba(15, 153, 96, 0.5),\n\t\t0 0 20px rgba(15, 153, 96, 0.3) !important;\n\ttransition: all 0.2s ease-in-out !important;\n\tbackground: linear-gradient(135deg, #0f9960 0%, #0a7a4a 100%) !important;\n\tborder: none !important;\n\tcolor: white !important;\n}\n\n.allow-analytics-button:hover {\n\tbox-shadow:\n\t\t0 6px 24px rgba(15, 153, 96, 0.6),\n\t\t0 0 30px rgba(15, 153, 96, 0.4) !important;\n\ttransform: translateY(-2px);\n\tbackground: linear-gradient(135deg, #15b371 0%, #0f9960 100%) !important;\n}\n\n.allow-analytics-button:active {\n\ttransform: translateY(0);\n\tbox-shadow: 0 2px 12px rgba(15, 153, 96, 0.5) !important;\n}\n\n.disagree-analytics-button {\n\topacity: 0.7;\n\ttransition: opacity 0.2s ease-in-out;\n\tcolor: #5c7080 !important;\n\tborder-color: #ced9e0 !important;\n}\n\n.disagree-analytics-button:hover {\n\topacity: 0.9;\n\tbackground-color: #f5f8fa !important;\n}\n\n.bp4-dark .disagree-analytics-button {\n\tcolor: #8a9ba8 !important;\n\tborder-color: #5c7080 !important;\n}\n\n.bp4-dark .disagree-analytics-button:hover {\n\tbackground-color: #394b59 !important;\n}\n"
  },
  {
    "path": "src/client-viewer/src/components/PrivacyControlDialog/index.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport {\n\tButton,\n\tClasses,\n\tDialog,\n\tDivider,\n\tH2,\n\tH3,\n\tHTMLSelect,\n\tIcon,\n} from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport i18nInstance from '../../config/i18n';\nimport { getConsentStatus, type ConsentStatus } from '../../utils/analytics';\nimport './index.css';\n\ninterface PrivacyControlDialogProps {\n\tisOpen: boolean;\n\tonClose: () => void;\n\tonAccept: () => void;\n\tonOptOut: () => void;\n}\n\nconst AVAILABLE_LANGUAGES = [\n\t{ code: 'en', name: 'English' },\n\t{ code: 'es', name: 'Español' },\n\t{ code: 'de', name: 'Deutsch' },\n\t{ code: 'fr', name: 'Français' },\n\t{ code: 'ko', name: '한국어' },\n\t{ code: 'fi', name: 'Suomi' },\n\t{ code: 'it', name: 'Italiano' },\n\t{ code: 'da', name: 'Dansk' },\n\t{ code: 'sv', name: 'Svenska' },\n\t{ code: 'nl', name: 'Nederlands' },\n\t{ code: 'ua', name: 'Українська' },\n\t{ code: 'ru', name: 'Русский' },\n\t{ code: 'zh_CN', name: '简体中文' },\n\t{ code: 'zh_TW', name: '繁體中文' },\n\t{ code: 'ja', name: '日本語' },\n];\n\nfunction TranslatedContent() {\n\tconst { t } = useTranslation();\n\n\treturn (\n\t\t<>\n\t\t\t<H2 style={{ marginBottom: '16px' }}>{t('Analytics Reference')}</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t{t(\n\t\t\t\t\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.',\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('What we collect:')}\n\t\t\t</H2>\n\t\t\t<ul style={{ marginBottom: '16px', paddingLeft: '20px' }}>\n\t\t\t\t<li>{t('Page views (which screens you visit)')}</li>\n\t\t\t\t<li>{t('Time spent on pages')}</li>\n\t\t\t\t<li>{t('Basic device info (browser type, screen size)')}</li>\n\t\t\t\t<li>\n\t\t\t\t\t{t('Your IP address (anonymized — last part removed for privacy)')}\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t(\"What we DON'T collect:\")}\n\t\t\t</H2>\n\t\t\t<ul style={{ marginBottom: '16px', paddingLeft: '20px' }}>\n\t\t\t\t<li>{t('Personal info (names, emails, passwords)')}</li>\n\t\t\t\t<li>{t('Exact location')}</li>\n\t\t\t\t<li>{t('Any files or content you interact with')}</li>\n\t\t\t</ul>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('Why anonymous?')}\n\t\t\t</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t{t(\n\t\t\t\t\t'Your IP is automatically shortened, and no one can identify you personally from this data.',\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<H2 style={{ marginBottom: '16px', marginTop: '24px' }}>\n\t\t\t\t{t('Change your preference:')}\n\t\t\t</H2>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t<strong>{t('Enable analytics:')}</strong>{' '}\n\t\t\t\t{t(\"We'll track anonymized usage to help improve the app.\")}\n\t\t\t</p>\n\t\t\t<p style={{ marginBottom: '16px' }}>\n\t\t\t\t<strong>{t('Disable analytics:')}</strong>{' '}\n\t\t\t\t{t(\n\t\t\t\t\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.)\",\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<p style={{ marginBottom: '24px', fontSize: '14px', color: '#5C7080' }}>\n\t\t\t\t{t('Data goes to: Google Analytics. See')}{' '}\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://policies.google.com/privacy\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t>\n\t\t\t\t\t{t('their privacy policy')}\n\t\t\t\t</a>\n\t\t\t\t.\n\t\t\t</p>\n\t\t</>\n\t);\n}\n\nfunction TranslatedButtons({\n\tonAccept,\n\tonOptOut,\n\tcurrentStatus,\n}: {\n\tonAccept: () => void;\n\tonOptOut: () => void;\n\tcurrentStatus: ConsentStatus;\n}) {\n\tconst { t } = useTranslation();\n\n\treturn (\n\t\t<Row center=\"xs\" style={{ padding: '20px', gap: '16px' }}>\n\t\t\t<Col xs={12} sm={currentStatus === 'opted-out' ? 12 : 7}>\n\t\t\t\t<Button\n\t\t\t\t\tintent={currentStatus === 'opted-out' ? 'success' : 'none'}\n\t\t\t\t\tlarge={currentStatus === 'opted-out'}\n\t\t\t\t\tfill\n\t\t\t\t\tclassName={\n\t\t\t\t\t\tcurrentStatus === 'opted-out'\n\t\t\t\t\t\t\t? 'allow-analytics-button'\n\t\t\t\t\t\t\t: 'disagree-analytics-button'\n\t\t\t\t\t}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: currentStatus === 'opted-out' ? '60px' : '45px',\n\t\t\t\t\t\tfontSize: currentStatus === 'opted-out' ? '18px' : '14px',\n\t\t\t\t\t\tfontWeight: currentStatus === 'opted-out' ? '600' : 'normal',\n\t\t\t\t\t}}\n\t\t\t\t\tonClick={onAccept}\n\t\t\t\t>\n\t\t\t\t\t{t('Enable Analytics')}\n\t\t\t\t</Button>\n\t\t\t</Col>\n\t\t\t{currentStatus === 'accepted' && (\n\t\t\t\t<Col xs={12} sm={5}>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tintent=\"none\"\n\t\t\t\t\t\tfill\n\t\t\t\t\t\tclassName=\"disagree-analytics-button\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\theight: '45px',\n\t\t\t\t\t\t\tfontSize: '14px',\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonClick={onOptOut}\n\t\t\t\t\t>\n\t\t\t\t\t\t{t('Disable Analytics')}\n\t\t\t\t\t</Button>\n\t\t\t\t</Col>\n\t\t\t)}\n\t\t</Row>\n\t);\n}\n\nfunction PrivacyControlDialog(props: PrivacyControlDialogProps) {\n\tconst { t, i18n } = useTranslation();\n\tconst { isOpen, onClose, onAccept, onOptOut } = props;\n\tconst [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');\n\tconst [forceUpdate, setForceUpdate] = useState(0);\n\tconst [currentStatus, setCurrentStatus] = useState<ConsentStatus>(null);\n\n\tuseEffect(() => {\n\t\tif (isOpen) {\n\t\t\tsetCurrentStatus(getConsentStatus());\n\t\t}\n\t}, [isOpen]);\n\n\tuseEffect(() => {\n\t\tconst updateLanguage = () => {\n\t\t\tconst newLang = i18nInstance.language || 'en';\n\t\t\tsetCurrentLanguage(newLang);\n\t\t\tsetForceUpdate((prev) => prev + 1);\n\t\t};\n\n\t\tconst handleLanguageChanged = () => {\n\t\t\tupdateLanguage();\n\t\t};\n\n\t\ti18nInstance.on('languageChanged', handleLanguageChanged);\n\t\ti18nInstance.on('loaded', handleLanguageChanged);\n\n\t\treturn () => {\n\t\t\ti18nInstance.off('languageChanged', handleLanguageChanged);\n\t\t\ti18nInstance.off('loaded', handleLanguageChanged);\n\t\t};\n\t}, []);\n\n\tconst handleLanguageChange = (\n\t\tevent: React.ChangeEvent<HTMLSelectElement>,\n\t) => {\n\t\tconst newLang = event.target.value;\n\t\ti18nInstance\n\t\t\t.changeLanguage(newLang)\n\t\t\t.then(() => {\n\t\t\t\tsetCurrentLanguage(newLang);\n\t\t\t\tsetForceUpdate((prev) => prev + 1);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconsole.error('Error changing language:', err);\n\t\t\t});\n\t};\n\n\tconst handleAcceptClick = () => {\n\t\tonAccept();\n\t\tsetCurrentStatus('accepted');\n\t};\n\n\tconst handleOptOutClick = () => {\n\t\tonOptOut();\n\t\tsetCurrentStatus('opted-out');\n\t};\n\n\tconst langKey = `${currentLanguage}-${forceUpdate}`;\n\n\treturn (\n\t\t<Dialog\n\t\t\tclassName=\"privacy-control-dialog\"\n\t\t\tautoFocus\n\t\t\tcanEscapeKeyClose={true}\n\t\t\tcanOutsideClickClose={true}\n\t\t\tenforceFocus\n\t\t\tisOpen={isOpen}\n\t\t\tonClose={onClose}\n\t\t\tstyle={{\n\t\t\t\twidth: '90%',\n\t\t\t\tmaxWidth: '800px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\tmaxHeight: '90vh',\n\t\t\t}}\n\t\t\tbackdropClassName=\"privacy-control-dialog-backdrop\"\n\t\t>\n\t\t\t<Row\n\t\t\t\tstyle={{\n\t\t\t\t\tpadding: '20px 20px 16px 20px',\n\t\t\t\t\tflexShrink: 0,\n\t\t\t\t\talignItems: 'center',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Col xs={6}>\n\t\t\t\t\t<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>\n\t\t\t\t\t\t<Icon icon=\"translate\" style={{ color: '#5C7080' }} />\n\t\t\t\t\t\t<HTMLSelect\n\t\t\t\t\t\t\tvalue={currentLanguage}\n\t\t\t\t\t\t\tonChange={handleLanguageChange}\n\t\t\t\t\t\t\toptions={AVAILABLE_LANGUAGES.map((lang) => ({\n\t\t\t\t\t\t\t\tvalue: lang.code,\n\t\t\t\t\t\t\t\tlabel: lang.name,\n\t\t\t\t\t\t\t}))}\n\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\tstyle={{ minWidth: '120px' }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</Col>\n\t\t\t\t<Col xs={6}>\n\t\t\t\t\t<H3\n\t\t\t\t\t\tkey={langKey}\n\t\t\t\t\t\tclassName={Classes.TEXT_MUTED}\n\t\t\t\t\t\tstyle={{ textAlign: 'right', margin: 0 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{t('Privacy Settings')}\n\t\t\t\t\t</H3>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Divider style={{ flexShrink: 0 }} />\n\t\t\t<div\n\t\t\t\tclassName={Classes.DIALOG_BODY}\n\t\t\t\tstyle={{ padding: '20px', overflowY: 'auto', flex: '1 1 auto' }}\n\t\t\t>\n\t\t\t\t<TranslatedContent key={langKey} />\n\t\t\t</div>\n\t\t\t<Divider style={{ flexShrink: 0 }} />\n\t\t\t<div className=\"privacy-control-buttons-container\">\n\t\t\t\t<TranslatedButtons\n\t\t\t\t\tkey={langKey}\n\t\t\t\t\tonAccept={handleAcceptClick}\n\t\t\t\t\tonOptOut={handleOptOutClick}\n\t\t\t\t\tcurrentStatus={currentStatus}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</Dialog>\n\t);\n}\n\nexport default PrivacyControlDialog;\n"
  },
  {
    "path": "src/client-viewer/src/components/VideoJSPlayer/index.tsx",
    "content": "import { useEffect, useRef } from 'react';\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore no types provided by video.js in this setup\nimport videojs from 'video.js';\nimport 'video.js/dist/video-js.css';\nimport './videojs-contain.css';\n\ninterface VideoJSPlayerProps {\n\tstream: MediaStream | null;\n\tplaying: boolean;\n\tcontainerEl: HTMLElement | null;\n}\n\ntype VideoJsPlayerInstance = {\n\tplay?: () => void | Promise<void>;\n\tpause?: () => void;\n\tdispose?: () => void;\n};\n\nfunction VideoJSPlayer(props: VideoJSPlayerProps) {\n\tconst { stream, playing, containerEl } = props;\n\tconst videoElRef = useRef<HTMLVideoElement | null>(null);\n\tconst playerRef = useRef<VideoJsPlayerInstance | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!containerEl || !videojs) return;\n\t\tif (playerRef.current) return;\n\n\t\tconst videoEl = document.createElement('video');\n\t\tvideoEl.className = 'video-js vjs-default-skin';\n\t\tvideoEl.setAttribute('playsinline', 'true');\n\t\tvideoEl.setAttribute('webkit-playsinline', 'true');\n\t\tvideoEl.muted = true; // allow autoplay on mobile/safari\n\t\tvideoEl.style.width = '100%';\n\t\tvideoEl.style.height = '100%';\n\t\tvideoEl.style.objectFit = 'contain';\n\t\tvideoEl.style.backgroundColor = 'black';\n\t\t// set container background to black to show letterboxing\n\t\ttry {\n\t\t\tcontainerEl.style.backgroundColor = 'black';\n\t\t} catch {\n\t\t\t// ignore styling errors\n\t\t}\n\t\tcontainerEl.appendChild(videoEl);\n\t\tvideoElRef.current = videoEl;\n\n\t\tplayerRef.current = videojs(videoEl, {\n\t\t\tcontrols: false,\n\t\t\tautoplay: false,\n\t\t\tpreload: 'auto',\n\t\t\tfluid: false,\n\t\t\tfill: false,\n\t\t\tresponsive: false,\n\t\t\tinactivityTimeout: 0,\n\t\t});\n\n\t\treturn () => {\n\t\t\ttry {\n\t\t\t\tif (playerRef.current) {\n\t\t\t\t\tconst instance = playerRef.current;\n\t\t\t\t\tinstance.dispose?.();\n\t\t\t\t\tplayerRef.current = null;\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (videoElRef.current && videoElRef.current.parentNode) {\n\t\t\t\t\tvideoElRef.current.parentNode.removeChild(videoElRef.current);\n\t\t\t\t}\n\t\t\t\tvideoElRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [containerEl]);\n\n\tuseEffect(() => {\n\t\tconst el = videoElRef.current;\n\t\tif (!el) return;\n\t\ttry {\n\t\t\tif (stream instanceof MediaStream) {\n\t\t\t\t// @ts-ignore srcObject exists on HTMLMediaElement\n\t\t\t\tel.srcObject = stream;\n\t\t\t\tel.style.objectFit = 'contain';\n\t\t\t\tel.style.backgroundColor = 'black';\n\t\t\t\tif (playing) {\n\t\t\t\t\t// attempt play after attaching\n\t\t\t\t\tconst p = el.play();\n\t\t\t\t\tif (p && typeof p.catch === 'function') {\n\t\t\t\t\t\tp.catch(() => {\n\t\t\t\t\t\t\t// ignore autoplay failures\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// @ts-ignore\n\t\t\t\tel.srcObject = null;\n\t\t\t\tel.removeAttribute('src');\n\t\t\t\tel.load();\n\t\t\t}\n\t\t} catch (e) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.error('Failed to attach MediaStream to element', e);\n\t\t}\n\t}, [stream, playing]);\n\n\tuseEffect(() => {\n\t\tconst player = playerRef.current;\n\t\tif (!player) return;\n\t\tif (playing) {\n\t\t\tplayer.play && player.play();\n\t\t} else {\n\t\t\tplayer.pause && player.pause();\n\t\t}\n\t}, [playing]);\n\n\treturn null;\n}\n\nexport default VideoJSPlayer;\n"
  },
  {
    "path": "src/client-viewer/src/components/VideoJSPlayer/videojs-contain.css",
    "content": ".video-js,\n.video-js .vjs-tech {\n\twidth: 100% !important;\n\theight: 100% !important;\n\tbackground-color: #000 !important;\n}\n\n.video-js .vjs-tech,\n.video-js .vjs-tech[style] {\n\tobject-fit: contain !important;\n}\n\n:fullscreen .video-js,\n:fullscreen .video-js .vjs-tech,\n:-webkit-full-screen .video-js,\n:-webkit-full-screen .video-js .vjs-tech {\n\twidth: 100% !important;\n\theight: 100% !important;\n\tobject-fit: contain !important;\n\tbackground-color: #000 !important;\n}\n"
  },
  {
    "path": "src/client-viewer/src/config/i18n.ts",
    "content": "/* istanbul ignore file */\n\nimport i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport Backend from 'i18next-http-backend';\n\n// don't want to use this?\n// have a look at the Quick start guide\n// for passing in lng and translations on init\n\ni18n\n\t// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)\n\t// learn more: https://github.com/i18next/i18next-http-backend\n\t.use(Backend)\n\t// pass the i18n instance to react-i18next.\n\t.use(initReactI18next)\n\t// init i18next\n\t// for all options read: https://www.i18next.com/overview/configuration-options\n\t.init({\n\t\tlng: 'en',\n\t\tsaveMissing: true,\n\t\tsaveMissingTo: 'all',\n\t\tfallbackLng: 'en', // TODO: to generate missing keys use false as value here, will be useful when custom nodejs server is created to store missing values\n\t\tdebug: false, // change to true to see debug message logs in browser console\n\t\t// whitelist: ['en', 'es', 'ru', 'ua', 'zh_CN', 'zh_TW', 'da', 'de', 'fi', 'ko', 'it', 'ja', 'nl', 'fr', 'sv'],\n\t\tbackend: {\n\t\t\t// path where resources get loaded from\n\t\t\tloadPath: '/locales/{{lng}}/{{ns}}.json',\n\t\t\t// 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\n\t\t\t// path to post missing resources\n\t\t\taddPath: '/locales/{{lng}}/{{ns}}.json',\n\t\t\t// jsonIndent to use when storing json files\n\t\t\tjsonIndent: 2,\n\t\t},\n\n\t\tkeySeparator: false, // we do not use keys in form messages.welcome\n\n\t\tinterpolation: {\n\t\t\tescapeValue: false, // react already safes from xss\n\t\t},\n\t});\n\nexport default i18n;\n"
  },
  {
    "path": "src/client-viewer/src/constants/appConstants.ts",
    "content": "export const PLAYER_WRAPPER_ID = 'player-wrapper-id';\nexport const VIDEO_QUALITY_TO_DECIMAL = {\n\t'25%': 0.25, // Q_25_PERCENT\n\t'40%': 0.4, // Q_40_PERCENT\n\t'60%': 0.6, // Q_60_PERCENT\n\t'80%': 0.8, // Q_80_PERCENT\n\t'100%': 1, // Q_100_PERCENT\n};\nexport const COMPARISON_CANVAS_ID = 'comparison-canvas';\nexport const DUMMY_MY_DEVICE_DETAILS = {\n\tmyIP: '',\n\tmyOS: '',\n\tmyDeviceType: '',\n\tmyBrowser: '',\n\tmyRoomId: '',\n};\n"
  },
  {
    "path": "src/client-viewer/src/constants/styleConstants.ts",
    "content": "export const LIGHT_UI_BACKGROUND = 'rgba(240, 248, 250, 1)';\n"
  },
  {
    "path": "src/client-viewer/src/containers/ConnectionPrompts/index.tsx",
    "content": "import { Row, Col } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport { LIGHT_UI_BACKGROUND } from '../../constants/styleConstants';\nimport MyDeviceInfoCard from '../../components/MyDeviceInfoCard';\nimport type { TFunction } from 'i18next';\nimport { Button, H3 } from '@blueprintjs/core';\nimport ConnectingIndicator from '../../components/ConnectingIndicator';\nimport DeskreenLogo from '../../images/deskreen_logo_128x128.png';\n\ninterface ConnectionPropmptsProps {\n\tmyDeviceDetails: DeviceDetails;\n\tisShownTextPrompt: boolean;\n\tpromptStep: number;\n\tconnectionIconType: ConnectionIconType;\n\tisShownSpinnerIcon: boolean;\n\tspinnerIconType: LoadingSharingIconType;\n}\n\nfunction getPromptContent(t: TFunction, step: number) {\n\tswitch (step) {\n\t\tcase 1:\n\t\t\treturn (\n\t\t\t\t<H3>\n\t\t\t\t\t{\n\t\t\t\t\t\tt(\n\t\t\t\t\t\t\t'Waiting for user to click ALLOW button on screen sharing device...',\n\t\t\t\t\t\t) as string\n\t\t\t\t\t}\n\t\t\t\t</H3>\n\t\t\t);\n\t\tcase 2:\n\t\t\treturn <H3>{t('Connected!') as string}</H3>;\n\t\tcase 3:\n\t\t\treturn (\n\t\t\t\t<H3>\n\t\t\t\t\t{\n\t\t\t\t\t\tt(\n\t\t\t\t\t\t\t'Waiting for user to select source to share from screen sharing device...',\n\t\t\t\t\t\t) as string\n\t\t\t\t\t}\n\t\t\t\t</H3>\n\t\t\t);\n\t\tdefault:\n\t\t\treturn <H3>{`${t('Error occurred')} :(`}</H3>;\n\t}\n}\n\nfunction ConnectionPropmpts(props: ConnectionPropmptsProps) {\n\tconst {\n\t\tmyDeviceDetails,\n\t\tpromptStep,\n\t\tconnectionIconType,\n\t\tisShownSpinnerIcon,\n\t\tspinnerIconType,\n\t} = props;\n\n\tconst { t } = useTranslation();\n\n\tconst handleReinitiateConnection = () => {\n\t\twindow.location.reload();\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tposition: 'absolute',\n\t\t\t\tzIndex: 10,\n\t\t\t\ttop: 0,\n\t\t\t\tleft: 0,\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100vh',\n\t\t\t\tboxShadow: '0 0 0 5px #A7B6C2',\n\t\t\t\tbackgroundColor: LIGHT_UI_BACKGROUND,\n\t\t\t}}\n\t\t>\n\t\t\t<Row\n\t\t\t\tbottom=\"xs\"\n\t\t\t\tstyle={{\n\t\t\t\t\theight: '50vh',\n\t\t\t\t\twidth: '100%',\n\t\t\t\t\tmarginRight: '0px',\n\t\t\t\t\tmarginLeft: '0px',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Row center=\"xs\" style={{ width: '100%', margin: '0 auto' }}>\n\t\t\t\t\t<Col\n\t\t\t\t\t\txs={12}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginBottom: '50px',\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div style={{ width: '100%' }}>\n\t\t\t\t\t\t\t<Row\n\t\t\t\t\t\t\t\tcenter=\"xs\"\n\t\t\t\t\t\t\t\tstyle={{ marginTop: '30px', marginBottom: '10px' }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tsrc={DeskreenLogo}\n\t\t\t\t\t\t\t\t\talt=\"Deskreen Logo\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\twidth: '80px',\n\t\t\t\t\t\t\t\t\t\theight: '80px',\n\t\t\t\t\t\t\t\t\t\tmarginBottom: '5px',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t\t\t<H3>Deskreen CE Viewer</H3>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t<Row center=\"xs\" style={{ width: '100%', margin: '0 auto' }}>\n\t\t\t\t\t\t\t\t<Col md={6} xl={4}>\n\t\t\t\t\t\t\t\t\t<MyDeviceInfoCard deviceDetails={myDeviceDetails} />\n\t\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t<div id=\"prompt-text\" style={{ fontSize: '20px' }}>\n\t\t\t\t\t\t\t\t{getPromptContent(t, promptStep)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '20px' }}>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\tclassName=\"rounded-pill-button\"\n\t\t\t\t\t\t\t\t\tintent=\"warning\"\n\t\t\t\t\t\t\t\t\tonClick={handleReinitiateConnection}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{t('re-initiate-connection') as string}\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Row>\n\t\t\t<ConnectingIndicator\n\t\t\t\tcurrentStep={promptStep}\n\t\t\t\tconnectionIconType={connectionIconType}\n\t\t\t\tisShownSelectingSharingIcon={isShownSpinnerIcon}\n\t\t\t\tselectingSharingIconType={spinnerIconType}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nexport default ConnectionPropmpts;\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/ConnectionIconEnum.ts",
    "content": "const ConnectionIcon = {\n\tFEED: 'feed',\n\tFEED_SUBSCRIBED: 'feed-subscribed',\n} as const;\n\nexport default ConnectionIcon;\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/LoadingSharingIconEnum.ts",
    "content": "export const LoadingSharingIconEnum = {\n\tDESKTOP: 'desktop',\n\tAPPLICATION: 'application',\n} as const;\n\nexport type LoadingSharingIconType =\n\t(typeof LoadingSharingIconEnum)[keyof typeof LoadingSharingIconEnum];\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/changeLanguage.ts",
    "content": "import i18n from '../../config/i18n';\n\nexport default (lng: string) => {\n\ti18n.changeLanguage(lng);\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/handleCreatePeerConnection.ts",
    "content": "import PeerConnection from '../../features/PeerConnection';\nimport PeerConnectionUIHandler from '../../features/PeerConnection/PeerConnectionUIHandler';\nimport VideoAutoQualityOptimizer from '../../features/VideoAutoQualityOptimizer';\nimport changeLanguage from './changeLanguage';\nimport ConnectionIcon from './ConnectionIconEnum';\n\nexport default (params: CreatePeerConnectionUseEffectParams) => {\n\tconst {\n\t\tpeer,\n\t\tconnectionRoomId,\n\t\tsetMyDeviceDetails,\n\t\tsetConnectionIconType,\n\t\tsetIsShownTextPrompt,\n\t\tsetPromptStep,\n\t\tsetScreenSharingSourceType,\n\t\tsetDialogErrorMessage,\n\t\tsetIsErrorDialogOpen,\n\t\tsetUrl,\n\t\tsetPeer,\n\t} = params;\n\n\t// return the effect function\n\treturn () => {\n\t\tif (!peer) {\n\t\t\tif (connectionRoomId === '') {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst UIHandler = new PeerConnectionUIHandler(\n\t\t\t\tsetMyDeviceDetails,\n\t\t\t\t() => {\n\t\t\t\t\tsetConnectionIconType(ConnectionIcon.FEED_SUBSCRIBED);\n\n\t\t\t\t\tsetIsShownTextPrompt(false);\n\t\t\t\t\tsetIsShownTextPrompt(true);\n\t\t\t\t\tsetPromptStep(2);\n\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tsetIsShownTextPrompt(false);\n\t\t\t\t\t\tsetIsShownTextPrompt(true);\n\t\t\t\t\t\tsetPromptStep(3);\n\t\t\t\t\t}, 2000);\n\t\t\t\t},\n\t\t\t\tsetScreenSharingSourceType,\n\t\t\t\tchangeLanguage,\n\t\t\t\tsetDialogErrorMessage,\n\t\t\t\tsetIsErrorDialogOpen,\n\t\t\t);\n\n\t\t\tconst _peer = new PeerConnection(\n\t\t\t\tconnectionRoomId,\n\t\t\t\tsetUrl,\n\t\t\t\tnew VideoAutoQualityOptimizer(),\n\t\t\t\tUIHandler,\n\t\t\t);\n\n\t\t\tsetPeer(_peer);\n\n\t\t\tsetTimeout(() => {\n\t\t\t\tsetIsShownTextPrompt(true);\n\t\t\t}, 100);\n\t\t}\n\n\t\t// return cleanup function - cleanup when connectionRoomId changes or component unmounts\n\t\treturn () => {\n\t\t\tif (peer) {\n\t\t\t\tpeer.destroy();\n\t\t\t\tsetPeer(undefined);\n\t\t\t}\n\t\t};\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/handleDisplayingLoadingSharingIconLoop.ts",
    "content": "import { LoadingSharingIconEnum } from './LoadingSharingIconEnum';\n\nexport default (params: handleDisplayingLoadingSharingIconLoopParams) => {\n\tconst {\n\t\tpromptStep,\n\t\turl,\n\t\tsetIsShownLoadingSharingIcon,\n\t\tloadingSharingIconType,\n\t\tisShownLoadingSharingIcon,\n\t\tsetLoadingSharingIconType,\n\t} = params;\n\treturn () => {\n\t\tlet interval: NodeJS.Timeout;\n\t\tif (promptStep === 3 && url === null) {\n\t\t\tsetIsShownLoadingSharingIcon(true);\n\n\t\t\tlet currentIcon = loadingSharingIconType;\n\t\t\tlet isShownIcon = isShownLoadingSharingIcon;\n\t\t\tlet isShownWithFadingUIEffect = false;\n\t\t\tinterval = setInterval(() => {\n\t\t\t\tisShownIcon = !isShownIcon;\n\t\t\t\tsetIsShownLoadingSharingIcon(isShownIcon);\n\t\t\t\tif (isShownWithFadingUIEffect) {\n\t\t\t\t\tcurrentIcon =\n\t\t\t\t\t\tcurrentIcon === LoadingSharingIconEnum.DESKTOP\n\t\t\t\t\t\t\t? LoadingSharingIconEnum.APPLICATION\n\t\t\t\t\t\t\t: LoadingSharingIconEnum.DESKTOP;\n\t\t\t\t\tsetLoadingSharingIconType(currentIcon);\n\t\t\t\t\tisShownWithFadingUIEffect = false;\n\t\t\t\t} else {\n\t\t\t\t\tisShownWithFadingUIEffect = true;\n\t\t\t\t}\n\t\t\t}, 1500);\n\t\t}\n\n\t\treturn () => {\n\t\t\tclearInterval(interval);\n\t\t};\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/handleNoConnectionTimeout.ts",
    "content": "import { DUMMY_MY_DEVICE_DETAILS } from '../../constants/appConstants';\n\nexport default (\n\tmyDeviceDetails: DeviceDetails,\n\tsetIsErrorDialogOpen: (_: boolean) => void,\n) => {\n\treturn () => {\n\t\tconst timeout = setTimeout(() => {\n\t\t\tif (myDeviceDetails === DUMMY_MY_DEVICE_DETAILS) {\n\t\t\t\tsetIsErrorDialogOpen(true);\n\t\t\t}\n\t\t}, 10000);\n\t\treturn () => {\n\t\t\tclearTimeout(timeout);\n\t\t};\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/handleRemoveDanglingReactRevealContainer.ts",
    "content": "export default (url: MediaStream | null) => {\n\treturn () => {\n\t\tif (url !== null) {\n\t\t\tsetTimeout(() => {\n\t\t\t\t// @ts-ignore\n\t\t\t\tdocument.querySelector('.container > div:nth-child(1)').style.display =\n\t\t\t\t\t'none';\n\t\t\t}, 1000);\n\t\t}\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/handleSetVideoQuality.ts",
    "content": "import PeerConnection from '../../features/PeerConnection';\nimport { type VideoQualityType } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';\n\nexport default (\n\tvideoQuality: VideoQualityType,\n\tpeer: PeerConnection | undefined,\n) => {\n\treturn () => {\n\t\tif (!peer) return;\n\t\tif (!peer.isStreamStarted) return;\n\t\tpeer.setVideoQuality(videoQuality);\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/index.css",
    "content": "@media (prefers-reduced-motion: no-preference) {\n\t.App-logo {\n\t\tanimation: App-logo-spin infinite 20s linear;\n\t}\n\n\t#pulsing-circle-1 {\n\t\tanimation: pulse1 infinite 6s linear;\n\t}\n\n\t#pulsing-circle-2 {\n\t\tanimation: pulse2 infinite 6s linear;\n\t}\n\n\t.pulse-3-once {\n\t\tanimation: pulse3twice 2 750ms linear;\n\t}\n}\n\n@keyframes pulse1 {\n\t0% {\n\t\ttransform: scale(0.95);\n\t\tbox-shadow: 0 0 0 0 rgba(72, 175, 240, 0.7);\n\t}\n\n\t80% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 15px rgba(72, 175, 240, 0.4);\n\t}\n\n\t100% {\n\t\ttransform: scale(0.95);\n\t\tbox-shadow: 0 0 0 0 rgba(72, 175, 240, 0);\n\t}\n}\n\n@keyframes pulse2 {\n\t100% {\n\t\ttransform: scale(0.95);\n\t\tbox-shadow: 0 0 0 0 rgba(72, 175, 240, 0);\n\t}\n\n\t80% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 33px rgba(72, 175, 240, 0.3);\n\t}\n\n\t0% {\n\t\ttransform: scale(0.95);\n\t\tbox-shadow: 0 0 0 0 rgba(72, 175, 240, 1);\n\t}\n}\n\n@keyframes pulse3twice {\n\t0% {\n\t\tbox-shadow: 0 0 0 0 rgba(21, 179, 113, 0.7);\n\t}\n\n\t50% {\n\t\tbox-shadow: 0 0 0 30px rgba(21, 179, 113, 0.3);\n\t}\n\n\t100% {\n\t\tbox-shadow: 0 0 0 0 rgba(21, 179, 113, 0);\n\t}\n}\n\n.container > .react-reveal {\n\toverflow: hidden;\n}\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/index.tsx",
    "content": "import { useEffect, useState, useCallback } from 'react';\nimport { Grid } from 'react-flexbox-grid';\nimport screenfull from 'screenfull';\nimport './index.css';\nimport PeerConnection from '../../features/PeerConnection';\nimport {\n\tVideoQuality,\n\ttype VideoQualityType,\n} from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';\nimport ErrorDialog from '../../components/ErrorDialog';\nimport {\n\tErrorMessage,\n\ttype ErrorMessageType,\n} from '../../components/ErrorDialog/ErrorMessageEnum';\nimport ConnectionPropmpts from '../../containers/ConnectionPrompts';\nimport PlayerView from '../../containers/PlayerView';\nimport handleSetVideoQuality from './handleSetVideoQuality';\nimport { DUMMY_MY_DEVICE_DETAILS } from '../../constants/appConstants';\nimport handleNoConnectionTimeout from './handleNoConnectionTimeout';\nimport handleCreatePeerConnection from './handleCreatePeerConnection';\nimport handleRemoveDanglingReactRevealContainer from './handleRemoveDanglingReactRevealContainer';\nimport handleDisplayingLoadingSharingIconLoop from './handleDisplayingLoadingSharingIconLoop';\nimport { ScreenSharingSource } from '../../features/PeerConnection/ScreenSharingSourceEnum';\nimport ConnectionIcon from './ConnectionIconEnum';\nimport { LoadingSharingIconEnum } from './LoadingSharingIconEnum';\nimport { useScreenViewingTracker } from './useScreenViewingTracker';\n\nfunction MainView() {\n\tconst [isErrorDialogOpen, setIsErrorDialogOpen] = useState(false);\n\n\tconst [promptStep, setPromptStep] = useState(1);\n\tconst [dialogErrorMessage, setDialogErrorMessage] =\n\t\tuseState<ErrorMessageType>(ErrorMessage.UNKNOWN_ERROR);\n\tconst [connectionIconType, setConnectionIconType] =\n\t\tuseState<ConnectionIconType>(ConnectionIcon.FEED);\n\tconst [myDeviceDetails, setMyDeviceDetails] = useState<DeviceDetails>(\n\t\tDUMMY_MY_DEVICE_DETAILS,\n\t);\n\n\tconst [playing, setPlaying] = useState(true);\n\tconst [url, setUrl] = useState<MediaStream | null>(null);\n\tconst [screenSharingSourceType, setScreenSharingSourceType] =\n\t\tuseState<ScreenSharingSourceType>(ScreenSharingSource.SCREEN);\n\tconst [isWithControls, setIsWithControls] = useState(!screenfull.isEnabled);\n\tconst [isShownTextPrompt, setIsShownTextPrompt] = useState(false);\n\tconst [isShownLoadingSharingIcon, setIsShownLoadingSharingIcon] =\n\t\tuseState(false);\n\tconst [loadingSharingIconType, setLoadingSharingIconType] =\n\t\tuseState<LoadingSharingIconType>(LoadingSharingIconEnum.DESKTOP);\n\tconst [videoQuality, setVideoQuality] = useState<VideoQualityType>(\n\t\tVideoQuality.Q_100_PERCENT,\n\t);\n\tconst [peer, setPeer] = useState<undefined | PeerConnection>();\n\tconst [connectionRoomId, setConnectionRoomId] = useState<string>('');\n\n\tuseEffect(() => {\n\t\tconst { pathname } = window.location;\n\t\tconst normalizedPath = pathname.startsWith('/')\n\t\t\t? pathname.slice(1)\n\t\t\t: pathname;\n\t\tconst extractedRoomId = normalizedPath.split('/').filter(Boolean)[0] || '';\n\n\t\tif (extractedRoomId !== '') {\n\t\t\tsetConnectionRoomId(extractedRoomId);\n\t\t\treturn;\n\t\t}\n\n\t\tconst fallbackRoomId = Math.random().toString(36).substring(2, 10);\n\t\tsetConnectionRoomId(fallbackRoomId);\n\t}, []);\n\n\tuseEffect(handleSetVideoQuality(videoQuality, peer), [videoQuality, peer]);\n\n\tuseEffect(handleNoConnectionTimeout(myDeviceDetails, setIsErrorDialogOpen), [\n\t\tmyDeviceDetails,\n\t]);\n\n\tuseEffect(\n\t\thandleCreatePeerConnection({\n\t\t\tpeer,\n\t\t\tconnectionRoomId,\n\t\t\tsetMyDeviceDetails,\n\t\t\tsetConnectionIconType,\n\t\t\tsetIsShownTextPrompt,\n\t\t\tsetPromptStep,\n\t\t\tsetScreenSharingSourceType,\n\t\t\tsetDialogErrorMessage,\n\t\t\tsetIsErrorDialogOpen,\n\t\t\tsetUrl,\n\t\t\tsetPeer,\n\t\t}),\n\t\t[connectionRoomId],\n\t);\n\n\tconst handlePlayPause = useCallback(() => {\n\t\tsetPlaying(!playing);\n\t}, [playing]);\n\n\tuseEffect(handleRemoveDanglingReactRevealContainer(url), [url]);\n\n\tuseEffect(\n\t\thandleDisplayingLoadingSharingIconLoop({\n\t\t\tpromptStep,\n\t\t\turl,\n\t\t\tsetIsShownLoadingSharingIcon,\n\t\t\tloadingSharingIconType,\n\t\t\tisShownLoadingSharingIcon,\n\t\t\tsetLoadingSharingIconType,\n\t\t}),\n\t\t[promptStep, url],\n\t);\n\n\tuseScreenViewingTracker({\n\t\tstreamUrl: url,\n\t\tisPlaying: playing,\n\t\tisErrorDialogOpen,\n\t\tdialogErrorMessage,\n\t});\n\n\treturn (\n\t\t<Grid>\n\t\t\t<ConnectionPropmpts\n\t\t\t\tmyDeviceDetails={myDeviceDetails}\n\t\t\t\tisShownTextPrompt={isShownTextPrompt}\n\t\t\t\tpromptStep={promptStep}\n\t\t\t\tconnectionIconType={connectionIconType}\n\t\t\t\tspinnerIconType={loadingSharingIconType}\n\t\t\t\tisShownSpinnerIcon={isShownLoadingSharingIcon}\n\t\t\t/>\n\t\t\t<PlayerView\n\t\t\t\tstreamUrl={url}\n\t\t\t\tscreenSharingSourceType={screenSharingSourceType}\n\t\t\t\tsetIsWithControls={setIsWithControls}\n\t\t\t\tisWithControls={isWithControls}\n\t\t\t\thandlePlayPause={handlePlayPause}\n\t\t\t\tisPlaying={playing}\n\t\t\t\tsetPlaying={setPlaying}\n\t\t\t\tsetVideoQuality={setVideoQuality}\n\t\t\t\tvideoQuality={videoQuality}\n\t\t\t/>\n\t\t\t<ErrorDialog\n\t\t\t\terrorMessage={dialogErrorMessage}\n\t\t\t\tisOpen={isErrorDialogOpen}\n\t\t\t\tsetIsOpen={setIsErrorDialogOpen}\n\t\t\t/>\n\t\t</Grid>\n\t);\n}\n\nexport default MainView;\n"
  },
  {
    "path": "src/client-viewer/src/containers/MainView/useScreenViewingTracker.ts",
    "content": "import { useEffect, useRef } from 'react';\nimport { trackAnalyticsEvent } from '../../utils/analytics';\nimport { type ErrorMessageType } from '../../components/ErrorDialog/ErrorMessageEnum';\n\ninterface UseScreenViewingTrackerParams {\n\tstreamUrl: MediaStream | null;\n\tisPlaying: boolean;\n\tisErrorDialogOpen: boolean;\n\tdialogErrorMessage: ErrorMessageType;\n}\n\nfunction formatErrorMessageForEvent(errorMessage: ErrorMessageType): string {\n\t// convert error message to event-friendly format\n\t// e.g., \"An unknown error occurred\" -> \"an_unknown_error_occurred\"\n\treturn errorMessage\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9\\s]/g, '')\n\t\t.replace(/\\s+/g, '_');\n}\n\nexport function useScreenViewingTracker(\n\tparams: UseScreenViewingTrackerParams,\n): void {\n\tconst { streamUrl, isPlaying, isErrorDialogOpen, dialogErrorMessage } =\n\t\tparams;\n\n\tconst startTimeRef = useRef<number | null>(null);\n\tconst intervalRef = useRef<NodeJS.Timeout | null>(null);\n\tconst lastMinuteTrackedRef = useRef<number>(0);\n\tconst errorEventSentRef = useRef<boolean>(false);\n\tconst previousErrorDialogOpenRef = useRef<boolean>(false);\n\tconst isErrorDialogOpenRef = useRef<boolean>(false);\n\n\t// determine if stream is currently visible (without error dialog check)\n\tconst isStreamVisible = streamUrl !== null && isPlaying;\n\n\t// update error dialog ref\n\tisErrorDialogOpenRef.current = isErrorDialogOpen;\n\n\t// handle error dialog appearance\n\tuseEffect(() => {\n\t\t// if error dialog just appeared and we were tracking\n\t\tif (\n\t\t\tisErrorDialogOpen &&\n\t\t\t!previousErrorDialogOpenRef.current &&\n\t\t\tstartTimeRef.current !== null &&\n\t\t\t!errorEventSentRef.current\n\t\t) {\n\t\t\t// clear interval\n\t\t\tif (intervalRef.current !== null) {\n\t\t\t\tclearInterval(intervalRef.current);\n\t\t\t\tintervalRef.current = null;\n\t\t\t}\n\n\t\t\t// calculate total minutes spent viewing\n\t\t\tconst elapsedMs = Date.now() - startTimeRef.current;\n\t\t\tconst elapsedMinutes = Math.floor(elapsedMs / 60000);\n\t\t\tconst errorReason = formatErrorMessageForEvent(dialogErrorMessage);\n\n\t\t\t// send error event\n\t\t\ttrackAnalyticsEvent(\n\t\t\t\t`error_dialog_reason_${errorReason}_spent_screen_viewing_${elapsedMinutes}_minutes`,\n\t\t\t\t{},\n\t\t\t);\n\n\t\t\terrorEventSentRef.current = true;\n\t\t}\n\n\t\tpreviousErrorDialogOpenRef.current = isErrorDialogOpen;\n\t}, [isErrorDialogOpen, dialogErrorMessage]);\n\n\t// handle stream visibility tracking\n\tuseEffect(() => {\n\t\t// if stream becomes visible and no error dialog, start tracking\n\t\tif (\n\t\t\tisStreamVisible &&\n\t\t\t!isErrorDialogOpen &&\n\t\t\tstartTimeRef.current === null\n\t\t) {\n\t\t\tstartTimeRef.current = Date.now();\n\t\t\tlastMinuteTrackedRef.current = 0;\n\t\t\terrorEventSentRef.current = false;\n\n\t\t\t// set up interval to check every minute\n\t\t\tintervalRef.current = setInterval(() => {\n\t\t\t\tif (startTimeRef.current === null || isErrorDialogOpenRef.current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst elapsedMs = Date.now() - startTimeRef.current;\n\t\t\t\tconst elapsedMinutes = Math.floor(elapsedMs / 60000);\n\n\t\t\t\t// send event for each new minute\n\t\t\t\tif (elapsedMinutes > lastMinuteTrackedRef.current) {\n\t\t\t\t\tlastMinuteTrackedRef.current = elapsedMinutes;\n\t\t\t\t\ttrackAnalyticsEvent(`screen_viewing_${elapsedMinutes}_minutes`, {});\n\t\t\t\t}\n\t\t\t}, 60000); // check every minute\n\t\t}\n\n\t\t// if stream is not visible (and no error dialog), stop tracking and reset\n\t\tif (\n\t\t\t(!isStreamVisible || isErrorDialogOpen) &&\n\t\t\tintervalRef.current !== null\n\t\t) {\n\t\t\tclearInterval(intervalRef.current);\n\t\t\tintervalRef.current = null;\n\t\t}\n\n\t\t// reset tracking state when stream is not visible (only if no error dialog)\n\t\tif (!isStreamVisible && !isErrorDialogOpen) {\n\t\t\tstartTimeRef.current = null;\n\t\t\tlastMinuteTrackedRef.current = 0;\n\t\t\terrorEventSentRef.current = false;\n\t\t}\n\n\t\t// if error dialog closes and stream is still visible, restart tracking\n\t\tif (\n\t\t\t!isErrorDialogOpen &&\n\t\t\tpreviousErrorDialogOpenRef.current &&\n\t\t\tisStreamVisible &&\n\t\t\terrorEventSentRef.current\n\t\t) {\n\t\t\t// reset error event flag and restart tracking\n\t\t\terrorEventSentRef.current = false;\n\t\t\tstartTimeRef.current = Date.now();\n\t\t\tlastMinuteTrackedRef.current = 0;\n\n\t\t\t// restart interval\n\t\t\tif (intervalRef.current === null) {\n\t\t\t\tintervalRef.current = setInterval(() => {\n\t\t\t\t\tif (startTimeRef.current === null || isErrorDialogOpenRef.current) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst elapsedMs = Date.now() - startTimeRef.current;\n\t\t\t\t\tconst elapsedMinutes = Math.floor(elapsedMs / 60000);\n\n\t\t\t\t\t// send event for each new minute\n\t\t\t\t\tif (elapsedMinutes > lastMinuteTrackedRef.current) {\n\t\t\t\t\t\tlastMinuteTrackedRef.current = elapsedMinutes;\n\t\t\t\t\t\ttrackAnalyticsEvent(`screen_viewing_${elapsedMinutes}_minutes`, {});\n\t\t\t\t\t}\n\t\t\t\t}, 60000);\n\t\t\t}\n\t\t}\n\n\t\t// cleanup on unmount\n\t\treturn () => {\n\t\t\tif (intervalRef.current !== null) {\n\t\t\t\tclearInterval(intervalRef.current);\n\t\t\t\tintervalRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [isStreamVisible, isErrorDialogOpen]);\n}\n"
  },
  {
    "path": "src/client-viewer/src/containers/PlayerView/index.tsx",
    "content": "import { useEffect, useRef, useCallback } from 'react';\nimport { OverlayToaster, Position } from '@blueprintjs/core';\nimport { useTranslation } from 'react-i18next';\nimport VideoJSPlayer from '../../components/VideoJSPlayer';\nimport PlayerControlPanel from '../../components/PlayerControlPanel';\nimport {\n\tCOMPARISON_CANVAS_ID,\n\tPLAYER_WRAPPER_ID,\n} from '../../constants/appConstants';\nimport { type VideoQualityType } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';\nimport { togglePlayerFullscreen } from '../../utils/playerFullscreen';\n\ninterface PlayerViewProps {\n\tisWithControls: boolean;\n\tsetIsWithControls: (_: boolean) => void;\n\thandlePlayPause: () => void;\n\tisPlaying: boolean;\n\tsetPlaying: (playing: boolean) => void;\n\tsetVideoQuality: (_: VideoQualityType) => void;\n\tvideoQuality: VideoQualityType;\n\tscreenSharingSourceType: ScreenSharingSourceType;\n\tstreamUrl: MediaStream | null;\n}\n\ntype IOSVideoElement = HTMLVideoElement & {\n\twebkitEnterFullscreen?: () => void;\n\twebkitExitFullscreen?: () => void;\n\twebkitSupportsFullscreen?: boolean;\n\twebkitDisplayingFullscreen?: boolean;\n};\n\nfunction PlayerView(props: PlayerViewProps) {\n\tconst { t } = useTranslation();\n\tconst {\n\t\tscreenSharingSourceType,\n\t\tsetIsWithControls,\n\t\tisWithControls,\n\t\thandlePlayPause,\n\t\tisPlaying,\n\t\tsetPlaying,\n\t\tsetVideoQuality,\n\t\tvideoQuality,\n\t\tstreamUrl,\n\t} = props;\n\n\t// const player = useRef(null);\n\n\tconst videoRef = useRef<HTMLVideoElement>(null);\n\tconst toasterRef = useRef<Awaited<ReturnType<typeof OverlayToaster.create>> | null>(null);\n\t// no external player ref needed for video.js variant\n\n\tuseEffect(() => {\n\t\tif (!streamUrl) return;\n\n\t\t// html5 video mode\n\t\tif (isWithControls && videoRef.current) {\n\t\t\tif (streamUrl instanceof MediaStream) {\n\t\t\t\tvideoRef.current.srcObject = streamUrl;\n\t\t\t} else {\n\t\t\t\tvideoRef.current.src = streamUrl;\n\t\t\t}\n\t\t\tvideoRef.current.play().catch((error) => {\n\t\t\t\tconsole.error('Error playing video:', error);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// video.js mode (default) doesn't need imperative src assignment here\n\t}, [streamUrl, isWithControls]);\n\n\tuseEffect(() => {\n\t\tif (isWithControls) {\n\t\t\tif (!videoRef.current) return;\n\t\t\tif (isPlaying) {\n\t\t\t\tvideoRef.current.play().catch((error) => {\n\t\t\t\t\tconsole.error('Error playing video:', error);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tvideoRef.current.pause();\n\t\t\t}\n\t\t}\n\t\t// react-player play/pause is handled via its `playing` prop\n\t}, [isPlaying, isWithControls]);\n\n\t// initialize toaster\n\tuseEffect(() => {\n\t\tconst initToaster = async () => {\n\t\t\tif (!toasterRef.current) {\n\t\t\t\ttoasterRef.current = await OverlayToaster.create({\n\t\t\t\t\tposition: Position.BOTTOM,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\tinitToaster();\n\t}, []);\n\n\t// wrap handlePlayPause to show toaster notifications\n\tconst handlePlayPauseWithNotification = useCallback(() => {\n\t\tconst nextPlaying = !isPlaying;\n\t\thandlePlayPause();\n\t\t\n\t\t// show notification after a small delay to ensure state is updated\n\t\tsetTimeout(() => {\n\t\t\tif (toasterRef.current) {\n\t\t\t\ttoasterRef.current.show({\n\t\t\t\t\tmessage: nextPlaying ? t('Video stream is playing') : t('Video stream is paused'),\n\t\t\t\t\tintent: nextPlaying ? 'success' : 'warning',\n\t\t\t\t\ttimeout: 2000,\n\t\t\t\t});\n\t\t\t}\n\t\t}, 50);\n\t}, [handlePlayPause, isPlaying, t]);\n\n\t// handle iPhone fullscreen exit - detect when video stops and auto-resume\n\tuseEffect(() => {\n\t\tif (!streamUrl) return;\n\n\t\tconst getVideoElement = (): IOSVideoElement | null => {\n\t\t\tif (isWithControls && videoRef.current) {\n\t\t\t\treturn videoRef.current as IOSVideoElement;\n\t\t\t}\n\t\t\tconst container = document.getElementById(PLAYER_WRAPPER_ID);\n\t\t\tif (!container) return null;\n\t\t\tconst maybeVideo = container.querySelector('video');\n\t\t\tif (!(maybeVideo instanceof HTMLVideoElement)) return null;\n\t\t\treturn maybeVideo as IOSVideoElement;\n\t\t};\n\n\t\tconst handleFullscreenEnd = () => {\n\t\t\t// small delay to ensure video state is updated after fullscreen exit\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst video = getVideoElement();\n\t\t\t\tif (!video) return;\n\n\t\t\t\t// check if video is paused after exiting fullscreen\n\t\t\t\tif (video.paused) {\n\t\t\t\t\t// sync play state - ensure button shows \"Play\" instead of \"Pause\"\n\t\t\t\t\tsetPlaying(false);\n\n\t\t\t\t\t// show warning notification that video stopped and user needs to click play\n\t\t\t\t\tif (toasterRef.current) {\n\t\t\t\t\t\ttoasterRef.current.show({\n\t\t\t\t\t\t\tmessage: t('Video stream paused after exiting fullscreen. Please click Play to continue.'),\n\t\t\t\t\t\t\tintent: 'warning',\n\t\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// video is playing, but state might be wrong - sync it\n\t\t\t\t\tif (!isPlaying) {\n\t\t\t\t\t\tsetPlaying(true);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, 150);\n\t\t};\n\n\t\tconst attachListener = (video: IOSVideoElement | null) => {\n\t\t\tif (video) {\n\t\t\t\tvideo.addEventListener('webkitendfullscreen', handleFullscreenEnd);\n\t\t\t}\n\t\t};\n\n\t\tconst detachListener = (video: IOSVideoElement | null) => {\n\t\t\tif (video) {\n\t\t\t\tvideo.removeEventListener('webkitendfullscreen', handleFullscreenEnd);\n\t\t\t}\n\t\t};\n\n\t\tlet currentVideo: IOSVideoElement | null = getVideoElement();\n\t\tattachListener(currentVideo);\n\n\t\t// watch for video element changes (especially for VideoJSPlayer)\n\t\tconst container = document.getElementById(PLAYER_WRAPPER_ID);\n\t\tlet observer: MutationObserver | null = null;\n\n\t\tif (container) {\n\t\t\tobserver = new MutationObserver(() => {\n\t\t\t\tconst newVideo = getVideoElement();\n\t\t\t\tif (newVideo !== currentVideo) {\n\t\t\t\t\tdetachListener(currentVideo);\n\t\t\t\t\tcurrentVideo = newVideo;\n\t\t\t\t\tattachListener(currentVideo);\n\t\t\t\t}\n\t\t\t});\n\t\t\tobserver.observe(container, { childList: true, subtree: true });\n\t\t}\n\n\t\treturn () => {\n\t\t\tdetachListener(currentVideo);\n\t\t\tif (observer) {\n\t\t\t\tobserver.disconnect();\n\t\t\t}\n\t\t};\n\t}, [streamUrl, isWithControls, isPlaying, setPlaying, t]);\n\n\t// @ts-ignore\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tposition: 'absolute',\n\t\t\t\tzIndex: 1,\n\t\t\t\ttop: 0,\n\t\t\t\tleft: 0,\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100vh',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\toverflow: 'hidden',\n\t\t\t}}\n\t\t>\n\t\t\t<PlayerControlPanel\n\t\t\t\tonSwitchChangedCallback={(isEnabled) => setIsWithControls(isEnabled)}\n\t\t\t\tisDefaultPlayerTurnedOn={isWithControls}\n\t\t\t\thandleClickFullscreen={() => {\n\t\t\t\t\tconst result = togglePlayerFullscreen();\n\t\t\t\t\tif (result === 'failed') {\n\t\t\t\t\t\tconsole.warn('Unable to toggle fullscreen');\n\t\t\t\t\t}\n\t\t\t\t\treturn result;\n\t\t\t\t}}\n\t\t\t\thandleClickPlayPause={handlePlayPauseWithNotification}\n\t\t\t\tisPlaying={isPlaying}\n\t\t\t\tsetVideoQuality={setVideoQuality}\n\t\t\t\tselectedVideoQuality={videoQuality}\n\t\t\t\tscreenSharingSourceType={screenSharingSourceType}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tid=\"video-container\"\n\t\t\t\tstyle={{\n\t\t\t\t\tmargin: '0 auto',\n\t\t\t\t\tposition: 'relative',\n\t\t\t\t\tflex: 1,\n\t\t\t\t\twidth: '100%',\n\t\t\t\t\theight: '100%',\n\t\t\t\t\tminHeight: 0,\n\t\t\t\t\tbackgroundColor: 'black',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tid={PLAYER_WRAPPER_ID}\n\t\t\t\t\tclassName=\"player-wrapper\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\tbackgroundColor: 'black',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{isWithControls ? (\n\t\t\t\t\t\t<video\n\t\t\t\t\t\t\tref={videoRef}\n\t\t\t\t\t\t\tautoPlay\n\t\t\t\t\t\t\tplaysInline\n\t\t\t\t\t\t\tmuted\n\t\t\t\t\t\t\tclassName=\"absolute top-0 left-0 w-full h-full\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\t\t\tobjectFit: 'contain',\n\t\t\t\t\t\t\t\tbackgroundColor: 'black',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<VideoJSPlayer\n\t\t\t\t\t\t\tstream={streamUrl}\n\t\t\t\t\t\t\tplaying={isPlaying}\n\t\t\t\t\t\t\tcontainerEl={document.getElementById(PLAYER_WRAPPER_ID)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t<canvas id={COMPARISON_CANVAS_ID} style={{ display: 'none' }}></canvas>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport default PlayerView;\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/NullUser.ts",
    "content": "export default { username: '', id: '' };\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/PartnerPeerUser.d.ts",
    "content": "interface PartnerPeerUser {\n\tusername: string;\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/PeerConnection.d.ts",
    "content": "type PeerConnection = import('.').default;\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/PeerConnectionUIHandler.ts",
    "content": "import {\n\tErrorMessage,\n\ttype ErrorMessageType,\n} from '../../components/ErrorDialog/ErrorMessageEnum';\n\nexport default class PeerConnectionUIHandler {\n\tsetMyDeviceDetails: (details: DeviceDetails) => void;\n\n\thostAllowedToConnectCallback: () => void;\n\n\tsetScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void;\n\n\tsetAppLanguageCallback: (newLang: string) => void;\n\n\tsetDialogErrorMessageCallback: (message: ErrorMessageType) => void;\n\n\tsetIsErrorDialogOpen: (val: boolean) => void;\n\n\terrorDialogMessage: ErrorMessageType = ErrorMessage.UNKNOWN_ERROR;\n\n\tconstructor(\n\t\tsetMyDeviceDetails: (details: DeviceDetails) => void,\n\t\thostAllowedToConnectCallback: () => void,\n\t\tsetScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void,\n\t\tsetAppLanguageCallback: (newLang: string) => void,\n\t\tsetDialogErrorMessageCallback: (message: ErrorMessageType) => void,\n\t\tsetIsErrorDialogOpen: (val: boolean) => void,\n\t) {\n\t\tthis.hostAllowedToConnectCallback = hostAllowedToConnectCallback;\n\t\tthis.setMyDeviceDetails = setMyDeviceDetails;\n\t\tthis.setScreenSharingSourceTypeCallback =\n\t\t\tsetScreenSharingSourceTypeCallback;\n\t\tthis.setAppLanguageCallback = setAppLanguageCallback;\n\t\tthis.setDialogErrorMessageCallback = setDialogErrorMessageCallback;\n\t\tthis.setIsErrorDialogOpen = setIsErrorDialogOpen;\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts",
    "content": "interface ReceiveEncryptedMessagePayload {\n\tfromSocketID: string;\n\ttype: string;\n\tpayload: Record<string, unknown>;\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/ScreenSharingSourceEnum.ts",
    "content": "export const ScreenSharingSource = {\n\tWINDOW: 'window',\n\tSCREEN: 'screen',\n} as const;\n\nexport type ScreenSharingSourceType =\n\t(typeof ScreenSharingSource)[keyof typeof ScreenSharingSource];\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts",
    "content": "export default class PeerConnectionPartnerIsNotDefinedError extends Error {\n\tconstructor() {\n\t\tsuper('partner should be defined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(\n\t\t\tthis,\n\t\t\tPeerConnectionPartnerIsNotDefinedError.prototype,\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts",
    "content": "export default class PeerConnectionPeerIsNullError extends Error {\n\tconstructor() {\n\t\tsuper('peer of PeerConnection should not be null!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, PeerConnectionPeerIsNullError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts",
    "content": "export default class PeerConnectionSocketNotDefined extends Error {\n\tconstructor() {\n\t\tsuper('socket should be defined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, PeerConnectionSocketNotDefined.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts",
    "content": "export default class PeerConnectionUserIsNotDefinedError extends Error {\n\tconstructor() {\n\t\tsuper('user should be defined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, PeerConnectionUserIsNotDefinedError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/index.ts",
    "content": "import shortId from 'shortid';\nimport SimplePeer from 'simple-peer';\nimport { UAParser } from 'ua-parser-js';\nimport type { Socket } from 'socket.io-client';\nimport type { LocalPeerUser } from '../../../../common/LocalPeerUser';\nimport type { SendEncryptedMessagePayload } from '../../../../common/SendEncryptedMessagePayload';\nimport { connect as connectSocket } from '../../utils/socket';\nimport {\n\tprepare as prepareMessage,\n\ttype ProcessedPayload,\n} from '../../utils/message';\nimport setSdpMediaBitrate from './setSdpMediaBitrate';\nimport VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';\nimport {\n\tVideoQuality,\n\ttype VideoQualityType,\n} from '../VideoAutoQualityOptimizer/VideoQualityEnum';\nimport { prepareDataMessageToChangeQuality } from './simplePeerDataMessages';\nimport { VIDEO_QUALITY_TO_DECIMAL } from './../../constants/appConstants';\nimport { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';\nimport peerConnectionHandleSocket from './peerConnectionHandleSocket';\nimport peerConnectionHandlePeer from './peerConnectionHandlePeer';\nimport peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage';\nimport startSocketConnectedCheckingLoop from './startSocketConnectedCheckingLoop';\nimport NullUser from './NullUser';\nimport PeerConnectionUIHandler from './PeerConnectionUIHandler';\nimport setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';\nimport PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';\nimport PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';\nimport PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError';\n\nexport default class PeerConnection {\n\troomId: string;\n\n\tsocket: Socket | null = null;\n\n\tuser: LocalPeerUser = NullUser;\n\n\tpartner: PartnerPeerUser = NullUser;\n\n\tpeer: null | SimplePeer.Instance = null;\n\n\tmyDeviceDetails: DeviceDetails = {\n\t\tmyIP: '',\n\t\tmyOS: '',\n\t\tmyDeviceType: '',\n\t\tmyBrowser: '',\n\t\tmyRoomId: '',\n\t};\n\n\tsetUrlCallback: (url: MediaStream | null) => void;\n\n\tuaParser: UAParser;\n\n\tscreenSharingSourceType: string | undefined = undefined;\n\n\tvideoQuality: VideoQualityType = VideoQuality.Q_100_PERCENT;\n\n\tvideoAutoQualityOptimizer: VideoAutoQualityOptimizer;\n\n\tisStreamStarted: boolean = false;\n\n\tUIHandler: PeerConnectionUIHandler;\n\n\tbeforeunloadHandler: (() => void) | null = null;\n\n\tconnectionCheckInterval: NodeJS.Timeout | null = null;\n\n\treconnectTimeout: NodeJS.Timeout | null = null;\n\n\tgetMyIPTimeout: NodeJS.Timeout | null = null;\n\n\tsetMyDeviceDetailsTimeout: NodeJS.Timeout | null = null;\n\n\tconstructor(\n\t\troomId: string,\n\t\tsetUrlCallback: (url: MediaStream | null) => void,\n\t\tvideoAutoQualityOptimizer: VideoAutoQualityOptimizer,\n\t\tUIHandler: PeerConnectionUIHandler,\n\t) {\n\t\tthis.setUrlCallback = setUrlCallback;\n\t\tthis.videoAutoQualityOptimizer = videoAutoQualityOptimizer;\n\t\tthis.UIHandler = UIHandler;\n\t\tthis.roomId = roomId;\n\t\tthis.socket = connectSocket(this.roomId);\n\t\tthis.uaParser = new UAParser();\n\t\tthis.createUserAndInitSocket();\n\t\tthis.createPeer();\n\n\t\tif (!this.roomId || this.roomId === '') {\n\t\t\tsetAndShowErrorDialogMessage(this, ErrorMessage.NOT_ALLOWED);\n\t\t}\n\n\t\tthis.connectionCheckInterval = startSocketConnectedCheckingLoop(this);\n\t}\n\n\tsetVideoQuality(videoQuality: VideoQualityType) {\n\t\tthis.videoQuality = videoQuality;\n\t\tthis.videoQualityChangedCallback();\n\t}\n\n\tvideoQualityChangedCallback() {\n\t\tif (!this.peer) return;\n\t\tif (this.videoQuality === VideoQuality.Q_AUTO) {\n\t\t\tthis.peer.send(prepareDataMessageToChangeQuality(1));\n\t\t} else {\n\t\t\tthis.peer.send(\n\t\t\t\tprepareDataMessageToChangeQuality(\n\t\t\t\t\tVIDEO_QUALITY_TO_DECIMAL[this.videoQuality],\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tstopStream() {\n\t\t// stop the video stream by clearing the stream URL\n\t\tthis.setUrlCallback(null);\n\t\tthis.isStreamStarted = false;\n\n\t\t// destroy the peer connection\n\t\tif (this.peer) {\n\t\t\ttry {\n\t\t\t\tthis.peer.removeAllListeners();\n\t\t\t\tthis.peer.destroy();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Error destroying peer connection:', error);\n\t\t\t}\n\t\t\tthis.peer = null;\n\t\t}\n\t}\n\n\tdestroy() {\n\t\t// remove window event listener\n\t\tif (this.beforeunloadHandler) {\n\t\t\twindow.removeEventListener('beforeunload', this.beforeunloadHandler);\n\t\t\tthis.beforeunloadHandler = null;\n\t\t}\n\n\t\t// clear connection check interval\n\t\tif (this.connectionCheckInterval) {\n\t\t\tclearInterval(this.connectionCheckInterval);\n\t\t\tthis.connectionCheckInterval = null;\n\t\t}\n\n\t\t// clear all timeouts\n\t\tif (this.reconnectTimeout) {\n\t\t\tclearTimeout(this.reconnectTimeout);\n\t\t\tthis.reconnectTimeout = null;\n\t\t}\n\t\tif (this.getMyIPTimeout) {\n\t\t\tclearTimeout(this.getMyIPTimeout);\n\t\t\tthis.getMyIPTimeout = null;\n\t\t}\n\t\tif (this.setMyDeviceDetailsTimeout) {\n\t\t\tclearTimeout(this.setMyDeviceDetailsTimeout);\n\t\t\tthis.setMyDeviceDetailsTimeout = null;\n\t\t}\n\n\t\t// stop stream if started\n\t\tif (this.isStreamStarted) {\n\t\t\tthis.stopStream();\n\t\t}\n\n\t\t// cleanup peer connection\n\t\tif (this.peer) {\n\t\t\ttry {\n\t\t\t\tthis.peer.removeAllListeners();\n\t\t\t\tthis.peer.destroy();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Error destroying peer:', error);\n\t\t\t}\n\t\t\tthis.peer = null;\n\t\t}\n\n\t\t// cleanup socket\n\t\tif (this.socket) {\n\t\t\tthis.socket.removeAllListeners();\n\t\t\tthis.socket.disconnect();\n\t\t}\n\t}\n\n\tcreatePeer() {\n\t\t// cleanup existing peer before creating new one\n\t\tif (this.peer) {\n\t\t\ttry {\n\t\t\t\tthis.peer.removeAllListeners();\n\t\t\t\tthis.peer.destroy();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Error cleaning up existing peer:', error);\n\t\t\t}\n\t\t\tthis.peer = null;\n\t\t}\n\n\t\t// When we are testing with jest, SimplePeer() can not be created, so we just return\n\t\tconst peer = new SimplePeer({\n\t\t\tinitiator: false,\n\t\t\tconfig: { iceServers: [] },\n\t\t\tsdpTransform: (sdp) => {\n\t\t\t\tlet newSDP = sdp;\n\t\t\t\tnewSDP = setSdpMediaBitrate(\n\t\t\t\t\tnewSDP as unknown as string,\n\t\t\t\t\t'video',\n\t\t\t\t\t500000,\n\t\t\t\t) as unknown as typeof sdp;\n\t\t\t\treturn newSDP;\n\t\t\t},\n\t\t});\n\n\t\tthis.peer = peer;\n\t\tthis.peer.on('error', (e) => {\n\t\t\tconsole.error('error in simple peer happened!');\n\t\t\tconsole.error(e);\n\t\t\tsetAndShowErrorDialogMessage(this, ErrorMessage.WEBRTC_ERROR);\n\t\t});\n\t\tpeerConnectionHandlePeer(this);\n\t}\n\n\tinitApp(user: LocalPeerUser, myIP: string) {\n\t\tif (!this.socket) {\n\t\t\tthrow new PeerConnectionSocketNotDefined();\n\t\t}\n\t\tthis.socket.emit('USER_ENTER', {\n\t\t\tusername: user.username,\n\t\t\tip: myIP, // TODO: remove as it is not used\n\t\t});\n\t}\n\n\tcreateUser() {\n\t\treturn new Promise<LocalPeerUser>((resolve) => {\n\t\t\tconst username = shortId.generate();\n\t\t\tconst id = shortId.generate();\n\n\t\t\tresolve({\n\t\t\t\tusername,\n\t\t\t\tid,\n\t\t\t});\n\t\t});\n\t}\n\n\tsendEncryptedMessage(payload: SendEncryptedMessagePayload) {\n\t\tconst socket = this.socket;\n\t\tif (!socket) {\n\t\t\tthrow new PeerConnectionSocketNotDefined();\n\t\t}\n\t\tif (!this.user || this.user === NullUser) {\n\t\t\tthrow new PeerConnectionUserIsNotDefinedError();\n\t\t}\n\t\tif (!this.partner || this.partner === NullUser) {\n\t\t\tthrow new PeerConnectionPartnerIsNotDefinedError();\n\t\t}\n\t\tif (!this.partner.username) return;\n\t\tprepareMessage(payload, this.user).then((msg: ProcessedPayload) => {\n\t\t\tsocket.emit('MESSAGE', msg.toSend);\n\t\t});\n\t}\n\n\treceiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) {\n\t\tpeerConnectionReceiveEncryptedMessage(this, payload);\n\t}\n\n\tcreateUserAndInitSocket() {\n\t\tconst socket = this.socket;\n\t\tif (!socket) {\n\t\t\tthrow new PeerConnectionSocketNotDefined();\n\t\t}\n\n\t\tsocket.removeAllListeners();\n\n\t\tconst userCreatedCallback = (createdUser: LocalPeerUser) => {\n\t\t\tthis.user = createdUser;\n\n\t\t\tpeerConnectionHandleSocket(this);\n\n\t\t\tthis.beforeunloadHandler = () => {\n\t\t\t\tsocket.emit('USER_DISCONNECT');\n\t\t\t};\n\t\t\twindow.addEventListener('beforeunload', this.beforeunloadHandler);\n\t\t};\n\n\t\tthis.createUser().then((newUser: LocalPeerUser) =>\n\t\t\tuserCreatedCallback(newUser),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts",
    "content": "export const INPUTtestWindowNavigatorUserAgent =\n\t'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';\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/mocks/INPUTvideo500000testSdpMediaBitrate.ts",
    "content": "export const INPUTtestSdpMediaBitrate = `\nv=0\no=- 5730467698688819135 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE 0 1\na=msid-semantic: WMS\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:PY+h\na=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc\na=ice-options:trickle\na=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\na=setup:active\na=mid:0\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:3 urn:3gpp:video-orientation\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\na=recvonly\na=rtcp-mux\na=rtcp-rsize\na=rtpmap:96 VP8/90000\na=rtcp-fb:96 goog-remb\na=rtcp-fb:96 transport-cc\na=rtcp-fb:96 ccm fir\na=rtcp-fb:96 nack\na=rtcp-fb:96 nack pli\na=rtpmap:97 rtx/90000\na=fmtp:97 apt=96\na=rtpmap:98 VP9/90000\na=rtcp-fb:98 goog-remb\na=rtcp-fb:98 transport-cc\na=rtcp-fb:98 ccm fir\na=rtcp-fb:98 nack\na=rtcp-fb:98 nack pli\na=fmtp:98 profile-id=0\na=rtpmap:99 rtx/90000\na=fmtp:99 apt=98\na=rtpmap:100 VP9/90000\na=rtcp-fb:100 goog-remb\na=rtcp-fb:100 transport-cc\na=rtcp-fb:100 ccm fir\na=rtcp-fb:100 nack\na=rtcp-fb:100 nack pli\na=fmtp:100 profile-id=2\na=rtpmap:101 rtx/90000\na=fmtp:101 apt=100\na=rtpmap:102 H264/90000\na=rtcp-fb:102 goog-remb\na=rtcp-fb:102 transport-cc\na=rtcp-fb:102 ccm fir\na=rtcp-fb:102 nack\na=rtcp-fb:102 nack pli\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\na=rtpmap:121 rtx/90000\na=fmtp:121 apt=102\na=rtpmap:127 H264/90000\na=rtcp-fb:127 goog-remb\na=rtcp-fb:127 transport-cc\na=rtcp-fb:127 ccm fir\na=rtcp-fb:127 nack\na=rtcp-fb:127 nack pli\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\na=rtpmap:120 rtx/90000\na=fmtp:120 apt=127\na=rtpmap:125 H264/90000\na=rtcp-fb:125 goog-remb\na=rtcp-fb:125 transport-cc\na=rtcp-fb:125 ccm fir\na=rtcp-fb:125 nack\na=rtcp-fb:125 nack pli\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\na=rtpmap:107 rtx/90000\na=fmtp:107 apt=125\na=rtpmap:108 H264/90000\na=rtcp-fb:108 goog-remb\na=rtcp-fb:108 transport-cc\na=rtcp-fb:108 ccm fir\na=rtcp-fb:108 nack\na=rtcp-fb:108 nack pli\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\na=rtpmap:109 rtx/90000\na=fmtp:109 apt=108\na=rtpmap:114 red/90000\na=rtpmap:115 rtx/90000\na=fmtp:115 apt=114\na=rtpmap:116 ulpfec/90000\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\nc=IN IP4 0.0.0.0\nb=AS:30\na=ice-ufrag:PY+h\na=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc\na=ice-options:trickle\na=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\na=setup:active\na=mid:1\na=sctp-port:5000\na=max-message-size:262144\n`;\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts",
    "content": "export const OUTPUTDeviceDetailsFromUAParsed: DeviceDetails = {\n\tmyBrowser: 'Chrome 87.0.4280.88',\n\tmyDeviceType: 'computer',\n\tmyIP: '123.123.123.123',\n\tmyOS: 'Mac OS 10.15.6',\n\tmyRoomId: 'asdf2314',\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/mocks/OUTPUTvideo500000testSdpMediaBitrate.ts",
    "content": "export const OUTPUTtestSdpMediaBitrate = `\nv=0\no=- 5730467698688819135 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE 0 1\na=msid-semantic: WMS\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116\nc=IN IP4 0.0.0.0\nb=AS:500000\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:PY+h\na=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc\na=ice-options:trickle\na=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\na=setup:active\na=mid:0\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:3 urn:3gpp:video-orientation\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\na=recvonly\na=rtcp-mux\na=rtcp-rsize\na=rtpmap:96 VP8/90000\na=rtcp-fb:96 goog-remb\na=rtcp-fb:96 transport-cc\na=rtcp-fb:96 ccm fir\na=rtcp-fb:96 nack\na=rtcp-fb:96 nack pli\na=rtpmap:97 rtx/90000\na=fmtp:97 apt=96\na=rtpmap:98 VP9/90000\na=rtcp-fb:98 goog-remb\na=rtcp-fb:98 transport-cc\na=rtcp-fb:98 ccm fir\na=rtcp-fb:98 nack\na=rtcp-fb:98 nack pli\na=fmtp:98 profile-id=0\na=rtpmap:99 rtx/90000\na=fmtp:99 apt=98\na=rtpmap:100 VP9/90000\na=rtcp-fb:100 goog-remb\na=rtcp-fb:100 transport-cc\na=rtcp-fb:100 ccm fir\na=rtcp-fb:100 nack\na=rtcp-fb:100 nack pli\na=fmtp:100 profile-id=2\na=rtpmap:101 rtx/90000\na=fmtp:101 apt=100\na=rtpmap:102 H264/90000\na=rtcp-fb:102 goog-remb\na=rtcp-fb:102 transport-cc\na=rtcp-fb:102 ccm fir\na=rtcp-fb:102 nack\na=rtcp-fb:102 nack pli\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\na=rtpmap:121 rtx/90000\na=fmtp:121 apt=102\na=rtpmap:127 H264/90000\na=rtcp-fb:127 goog-remb\na=rtcp-fb:127 transport-cc\na=rtcp-fb:127 ccm fir\na=rtcp-fb:127 nack\na=rtcp-fb:127 nack pli\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\na=rtpmap:120 rtx/90000\na=fmtp:120 apt=127\na=rtpmap:125 H264/90000\na=rtcp-fb:125 goog-remb\na=rtcp-fb:125 transport-cc\na=rtcp-fb:125 ccm fir\na=rtcp-fb:125 nack\na=rtcp-fb:125 nack pli\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\na=rtpmap:107 rtx/90000\na=fmtp:107 apt=125\na=rtpmap:108 H264/90000\na=rtcp-fb:108 goog-remb\na=rtcp-fb:108 transport-cc\na=rtcp-fb:108 ccm fir\na=rtcp-fb:108 nack\na=rtcp-fb:108 nack pli\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\na=rtpmap:109 rtx/90000\na=fmtp:109 apt=108\na=rtpmap:114 red/90000\na=rtpmap:115 rtx/90000\na=fmtp:115 apt=114\na=rtpmap:116 ulpfec/90000\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\nc=IN IP4 0.0.0.0\nb=AS:30\na=ice-ufrag:PY+h\na=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc\na=ice-options:trickle\na=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\na=setup:active\na=mid:1\na=sctp-port:5000\na=max-message-size:262144\n`;\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/peerConnectionHandlePeer.ts",
    "content": "import {\n\tprepareDataMessageToChangeQuality,\n\tprepareDataMessageToGetSharingSourceType,\n} from './simplePeerDataMessages';\nimport { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';\nimport { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';\nimport PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError';\nimport { ScreenSharingSource } from './ScreenSharingSourceEnum';\n\nexport function getSharingShourceType(peerConnection: PeerConnection) {\n\ttry {\n\t\tpeerConnection.peer?.send(prepareDataMessageToGetSharingSourceType());\n\t} catch (e) {\n\t\tconsole.log(e);\n\t}\n}\n\nexport default (peerConnection: PeerConnection) => {\n\tif (peerConnection.peer === null) {\n\t\tthrow new PeerConnectionPeerIsNullError();\n\t}\n\tpeerConnection.peer.on('stream', (stream) => {\n\t\tpeerConnection.setUrlCallback(stream);\n\n\t\tsetTimeout(() => {\n\t\t\tpeerConnection.videoAutoQualityOptimizer.setGoodQualityCallback(() => {\n\t\t\t\tif (peerConnection.videoQuality === VideoQuality.Q_AUTO) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpeerConnection.peer?.send(prepareDataMessageToChangeQuality(1));\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tconsole.log(e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tpeerConnection.videoAutoQualityOptimizer.setHalfQualityCallbak(() => {\n\t\t\t\tif (peerConnection.videoQuality === VideoQuality.Q_AUTO) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpeerConnection.peer?.send(prepareDataMessageToChangeQuality(0.5));\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tconsole.log(e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}, 1000);\n\n\t\tpeerConnection.videoAutoQualityOptimizer.startOptimizationLoop();\n\n\t\tsetTimeout(getSharingShourceType, 1000, peerConnection);\n\n\t\tpeerConnection.isStreamStarted = true;\n\n\t\t// if any transient error dialog was shown earlier, close it now\n\t\ttry {\n\t\t\tpeerConnection.UIHandler.setIsErrorDialogOpen(false);\n\t\t\tpeerConnection.UIHandler.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;\n\t\t} catch (_) {\n\t\t\t// ignore\n\t\t}\n\t});\n\n\tpeerConnection.peer.on('signal', (data) => {\n\t\t// fired when webrtc done preparation to start call on peerConnection machine\n\t\tpeerConnection.sendEncryptedMessage({\n\t\t\ttype: 'CALL_ACCEPTED',\n\t\t\tpayload: {\n\t\t\t\tsignalData: data,\n\t\t\t},\n\t\t});\n\t});\n\n\tpeerConnection.peer.on('data', (data) => {\n\t\tconst dataJSON = JSON.parse(data);\n\n\t\tif (dataJSON.type === 'screen_sharing_source_type') {\n\t\t\tpeerConnection.screenSharingSourceType = dataJSON.payload.value;\n\t\t\tif (\n\t\t\t\tpeerConnection.screenSharingSourceType === ScreenSharingSource.SCREEN ||\n\t\t\t\tpeerConnection.screenSharingSourceType === ScreenSharingSource.WINDOW\n\t\t\t) {\n\t\t\t\tpeerConnection.UIHandler.setScreenSharingSourceTypeCallback(\n\t\t\t\t\tpeerConnection.screenSharingSourceType,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/peerConnectionHandleSocket.ts",
    "content": "import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';\nimport {\n\tgetBrowserFromUAParser,\n\tgetDeviceTypeFromUAParser,\n\tgetOSFromUAParser,\n} from '../../utils/userAgentParserHelpers';\nimport PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';\nimport setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';\n\nexport function getMyIPCallback(\n\tpeerConnection: PeerConnection,\n\tip: string,\n\tuserAgent: string,\n) {\n\tpeerConnection.myDeviceDetails.myIP = ip;\n\n\tpeerConnection.uaParser.setUA(userAgent);\n\tpeerConnection.myDeviceDetails.myOS = getOSFromUAParser(\n\t\tpeerConnection.uaParser,\n\t);\n\tpeerConnection.myDeviceDetails.myDeviceType = getDeviceTypeFromUAParser(\n\t\tpeerConnection.uaParser,\n\t);\n\tpeerConnection.myDeviceDetails.myBrowser = getBrowserFromUAParser(\n\t\tpeerConnection.uaParser,\n\t);\n\n\tpeerConnection.initApp(peerConnection.user, ip);\n}\n\nexport default (peerConnection: PeerConnection) => {\n\tlet disconnectCount = 0;\n\tlet isAllowed = true;\n\tconst socket = peerConnection.socket;\n\tif (!socket) {\n\t\tthrow new PeerConnectionSocketNotDefined();\n\t}\n\n\tsocket.on('disconnect', () => {\n\t\tdisconnectCount++;\n\t\t// handle disconnect even when stream is started - stop stream and show error\n\t\tif (peerConnection.isStreamStarted && disconnectCount >= 1) {\n\t\t\tpeerConnection.stopStream();\n\t\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);\n\t\t\treturn;\n\t\t}\n\t\t// for pre-stream disconnects, wait for sustained disconnection before showing error\n\t\tif (disconnectCount > 6 && isAllowed) {\n\t\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);\n\t\t}\n\t});\n\n\tsocket.on('connect', () => {\n\t\tlet ipCallbackReceived = false;\n\n\t\t// clear any existing reconnect timeout\n\t\tif (peerConnection.reconnectTimeout) {\n\t\t\tclearTimeout(peerConnection.reconnectTimeout);\n\t\t}\n\n\t\tpeerConnection.reconnectTimeout = setTimeout(() => {\n\t\t\tif (!ipCallbackReceived && isAllowed) {\n\t\t\t\tconsole.log('GET_MY_IP callback not received, reconnecting socket');\n\t\t\t\tsocket.disconnect();\n\t\t\t\tsocket.connect();\n\t\t\t}\n\t\t}, 2500); // 2 seconds timeout to wait for callback\n\n\t\t// clear any existing getMyIP timeout\n\t\tif (peerConnection.getMyIPTimeout) {\n\t\t\tclearTimeout(peerConnection.getMyIPTimeout);\n\t\t}\n\n\t\tpeerConnection.getMyIPTimeout = setTimeout(() => {\n\t\t\tif (!isAllowed) return;\n\t\t\tsocket.emit('GET_MY_IP', (ip: string) => {\n\t\t\t\tconsole.log('GET_MY_IP', ip);\n\t\t\t\tipCallbackReceived = true;\n\t\t\t\tif (peerConnection.reconnectTimeout) {\n\t\t\t\t\tclearTimeout(peerConnection.reconnectTimeout);\n\t\t\t\t\tpeerConnection.reconnectTimeout = null;\n\t\t\t\t}\n\t\t\t\tgetMyIPCallback(peerConnection, ip, window.navigator.userAgent);\n\t\t\t});\n\t\t}, 500);\n\t});\n\n\tsocket.on('NOT_ALLOWED', () => {\n\t\tisAllowed = false;\n\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.NOT_ALLOWED);\n\t});\n\n\tsocket.on('USER_ENTER', (payload: { users: PartnerPeerUser[] }) => {\n\t\tif (!isAllowed) return;\n\t\tconst filteredPartner = payload.users.filter((v) => {\n\t\t\treturn peerConnection.user.username !== v.username;\n\t\t});\n\n\t\tpeerConnection.partner = filteredPartner[0];\n\n\t\tif (!peerConnection.partner) return;\n\n\t\tpeerConnection.sendEncryptedMessage({\n\t\t\ttype: 'DEVICE_DETAILS',\n\t\t\t// TODO: add deviceIP in this payload\n\t\t\tpayload: {\n\t\t\t\tos: peerConnection.myDeviceDetails.myOS,\n\t\t\t\tdeviceType: peerConnection.myDeviceDetails.myDeviceType,\n\t\t\t\tbrowser: peerConnection.myDeviceDetails.myBrowser,\n\t\t\t\tdeviceScreenWidth: window.screen.width,\n\t\t\t\tdeviceScreenHeight: window.screen.height,\n\t\t\t},\n\t\t});\n\n\t\tpeerConnection.sendEncryptedMessage({\n\t\t\ttype: 'GET_APP_LANGUAGE',\n\t\t\tpayload: {},\n\t\t});\n\n\t\t// clear any existing timeout\n\t\tif (peerConnection.setMyDeviceDetailsTimeout) {\n\t\t\tclearTimeout(peerConnection.setMyDeviceDetailsTimeout);\n\t\t}\n\n\t\tpeerConnection.setMyDeviceDetailsTimeout = setTimeout(() => {\n\t\t\tpeerConnection.UIHandler.setMyDeviceDetails({\n\t\t\t\tmyIP: peerConnection.myDeviceDetails.myIP,\n\t\t\t\tmyOS: peerConnection.myDeviceDetails.myOS,\n\t\t\t\tmyBrowser: peerConnection.myDeviceDetails.myBrowser,\n\t\t\t\tmyDeviceType: peerConnection.myDeviceDetails.myDeviceType,\n\t\t\t\tmyRoomId: peerConnection.roomId,\n\t\t\t});\n\t\t}, 100);\n\t});\n\n\t// peerConnection.socket.on('USER_EXIT', (payload: any) => {\n\t//   // peerConnection.props.receiveUnencryptedMessage('USER_EXIT', payload);\n\t// });\n\n\tsocket.on('MESSAGE', (payload: ReceiveEncryptedMessagePayload) => {\n\t\tif (!isAllowed) return;\n\t\tpeerConnection.receiveEncryptedMessage(payload);\n\t});\n\n\tsocket.on('ROOM_LOCKED', () => {\n\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);\n\t});\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts",
    "content": "import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';\nimport { process as processMessage } from '../../utils/message';\nimport NullUser from './NullUser';\nimport PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';\nimport setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';\n\nexport default async (\n\tpeerConnection: PeerConnection,\n\tpayload: ReceiveEncryptedMessagePayload,\n) => {\n\tif (peerConnection.user === NullUser) {\n\t\tthrow new PeerConnectionUserIsNotDefinedError();\n\t}\n\tconst message = await processMessage(payload);\n\t// const message = payload as any;\n\tif (message.type === 'CALL_USER') {\n\t\tpeerConnection.peer?.signal(message.payload.signalData);\n\t}\n\tif (message.type === 'DENY_TO_CONNECT') {\n\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);\n\t}\n\tif (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') {\n\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);\n\t}\n\tif (message.type === 'ALLOWED_TO_CONNECT') {\n\t\tpeerConnection.UIHandler.hostAllowedToConnectCallback();\n\t}\n\tif (message.type === 'APP_LANGUAGE') {\n\t\tpeerConnection.UIHandler.setAppLanguageCallback(message.payload.value);\n\t}\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/setAndShowErrorDialogMessage.ts",
    "content": "import {\n\tErrorMessage,\n\ttype ErrorMessageType,\n} from '../../components/ErrorDialog/ErrorMessageEnum';\n\nexport default (\n\tpeerConnection: PeerConnection,\n\terrorMessage: ErrorMessageType,\n) => {\n\t// allow showing disconnect errors even when stream is started\n\tconst isDisconnectError = errorMessage === ErrorMessage.DISCONNECTED;\n\tif (peerConnection.isStreamStarted && !isDisconnectError) {\n\t\t// avoid flashing an error if the stream already started (except for disconnect errors)\n\t\treturn;\n\t}\n\tif (\n\t\tpeerConnection.UIHandler.errorDialogMessage ===\n\t\t\tErrorMessage.UNKNOWN_ERROR ||\n\t\tisDisconnectError\n\t) {\n\t\tpeerConnection.UIHandler.setDialogErrorMessageCallback(errorMessage);\n\t\tpeerConnection.UIHandler.setIsErrorDialogOpen(true);\n\t\tpeerConnection.UIHandler.errorDialogMessage = errorMessage;\n\t}\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/setSdpMediaBitrate.ts",
    "content": "export default (sdp: string, mediaType: string, bitrate: number) => {\n\tconst sdpLines = sdp.split('\\n');\n\tlet mediaLineIndex = -1;\n\tconst mediaLine = `m=${mediaType}`;\n\tlet bitrateLineIndex = -1;\n\tconst bitrateLine = `b=AS:${bitrate}`;\n\tmediaLineIndex = sdpLines.findIndex((line) => line.startsWith(mediaLine));\n\n\t// If we find a line matching “m={mediaType}”\n\tif (mediaLineIndex && mediaLineIndex < sdpLines.length) {\n\t\t// Skip the media line\n\t\tbitrateLineIndex = mediaLineIndex + 1;\n\n\t\t// Skip both i=* and c=* lines (bandwidths limiters have to come afterwards)\n\t\twhile (\n\t\t\tsdpLines[bitrateLineIndex].startsWith('i=') ||\n\t\t\tsdpLines[bitrateLineIndex].startsWith('c=')\n\t\t) {\n\t\t\tbitrateLineIndex += 1;\n\t\t}\n\n\t\tif (sdpLines[bitrateLineIndex].startsWith('b=')) {\n\t\t\t// If the next line is a b=* line, replace it with our new bandwidth\n\t\t\tsdpLines[bitrateLineIndex] = bitrateLine;\n\t\t} else {\n\t\t\t// Otherwise insert a new bitrate line.\n\t\t\tsdpLines.splice(bitrateLineIndex, 0, bitrateLine);\n\t\t}\n\t}\n\n\t// Then return the updated sdp content as a string\n\treturn sdpLines.join('\\n');\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/simplePeerDataMessages.ts",
    "content": "export function prepareDataMessageToChangeQuality(q: number) {\n\treturn `\n    {\n      \"type\": \"set_video_quality\",\n      \"payload\": {\n        \"value\": ${q}\n      }\n    }\n  `;\n}\n\nexport function prepareDataMessageToGetSharingSourceType() {\n\treturn `\n    {\n      \"type\": \"get_sharing_source_type\",\n      \"payload\": {\n      }\n    }\n  `;\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts",
    "content": "import PeerConnection from '..';\nimport { ErrorMessage } from '../../../components/ErrorDialog/ErrorMessageEnum';\nimport setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage';\n\nexport default (peerConnection: PeerConnection): NodeJS.Timeout => {\n\tlet disconnectedStreak = 0;\n\tlet pingTimeout: NodeJS.Timeout | null = null;\n\n\tconst checkConnection = () => {\n\t\tconst socket = peerConnection.socket;\n\t\tif (!socket) {\n\t\t\tdisconnectedStreak++;\n\t\t\thandleDisconnection();\n\t\t\treturn;\n\t\t}\n\t\tconst isSocketConnected = !!socket.connected;\n\n\t\tif (isSocketConnected) {\n\t\t\t// perform explicit ping/pong check to verify server is alive\n\t\t\ttry {\n\t\t\t\tif (pingTimeout) {\n\t\t\t\t\tclearTimeout(pingTimeout);\n\t\t\t\t}\n\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\t// ping timeout - server didn't respond\n\t\t\t\t\tdisconnectedStreak++;\n\t\t\t\t\thandleDisconnection();\n\t\t\t\t}, 3000);\n\n\t\t\t\tpingTimeout = timeout;\n\n\t\t\t\tsocket.emit('PING', (response: string) => {\n\t\t\t\t\tif (pingTimeout) {\n\t\t\t\t\t\tclearTimeout(pingTimeout);\n\t\t\t\t\t\tpingTimeout = null;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (response === 'PONG') {\n\t\t\t\t\t\tdisconnectedStreak = 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdisconnectedStreak++;\n\t\t\t\t\t\thandleDisconnection();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// socket error during ping\n\t\t\t\tdisconnectedStreak++;\n\t\t\t\thandleDisconnection();\n\t\t\t}\n\t\t} else {\n\t\t\t// socket is not connected\n\t\t\tdisconnectedStreak++;\n\t\t\thandleDisconnection();\n\t\t}\n\t};\n\n\tconst handleDisconnection = () => {\n\t\t// show error and stop stream after sustained disconnection\n\t\tif (disconnectedStreak >= 3) {\n\t\t\t// stop the video stream\n\t\t\tif (peerConnection.isStreamStarted) {\n\t\t\t\tpeerConnection.stopStream();\n\t\t\t}\n\t\t\t// show error dialog (now allows showing even when stream was started)\n\t\t\tsetAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);\n\t\t}\n\t};\n\n\treturn setInterval(checkConnection, 5000);\n};\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/VideoQualityEnum.ts",
    "content": "export const VideoQuality = {\n\tQ_AUTO: 'Auto',\n\tQ_25_PERCENT: '25%',\n\tQ_40_PERCENT: '40%',\n\tQ_60_PERCENT: '60%',\n\tQ_80_PERCENT: '80%',\n\tQ_100_PERCENT: '100%',\n} as const;\n\nexport type VideoQualityType = (typeof VideoQuality)[keyof typeof VideoQuality];\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts",
    "content": "export default class CanvasNotDefinedError extends Error {\n\tconstructor() {\n\t\tsuper('internal variable of canvas DOM element should be defined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, CanvasNotDefinedError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts",
    "content": "export default class ImageDataIsUndefinedError extends Error {\n\tconstructor() {\n\t\tsuper('imageData retrieved is undefined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, ImageDataIsUndefinedError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts",
    "content": "export default class VideoDimensionsAreWrongError extends Error {\n\tconstructor() {\n\t\tsuper('video dimensions are wrong, neither width nor height can be zero!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, VideoDimensionsAreWrongError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts",
    "content": "export default class VideoNotDefinedError extends Error {\n\tconstructor() {\n\t\tsuper('internal variable of video DOM element should be defined!');\n\t\t// Set the prototype explicitly.\n\t\tObject.setPrototypeOf(this, VideoNotDefinedError.prototype);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/features/VideoAutoQualityOptimizer/index.ts",
    "content": "import { COMPARISON_CANVAS_ID } from './../../constants/appConstants';\nimport pixelmatch from 'pixelmatch';\nimport { PLAYER_WRAPPER_ID } from '../../constants/appConstants';\nimport CanvasNotDefinedError from './errors/CanvasNotDefinedError';\nimport VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError';\nimport VideoNotDefinedError from './errors/VideoNotDefinedError';\nimport ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError';\n\nexport const CANVAS_SCALE_MULTIPLIER = 0.125; // 1/8 of original canvas size, to speed up calculations\nexport const MISMATCH_PERCENT_THRESHOLD = 0.1;\n\nexport default class VideoAutoQualityOptimizer {\n\tvideo: undefined | HTMLVideoElement;\n\n\tcanvas: undefined | HTMLCanvasElement;\n\n\tprevFrame: undefined | ImageData;\n\n\tlargeMismatchFramesCount = 0;\n\n\tisRequestedHalfQuality = false;\n\n\tgoodQualityCallback = () => {\n\t\t// noop until callbacks are provided\n\t};\n\n\thalfQualityCallbak = () => {\n\t\t// noop until callbacks are provided\n\t};\n\n\tsetGoodQualityCallback(callback: () => void) {\n\t\tthis.goodQualityCallback = callback;\n\t}\n\n\tsetHalfQualityCallbak(callback: () => void) {\n\t\tthis.halfQualityCallbak = callback;\n\t}\n\n\tstartOptimizationLoop() {\n\t\tthis.prepareCanvasAndVideo();\n\t\tsetInterval(() => {\n\t\t\ttry {\n\t\t\t\tthis.doFrameComparisonAndQualityOptimization();\n\t\t\t} catch (e) {\n\t\t\t\t// some errors may be thrown here, better ignore them in production\n\t\t\t\tif (process.env.NODE_ENV === 'development') {\n\t\t\t\t\tconsole.error(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}, 1000);\n\t}\n\n\tdoFrameComparisonAndQualityOptimization() {\n\t\tthis.validateBeforeCalculations();\n\t\tthis.clearCanvas();\n\t\tthis.scaleCanvas();\n\t\tthis.drawVideoFrameToCanvas();\n\t\tconst imageData = this.getImageDataFromCanvas();\n\n\t\tif (!imageData) {\n\t\t\tthrow new ImageDataIsUndefinedError();\n\t\t}\n\t\tif (!this.prevFrame) {\n\t\t\tthis.prevFrame = imageData;\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst mismatchInPercent =\n\t\t\t\tthis.getPreviousAndCurrentFrameMismatchInPercent(imageData);\n\t\t\tthis.handleFramesMismatch(mismatchInPercent);\n\t\t} catch (_e) {\n\t\t\t// usually frames size mismatch thrown here, so can be ignored as it happens\n\t\t\t// often when changing sharing window size\n\t\t\t// so logging this error may be not necessary\n\t\t}\n\n\t\tthis.prevFrame = imageData;\n\t}\n\n\tfindAndSetVideoInternalVariable(document: Document) {\n\t\tthis.video = document.querySelector(\n\t\t\t`#${PLAYER_WRAPPER_ID} > video`,\n\t\t) as HTMLVideoElement;\n\t}\n\n\tfindAndSetCanvasInternalVariable(document: Document) {\n\t\tthis.canvas = document.querySelector(\n\t\t\t`#${COMPARISON_CANVAS_ID}`,\n\t\t) as HTMLCanvasElement;\n\t}\n\n\tprepareCanvasAndVideo() {\n\t\tsetTimeout(() => {\n\t\t\tthis.findAndSetVideoInternalVariable(document);\n\t\t\tthis.findAndSetCanvasInternalVariable(document);\n\t\t}, 1000);\n\t}\n\n\tclearCanvas() {\n\t\tthis.canvas\n\t\t\t?.getContext('2d')\n\t\t\t?.clearRect(0, 0, this.canvas.width, this.canvas.height);\n\t}\n\n\tvalidateVideoWidthAndHeight() {\n\t\tif (this.video?.videoWidth === 0 || this.video?.videoHeight === 0) {\n\t\t\tthrow new VideoDimensionsAreWrongError();\n\t\t}\n\t}\n\n\tvalidateBeforeCalculations() {\n\t\tthis.validateVideoWidthAndHeight();\n\t\tthis.validateVideoIsDefined();\n\t\tthis.validateCanvasIsDefined();\n\t}\n\n\tvalidateVideoIsDefined() {\n\t\tif (!this.video) {\n\t\t\tthrow new VideoNotDefinedError();\n\t\t}\n\t}\n\n\tvalidateCanvasIsDefined() {\n\t\tif (!this.canvas) {\n\t\t\tthrow new CanvasNotDefinedError();\n\t\t}\n\t}\n\n\tscaleCanvas() {\n\t\tif (!this.canvas || !this.video) return;\n\t\tthis.canvas.width = this.video.videoWidth * CANVAS_SCALE_MULTIPLIER;\n\t\tthis.canvas.height = this.video.videoHeight * CANVAS_SCALE_MULTIPLIER;\n\t}\n\n\tdrawVideoFrameToCanvas() {\n\t\tif (!this.video) return;\n\t\tthis.canvas\n\t\t\t?.getContext('2d')\n\t\t\t?.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);\n\t}\n\n\tgetImageDataFromCanvas() {\n\t\treturn this.canvas\n\t\t\t?.getContext('2d')\n\t\t\t?.getImageData(0, 0, this.canvas.width, this.canvas.height);\n\t}\n\n\tgetNumberOfMismatchedPixels(imageData: ImageData) {\n\t\tif (!this.canvas || !this.canvas.width || !this.prevFrame) return 0;\n\t\treturn pixelmatch(\n\t\t\tthis.prevFrame.data,\n\t\t\timageData.data,\n\t\t\tundefined,\n\t\t\tthis.canvas.width,\n\t\t\tthis.canvas.height,\n\t\t\t{ threshold: 0.1 },\n\t\t);\n\t}\n\n\tgetPreviousAndCurrentFrameMismatchInPercent(imageData: ImageData) {\n\t\tif (!this.canvas) return 0;\n\t\treturn (\n\t\t\tthis.getNumberOfMismatchedPixels(imageData) /\n\t\t\t(this.canvas.width * this.canvas.height)\n\t\t);\n\t}\n\n\thandleFramesMismatch(mismatchInPercent: number) {\n\t\tif (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) {\n\t\t\tthis.largeMismatchFramesCount -= 1;\n\t\t} else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) {\n\t\t\tthis.largeMismatchFramesCount = 0;\n\t\t\tthis.isRequestedHalfQuality = false;\n\t\t\tthis.goodQualityCallback();\n\t\t} else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) {\n\t\t\tif (this.largeMismatchFramesCount < 3) {\n\t\t\t\tthis.largeMismatchFramesCount += 1;\n\t\t\t} else {\n\t\t\t\tthis.halfQualityCallbak();\n\t\t\t\tthis.isRequestedHalfQuality = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tisLowMismatchPercent(mismatchInPercent: number) {\n\t\treturn mismatchInPercent < MISMATCH_PERCENT_THRESHOLD;\n\t}\n\n\tisHighMismatchPercent(mismatchInPercent: number) {\n\t\treturn mismatchInPercent >= MISMATCH_PERCENT_THRESHOLD;\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/index.css",
    "content": "@import \"normalize.css\";\n@import \"@blueprintjs/core/lib/css/blueprint.css\";\n/*@import \"@blueprintjs/icons/lib/css/blueprint-icons.css\";*/\n\nbody {\n\tmargin: 0;\n\tfont-family:\n\t\t-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n\t\t\"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n\t-webkit-font-smoothing: antialiased;\n\t-moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n\tfont-family:\n\t\tsource-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\n/*:root {*/\n/*  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;*/\n/*  line-height: 1.5;*/\n/*  font-weight: 400;*/\n\n/*  color-scheme: light dark;*/\n/*  color: rgba(255, 255, 255, 0.87);*/\n/*  background-color: #242424;*/\n\n/*  font-synthesis: none;*/\n/*  text-rendering: optimizeLegibility;*/\n/*  -webkit-font-smoothing: antialiased;*/\n/*  -moz-osx-font-smoothing: grayscale;*/\n/*}*/\n\n/*a {*/\n/*  font-weight: 500;*/\n/*  color: #646cff;*/\n/*  text-decoration: inherit;*/\n/*}*/\n/*a:hover {*/\n/*  color: #535bf2;*/\n/*}*/\n\n/*body {*/\n/*  margin: 0;*/\n/*  display: flex;*/\n/*  place-items: center;*/\n/*  min-width: 320px;*/\n/*  min-height: 100vh;*/\n/*}*/\n\n/*h1 {*/\n/*  font-size: 3.2em;*/\n/*  line-height: 1.1;*/\n/*}*/\n\n/*button {*/\n/*  border-radius: 8px;*/\n/*  border: 1px solid transparent;*/\n/*  padding: 0.6em 1.2em;*/\n/*  font-size: 1em;*/\n/*  font-weight: 500;*/\n/*  font-family: inherit;*/\n/*  background-color: #1a1a1a;*/\n/*  cursor: pointer;*/\n/*  transition: border-color 0.25s;*/\n/*}*/\n/*button:hover {*/\n/*  border-color: #646cff;*/\n/*}*/\n/*button:focus,*/\n/*button:focus-visible {*/\n/*  outline: 4px auto -webkit-focus-ring-color;*/\n/*}*/\n\n/*@media (prefers-color-scheme: light) {*/\n/*  :root {*/\n/*    color: #213547;*/\n/*    background-color: #ffffff;*/\n/*  }*/\n/*  a:hover {*/\n/*    color: #747bff;*/\n/*  }*/\n/*  button {*/\n/*    background-color: #f9f9f9;*/\n/*  }*/\n/*}*/\n\n.rounded-pill-button {\n\tborder-radius: 9999px;\n}\n"
  },
  {
    "path": "src/client-viewer/src/main.tsx",
    "content": "import { initializeGARequestInterceptor } from './utils/gaRequestInterceptor';\n\n// initialize GA request interceptor immediately to block requests before consent\ninitializeGARequestInterceptor();\n\nimport { StrictMode, Suspense } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './index.css';\nimport './config/i18n';\nimport App from './App.tsx';\nimport { AppContextProvider } from './providers/AppContextProvider';\nimport LoadingScreen from './components/LoadingScreen';\n\ncreateRoot(document.getElementById('root')!).render(\n\t<StrictMode>\n\t\t<Suspense fallback={<LoadingScreen />}>\n\t\t\t<AppContextProvider>\n\t\t\t\t<App />\n\t\t\t</AppContextProvider>\n\t\t</Suspense>\n\t</StrictMode>,\n);\n"
  },
  {
    "path": "src/client-viewer/src/providers/AppContextProvider/index.tsx",
    "content": "import React, { useState } from 'react';\n\ninterface AppContextInterface {\n\tappLanguage: string;\n\tsetAppLanguageHook: (val: string) => void;\n}\n\nconst defaultAppContextValue = {\n\tappLanguage: 'en',\n\tsetAppLanguageHook: () => {\n\t\t// noop default\n\t},\n};\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport const AppContext = React.createContext<AppContextInterface>(\n\tdefaultAppContextValue,\n);\n\nexport const AppContextProvider: React.FC<{ children: React.ReactNode }> = ({\n\tchildren,\n}) => {\n\tconst [appLanguage, setAppLanguage] = useState('en');\n\n\tconst setAppLanguageHook = (newLang: string) => {\n\t\tsetAppLanguage(newLang);\n\t};\n\n\tconst value = {\n\t\tappLanguage,\n\t\tsetAppLanguageHook,\n\t};\n\n\treturn <AppContext.Provider value={value}>{children}</AppContext.Provider>;\n};\n"
  },
  {
    "path": "src/client-viewer/src/utils/ProcessedMessage.d.ts",
    "content": "type CallUserMessageWithPayload = {\n\ttype: 'CALL_USER';\n\tpayload: {\n\t\tsignalData: string;\n\t};\n};\n\ntype DeviceDetailsMessageWithPayload = {\n\ttype: 'DEVICE_DETAILS';\n\tpayload: {\n\t\tsocketID: string;\n\t\tdeviceType: { type: string };\n\t\tos: { name: string; version: string };\n\t\tbrowser: { name: string; version: string; major: string };\n\t\tdeviceScreenWidth: number;\n\t\tdeviceScreenHeight: number;\n\t};\n};\n\ntype DenyToConnectMessageWithPayload = {\n\ttype: 'DENY_TO_CONNECT';\n\tpayload: {};\n};\n\ntype DisconnectByHostMachineUserMessageWithPayload = {\n\ttype: 'DISCONNECT_BY_HOST_MACHINE_USER';\n\tpayload: {};\n};\n\ntype AllowedToConnectMessageWithPayload = {\n\ttype: 'ALLOWED_TO_CONNECT';\n\tpayload: {};\n};\n\ntype AppLanguageMessageWithPayload = {\n\ttype: 'APP_LANGUAGE';\n\tpayload: {\n\t\tvalue: string;\n\t};\n};\n\ntype ProcessedMessage =\n\t| CallUserMessageWithPayload\n\t| DeviceDetailsMessageWithPayload\n\t| DenyToConnectMessageWithPayload\n\t| DisconnectByHostMachineUserMessageWithPayload\n\t| AllowedToConnectMessageWithPayload\n\t| AppLanguageMessageWithPayload;\n"
  },
  {
    "path": "src/client-viewer/src/utils/analytics.ts",
    "content": "// google analytics type declarations\ndeclare global {\n\tinterface Window {\n\t\tdataLayer: unknown[];\n\t\tgtag: (...args: unknown[]) => void;\n\t}\n}\n\nconst CONSENT_KEY = 'deskreen_ga_consent';\nconst GA_TAG_PLACEHOLDER = '%VITE_CLIENT_VIEWER_GA_TAG%';\nconst CLIENT_VIEWER_VERSION_PLACEHOLDER = '%VITE_CLIENT_VIEWER_VERSION%';\n\nlet versionEventSent = false;\n\ntype AnalyticsEventParams = Record<string, string | number | boolean>;\n\nexport type ConsentStatus = 'accepted' | 'opted-out' | null;\n\nexport function getConsentStatus(): ConsentStatus {\n\tif (typeof window === 'undefined') {\n\t\treturn null;\n\t}\n\n\tconst stored = localStorage.getItem(CONSENT_KEY);\n\tif (stored === 'accepted' || stored === 'opted-out') {\n\t\treturn stored;\n\t}\n\treturn null;\n}\n\nexport function setConsentStatus(status: 'accepted' | 'opted-out'): void {\n\tif (typeof window === 'undefined') {\n\t\treturn;\n\t}\n\tlocalStorage.setItem(CONSENT_KEY, status);\n}\n\nexport function clearConsentStatus(): void {\n\tif (typeof window === 'undefined') {\n\t\treturn;\n\t}\n\tlocalStorage.removeItem(CONSENT_KEY);\n}\n\nexport function loadGoogleAnalytics(gaTagId: string): void {\n\tif (\n\t\ttypeof window === 'undefined' ||\n\t\t!gaTagId ||\n\t\tgaTagId === GA_TAG_PLACEHOLDER\n\t) {\n\t\treturn;\n\t}\n\n\t// check if GA script is already loaded in DOM (from HTML)\n\tconst existingScript = document.querySelector(\n\t\t'script[src*=\"googletagmanager.com/gtag/js\"]',\n\t);\n\tif (existingScript && window.dataLayer && typeof window.gtag === 'function') {\n\t\t// GA is already loaded from HTML, just update consent and send page_view\n\t\tconst consentStatus = getConsentStatus();\n\t\tconst analyticsConsent =\n\t\t\tconsentStatus === 'accepted' ? 'granted' : 'denied';\n\n\t\t// update consent mode\n\t\twindow.gtag('consent', 'update', {\n\t\t\tanalytics_storage: analyticsConsent,\n\t\t\tad_storage: 'denied',\n\t\t});\n\n\t\t// if user has consent, wait for GA to be ready and send page_view\n\t\tif (analyticsConsent === 'granted') {\n\t\t\twaitForGAReady(() => {\n\t\t\t\tsendPageView();\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\n\t// initialize dataLayer BEFORE gtag.js loads (required for consent mode)\n\twindow.dataLayer = window.dataLayer || [];\n\n\tfunction gtag(...args: unknown[]) {\n\t\twindow.dataLayer.push(args);\n\t}\n\n\twindow.gtag = gtag;\n\tgtag('js', new Date());\n\n\t// set default consent mode to denied (will be updated when user accepts)\n\tgtag('consent', 'default', {\n\t\tanalytics_storage: 'denied',\n\t\tad_storage: 'denied',\n\t});\n\n\t// load gtag.js script\n\tconst script = document.createElement('script');\n\tscript.async = true;\n\tscript.src = `https://www.googletagmanager.com/gtag/js?id=${gaTagId}`;\n\n\tscript.onload = () => {\n\t\t// configure GA after script loads\n\t\tconst consentStatus = getConsentStatus();\n\t\tconst analyticsConsent =\n\t\t\tconsentStatus === 'accepted' ? 'granted' : 'denied';\n\n\t\twindow.gtag('config', gaTagId, {\n\t\t\tsend_page_view: true,\n\t\t\tanonymize_ip: true,\n\t\t});\n\n\t\t// update consent mode based on current status\n\t\twindow.gtag('consent', 'update', {\n\t\t\tanalytics_storage: analyticsConsent,\n\t\t\tad_storage: 'denied',\n\t\t});\n\n\t\t// send page_view event after script is ready\n\t\tif (analyticsConsent === 'granted') {\n\t\t\twaitForGAReady(() => {\n\t\t\t\tsendPageView();\n\t\t\t});\n\t\t}\n\t};\n\n\tdocument.head.appendChild(script);\n}\n\nfunction sendPageView(): void {\n\tif (typeof window === 'undefined' || !window.gtag) {\n\t\treturn;\n\t}\n\n\t// send page_view event with proper GA4 format for real-time tracking\n\ttrackAnalyticsEvent('page_view', {\n\t\tpage_title: document.title,\n\t\tpage_location: window.location.href,\n\t\tpage_path: window.location.pathname,\n\t});\n\n\tsendClientViewerVersionEvent();\n}\n\nfunction waitForGAReady(callback: () => void): void {\n\tif (typeof window === 'undefined') {\n\t\treturn;\n\t}\n\n\t// check if GA script exists\n\tconst script = document.querySelector(\n\t\t'script[src*=\"googletagmanager.com/gtag/js\"]',\n\t);\n\n\tif (!script || typeof window.gtag !== 'function' || !window.dataLayer) {\n\t\t// if script not loaded yet, wait for window load event\n\t\tif (document.readyState === 'loading') {\n\t\t\twindow.addEventListener('load', () => {\n\t\t\t\tsetTimeout(callback, 300);\n\t\t\t});\n\t\t} else {\n\t\t\t// document already loaded, wait a bit for GA to initialize\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (typeof window.gtag === 'function' && window.dataLayer) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}, 300);\n\t\t}\n\t\treturn;\n\t}\n\n\t// script exists, wait for GA to fully initialize\n\t// if document is already loaded, GA should be ready soon\n\tif (document.readyState === 'complete') {\n\t\tsetTimeout(callback, 200);\n\t} else {\n\t\t// wait for document to finish loading first\n\t\twindow.addEventListener('load', () => {\n\t\t\tsetTimeout(callback, 200);\n\t\t});\n\t}\n}\n\nexport function updateAnalyticsConsent(consentStatus: ConsentStatus): void {\n\tif (typeof window === 'undefined' || !window.dataLayer) {\n\t\treturn;\n\t}\n\n\tconst analyticsConsent = consentStatus === 'accepted' ? 'granted' : 'denied';\n\n\t// update consent mode\n\tif (window.gtag) {\n\t\twindow.gtag('consent', 'update', {\n\t\t\tanalytics_storage: analyticsConsent,\n\t\t\tad_storage: 'denied',\n\t\t});\n\n\t\t// if consent granted, send page_view event after GA is ready\n\t\tif (analyticsConsent === 'granted') {\n\t\t\twaitForGAReady(() => {\n\t\t\t\tsendPageView();\n\t\t\t});\n\t\t}\n\t} else {\n\t\t// queue consent update if gtag not ready yet\n\t\twindow.dataLayer.push([\n\t\t\t'consent',\n\t\t\t'update',\n\t\t\t{\n\t\t\t\tanalytics_storage: analyticsConsent,\n\t\t\t\tad_storage: 'denied',\n\t\t\t},\n\t\t]);\n\n\t\t// if consent granted, queue page_view for when GA loads\n\t\tif (analyticsConsent === 'granted') {\n\t\t\twaitForGAReady(() => {\n\t\t\t\tsendPageView();\n\t\t\t});\n\t\t}\n\t}\n}\n\nexport function getGaTagIdFromMeta(): string | null {\n\tconst metaTag = document.querySelector('meta[name=\"ga-tag-id\"]');\n\tif (metaTag) {\n\t\treturn metaTag.getAttribute('content');\n\t}\n\n\t// fallback: try to extract from any existing script tags\n\tconst scripts = document.querySelectorAll(\n\t\t'script[src*=\"googletagmanager.com/gtag/js\"]',\n\t);\n\tfor (const script of scripts) {\n\t\tconst src = script.getAttribute('src');\n\t\tif (src) {\n\t\t\tconst match = src.match(/id=([^&]+)/);\n\t\t\tif (match && match[1] !== GA_TAG_PLACEHOLDER) {\n\t\t\t\treturn match[1];\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getClientViewerVersion(): string | null {\n\tif (typeof window === 'undefined') {\n\t\treturn null;\n\t}\n\n\tconst metaTag = document.querySelector('meta[name=\"client-viewer-version\"]');\n\tif (!metaTag) {\n\t\treturn null;\n\t}\n\n\tconst version = metaTag.getAttribute('content');\n\tif (!version || version === CLIENT_VIEWER_VERSION_PLACEHOLDER) {\n\t\treturn null;\n\t}\n\n\treturn version;\n}\n\nfunction sendClientViewerVersionEvent(): void {\n\tif (versionEventSent || typeof window === 'undefined' || !window.gtag) {\n\t\treturn;\n\t}\n\n\tconst version = getClientViewerVersion();\n\tif (!version) {\n\t\treturn;\n\t}\n\n\tversionEventSent = true;\n\ttrackAnalyticsEvent('client_viewer_version', {\n\t\tclient_viewer_version: version,\n\t});\n}\n\nexport function trackAnalyticsEvent(\n\teventName: string,\n\tparams: AnalyticsEventParams = {},\n): void {\n\tif (typeof window === 'undefined') {\n\t\treturn;\n\t}\n\n\tif (typeof window.gtag === 'function') {\n\t\twindow.gtag('event', eventName, params);\n\t\treturn;\n\t}\n\n\tif (\n\t\twindow.dataLayer &&\n\t\ttypeof (window.dataLayer as unknown[]).push === 'function'\n\t) {\n\t\t(window.dataLayer as unknown[]).push(['event', eventName, params]);\n\t}\n}\n"
  },
  {
    "path": "src/client-viewer/src/utils/gaRequestInterceptor.ts",
    "content": "import { getConsentStatus } from './analytics';\n\nconst GA_DOMAINS = [\n\t'google-analytics.com',\n\t'googletagmanager.com',\n\t'google-analytics.co',\n\t'analytics.google.com',\n\t'region1.google-analytics.com',\n\t'region2.google-analytics.com',\n\t'region3.google-analytics.com',\n\t'region4.google-analytics.com',\n\t'region5.google-analytics.com',\n\t'region6.google-analytics.com',\n\t'region7.google-analytics.com',\n\t'region8.google-analytics.com',\n\t'region9.google-analytics.com',\n\t'region10.google-analytics.com',\n\t'region11.google-analytics.com',\n\t'region12.google-analytics.com',\n\t'region13.google-analytics.com',\n\t'region14.google-analytics.com',\n\t'region15.google-analytics.com',\n\t'region16.google-analytics.com',\n\t'region17.google-analytics.com',\n\t'region18.google-analytics.com',\n\t'region19.google-analytics.com',\n\t'region20.google-analytics.com',\n];\n\nfunction isGoogleAnalyticsUrl(url: string): boolean {\n\ttry {\n\t\tconst urlObj = new URL(url, window.location.href);\n\t\tconst hostname = urlObj.hostname.toLowerCase();\n\t\treturn GA_DOMAINS.some(\n\t\t\t(domain) => hostname === domain || hostname.endsWith('.' + domain),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction shouldBlockRequest(): boolean {\n\tconst consentStatus = getConsentStatus();\n\treturn consentStatus !== 'accepted';\n}\n\nfunction isLocalIP(ip: string): boolean {\n\tconst parts = ip.split('.').map(Number);\n\tif (parts.length !== 4 || parts.some(isNaN)) {\n\t\treturn false;\n\t}\n\n\t// 127.0.0.0/8\n\tif (parts[0] === 127) {\n\t\treturn true;\n\t}\n\n\t// 10.0.0.0/8\n\tif (parts[0] === 10) {\n\t\treturn true;\n\t}\n\n\t// 172.16.0.0/12\n\tif (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {\n\t\treturn true;\n\t}\n\n\t// 192.168.0.0/16\n\tif (parts[0] === 192 && parts[1] === 168) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nfunction sanitizeGAUrl(url: string): string {\n\ttry {\n\t\tconst urlObj = new URL(url);\n\n\t\t// only sanitize /g/collect requests\n\t\tif (!urlObj.pathname.includes('/g/collect')) {\n\t\t\treturn url;\n\t\t}\n\n\t\tconst dlParam = urlObj.searchParams.get('dl');\n\t\tif (!dlParam) {\n\t\t\treturn url;\n\t\t}\n\n\t\ttry {\n\t\t\tconst dlUrl = new URL(decodeURIComponent(dlParam));\n\t\t\tconst hostname = dlUrl.hostname;\n\n\t\t\t// check if hostname is a local IP address\n\t\t\tif (isLocalIP(hostname)) {\n\t\t\t\turlObj.searchParams.set('dl', encodeURIComponent('http://localhost'));\n\t\t\t\treturn urlObj.toString();\n\t\t\t}\n\t\t} catch {\n\t\t\t// if dl parameter is not a valid URL, leave it as is\n\t\t}\n\n\t\treturn url;\n\t} catch {\n\t\treturn url;\n\t}\n}\n\nlet originalFetch: typeof fetch;\nlet originalXHROpen: typeof XMLHttpRequest.prototype.open;\nlet originalXHRSend: typeof XMLHttpRequest.prototype.send;\nlet originalSendBeacon: typeof navigator.sendBeacon;\n\nfunction interceptFetch(): void {\n\tif (typeof window === 'undefined' || window.fetch === originalFetch) {\n\t\treturn;\n\t}\n\n\toriginalFetch = window.fetch;\n\n\twindow.fetch = function (input: RequestInfo | URL, init?: RequestInit) {\n\t\tlet url =\n\t\t\ttypeof input === 'string'\n\t\t\t\t? input\n\t\t\t\t: input instanceof Request\n\t\t\t\t\t? input.url\n\t\t\t\t\t: '';\n\n\t\tif (isGoogleAnalyticsUrl(url)) {\n\t\t\tif (shouldBlockRequest()) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t'Google Analytics request blocked: user consent not granted',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// sanitize URL before making request\n\t\t\turl = sanitizeGAUrl(url);\n\t\t\t// if input was a Request object, we need to create a new one with sanitized URL\n\t\t\tif (input instanceof Request) {\n\t\t\t\tinput = new Request(url, init || input);\n\t\t\t} else {\n\t\t\t\tinput = url;\n\t\t\t}\n\t\t}\n\n\t\treturn originalFetch.call(this, input, init);\n\t};\n}\n\nfunction interceptXMLHttpRequest(): void {\n\tif (typeof window === 'undefined' || !window.XMLHttpRequest) {\n\t\treturn;\n\t}\n\n\tconst XHR = window.XMLHttpRequest;\n\tif (XHR.prototype.open === originalXHROpen) {\n\t\treturn;\n\t}\n\n\toriginalXHROpen = XHR.prototype.open;\n\toriginalXHRSend = XHR.prototype.send;\n\n\tXHR.prototype.open = function (...args: unknown[]) {\n\t\tlet url = args[1] as string | URL;\n\t\tlet urlString = typeof url === 'string' ? url : url.toString();\n\n\t\tif (isGoogleAnalyticsUrl(urlString)) {\n\t\t\tif (shouldBlockRequest()) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'Google Analytics request blocked: user consent not granted',\n\t\t\t\t);\n\t\t\t}\n\t\t\t// sanitize URL before making request\n\t\t\turlString = sanitizeGAUrl(urlString);\n\t\t\turl = urlString;\n\t\t\targs[1] = url;\n\t\t}\n\n\t\t(this as XMLHttpRequest & { _interceptedUrl?: string })._interceptedUrl =\n\t\t\turlString;\n\n\t\treturn (originalXHROpen as (...args: unknown[]) => void).apply(this, args);\n\t};\n\n\tXHR.prototype.send = function (...args) {\n\t\tconst url =\n\t\t\t(this as XMLHttpRequest & { _interceptedUrl?: string })._interceptedUrl ||\n\t\t\t'';\n\n\t\tif (isGoogleAnalyticsUrl(url) && shouldBlockRequest()) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn originalXHRSend.apply(this, args);\n\t};\n}\n\nfunction interceptSendBeacon(): void {\n\tif (\n\t\ttypeof window === 'undefined' ||\n\t\t!navigator.sendBeacon ||\n\t\tnavigator.sendBeacon === originalSendBeacon\n\t) {\n\t\treturn;\n\t}\n\n\toriginalSendBeacon = navigator.sendBeacon;\n\n\tnavigator.sendBeacon = function (\n\t\turl: string | URL,\n\t\tdata?: BodyInit | null,\n\t): boolean {\n\t\tlet urlString = typeof url === 'string' ? url : url.toString();\n\n\t\tif (isGoogleAnalyticsUrl(urlString)) {\n\t\t\tif (shouldBlockRequest()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// sanitize URL before making request\n\t\t\turlString = sanitizeGAUrl(urlString);\n\t\t\turl = urlString;\n\t\t}\n\n\t\treturn originalSendBeacon.call(this, url, data);\n\t};\n}\n\nexport function initializeGARequestInterceptor(): void {\n\tif (typeof window === 'undefined') {\n\t\treturn;\n\t}\n\n\tinterceptFetch();\n\tinterceptXMLHttpRequest();\n\tinterceptSendBeacon();\n}\n"
  },
  {
    "path": "src/client-viewer/src/utils/message.ts",
    "content": "import type { LocalPeerUser } from '../../../common/LocalPeerUser';\nimport type { SendEncryptedMessagePayload } from '../../../common/SendEncryptedMessagePayload';\n\nexport interface ProcessedPayload {\n\ttoSend: ProcessedMessage;\n\toriginal: ProcessedMessage;\n}\n\nexport const process = (\n\tpayload: ReceiveEncryptedMessagePayload,\n): Promise<ProcessedMessage> => Promise.resolve(payload as ProcessedMessage);\n\nexport const prepare = (\n\tpayload: SendEncryptedMessagePayload,\n\tuser: LocalPeerUser,\n): Promise<ProcessedPayload> =>\n\tnew Promise<ProcessedPayload>((resolve) => {\n\t\tconst myUsername = user.username;\n\t\tconst myId = user.id;\n\t\tconst innerPayload = { ...payload.payload } as Record<string, unknown>;\n\t\tif (typeof (innerPayload as { text?: unknown }).text === 'string') {\n\t\t\t(innerPayload as { text?: string }).text = encodeURI(\n\t\t\t\t(innerPayload as { text?: string }).text as string,\n\t\t\t);\n\t\t}\n\t\tconst jsonToSend = {\n\t\t\t...payload,\n\t\t\tpayload: {\n\t\t\t\t...innerPayload,\n\t\t\t\tsender: myId,\n\t\t\t\tusername: myUsername,\n\t\t\t},\n\t\t} as ProcessedMessage;\n\n\t\tresolve({\n\t\t\ttoSend: jsonToSend,\n\t\t\toriginal: jsonToSend,\n\t\t});\n\t});\n"
  },
  {
    "path": "src/client-viewer/src/utils/playerFullscreen.ts",
    "content": "import screenfull from 'screenfull';\nimport { PLAYER_WRAPPER_ID } from '../constants/appConstants';\n\ntype LegacyFullscreenElement = HTMLElement & {\n\twebkitRequestFullscreen?: () => Promise<void> | void;\n\twebkitRequestFullScreen?: () => Promise<void> | void;\n\tmozRequestFullScreen?: () => Promise<void> | void;\n\tmsRequestFullscreen?: () => Promise<void> | void;\n};\n\ntype LegacyFullscreenDocument = Document & {\n\twebkitExitFullscreen?: () => Promise<void> | void;\n\tmozCancelFullScreen?: () => Promise<void> | void;\n\tmsExitFullscreen?: () => Promise<void> | void;\n\twebkitFullscreenElement?: Element | null;\n\tmozFullScreenElement?: Element | null;\n\tmsFullscreenElement?: Element | null;\n};\n\ntype IOSVideoElement = HTMLVideoElement & {\n\twebkitEnterFullscreen?: () => void;\n\twebkitExitFullscreen?: () => void;\n\twebkitSupportsFullscreen?: boolean;\n\twebkitDisplayingFullscreen?: boolean;\n};\n\ntype Unsubscribe = () => void;\n\nconst fullscreenEventNames = [\n\t'fullscreenchange',\n\t'webkitfullscreenchange',\n\t'MSFullscreenChange',\n\t'mozfullscreenchange',\n];\n\nconst getPlayerContainer = (): HTMLElement | null => {\n\treturn document.getElementById(PLAYER_WRAPPER_ID);\n};\n\nconst getPlayerVideo = (): IOSVideoElement | null => {\n\tconst container = getPlayerContainer();\n\tif (!container) return null;\n\tconst maybeVideo = container.querySelector('video');\n\tif (!(maybeVideo instanceof HTMLVideoElement)) return null;\n\treturn maybeVideo as IOSVideoElement;\n};\n\nconst requestStandardFullscreen = (element: HTMLElement | null): boolean => {\n\tif (!element) return false;\n\tconst target = element as LegacyFullscreenElement;\n\tconst request =\n\t\ttarget.requestFullscreen ||\n\t\ttarget.webkitRequestFullscreen ||\n\t\ttarget.webkitRequestFullScreen ||\n\t\ttarget.mozRequestFullScreen ||\n\t\ttarget.msRequestFullscreen;\n\tif (typeof request !== 'function') return false;\n\trequest.call(target);\n\treturn true;\n};\n\nconst exitStandardFullscreen = (): boolean => {\n\tconst doc = document as LegacyFullscreenDocument;\n\tconst exit =\n\t\tdoc.exitFullscreen ||\n\t\tdoc.webkitExitFullscreen ||\n\t\tdoc.mozCancelFullScreen ||\n\t\tdoc.msExitFullscreen;\n\tif (typeof exit !== 'function') return false;\n\texit.call(doc);\n\treturn true;\n};\n\nexport const isPlayerFullscreen = (): boolean => {\n\tconst doc = document as LegacyFullscreenDocument;\n\tif (\n\t\tdoc.fullscreenElement ||\n\t\tdoc.webkitFullscreenElement ||\n\t\tdoc.mozFullScreenElement ||\n\t\tdoc.msFullscreenElement\n\t) {\n\t\treturn true;\n\t}\n\tconst video = getPlayerVideo();\n\tif (!video) return false;\n\tif (typeof video.webkitDisplayingFullscreen === 'boolean') {\n\t\treturn video.webkitDisplayingFullscreen;\n\t}\n\treturn false;\n};\n\nexport const enterPlayerFullscreen = (): boolean => {\n\tconst container = getPlayerContainer();\n\tif (container && screenfull.isEnabled) {\n\t\tscreenfull.request(container);\n\t\treturn true;\n\t}\n\tif (requestStandardFullscreen(container)) return true;\n\tconst video = getPlayerVideo();\n\tif (requestStandardFullscreen(video)) return true;\n\tif (video && typeof video.webkitEnterFullscreen === 'function') {\n\t\tif (\n\t\t\ttypeof video.webkitSupportsFullscreen === 'boolean' &&\n\t\t\t!video.webkitSupportsFullscreen\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t\tvideo.webkitEnterFullscreen();\n\t\treturn true;\n\t}\n\treturn false;\n};\n\nexport const exitPlayerFullscreen = (): boolean => {\n\tif (screenfull.isEnabled && screenfull.isFullscreen) {\n\t\tscreenfull.exit();\n\t\treturn true;\n\t}\n\tif (exitStandardFullscreen()) return true;\n\tconst video = getPlayerVideo();\n\tif (video && typeof video.webkitExitFullscreen === 'function') {\n\t\tif (\n\t\t\ttypeof video.webkitDisplayingFullscreen === 'boolean' &&\n\t\t\t!video.webkitDisplayingFullscreen\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t\tvideo.webkitExitFullscreen();\n\t\treturn true;\n\t}\n\treturn false;\n};\n\nexport const togglePlayerFullscreen = (): 'entered' | 'exited' | 'failed' => {\n\tif (isPlayerFullscreen()) {\n\t\treturn exitPlayerFullscreen() ? 'exited' : 'failed';\n\t}\n\treturn enterPlayerFullscreen() ? 'entered' : 'failed';\n};\n\nexport const subscribeToPlayerFullscreenChange = (\n\tlistener: (isFullscreen: boolean) => void,\n): Unsubscribe => {\n\tconst handleChange = () => listener(isPlayerFullscreen());\n\tconst handleVideoBegin = () => listener(true);\n\tconst handleVideoEnd = () => listener(false);\n\n\tif (screenfull.isEnabled) {\n\t\tscreenfull.on('change', handleChange);\n\t}\n\n\tfullscreenEventNames.forEach((eventName) => {\n\t\tdocument.addEventListener(eventName, handleChange);\n\t});\n\n\tlet currentVideo: IOSVideoElement | null = null;\n\n\tconst attachVideoListeners = (video: IOSVideoElement | null) => {\n\t\tif (currentVideo === video) return;\n\t\tif (currentVideo) {\n\t\t\tcurrentVideo.removeEventListener(\n\t\t\t\t'webkitbeginfullscreen',\n\t\t\t\thandleVideoBegin,\n\t\t\t);\n\t\t\tcurrentVideo.removeEventListener('webkitendfullscreen', handleVideoEnd);\n\t\t}\n\t\tcurrentVideo = video;\n\t\tif (currentVideo) {\n\t\t\tcurrentVideo.addEventListener('webkitbeginfullscreen', handleVideoBegin);\n\t\t\tcurrentVideo.addEventListener('webkitendfullscreen', handleVideoEnd);\n\t\t}\n\t};\n\n\tattachVideoListeners(getPlayerVideo());\n\n\tconst container = getPlayerContainer();\n\tlet observer: MutationObserver | null = null;\n\n\tif (container) {\n\t\tobserver = new MutationObserver(() => {\n\t\t\tattachVideoListeners(getPlayerVideo());\n\t\t});\n\t\tobserver.observe(container, { childList: true, subtree: true });\n\t}\n\n\tlistener(isPlayerFullscreen());\n\n\treturn () => {\n\t\tif (screenfull.isEnabled) {\n\t\t\tscreenfull.off('change', handleChange);\n\t\t}\n\t\tfullscreenEventNames.forEach((eventName) => {\n\t\t\tdocument.removeEventListener(eventName, handleChange);\n\t\t});\n\t\tif (currentVideo) {\n\t\t\tcurrentVideo.removeEventListener(\n\t\t\t\t'webkitbeginfullscreen',\n\t\t\t\thandleVideoBegin,\n\t\t\t);\n\t\t\tcurrentVideo.removeEventListener('webkitendfullscreen', handleVideoEnd);\n\t\t}\n\t\tif (observer) {\n\t\t\tobserver.disconnect();\n\t\t}\n\t};\n};\n"
  },
  {
    "path": "src/client-viewer/src/utils/socket.ts",
    "content": "import socketIO, { Socket } from 'socket.io-client';\nimport { generateUrl } from '../api/generator';\n\nlet socket: Socket;\n\nexport const connect = (roomId: string) => {\n\tsocket = socketIO(generateUrl(), {\n\t\tquery: {\n\t\t\troomId,\n\t\t},\n\t\tforceNew: true,\n\t});\n\treturn socket;\n};\n\nexport const getSocket = () => socket;\n"
  },
  {
    "path": "src/client-viewer/src/utils/userAgentParserHelpers.ts",
    "content": "// @ts-ignore\nimport { UAParser } from 'ua-parser-js';\n\nexport function getOSFromUAParser(uaParser: UAParser) {\n\tconst osFromUAParser = uaParser.getResult().os;\n\n\treturn `${osFromUAParser.name ? osFromUAParser.name : ''} ${\n\t\tosFromUAParser.version ? osFromUAParser.version : ''\n\t}`;\n}\n\nexport function getDeviceTypeFromUAParser(uaParser: UAParser) {\n\tconst deviceTypeFromUAParser = uaParser.getResult().device;\n\n\treturn deviceTypeFromUAParser.type\n\t\t? deviceTypeFromUAParser.type.toString()\n\t\t: 'computer';\n}\n\nexport function getBrowserFromUAParser(uaParser: UAParser) {\n\tconst browserFromUAParser = uaParser.getResult().browser;\n\n\treturn `${browserFromUAParser.name ? browserFromUAParser.name : ''} ${\n\t\tbrowserFromUAParser.version ? browserFromUAParser.version : ''\n\t}`;\n}\n"
  },
  {
    "path": "src/client-viewer/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\ntype ConnectionIconType =\n\t| ConnectionIconEnum.FEED\n\t| ConnectionIconEnum.FEED_SUBSCRIBED;\ntype LoadingSharingIconType =\n\t| LoadingSharingIconEnum.DESKTOP\n\t| LoadingSharingIconEnum.APPLICATION;\ntype ScreenSharingSourceType =\n\t| ScreenSharingSourceEnum.SCREEN\n\t| ScreenSharingSourceEnum.WINDOW;\ntype CreatePeerConnectionUseEffectParams = {\n\tconnectionRoomId: string;\n\tpeer: undefined | PeerConnection;\n\tsetMyDeviceDetails: (_: DeviceDetails) => void;\n\tsetConnectionIconType: (_: ConnectionIconType) => void;\n\tsetIsShownTextPrompt: (_: boolean) => void;\n\tsetPromptStep: (_: number) => void;\n\tsetScreenSharingSourceType: (_: ScreenSharingSourceType) => void;\n\tsetDialogErrorMessage: (_: ErrorMessage) => void;\n\tsetIsErrorDialogOpen: (_: boolean) => void;\n\tsetUrl: (_: MediaStream | null) => void;\n\tsetPeer: (_: undefined | PeerConnection) => void;\n};\ntype handleDisplayingLoadingSharingIconLoopParams = {\n\tpromptStep: number;\n\turl: MediaStream | null;\n\tsetIsShownLoadingSharingIcon: (_: boolean) => void;\n\tloadingSharingIconType: LoadingSharingIconType;\n\tisShownLoadingSharingIcon: boolean;\n\tsetLoadingSharingIconType: (_: LoadingSharingIconType) => void;\n};\n\ninterface Document {\n\t/**\n\t * Indicates whether the document is currently in the process of prerendering.\n\t * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering\n\t * @see https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering\n\t */\n\tprerendering?: boolean;\n\n\t/**\n\t * An event handler for the prerenderingchange event, which is fired when\n\t * a prerendered document is activated.\n\t * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/prerenderingchange_event\n\t */\n\tonprerenderingchange?: ((this: Document, ev: Event) => void) | null;\n}\n"
  },
  {
    "path": "src/client-viewer/tsconfig.app.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n\t\t\"target\": \"ES2022\",\n\t\t\"useDefineForClassFields\": true,\n\t\t\"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n\t\t\"module\": \"ESNext\",\n\t\t\"skipLibCheck\": true,\n\n\t\t/* Bundler mode */\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"verbatimModuleSyntax\": true,\n\t\t\"moduleDetection\": \"force\",\n\t\t\"noEmit\": true,\n\t\t\"jsx\": \"react-jsx\",\n\n\t\t/* Linting */\n\t\t\"strict\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"erasableSyntaxOnly\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"noUncheckedSideEffectImports\": true\n\t},\n\t\"include\": [\"src\"]\n}\n"
  },
  {
    "path": "src/client-viewer/tsconfig.json",
    "content": "{\n\t\"files\": [],\n\t\"references\": [\n\t\t{ \"path\": \"./tsconfig.app.json\" },\n\t\t{ \"path\": \"./tsconfig.node.json\" }\n\t]\n}\n"
  },
  {
    "path": "src/client-viewer/tsconfig.node.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n\t\t\"target\": \"ES2023\",\n\t\t\"lib\": [\"ES2023\"],\n\t\t\"module\": \"ESNext\",\n\t\t\"skipLibCheck\": true,\n\n\t\t/* Bundler mode */\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"verbatimModuleSyntax\": true,\n\t\t\"moduleDetection\": \"force\",\n\t\t\"noEmit\": true,\n\n\t\t/* Linting */\n\t\t\"strict\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"erasableSyntaxOnly\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"noUncheckedSideEffectImports\": true\n\t},\n\t\"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "src/client-viewer/vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport legacy from '@vitejs/plugin-legacy';\nimport { nodePolyfills } from 'vite-plugin-node-polyfills';\nimport type { Plugin } from 'vite';\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\ninterface PackageJson {\n\tversion?: string;\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst packageJson = JSON.parse(\n\treadFileSync(new URL('./package.json', import.meta.url), 'utf-8'),\n) as PackageJson;\nconst clientViewerVersion =\n\tprocess.env.VITE_CLIENT_VIEWER_VERSION || packageJson.version || '';\n\n// load GA interceptor script from separate file\nconst gaInterceptorScript = readFileSync(\n\tjoin(__dirname, 'scripts', 'ga-interceptor.js'),\n\t'utf-8',\n);\n\n// plugin to replace html placeholders with env variables and inject GA interceptor\nconst replaceHtmlEnvPlugin = (): Plugin => {\n\treturn {\n\t\tname: 'replace-html-env',\n\t\ttransformIndexHtml(html) {\n\t\t\tconst gaTagId = process.env.VITE_CLIENT_VIEWER_GA_TAG || '';\n\t\t\tlet transformed = html\n\t\t\t\t.replace(/%VITE_CLIENT_VIEWER_GA_TAG%/g, gaTagId)\n\t\t\t\t.replace(/%VITE_CLIENT_VIEWER_VERSION%/g, clientViewerVersion);\n\n\t\t\t// inject GA interceptor script before GA script loads\n\t\t\tif (\n\t\t\t\ttransformed.includes(\n\t\t\t\t\t'<script async src=\"https://www.googletagmanager.com/gtag/js',\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\ttransformed = transformed.replace(\n\t\t\t\t\t'<script async src=\"https://www.googletagmanager.com/gtag/js',\n\t\t\t\t\t`<script>${gaInterceptorScript}</script>\\n    <script async src=\"https://www.googletagmanager.com/gtag/js`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn transformed;\n\t\t},\n\t};\n};\n\n// https://vite.dev/config/\nexport default defineConfig({\n\tplugins: [\n\t\treact(),\n\t\tlegacy({\n\t\t\ttargets: ['defaults', 'not IE 11'], // Or your specific browser targets\n\t\t}),\n\t\tnodePolyfills(),\n\t\treplaceHtmlEnvPlugin(),\n\t],\n});\n"
  },
  {
    "path": "src/common/DesktopCapturerSourceType.ts",
    "content": "enum DesktopCapturerSourceType {\n\tWINDOW = 'window',\n\tSCREEN = 'screen',\n}\n\nexport default DesktopCapturerSourceType;\n"
  },
  {
    "path": "src/common/Device.ts",
    "content": "export interface Device {\n\tid: string;\n\tsharingSessionID: string;\n\tdeviceOS: string;\n\tdeviceType: string;\n\tdeviceIP: string;\n\tdeviceBrowser: string;\n\tdeviceScreenWidth: number;\n\tdeviceScreenHeight: number;\n\tdeviceRoomId: string;\n}\n"
  },
  {
    "path": "src/common/ElectronStoreKeys.enum.ts",
    "content": "export enum ElectronStoreKeys {\n\tAppLanguage = 'appLanguage',\n\tIsNotFirstTimeAppStart = 'isNotFirstTimeAppStart',\n}\n"
  },
  {
    "path": "src/common/IpcEvents.enum.ts",
    "content": "export enum IpcEvents {\n\tCreateWaitingForConnectionSharingSession = 'create-waiting-for-connection-sharing-session',\n\tSetPendingConnectionDevice = 'set-pending-connection-device',\n\tUnmarkRoomIDAsTaken = 'unmark-room-id-as-taken',\n\tGetAppPath = 'get-app-path',\n\tResetWaitingForConnectionSharingSession = 'reset-waiting-for-connection-sharing-session',\n\tSetDeviceConnectedStatus = 'set-device-connected-status',\n\tGetSourceDisplayIDByDesktopCapturerSourceID = 'get-source-display-id-by-desktop-capturer-source-id',\n\tDisconnectPeerAndDestroySharingSessionBySessionID = 'disconnect-peer-and-destroy-sharing-session-by-session-id',\n\tGetDesktopCapturerSourceIdBySharingSessionId = 'get-desktop-capturer-source-id-by-sharing-session-id',\n\tGetConnectedDevices = 'get-connected-devices-list',\n\tDisconnectDeviceById = 'disconnect-device-by-id',\n\tDisconnectAllDevices = 'disconnect-all-devices',\n\tGetViewerConnectionAvailability = 'get-viewer-connection-availability',\n\tViewerConnectionAvailabilityChanged = 'viewer-connection-availability-changed',\n\tAppLanguageChanged = 'app-language-changed',\n\tGetDesktopCapturerServiceSourcesMap = 'get-desktop-capturer-service-sources-map',\n\tGetDesktopCapturerServiceSourcesByIds = 'get-desktop-capturer-service-sources-by-ids',\n\tGetWaitingForConnectionSharingSessionSourceId = 'get-waiting-for-connection-sharing-session-source-id',\n\tStartSharingOnWaitingForConnectionSharingSession = 'start-sharing-on-waiting-for-connection-sharing-session',\n\tGetPendingConnectionDevice = 'get-pending-connection-device',\n\tGetWaitingForConnectionSharingSessionRoomId = 'get-waiting-for-connection-sharing-session-room-id',\n\tGetIsLinuxWaylandSession = 'get-is-linux-wayland-session',\n\tRequestDesktopCapturerPortalSource = 'request-desktop-capturer-portal-source',\n\tGetDesktopSharingSourceIds = 'get-desktop-sharing-source-ids',\n\tSetDesktopCapturerSourceId = 'set-desktop-capturer-source-id',\n\tGetAppLanguage = 'get-app-language',\n\tGetIsFirstTimeAppStart = 'get-is-not-first-time-app-start',\n\tSetAppStartedOnce = 'set-app-started-once',\n\tDestroySharingSessionById = 'destroy-sharing-session-by-id',\n\tGetPort = 'get-port',\n\tOpenExternalLink = 'open-external-link',\n\tWriteTextToClipboard = 'write-text-to-clipboard',\n}\n"
  },
  {
    "path": "src/common/LocalPeerUser.ts",
    "content": "export interface LocalPeerUser {\n\tusername: string;\n\tid: string;\n}\n"
  },
  {
    "path": "src/common/SendEncryptedMessagePayload.ts",
    "content": "export interface SendEncryptedMessagePayload {\n\ttype: string;\n\tpayload: Record<string, unknown>;\n}\n"
  },
  {
    "path": "src/common/app.lang.config.ts",
    "content": "export default {\n\tfallbackLng: 'en',\n\tnamespace: 'translation',\n\tlanguages: [\n\t\t'ua',\n\t\t'en',\n\t\t'es',\n\t\t'zh_CN',\n\t\t'zh_TW',\n\t\t'da',\n\t\t'ru',\n\t\t'de',\n\t\t'fi',\n\t\t'ko',\n\t\t'it',\n\t\t'ja',\n\t\t'nl',\n\t\t'fr',\n\t\t'sv',\n\t],\n\tlangISOKeyToLangFullNameMap: {\n\t\ten: 'English',\n\t\tes: 'Español',\n\t\tru: 'Русский',\n\t\tua: 'Українська',\n\t\tda: 'Dansk',\n\t\tde: 'Deutsch',\n\t\tfi: 'Suomi',\n\t\tit: 'Italiano',\n\t\tnl: 'Nederlands',\n\t\tfr: 'Français',\n\t\tsv: 'Svenska',\n\t\tko: '한국어',\n\t\tzh_CN: '简体中文',\n\t\tzh_TW: '繁體中文',\n\t\tja: '日本語',\n\t},\n};\n"
  },
  {
    "path": "src/common/config.ts",
    "content": "/* istanbul ignore file */\n\nlet hostname;\nlet protocol;\nlet primaryPort;\nlet backupPort;\n\nif (!hostname && !protocol && !primaryPort && !backupPort) {\n\thostname = 'localhost';\n\tprotocol = 'http';\n\tprimaryPort = 3131;\n\tbackupPort = 3132;\n}\n\nexport default {\n\thostname,\n\tprotocol,\n\tprimaryPort,\n\tbackupPort,\n};\n"
  },
  {
    "path": "src/common/connectSocket.ts",
    "content": "import socketIO from 'socket.io-client';\n\nexport const connectSocket = (port: string, roomId: string) => {\n\treturn socketIO(`http://127.0.0.1:${port}`, {\n\t\tquery: {\n\t\t\troomId,\n\t\t},\n\t\tforceNew: true,\n\t});\n};\n\nexport default {};\n"
  },
  {
    "path": "src/common/deskreen-electron-store.ts",
    "content": "// import Store from 'electron-store';\n//\n// export const store = new Store() as unknown as {\n//   has: (key: string) => boolean;\n//   get: (key: string) => any;\n//   delete: (key: string) => void;\n//   set: (key: string, value: any) => void;\n// };\n\n// In an Electron app, it's best to use the built-in 'app' module\n// to get the correct path for user data.\n// In a standalone script, you could use os.homedir() and a custom directory.\nimport { app } from 'electron';\nimport * as path from 'path';\nimport * as fs from 'fs';\n\n// Define the shape of our store's data.\n// This generic allows the storage class to be type-safe.\nexport type DataStore = Record<string, string>;\n\n/**\n * A simple file-based key-value store for Electron applications.\n * Data is persisted to a JSON file in the user's data directory.\n */\nconst readDataFromDisk = (filePath: string): DataStore => {\n\ttry {\n\t\t// Read the file synchronously to ensure data is loaded before any operations.\n\t\tconst fileContent = fs.readFileSync(filePath, 'utf-8');\n\t\treturn JSON.parse(fileContent) as DataStore;\n\t} catch (_error) {\n\t\t// If the file doesn't exist or is invalid JSON, return an empty object.\n\t\treturn {};\n\t}\n};\n\nexport class PersistentStorage {\n\tprivate filePath: string;\n\tprivate data: DataStore;\n\n\t/**\n\t * @param fileName The name of the file to save the data to. Defaults to 'deskreen-ce-storage.json'.\n\t */\n\tconstructor(fileName = 'deskreen-ce-storage.json') {\n\t\tconst userDataPath = app.getPath('userData');\n\t\tthis.filePath = path.join(userDataPath, fileName);\n\t\tthis.data = readDataFromDisk(this.filePath);\n\t}\n\n\t/**\n\t * Writes the current data object to the JSON file.\n\t */\n\tprivate writeData(): void {\n\t\ttry {\n\t\t\t// Use writeFileSync for synchronous writing. This is generally safe for app settings\n\t\t\t// and ensures the data is saved before the app exits.\n\t\t\tconst jsonContent = JSON.stringify(this.data, null, 2);\n\t\t\tfs.writeFileSync(this.filePath, jsonContent, 'utf-8');\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to write data to file:', error);\n\t\t}\n\t}\n\n\t/**\n\t * Checks if a key exists in the store.\n\t * @param key The key to check for.\n\t * @returns `true` if the key exists, `false` otherwise.\n\t */\n\tpublic has(key: string): boolean {\n\t\treturn Object.hasOwn(this.data, key);\n\t}\n\n\t/**\n\t * Gets a value from the store by its key.\n\t * @param key The key to retrieve.\n\t * @returns The value associated with the key, or `undefined` if not found.\n\t */\n\tpublic get(key: string): string | undefined {\n\t\treturn this.data[key];\n\t}\n\n\t/**\n\t * Sets a value in the store.\n\t * @param key The key to set.\n\t * @param value The string value to store.\n\t */\n\tpublic set(key: string, value: string): void {\n\t\tthis.data[key] = value;\n\t\tthis.writeData(); // Persist the change immediately.\n\t}\n\n\t/**\n\t * Deletes a key-value pair from the store.\n\t * @param key The key to delete.\n\t */\n\tpublic delete(key: string): void {\n\t\tdelete this.data[key];\n\t\tthis.writeData(); // Persist the change immediately.\n\t}\n\n\t/**\n\t * Clears all data from the store.\n\t */\n\tpublic clear(): void {\n\t\tthis.data = {};\n\t\tthis.writeData();\n\t}\n}\n\nexport const store = new PersistentStorage();\n"
  },
  {
    "path": "src/common/getAppLanguage.ts",
    "content": "import { IpcEvents } from './IpcEvents.enum';\n\nexport default async function getAppLanguage(): Promise<string> {\n\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t// @ts-ignore\n\tconst appLanguage = await window.electron.ipcRenderer.invoke(\n\t\tIpcEvents.GetAppLanguage,\n\t);\n\treturn appLanguage;\n}\n"
  },
  {
    "path": "src/common/isProduction.ts",
    "content": "export default function isProduction(): boolean {\n\treturn (\n\t\tprocess.env.NODE_ENV === 'production' &&\n\t\tprocess.env.RUN_MODE !== 'dev' &&\n\t\tprocess.env.RUN_MODE !== 'test'\n\t);\n\t// return true; // for animations and other things debugging as in production mode\n\t// return false;\n}\n"
  },
  {
    "path": "src/common/locales/da/translation.json",
    "content": "{\n\t\"hello\": \"Hej\",\n\t\"continue\": \"Fortsæt\",\n\t\"language\": \"Sprog\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Donér\",\n\t\"get-deskreen-pro\": \"Hent Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hent Deskreen Pro - åbner downloadsiden.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Klik her for at besøge vores hjemmeside\",\n\t\"connected-devices\": \"Forbundede Enheder\",\n\t\"tutorial\": \"Introduktion\",\n\t\"settings\": \"Indstillinger\",\n\t\"connect\": \"Forbind\",\n\t\"select\": \"Vælg\",\n\t\"confirm\": \"Bekræft\",\n\t\"scan-the-qr-code\": \"Skan QR koden\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Tjek at din computer- og skærmvisningsenhed er forbundet til det samme Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Eller skriv følgende addresse i dit browservindue på enhver enhed\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Nogen prøver at oprette forbindelse, tillader du?\",\n\t\"click-to-make-bigger\": \"Klik for at forstørre\",\n\t\"click-to-copy\": \"Klik for at kopiere\",\n\t\"partner-device-info\": \"Partner enhedsinfo\",\n\t\"device-type\": \"Enhedstype\",\n\t\"device-ip\": \"Enhedens IP\",\n\t\"device-browser\": \"Enhedens Browser\",\n\t\"device-os\": \"Enhedens Operativsystem\",\n\t\"session-id\": \"Sessionsid\",\n\t\"allow\": \"Tillad\",\n\t\"deny\": \"Afvis\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Enheden blev succesfuldt afbrudt af dig. Du kan nu forbinde en ny enhed.\",\n\t\"deskreen-ce-update-is-available\": \"Der er en Deskreen CE opdatering tilgængelig!\",\n\t\"your-current-version-is\": \"Din nuværende version er\",\n\t\"click-to-download-new-updated-version\": \"Klik her for at downloade den nye, opdaterede version\",\n\t\"new-version-available\": \"Ny version tilgængelig!\",\n\t\"connected\": \"Forbundet\",\n\t\"click-to-see-more\": \"Klik her for at se mere\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Det her skal matche med enhedens IP, som bliver vist på skærmen af enheden som prøver at forbinde\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Hvis IP-addresser ikke matcher, så klik på knappen Afbryd\",\n\t\"disconnect\": \"Afbryd\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Vælg Hele Skærmen eller Appvinduet som du ønsker at dele\",\n\t\"or\": \"ELLER\",\n\t\"entire-screen\": \"Hele Skærmen\",\n\t\"application-window\": \"Programvindue\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Tjek om alt er OK og klik Bekræft\",\n\t\"confirm-button-text\": \"Bekræft\",\n\t\"no-i-need-to-choose-other\": \"Nej, jeg vil vælge en anden\",\n\t\"done\": \"Færdig!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Nu kan du se din skærm på den anden enhed.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Du kan håndtere forbundede enheder ved at klikke på knappen Forbundede Enheder i toppanelet.\",\n\t\"connect-new-device\": \"Forbind Ny Enhed\",\n\t\"select-entire-screen-to-share\": \"Vælg at Dele Hele Skærmen\",\n\t\"select-app-window-to-share\": \"Vælg at Dele et Appvindue\",\n\t\"refresh\": \"Opdatér\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Afbryd alle enheder\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Er du sikker på, at du vil afbryde alle forbundede enheder?\",\n\t\"this-step-can-not-be-undone\": \"Denne handling kan ikke fortrydes\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Du bliver nødt til at forbinde alle enheder manuelt igen\",\n\t\"no-cancel\": \"Nej, Annullér\",\n\t\"yes-disconnect-all\": \"Ja, Afbryd Alle\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"En ny version af Deskreen CE er tilgængelig! Klik her for at downloade\",\n\t\"security\": \"Sikkerhed\",\n\t\"general\": \"Generelt\",\n\t\"about\": \"Om\",\n\t\"website\": \"Hjemmeside\",\n\t\"about-deskreen\": \"Om Deskreen CE\",\n\t\"security-settings\": \"Sikkerhedsindstillinger\",\n\t\"color-theme\": \"Farvetema\",\n\t\"automatic-updates\": \"Automatiske Opdateringer\",\n\t\"general-settings\": \"Generelle Indstillinger\",\n\t\"disabled\": \"Deaktiveret\",\n\t\"version\": \"Version\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Redigere\",\n\t\"hide-deskreen\": \"Skjul Deskreen CE\",\n\t\"hide-others\": \"Skjul Andre\",\n\t\"show-all\": \"Vis Alt\",\n\t\"quit\": \"Forlad\",\n\t\"undo\": \"Fortryd\",\n\t\"redo\": \"Gentag\",\n\t\"cut\": \"Klip\",\n\t\"copy\": \"Kopiér\",\n\t\"paste\": \"Indsæt\",\n\t\"select-all\": \"Vælg Alt\",\n\t\"view\": \"Se\",\n\t\"reload\": \"Genindlæs\",\n\t\"toggle-full-screen\": \"Skift til Fuldskærm\",\n\t\"toggle-developer-tools\": \"Vis Udviklingsværktøjer\",\n\t\"window\": \"Vindue\",\n\t\"minimize\": \"Minimér\",\n\t\"close\": \"Luk\",\n\t\"bring-all-to-front\": \"Bring alt til fronten\",\n\t\"help\": \"Hjælp\",\n\t\"learn-more\": \"Lær Mere\",\n\t\"documentation\": \"Dokumentation\",\n\t\"community-discussions\": \"Fællesskabsdiskussioner\",\n\t\"search-issues\": \"Søgeproblemer\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Betroede Tilgængelige Enheder\",\n\t\"make-this-device-trusted\": \"Gør denne enhed betroet\",\n\t\"click-to-select-other-screen-source-to-share\": \"Klik for at vælge at dele en anden skærm\",\n\t\"click-to-edit-device-alias\": \"Klik her for at redigere Enhedens Alias\",\n\t\"trusted-device-id\": \"Betroet Enhedsid\",\n\t\"trusted\": \"Betroet\",\n\t\"make-trusted\": \"Gør Betroet\",\n\t\"forget-this-device\": \"Glem Denne Enhed\",\n\t\"device-alias\": \"Enhedens Alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Del automatisk Hele den sidste Skærm når enheden er tilgængelig\",\n\t\"all-devices-are-successfully-disconnected\": \"Alle enheder er afbrudt succesfuldt\",\n\t\"device-was-disconnected\": \"Enheden blev afbrudt\",\n\t\"networking\": \"Netværk\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE Applikationsport\",\n\t\"port-is-already-used-by-other-app\": \"Porten er allerede i brug af en anden App\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Klik her for at ændre Deskreen CE Applikationsport\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Indtast et tal mellem 3000 og 64000, som vil blive brugt som Deskreen CE Applikationsport\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Indtast et andet tal mellem 3000 og 64000\",\n\t\"select-network-interface\": \"Vælg Netværksgrænseflade\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Jeg kender min computers IP-addresse\",\n\t\"type-your-computer-ip\": \"Indtast din computers IP-addresse\",\n\t\"click-to-type-ip-manually\": \"Klik her for at indtaste IP-addressen manuelt\",\n\t\"banned-ips\": \"Bandlyste IP'er\",\n\t\"ban-new-ip\": \"Bandlys ny IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Indtast den IP-addresse, som du gerne vil bandlyse\",\n\t\"unban-this-ip\": \"Tillad denne IP\",\n\t\"unban-all-ips\": \"Tillad alle IP'er\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Nulstil Deskreen CE indstillinger til standardindstillinger\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Spørg brugeren om password, når de opretter forbindelse\",\n\t\"change-password\": \"Ændr Adgangskode\",\n\t\"type-a-new-password\": \"Indtast en ny Adgangskode\",\n\t\"cancel\": \"Annullér\",\n\t\"device-status\": \"Enhedens Status\",\n\t\"sharing-screen\": \"Skærm som deles\",\n\t\"available-no-screen-sharing\": \"Tilgængelig, ingen skærmdeling\",\n\t\"not-available\": \"Ikke Tilgængelig\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Start Deskreen CE App'en automatisk når du logger ind\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Åben Deskreen CE Appvinduet når du logger ind\",\n\t\"use-system-tray\": \"Brug system bakke\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE System Bakke\",\n\t\"open-app-window\": \"Åben Appvinduet\",\n\t\"minimize-to-tray\": \"Minimér til System Bakke\",\n\t\"show-connected-devices\": \"Vis Forbundede Enheder\",\n\t\"quit-deskreen-ce\": \"Forlad Deskreen CE\",\n\t\"fix-reset\": \"Ret & Nulstil\",\n\t\"fix-reset-tooltip\": \"Ret & Nulstil. Har du problemer med at forbinde nye klienter? At klikke på denne knap kan hjælpe dig.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"En visningsklient er allerede forbundet.\",\n\t\"viewing-client-connected-label\": \"Visningsklient tilsluttet\",\n\t\"connection-limit-reached-tooltip\": \"Forbindelsesgrænsen er nået. Afbryd den nuværende visningsklient (eller vent på, at den afbryder), for at tilslutte en ny.\",\n\t\"scan-the-qr-code-to-connect\": \"Scan QR-koden for at oprette forbindelse\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Indtast følgende adresse i adresselinjen på en hvilken som helst enhed\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE tillader kun et enkelt visningsapparat tilsluttet ad gangen.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Muligheden for at tilslutte mere end én enhed er kun tilgængelig i Pro-versionen.\"\n}\n"
  },
  {
    "path": "src/common/locales/de/translation.json",
    "content": "{\n\t\"hello\": \"Hallo\",\n\t\"continue\": \"Weiter\",\n\t\"language\": \"Sprache\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Spenden\",\n\t\"get-deskreen-pro\": \"Deskreen Pro erhalten\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro erhalten - öffnet die Download-Seite.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Klicken um unsere Website zu besuchen\",\n\t\"connected-devices\": \"Verbundene Geräte\",\n\t\"tutorial\": \"Einführung\",\n\t\"settings\": \"Einstellungen\",\n\t\"connect\": \"Verbinden\",\n\t\"select\": \"Auswählen\",\n\t\"confirm\": \"Bestätigen\",\n\t\"scan-the-qr-code\": \"Den QR-Code scannen\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Stellen Sie sicher, dass Ihr Computer und das Anzeigegerät mit demselben WLAN verbunden sind\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Oder geben Sie die folgende Adresse in die Adresszeile des Browsers auf einem beliebigen Gerät ein\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Jemand versucht, sich zu verbinden. Zulassen?\",\n\t\"click-to-make-bigger\": \"Zum Vergrößern klicken\",\n\t\"click-to-copy\": \"Zum Kopieren klicken\",\n\t\"partner-device-info\": \"Info zum Partnergerät\",\n\t\"device-type\": \"Gerätetyp\",\n\t\"device-ip\": \"Geräte-IP\",\n\t\"device-browser\": \"Geräte-Browser\",\n\t\"device-os\": \"Geräte-Betriebssystem\",\n\t\"session-id\": \"Sitzungs-ID\",\n\t\"allow\": \"Erlauben\",\n\t\"deny\": \"Verweigern\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Das Gerät wurde erfolgreich getrennt. Du kannst ein neues Gerät verbinden.\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE Update verfügbar!\",\n\t\"your-current-version-is\": \"Die aktuelle Version ist\",\n\t\"click-to-download-new-updated-version\": \"Klicken, um die neue Version herunterzuladen\",\n\t\"new-version-available\": \"Neue Version verfügbar!\",\n\t\"connected\": \"Verbunden\",\n\t\"click-to-see-more\": \"Klicken, um mehr anzuzeigen\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Das sollte mit der Geräte-IP auf dem Bildschirm des Gerätes übereinstimmen, das sich zu verbinden versucht.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Klicken Sie auf Trennen, wenn die IP-Adressen nicht übereinstimmen.\",\n\t\"disconnect\": \"Trennen\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Den gesamten Bildschirm oder ein Anwendungsfenster zum Teilen auswählen\",\n\t\"or\": \"ODER\",\n\t\"entire-screen\": \"Gesamter Bildschirm\",\n\t\"application-window\": \"Anwendungsfenster\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Überprüfen Sie die Eingaben und klicken Sie Bestätigen\",\n\t\"confirm-button-text\": \"Bestätigen\",\n\t\"no-i-need-to-choose-other\": \"Nein, ich will etwas anderes teilen\",\n\t\"done\": \"Fertig!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Jetzt ist der Bildschirm auf dem anderen Gerät sichtbar.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Verbundene Geräte können über den Menüpunkt Verbundene Geräte in der oberen Leiste verwaltet werden.\",\n\t\"connect-new-device\": \"Neues Gerät verbinden\",\n\t\"select-entire-screen-to-share\": \"Gesamten Bildschirm zum Teilen auswählen\",\n\t\"select-app-window-to-share\": \"Anwendung oder Bildschirm zum Teilen auswählen\",\n\t\"refresh\": \"Neu laden\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Alle Geräte trennen\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Sicher, dass alle verbundenen Anzeigegeräte getrennt werden sollen?\",\n\t\"this-step-can-not-be-undone\": \"Dieser Schritt kann nicht rückgängig gemacht werden\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Alle Geräte müssen manuell neu verbunden werden\",\n\t\"no-cancel\": \"Nein, Abbrechen\",\n\t\"yes-disconnect-all\": \"Ja, Alle trennen\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Eine neue Version von Deskreen CE ist verfügbar! Klicken zum Herunterladen der neuen Version\",\n\t\"security\": \"Sicherheit\",\n\t\"general\": \"Allgemein\",\n\t\"about\": \"Über\",\n\t\"website\": \"Website\",\n\t\"about-deskreen\": \"Über Deskreen CE\",\n\t\"security-settings\": \"Sicherheitseinstellungen\",\n\t\"color-theme\": \"Farbschema\",\n\t\"automatic-updates\": \"Automatische Updates\",\n\t\"general-settings\": \"Allgemeine Einstellungen\",\n\t\"disabled\": \"Deaktiviert\",\n\t\"version\": \"Version\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Bearbeiten\",\n\t\"hide-deskreen\": \"Deskreen CE ausblenden\",\n\t\"hide-others\": \"Andere ausblenden\",\n\t\"show-all\": \"Alle Anzeigen\",\n\t\"quit\": \"Beenden\",\n\t\"undo\": \"Rückgängig\",\n\t\"redo\": \"Wiederherstellen\",\n\t\"cut\": \"Ausschneiden\",\n\t\"copy\": \"Kopieren\",\n\t\"paste\": \"Einfügen\",\n\t\"select-all\": \"Alle Auswählen\",\n\t\"view\": \"Ansicht\",\n\t\"reload\": \"Neu laden\",\n\t\"toggle-full-screen\": \"Vollbild umschalten\",\n\t\"toggle-developer-tools\": \"Developer Tools ein-/ausblenden\",\n\t\"window\": \"Fenster\",\n\t\"minimize\": \"Minimieren\",\n\t\"close\": \"Schließen\",\n\t\"bring-all-to-front\": \"Alle in den Vordergrund holen\",\n\t\"help\": \"Hilfe\",\n\t\"learn-more\": \"Mehr Erfahren\",\n\t\"documentation\": \"Dokumentation\",\n\t\"community-discussions\": \"Community Diskussionen\",\n\t\"search-issues\": \"Probleme durchsuchen\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Verfügbare vertraute Geräte\",\n\t\"make-this-device-trusted\": \"Diesem Gerät vertrauen\",\n\t\"click-to-select-other-screen-source-to-share\": \"Klicken um eine andere Quelle zu teilen\",\n\t\"click-to-edit-device-alias\": \"Klicken um Geräte-Alias zu ändern\",\n\t\"trusted-device-id\": \"Vertraute Geräte-ID\",\n\t\"trusted\": \"Vertraut\",\n\t\"make-trusted\": \"Vertrauen\",\n\t\"forget-this-device\": \"Dieses Gerät vergessen\",\n\t\"device-alias\": \"Geräte-Alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Automatisch letzen Bildschirm teilen, wenn das Gerät verfügbar ist\",\n\t\"all-devices-are-successfully-disconnected\": \"Alle Geräte wurden erfolgreich getrennt\",\n\t\"device-was-disconnected\": \"Gerät wurde getrennt\",\n\t\"networking\": \"Netzwerk\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE Anwendungsport\",\n\t\"port-is-already-used-by-other-app\": \"Port wird bereits von einer anderen Anwendung verwendet\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Klicken um den Deskreen CE Anwendungsport zu ändern\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Eine Zahl zwischen 3000 und 64000 eingeben, um sie als Deskreen CE Anwendungsport zu verwenden\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Eine weitere Zahl zwischen 3000 und 64000 eingeben\",\n\t\"select-network-interface\": \"Netzwerkadapter auswählen\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Ich kenne die IP meines Computer und möchte sie manuell eingeben\",\n\t\"type-your-computer-ip\": \"Die IP deines Computers\",\n\t\"click-to-type-ip-manually\": \"Klicken um die IP manuell einzugeben\",\n\t\"banned-ips\": \"Gesperrte IPs\",\n\t\"ban-new-ip\": \"Neue IP sperren\",\n\t\"type-the-ip-you-want-to-ban\": \"IP eingeben, die gesperrt werden soll\",\n\t\"unban-this-ip\": \"Diese IP entsperren\",\n\t\"unban-all-ips\": \"Alle IPs entsperren\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Deskreen CE Einstellungen auf Standard zurücksetzen\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Nutzer nach einem Passwort beim verbinden fragen\",\n\t\"change-password\": \"Passwört ändern\",\n\t\"type-a-new-password\": \"Neues Passwort eingeben\",\n\t\"cancel\": \"Abbrechen\",\n\t\"device-status\": \"Gerätestatus\",\n\t\"sharing-screen\": \"Bildschirm wird geteilt\",\n\t\"available-no-screen-sharing\": \"Verfügbar, Bildschirm wird nicht geteilt\",\n\t\"not-available\": \"Nicht verfügbar\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Deskreen CE beim Anmelden automatisch starten\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Deskreen CE-Fenster beim Anmelden automatisch öffnen\",\n\t\"use-system-tray\": \"Systemleiste benutzen\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE Systemleiste\",\n\t\"open-app-window\": \"Anwendungsfenster öfnnen\",\n\t\"minimize-to-tray\": \"In die Systemleiste minimieren\",\n\t\"show-connected-devices\": \"Verbundene Geräte anzeigen\",\n\t\"quit-deskreen-ce\": \"Deskreen CE beenden\",\n\t\"fix-reset\": \"Reparieren & Zurücksetzen\",\n\t\"fix-reset-tooltip\": \"Reparieren & Zurücksetzen. Probleme beim Verbinden neuer Clients? Ein Klick auf diese Schaltfläche kann helfen.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Ein Anzeige-Client ist bereits verbunden.\",\n\t\"viewing-client-connected-label\": \"Anzeige-Client verbunden\",\n\t\"connection-limit-reached-tooltip\": \"Verbindungslimit erreicht. Trenne den aktuell verbundenen Anzeige-Client (oder warte, bis er sich trennt), um einen neuen zu verbinden.\",\n\t\"scan-the-qr-code-to-connect\": \"QR-Code scannen, um eine Verbindung herzustellen\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Gib folgende Adresse in die Adressleiste eines beliebigen Geräts ein\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE erlaubt nur ein gleichzeitig verbundenes Anzeigegerät.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Die Option zum Verbinden von mehr als einem Gerät ist nur in der Pro-Version verfügbar.\"\n}\n"
  },
  {
    "path": "src/common/locales/en/translation.json",
    "content": "{\n\t\"hello\": \"Hello\",\n\t\"continue\": \"Continue\",\n\t\"language\": \"Language\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"nl\": \"Nederlands\",\n\t\"fr\": \"Français\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Donate\",\n\t\"get-deskreen-pro\": \"Get Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Get Deskreen Pro - opens the download page.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Click to visit our website\",\n\t\"connected-devices\": \"Connected Devices\",\n\t\"tutorial\": \"Tutorial\",\n\t\"settings\": \"Settings\",\n\t\"connect\": \"Connect\",\n\t\"select\": \"Select\",\n\t\"confirm\": \"Confirm\",\n\t\"scan-the-qr-code\": \"Scan the QR code\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Make sure your computer and screen viewing device are connected to same Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Or type the following address in browser address bar on any device\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Someone is trying to connect, do you allow?\",\n\t\"click-to-make-bigger\": \"Click to make bigger\",\n\t\"click-to-copy\": \"Click to copy\",\n\t\"partner-device-info\": \"Partner Device Info\",\n\t\"device-type\": \"Device Type\",\n\t\"device-ip\": \"Device IP\",\n\t\"device-browser\": \"Device Browser\",\n\t\"device-os\": \"Device OS\",\n\t\"session-id\": \"Session ID\",\n\t\"allow\": \"Allow\",\n\t\"deny\": \"Deny\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Device is successfully disconnected by you. You can connect a new device.\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE Update is Available!\",\n\t\"your-current-version-is\": \"Your current version is\",\n\t\"click-to-download-new-updated-version\": \"Click to download new updated version\",\n\t\"new-version-available\": \"New version available!\",\n\t\"connected\": \"Connected\",\n\t\"click-to-see-more\": \"Click to see more\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"This should match with Device IP displayed on the screen of device that is trying to connect.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"If IP addresses don't match click Disconnect button to prevent unauthorized access to your computer screen.\",\n\t\"disconnect\": \"Disconnect\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Choose Entire Screen or App window you want to share\",\n\t\"entire-screen\": \"Entire Screen\",\n\t\"application-window\": \"Application Window\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Check if all is OK and click Confirm\",\n\t\"confirm-button-text\": \"Confirm\",\n\t\"no-i-need-to-choose-other\": \"No, I need to share other thing\",\n\t\"done\": \"Done!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Now you can see your screen on other device.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"You can manage connected devices by clicking Connected Devices button in top panel.\",\n\t\"connect-new-device\": \"Connect New Device\",\n\t\"select-entire-screen-to-share\": \"Select Entire Screen to Share\",\n\t\"select-app-window-to-share\": \"Select App Window to Share\",\n\t\"refresh\": \"Refresh\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Disconnect all devices\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Are you sure you want to disconnect all connected viewing devices?\",\n\t\"this-step-can-not-be-undone\": \"This step can not be undone\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"You will have to connect all devices manually again\",\n\t\"no-cancel\": \"No, Cancel\",\n\t\"yes-disconnect-all\": \"Yes, Disconnect All\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"A new version of Deskreen CE is available! Click to download new version\",\n\t\"security\": \"Security\",\n\t\"general\": \"General\",\n\t\"about\": \"About\",\n\t\"website\": \"Website\",\n\t\"about-deskreen\": \"About Deskreen CE\",\n\t\"security-settings\": \"Security Settings\",\n\t\"color-theme\": \"Color Theme\",\n\t\"automatic-updates\": \"Automatic Updates\",\n\t\"general-settings\": \"General Settings\",\n\t\"disabled\": \"Disabled\",\n\t\"version\": \"Version\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Edit\",\n\t\"hide-deskreen\": \"Hide Deskreen CE\",\n\t\"hide-others\": \"Hide Others\",\n\t\"show-all\": \"Show All\",\n\t\"quit\": \"Quit\",\n\t\"undo\": \"Undo\",\n\t\"redo\": \"Redo\",\n\t\"cut\": \"Cut\",\n\t\"copy\": \"Copy\",\n\t\"paste\": \"Paste\",\n\t\"select-all\": \"Select All\",\n\t\"view\": \"View\",\n\t\"reload\": \"Reload\",\n\t\"toggle-full-screen\": \"Toggle Full Screen\",\n\t\"toggle-developer-tools\": \"Toggle Developer Tools\",\n\t\"window\": \"Window\",\n\t\"minimize\": \"Minimize\",\n\t\"close\": \"Close\",\n\t\"bring-all-to-front\": \"Bring All to Front\",\n\t\"help\": \"Help\",\n\t\"learn-more\": \"Learn More\",\n\t\"documentation\": \"Documentation\",\n\t\"community-discussions\": \"Community Discussions\",\n\t\"search-issues\": \"Search Issues\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Available Trusted Devices\",\n\t\"make-this-device-trusted\": \"Make this device trusted\",\n\t\"click-to-select-other-screen-source-to-share\": \"Click to select other screen source to share\",\n\t\"click-to-edit-device-alias\": \"Click to edit Device Alias\",\n\t\"trusted-device-id\": \"Trusted Device ID\",\n\t\"trusted\": \"Trusted\",\n\t\"make-trusted\": \"Make Trusted\",\n\t\"forget-this-device\": \"Forget This Device\",\n\t\"device-alias\": \"Device Alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Auto share last Entire Screen source when device is available\",\n\t\"all-devices-are-successfully-disconnected\": \"All devices are successfully disconnected\",\n\t\"device-was-disconnected\": \"Device was disconnected\",\n\t\"networking\": \"Networking\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE Application Port\",\n\t\"port-is-already-used-by-other-app\": \"Port is already used by other App\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Click to change Deskreen CE Application Port\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Type a number from 3000 to 64000 to use as a Deskreen CE Application Port\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Type another number in range from 3000 to 64000\",\n\t\"select-network-interface\": \"Select Network Interface\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"I know IP of my computer and I want to type it manually\",\n\t\"type-your-computer-ip\": \"Type Your Computer IP\",\n\t\"click-to-type-ip-manually\": \"Click to type IP manually\",\n\t\"banned-ips\": \"Banned IPs\",\n\t\"ban-new-ip\": \"Ban New IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Type the IP you want to ban\",\n\t\"unban-this-ip\": \"Unban this IP\",\n\t\"unban-all-ips\": \"Unban all IPs\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Reset Deskreen CE settings to default\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Ask user to enter password when connecting\",\n\t\"change-password\": \"Change Password\",\n\t\"type-a-new-password\": \"Type a New Password\",\n\t\"cancel\": \"Cancel\",\n\t\"device-status\": \"Device Status\",\n\t\"sharing-screen\": \"Sharing Screen\",\n\t\"available-no-screen-sharing\": \"Available, no screen sharing\",\n\t\"not-available\": \"Not Available\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Autostart Deskreen CE App on login\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Open Deskreen CE App window on login\",\n\t\"use-system-tray\": \"Use system tray\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE System Tray\",\n\t\"open-app-window\": \"Open App Window\",\n\t\"minimize-to-tray\": \"Minimize To Tray\",\n\t\"show-connected-devices\": \"Show Connected Devices\",\n\t\"quit-deskreen-ce\": \"Quit Deskreen CE\",\n\t\"glory-to-ukraine-glory-to-ukrainian-heroes\": \"GLORY TO UKRAINE! GLORY TO UKRAINIAN HEROES!\",\n\t\"share-same-screen-to-all-devices\": \"Share same screen to all devices\",\n\t\"device-connection-id\": \"Device Connection ID\",\n\t\"fix-reset\": \"Fix & Reset\",\n\t\"fix-reset-tooltip\": \"Fix & Reset. Having troubles connecting new clients ? Clicking this button may help you.\",\n\t\"allow-connection-to-all-devices\": \"Allow connection to all devices\",\n\t\"share-same-screen-to-all-client-devices\": \"Share same screen to all client devices\",\n\t\"no-wifi-and-lan-connection\": \"No WiFi and LAN connection.\",\n\t\"deskreen-ce-works-only-with-wifi-and-lan-networks\": \"Deskreen CE works only with WiFi and LAN networks.\",\n\t\"waiting-for-connection\": \"Waiting for connection.\",\n\t\"currently-sharing-to-all\": \"Currently sharing to all\",\n\t\"click-to-select-another-source-to-share\": \"Click to select another source to share\",\n\t\"click-to-select-what-new-clients-will-see\": \"Click to select what new clients will see\",\n\t\"screen-to-share-selected\": \"Screen to Share Selected\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Enter the following address in browser address bar on any device\",\n\t\"scan-the-qr-code-to-connect\": \"Scan the QR code to connect\",\n\t\"or\": \"Or\",\n\t\"go-back\": \"Go Back\",\n\t\"this-screen-source-will-be-seen-by-the-client\": \"This Screen Source will be seen by the client\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"One viewing client is connected already.\",\n\t\"viewing-client-connected-label\": \"Viewing client connected\",\n\t\"connection-limit-reached-tooltip\": \"Connection limit reached. Disconnect the current viewing client (or wait for it to disconnect) to connect a new one.\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE allows only one client viewing device connected at the same time.\",\n\t\"this-will-be-available-only-in-pro-version\": \"More than one devices connection option will be available only in Pro version.\"\n}\n"
  },
  {
    "path": "src/common/locales/es/translation.json",
    "content": "{\n\t\"hello\": \"Hola\",\n\t\"continue\": \"Continuar\",\n\t\"language\": \"Idioma\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Donar\",\n\t\"get-deskreen-pro\": \"Obtener Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtener Deskreen Pro - abre la página de descarga.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Clic para visitar nuestro sitio web\",\n\t\"connected-devices\": \"Dispositivos conectados\",\n\t\"tutorial\": \"Tutorial\",\n\t\"settings\": \"Configuraciones\",\n\t\"connect\": \"Connectar\",\n\t\"select\": \"Seleccionar\",\n\t\"confirm\": \"Confirmar\",\n\t\"scan-the-qr-code\": \"Escanea el código QR\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Asegúrate de que tu computadora y tu dispositivo de visualización de pantalla estén conectados a la misma red Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"O escribe la siguiente dirección en la barra de direcciones del navegador en cualquier dispositivo\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Alguien está intentando conectarse, ¿deseas permitirlo?\",\n\t\"click-to-make-bigger\": \"Clic para agrandar\",\n\t\"click-to-copy\": \"Clic para copiar\",\n\t\"partner-device-info\": \"Información del dispositivo asociado\",\n\t\"device-type\": \"Tipo del dispositivo\",\n\t\"device-ip\": \"IP del dispositivo\",\n\t\"device-browser\": \"Navegador del dispositivo\",\n\t\"device-os\": \"SO del dispositivo\",\n\t\"session-id\": \"ID de sesión\",\n\t\"allow\": \"Permitir\",\n\t\"deny\": \"Denegar\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Desconectaste correctamente el dispositivo. Puedes conectar un nuevo dispositivo.\",\n\t\"deskreen-ce-update-is-available\": \"¡Actualización de Deskreen CE disponible!\",\n\t\"your-current-version-is\": \"Tu versión actual es\",\n\t\"click-to-download-new-updated-version\": \"Clic para descargar la versión actualizada\",\n\t\"new-version-available\": \"¡Nueva versión disponible!\",\n\t\"connected\": \"Conectado\",\n\t\"click-to-see-more\": \"Clic para ver más\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Esto debería coincidir con la IP del dispositivo que se muestra en la pantalla del dispositivo que está intentando conectarse.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Si las direcciones IP no coinciden, haz clic en el botón Desconectar para evitar el acceso no autorizado a la pantalla de tu computadora.\",\n\t\"disconnect\": \"Desconectar\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Selecciona pantalla completa o la ventana de la aplicación que deseas compartir\",\n\t\"or\": \"O\",\n\t\"entire-screen\": \"Pantalla completa\",\n\t\"application-window\": \"Ventana de aplicación\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Comprueba si todo está bien y haz clic en Confirmar\",\n\t\"confirm-button-text\": \"Confirmar\",\n\t\"no-i-need-to-choose-other\": \"No, necesito compartir otra cosa\",\n\t\"done\": \"¡Hecho!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Ahora puedes ver tu pantalla en otro dispositivo.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Puedes administrar los dispositivos conectados haciendo clic en el botón Dispositivos conectados, en el panel superior.\",\n\t\"connect-new-device\": \"Connectar un nuevo dispositivo\",\n\t\"select-entire-screen-to-share\": \"Seleccionar Pantalla completa para compartir\",\n\t\"select-app-window-to-share\": \"Seleccionar Ventana de aplicación para compartir\",\n\t\"refresh\": \"Actualizar\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Desconectar todos los dispositivos\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"¿Estás seguro que quieres desconectar todos los dispositivos de visualización conectados?\",\n\t\"this-step-can-not-be-undone\": \"Este paso no se puede revertir\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Tendrás que volver a conectar manualmente todos los dispositivos\",\n\t\"no-cancel\": \"No, Cancelar\",\n\t\"yes-disconnect-all\": \"Sí, desconectar todo\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"¡Hay disponible una nueva versión de Deskreen CE! Haz clic para descargar la nueva versión\",\n\t\"security\": \"Seguridad\",\n\t\"general\": \"General\",\n\t\"about\": \"Acerca de\",\n\t\"website\": \"Sitio web\",\n\t\"about-deskreen\": \"Acerca de Deskreen CE\",\n\t\"security-settings\": \"Configuraciones de seguridad\",\n\t\"color-theme\": \"Tema\",\n\t\"automatic-updates\": \"Actualizaciones automáticas\",\n\t\"general-settings\": \"Configuraciones generales\",\n\t\"disabled\": \"Deshabilitado\",\n\t\"version\": \"Versión\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Editar\",\n\t\"hide-deskreen\": \"Ocultar Deskreen CE\",\n\t\"hide-others\": \"Ocultar otros\",\n\t\"show-all\": \"Mostrar todos\",\n\t\"quit\": \"Salir\",\n\t\"undo\": \"Deshacer\",\n\t\"redo\": \"Rehacer\",\n\t\"cut\": \"Cortar\",\n\t\"copy\": \"Copiar\",\n\t\"paste\": \"Pegar\",\n\t\"select-all\": \"Seleccionar todo\",\n\t\"view\": \"Vista\",\n\t\"reload\": \"Recargar\",\n\t\"toggle-full-screen\": \"Alternar pantalla completa\",\n\t\"toggle-developer-tools\": \"Alternar herramientas para desarrolladores\",\n\t\"window\": \"Ventana\",\n\t\"minimize\": \"Minimizar\",\n\t\"close\": \"Cerrar\",\n\t\"bring-all-to-front\": \"Traer todo al frente\",\n\t\"help\": \"Ayuda\",\n\t\"learn-more\": \"Aprender más\",\n\t\"documentation\": \"Documentación\",\n\t\"community-discussions\": \"Discusiones de la comunidad\",\n\t\"search-issues\": \"Buscar problemas\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Dispositivos confiables disponibles\",\n\t\"make-this-device-trusted\": \"Hacer que este dispositivo sea de confianza\",\n\t\"click-to-select-other-screen-source-to-share\": \"Haz clic para seleccionar otra fuente de pantalla para compartir\",\n\t\"click-to-edit-device-alias\": \"Clic para editar alias de dispositivo\",\n\t\"trusted-device-id\": \"ID de dispositivo de confianza\",\n\t\"trusted\": \"De confianza\",\n\t\"make-trusted\": \"Hacer de confianza\",\n\t\"forget-this-device\": \"Olvidar este dispositivo\",\n\t\"device-alias\": \"Alias del dispositivo\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Compartir automáticamente la última fuente de pantalla completa cuando el dispositivo está disponible\",\n\t\"all-devices-are-successfully-disconnected\": \"Todos los dispositivos fueron desconectados exitosamente\",\n\t\"device-was-disconnected\": \"El dispositivo fue desconectado\",\n\t\"networking\": \"Redes\",\n\t\"deskreen-ce-application-port\": \"Puerto de la aplicación Deskreen CE\",\n\t\"port-is-already-used-by-other-app\": \"El puerto ya está siendo usado por otra aplicación\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Clic para cambiar el puerto de la aplicación de Deskreen CE\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Escribe un número del 3000 al 64000 para usarlo como puerto de aplicación de Deskreen CE\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Escribe otro número del 3000 al 64000\",\n\t\"select-network-interface\": \"Seleccionar interfaz de red\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Conozco la IP de mi computadora y quiero escribirla manualmente\",\n\t\"type-your-computer-ip\": \"Escribe la IP de tu computadora\",\n\t\"click-to-type-ip-manually\": \"Clic para escribir la IP manualmente\",\n\t\"banned-ips\": \"IPs prohibidas\",\n\t\"ban-new-ip\": \"Prohibir una nueva IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Escribe la IP que quieres prohibir\",\n\t\"unban-this-ip\": \"Permitir esta IP\",\n\t\"unban-all-ips\": \"Permitir todas las IPs\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Restablecer las configuraciones de Deskreen CE a los valores predeterminados\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Solicitar que el usuario ingrese la contraseña al conectarse\",\n\t\"change-password\": \"Modificar contraseña\",\n\t\"type-a-new-password\": \"Escribe una nueva contraseña\",\n\t\"cancel\": \"Cancelar\",\n\t\"device-status\": \"Estado del dispositivo\",\n\t\"sharing-screen\": \"Compartiendo pantalla\",\n\t\"available-no-screen-sharing\": \"Disponible, sin compartir pantalla\",\n\t\"not-available\": \"No disponible\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Iniciar automáticamente Deskreen CE al iniciar sesión\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Abrir Deskreen CE al iniciar sesión\",\n\t\"use-system-tray\": \"Usar la bandeja del sistema\",\n\t\"deskreen-ce-system-tray\": \"Bandeja del sistema Deskreen CE\",\n\t\"open-app-window\": \"Abrir ventana de la aplicación\",\n\t\"minimize-to-tray\": \"Minimizar a la bandeja\",\n\t\"show-connected-devices\": \"Mostrar dispositivos conectados\",\n\t\"quit-deskreen-ce\": \"Salir de Deskreen CE\",\n\t\"fix-reset\": \"Reparar y Restablecer\",\n\t\"fix-reset-tooltip\": \"Reparar y Restablecer. ¿Tienes problemas para conectar nuevos clientes? Hacer clic en este botón puede ayudarte.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Ya hay un dispositivo de visualización conectado.\",\n\t\"viewing-client-connected-label\": \"Dispositivo de visualización conectado\",\n\t\"connection-limit-reached-tooltip\": \"Límite de conexiones alcanzado. Desconecta al cliente de visualización actual (o espera a que se desconecte) para conectar uno nuevo.\",\n\t\"scan-the-qr-code-to-connect\": \"Escanea el código QR para conectarte\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Introduce la siguiente dirección en la barra del navegador en cualquier dispositivo\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE permite solo un dispositivo de visualización conectado al mismo tiempo.\",\n\t\"this-will-be-available-only-in-pro-version\": \"La opción de conexión de más de un dispositivo estará disponible solo en la versión Pro.\"\n}\n"
  },
  {
    "path": "src/common/locales/fi/translation.json",
    "content": "{\n\t\"hello\": \"Hei\",\n\t\"continue\": \"Jatkaa\",\n\t\"language\": \"Kieli\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Lahjoittaa\",\n\t\"get-deskreen-pro\": \"Hanki Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hanki Deskreen Pro - avaa lataussivun.\",\n\t\"if-you-like-deskreen-ce-consider-contributing-financially-deskreen-ce-is-open-source-your-donations-keep-us-motivated-to-make-deskreen-ce-even-better\": \"Jos pidät Deskreen CE, harkitse taloudellista osallistumista. Deskreen CE on avoimen lähdekoodin lähde. Lahjoituksesi motivoivat meitä tekemään Deskreen CE entistä paremman.\",\n\t\"click-to-visit-our-website\": \"Napsauta vieraillaksesi verkkosivuillamme\",\n\t\"connected-devices\": \"Yhdistetyt laitteet\",\n\t\"tutorial\": \"Opetusohjelma\",\n\t\"settings\": \"Asetukset\",\n\t\"connect\": \"Kytkeä\",\n\t\"select\": \"Valitse\",\n\t\"confirm\": \"Vahvistaa\",\n\t\"scan-the-qr-code\": \"Skannaa QR-koodi\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Varmista, että tietokoneesi ja näytön katselulaite on yhdistetty samaan Wi-Fi-verkkoon\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Tai kirjoita seuraava osoite minkä tahansa laitteen selaimen osoiteriville\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Joku yrittää muodostaa yhteyden, sallitko?\",\n\t\"click-to-make-bigger\": \"Klikkaa suuremmaksi\",\n\t\"click-to-copy\": \"Napsauta kopioidaksesi\",\n\t\"partner-device-info\": \"Yhteistyökumppanin laitetiedot\",\n\t\"device-type\": \"Laitetyyppi\",\n\t\"device-ip\": \"Laitteen IP\",\n\t\"device-browser\": \"Laitteen selain\",\n\t\"device-os\": \"Laitteen OS\",\n\t\"session-id\": \"Istunnon tunniste\",\n\t\"allow\": \"Sallia\",\n\t\"deny\": \"Kiellä\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Katkaisit laitteen yhteyden onnistuneesti. Voit liittää uuden laitteen.\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE-päivitys on saatavilla!\",\n\t\"your-current-version-is\": \"Nykyinen versiosi on\",\n\t\"click-to-download-new-updated-version\": \"Lataa uusi päivitetty versio napsauttamalla\",\n\t\"new-version-available\": \"Uusi versio saatavilla!\",\n\t\"connected\": \"Yhdistetty\",\n\t\"click-to-see-more\": \"Klikkaa nähdäksesi lisää\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Tämän pitäisi vastata laitteen IP-osoitetta, joka näkyy sen laitteen näytöllä, joka yrittää muodostaa yhteyden.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Jos IP-osoitteet eivät täsmää, napsauta Katkaise yhteys -painiketta estääksesi luvattoman pääsyn tietokoneesi näyttöön.\",\n\t\"disconnect\": \"Katkaista\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Valitse koko näyttö tai sovellusikkuna, jonka haluat jakaa\",\n\t\"or\": \"TAI\",\n\t\"entire-screen\": \"Koko näyttö\",\n\t\"application-window\": \"Sovellusikkuna\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Tarkista, onko kaikki kunnossa ja napsauta Vahvista\",\n\t\"confirm-button-text\": \"Vahvistaa\",\n\t\"no-i-need-to-choose-other\": \"Ei, minun täytyy jakaa jotain muuta\",\n\t\"done\": \"Tehty!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Nyt näet näytön toisella laitteella.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Voit hallita yhdistettyjä laitteita napsauttamalla Yhdistetyt laitteet -painiketta yläpaneelissa.\",\n\t\"connect-new-device\": \"Yhdistä uusi laite\",\n\t\"select-entire-screen-to-share\": \"Valitse koko näyttö jaettavaksi\",\n\t\"select-app-window-to-share\": \"Valitse jaettava sovellusikkuna\",\n\t\"refresh\": \"Virkistää\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Irrota kaikki laitteet\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Haluatko varmasti katkaista kaikki kytketyt katselulaitteet?\",\n\t\"this-step-can-not-be-undone\": \"Tätä vaihetta ei voi peruuttaa\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Sinun on liitettävä kaikki laitteet manuaalisesti uudelleen\",\n\t\"no-cancel\": \"Ei, peruuta\",\n\t\"yes-disconnect-all\": \"Kyllä, irrota kaikki\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Uusi versio Deskreen-CEistä on saatavilla! Lataa uusi versio napsauttamalla\",\n\t\"security\": \"Turvallisuus\",\n\t\"general\": \"Kenraali\",\n\t\"about\": \"Noin\",\n\t\"website\": \"Verkkosivusto\",\n\t\"about-deskreen\": \"Tietoja Deskreen CE\",\n\t\"security-settings\": \"Turvallisuusasetukset\",\n\t\"color-theme\": \"Väriteema\",\n\t\"automatic-updates\": \"Automaattiset päivitykset\",\n\t\"general-settings\": \"Yleiset asetukset\",\n\t\"disabled\": \"Liikuntarajoitteinen\",\n\t\"version\": \"Versio\",\n\t\"copyright\": \"Tekijänoikeus\",\n\t\"edit\": \"Muokata\",\n\t\"hide-deskreen\": \"Piilota Deskreen CE\",\n\t\"hide-others\": \"Piilota muut\",\n\t\"show-all\": \"Näytä kaikki\",\n\t\"quit\": \"Lopettaa\",\n\t\"undo\": \"Kumoa\",\n\t\"redo\": \"Toista\",\n\t\"cut\": \"Leikata\",\n\t\"copy\": \"Kopio\",\n\t\"paste\": \"Liitä\",\n\t\"select-all\": \"Valitse kaikki\",\n\t\"view\": \"Näytä\",\n\t\"reload\": \"Lataa uudelleen\",\n\t\"toggle-full-screen\": \"Koko näyttö päälle/pois\",\n\t\"toggle-developer-tools\": \"Vaihda kehittäjätyökalut\",\n\t\"window\": \"Ikkuna\",\n\t\"minimize\": \"Minimoida\",\n\t\"close\": \"Kiinni\",\n\t\"bring-all-to-front\": \"Tuo kaikki eteen\",\n\t\"help\": \"Auta\",\n\t\"learn-more\": \"Lisätietoja\",\n\t\"documentation\": \"Dokumentointi\",\n\t\"community-discussions\": \"Yhteisön keskustelut\",\n\t\"search-issues\": \"Hakuongelmat\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Saatavilla olevat luotettavat laitteet\",\n\t\"make-this-device-trusted\": \"Tee tästä laitteesta luotettava\",\n\t\"click-to-select-other-screen-source-to-share\": \"Valitse toinen näyttölähde jaettavaksi napsauttamalla\",\n\t\"click-to-edit-device-alias\": \"Napsauta muokataksesi laitteen aliasta\",\n\t\"trusted-device-id\": \"Luotetun laitteen tunnus\",\n\t\"trusted\": \"Luotettu\",\n\t\"make-trusted\": \"Luota\",\n\t\"forget-this-device\": \"Unohda tämä laite\",\n\t\"device-alias\": \"Laitteen alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Jaa automaattisesti viimeinen koko näytön lähde, kun laite on saatavilla\",\n\t\"all-devices-are-successfully-disconnected\": \"Kaikki laitteet on irrotettu onnistuneesti\",\n\t\"device-was-disconnected\": \"Laite irrotettiin\",\n\t\"networking\": \"Verkostoituminen\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE-sovellusportti\",\n\t\"port-is-already-used-by-other-app\": \"Portti on jo toisen sovelluksen käytössä\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Napsauta muuttaaksesi Deskreen CE-sovellusporttia\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Kirjoita luku väliltä 3000 - 64000 käytettäväksi Deskreen CE-sovellusporttina\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Kirjoita toinen luku väliltä 3000–64000\",\n\t\"select-network-interface\": \"Valitse verkkoliitäntä\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Tiedän tietokoneeni IP-osoitteen ja haluan kirjoittaa sen manuaalisesti\",\n\t\"type-your-computer-ip\": \"Kirjoita tietokoneesi IP\",\n\t\"click-to-type-ip-manually\": \"Napsauta kirjoittaaksesi IP manuaalisesti\",\n\t\"banned-ips\": \"Kielletyt IP-osoitteet\",\n\t\"ban-new-ip\": \"Kiellä uusi IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Kirjoita IP, jonka haluat estää\",\n\t\"unban-this-ip\": \"Poista tämän IP-osoitteen esto\",\n\t\"unban-all-ips\": \"Poista kaikkien IP-osoitteiden esto\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Palauta Deskreen-CEin asetukset oletusasetuksiin\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Pyydä käyttäjää antamaan salasana yhteyden muodostamisen yhteydessä\",\n\t\"change-password\": \"Vaihda salasana\",\n\t\"type-a-new-password\": \"Kirjoita uusi salasana\",\n\t\"cancel\": \"Peruuttaa\",\n\t\"device-status\": \"Laitteen tila\",\n\t\"sharing-screen\": \"Jakamisnäyttö\",\n\t\"available-no-screen-sharing\": \"Saatavilla, ei näytön jakamista\",\n\t\"not-available\": \"Ei saatavilla\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Autostart Deskreen CE App kirjautumisen yhteydessä\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Avaa Deskreen CE App -ikkuna kirjautumisen yhteydessä\",\n\t\"use-system-tray\": \"Käytä ilmaisinaluetta\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE Järjestelmälokero\",\n\t\"open-app-window\": \"Avaa sovellusikkuna\",\n\t\"minimize-to-tray\": \"Pienennä lokeroon\",\n\t\"show-connected-devices\": \"Näytä yhdistetyt laitteet\",\n\t\"quit-deskreen-ce\": \"Lopeta Deskreen CE\",\n\t\"glory-to-ukraine-glory-to-ukrainian-heroes\": \"GLORY TO UKRAINE! GLORY TO UKRAINIAN HEROES!\",\n\t\"fix-reset\": \"Korjaa & Nollaa\",\n\t\"fix-reset-tooltip\": \"Korjaa & Nollaa. Onko sinulla ongelmia uusien asiakkaiden yhdistämisessä? Tämän painikkeen klikkaaminen voi auttaa.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Yksi katselulaite on jo yhdistetty.\",\n\t\"viewing-client-connected-label\": \"Katselulaite yhdistetty\",\n\t\"connection-limit-reached-tooltip\": \"Yhteysraja on saavutettu. Katkaise nykyisen katseluasiakkaan yhteys (tai odota, että se katkeaa), jotta voit yhdistää uuden.\",\n\t\"scan-the-qr-code-to-connect\": \"Skannaa QR-koodi yhdistääksesi\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Kirjoita seuraava osoite minkä tahansa laitteen selaimen osoiteriville\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE sallii vain yhden katselulaitteen yhteyden kerrallaan.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Useamman kuin yhden laitteen liittämisvaihtoehto on saatavilla vain Pro-versiossa.\"\n}\n"
  },
  {
    "path": "src/common/locales/fr/translation.json",
    "content": "{\n\t\"hello\": \"Bonjour\",\n\t\"continue\": \"Continuer\",\n\t\"language\": \"Langage\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fr\": \"Français\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Donation\",\n\t\"get-deskreen-pro\": \"Obtenir Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Obtenir Deskreen Pro - ouvre la page de téléchargement.\",\n\t\"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 .\",\n\t\"click-to-visit-our-website\": \"Cliquez ici pour visiter notre site web\",\n\t\"connected-devices\": \"Appareils connectés\",\n\t\"tutorial\": \"Tutoriel\",\n\t\"settings\": \"Paramètres\",\n\t\"connect\": \"Connecter\",\n\t\"select\": \"Selectionner\",\n\t\"confirm\": \"Confirmer\",\n\t\"scan-the-qr-code\": \"Scanner le QR code\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Assurez vous que votre ordinateur et votre appareil de visionnage sont connectés au même réseau Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Ou taper l'adresse suivante dans votre bar d'adresse de navigateur ou votre appareil\",\n\t\"someone-is-trying-to-connect,-do-you-allow?\": \"Quelqu'un essaye de se connecter, autoriser ?\",\n\t\"click-to-make-bigger\": \"Cliquez pour agrandir\",\n\t\"click-to-copy\": \"Cliquez pour copier\",\n\t\"partner-device-info\": \"Informations sur l'appareil du partenaire\",\n\t\"device-type\": \"Type d'appareil\",\n\t\"device-ip\": \"IP de l'appareil\",\n\t\"device-browser\": \"Navigateur de l'appareil\",\n\t\"device-os\": \"OS de l'appareil\",\n\t\"session-id\": \"ID de la session\",\n\t\"allow\": \"Autoriser\",\n\t\"deny\": \"Refuser\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Déconnexion de l'appareil effectuée avec succès. Vous pouvez connecter un nouvel appareil\",\n\t\"deskreen-ce-update-is-available\": \"Une mise à jour de Deskreen CE est disponible!\",\n\t\"your-current-version-is\": \"Votre version actuelle est\",\n\t\"click-to-download-new-updated-version\": \"Cliquer ici pour télécharger la nouvelle version\",\n\t\"new-version-available\": \"Nouvelle version disponible !\",\n\t\"connected\": \"Connecter\",\n\t\"click-to-see-more\": \"Cliquez pour en voir plus\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Cela doit correspondre avec l'IP de l'appareil affichée sur l'écran de l'appareil sur lequel la connexion est tentée\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Si les adresses IP ne correspondent pas, cliquer sur le bouton Deconnecter.\",\n\t\"disconnect\": \"Deconnecter\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Choisissez l'écran entier ou la fenêtre d'application que vous souhaitez partager\",\n\t\"or\": \"OU\",\n\t\"entire-screen\": \"Écran entier\",\n\t\"application-window\": \"fenêtre d'application\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Vérifiez que tout est OK et cliquez sur confirmer\",\n\t\"confirm-button-text\": \"Confirmer\",\n\t\"no,-i-need-to-choose-other\": \"Non, je veux partager autre chose\",\n\t\"done!\": \"Fait!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Maintenant vous pouvez voir votre écran sur l'autre appareil\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Vous pouvez gérer vos appareil connectés en cliquant sur le bouton Appareil Connectés dans le panneau supérieur.\",\n\t\"connect-new-device\": \"Connecter un nouvel appareil\",\n\t\"select-entire-screen-to-share\": \"Selectionner un écran entier à partager\",\n\t\"select-app-window-to-share\": \"Selectionner une fenêtre d'application à partager\",\n\t\"refresh\": \"Rafraichir\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Déconnecter tous les appareils\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices?\": \"Etes vous sur de vouloir déconnecter tous les appareils de visionnages ?\",\n\t\"this-step-can-not-be-undone\": \"Cette action ne peut pas être annulée\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Vous devez à nouveau connecter tous les appareils manuellement\",\n\t\"no,-cancel\": \"Non, Annuler\",\n\t\"yes,-disconnect-all\": \"Oui, Tout déconnecter\",\n\t\"a-new-version-of-deskreen-ce-is-available!-click-to-download-new-version\": \"Une nouvelle version de Deskreen CE est disponible! Cliquez pour télécharger la nouvelle version\",\n\t\"security\": \"Sécurité\",\n\t\"general\": \"General\",\n\t\"about\": \"À-propos\",\n\t\"website\": \"Site web\",\n\t\"about-deskreen\": \"À-propos de Deskreen CE\",\n\t\"security-settings\": \"Paramètres de Sécurité\",\n\t\"color-theme\": \"Couleur du thème\",\n\t\"automatic-updates\": \"Mises à jour automatique\",\n\t\"general-settings\": \"Paramétres Généraux\",\n\t\"disabled\": \"Désactivé\",\n\t\"version\": \"Version\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Éditer\",\n\t\"hide-deskreen\": \"Masquer Deskreen CE\",\n\t\"hide-others\": \"Masquer Autres\",\n\t\"show-all\": \"Montrer tout\",\n\t\"quit\": \"Quitter\",\n\t\"undo\": \"Revenir en arrière\",\n\t\"redo\": \"Rétablir\",\n\t\"cut\": \"Couper\",\n\t\"copy\": \"Copier\",\n\t\"paste\": \"Coller\",\n\t\"select-all\": \"Selectionner tout\",\n\t\"view\": \"Voir\",\n\t\"reload\": \"Recharger\",\n\t\"toggle-full-screen\": \"Basculer en plein écran\",\n\t\"toggle-developer-tools\": \"Affichier les outils développeur\",\n\t\"window\": \"Fenêtre\",\n\t\"minimize\": \"Minimiser\",\n\t\"close\": \"Fermer\",\n\t\"bring-all-to-front\": \"Mettre au premier plan\",\n\t\"help\": \"Aide\",\n\t\"learn-more\": \"En savoir plus\",\n\t\"documentation\": \"Documentation\",\n\t\"community-discussions\": \"Discussion de la communauté\",\n\t\"search-issues\": \"Cherchez des solutions\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Appareils approuvés disponibles\",\n\t\"make-this-device-trusted\": \"Approuver cet appareil\",\n\t\"click-to-select-other-screen-source-to-share\": \"Cliquez pour sélectionner un autre écran à partager\",\n\t\"click-to-edit-device-alias\": \"Cliquez pour changer l'alias de l'appareil\",\n\t\"trusted-device-id\": \"ID de l'appareil approuvé\",\n\t\"trusted\": \"Approuvé\",\n\t\"make-trusted\": \"Approuver\",\n\t\"forget-this-device\": \"Oublier cet appareil\",\n\t\"device-alias\": \"Alias de l'appareil\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Partager automatiquement le dernier écran complet quand l'appareil est disponible\",\n\t\"all-devices-are-successfully-disconnected\": \"Tous les appareils ont été déconnectés avec succés\",\n\t\"device-was-disconnected\": \"L'appareil à été déconnecté\",\n\t\"networking\": \"Mise en réseau\",\n\t\"deskreen-ce-application-port\": \"Port de l'application Deskreen CE\",\n\t\"port-is-already-used-by-other-app\": \"le port est déjà utilisé par une autre application\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Cliquez pour changer le port de l'application Deskreen CE\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Tapez un nombre entre 3000 et 64000 à utiliser comme port pour l'application Deskreen CE\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Tapez un autre nombre dans l'intervalle 3000 à 64000\",\n\t\"select-network-interface\": \"Selectionnez une interface réseau\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Je connais l'adress IP de mon ordinateur et je veux la rentrer manuellement\",\n\t\"type-your-computer-ip\": \"Tapez l'adresse IP de votre ordinateur\",\n\t\"click-to-type-ip-manually\": \"Cliquez pour taper l'adresse IP manuellement\",\n\t\"banned-ips\": \"IPs bannies\",\n\t\"ban-new-ip\": \"Bannir une nouvelle adresse IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Taper l'adresse IP que vous souhaitez bannir\",\n\t\"unban-this-ip\": \"Débannir l'adresse IP\",\n\t\"unban-all-ips\": \"Débannir toutes les adresses IP\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Restaurer les paramètres par défaut\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Demander à l'utilisateur de rentrer un mot de passe lors de la connexion\",\n\t\"change-password\": \"Changer le mot de passe\",\n\t\"type-a-new-password\": \"Tapez un nouveau mot de passe\",\n\t\"cancel\": \"Annuler\",\n\t\"device-status\": \"État de l'appareil\",\n\t\"sharing-screen\": \"Partage de l'écran\",\n\t\"available-no-screen-sharing\": \"Disponible, aucun écran partagé\",\n\t\"not-available\": \"Non disponible\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Lancer Deskreen CE automatiquement au démarrage\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Ouvrir Deskreen CE automatiquement au démérage\",\n\t\"use-system-tray\": \"Utiliser la zone de notifications\",\n\t\"deskreen-ce-system-tray\": \"Zone de notification Deskreen CE\",\n\t\"open-app-window\": \"Ouvrir la fenêtre\",\n\t\"minimize-to-tray\": \"Réduire dans la zone de notifications\",\n\t\"show-connected-devices\": \"Afficher les appareils connectés\",\n\t\"quit-deskreen-ce\": \"Quitter Deskreen CE\",\n\t\"fix-reset\": \"Réparer et Réinitialiser\",\n\t\"fix-reset-tooltip\": \"Réparer et Réinitialiser. Vous avez des difficultés à connecter de nouveaux clients ? Cliquer sur ce bouton peut vous aider.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Un client de visualisation est déjà connecté.\",\n\t\"viewing-client-connected-label\": \"Client de visualisation connecté\",\n\t\"connection-limit-reached-tooltip\": \"Limite de connexions atteinte. Déconnectez le client de visualisation actuel (ou attendez qu'il se déconnecte) pour en connecter un nouveau.\",\n\t\"scan-the-qr-code-to-connect\": \"Scannez le QR code pour vous connecter\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Saisissez l'adresse suivante dans la barre du navigateur sur n'importe quel appareil\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE ne permet qu'un seul appareil de visualisation connecté en même temps.\",\n\t\"this-will-be-available-only-in-pro-version\": \"L’option de connexion de plusieurs appareils ne sera disponible que dans la version Pro.\"\n}\n"
  },
  {
    "path": "src/common/locales/it/translation.json",
    "content": "{\n\t\"hello\": \"Ciao\",\n\t\"continue\": \"Continua\",\n\t\"language\": \"Lingua\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Dona\",\n\t\"get-deskreen-pro\": \"Ottieni Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ottieni Deskreen Pro - apre la pagina di download.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Clicca per visitare il nostro sito\",\n\t\"connected-devices\": \"Dispositivi Connessi\",\n\t\"tutorial\": \"Guide\",\n\t\"settings\": \"Impostazioni\",\n\t\"connect\": \"Connetti\",\n\t\"select\": \"Seleziona\",\n\t\"confirm\": \"Conferma\",\n\t\"scan-the-qr-code\": \"Scannerizza il codice QR\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Assicurati che il tuo computer e il dispositivo di visualizzazione siano connessi alla stessa rete Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"O inserisci l'indirizzo seguente nella barra degli indirizzi del browser di qualsiasi dispositivo\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Qualcuno sta tentando di connettersi, vuoi consentirlo?\",\n\t\"click-to-make-bigger\": \"Clicca per ingrandire\",\n\t\"click-to-copy\": \"Clicca per copiare\",\n\t\"partner-device-info\": \"Informazioni sul Dispositivo\",\n\t\"device-type\": \"Tipologia Dispositivo\",\n\t\"device-ip\": \"IP Dispositivo\",\n\t\"device-browser\": \"Browser Dispositivo\",\n\t\"device-os\": \"OS Dispositivo\",\n\t\"session-id\": \"ID Sessione\",\n\t\"allow\": \"Consenti\",\n\t\"deny\": \"Nega\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Dispositivo disconnesso correttamente da te. Puoi connetterti a un nuovo dispositivo.\",\n\t\"deskreen-ce-update-is-available\": \"È disponibile un Aggiornamento di Deskreen CE!\",\n\t\"your-current-version-is\": \"La tua versione corrente è\",\n\t\"click-to-download-new-updated-version\": \"Clicca per scaricare la versione aggiornata\",\n\t\"new-version-available\": \"Nuova versione disponibile!\",\n\t\"connected\": \"Connesso\",\n\t\"click-to-see-more\": \"Clicca per vedere di più\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Questo dovrebbe corrispondere all'IP mostrato sul Dispositivo che sta tentando di connettersi.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Se l'indirizzo IP non corrisponde, clicca il tasto Disconnetti per prevenire l'accesso allo schermo del tuo computer.\",\n\t\"disconnect\": \"Disconnetti\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Selezione Schermo Intero o l'applicazione che vuoi condividere\",\n\t\"or\": \"O\",\n\t\"entire-screen\": \"Schermo Intero\",\n\t\"application-window\": \"Finestra Applicazione\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Controlla che sia tutto OK e clicca Conferma\",\n\t\"confirm-button-text\": \"Conferma\",\n\t\"no-i-need-to-choose-other\": \"No, devo scegliere altro\",\n\t\"done\": \"Fatto!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Adesso puoi vedere il tuo schermo nell'altro dispositivo.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Puoi gestire i dispositivi connessi cliccando su Dispositivi Connessi in cima al pannello.\",\n\t\"connect-new-device\": \"Connetti nuovo Dispositivo\",\n\t\"select-entire-screen-to-share\": \"Seleziona Schermo Intero da Condividere\",\n\t\"select-app-window-to-share\": \"Seleziona Finestra Applicazione da Condividere\",\n\t\"refresh\": \"Ricarica\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Disconnetti tutti i Dispositivi\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Sei sicuro di voler disconnettere tutti i dispositivi connessi?\",\n\t\"this-step-can-not-be-undone\": \"Questo passaggio non è reversibile\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Dovrai nuovamente connettere tutti i Dispositivi manualmente\",\n\t\"no-cancel\": \"No, Annulla\",\n\t\"yes-disconnect-all\": \"Si, Disconnetti Tutto\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"È disponibile un nuova versione di Deskreen CE! Clicca per scaricarla\",\n\t\"security\": \"Sicurezza\",\n\t\"general\": \"Generali\",\n\t\"about\": \"Info\",\n\t\"website\": \"Sito\",\n\t\"about-deskreen\": \"Info su Deskreen CE\",\n\t\"security-settings\": \"Impostazioni sulla Sicurezza\",\n\t\"color-theme\": \"Tema Colore\",\n\t\"automatic-updates\": \"Aggiornamenti Automatici\",\n\t\"general-settings\": \"Impostazioni Generali\",\n\t\"disabled\": \"Disabilitato\",\n\t\"version\": \"Versione\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Modifica\",\n\t\"hide-deskreen\": \"Nascondi Deskreen CE\",\n\t\"hide-others\": \"Nascondi gli altri\",\n\t\"show-all\": \"Mostra Tutto\",\n\t\"quit\": \"Esci\",\n\t\"undo\": \"Annulla\",\n\t\"redo\": \"Rifai\",\n\t\"cut\": \"Taglia\",\n\t\"copy\": \"Copia\",\n\t\"paste\": \"Incolla\",\n\t\"select-all\": \"Seleziona Tutto\",\n\t\"view\": \"Visualizza\",\n\t\"reload\": \"Ricarica\",\n\t\"toggle-full-screen\": \"Attiva\\\\/disattiva Schermo Intero\",\n\t\"toggle-developer-tools\": \"Attiva\\\\/disattiva Strumenti Sviluppatore\",\n\t\"window\": \"Finestra\",\n\t\"minimize\": \"Minimizza\",\n\t\"close\": \"Chiudi\",\n\t\"bring-all-to-front\": \"Porta tutto in primo piano\",\n\t\"help\": \"Aiuto\",\n\t\"learn-more\": \"Scopri di più\",\n\t\"documentation\": \"Documentazione\",\n\t\"community-discussions\": \"Discussioni della comunità\",\n\t\"search-issues\": \"Problemi di ricerca\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Dispositivi Attendibili Disponibili\",\n\t\"make-this-device-trusted\": \"Rendi Attendibile questo Dispositivo\",\n\t\"click-to-select-other-screen-source-to-share\": \"Clicca per selezionare altre sorgenti da condividere\",\n\t\"click-to-edit-device-alias\": \"Clicca per modificare l'Alias del Dispositivo\",\n\t\"trusted-device-id\": \"ID del Dispositivo Attendibile\",\n\t\"trusted\": \"Attendibile\",\n\t\"make-trusted\": \"Rendi Attendibile\",\n\t\"forget-this-device\": \"Dimentica questo Dispositivo\",\n\t\"device-alias\": \"Alias del Dispositivo\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Condividi automaticamente lo Schermo Intero quando il dispositivo è disponibile\",\n\t\"all-devices-are-successfully-disconnected\": \"Tutti i Dispositivi sono stati Disconnessi correttamente\",\n\t\"device-was-disconnected\": \"Il dispositivo è stato Disconnesso\",\n\t\"networking\": \"Rete\",\n\t\"deskreen-ce-application-port\": \"Porta di Deskreen CE\",\n\t\"port-is-already-used-by-other-app\": \"Porta già utilizzata da un'altra applicazione\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Clicca per cambiare la porta di Deskreen CE\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Scrivi un numero tra 3000 e 64000 da utilizzare come porta per Deskreen CE\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Scrivi un altro numero tra 3000 e 64000\",\n\t\"select-network-interface\": \"Selezione un'interfaccia di Rete\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Conosco l'IP del mio computer e voglio scriverlo manualmente\",\n\t\"type-your-computer-ip\": \"Scrivi l'IP del tuo computer\",\n\t\"click-to-type-ip-manually\": \"Clicca per scrivere il tuo IP manualmente\",\n\t\"banned-ips\": \"IP Bannati\",\n\t\"ban-new-ip\": \"Banna un nuovo IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Scrivi l'IP che vuoi bannare\",\n\t\"unban-this-ip\": \"Sbanna questo IP\",\n\t\"unban-all-ips\": \"Sbanna tutti gli IP\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Reimposta Deskreen CE alle impostazioni predefinite\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Chiedi all'utente di inserire la password durante la connessione\",\n\t\"change-password\": \"Cambia Password\",\n\t\"type-a-new-password\": \"Inserisci un nuova Password\",\n\t\"cancel\": \"Annulla\",\n\t\"device-status\": \"Stato del Dispositivo\",\n\t\"sharing-screen\": \"Condivisione dello schermo\",\n\t\"available-no-screen-sharing\": \"Disponibile, nessuna condivisione dello schermo\",\n\t\"not-available\": \"Non Disponibile\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Avvia automaticamente Deskreen CE al login\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Apri la finestra di Deskreen CE al login\",\n\t\"use-system-tray\": \"Usa Tray di sistema\",\n\t\"deskreen-ce-system-tray\": \"Tray di Deskreen CE\",\n\t\"open-app-window\": \"Apri Finestra Applicazione\",\n\t\"minimize-to-tray\": \"Minimizza nella Tray\",\n\t\"show-connected-devices\": \"Mostra Dispositivi Connessi\",\n\t\"quit-deskreen-ce\": \"Esci da Deskreen CE\",\n\t\"fix-reset\": \"Ripara e Reimposta\",\n\t\"fix-reset-tooltip\": \"Ripara e Reimposta. Hai problemi a collegare nuovi client? Cliccare questo pulsante può aiutarti.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Un client di visualizzazione è già connesso.\",\n\t\"viewing-client-connected-label\": \"Client di visualizzazione connesso\",\n\t\"connection-limit-reached-tooltip\": \"Limite di connessioni raggiunto. Disconnetti il client di visualizzazione attuale (o attendi che si disconnetta) per collegarne uno nuovo.\",\n\t\"scan-the-qr-code-to-connect\": \"Scansiona il codice QR per connetterti\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Inserisci il seguente indirizzo nella barra del browser su qualsiasi dispositivo\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE consente solo un dispositivo di visualizzazione connesso alla volta.\",\n\t\"this-will-be-available-only-in-pro-version\": \"L'opzione di connessione di più dispositivi sarà disponibile solo nella versione Pro.\"\n}\n"
  },
  {
    "path": "src/common/locales/ja/translation.json",
    "content": "{\n\t\"hello\": \"こんにちは\",\n\t\"continue\": \"続ける\",\n\t\"language\": \"言語\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"寄付\",\n\t\"get-deskreen-pro\": \"Deskreen Pro を入手\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro を入手 - ダウンロードページを開きます。\",\n\t\"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をより良くするためのモチベーションが保つことができます。\",\n\t\"click-to-visit-our-website\": \"クリックするとウェブサイトが開きます。\",\n\t\"connected-devices\": \"接続されたデバイス\",\n\t\"tutorial\": \"チュートリアル\",\n\t\"settings\": \"設定\",\n\t\"connect\": \"接続\",\n\t\"select\": \"選択\",\n\t\"confirm\": \"確認\",\n\t\"scan-the-qr-code\": \"QRコードをスキャン\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"パソコンと画面表示デバイスが同じWi-Fiに接続されていることを確認してください。\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"または、各端末のブラウザのアドレスバーに以下のアドレスを入力してください。\",\n\t\"someone-is-trying-to-connect,-do-you-allow?\": \"誰かが接続しようとしています。許可しますか？\",\n\t\"click-to-make-bigger\": \"クリックで拡大\",\n\t\"click-to-copy\": \"クリックでコピー\",\n\t\"partner-device-info\": \"接続するデバイスの情報\",\n\t\"device-type\": \"デバイスの種類\",\n\t\"device-ip\": \"デバイスのIP\",\n\t\"device-browser\": \"デバイスのブラウザ\",\n\t\"device-os\": \"デバイスのOS\",\n\t\"session-id\": \"セッションID\",\n\t\"allow\": \"許可\",\n\t\"deny\": \"拒否\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"デバイスが正常に切断されました。新しいデバイスを接続することができます。\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen-CEのアップデートが利用可能です！\",\n\t\"your-current-version-is\": \"現在お使いのバージョン：\",\n\t\"click-to-download-new-updated-version\": \"クリックすると新しいアップデートされたバージョンをダウンロードできます。\",\n\t\"new-version-available\": \"新しいバージョンが利用可能です！\",\n\t\"connected\": \"接続されています\",\n\t\"click-to-see-more\": \"クリックして詳細を見る\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"この値は、接続しようとしているデバイスの画面に表示されているデバイスのIPと一致する必要があります。\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"IPアドレスが一致しない場合は、「切断」ボタンをクリックして、コンピュータの画面への不正アクセスを防止してください。\",\n\t\"disconnect\": \"切断する\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"共有したい画面全体またはアプリのウィンドウを選択する\",\n\t\"or\": \"or\",\n\t\"entire-screen\": \"画面全体\",\n\t\"application-window\": \"アプリケーションウィンドウ\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"すべて問題ないことを確認し、「確認」をクリックします。\",\n\t\"confirm-button-text\": \"確認\",\n\t\"no,-i-need-to-choose-other\": \"いいえ、その他を共有します\",\n\t\"done!\": \"完了!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"これで、接続されたデバイスで自分の画面を見ることができます。\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"トップパネルの「接続されたデバイス」をクリックして接続されたデバイスを管理することができます。\",\n\t\"connect-new-device\": \"新しいデバイスを接続\",\n\t\"select-entire-screen-to-share\": \"共有する画面を選択\",\n\t\"select-app-window-to-share\": \"共有するアプリのウィンドウを選択\",\n\t\"refresh\": \"リフレッシュ\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"すべてのデバイスを切断する\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices?\": \"接続されているすべてのデバイスを切断してよろしいですか？\",\n\t\"this-step-can-not-be-undone\": \"このステップは元に戻すことができません\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"再度、すべてのデバイスを手動で接続する必要があります\",\n\t\"no,-cancel\": \"キャンセル\",\n\t\"yes,-disconnect-all\": \"すべて切断する\",\n\t\"a-new-version-of-deskreen-ce-is-available!-click-to-download-new-version\": \"Deskreen-CEの新バージョンがリリースされました。クリックして新バージョンがダウンロードできます。\",\n\t\"security\": \"セキュリティ\",\n\t\"general\": \"一般\",\n\t\"about\": \"詳細\",\n\t\"website\": \"ウェブサイト\",\n\t\"about-deskreen\": \"Deskreen-CEについて\",\n\t\"security-settings\": \"セキュリティの設定\",\n\t\"color-theme\": \"カラーテーマ\",\n\t\"automatic-updates\": \"自動更新\",\n\t\"general-settings\": \"一般的な設定\",\n\t\"disabled\": \"使用不可\",\n\t\"version\": \"バージョン\",\n\t\"copyright\": \"コピーライト\",\n\t\"edit\": \"編集\",\n\t\"hide-deskreen\": \"Deskreen-CEを隠す\",\n\t\"hide-others\": \"その他を隠す\",\n\t\"show-all\": \"すべて表示\",\n\t\"quit\": \"閉じる\",\n\t\"undo\": \"取り消す\",\n\t\"redo\": \"やり直す\",\n\t\"cut\": \"カット\",\n\t\"copy\": \"コピー\",\n\t\"paste\": \"ペースト\",\n\t\"select-all\": \"すべて表示\",\n\t\"view\": \"表示\",\n\t\"reload\": \"再読込\",\n\t\"toggle-full-screen\": \"全画面表示\",\n\t\"toggle-developer-tools\": \"デベロッパーツールの切り替え\",\n\t\"window\": \"ウィンドウ\",\n\t\"minimize\": \"最小化\",\n\t\"close\": \"閉じる\",\n\t\"bring-all-to-front\": \"すべてを前面に出す\",\n\t\"help\": \"ヘルプ\",\n\t\"learn-more\": \"さらに詳しく\",\n\t\"documentation\": \"文書\",\n\t\"community-discussions\": \"コミュニティーディスカッション\",\n\t\"search-issues\": \"検索条件\",\n\t\"translations-below-are-not-added-to-ui-yet,-but-your-translations-are-welcome!-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"利用可能な信頼できるデバイス\",\n\t\"make-this-device-trusted\": \"このデバイスを信頼する\",\n\t\"click-to-select-other-screen-source-to-share\": \"クリックして共有する他の画面ソースを選択\",\n\t\"click-to-edit-device-alias\": \"クリックしてデバイスエイリアスを編集\",\n\t\"trusted-device-id\": \"信頼できるデバイスのID\",\n\t\"trusted\": \"信頼されています\",\n\t\"make-trusted\": \"信頼する\",\n\t\"forget-this-device\": \"このデバイスを忘れる\",\n\t\"device-alias\": \"デバイスエイリアス\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"デバイスが利用可能な場合、最後の画面ソースを自動共有する\",\n\t\"all-devices-are-successfully-disconnected\": \"すべてのデバイスは正常に切断されました\",\n\t\"device-was-disconnected\": \"デバイスは切断されました\",\n\t\"networking\": \"ネットワーク構築\",\n\t\"deskreen-ce-application-port\": \"Deskreen-CEアプリケーションポート\",\n\t\"port-is-already-used-by-other-app\": \"他のアプリで既に使用されているポート\",\n\t\"click-to-change-deskreen-ce-application-port\": \"クリックすると、Deskreen-CEアプリケーションポートが変更されます。\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Deskreen-CEアプリケーションポートとして使用する3000から64000までの番号を入力します。\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"3000から64000の範囲で別の数値を入力します。\",\n\t\"select-network-interface\": \"ネットワークインターフェースの選択\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"自分のコンピュータのIPが分かっているので、それを手動で入力したい。\",\n\t\"type-your-computer-ip\": \"コンピューターのIPを入力\",\n\t\"click-to-type-ip-manually\": \"クリックしてIPを手動で入力\",\n\t\"banned-ips\": \"禁止されたIP\",\n\t\"ban-new-ip\": \"新しくIPを禁止する\",\n\t\"type-the-ip-you-want-to-ban\": \"禁止したいIPを入力する\",\n\t\"unban-this-ip\": \"このIPの使用禁止を解除\",\n\t\"unban-all-ips\": \"すべてのIPの使用禁止を解除\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Deskreen-CEの設定をデフォルトに戻す\",\n\t\"ask-user-to-enter-password-when-connecting\": \"接続時にパスワードの入力を要求する\",\n\t\"change-password\": \"パスワードを変更\",\n\t\"type-a-new-password\": \"新しいパスワードを入力\",\n\t\"cancel\": \"キャンセル\",\n\t\"device-status\": \"デバイスの状態\",\n\t\"sharing-screen\": \"共有中の画面\",\n\t\"available,-no-screen-sharing\": \"利用可能 画面共有不可\",\n\t\"not-available\": \"利用不可\",\n\t\"autostart-deskreen-ce-app-on-login\": \"ログイン時にDeskreen-CEアプリを自動起動する\",\n\t\"open-deskreen-ce-app-window-on-login\": \"ログイン時にDeskreen-CE Appのウィンドウを開く\",\n\t\"use-system-tray\": \"システムトレイを使用する\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE システムトレイ\",\n\t\"open-app-window\": \"アプリのウィンドウを開く\",\n\t\"minimize-to-tray\": \"トレイに最小化する\",\n\t\"show-connected-devices\": \"接続されたデバイスを見る\",\n\t\"quit-deskreen-ce\": \"Deskreen-CEを閉じる\",\n\t\"fix-reset\": \"修正 & リセット\",\n\t\"fix-reset-tooltip\": \"修正 & リセット. 新しいクライアントの接続に問題がありますか？このボタンをクリックすると、問題解決に役立つ場合があります。\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"表示クライアントが既に接続されています。\",\n\t\"viewing-client-connected-label\": \"表示クライアントが接続されました\",\n\t\"connection-limit-reached-tooltip\": \"接続数の上限に達しました。新しいクライアントを接続するには、現在接続されている閲覧クライアントの接続を切断するか、切断されるまでお待ちください。\",\n\t\"scan-the-qr-code-to-connect\": \"接続するにはQRコードをスキャンしてください\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"任意のデバイスのブラウザのアドレスバーに次のアドレスを入力してください\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CEでは同時に接続できる表示クライアントは1つだけです。\",\n\t\"this-will-be-available-only-in-pro-version\": \"複数デバイス接続のオプションはPro版でのみ利用できます。\"\n}\n"
  },
  {
    "path": "src/common/locales/ko/translation.json",
    "content": "{\n\t\"hello\": \"안녕하세요\",\n\t\"continue\": \"계속\",\n\t\"language\": \"언어\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"기부하기\",\n\t\"get-deskreen-pro\": \"Deskreen Pro 받기\",\n\t\"get-deskreen-pro-tooltip\": \"Deskreen Pro 받기 - 다운로드 페이지를 엽니다.\",\n\t\"if-you-like-deskreen-ce-consider-contributing-financially-deskreen-ce-is-open-source-your-donations-keep-us-motivated-to-make-deskreen-ce-even-better\": \"오픈소스 프로젝트에 재정적으로 기여하는 것은 더 좋은 프로그램 개발 동기를 부여합니다.\",\n\t\"click-to-visit-our-website\": \"클릭하면 웹 사이트를 방문합니다\",\n\t\"connected-devices\": \"연결된 장치\",\n\t\"tutorial\": \"사용법 배우기\",\n\t\"settings\": \"설정\",\n\t\"connect\": \"연결\",\n\t\"select\": \"선택\",\n\t\"confirm\": \"확인\",\n\t\"scan-the-qr-code\": \"QR 코드를 스캔하십시오\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"컴퓨터와 화면을 공유할 장치가 동일한 Wi-Fi에 연결되어 있는지 확인하십시오.\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"또는 화면을 공유할 장치의 브라우저 주소 표시 줄에 다음 주소를 입력하십시오.\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"아래 기기에서 연결을 시도하고 있습니다. 허용하시겠습니까?\",\n\t\"click-to-make-bigger\": \"클릭하여 더 크게 만들기\",\n\t\"click-to-copy\": \"클릭하여 복사합니다\",\n\t\"partner-device-info\": \"연결 요청 장치 정보\",\n\t\"device-type\": \"기기 종류\",\n\t\"device-ip\": \"장치 IP\",\n\t\"device-browser\": \"장치 브라우저\",\n\t\"device-os\": \"장치 OS\",\n\t\"session-id\": \"세션 ID\",\n\t\"allow\": \"허용합니다\",\n\t\"deny\": \"거부합니다\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"장치가 연결이 끊어졌습니다. 새 장치를 연결할 수 있습니다.\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE 업데이트를 사용할 수 있습니다!\",\n\t\"your-current-version-is\": \"현재 사용 중인 버전은\",\n\t\"click-to-download-new-updated-version\": \"새로운 업데이트 버전을 다운로드하려면 클릭하세요\",\n\t\"new-version-available\": \"새 버전을 사용할 수 있습니다!\",\n\t\"connected\": \"연결됨\",\n\t\"click-to-see-more\": \"더 많은 것을 보려면 클릭하십시오\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"연결하려는 장치 화면에 표시된 장치 IP와 일치해야합니다.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"IP 주소가 일치하지 않으면 연결끊기 버튼을 클릭하여 컴퓨터 화면에 무단 액세스를 방지하십시오.\",\n\t\"disconnect\": \"연결끊기\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"공유하려는 전체 화면 또는 프로그램 창을 선택하십시오.\",\n\t\"or\": \"or\",\n\t\"entire-screen\": \"전체 화면\",\n\t\"application-window\": \"프로그램 창\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"모든 정보가 맞으면 확인을 클릭하십시오\",\n\t\"confirm-button-text\": \"확인\",\n\t\"no-i-need-to-choose-other\": \"아니오, 다른 화면을 선택해서 공유하겠습니다.\",\n\t\"done\": \"완료!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"이제 다른 장치에서 화면을 볼 수 있습니다.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"상단 메뉴에서 연결된 장치 버튼을 클릭하여 연결된 장치를 관리 할 수 있습니다.\",\n\t\"connect-new-device\": \"새 장치 연결\",\n\t\"select-entire-screen-to-share\": \"공유할 전체 화면을 선택하십시오\",\n\t\"select-app-window-to-share\": \"공유할 프로그램 창을 선택하십시오\",\n\t\"refresh\": \"새로고침\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"모든 장치 연결끊기\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"연결된 모든 장치의 연결을 해제하시겠습니까?\",\n\t\"this-step-can-not-be-undone\": \"이 단계는 되돌릴 수 없습니다\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"모든 장치를 다시 수동으로 연결해야합니다\",\n\t\"no-cancel\": \"아니오, 취소합니다\",\n\t\"yes-disconnect-all\": \"예, 모든 연결을 끊습니다\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"새 버전의 프로그램을 사용할 수 있습니다. 새 버전을 다운로드하려면 클릭하십시오\",\n\t\"security\": \"보안\",\n\t\"general\": \"일반\",\n\t\"about\": \"정보\",\n\t\"website\": \"웹사이트\",\n\t\"about-deskreen\": \"프로그램 정보\",\n\t\"security-settings\": \"보안 설정\",\n\t\"color-theme\": \"색상 테마\",\n\t\"automatic-updates\": \"자동 업데이트\",\n\t\"general-settings\": \"일반 설정\",\n\t\"disabled\": \"사용안함\",\n\t\"version\": \"버전\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"편집\",\n\t\"hide-deskreen\": \"Deskreen CE 숨기기\",\n\t\"hide-others\": \"다른 창 숨기기\",\n\t\"show-all\": \"모두 보기\",\n\t\"quit\": \"종료\",\n\t\"undo\": \"되돌리기\",\n\t\"redo\": \"재실행\",\n\t\"cut\": \"잘라내기\",\n\t\"copy\": \"복사\",\n\t\"paste\": \"붙여넣기\",\n\t\"select-all\": \"모두 선택\",\n\t\"view\": \"보기\",\n\t\"reload\": \"다시읽기\",\n\t\"toggle-full-screen\": \"전체 화면 전환\",\n\t\"toggle-developer-tools\": \"개발자 도구 온\\\\/오프\",\n\t\"window\": \"창\",\n\t\"minimize\": \"최소화\",\n\t\"close\": \"닫기\",\n\t\"bring-all-to-front\": \"모든 창 앞으로 가져오기\",\n\t\"help\": \"도움말\",\n\t\"learn-more\": \"더 알아보기\",\n\t\"documentation\": \"문서\",\n\t\"community-discussions\": \"커뮤니티\",\n\t\"search-issues\": \"이슈 검색\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"사용 가능한 신뢰할 수 있는 장치\",\n\t\"make-this-device-trusted\": \"이 장치를 신뢰할 수 있도록 설정\",\n\t\"click-to-select-other-screen-source-to-share\": \"공유할 다른 화면 소스를 선택하려면 클릭하십시오\",\n\t\"click-to-edit-device-alias\": \"장치 별명을 편집하려면 클릭하십시오\",\n\t\"trusted-device-id\": \"신뢰할 수 있는 장치 ID.\",\n\t\"trusted\": \"신뢰함\",\n\t\"make-trusted\": \"신뢰함으로 설정\",\n\t\"forget-this-device\": \"신뢰하는 장치 목록에서 이 장치를 제거\",\n\t\"device-alias\": \"장치 별명\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"장치를 사용할 수 있을때 마지막 전체화면을 자동으로 공유\",\n\t\"all-devices-are-successfully-disconnected\": \"모든 장치 연결이 끊어졌습니다\",\n\t\"device-was-disconnected\": \"장치 연결이 끊어졌습니다\",\n\t\"networking\": \"네트워킹\",\n\t\"deskreen-ce-application-port\": \"프로그램 포트\",\n\t\"port-is-already-used-by-other-app\": \"포트는 이미 다른 프로그램에서 사용 중입니다\",\n\t\"click-to-change-deskreen-ce-application-port\": \"프로그램 포트를 변경하려면 클릭하십시오\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"사용할 포트로 3000에서 64000 사이의 숫자를 입력하십시오.\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"3000 에서 64000 사이의 다른 숫자를 입력하십시오\",\n\t\"select-network-interface\": \"네트워크 인터페이스를 선택하십시오\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"내 컴퓨터의 IP를 알고 있고 수동으로 입력하고 싶습니다.\",\n\t\"type-your-computer-ip\": \"컴퓨터 IP를 입력하십시오\",\n\t\"click-to-type-ip-manually\": \"수동으로 IP를 입력하려면 클릭하십시오\",\n\t\"banned-ips\": \"차단한 IP 목록\",\n\t\"ban-new-ip\": \"차단할 새 IP\",\n\t\"type-the-ip-you-want-to-ban\": \"차단할 IP를 입력하십시오\",\n\t\"unban-this-ip\": \"이 IP를 차단 해제\",\n\t\"unban-all-ips\": \"차단한 모든 IP를 해제\",\n\t\"reset-deskreen-ce-settings-to-default\": \"프로그램 기본설정으로 초기화\",\n\t\"ask-user-to-enter-password-when-connecting\": \"연결할 때 사용자에게 암호를 입력하도록 요청하십시오\",\n\t\"change-password\": \"암호 변경\",\n\t\"type-a-new-password\": \"새 암호를 입력하십시오\",\n\t\"cancel\": \"취소\",\n\t\"device-status\": \"장치 상태\",\n\t\"sharing-screen\": \"화면 공유\",\n\t\"available-no-screen-sharing\": \"사용 가능, 화면 공유 없음\",\n\t\"not-available\": \"사용할 수 없음\",\n\t\"autostart-deskreen-ce-app-on-login\": \"로그인 시 프로그램 자동 시작\",\n\t\"open-deskreen-ce-app-window-on-login\": \"로그인 시 프로그램 창 열기\",\n\t\"use-system-tray\": \"시스템 트레이 사용\",\n\t\"deskreen-ce-system-tray\": \"프로그램 시스템 트레이\",\n\t\"open-app-window\": \"프로그램 창 열기\",\n\t\"minimize-to-tray\": \"트레이에 최소화\",\n\t\"show-connected-devices\": \"연결된 장치를 보여줍니다\",\n\t\"quit-deskreen-ce\": \"프로그램 종료\",\n\t\"fix-reset\": \"수정 및 재설정\",\n\t\"fix-reset-tooltip\": \"수정 및 재설정. 새 클라이언트 연결에 문제가 있으신가요? 이 버튼을 클릭하면 도움이 될 수 있습니다.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"뷰잉 클라이언트가 이미 연결되어 있습니다.\",\n\t\"viewing-client-connected-label\": \"뷰잉 클라이언트가 연결되었습니다\",\n\t\"connection-limit-reached-tooltip\": \"연결 한도에 도달했습니다. 새 클라이언트를 연결하려면 현재 시청 중인 클라이언트의 연결을 끊거나 끊어질 때까지 기다리세요.\",\n\t\"scan-the-qr-code-to-connect\": \"연결하려면 QR 코드를 스캔하세요\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"어떤 기기의 브라우저 주소창에 다음 주소를 입력하세요\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE는 동시에 하나의 시청 클라이언트만 연결할 수 있습니다.\",\n\t\"this-will-be-available-only-in-pro-version\": \"두 개 이상의 기기를 연결하는 옵션은 Pro 버전에서만 사용할 수 있습니다.\"\n}\n"
  },
  {
    "path": "src/common/locales/nl/translation.json",
    "content": "{\n\t\"hello\": \"Hallo\",\n\t\"continue\": \"Verder\",\n\t\"language\": \"Taal\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Doneer\",\n\t\"get-deskreen-pro\": \"Ontvang Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Ontvang Deskreen Pro - opent de downloadpagina.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Klik om onze website te bezoeken\",\n\t\"connected-devices\": \"Verbonden Apparaten\",\n\t\"tutorial\": \"Handleiding\",\n\t\"settings\": \"Instellingen\",\n\t\"connect\": \"Verbinden\",\n\t\"select\": \"Selecteer\",\n\t\"confirm\": \"Bevestig\",\n\t\"scan-the-qr-code\": \"Scan de QR code\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Controleer of uw computer en scherm-bekijk-apparaat verbonden zijn met dezelfde Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Of typ het volgende adres in een adres van een webbrowser van een willekeurig apparaat\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Iemand probeert verbinding te maken, staat u dit toe?\",\n\t\"click-to-make-bigger\": \"Klik om groter te maken\",\n\t\"click-to-copy\": \"Klik om te kopieren\",\n\t\"partner-device-info\": \"Partner Apparaat Info\",\n\t\"device-type\": \"Apparaat Type\",\n\t\"device-ip\": \"Apparaat IP\",\n\t\"device-browser\": \"Apparaat Browser\",\n\t\"device-os\": \"Apparaat OS\",\n\t\"session-id\": \"Sessie ID\",\n\t\"allow\": \"Toestaan\",\n\t\"deny\": \"Weigeren\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Apparaat heeft succesvol de verbinding verbroken U kunt een nieuw apparaat verbinden\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE Update is Beschikbaar!\",\n\t\"your-current-version-is\": \"Uw huidige versie is\",\n\t\"click-to-download-new-updated-version\": \"Klik om de nieuwe geüpdate versie te downloaden\",\n\t\"new-version-available\": \"Nieuwe versie beschikbaar!\",\n\t\"connected\": \"Verbonden\",\n\t\"click-to-see-more\": \"Klik om meer te zien\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Dit moet identiek zijn aan het Apparaat IP welke weergegeven is op het scherm van het apparaat dat probeert verbinding te maken\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Als de IP adressen niet overeenkomen klik de Verbinding verbreken knop\",\n\t\"disconnect\": \"Verbinding verbreken\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Kies het gehele scherm of het applicatie venster dat u wilt delen\",\n\t\"or\": \"of\",\n\t\"entire-screen\": \"Gehele Scherm\",\n\t\"application-window\": \"Applicatie Venster\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Controleer of alles OK is en klik Bevestigen\",\n\t\"confirm-button-text\": \"Bevestigen\",\n\t\"no-i-need-to-choose-other\": \"Nee, ik moet iets anders kiezen\",\n\t\"done\": \"Gereed!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"U kunt nu uw scherm zien op het andere apparaat\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"U kunt verbonden apparaten beheren door op Verbonden Apparaten te klikken in het bovenste paneel\",\n\t\"connect-new-device\": \"Nieuw Apparaat Verbinden\",\n\t\"select-entire-screen-to-share\": \"Selecteer gehele scherm om te delen\",\n\t\"select-app-window-to-share\": \"Selecteer applicatie venster om te delen\",\n\t\"refresh\": \"Verversen\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Verbinding verbreken met alle apparaten\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Weet u zeker dat u de verbinding met alle verbonden scherm-bekijk-apparaten wilt verbreken?\",\n\t\"this-step-can-not-be-undone\": \"Deze stap kan niet ongedaan gemaakt worden\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"U zult met alle apparaten handmatig opnieuw verbinding moeten maken\",\n\t\"no-cancel\": \"Nee, Annuleren\",\n\t\"yes-disconnect-all\": \"Ja, alle verbindingen verbreken\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Een nieuwe versie van Deskreen CE is beschikbaar. Klik om de nieuwe versie te downloaden!\",\n\t\"security\": \"Beveiliging\",\n\t\"general\": \"Algemeen\",\n\t\"about\": \"Over\",\n\t\"website\": \"Website\",\n\t\"about-deskreen\": \"Over Deskreen CE\",\n\t\"security-settings\": \"Beveiligings instellingen\",\n\t\"color-theme\": \"Kleur Thema\",\n\t\"automatic-updates\": \"Automatische Updates\",\n\t\"general-settings\": \"Algemene Instellingen\",\n\t\"disabled\": \"Uitgeschakeld\",\n\t\"version\": \"Versie\",\n\t\"copyright\": \"Copyright\",\n\t\"edit\": \"Wijzig\",\n\t\"hide-deskreen\": \"Verberg Deskreen CE\",\n\t\"hide-others\": \"Verberg Andere\",\n\t\"show-all\": \"Toon alle\",\n\t\"quit\": \"Afsluiten\",\n\t\"undo\": \"Ongedaan maken\",\n\t\"redo\": \"Opnieuw doen\",\n\t\"cut\": \"Knippen\",\n\t\"copy\": \"Kopiëren\",\n\t\"paste\": \"Plakken\",\n\t\"select-all\": \"Selecteer Alles\",\n\t\"view\": \"Bekijken\",\n\t\"reload\": \"Herladen\",\n\t\"toggle-full-screen\": \"Schakelen Volledig Scherm\",\n\t\"toggle-developer-tools\": \"Schakelen Ontwikkelaars Gereedschappen\",\n\t\"window\": \"Scherm\",\n\t\"minimize\": \"Minimaliseer\",\n\t\"close\": \"Afsluiten\",\n\t\"bring-all-to-front\": \"Breng Alle naar Voren\",\n\t\"help\": \"Help\",\n\t\"learn-more\": \"Leer meer\",\n\t\"documentation\": \"Documentatie\",\n\t\"community-discussions\": \"Gemeenschap Discussies\",\n\t\"search-issues\": \"Doorzoek Problemen\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Beschikbare Vertrouwde Apparaten\",\n\t\"make-this-device-trusted\": \"Maak dit apparaat vertrouwd\",\n\t\"click-to-select-other-screen-source-to-share\": \"Klik om een andere scherm-bron te delen\",\n\t\"click-to-edit-device-alias\": \"Klik voor het wijzigen van het Apparaat Alias\",\n\t\"trusted-device-id\": \"Vertrouwd Apparaat ID\",\n\t\"trusted\": \"Vertrouwd\",\n\t\"make-trusted\": \"Maak Vertrouwd\",\n\t\"forget-this-device\": \"Vergeet Dit Apparaat\",\n\t\"device-alias\": \"Apparaat Alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Deel automatisch de laatste scherm-bron wanneer het apparaat beschikbaar is\",\n\t\"all-devices-are-successfully-disconnected\": \"Alle apparaten hebben succesvol de verbinding verbroken\",\n\t\"device-was-disconnected\": \"Verbinding met apparaat was verbroken\",\n\t\"networking\": \"Netwerk\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE Applicatie Poort\",\n\t\"port-is-already-used-by-other-app\": \"Poort wordt is al in gebruik door een andere App\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Klik om Deskreen CE Applicatie Poort aan te passen\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Type een nummer van 3000 tot 64000 om te gebruiken als Deskreen CE Applicatie Poort\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Type een ander nummer in de reeks van 3000 tot 64000\",\n\t\"select-network-interface\": \"Selecteer Netwerk Interface\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Ik weet het IP van mijn computer en wil het handmatig invoeren\",\n\t\"type-your-computer-ip\": \"Typ uw Computer IP in\",\n\t\"click-to-type-ip-manually\": \"Klik om IP handmatig in te typen\",\n\t\"banned-ips\": \"Geblokkeerde IPs\",\n\t\"ban-new-ip\": \"Blokkeer Nieuw IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Typ het IP wat u wilt blokkeren\",\n\t\"unban-this-ip\": \"Deblokkeer dit IP\",\n\t\"unban-all-ips\": \"Deblokkeer alle IPs\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Herstel Deskreen CE instellingen naar de standaard\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Vraag de gebruiker het wachtwoord in te voeren bij het maken van de verbinding\",\n\t\"change-password\": \"Wachtwoord wijzigen\",\n\t\"type-a-new-password\": \"Typ een Nieuw Wachtwoord\",\n\t\"cancel\": \"Annuleren\",\n\t\"device-status\": \"Apparaat Status\",\n\t\"sharing-screen\": \"Scherm Gedeeld\",\n\t\"available-no-screen-sharing\": \"Beschikbaar, geen scherm gedeeld\",\n\t\"not-available\": \"Niet Beschikbaar\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Autostart Deskreen CE App tijdens inloggen\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Open Deskreen CE App venster tijdens inloggen\",\n\t\"use-system-tray\": \"Gebruik systeemvak\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE System Vak\",\n\t\"open-app-window\": \"Open App Venster\",\n\t\"minimize-to-tray\": \"Minimaliseer Naar Systeemvak\",\n\t\"show-connected-devices\": \"Laat Verbonden Apparaten zien\",\n\t\"quit-deskreen-ce\": \"Deskreen CE Afsluiten\",\n\t\"fix-reset\": \"Herstellen & Resetten\",\n\t\"fix-reset-tooltip\": \"Herstellen & Resetten. Problemen met het verbinden van nieuwe clients? Klikken op deze knop kan helpen.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Er is al één weergaveclient verbonden.\",\n\t\"viewing-client-connected-label\": \"Weergaveclient verbonden\",\n\t\"connection-limit-reached-tooltip\": \"Maximaal aantal verbindingen bereikt. Verbreek de verbinding met de huidige weergaveclient (of wacht tot deze verbreekt) om een nieuwe te verbinden.\",\n\t\"scan-the-qr-code-to-connect\": \"Scan de QR-code om verbinding te maken\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Voer het volgende adres in de adresbalk van een willekeurig apparaat in\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE staat slechts één weergaveapparaat tegelijk toe.\",\n\t\"this-will-be-available-only-in-pro-version\": \"De optie om meer dan één apparaat te verbinden is alleen beschikbaar in de Pro-versie.\"\n}\n"
  },
  {
    "path": "src/common/locales/ru/translation.json",
    "content": "{\n\t\"hello\": \"Привет\",\n\t\"continue\": \"Продолжить\",\n\t\"language\": \"Язык\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Пожертвовать\",\n\t\"get-deskreen-pro\": \"Получить Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Получить Deskreen Pro - открывает страницу загрузки.\",\n\t\"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 еще лучше.\",\n\t\"click-to-visit-our-website\": \"Нажмите, чтобы посетить наш сайт\",\n\t\"connected-devices\": \"Подключенные устройства\",\n\t\"tutorial\": \"Инструкция по использованию\",\n\t\"settings\": \"Настройки\",\n\t\"connect\": \"Подключите\",\n\t\"select\": \"Выберите\",\n\t\"confirm\": \"Подтвердите\",\n\t\"scan-the-qr-code\": \"Отсканируйте QR код\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Убедитесь, что ваш компьютер и устройство просмотра экрана подключены к одному и тому же Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Или введите следующий адрес вручную в адресной строке браузера на любом устройстве\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Кто-то пытается подключиться, вы разрешаете?\",\n\t\"click-to-make-bigger\": \"Нажмите, чтобы увеличить\",\n\t\"click-to-copy\": \"Нажмите, чтобы скопировать\",\n\t\"partner-device-info\": \"Информация об устройстве партнера\",\n\t\"device-type\": \"Тип устройства\",\n\t\"device-ip\": \"IP-aдрес устройства\",\n\t\"device-browser\": \"Веб-браузер устройства\",\n\t\"device-os\": \"ОС устройства\",\n\t\"session-id\": \"ID сессии\",\n\t\"allow\": \"Разрешить\",\n\t\"deny\": \"Отказать\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Устройство успешно отключено вами. Вы можете подключить новое устройство.\",\n\t\"deskreen-ce-update-is-available\": \"Вышло обновление Deskreen CE!\",\n\t\"your-current-version-is\": \"Ваша текущая версия\",\n\t\"click-to-download-new-updated-version\": \"Нажмите, чтобы загрузить новую обновленную версию\",\n\t\"new-version-available\": \"Доступна новая версия!\",\n\t\"connected\": \"Подключено\",\n\t\"click-to-see-more\": \"Нажмите, чтобы увидеть больше\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Это должно совпадать с IP-адресом устройства, отображаемым на экране устройства, которое пытается подключиться.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Если IP-адреса не совпадают, нажмите кнопку «Отключить», чтобы предотвратить несанкционированный доступ к экрану вашего компьютера.\",\n\t\"disconnect\": \"Отсоединить\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Выберите Весь экран или Окно приложения, которым хотите поделиться\",\n\t\"or\": \"ИЛИ\",\n\t\"entire-screen\": \"Весь экран\",\n\t\"application-window\": \"Окно приложения\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Убедитесь, что все в порядке, и нажмите Подтвердить\",\n\t\"confirm-button-text\": \"Подтвердить\",\n\t\"no-i-need-to-choose-other\": \"Нет, мне нужно выбрать другое\",\n\t\"done\": \"Сделано!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Теперь вы можете видеть свой экран на другом устройстве.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Вы можете управлять подключенными устройствами, нажав кнопку «Подключенные устройства» на верхней панели.\",\n\t\"connect-new-device\": \"Подключить новое устройство\",\n\t\"select-entire-screen-to-share\": \"Выберите экран которым хотите поделиться\",\n\t\"select-app-window-to-share\": \"Выберите окно приложения которым хотите поделиться\",\n\t\"refresh\": \"Обновить\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Отсоединить все устройства\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Вы уверены, что хотите отключить все подключенные устройства просмотра?\",\n\t\"this-step-can-not-be-undone\": \"Этот шаг невозможно будет отменить\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"После этого вам придется снова подключить каждое устройство\",\n\t\"no-cancel\": \"Нет, отменить\",\n\t\"yes-disconnect-all\": \"Да, отсоединить все устройства\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Доступна новая версия Deskreen CE! Нажмите, чтобы загрузить новую версию\",\n\t\"security\": \"Безопасность\",\n\t\"general\": \"Общие\",\n\t\"about\": \"О нас\",\n\t\"website\": \"Веб-сайт\",\n\t\"about-deskreen\": \"Про Deskreen CE\",\n\t\"security-settings\": \"Настройки безопасности\",\n\t\"color-theme\": \"Цветовая схема\",\n\t\"automatic-updates\": \"Авто-обновления\",\n\t\"general-settings\": \"Общие настройки\",\n\t\"disabled\": \"Отключено\",\n\t\"version\": \"Версия\",\n\t\"copyright\": \"Авторские права\",\n\t\"edit\": \"Править\",\n\t\"hide-deskreen\": \"Скрыть Deskreen CE\",\n\t\"hide-others\": \"Скрыть Другие\",\n\t\"show-all\": \"Показать все\",\n\t\"quit\": \"Выйти\",\n\t\"undo\": \"Отменить\",\n\t\"redo\": \"Повторить\",\n\t\"cut\": \"Вырезать\",\n\t\"copy\": \"Копировать\",\n\t\"paste\": \"Вставить\",\n\t\"select-all\": \"Выбрать все\",\n\t\"view\": \"Вид\",\n\t\"reload\": \"Перезагрузить\",\n\t\"toggle-full-screen\": \"Включить полноэкранный режим\",\n\t\"toggle-developer-tools\": \"Включить инструменты разработчика\",\n\t\"window\": \"Окно\",\n\t\"minimize\": \"Свернуть\",\n\t\"close\": \"Закрыть\",\n\t\"bring-all-to-front\": \"Вынести все на передний план\",\n\t\"help\": \"Помощь\",\n\t\"learn-more\": \"Узнать больше\",\n\t\"documentation\": \"Документация\",\n\t\"community-discussions\": \"Обсуждения в сообществе\",\n\t\"search-issues\": \"Поиск проблем\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Available Trusted Devices\",\n\t\"make-this-device-trusted\": \"Make this device trusted\",\n\t\"click-to-select-other-screen-source-to-share\": \"Click to select other screen source to share\",\n\t\"click-to-edit-device-alias\": \"Click to edit Device Alias\",\n\t\"trusted-device-id\": \"Trusted Device ID\",\n\t\"trusted\": \"Trusted\",\n\t\"make-trusted\": \"Make Trusted\",\n\t\"forget-this-device\": \"Forget This Device\",\n\t\"device-alias\": \"Device Alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Auto share last Entire Screen source when device is available\",\n\t\"all-devices-are-successfully-disconnected\": \"All devices are successfully disconnected\",\n\t\"device-was-disconnected\": \"Device was disconnected\",\n\t\"networking\": \"Networking\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE Application Port\",\n\t\"port-is-already-used-by-other-app\": \"Port is already used by other App\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Click to change Deskreen CE Application Port\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Type a number from 3000 to 64000 to use as a Deskreen CE Application Port\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Type another number in range from 3000 to 64000\",\n\t\"select-network-interface\": \"Select Network Interface\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"I know IP of my computer and I want to type it manually\",\n\t\"type-your-computer-ip\": \"Type Your Computer IP\",\n\t\"click-to-type-ip-manually\": \"Click to type IP manually\",\n\t\"banned-ips\": \"Banned IPs\",\n\t\"ban-new-ip\": \"Ban New IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Type the IP you want to ban\",\n\t\"unban-this-ip\": \"Unban this IP\",\n\t\"unban-all-ips\": \"Unban all IPs\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Reset Deskreen CE settings to default\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Ask user to enter password when connecting\",\n\t\"change-password\": \"Change Password\",\n\t\"type-a-new-password\": \"Type a New Password\",\n\t\"cancel\": \"Cancel\",\n\t\"device-status\": \"Device Status\",\n\t\"sharing-screen\": \"Sharing Screen\",\n\t\"available-no-screen-sharing\": \"Available, no screen sharing\",\n\t\"not-available\": \"Not Available\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Autostart Deskreen CE App on login\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Open Deskreen CE App window on login\",\n\t\"use-system-tray\": \"Use system tray\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE System Tray\",\n\t\"open-app-window\": \"Open App Window\",\n\t\"minimize-to-tray\": \"Minimize To Tray\",\n\t\"show-connected-devices\": \"Show Connected Devices\",\n\t\"quit-deskreen-ce\": \"Quit Deskreen CE\",\n\t\"fix-reset\": \"Исправить и Сбросить\",\n\t\"fix-reset-tooltip\": \"Исправить и Сбросить. Проблемы с подключением новых клиентов? Нажатие этой кнопки может помочь.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Один клиент просмотра уже подключен.\",\n\t\"viewing-client-connected-label\": \"Клиент просмотра подключен\",\n\t\"connection-limit-reached-tooltip\": \"Достигнут предел подключений. Отключите текущего клиента просмотра (или дождитесь, пока он отключится), чтобы подключить нового.\",\n\t\"scan-the-qr-code-to-connect\": \"Отсканируйте QR-код, чтобы подключиться\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Введите следующий адрес в адресную строку браузера на любом устройстве\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE позволяет одновременно подключить только одно устройство просмотра.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Возможность подключения более одного устройства будет доступна только в версии Pro.\"\n}\n"
  },
  {
    "path": "src/common/locales/sv/translation.json",
    "content": "{\n\t\"hello\": \"Hej\",\n\t\"continue\": \"Fortsätt\",\n\t\"language\": \"Språk\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Donera\",\n\t\"get-deskreen-pro\": \"Hämta Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Hämta Deskreen Pro - öppnar nedladdningssidan.\",\n\t\"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.\",\n\t\"click-to-visit-our-website\": \"Klicka här för att komma till vår webbplats\",\n\t\"connected-devices\": \"Anslutna enheter\",\n\t\"tutorial\": \"Handledning\",\n\t\"settings\": \"Inställningar\",\n\t\"connect\": \"Anslut\",\n\t\"select\": \"Välj\",\n\t\"confirm\": \"Bekräfta\",\n\t\"scan-the-qr-code\": \"Skanna QR-koden\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Se till att din dator och den enhet du tänker använda för skärmdelning båda är anslutna till samma trådlösa nätverk\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Eller skriv in följande adress i webbläsarens addressrad för någon de enheter du vill ansluta med\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Någon försöker ansluta till Deskreen CE, Tillåter du det?\",\n\t\"click-to-make-bigger\": \"Klicka för att förstora\",\n\t\"click-to-copy\": \"Klicka för att kopiera\",\n\t\"partner-device-info\": \"Deltagande enhetens information\",\n\t\"device-type\": \"Enhetens typ\",\n\t\"device-ip\": \"Enhetens IP\",\n\t\"device-browser\": \"Enhetens webbläsare\",\n\t\"device-os\": \"Enhetens operativsystem\",\n\t\"session-id\": \"ID för sessionen\",\n\t\"allow\": \"Tillåt\",\n\t\"deny\": \"Neka\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Du har kopplat från enheten. Du kan nu ansluta en ny enhet.\",\n\t\"deskreen-ce-update-is-available\": \"Det finns en uppdatering till Deskreen CE!\",\n\t\"your-current-version-is\": \"Din nuvarande version är\",\n\t\"click-to-download-new-updated-version\": \"Klicka här för att ladda ner en uppdaterad version\",\n\t\"new-version-available\": \"Ny version tillgänglig!\",\n\t\"connected\": \"Ansluten\",\n\t\"click-to-see-more\": \"Klicka för att visa mer\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Detta bör matcha med det IP som visas på skärmen på den enhet som försöker ansluta.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Om IP-adresserna inte matchar, klicka på knappen 'Koppla ner' för att förhindra obehörig åtkomst till din datorskärm.\",\n\t\"disconnect\": \"Koppla ner\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"välj hela skärmen eller ett applikationsfönster som du vill dela\",\n\t\"or\": \"eller\",\n\t\"entire-screen\": \"Hela skärmen\",\n\t\"application-window\": \"Applikationsfönster\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Säkerställ att allt är okej och tryck på 'Bekräfta'\",\n\t\"confirm-button-text\": \"Bekräfta\",\n\t\"no-i-need-to-choose-other\": \"Nej, Jag vill välja en annan\",\n\t\"done\": \"Klart!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Nu kan du visa din skärm på en annan enhet.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Du kan hantera anslutna enheter genom att klicka på knappen 'Ansluta enheter' i toppanelen.\",\n\t\"connect-new-device\": \"Anslut ny enhet\",\n\t\"select-entire-screen-to-share\": \"Välj att dela hela skärmen\",\n\t\"select-app-window-to-share\": \"Välj ett applikationsfönster att dela\",\n\t\"refresh\": \"Ladda om\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Koppla ner alla anslutna enheter\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Är du säker på att du vill koppla ner alla anslutna enheter?\",\n\t\"this-step-can-not-be-undone\": \"Den här åtgärden kan inte ångras\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Du kommer att behöva ansluta till alla enheter manuellt igen\",\n\t\"no-cancel\": \"Nej, Avbryt\",\n\t\"yes-disconnect-all\": \"Ja, Koppla ner alla\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Det finns en ny version av Deskreen CE tillgänglig! Klicka här för att ladda ned den nya versionen\",\n\t\"security\": \"Säkerhet\",\n\t\"general\": \"Allmänt\",\n\t\"about\": \"Om\",\n\t\"website\": \"Webbplats\",\n\t\"about-deskreen\": \"Om Deskreen CE\",\n\t\"security-settings\": \"Säkerhetsinställningar\",\n\t\"color-theme\": \"Färgschema\",\n\t\"automatic-updates\": \"Automatiska uppdateringar\",\n\t\"general-settings\": \"Generella inställningar\",\n\t\"disabled\": \"Inaktiverad\",\n\t\"version\": \"Version\",\n\t\"copyright\": \"Upphovsrätt\",\n\t\"edit\": \"Redigera\",\n\t\"hide-deskreen\": \"Dölj Deskreen CE\",\n\t\"hide-others\": \"Dölj Övriga\",\n\t\"show-all\": \"Visa Alla\",\n\t\"quit\": \"Avsluta\",\n\t\"undo\": \"Ångra\",\n\t\"redo\": \"Gör om\",\n\t\"cut\": \"Klipp ut\",\n\t\"copy\": \"Kopiera\",\n\t\"paste\": \"Klistra in\",\n\t\"select-all\": \"Select All\",\n\t\"view\": \"Visa\",\n\t\"reload\": \"Ladda om\",\n\t\"toggle-full-screen\": \"Växla fullskärmsläge\",\n\t\"toggle-developer-tools\": \"Växla utvecklarverktyg\",\n\t\"window\": \"Fönster\",\n\t\"minimize\": \"Minimera\",\n\t\"close\": \"Stäng\",\n\t\"bring-all-to-front\": \"Ta fram alla\",\n\t\"help\": \"Hjälp\",\n\t\"learn-more\": \"Läs mer\",\n\t\"documentation\": \"Dokumentation\",\n\t\"community-discussions\": \"Gemenskapsdiskussioner\",\n\t\"search-issues\": \"Sök efter problem\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Tillgängliga tillförlitliga enheter\",\n\t\"make-this-device-trusted\": \"Gör den här enheten tillförlitlig\",\n\t\"click-to-select-other-screen-source-to-share\": \"Klicka här för att välja en annan skärmkälla att dela\",\n\t\"click-to-edit-device-alias\": \"Klicka här för att redigera enhetens alias\",\n\t\"trusted-device-id\": \"Tillförlitlig enhets ID\",\n\t\"trusted\": \"Tillförlitlig\",\n\t\"make-trusted\": \"Gör den tillförlitlig\",\n\t\"forget-this-device\": \"Glöm bort den här enheten\",\n\t\"device-alias\": \"Enhetens alias\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Dela den senast använda Helskärmen när enheten är tillgänglig\",\n\t\"all-devices-are-successfully-disconnected\": \"Alla enheter är nu frånkopplade\",\n\t\"device-was-disconnected\": \"Enheten är nu frånkopplad\",\n\t\"networking\": \"Nätverk\",\n\t\"deskreen-ce-application-port\": \"Post för Deskreen CE\",\n\t\"port-is-already-used-by-other-app\": \"Porten används redan av en annan applikation\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Klicka här för att ange en annan port\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Ange ett nummer mellan 3000 och 64000 för att använda som port för Deskreen CE\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Ange ett annat nummer mellan 3000 och 64000\",\n\t\"select-network-interface\": \"Välj nätverksgränssnitt\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Jag känner till mitt IP-nummer och och vill ange det manuellt\",\n\t\"type-your-computer-ip\": \"Ange din dators IP-nummer\",\n\t\"click-to-type-ip-manually\": \"Klicka här för att ange ditt IP-nummer manuellt\",\n\t\"banned-ips\": \"Blockerade IP-nummer\",\n\t\"ban-new-ip\": \"Blockera nytt IP-nummer\",\n\t\"type-the-ip-you-want-to-ban\": \"Ange det IP-nummer du vill blockera\",\n\t\"unban-this-ip\": \"Tillåt det här IP-numret\",\n\t\"unban-all-ips\": \"Tillåt alla IP-nummer\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Återställ Deskreen CE inställningar till standardinställningarna\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Be användaren att ange lösenord vid anslutning\",\n\t\"change-password\": \"Ändra lösenord\",\n\t\"type-a-new-password\": \"Ange ett nytt lösenord\",\n\t\"cancel\": \"Avbryt\",\n\t\"device-status\": \"Enhetens status\",\n\t\"sharing-screen\": \"Delar skärm\",\n\t\"available-no-screen-sharing\": \"Tillgängliga, ingen skärmdelning\",\n\t\"not-available\": \"Inte tillgängligt\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Starta Deskreen CE automatiskt vid inloggning\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Öppna Deskreen CE automatiskt vid inloggning\",\n\t\"use-system-tray\": \"Use system tray\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE System Tray\",\n\t\"open-app-window\": \"Öppna Applikationsfönstret\",\n\t\"minimize-to-tray\": \"Minimize To Tray\",\n\t\"show-connected-devices\": \"Visa Anslutna Enheter\",\n\t\"quit-deskreen-ce\": \"Avsluta Deskreen CE\",\n\t\"fix-reset\": \"Åtgärda & Återställ\",\n\t\"fix-reset-tooltip\": \"Åtgärda & Återställ. Har du problem med att ansluta nya klienter? Att klicka på denna knapp kan hjälpa dig.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"En visningsklient är redan ansluten.\",\n\t\"viewing-client-connected-label\": \"Visningsklient ansluten\",\n\t\"connection-limit-reached-tooltip\": \"Anslutningsgränsen har nåtts. Koppla från den aktuella visningsklienten (eller vänta tills den kopplar från) för att ansluta en ny.\",\n\t\"scan-the-qr-code-to-connect\": \"Skanna QR-koden för att ansluta\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Ange följande adress i webbläsarens adressfält på valfri enhet\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE tillåter endast en visningsklient ansluten åt gången.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Alternativet att ansluta fler än en enhet finns endast i Pro-versionen.\"\n}\n"
  },
  {
    "path": "src/common/locales/ua/translation.json",
    "content": "{\n\t\"hello\": \"Привіт\",\n\t\"continue\": \"Продовжити\",\n\t\"language\": \"Мова\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"nl\": \"Nederlands\",\n\t\"fr\": \"Français\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"Пожертвувати\",\n\t\"get-deskreen-pro\": \"Отримати Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"Отримати Deskreen Pro - відкриває сторінку завантаження.\",\n\t\"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 ще краще.\",\n\t\"click-to-visit-our-website\": \"Натисніть, щоб відвідати наш сайт\",\n\t\"connected-devices\": \"Підключені пристрої\",\n\t\"tutorial\": \"Інструкція по використанню\",\n\t\"settings\": \"Налаштування\",\n\t\"connect\": \"Підключіть\",\n\t\"select\": \"Виберіть\",\n\t\"confirm\": \"Підтвердіть\",\n\t\"scan-the-qr-code\": \"Відскануйте QR код\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"Переконайтеся, що ваш комп'ютер і пристрій перегляду екрану підключені до одного й того ж Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"Або введіть наступну адресу в адресному рядку браузера на будь-якому пристрої\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"Хтось намагається підключитися, ви дозволяєте?\",\n\t\"click-to-make-bigger\": \"Натисніть, щоб збільшити\",\n\t\"click-to-copy\": \"Натисніть, щоб скопіювати\",\n\t\"partner-device-info\": \"Інформація про пристрій партнера\",\n\t\"device-type\": \"Тип пристрою\",\n\t\"device-ip\": \"IP-aдрес пристрою\",\n\t\"device-browser\": \"Веб-браузер пристрою\",\n\t\"device-os\": \"ОС пристрою\",\n\t\"session-id\": \"ID сесії\",\n\t\"allow\": \"Дозволити\",\n\t\"deny\": \"Відмовити\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"Пристрій успішно відключено вами. Ви можете підключити новий пристрій.\",\n\t\"deskreen-ce-update-is-available\": \"Вийшло оновлення Deskreen CE!\",\n\t\"your-current-version-is\": \"Ваша поточна версія\",\n\t\"click-to-download-new-updated-version\": \"Натисніть, щоб завантажити нову оновлену версію\",\n\t\"new-version-available\": \"Доступна нова версія!\",\n\t\"connected\": \"З'єднано\",\n\t\"click-to-see-more\": \"Натисніть, щоб побачити більше\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"Це повинно збігатися з IP-адресою пристрою на екрані пристрою, який намагається підключитися.\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"Якщо IP-адреси не збігаються, натисніть кнопку «Відключити», щоб запобігти несанкціонованому доступу до екрану вашого комп'ютера.\",\n\t\"disconnect\": \"Від'єднати\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"Виберіть Весь екран або Вікно додатка, яким хочете поділитися\",\n\t\"or\": \"ЧИ\",\n\t\"entire-screen\": \"Весь екран\",\n\t\"application-window\": \"Вікно додатка\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"Переконайтеся, що все гаразд, і натисніть Підтвердити\",\n\t\"confirm-button-text\": \"Підтвердити\",\n\t\"no-i-need-to-choose-other\": \"Ні, мені потрібно вибрати інше\",\n\t\"done\": \"Зроблено!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"Тепер ви можете бачити свій екран на іншому пристрої.\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"Ви можете управляти підключеними пристроями, натиснувши кнопку «Підключені пристрої» на верхній панелі.\",\n\t\"connect-new-device\": \"Підключити новий пристрій\",\n\t\"select-entire-screen-to-share\": \"Виберіть екран яким хочете поділитися\",\n\t\"select-app-window-to-share\": \"Виберіть вікно додатка яким хочете поділитися\",\n\t\"refresh\": \"Відновити\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"Від'єднати всі пристрої\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"Ви впевнені, що хочете відключити всі підключені пристрої перегляду?\",\n\t\"this-step-can-not-be-undone\": \"Цей крок неможливо буде скасувати\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"Після цього вам доведеться знову підключити кожен пристрій\",\n\t\"no-cancel\": \"Ні, скасувати\",\n\t\"yes-disconnect-all\": \"Так, від'єднати всі пристрої\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"Доступна нова версія Deskreen CE! Натисніть, щоб завантажити нову версію\",\n\t\"security\": \"Безпека\",\n\t\"general\": \"Загальні\",\n\t\"about\": \"Про нас\",\n\t\"website\": \"Веб сайт\",\n\t\"about-deskreen\": \"Про Deskreen CE\",\n\t\"security-settings\": \"Налаштування безпеки\",\n\t\"color-theme\": \"Схема кольорів\",\n\t\"automatic-updates\": \"Авто-оновлення\",\n\t\"general-settings\": \"Загальні налаштування\",\n\t\"disabled\": \"Відключено\",\n\t\"version\": \"Версія\",\n\t\"copyright\": \"Авторські права\",\n\t\"edit\": \"Правити\",\n\t\"hide-deskreen\": \"Сховати Deskreen CE\",\n\t\"hide-others\": \"Сховати інші\",\n\t\"show-all\": \"Показати всі\",\n\t\"quit\": \"Вийти\",\n\t\"undo\": \"Скасувати\",\n\t\"redo\": \"Повторити\",\n\t\"cut\": \"Вирізати\",\n\t\"copy\": \"Копіювати\",\n\t\"paste\": \"Вставити\",\n\t\"select-all\": \"Вибрати все\",\n\t\"view\": \"Вид\",\n\t\"reload\": \"Перезавантажити\",\n\t\"toggle-full-screen\": \"Переключити на весь екран\",\n\t\"toggle-developer-tools\": \"Відкрити інструменти розробника\",\n\t\"window\": \"Вікно\",\n\t\"minimize\": \"Мінімізувати\",\n\t\"close\": \"Закрити\",\n\t\"bring-all-to-front\": \"Вивести всі наперед\",\n\t\"help\": \"Допомога\",\n\t\"learn-more\": \"Дізнатися більше\",\n\t\"documentation\": \"Документація\",\n\t\"community-discussions\": \"Обговорення в спільноті\",\n\t\"search-issues\": \"Пошук проблем\",\n\t\"translations-below-are-not-added-to-ui-yet-but-your-translations-are-welcome-the-features-will-be-added-soon-so-your-translations-are-needed\": \"\",\n\t\"available-trusted-devices\": \"Доступні довірені пристрої\",\n\t\"make-this-device-trusted\": \"Зробити пристрій довіреним\",\n\t\"click-to-select-other-screen-source-to-share\": \"Нажміть щоб вибрати інше джерело\",\n\t\"click-to-edit-device-alias\": \"Нажміть щоб редагувати аліас пристроя\",\n\t\"trusted-device-id\": \"ID довіреного пристрою\",\n\t\"trusted\": \"Довірений\",\n\t\"make-trusted\": \"Зробити довіреним\",\n\t\"forget-this-device\": \"Забути цей пристрій\",\n\t\"device-alias\": \"Аліас пристрою\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"Автоматично розділяти екран коли пристрій доступний\",\n\t\"all-devices-are-successfully-disconnected\": \"Всі пристрої успішно відключені\",\n\t\"device-was-disconnected\": \"Пристрій відключено\",\n\t\"networking\": \"Мережа\",\n\t\"deskreen-ce-application-port\": \"Порт Deskreen CE додатку\",\n\t\"port-is-already-used-by-other-app\": \"Порт вже використовується іншим додатком\",\n\t\"click-to-change-deskreen-ce-application-port\": \"Нажмість щоб змінити порт Deskreen CE додатку\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"Вкажіть число від 3000 до 64000 яке буде портом Deskreen CE додатка\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"Спробуйте інше число від 3000 до 64000\",\n\t\"select-network-interface\": \"Оберіть мережевий інтерфейс\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"Я знаю ІР мого комп'ютера і хочу ввести його вручну\",\n\t\"type-your-computer-ip\": \"Введіть ІР вашого комп'ютера\",\n\t\"click-to-type-ip-manually\": \"Нажміть щоб ввести ІР вручну\",\n\t\"banned-ips\": \"Заблоковані IP\",\n\t\"ban-new-ip\": \"Заблокувати новий IP\",\n\t\"type-the-ip-you-want-to-ban\": \"Введіть ІР який ви бажаєте заблокувати\",\n\t\"unban-this-ip\": \"Розблокувати цей IP\",\n\t\"unban-all-ips\": \"Розблокувати всі IP\",\n\t\"reset-deskreen-ce-settings-to-default\": \"Скинути Deskreen CE налаштування до стандартних\",\n\t\"ask-user-to-enter-password-when-connecting\": \"Запитувати у користувача пароль при підключені\",\n\t\"change-password\": \"Змінити пароль\",\n\t\"type-a-new-password\": \"Введіть новий пароль\",\n\t\"cancel\": \"Скасувати\",\n\t\"device-status\": \"Статус пристроя\",\n\t\"sharing-screen\": \"Поділитись екраном\",\n\t\"available-no-screen-sharing\": \"Доступно, екран не ділиться\",\n\t\"not-available\": \"Не доступно\",\n\t\"autostart-deskreen-ce-app-on-login\": \"Автозапуск Deskreen CE при вході\",\n\t\"open-deskreen-ce-app-window-on-login\": \"Відкрити Deskreen CE вікно при вході\",\n\t\"use-system-tray\": \"Використовуйте системний трей\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE системний трей\",\n\t\"open-app-window\": \"Відкрити вікно програми\",\n\t\"minimize-to-tray\": \"Сховати в трей\",\n\t\"show-connected-devices\": \"Показати підключені пристрої\",\n\t\"quit-deskreen-ce\": \"Вийти з Deskreen CE\",\n\t\"fix-reset\": \"Виправити та Скинути\",\n\t\"fix-reset-tooltip\": \"Виправити та Скинути. Проблеми з підключенням нових клієнтів? Натискання цієї кнопки може допомогти.\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"Один клієнт перегляду вже підключено.\",\n\t\"viewing-client-connected-label\": \"Клієнт перегляду підключено\",\n\t\"connection-limit-reached-tooltip\": \"Досягнуто ліміту підключень. Від'єднайте поточного клієнта перегляду (або дочекайтеся, поки він від'єднається), щоб підключити нового.\",\n\t\"scan-the-qr-code-to-connect\": \"Скануйте QR-код, щоб підключитися\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"Введіть наступну адресу в адресний рядок браузера на будь-якому пристрої\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE дозволяє одночасно підключити лише один пристрій перегляду.\",\n\t\"this-will-be-available-only-in-pro-version\": \"Можливість підключати більше ніж один пристрій буде доступна лише у Pro версії.\"\n}\n"
  },
  {
    "path": "src/common/locales/zh_CN/translation.json",
    "content": "{\n\t\"hello\": \"您好\",\n\t\"continue\": \"继续\",\n\t\"language\": \"语言\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"捐赠\",\n\t\"get-deskreen-pro\": \"获取 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"获取 Deskreen Pro - 打开下载页面。\",\n\t\"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 变得更好。\",\n\t\"click-to-visit-our-website\": \"点击访问我们的网站\",\n\t\"connected-devices\": \"连接设备\",\n\t\"tutorial\": \"教程\",\n\t\"settings\": \"设置\",\n\t\"connect\": \"连接\",\n\t\"select\": \"选择\",\n\t\"confirm\": \"确认\",\n\t\"scan-the-qr-code\": \"扫描二维码\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"确保您的计算机和屏幕查看设备连接到相同的 Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"或在任何设备的浏览器地址栏中键入以下地址\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"有人试图连接，您允许吗？\",\n\t\"click-to-make-bigger\": \"单击可放大\",\n\t\"click-to-copy\": \"点击复制\",\n\t\"partner-device-info\": \"伙伴设备信息\",\n\t\"device-type\": \"设备类型\",\n\t\"device-ip\": \"设备 IP\",\n\t\"device-browser\": \"设备浏览器\",\n\t\"device-os\": \"设备操作系统\",\n\t\"session-id\": \"会话 ID\",\n\t\"allow\": \"允许\",\n\t\"deny\": \"拒绝\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"您已成功断开设备连接。您可以连接新设备。\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE 更新可用！\",\n\t\"your-current-version-is\": \"您当前的版本是\",\n\t\"click-to-download-new-updated-version\": \"单击以下载新的更新版本\",\n\t\"new-version-available\": \"有新版本可用！\",\n\t\"connected\": \"已连接\",\n\t\"click-to-see-more\": \"单击以查看更多信息\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"这应该与正在尝试连接的设备的屏幕上显示的设备 IP 相匹配。\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"如果 IP 地址不匹配，请单击断开按钮以防止未经授权访问您的计算机屏幕。\",\n\t\"disconnect\": \"断开连接\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"选择要共享的整个屏幕或应用程序窗口\",\n\t\"or\": \"或\",\n\t\"entire-screen\": \"整个屏幕\",\n\t\"application-window\": \"应用程序窗口\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"检查是否一切正常，然后单击确认\",\n\t\"confirm-button-text\": \"确认\",\n\t\"no-i-need-to-choose-other\": \"不，我需要分享其他东西\",\n\t\"done\": \"完成!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"现在，您可以在其他设备上看到您的屏幕。\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"您可以通过单击顶部面板中的已连接设备按钮来管理已连接设备。\",\n\t\"connect-new-device\": \"连接新设备\",\n\t\"select-entire-screen-to-share\": \"选择要共享的整个屏幕\",\n\t\"select-app-window-to-share\": \"选择要共享的应用程序窗口\",\n\t\"refresh\": \"刷新\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"断开所有设备的连接\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"是否确实要断开所有连接的观看设备？\",\n\t\"this-step-can-not-be-undone\": \"此步骤无法恢复\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"您必须再次手动连接所有设备\",\n\t\"no-cancel\": \"否，取消\",\n\t\"yes-disconnect-all\": \"是，全部断开\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"新版本的 Deskreen CE 现已发布！点击下载新版本\",\n\t\"security\": \"安全\",\n\t\"general\": \"通用\",\n\t\"about\": \"关于\",\n\t\"website\": \"网站\",\n\t\"about-deskreen\": \"关于 Deskreen CE\",\n\t\"security-settings\": \"安全设置\",\n\t\"color-theme\": \"色彩主题\",\n\t\"automatic-updates\": \"自动更新\",\n\t\"general-settings\": \"通用设置\",\n\t\"disabled\": \"禁用\",\n\t\"version\": \"版本\",\n\t\"copyright\": \"版权\",\n\t\"edit\": \"编辑\",\n\t\"hide-deskreen\": \"隐藏 Deskreen CE\",\n\t\"hide-others\": \"隐藏其他\",\n\t\"show-all\": \"显示所有\",\n\t\"quit\": \"退出\",\n\t\"undo\": \"取消\",\n\t\"redo\": \"重做\",\n\t\"cut\": \"剪切\",\n\t\"copy\": \"复制\",\n\t\"paste\": \"粘贴\",\n\t\"select-all\": \"选择所有\",\n\t\"view\": \"查看\",\n\t\"reload\": \"重载\",\n\t\"toggle-full-screen\": \"切换全屏\",\n\t\"toggle-developer-tools\": \"切换开发人员工具\",\n\t\"window\": \"窗口\",\n\t\"minimize\": \"最小化\",\n\t\"close\": \"关闭\",\n\t\"bring-all-to-front\": \"全部放在前面\",\n\t\"help\": \"帮助\",\n\t\"learn-more\": \"了解更多\",\n\t\"documentation\": \"文档\",\n\t\"community-discussions\": \"社区讨论\",\n\t\"search-issues\": \"搜索 Issues\",\n\t\"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，但欢迎您的翻译！功能将很快添加，因此需要您的翻译\",\n\t\"available-trusted-devices\": \"可用的受信设备\",\n\t\"make-this-device-trusted\": \"使此设备受信\",\n\t\"click-to-select-other-screen-source-to-share\": \"单击以选择要共享的其他屏幕源\",\n\t\"click-to-edit-device-alias\": \"单击以编辑设备别名\",\n\t\"trusted-device-id\": \"受信的设备 ID\",\n\t\"trusted\": \"受信\",\n\t\"make-trusted\": \"使受信\",\n\t\"forget-this-device\": \"忘记这个设备\",\n\t\"device-alias\": \"设备别名\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"当设备可用时自动共享上一个整屏信号源\",\n\t\"all-devices-are-successfully-disconnected\": \"所有设备均已成功断开连接\",\n\t\"device-was-disconnected\": \"设备已断开连接\",\n\t\"networking\": \"联网\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE 应用程序端口\",\n\t\"port-is-already-used-by-other-app\": \"端口已被其他应用程序使用\",\n\t\"click-to-change-deskreen-ce-application-port\": \"单击以更改 Deskreen CE 应用程序端口\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"输入一个介于 3000 到 64000 之间的数字以用作 Deskreen CE 应用程序端口\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"输入范围在 3000 到 64000 之间的另一个数字\",\n\t\"select-network-interface\": \"选择网络接口\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"我知道我电脑的 IP 地址，我想手动输入\",\n\t\"type-your-computer-ip\": \"输入您的计算机 IP\",\n\t\"click-to-type-ip-manually\": \"单击以手动输入 IP\",\n\t\"banned-ips\": \"被禁 IP\",\n\t\"ban-new-ip\": \"禁止新 IP\",\n\t\"type-the-ip-you-want-to-ban\": \"键入您要禁用的 IP\",\n\t\"unban-this-ip\": \"解禁此 IP\",\n\t\"unban-all-ips\": \"解禁所有 IP\",\n\t\"reset-deskreen-ce-settings-to-default\": \"将 Deskreen CE 设置重置为默认值\",\n\t\"ask-user-to-enter-password-when-connecting\": \"连接时要求用户输入密码\",\n\t\"change-password\": \"更改密码\",\n\t\"type-a-new-password\": \"输入新密码\",\n\t\"cancel\": \"取消\",\n\t\"device-status\": \"设备状态\",\n\t\"sharing-screen\": \"共享屏幕\",\n\t\"available-no-screen-sharing\": \"可用，无屏幕共享\",\n\t\"not-available\": \"无可用\",\n\t\"autostart-deskreen-ce-app-on-login\": \"登录时自动启动 Deskreen CE 应用程序\",\n\t\"open-deskreen-ce-app-window-on-login\": \"登录时打开 Deskreen CE 应用程序窗口\",\n\t\"use-system-tray\": \"使用系统托盘\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE 系统托盘\",\n\t\"open-app-window\": \"打开应用程序窗口\",\n\t\"minimize-to-tray\": \"最小化到托盘\",\n\t\"show-connected-devices\": \"显示连接的设备\",\n\t\"quit-deskreen-ce\": \"退出 Deskreen CE\",\n\t\"fix-reset\": \"修复并重置\",\n\t\"fix-reset-tooltip\": \"修复并重置。连接新客户端时遇到问题？单击此按钮可能会对您有所帮助。\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"已有一个观看客户端连接。\",\n\t\"viewing-client-connected-label\": \"观看客户端已连接\",\n\t\"connection-limit-reached-tooltip\": \"已达到连接上限。请断开当前的观看客户端（或等待其断开）后再连接新的客户端。\",\n\t\"scan-the-qr-code-to-connect\": \"扫描二维码以连接\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"在任意设备的浏览器地址栏输入以下地址\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE 仅允许同时连接一个观看客户端设备。\",\n\t\"this-will-be-available-only-in-pro-version\": \"多设备连接选项仅在 Pro 版本中可用。\"\n}\n"
  },
  {
    "path": "src/common/locales/zh_TW/translation.json",
    "content": "{\n\t\"hello\": \"您好\",\n\t\"continue\": \"繼續\",\n\t\"language\": \"語言\",\n\t\"ru\": \"Русский\",\n\t\"en\": \"English\",\n\t\"es\": \"Español\",\n\t\"ua\": \"Українська\",\n\t\"zh-cn\": \"简体中文\",\n\t\"zh-tw\": \"繁體中文\",\n\t\"da\": \"Dansk\",\n\t\"de\": \"Deutsch\",\n\t\"fi\": \"Suomi\",\n\t\"ko\": \"한국어\",\n\t\"it\": \"Italiano\",\n\t\"ja\": \"日本語\",\n\t\"fr\": \"Français\",\n\t\"nl\": \"Nederlands\",\n\t\"sv\": \"Svenska\",\n\t\"donate\": \"捐贈\",\n\t\"get-deskreen-pro\": \"取得 Deskreen Pro\",\n\t\"get-deskreen-pro-tooltip\": \"取得 Deskreen Pro - 開啟下載頁面。\",\n\t\"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 變得更好。\",\n\t\"click-to-visit-our-website\": \"點選訪問我們的網站\",\n\t\"connected-devices\": \"連線裝置\",\n\t\"tutorial\": \"教程\",\n\t\"settings\": \"設定\",\n\t\"connect\": \"連線\",\n\t\"select\": \"選擇\",\n\t\"confirm\": \"確認\",\n\t\"scan-the-qr-code\": \"掃描二維碼\",\n\t\"make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi\": \"確保您的計算機和螢幕檢視裝置連線到相同的 Wi-Fi\",\n\t\"or-type-the-following-address-in-browser-address-bar-on-any-device\": \"或在任何裝置的瀏覽器位址列中鍵入以下地址\",\n\t\"someone-is-trying-to-connect-do-you-allow\": \"有人試圖連線，您允許嗎？\",\n\t\"click-to-make-bigger\": \"單擊可放大\",\n\t\"click-to-copy\": \"點選複製\",\n\t\"partner-device-info\": \"夥伴裝置資訊\",\n\t\"device-type\": \"裝置型別\",\n\t\"device-ip\": \"裝置 IP\",\n\t\"device-browser\": \"裝置瀏覽器\",\n\t\"device-os\": \"裝置作業系統\",\n\t\"session-id\": \"會話 ID\",\n\t\"allow\": \"允許\",\n\t\"deny\": \"拒絕\",\n\t\"device-is-successfully-disconnected-by-you-you-can-connect-a-new-device\": \"您已成功斷開裝置連線。您可以連線新裝置。\",\n\t\"deskreen-ce-update-is-available\": \"Deskreen CE 更新可用！\",\n\t\"your-current-version-is\": \"您當前的版本是\",\n\t\"click-to-download-new-updated-version\": \"單擊以下載新的更新版本\",\n\t\"new-version-available\": \"有新版本可用！\",\n\t\"connected\": \"已連線\",\n\t\"click-to-see-more\": \"單擊以檢視更多資訊\",\n\t\"this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect\": \"這應該與正在嘗試連線的裝置的螢幕上顯示的裝置 IP 相匹配。\",\n\t\"if-ip-addresses-dont-match-click-disconnect-button\": \"如果 IP 地址不匹配，請單擊斷開按鈕以防止未經授權訪問您的計算機螢幕。\",\n\t\"disconnect\": \"斷開連線\",\n\t\"choose-entire-screen-or-app-window-you-want-to-share\": \"選擇要共享的整個螢幕或應用程式視窗\",\n\t\"or\": \"或\",\n\t\"entire-screen\": \"整個螢幕\",\n\t\"application-window\": \"應用程式視窗\",\n\t\"check-if-all-is-ok-and-click-confirm\": \"檢查是否一切正常，然後單擊確認\",\n\t\"confirm-button-text\": \"確認\",\n\t\"no-i-need-to-choose-other\": \"不，我需要分享其他東西\",\n\t\"done\": \"完成!\",\n\t\"now-you-can-see-your-screen-on-other-device\": \"現在，您可以在其他裝置上看到您的螢幕。\",\n\t\"you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel\": \"您可以透過單擊頂部面板中的已連線裝置按鈕來管理已連線裝置。\",\n\t\"connect-new-device\": \"連線新裝置\",\n\t\"select-entire-screen-to-share\": \"選擇要共享的整個螢幕\",\n\t\"select-app-window-to-share\": \"選擇要共享的應用程式視窗\",\n\t\"refresh\": \"重新整理\",\n\t\"re-initiate-connection\": \"Re-initiate Connection\",\n\t\"disconnect-all-devices\": \"斷開所有裝置的連線\",\n\t\"are-you-sure-you-want-to-disconnect-all-connected-viewing-devices\": \"是否確實要斷開所有連線的觀看裝置？\",\n\t\"this-step-can-not-be-undone\": \"此步驟無法恢復\",\n\t\"you-will-have-to-connect-all-devices-manually-again\": \"您必須再次手動連線所有裝置\",\n\t\"no-cancel\": \"否，取消\",\n\t\"yes-disconnect-all\": \"是，全部斷開\",\n\t\"a-new-version-of-deskreen-ce-is-available-click-to-download-new-version\": \"新版本的 Deskreen CE 現已釋出！點選下載新版本\",\n\t\"security\": \"安全\",\n\t\"general\": \"通用\",\n\t\"about\": \"關於\",\n\t\"website\": \"網站\",\n\t\"about-deskreen\": \"關於 Deskreen CE\",\n\t\"security-settings\": \"安全設定\",\n\t\"color-theme\": \"色彩主題\",\n\t\"automatic-updates\": \"自動更新\",\n\t\"general-settings\": \"通用設定\",\n\t\"disabled\": \"禁用\",\n\t\"version\": \"版本\",\n\t\"copyright\": \"版權\",\n\t\"edit\": \"編輯\",\n\t\"hide-deskreen\": \"隱藏 Deskreen CE\",\n\t\"hide-others\": \"隱藏其他\",\n\t\"show-all\": \"顯示所有\",\n\t\"quit\": \"退出\",\n\t\"undo\": \"取消\",\n\t\"redo\": \"重做\",\n\t\"cut\": \"剪下\",\n\t\"copy\": \"複製\",\n\t\"paste\": \"貼上\",\n\t\"select-all\": \"選擇所有\",\n\t\"view\": \"檢視\",\n\t\"reload\": \"過載\",\n\t\"toggle-full-screen\": \"切換全屏\",\n\t\"toggle-developer-tools\": \"切換開發人員工具\",\n\t\"window\": \"視窗\",\n\t\"minimize\": \"最小化\",\n\t\"close\": \"關閉\",\n\t\"bring-all-to-front\": \"全部放在前面\",\n\t\"help\": \"幫助\",\n\t\"learn-more\": \"瞭解更多\",\n\t\"documentation\": \"文件\",\n\t\"community-discussions\": \"社群討論\",\n\t\"search-issues\": \"搜尋 issues\",\n\t\"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，但歡迎您的翻譯！功能將很快新增，因此需要您的翻譯\",\n\t\"available-trusted-devices\": \"可用的受信裝置\",\n\t\"make-this-device-trusted\": \"使此裝置受信\",\n\t\"click-to-select-other-screen-source-to-share\": \"單擊以選擇要共享的其他螢幕源\",\n\t\"click-to-edit-device-alias\": \"單擊以編輯裝置別名\",\n\t\"trusted-device-id\": \"受信的裝置 ID\",\n\t\"trusted\": \"受信\",\n\t\"make-trusted\": \"使受信\",\n\t\"forget-this-device\": \"忘記這個裝置\",\n\t\"device-alias\": \"裝置別名\",\n\t\"auto-share-last-entire-screen-source-when-device-is-available\": \"當裝置可用時自動共享上一個整屏訊號源\",\n\t\"all-devices-are-successfully-disconnected\": \"所有裝置均已成功斷開連線\",\n\t\"device-was-disconnected\": \"裝置已斷開連線\",\n\t\"networking\": \"聯網\",\n\t\"deskreen-ce-application-port\": \"Deskreen CE 應用程式埠\",\n\t\"port-is-already-used-by-other-app\": \"埠已被其他應用程式使用\",\n\t\"click-to-change-deskreen-ce-application-port\": \"單擊以更改 Deskreen CE 應用程式埠\",\n\t\"type-a-number-from-3000-to-64000-to-use-as-a-deskreen-ce-application-port\": \"輸入一個介於 3000 到 64000 之間的數字以用作 Deskreen CE 應用程式埠\",\n\t\"type-another-number-in-range-from-3000-to-64000\": \"輸入範圍在 3000 到 64000 之間的另一個數字\",\n\t\"select-network-interface\": \"選擇網路介面\",\n\t\"i-know-ip-of-my-computer-and-i-want-to-type-it-manually\": \"我知道我電腦的 IP 地址，我想手動輸入\",\n\t\"type-your-computer-ip\": \"輸入您的計算機 IP\",\n\t\"click-to-type-ip-manually\": \"單擊以手動輸入 IP\",\n\t\"banned-ips\": \"被禁 IP\",\n\t\"ban-new-ip\": \"禁止新 IP\",\n\t\"type-the-ip-you-want-to-ban\": \"鍵入您要禁用的 IP\",\n\t\"unban-this-ip\": \"解禁此 IP\",\n\t\"unban-all-ips\": \"解禁所有 IP\",\n\t\"reset-deskreen-ce-settings-to-default\": \"將 Deskreen CE 設定重置為預設值\",\n\t\"ask-user-to-enter-password-when-connecting\": \"連線時要求使用者輸入密碼\",\n\t\"change-password\": \"更改密碼\",\n\t\"type-a-new-password\": \"輸入新密碼\",\n\t\"cancel\": \"取消\",\n\t\"device-status\": \"裝置狀態\",\n\t\"sharing-screen\": \"共享螢幕\",\n\t\"available-no-screen-sharing\": \"可用無螢幕共享\",\n\t\"not-available\": \"無可用\",\n\t\"autostart-deskreen-ce-app-on-login\": \"登入時自動啟動 Deskreen CE 應用程式\",\n\t\"open-deskreen-ce-app-window-on-login\": \"登入時開啟 Deskreen CE 應用程式視窗\",\n\t\"use-system-tray\": \"使用系統托盤\",\n\t\"deskreen-ce-system-tray\": \"Deskreen CE 系統托盤\",\n\t\"open-app-window\": \"開啟應用程式視窗\",\n\t\"minimize-to-tray\": \"最小化到托盤\",\n\t\"show-connected-devices\": \"顯示連線的裝置\",\n\t\"quit-deskreen-ce\": \"退出 Deskreen CE\",\n\t\"fix-reset\": \"修復並重設\",\n\t\"fix-reset-tooltip\": \"修復並重設。連接新客戶端時遇到問題？點擊此按鈕可能會對您有所幫助。\",\n\t\"deskreen-logo\": \"Deskreen logo\",\n\t\"one-viewing-client-is-connected-already\": \"已有一個觀看用戶端連線。\",\n\t\"viewing-client-connected-label\": \"觀看用戶端已連線\",\n\t\"connection-limit-reached-tooltip\": \"已達到連線上限。請先斷開目前的觀看用戶端（或等待其斷線），再連接新的用戶端。\",\n\t\"scan-the-qr-code-to-connect\": \"掃描 QR 碼以連線\",\n\t\"enter-the-following-address-in-browser-address-bar-on-any-device\": \"在任何裝置的瀏覽器網址列輸入以下地址\",\n\t\"deskreen-ce-allows-only-one-client-at-same-time\": \"Deskreen CE 僅允許同時連接一個觀看用戶端裝置。\",\n\t\"this-will-be-available-only-in-pro-version\": \"多個裝置的連線選項僅在 Pro 版本中可用。\"\n}\n"
  },
  {
    "path": "src/common/rateLimitedConsole.ts",
    "content": "// rate-limited console wrapper to prevent log spam and memory bloat\n// this helps prevent excessive console.log calls from accumulating in memory\n\ninterface LogEntry {\n\ttimestamp: number;\n\tmessage: string;\n\tcount: number;\n}\n\nconst logCache = new Map<string, LogEntry>();\nconst RATE_LIMIT_WINDOW = 5000; // 5 seconds\nconst MAX_LOGS_PER_WINDOW = 10; // max 10 identical logs per 5 seconds\nconst CACHE_CLEANUP_INTERVAL = 30000; // clean cache every 30 seconds\n\n// preserve original console methods before overriding (must be first)\nconst originalConsole = {\n\tlog: console.log.bind(console),\n\terror: console.error.bind(console),\n\twarn: console.warn.bind(console),\n\tinfo: console.info.bind(console),\n\tdebug: console.debug.bind(console),\n};\n\nfunction getLogKey(...args: unknown[]): string {\n\t// create a key from the log message (first 100 chars to avoid huge keys)\n\tconst message = args\n\t\t.map((arg) => {\n\t\t\tif (typeof arg === 'string') {\n\t\t\t\treturn arg.substring(0, 100);\n\t\t\t}\n\t\t\tif (typeof arg === 'object') {\n\t\t\t\ttry {\n\t\t\t\t\treturn JSON.stringify(arg).substring(0, 100);\n\t\t\t\t} catch {\n\t\t\t\t\treturn String(arg).substring(0, 100);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String(arg).substring(0, 100);\n\t\t})\n\t\t.join(' ');\n\treturn message;\n}\n\nfunction shouldLog(key: string): boolean {\n\tconst now = Date.now();\n\tconst entry = logCache.get(key);\n\n\tif (!entry) {\n\t\tlogCache.set(key, { timestamp: now, message: key, count: 1 });\n\t\treturn true;\n\t}\n\n\t// if entry is older than window, reset it\n\tif (now - entry.timestamp > RATE_LIMIT_WINDOW) {\n\t\tentry.timestamp = now;\n\t\tentry.count = 1;\n\t\treturn true;\n\t}\n\n\t// increment count\n\tentry.count += 1;\n\n\t// if under limit, allow log\n\tif (entry.count <= MAX_LOGS_PER_WINDOW) {\n\t\treturn true;\n\t}\n\n\t// if exactly at limit, log a warning about rate limiting\n\tif (entry.count === MAX_LOGS_PER_WINDOW + 1) {\n\t\toriginalConsole.warn(\n\t\t\t`[Rate Limited] Log message suppressed (exceeded ${MAX_LOGS_PER_WINDOW} logs in ${RATE_LIMIT_WINDOW}ms):`,\n\t\t\tkey.substring(0, 200),\n\t\t);\n\t}\n\n\t// suppress the log\n\treturn false;\n}\n\nfunction cleanupCache(): void {\n\tconst now = Date.now();\n\tfor (const [key, entry] of logCache.entries()) {\n\t\t// remove entries older than 1 minute\n\t\tif (now - entry.timestamp > 60000) {\n\t\t\tlogCache.delete(key);\n\t\t}\n\t}\n}\n\n// rate-limited console methods\nconst rateLimitedConsole = {\n\tlog: (...args: unknown[]) => {\n\t\tconst key = getLogKey(...args);\n\t\tif (shouldLog(key)) {\n\t\t\t// use original console.log to avoid recursion\n\t\t\toriginalConsole.log(...args);\n\t\t}\n\t},\n\terror: (...args: unknown[]) => {\n\t\t// errors are never rate-limited\n\t\toriginalConsole.error(...args);\n\t},\n\twarn: (...args: unknown[]) => {\n\t\t// warnings are never rate-limited\n\t\toriginalConsole.warn(...args);\n\t},\n\tinfo: (...args: unknown[]) => {\n\t\tconst key = getLogKey(...args);\n\t\tif (shouldLog(key)) {\n\t\t\toriginalConsole.info(...args);\n\t\t}\n\t},\n\tdebug: (...args: unknown[]) => {\n\t\tconst key = getLogKey(...args);\n\t\tif (shouldLog(key)) {\n\t\t\toriginalConsole.debug(...args);\n\t\t}\n\t},\n};\n\n// start periodic cache cleanup\nlet cleanupInterval: NodeJS.Timeout | null = null;\n\nexport function startConsoleRateLimiting(): void {\n\tif (cleanupInterval) {\n\t\treturn; // already started\n\t}\n\n\tcleanupInterval = setInterval(() => {\n\t\tcleanupCache();\n\t}, CACHE_CLEANUP_INTERVAL);\n}\n\nexport function stopConsoleRateLimiting(): void {\n\tif (cleanupInterval) {\n\t\tclearInterval(cleanupInterval);\n\t\tcleanupInterval = null;\n\t}\n\tlogCache.clear();\n}\n\n// override global console to use rate-limited versions\nexport function overrideGlobalConsole(): void {\n\t// override console methods with rate-limited versions\n\tconsole.log = rateLimitedConsole.log;\n\tconsole.info = rateLimitedConsole.info;\n\tconsole.debug = rateLimitedConsole.debug;\n\n\t// keep errors and warnings always available (never rate-limited)\n\t// but use original to ensure they're always shown\n\tconsole.error = originalConsole.error;\n\tconsole.warn = originalConsole.warn;\n}\n"
  },
  {
    "path": "src/features/ConnectedDevicesService/index.ts",
    "content": "import { Device } from '../../common/Device';\n\nexport const nullDevice: Device = {\n\tid: '',\n\tsharingSessionID: '',\n\tdeviceOS: '',\n\tdeviceType: '',\n\tdeviceIP: '',\n\tdeviceBrowser: '',\n\tdeviceScreenWidth: -1,\n\tdeviceScreenHeight: -1,\n\tdeviceRoomId: '',\n};\n\nconst SLOT_VIOLATION_MESSAGE = 'single viewer slot is already occupied';\n\ntype ViewerConnectionAvailability = 'available' | 'occupied';\n\nclass SingleViewerSlot {\n\tprivate device: Readonly<Device> | null = null;\n\n\toccupy(device: Device): void {\n\t\tif (this.device && this.device.id !== device.id) {\n\t\t\tthrow new Error(SLOT_VIOLATION_MESSAGE);\n\t\t}\n\t\tthis.device = Object.freeze({ ...device });\n\t}\n\n\treleaseById(deviceIDToRemove: string): boolean {\n\t\tif (!this.device) return false;\n\t\tif (this.device.id !== deviceIDToRemove) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.device = null;\n\t\treturn true;\n\t}\n\n\trelease(): void {\n\t\tthis.device = null;\n\t}\n\n\tisAvailable(): boolean {\n\t\treturn this.device === null;\n\t}\n\n\tsnapshot(): Device[] {\n\t\tif (!this.device) return [];\n\t\treturn [{ ...this.device }];\n\t}\n\n\tisOccupiedBy(deviceID: string): boolean {\n\t\tif (!this.device) return false;\n\t\treturn this.device.id === deviceID;\n\t}\n}\n\nexport class ConnectedDevicesService {\n\tprivate readonly slot = new SingleViewerSlot();\n\n\tpendingConnectionDevice: Device = nullDevice;\n\n\tprivate readonly availabilityListeners = new Set<\n\t\t(state: ViewerConnectionAvailability) => void\n\t>();\n\n\tresetPendingConnectionDevice(): void {\n\t\tthis.pendingConnectionDevice = nullDevice;\n\t}\n\n\tgetDevices(): Device[] {\n\t\treturn this.slot.snapshot();\n\t}\n\n\tisSlotAvailable(): boolean {\n\t\treturn this.slot.isAvailable();\n\t}\n\n\taddAvailabilityListener(\n\t\tlistener: (state: ViewerConnectionAvailability) => void,\n\t): () => void {\n\t\tthis.availabilityListeners.add(listener);\n\t\tlistener(this.getAvailabilityState());\n\t\treturn () => {\n\t\t\tthis.availabilityListeners.delete(listener);\n\t\t};\n\t}\n\n\tdisconnectAllDevices(): void {\n\t\tthis.slot.release();\n\t\tthis.notifyAvailabilityListeners();\n\t}\n\n\tdisconnectDeviceByID(deviceIDToRemove: string): Promise<undefined> {\n\t\treturn new Promise<undefined>((resolve) => {\n\t\t\tthis.slot.releaseById(deviceIDToRemove);\n\t\t\tthis.notifyAvailabilityListeners();\n\t\t\tresolve(undefined);\n\t\t});\n\t}\n\n\taddDevice(device: Device): void {\n\t\ttry {\n\t\t\tthis.slot.occupy(device);\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && error.message === SLOT_VIOLATION_MESSAGE) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tthis.notifyAvailabilityListeners();\n\t}\n\n\tsetPendingConnectionDevice(device: Device): void {\n\t\tthis.pendingConnectionDevice = device;\n\t}\n\n\tprivate getAvailabilityState(): ViewerConnectionAvailability {\n\t\treturn this.slot.isAvailable() ? 'available' : 'occupied';\n\t}\n\n\tprivate notifyAvailabilityListeners(): void {\n\t\tconst state = this.getAvailabilityState();\n\t\tthis.availabilityListeners.forEach((listener) => {\n\t\t\ttry {\n\t\t\t\tlistener(state);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('connected devices availability listener failed', error);\n\t\t\t}\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/features/DesktopCapturerSourcesService/index.ts",
    "content": "/* eslint-disable no-async-promise-executor */\n/* eslint-disable @typescript-eslint/no-unused-vars */\n\nimport { desktopCapturer, DesktopCapturerSource } from 'electron';\nimport Logger from '../../main/utils/LoggerWithFilePrefix';\nimport DesktopCapturerSourceType from '../../common/DesktopCapturerSourceType';\nimport isLinuxWaylandSession from '../../main/utils/isLinuxWaylandSession';\n\nexport interface DesktopCapturerSourceWithType {\n\tsource: import('electron').DesktopCapturerSource;\n\ttype: import('../../common/DesktopCapturerSourceType').default;\n}\n\nexport function getSourceTypeFromSourceID(\n\tid: string,\n): DesktopCapturerSourceType {\n\tif (id.includes(DesktopCapturerSourceType.SCREEN)) {\n\t\treturn DesktopCapturerSourceType.SCREEN;\n\t}\n\treturn DesktopCapturerSourceType.WINDOW;\n}\n\ntype SourcesDisappearListener = (ids: string[]) => void;\ntype SharingSessionID = string;\n\nclass DesktopCapturerSourcesService {\n\tsources: Map<string, DesktopCapturerSourceWithType>;\n\n\tlastAvailableScreenIDs: string[];\n\n\tlastAvailableWindowIDs: string[];\n\n\tonWindowClosedListeners: Map<SharingSessionID, SourcesDisappearListener[]>;\n\n\tonScreenDisconnectedListeners: Map<\n\t\tSharingSessionID,\n\t\tSourcesDisappearListener[]\n\t>;\n\n\tlog = new Logger(__filename);\n\n\tautoRefreshEnabled: boolean;\n\n\trefreshPromise: Promise<void> | null;\n\n\tportalSelectionPromise: Promise<DesktopCapturerSource | null> | null;\n\n\tconstructor() {\n\t\tthis.sources = new Map<string, DesktopCapturerSourceWithType>();\n\t\tthis.lastAvailableScreenIDs = [];\n\t\tthis.lastAvailableWindowIDs = [];\n\t\tthis.onWindowClosedListeners = new Map<\n\t\t\tSharingSessionID,\n\t\t\tSourcesDisappearListener[]\n\t\t>();\n\t\tthis.onScreenDisconnectedListeners = new Map<\n\t\t\tSharingSessionID,\n\t\t\tSourcesDisappearListener[]\n\t\t>();\n\t\tthis.autoRefreshEnabled = !isLinuxWaylandSession;\n\t\tthis.refreshPromise = null;\n\t\tthis.portalSelectionPromise = null;\n\n\t\tif (this.autoRefreshEnabled) {\n\t\t\tthis.startRefreshDesktopCapturerSourcesLoop();\n\t\t} else {\n\t\t\tthis.log.debug(\n\t\t\t\t'skipping desktop capturer auto refresh on wayland session',\n\t\t\t);\n\t\t}\n\t\tthis.startPollForInactiveListenersLoop();\n\t}\n\n\tgetSourcesMap(): Map<string, DesktopCapturerSourceWithType> {\n\t\treturn this.sources;\n\t}\n\n\tstartRefreshDesktopCapturerSourcesLoop(): void {\n\t\tif (!this.autoRefreshEnabled) {\n\t\t\treturn;\n\t\t}\n\t\tsetInterval(() => {\n\t\t\tthis.refreshDesktopCapturerSources();\n\t\t}, 5000);\n\t}\n\n\tgetScreenSources(): DesktopCapturerSource[] {\n\t\tconst screenSources: DesktopCapturerSource[] = [];\n\t\t[...this.sources.keys()].forEach((key) => {\n\t\t\tconst source = this.sources.get(key);\n\t\t\tif (!source) return;\n\t\t\tif (source.type === DesktopCapturerSourceType.SCREEN) {\n\t\t\t\tscreenSources.push(source.source);\n\t\t\t}\n\t\t});\n\t\treturn screenSources;\n\t}\n\n\tgetAppWindowSources(): DesktopCapturerSource[] {\n\t\tconst appWindowSources: DesktopCapturerSource[] = [];\n\t\t[...this.sources.keys()].forEach((key) => {\n\t\t\tconst source = this.sources.get(key);\n\t\t\tif (!source) return;\n\t\t\tif (source.type === DesktopCapturerSourceType.WINDOW) {\n\t\t\t\tappWindowSources.push(source.source);\n\t\t\t}\n\t\t});\n\t\treturn appWindowSources;\n\t}\n\n\tgetSourceDisplayIDByDisplayCapturerSourceID(sourceID: string): string {\n\t\tlet displayID = '';\n\t\t[...this.sources.keys()].forEach((key) => {\n\t\t\tconst source = this.sources.get(key);\n\t\t\tif (!source) return;\n\t\t\tif (source.source.id === sourceID) {\n\t\t\t\tdisplayID = source.source.display_id;\n\t\t\t}\n\t\t});\n\t\treturn displayID;\n\t}\n\n\taddWindowClosedListener(\n\t\t_sharingSessionID: string,\n\t\t_callback: SourcesDisappearListener,\n\t): void {\n\t\t// TODO: implement logic\n\t}\n\n\taddScreenDisconnectedListener(\n\t\t_sharingSessionID: string,\n\t\t_callback: SourcesDisappearListener,\n\t): void {\n\t\t// TODO: implement logic\n\t}\n\n\tasync updateDesktopCapturerSources(): Promise<void> {\n\t\t// TODO: implement logic of checking if last sources match new sources,\n\t\t// TODO: if source is gone, do proper actions and notify user if needed\n\t\t// this.lastAvailableScreenIDs = [];\n\t\t// this.lastAvailableWindowIDs = [];\n\n\t\t// [...this.sources.keys()].forEach((key) => {\n\t\t//   const oldSource = this.sources.get(key);\n\t\t//   if (!oldSource) return;\n\t\t//   if (oldSource.type === DesktopCapturerSourceType.WINDOW) {\n\t\t//     this.lastAvailableWindowIDs.push(oldSource.source.id);\n\t\t//   } else if (oldSource.type === DesktopCapturerSourceType.SCREEN) {\n\t\t//     this.lastAvailableScreenIDs.push(oldSource.source.id);\n\t\t//   }\n\t\t// });\n\n\t\tthis.sources = await this.getDesktopCapturerSources();\n\t}\n\n\tasync getDesktopCapturerSources(): Promise<\n\t\tMap<string, DesktopCapturerSourceWithType>\n\t> {\n\t\tconst newSources = new Map<string, DesktopCapturerSourceWithType>();\n\t\tconst capturerSources = await desktopCapturer.getSources({\n\t\t\ttypes: [\n\t\t\t\tDesktopCapturerSourceType.WINDOW,\n\t\t\t\tDesktopCapturerSourceType.SCREEN,\n\t\t\t],\n\t\t\tthumbnailSize: { width: 500, height: 500 },\n\t\t\tfetchWindowIcons: true, // TODO: use window icons in app UI !\n\t\t});\n\t\tcapturerSources.forEach((source) => {\n\t\t\tnewSources.set(source.id, {\n\t\t\t\ttype: getSourceTypeFromSourceID(source.id),\n\t\t\t\tsource,\n\t\t\t});\n\t\t});\n\t\treturn newSources;\n\t}\n\n\tasync refreshDesktopCapturerSources(): Promise<void> {\n\t\t// TODO: implement get available sources logic here;\n\t\tif (this.refreshPromise) {\n\t\t\treturn this.refreshPromise;\n\t\t}\n\n\t\tthis.refreshPromise = (async () => {\n\t\t\ttry {\n\t\t\t\tawait this.updateDesktopCapturerSources();\n\t\t\t\t// eventually run checkers that emit events\n\t\t\t\tthis.checkForClosedWindows();\n\t\t\t\tthis.checkForScreensDisconnected();\n\t\t\t} catch (e) {\n\t\t\t\tthis.log.error(e);\n\t\t\t} finally {\n\t\t\t\tthis.refreshPromise = null;\n\t\t\t}\n\t\t})();\n\n\t\treturn this.refreshPromise;\n\t}\n\n\tasync requestPortalSource(\n\t\ttypes: DesktopCapturerSourceType[],\n\t): Promise<DesktopCapturerSource | null> {\n\t\tif (this.portalSelectionPromise) {\n\t\t\treturn this.portalSelectionPromise;\n\t\t}\n\n\t\tthis.portalSelectionPromise = (async () => {\n\t\t\ttry {\n\t\t\t\tconst sources = await desktopCapturer.getSources({\n\t\t\t\t\ttypes,\n\t\t\t\t\tthumbnailSize: { width: 500, height: 500 },\n\t\t\t\t\tfetchWindowIcons: types.includes(DesktopCapturerSourceType.WINDOW),\n\t\t\t\t});\n\t\t\t\tif (sources.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst selectedSourcesMap = new Map<\n\t\t\t\t\tstring,\n\t\t\t\t\tDesktopCapturerSourceWithType\n\t\t\t\t>(this.sources);\n\t\t\t\tconst defaultType = types.length === 1 ? types[0] : undefined;\n\n\t\t\t\tsources.forEach((source) => {\n\t\t\t\t\tselectedSourcesMap.set(source.id, {\n\t\t\t\t\t\ttype: defaultType ?? getSourceTypeFromSourceID(source.id),\n\t\t\t\t\t\tsource,\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\t\tthis.sources = selectedSourcesMap;\n\n\t\t\t\treturn sources[0];\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.error(error);\n\t\t\t\treturn null;\n\t\t\t} finally {\n\t\t\t\tthis.portalSelectionPromise = null;\n\t\t\t}\n\t\t})();\n\n\t\treturn this.portalSelectionPromise;\n\t}\n\n\tstartPollForInactiveListenersLoop(): void {\n\t\tsetInterval(\n\t\t\t() => {\n\t\t\t\t// TODO: implement logic\n\t\t\t\t// if session ID no longer exists in SharingSessionsService -> remove its listener object\n\t\t\t},\n\t\t\t1000 * 60 * 60,\n\t\t); // runs every hour in infinite loop\n\t}\n\n\tcheckForClosedWindows(): void {\n\t\t// TODO: implement logic\n\t\t// const isSomeWindowsClosed = false;\n\t\t// const closedWindowsIDs: string[] = [];\n\t\t// if (isSomeWindowsClosed) {\n\t\t//   this.notifyOnWindowsClosedListeners(closedWindowsIDs);\n\t\t// }\n\t}\n\n\tnotifyOnWindowsClosedListeners(_closedWindowsIDs: string[]): void {\n\t\t// TODO: implement logic\n\t}\n\n\tcheckForScreensDisconnected(): void {\n\t\t// TODO: implement logic\n\t\t// const isSomeScreensDisconnected = false;\n\t\t// const disconnectedScreensIDs: string[] = [];\n\t\t// if (isSomeScreensDisconnected) {\n\t\t//   this.notifyOnScreensDisconnectedListeners(disconnectedScreensIDs);\n\t\t// }\n\t}\n\n\tnotifyOnScreensDisconnectedListeners(\n\t\t_disconnectedScreensIDs: string[],\n\t): void {\n\t\t// TODO: implement logic\n\t}\n}\n\nexport default DesktopCapturerSourcesService;\n"
  },
  {
    "path": "src/features/PeerConnectionHelperRendererService/index.ts",
    "content": "import { join } from 'path';\nimport { BrowserWindow } from 'electron';\nimport { is } from '@electron-toolkit/utils';\n\ntype RendererHelperWebcontentsID = number;\n\nexport default class RendererWebrtcHelpersService {\n\thelpers: Map<RendererHelperWebcontentsID, BrowserWindow>;\n\tappPath: string;\n\n\tconstructor(_appPath: string) {\n\t\tthis.helpers = new Map<RendererHelperWebcontentsID, BrowserWindow>();\n\t\tthis.appPath = _appPath;\n\t}\n\n\tcreatePeerConnectionHelperRenderer(): BrowserWindow {\n\t\tlet helperRendererWindow: BrowserWindow | null = null;\n\n\t\thelperRendererWindow = new BrowserWindow({\n\t\t\tshow: is.dev, // show in dev only\n\t\t\twebPreferences: {\n\t\t\t\tpreload: join(__dirname, '../preload/index.js'),\n\t\t\t\t// contextIsolation: true,\n\t\t\t\t// nodeIntegration: true,\n\t\t\t\tnodeIntegration: true,\n\t\t\t\tnodeIntegrationInSubFrames: true,\n\t\t\t\tnodeIntegrationInWorker: true,\n\t\t\t\tsandbox: false,\n\t\t\t},\n\t\t});\n\n\t\thelperRendererWindow.loadURL(\n\t\t\t`file://${this.appPath}/renderer/peerConnectionHelperRendererWindowIndex.html`,\n\t\t);\n\n\t\thelperRendererWindow.webContents.on('did-finish-load', () => {\n\t\t\tif (!helperRendererWindow) {\n\t\t\t\tthrow new Error('\"helperRendererWindow\" is not defined');\n\t\t\t}\n\t\t\thelperRendererWindow.webContents.send('start-peer-connection');\n\t\t});\n\n\t\tconst helperId = helperRendererWindow.webContents.id;\n\t\t// cleanup tracking map on close to prevent memory leaks\n\t\thelperRendererWindow.on('closed', () => {\n\t\t\tthis.helpers.delete(helperId);\n\t\t\thelperRendererWindow = null;\n\t\t});\n\n\t\tthis.helpers.set(helperId, helperRendererWindow);\n\n\t\tif (process.env.NODE_ENV === 'dev') {\n\t\t\thelperRendererWindow.webContents.toggleDevTools();\n\t\t}\n\t\t// helperRendererWindow.webContents.toggleDevTools();\n\n\t\treturn helperRendererWindow;\n\t}\n}\n"
  },
  {
    "path": "src/features/SharingSessionService/SharingSession.ts",
    "content": "import { BrowserWindow } from 'electron';\nimport uuid from 'uuid';\nimport SharingSessionStatusEnum from './SharingSessionStatusEnum';\nimport SharingTypeEnum from './SharingTypeEnum';\nimport PeerConnectionHelperRendererService from '../PeerConnectionHelperRendererService';\nimport { Device } from '../../common/Device';\nimport { LocalPeerUser } from '../../common/LocalPeerUser';\n\nexport type SharingSessionStatusChangeListener = (\n\tsharingSessionID: string,\n) => void;\n\nexport default class SharingSession {\n\tid: string;\n\tdeviceID: string;\n\tsharingType: SharingTypeEnum;\n\tsharingStream: MediaStream | null;\n\troomID: string;\n\tconnectedDeviceAt: Date | null;\n\tsharingStartedAt: Date | null;\n\tstatus: SharingSessionStatusEnum;\n\tstatusChangeListeners: SharingSessionStatusChangeListener[];\n\tpeerConnectionHelperRenderer: BrowserWindow | undefined;\n\tonDeviceConnectedCallback: undefined | ((device: Device) => void);\n\tdesktopCapturerSourceID: string;\n\n\tconstructor(\n\t\t_roomID: string,\n\t\tuser: LocalPeerUser,\n\t\tpeerConnectionHelperRendererService: PeerConnectionHelperRendererService,\n\t) {\n\t\tthis.id = uuid.v4();\n\t\tthis.deviceID = '';\n\t\tthis.sharingType = SharingTypeEnum.NOT_SET;\n\t\tthis.sharingStream = null;\n\t\tthis.roomID = _roomID;\n\t\tthis.connectedDeviceAt = null;\n\t\tthis.sharingStartedAt = null;\n\t\tthis.status = SharingSessionStatusEnum.NOT_CONNECTED;\n\t\tthis.statusChangeListeners = [] as SharingSessionStatusChangeListener[];\n\t\tthis.desktopCapturerSourceID = '';\n\t\tthis.onDeviceConnectedCallback = undefined;\n\n\t\tif (process.env.RUN_MODE === 'test') return;\n\n\t\tthis.peerConnectionHelperRenderer =\n\t\t\tpeerConnectionHelperRendererService.createPeerConnectionHelperRenderer();\n\n\t\tthis.peerConnectionHelperRenderer.webContents.on('did-finish-load', () => {\n\t\t\t// TODO: I need to remove dependency on renderer window, just to facilitate development\n\t\t\t// TODO: OR I can use a Utility or Child process to handle this. https://electron-vite.org/guide/dev#utility-process-and-child-process\n\t\t\t// TODO: probably using worker thread is the best option, but it will use the same resources as the main thread. child process is using more resources, but it is more isolated.\n\t\t\t// TODO: https://github.com/alex8088/electron-vite-worker-example\n\t\t\tthis.peerConnectionHelperRenderer?.webContents.send(\n\t\t\t\t'create-peer-connection-with-data',\n\t\t\t\t{\n\t\t\t\t\troomID: this.roomID,\n\t\t\t\t\tsharingSessionID: this.id,\n\t\t\t\t\tuser,\n\t\t\t\t},\n\t\t\t);\n\t\t});\n\n\t\tthis.peerConnectionHelperRenderer.webContents.on(\n\t\t\t'ipc-message',\n\t\t\t(_, channel, data) => {\n\t\t\t\tif (channel === 'peer-connected') {\n\t\t\t\t\tif (this.onDeviceConnectedCallback) {\n\t\t\t\t\t\tthis.onDeviceConnectedCallback(data);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tthis.statusChangeListeners.push(() => {\n\t\t\tif (this.status === SharingSessionStatusEnum.CONNECTED) {\n\t\t\t\tthis.peerConnectionHelperRenderer?.webContents.send(\n\t\t\t\t\t'send-user-allowed-to-connect',\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n\n\tdestroy(): void {\n\t\tthis.peerConnectionHelperRenderer?.close();\n\t}\n\n\tsetOnDeviceConnectedCallback(callback: (device: Device) => void): void {\n\t\tthis.onDeviceConnectedCallback = callback;\n\t}\n\n\tsetDesktopCapturerSourceID(id: string): void {\n\t\tthis.desktopCapturerSourceID = id;\n\t\tif (process.env.RUN_MODE === 'test') return;\n\t\tthis.peerConnectionHelperRenderer?.webContents.send(\n\t\t\t'set-desktop-capturer-source-id',\n\t\t\tid,\n\t\t);\n\t}\n\n\tcallPeer(): void {\n\t\tif (process.env.RUN_MODE === 'test') return;\n\t\tthis.peerConnectionHelperRenderer?.webContents.send('call-peer');\n\t}\n\n\tdisconnectByHostMachineUser(): void {\n\t\tthis.peerConnectionHelperRenderer?.webContents.send(\n\t\t\t'disconnect-by-host-machine-user',\n\t\t\tthis.deviceID,\n\t\t);\n\t}\n\n\tdenyConnectionForPartner(): void {\n\t\tthis.peerConnectionHelperRenderer?.webContents.send(\n\t\t\t'deny-connection-for-partner',\n\t\t);\n\t}\n\n\tappLanguageChanged(): void {\n\t\tthis.peerConnectionHelperRenderer?.webContents.send('app-language-changed');\n\t}\n\n\taddStatusChangeListener(callback: SharingSessionStatusChangeListener): void {\n\t\tthis.statusChangeListeners.push(callback);\n\t}\n\n\tnotifyStatusChangeListeners(): Promise<undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tfor (let i = 0; i < this.statusChangeListeners.length; i += 1) {\n\t\t\t\tthis.statusChangeListeners[i](this.id);\n\t\t\t}\n\t\t\tresolve(undefined);\n\t\t});\n\t}\n\n\tsetStatus(newStatus: SharingSessionStatusEnum): void {\n\t\tthis.status = newStatus;\n\t\tthis.notifyStatusChangeListeners();\n\t}\n\n\tsetDeviceID(deviceID: string): void {\n\t\tthis.deviceID = deviceID;\n\t}\n}\n"
  },
  {
    "path": "src/features/SharingSessionService/SharingSessionStatusEnum.ts",
    "content": "enum SharingSessionStatusEnum {\n\tNOT_CONNECTED,\n\tCONNECTED,\n\tSHARING,\n\tERROR,\n\tDESTROYED,\n}\n\nexport default SharingSessionStatusEnum;\n"
  },
  {
    "path": "src/features/SharingSessionService/SharingTypeEnum.ts",
    "content": "enum SharingTypeEnum {\n\tNOT_SET,\n\tSCREEN,\n\tAPP,\n}\n\nexport default SharingTypeEnum;\n"
  },
  {
    "path": "src/features/SharingSessionService/index.ts",
    "content": "import uuid from 'uuid';\nimport RoomIDService from '../../server/RoomIDService';\nimport { ConnectedDevicesService } from '../ConnectedDevicesService';\nimport RendererWebrtcHelpersService from '../PeerConnectionHelperRendererService';\nimport SharingSession from './SharingSession';\nimport SharingSessionStatusEnum from './SharingSessionStatusEnum';\nimport { LocalPeerUser } from '../../common/LocalPeerUser';\n\nexport default class SharingSessionService {\n\tuser: LocalPeerUser | null;\n\tsharingSessions: Map<string, SharingSession>;\n\twaitingForConnectionSharingSession: SharingSession | null;\n\troomIDService: RoomIDService;\n\tconnectedDevicesService: ConnectedDevicesService;\n\trendererWebrtcHelpersService: RendererWebrtcHelpersService;\n\tisCreatingNewSharingSession: boolean;\n\n\tconstructor(\n\t\t_roomIDService: RoomIDService,\n\t\t_connectedDevicesService: ConnectedDevicesService,\n\t\t_rendererWebrtcHelpersService: RendererWebrtcHelpersService,\n\t) {\n\t\tthis.roomIDService = _roomIDService;\n\t\tthis.connectedDevicesService = _connectedDevicesService;\n\t\tthis.rendererWebrtcHelpersService = _rendererWebrtcHelpersService;\n\t\tthis.waitingForConnectionSharingSession = null;\n\t\tthis.sharingSessions = new Map<string, SharingSession>();\n\t\tthis.user = null;\n\t\tthis.isCreatingNewSharingSession = false;\n\t\tthis.createUser();\n\n\t\tsetInterval(\n\t\t\t() => {\n\t\t\t\tthis.pollForInactiveSessions();\n\t\t\t},\n\t\t\t1000 * 60 * 60,\n\t\t); // every hour\n\t}\n\n\tcreateUser(): Promise<undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (process.env.RUN_MODE === 'test') resolve(undefined);\n\t\t\tconst username = uuid.v4();\n\t\t\tconst id = uuid.v4();\n\n\t\t\tthis.user = {\n\t\t\t\tusername,\n\t\t\t\tid,\n\t\t\t};\n\t\t\tresolve(undefined);\n\t\t});\n\t}\n\n\t// TODO: invoike this when got user ID from browser\n\tcreateWaitingForConnectionSharingSession(\n\t\troomID?: string,\n\t): Promise<SharingSession> {\n\t\tif (this.isCreatingNewSharingSession) {\n\t\t\treturn new Promise<SharingSession>((resolve, reject) => {\n\t\t\t\tconst intervalId = setInterval(() => {\n\t\t\t\t\tif (!this.isCreatingNewSharingSession) {\n\t\t\t\t\t\tclearInterval(intervalId);\n\t\t\t\t\t\tif (this.waitingForConnectionSharingSession) {\n\t\t\t\t\t\t\tresolve(this.waitingForConnectionSharingSession);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t\t'waiting sharing session is not available after creation.',\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, 50);\n\t\t\t});\n\t\t}\n\n\t\tif (!this.connectedDevicesService.isSlotAvailable()) {\n\t\t\treturn Promise.reject(\n\t\t\t\tnew Error(\n\t\t\t\t\t'unable to create waiting session while a device is connected',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tthis.isCreatingNewSharingSession = true;\n\n\t\treturn new Promise<SharingSession>((resolve, reject) => {\n\t\t\treturn this.waitWhileUserIsNotCreated()\n\t\t\t\t.then(async () => {\n\t\t\t\t\tif (this.waitingForConnectionSharingSession !== null) {\n\t\t\t\t\t\tthis.isCreatingNewSharingSession = false;\n\t\t\t\t\t\tresolve(this.waitingForConnectionSharingSession);\n\t\t\t\t\t\treturn this.waitingForConnectionSharingSession;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst newSession = await this.createNewSharingSession(roomID || '');\n\t\t\t\t\tthis.waitingForConnectionSharingSession = newSession;\n\t\t\t\t\tthis.isCreatingNewSharingSession = false;\n\t\t\t\t\tresolve(newSession);\n\t\t\t\t\treturn newSession;\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tthis.isCreatingNewSharingSession = false;\n\t\t\t\t\treject(error);\n\t\t\t\t});\n\t\t});\n\t}\n\n\tasync createNewSharingSession(_roomID: string): Promise<SharingSession> {\n\t\tconst roomID =\n\t\t\t_roomID || (await this.roomIDService.getSimpleAvailableRoomID());\n\t\tthis.roomIDService.markRoomIDAsTaken(roomID);\n\t\tconst sharingSession = new SharingSession(\n\t\t\troomID,\n\t\t\tthis.user as LocalPeerUser,\n\t\t\tthis.rendererWebrtcHelpersService,\n\t\t);\n\t\tthis.sharingSessions.set(sharingSession.id, sharingSession);\n\t\treturn sharingSession;\n\t}\n\n\tpollForInactiveSessions(): void {\n\t\t[...this.sharingSessions.keys()].forEach((key) => {\n\t\t\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t\t\t// @ts-ignore\n\t\t\tconst { status } = this.sharingSessions.get(key);\n\t\t\tif (\n\t\t\t\tstatus === SharingSessionStatusEnum.ERROR ||\n\t\t\t\tstatus === SharingSessionStatusEnum.DESTROYED\n\t\t\t) {\n\t\t\t\tthis.sharingSessions.delete(key);\n\t\t\t}\n\t\t});\n\t}\n\n\twaitWhileUserIsNotCreated(): Promise<undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst currentInterval = setInterval(() => {\n\t\t\t\tif (this.user !== null) {\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t\tclearInterval(currentInterval);\n\t\t\t\t}\n\t\t\t}, 1000);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/main/configs/i18next.config.ts",
    "content": "import i18n from 'i18next';\nimport config from '../../common/app.lang.config';\n// import { ElectronStoreKeys } from '../../common/ElectronStoreKeys.enum';\n// import { store } from '../../common/deskreen-electron-store';\n\nimport translationEN from '../../common/locales/en/translation.json';\nimport translationES from '../../common/locales/es/translation.json';\nimport translationKO from '../../common/locales/ko/translation.json';\nimport translationUA from '../../common/locales/ua/translation.json';\nimport translationRU from '../../common/locales/ru/translation.json';\nimport translationZH_CN from '../../common/locales/zh_CN/translation.json';\nimport translationZH_TW from '../../common/locales/zh_TW/translation.json';\nimport translationDA from '../../common/locales/da/translation.json';\nimport translationDE from '../../common/locales/de/translation.json';\nimport translationFI from '../../common/locales/fi/translation.json';\nimport translationIT from '../../common/locales/it/translation.json';\nimport translationJA from '../../common/locales/ja/translation.json';\nimport translationNL from '../../common/locales/nl/translation.json';\nimport translationFR from '../../common/locales/fr/translation.json';\nimport translationSV from '../../common/locales/sv/translation.json';\nimport { store } from '../../common/deskreen-electron-store';\nimport { ElectronStoreKeys } from '../../common/ElectronStoreKeys.enum';\n\nconst i18nextOptions = {\n\tfallbackLng: config.fallbackLng,\n\tlng: store.has(ElectronStoreKeys.AppLanguage)\n\t\t? String(store.get(ElectronStoreKeys.AppLanguage))\n\t\t: 'en',\n\t// lng: 'ua',\n\tns: 'translation',\n\tdefaultNS: 'translation',\n\tinterpolation: {\n\t\tescapeValue: false,\n\t},\n\tsaveMissing: true,\n\twhitelist: config.languages,\n\treact: {\n\t\t// wait: false,\n\t},\n\tresources: {\n\t\ten: {\n\t\t\ttranslation: translationEN,\n\t\t},\n\t\tes: {\n\t\t\ttranslation: translationES,\n\t\t},\n\t\tko: {\n\t\t\ttranslation: translationKO,\n\t\t},\n\t\tua: {\n\t\t\ttranslation: translationUA,\n\t\t},\n\t\tru: {\n\t\t\ttranslation: translationRU,\n\t\t},\n\t\tzh_CN: {\n\t\t\ttranslation: translationZH_CN,\n\t\t},\n\t\tzh_TW: {\n\t\t\ttranslation: translationZH_TW,\n\t\t},\n\t\tda: {\n\t\t\ttranslation: translationDA,\n\t\t},\n\t\tde: {\n\t\t\ttranslation: translationDE,\n\t\t},\n\t\tfi: {\n\t\t\ttranslation: translationFI,\n\t\t},\n\t\tit: {\n\t\t\ttranslation: translationIT,\n\t\t},\n\t\tja: {\n\t\t\ttranslation: translationJA,\n\t\t},\n\t\tnl: {\n\t\t\ttranslation: translationNL,\n\t\t},\n\t\tfr: {\n\t\t\ttranslation: translationFR,\n\t\t},\n\t\tsv: {\n\t\t\ttranslation: translationSV,\n\t\t},\n\t},\n};\n\nif (!i18n.isInitialized) {\n\ti18n.init(i18nextOptions);\n}\n\nexport default i18n;\n"
  },
  {
    "path": "src/main/helpers/getDeskreenGlobal.ts",
    "content": "import { DeskreenGlobal } from './initGlobals';\n\nexport const getDeskreenGlobal = (): DeskreenGlobal => {\n\treturn global as unknown as DeskreenGlobal;\n};\n"
  },
  {
    "path": "src/main/helpers/getMyLocalIpV4.ts",
    "content": "import os from \"os\";\n\n// Wi-Fi interface patterns for detection and prioritization\nconst macosWifiInterfaces = [\"en0\", \"en1\"]; // macOS Wi-Fi interfaces (commonly en0, en1, though can also be Ethernet)\nconst linuxWifiPrefixes = [\"wlan\", \"wlo\", \"wlp\"]; // Linux Wi-Fi interface prefixes\nconst windowsWifiInterfaces = [\n  \"Wi-Fi\",\n  \"Wireless LAN adapter Wi-Fi\",\n  \"Wireless LAN adapter\",\n  \"Wireless Network Connection\",\n  \"WLAN\",\n]; // Windows Wi-Fi interface patterns\n\n// Virtualization interface patterns\nconst virtualPrefixes = [\n  \"docker\", // Docker Bridge\n  \"veth\", // Linux Virtual Ethernet (Containers)\n  \"virbr\", // Linux KVM/QEMU/Libvirt\n  \"vboxnet\", // VirtualBox (Linux/macOS)\n  \"vmnet\", // VMware (Linux/macOS)\n  \"br-\", // Linux Bridge (often Docker or custom)\n  \"VirtualBox\", // Windows VirtualBox\n  \"VMware\", // Windows VMware\n];\n\nconst virtualInterfaces = [\n  \"awdl0\", // macOS peer-to-peer (AirDrop)\n  \"bridge100\", // macOS bridge (virtual interface)\n  \"vEthernet\", // Windows Ethernet Hyper-V\n  \"docker0\", // Linux Docker standard bridge\n  \"virbr0\",  // Linux KVM/QEMU standard bridge\n  \"vboxnet0\", // VirtualBox (Linux/macOS)\n  \"vmnet1\",   // VMware (Linux/macOS)\n  \"vmnet8\",   // VMware (Linux/macOS)\n  \"VirtualBox Host-Only Network\", // Windows VirtualBox\n  \"VMware Network Adapter VMnet1\", // Windows VMware\n  \"VMware Network Adapter VMnet8\", // Windows VMware\n];\n\nexport const interfacesToCheck = [\n  ...macosWifiInterfaces, // macOS Wi-Fi or Ethernet\n  \"eth0\", // macOS or Linux Ethernet (older setups)\n  \"wlan0\", // Linux/Android Wi-Fi\n  \"wlan1\", // Linux/Android Wi-Fi\n  \"wlpXsY\", // Linux (newer predictable names for Wi-Fi)\n  \"eth1\", // Linux Ethernet (older setups)\n  \"enpXsY\", // Linux predictable names for Ethernet\n  \"enxXXXXXX\", // Linux Ethernet (based on MAC address)\n  ...windowsWifiInterfaces, // Windows Wi-Fi\n  \"Ethernet\", // Windows Ethernet\n  \"Local Area Connection\", // Windows Ethernet (older versions)\n  \"usb0\", // Android/Chrome OS USB Ethernet adapters\n  ...virtualInterfaces,\n];\n\nexport const interfacesStartsWithCheck = [\n  \"usb\", // Android/Chrome OS USB Ethernet adapters\n  \"en\", // macOS Ethernet interfaces\n  \"eth\", // Linux Ethernet interfaces\n  ...linuxWifiPrefixes, // Linux/Android Wi-Fi interfaces\n  \"enx\", // Linux Ethernet based on MAC address\n  ...windowsWifiInterfaces, // Windows Wi-Fi\n  \"Ethernet\", // Windows Ethernet\n  \"vEthernet\", // Windows Ethernet Hyper-V\n  \"Local Area Connection\", // Windows Ethernet (older versions)\n  ...virtualPrefixes,\n];\n\n/**\n * Check if an interface name indicates a Wi-Fi interface across all operating systems\n * @param interfaceName - The network interface name to check\n * @returns true if the interface is likely Wi-Fi, false otherwise\n */\nfunction isWifiInterface(interfaceName: string): boolean {\n  // Check exact matches for macOS\n  if (macosWifiInterfaces.includes(interfaceName)) {\n    return true;\n  }\n  \n  // Check if starts with Linux Wi-Fi patterns\n  if (linuxWifiPrefixes.some((pattern) => interfaceName.startsWith(pattern))) {\n    return true;\n  }\n  \n  // Check if starts with or equals Windows Wi-Fi patterns\n  if (\n    windowsWifiInterfaces.some((pattern) =>\n      interfaceName.startsWith(pattern) || interfaceName === pattern,\n    )\n  ) {\n    return true;\n  }\n  \n  return false;\n}\n\n/**\n * Check if an interface name or MAC prefix indicates a virtual interface\n * @param interfaceName - The network interface name to check\n * @param interfaceMacAddress - The network MAC address to check\n * @returns true if the interface is likely virtual, false otherwise\n */\nfunction isVirtualInterface(interfaceName: string, interfaceMacAddress: string): boolean {\n  // Typical virtual MAC prefixes\n  const virtualMacPrefixes = [\n    '00:15:5d', // Microsoft Hyper-V\n    '00:05:69', // VMware\n    '00:0c:29', // VMware\n    '00:50:56', // VMware\n    '00:1c:42', // Parallels\n    '00:16:3e', // Xen\n    '08:00:27', // VirtualBox\n    '0a:00:27', // VirtualBox (Host-Only / LAA)\n    '52:54:00', // QEMU/KVM\n  ]\n\n  if (\n    virtualInterfaces.some((pattern) => \n      interfaceName.startsWith(pattern) || interfaceName === pattern\n    )\n  ) {\n    return true;\n  }\n\n  if (virtualPrefixes.some((pattern) => interfaceName.startsWith(pattern))) {\n    return true;\n  }\n\n  if (virtualMacPrefixes.some((pattern) => interfaceMacAddress.startsWith(pattern))) {\n    return true;\n  }\n\n  return false;\n}\n\n/**\n * Get the active network interface name and its IPv4 address\n * Prioritizes Wi-Fi interfaces across all operating systems\n * @returns Object with interfaceName and ipAddress, or undefined if no valid interface found\n */\nexport function getActiveNetworkInterface(): { interfaceName: string; ipAddress: string } | undefined {\n  // Get network interfaces\n  const networkInterfaces = os.networkInterfaces();\n  let virtualResult: { interfaceName: string; ipAddress: string } | undefined;\n  let localResult: { interfaceName: string; ipAddress: string } | undefined;\n  let wifiResult: { interfaceName: string; ipAddress: string } | undefined;\n  \n  // First pass: collect all valid interfaces, prioritizing Wi-Fi\n  Object.entries(networkInterfaces).forEach(([networksKey, networks]) => {\n    if (!networks) return;\n    if (networksKey.startsWith(\"bridge\")) return;\n    if (\n      !interfacesToCheck.includes(networksKey) &&\n      !interfacesStartsWithCheck.some((prefix) =>\n        networksKey.startsWith(prefix),\n      )\n    ) {\n      return;\n    }\n\n    networks.forEach((network) => {\n      if (!network.internal && network.family === \"IPv4\") {\n        const currentResult = {\n          interfaceName: networksKey,\n          ipAddress: network.address,\n        };\n        \n        // Prioritize Wi-Fi interfaces\n        if (isWifiInterface(networksKey)) {\n          if (!wifiResult) {\n            wifiResult = currentResult;\n          }\n        } else if (!isVirtualInterface(networksKey, network.mac)){\n          // Prioritize non-virtual interfaces next\n          if (!localResult) {\n            localResult = currentResult;\n          }\n        } else {\n          // Store virtual interface as fallback\n          if (!virtualResult) {\n            virtualResult = currentResult;\n          }\n        }\n      }\n    });\n  });\n\n  // Return Wi-Fi interface if found, otherwise return fallback\n  return wifiResult || localResult || virtualResult;\n}\n\nexport default function getMyLocalIpV4(): string | undefined {\n  // Get network interfaces\n  const networkInterfaces = os.networkInterfaces();\n  let localIp: string | undefined;\n  let wifiIp: string | undefined;\n  let fallbackIp: string | undefined;\n  \n  // First pass: collect all valid interfaces, prioritizing Wi-Fi\n  Object.entries(networkInterfaces).forEach(([networksKey, networks]) => {\n    if (!networks) return;\n    if (networksKey.startsWith(\"bridge\")) return;\n    if (\n      !interfacesToCheck.includes(networksKey) &&\n      !interfacesStartsWithCheck.some((prefix) =>\n        networksKey.startsWith(prefix),\n      )\n    ) {\n      return;\n    }\n\n    networks.forEach((network) => {\n      if (!network.internal && network.family === \"IPv4\") {\n        // Prioritize Wi-Fi interfaces\n        if (isWifiInterface(networksKey)) {\n          if (!wifiIp) {\n            wifiIp = network.address;\n          }\n        } else if (!isVirtualInterface(networksKey, network.mac)){\n          // Prioritize non-virtual interfaces next\n          if (!localIp) {\n            localIp = network.address;\n          }\n        } else {\n          // Store virtual interface as fallback\n          if (!fallbackIp) {\n            fallbackIp = network.address;\n          }\n        }\n      }\n    });\n  });\n\n  // Return Wi-Fi IP if found, otherwise return fallback\n  return wifiIp || localIp || fallbackIp;\n}\n"
  },
  {
    "path": "src/main/helpers/initGlobals.ts",
    "content": "import { app } from 'electron';\nimport { ConnectedDevicesService } from '../../features/ConnectedDevicesService';\nimport SharingSessionService from '../../features/SharingSessionService';\nimport RendererWebrtcHelpersService from '../../features/PeerConnectionHelperRendererService';\nimport RoomIDService from '../../server/RoomIDService';\nimport DesktopCapturerSources from '../../features/DesktopCapturerSourcesService';\nimport DesktopCapturerSourcesService from '../../features/DesktopCapturerSourcesService';\n\nexport interface DeskreenGlobal {\n\tappPath: string;\n\trendererWebrtcHelpersService: RendererWebrtcHelpersService;\n\troomIDService: RoomIDService;\n\tconnectedDevicesService: ConnectedDevicesService;\n\tsharingSessionService: SharingSessionService;\n\tdesktopCapturerSourcesService: DesktopCapturerSourcesService;\n\tlatestAppVersion: string;\n\tcurrentAppVersion: string;\n\tcliLocalIp?: string;\n}\n\nexport const initGlobals = (appPath: string, cliLocalIp?: string) => {\n\tconst deskreenGlobal: DeskreenGlobal = global as unknown as DeskreenGlobal;\n\n\tdeskreenGlobal.appPath = appPath;\n\tdeskreenGlobal.rendererWebrtcHelpersService =\n\t\tnew RendererWebrtcHelpersService(appPath);\n\tdeskreenGlobal.roomIDService = new RoomIDService();\n\tdeskreenGlobal.connectedDevicesService = new ConnectedDevicesService();\n\tdeskreenGlobal.sharingSessionService = new SharingSessionService(\n\t\tdeskreenGlobal.roomIDService,\n\t\tdeskreenGlobal.connectedDevicesService,\n\t\tdeskreenGlobal.rendererWebrtcHelpersService,\n\t);\n\tdeskreenGlobal.desktopCapturerSourcesService = new DesktopCapturerSources();\n\tdeskreenGlobal.latestAppVersion = '';\n\tdeskreenGlobal.currentAppVersion = app.getVersion();\n\tdeskreenGlobal.cliLocalIp = cliLocalIp;\n};\n"
  },
  {
    "path": "src/main/helpers/ipcMainHandlers.ts",
    "content": "import {\n\tDisplay,\n\tipcMain,\n\tBrowserWindow,\n\tscreen,\n\tclipboard,\n\tshell,\n} from 'electron';\nimport i18n from '../configs/i18next.config';\nimport { ConnectedDevicesService } from '../../features/ConnectedDevicesService';\nimport SharingSession from '../../features/SharingSessionService/SharingSession';\nimport RoomIDService from '../../server/RoomIDService';\nimport { signalingServer } from '../../server';\nimport { onDeviceConnectedCallback } from '../../server/onDeviceConnectedCallback';\nimport SharingSessionStatusEnum from '../../features/SharingSessionService/SharingSessionStatusEnum';\nimport getMyLocalIpV4 from './getMyLocalIpV4';\nimport isWifiConnected from './isWifiConnected';\nimport { getDeskreenGlobal } from './getDeskreenGlobal';\nimport { IpcEvents } from '../../common/IpcEvents.enum';\nimport { ElectronStoreKeys } from '../../common/ElectronStoreKeys.enum';\nimport { store } from '../../common/deskreen-electron-store';\nimport DesktopCapturerSourceType from '../../common/DesktopCapturerSourceType';\nimport isLinuxWaylandSession from '../utils/isLinuxWaylandSession';\n\nexport const initIpcMainHandlers = (mainWindow: BrowserWindow): void => {\n\tipcMain.on('client-changed-language', async (_, newLangCode) => {\n\t\ti18n.changeLanguage(newLangCode);\n\t\tif (store.has(ElectronStoreKeys.AppLanguage)) {\n\t\t\tif (store.get(ElectronStoreKeys.AppLanguage) === newLangCode) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstore.delete(ElectronStoreKeys.AppLanguage);\n\t\t}\n\t\tstore.set(ElectronStoreKeys.AppLanguage, newLangCode);\n\t});\n\n\tipcMain.handle('get-signaling-server-port', () => {\n\t\tif (mainWindow === null) return;\n\t\tmainWindow.webContents.send('sending-port-from-main', signalingServer.port);\n\t});\n\n\tipcMain.handle('get-all-displays', () => {\n\t\treturn screen.getAllDisplays();\n\t});\n\n\tipcMain.handle('get-display-size-by-display-id', (_, displayID: string) => {\n\t\tconst display = screen.getAllDisplays().find((d: Display) => {\n\t\t\treturn `${d.id}` === displayID;\n\t\t});\n\n\t\tif (display) {\n\t\t\treturn display.size;\n\t\t}\n\t\treturn undefined;\n\t});\n\n\tipcMain.handle(IpcEvents.GetIsLinuxWaylandSession, () => {\n\t\treturn isLinuxWaylandSession;\n\t});\n\n\tipcMain.handle(\n\t\tIpcEvents.RequestDesktopCapturerPortalSource,\n\t\tasync (_, { mode }: { mode: 'screen' | 'window' }) => {\n\t\t\tconst types =\n\t\t\t\tmode === 'window'\n\t\t\t\t\t? [DesktopCapturerSourceType.WINDOW]\n\t\t\t\t\t: [DesktopCapturerSourceType.SCREEN];\n\n\t\t\tif (!isLinuxWaylandSession) {\n\t\t\t\tawait getDeskreenGlobal().desktopCapturerSourcesService.refreshDesktopCapturerSources();\n\t\t\t\tif (mode === 'window') {\n\t\t\t\t\tconst sources =\n\t\t\t\t\t\tgetDeskreenGlobal().desktopCapturerSourcesService.getAppWindowSources();\n\t\t\t\t\treturn sources[0]?.id ?? null;\n\t\t\t\t}\n\t\t\t\tconst sources =\n\t\t\t\t\tgetDeskreenGlobal().desktopCapturerSourcesService.getScreenSources();\n\t\t\t\treturn sources[0]?.id ?? null;\n\t\t\t}\n\n\t\t\tconst source =\n\t\t\t\tawait getDeskreenGlobal().desktopCapturerSourcesService.requestPortalSource(\n\t\t\t\t\ttypes,\n\t\t\t\t);\n\t\t\treturn source?.id ?? null;\n\t\t},\n\t);\n\n\tipcMain.handle('main-window-onbeforeunload', () => {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\tdeskreenGlobal.connectedDevicesService = new ConnectedDevicesService();\n\t\tdeskreenGlobal.roomIDService = new RoomIDService();\n\t\tdeskreenGlobal.sharingSessionService.sharingSessions.forEach(\n\t\t\t(sharingSession: SharingSession) => {\n\t\t\t\tsharingSession.denyConnectionForPartner();\n\t\t\t\tsharingSession.destroy();\n\t\t\t},\n\t\t);\n\n\t\tdeskreenGlobal.rendererWebrtcHelpersService.helpers.forEach(\n\t\t\t(helperWindow) => {\n\t\t\t\thelperWindow.close();\n\t\t\t},\n\t\t);\n\n\t\tdeskreenGlobal.sharingSessionService.waitingForConnectionSharingSession =\n\t\t\tnull;\n\t\tdeskreenGlobal.rendererWebrtcHelpersService.helpers.clear();\n\t\tdeskreenGlobal.sharingSessionService.sharingSessions.clear();\n\t});\n\n\tipcMain.handle('get-latest-version', () => {\n\t\treturn getDeskreenGlobal().latestAppVersion;\n\t});\n\n\tipcMain.handle('get-current-version', () => {\n\t\treturn getDeskreenGlobal().currentAppVersion;\n\t});\n\n\tipcMain.handle('get-local-lan-ip', async () => {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\tif (deskreenGlobal.cliLocalIp) {\n\t\t\treturn deskreenGlobal.cliLocalIp;\n\t\t}\n\t\tconst ip = getMyLocalIpV4();\n\t\treturn ip;\n\t});\n\n\tipcMain.handle('check-wifi-connection', async () => {\n\t\treturn isWifiConnected();\n\t});\n\n\tipcMain.handle(IpcEvents.GetPort, () => {\n\t\treturn signalingServer.port;\n\t});\n\n\tipcMain.handle(IpcEvents.GetAppPath, () => {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\treturn deskreenGlobal.appPath;\n\t});\n\n\tipcMain.handle(IpcEvents.UnmarkRoomIDAsTaken, (_, roomID) => {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\tdeskreenGlobal.roomIDService.unmarkRoomIDAsTaken(roomID);\n\t});\n\n\tasync function createWaitingForConnectionSharingSession(\n\t\troomID?: string,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\t\tif (\n\t\t\t\tdeskreenGlobal.sharingSessionService\n\t\t\t\t\t.waitingForConnectionSharingSession !== null\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waitingSession =\n\t\t\t\tawait deskreenGlobal.sharingSessionService.createWaitingForConnectionSharingSession(\n\t\t\t\t\troomID,\n\t\t\t\t);\n\t\t\twaitingSession.setOnDeviceConnectedCallback(onDeviceConnectedCallback);\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to create waiting sharing session', error);\n\t\t}\n\t}\n\n\tipcMain.handle(\n\t\tIpcEvents.CreateWaitingForConnectionSharingSession,\n\t\tasync (_, roomID?: string) => {\n\t\t\tawait createWaitingForConnectionSharingSession(roomID);\n\t\t},\n\t);\n\n\tfunction resetWaitingForConnectionSharingSession(): void {\n\t\tconst sharingSession =\n\t\t\tgetDeskreenGlobal().sharingSessionService\n\t\t\t\t.waitingForConnectionSharingSession;\n\t\tconst roomID = sharingSession?.roomID;\n\t\tsharingSession?.denyConnectionForPartner();\n\t\tsharingSession?.disconnectByHostMachineUser();\n\t\tsharingSession?.destroy();\n\t\tsharingSession?.setStatus(SharingSessionStatusEnum.NOT_CONNECTED);\n\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.delete(\n\t\t\tsharingSession?.id as string,\n\t\t);\n\t\tif (roomID) {\n\t\t\tgetDeskreenGlobal().roomIDService.unmarkRoomIDAsTaken(roomID);\n\t\t}\n\t\tgetDeskreenGlobal().sharingSessionService.waitingForConnectionSharingSession =\n\t\t\tnull;\n\t}\n\n\tipcMain.handle(IpcEvents.ResetWaitingForConnectionSharingSession, () => {\n\t\tresetWaitingForConnectionSharingSession();\n\t});\n\n\tconst removeViewerAvailabilityListener =\n\t\tgetDeskreenGlobal().connectedDevicesService.addAvailabilityListener(\n\t\t\t(state) => {\n\t\t\t\tconst isAvailable = state === 'available';\n\t\t\t\tconst targetWindow = mainWindow?.isDestroyed() ? null : mainWindow;\n\t\t\t\tif (targetWindow) {\n\t\t\t\t\ttargetWindow.webContents.send(\n\t\t\t\t\t\tIpcEvents.ViewerConnectionAvailabilityChanged,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tisAvailable,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (isAvailable) {\n\t\t\t\t\tvoid createWaitingForConnectionSharingSession();\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\tmainWindow.on('closed', () => {\n\t\tremoveViewerAvailabilityListener();\n\t});\n\n\tipcMain.handle(IpcEvents.SetDeviceConnectedStatus, () => {\n\t\tif (\n\t\t\tgetDeskreenGlobal().sharingSessionService\n\t\t\t\t.waitingForConnectionSharingSession !== null\n\t\t) {\n\t\t\tconst sharingSession =\n\t\t\t\tgetDeskreenGlobal().sharingSessionService\n\t\t\t\t\t.waitingForConnectionSharingSession;\n\t\t\tsharingSession?.setStatus(SharingSessionStatusEnum.CONNECTED);\n\t\t}\n\t});\n\n\tipcMain.handle(\n\t\tIpcEvents.GetSourceDisplayIDByDesktopCapturerSourceID,\n\t\t(_, sourceId) => {\n\t\t\treturn getDeskreenGlobal().desktopCapturerSourcesService.getSourceDisplayIDByDisplayCapturerSourceID(\n\t\t\t\tsourceId,\n\t\t\t);\n\t\t},\n\t);\n\n\tipcMain.handle(\n\t\tIpcEvents.DisconnectPeerAndDestroySharingSessionBySessionID,\n\t\t(_, sessionId) => {\n\t\t\tconst sharingSession =\n\t\t\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.get(\n\t\t\t\t\tsessionId,\n\t\t\t\t);\n\t\t\tif (sharingSession) {\n\t\t\t\tgetDeskreenGlobal().connectedDevicesService.disconnectDeviceByID(\n\t\t\t\t\tsharingSession.deviceID,\n\t\t\t\t);\n\t\t\t}\n\t\t\tsharingSession?.disconnectByHostMachineUser();\n\t\t\tsharingSession?.destroy();\n\t\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.delete(\n\t\t\t\tsessionId,\n\t\t\t);\n\t\t},\n\t);\n\n\tipcMain.handle(\n\t\tIpcEvents.GetDesktopCapturerSourceIdBySharingSessionId,\n\t\t(_, sessionId) => {\n\t\t\treturn getDeskreenGlobal().sharingSessionService.sharingSessions.get(\n\t\t\t\tsessionId,\n\t\t\t)?.desktopCapturerSourceID;\n\t\t},\n\t);\n\n\tipcMain.handle(IpcEvents.GetConnectedDevices, () => {\n\t\treturn getDeskreenGlobal().connectedDevicesService.getDevices();\n\t});\n\n\tipcMain.handle(IpcEvents.GetViewerConnectionAvailability, () => {\n\t\treturn getDeskreenGlobal().connectedDevicesService.isSlotAvailable();\n\t});\n\n\tipcMain.handle(IpcEvents.DisconnectDeviceById, (_, id) => {\n\t\tgetDeskreenGlobal().connectedDevicesService.disconnectDeviceByID(id);\n\t});\n\n\tipcMain.handle(IpcEvents.DisconnectAllDevices, () => {\n\t\tgetDeskreenGlobal().connectedDevicesService.disconnectAllDevices();\n\t});\n\n\tipcMain.handle(IpcEvents.AppLanguageChanged, (_, newLang) => {\n\t\tif (store.has(ElectronStoreKeys.AppLanguage)) {\n\t\t\tstore.delete(ElectronStoreKeys.AppLanguage);\n\t\t}\n\t\tstore.set(ElectronStoreKeys.AppLanguage, newLang);\n\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.forEach(\n\t\t\t(sharingSession) => {\n\t\t\t\tsharingSession?.appLanguageChanged();\n\t\t\t},\n\t\t);\n\t\ti18n.changeLanguage(newLang);\n\t});\n\n\tipcMain.handle(IpcEvents.GetDesktopCapturerServiceSourcesMap, () => {\n\t\tconst map =\n\t\t\tgetDeskreenGlobal().desktopCapturerSourcesService.getSourcesMap();\n\t\tconst res = {};\n\n\t\tfor (const key of map.keys()) {\n\t\t\tconst source = map.get(key);\n\t\t\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t\t\t// @ts-ignore\n\t\t\tres[key] = {\n\t\t\t\tsource: {\n\t\t\t\t\tthumbnail: source?.source.thumbnail?.toDataURL(),\n\t\t\t\t\tappIcon: source?.source.appIcon?.toDataURL(),\n\t\t\t\t\tname: source?.source.name,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn res;\n\t});\n\n\tipcMain.handle(\n\t\tIpcEvents.GetDesktopCapturerServiceSourcesByIds,\n\t\t(_, ids: string[]) => {\n\t\t\tconst map =\n\t\t\t\tgetDeskreenGlobal().desktopCapturerSourcesService.getSourcesMap();\n\t\t\tconst res = {};\n\n\t\t\tids.forEach((id) => {\n\t\t\t\tconst source = map.get(id);\n\t\t\t\tif (!source) return;\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t\t\t\t// @ts-ignore\n\t\t\t\tres[id] = {\n\t\t\t\t\tsource: {\n\t\t\t\t\t\tthumbnail: source?.source.thumbnail?.toDataURL(),\n\t\t\t\t\t\tappIcon: source?.source.appIcon?.toDataURL(),\n\t\t\t\t\t\tname: source?.source.name,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn res;\n\t\t},\n\t);\n\n\tipcMain.handle(\n\t\tIpcEvents.GetWaitingForConnectionSharingSessionSourceId,\n\t\t() => {\n\t\t\treturn getDeskreenGlobal().sharingSessionService\n\t\t\t\t.waitingForConnectionSharingSession?.desktopCapturerSourceID;\n\t\t},\n\t);\n\n\tfunction startSharingOnWaitingForConnectionSharingSession(): void {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\tconst { connectedDevicesService, sharingSessionService, roomIDService } =\n\t\t\tdeskreenGlobal;\n\t\tif (!connectedDevicesService.isSlotAvailable()) {\n\t\t\tconst waitingSession =\n\t\t\t\tsharingSessionService.waitingForConnectionSharingSession;\n\t\t\twaitingSession?.denyConnectionForPartner();\n\t\t\twaitingSession?.setStatus(SharingSessionStatusEnum.NOT_CONNECTED);\n\t\t\tsharingSessionService.waitingForConnectionSharingSession = null;\n\t\t\tconnectedDevicesService.resetPendingConnectionDevice();\n\t\t\treturn;\n\t\t}\n\n\t\tconst pendingDevice = connectedDevicesService.pendingConnectionDevice;\n\t\tif (!pendingDevice.id) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst sharingSession =\n\t\t\tsharingSessionService.waitingForConnectionSharingSession;\n\t\tif (sharingSession !== null) {\n\t\t\troomIDService.unmarkRoomIDAsTaken(sharingSession.roomID);\n\t\t}\n\n\t\ttry {\n\t\t\tconnectedDevicesService.addDevice(pendingDevice);\n\t\t} catch (error) {\n\t\t\tconsole.error('failed to occupy single viewer slot', error);\n\t\t\tif (sharingSession !== null) {\n\t\t\t\tsharingSession.setStatus(SharingSessionStatusEnum.ERROR);\n\t\t\t\tsharingSession.denyConnectionForPartner();\n\t\t\t\tsharingSessionService.waitingForConnectionSharingSession = null;\n\t\t\t}\n\t\t\tconnectedDevicesService.resetPendingConnectionDevice();\n\t\t\treturn;\n\t\t}\n\n\t\tif (sharingSession !== null) {\n\t\t\tsharingSession.callPeer();\n\t\t\tsharingSession.setStatus(SharingSessionStatusEnum.SHARING);\n\t\t\tsharingSessionService.waitingForConnectionSharingSession = null;\n\t\t}\n\n\t\tconnectedDevicesService.resetPendingConnectionDevice();\n\t}\n\n\tipcMain.handle(\n\t\tIpcEvents.StartSharingOnWaitingForConnectionSharingSession,\n\t\t() => {\n\t\t\tstartSharingOnWaitingForConnectionSharingSession();\n\t\t},\n\t);\n\n\tipcMain.handle(IpcEvents.GetPendingConnectionDevice, () => {\n\t\treturn getDeskreenGlobal().connectedDevicesService.pendingConnectionDevice;\n\t});\n\n\tipcMain.handle(IpcEvents.GetWaitingForConnectionSharingSessionRoomId, () => {\n\t\tif (\n\t\t\tgetDeskreenGlobal().sharingSessionService\n\t\t\t\t.waitingForConnectionSharingSession === null\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn getDeskreenGlobal().sharingSessionService\n\t\t\t.waitingForConnectionSharingSession?.roomID;\n\t});\n\n\tipcMain.handle(\n\t\tIpcEvents.GetDesktopSharingSourceIds,\n\t\tasync (_, { isEntireScreenToShareChosen }) => {\n\t\t\tif (isLinuxWaylandSession) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\t// ensure sources are up to date at request time\n\t\t\tawait getDeskreenGlobal().desktopCapturerSourcesService.refreshDesktopCapturerSources();\n\n\t\t\tif (isEntireScreenToShareChosen === true) {\n\t\t\t\treturn getDeskreenGlobal()\n\t\t\t\t\t.desktopCapturerSourcesService.getScreenSources()\n\t\t\t\t\t.map((source) => source.id);\n\t\t\t}\n\t\t\treturn getDeskreenGlobal()\n\t\t\t\t.desktopCapturerSourcesService.getAppWindowSources()\n\t\t\t\t.map((source) => source.id);\n\t\t},\n\t);\n\n\tipcMain.handle(IpcEvents.SetDesktopCapturerSourceId, (_, id) => {\n\t\tgetDeskreenGlobal().sharingSessionService.waitingForConnectionSharingSession?.setDesktopCapturerSourceID(\n\t\t\tid,\n\t\t);\n\t});\n\n\tipcMain.handle(IpcEvents.GetIsFirstTimeAppStart, () => {\n\t\tif (store.has(ElectronStoreKeys.IsNotFirstTimeAppStart)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\n\tipcMain.handle(IpcEvents.SetAppStartedOnce, () => {\n\t\tif (store.has(ElectronStoreKeys.IsNotFirstTimeAppStart)) {\n\t\t\tstore.delete(ElectronStoreKeys.IsNotFirstTimeAppStart);\n\t\t}\n\t\tstore.set(ElectronStoreKeys.IsNotFirstTimeAppStart, 'true');\n\t});\n\n\tipcMain.handle(IpcEvents.GetAppLanguage, () => {\n\t\tif (store.has(ElectronStoreKeys.AppLanguage)) {\n\t\t\treturn store.get(ElectronStoreKeys.AppLanguage);\n\t\t}\n\t\treturn 'en';\n\t});\n\n\tipcMain.handle(IpcEvents.DestroySharingSessionById, (_, id) => {\n\t\tif (\n\t\t\tgetDeskreenGlobal().sharingSessionService\n\t\t\t\t.waitingForConnectionSharingSession?.id === id\n\t\t) {\n\t\t\tgetDeskreenGlobal().sharingSessionService.waitingForConnectionSharingSession =\n\t\t\t\tnull;\n\t\t}\n\t\tconst sharingSession =\n\t\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.get(id);\n\t\tsharingSession?.setStatus(SharingSessionStatusEnum.DESTROYED);\n\t\tsharingSession?.destroy();\n\t\tgetDeskreenGlobal().sharingSessionService.sharingSessions.delete(id);\n\t});\n\n\tipcMain.handle(IpcEvents.OpenExternalLink, (_, url: string) => {\n\t\tif (typeof url !== 'string') {\n\t\t\treturn;\n\t\t}\n\t\tshell.openExternal(url);\n\t});\n\n\tipcMain.handle(IpcEvents.WriteTextToClipboard, (_, text) => {\n\t\tclipboard.writeText(text);\n\t});\n\n\tvoid createWaitingForConnectionSharingSession();\n};\n"
  },
  {
    "path": "src/main/helpers/isWifiConnected.ts",
    "content": "import os from 'os';\nimport { interfacesToCheck, interfacesStartsWithCheck } from './getMyLocalIpV4';\n\nexport default function isWifiConnected(): boolean {\n\tconst networkInterfaces = os.networkInterfaces();\n\tlet hasValidInterface = false;\n\tObject.entries(networkInterfaces).some(([networksKey, networks]) => {\n\t\tif (!networks) return false;\n\t\tif (networksKey.startsWith('bridge')) return false;\n\t\tif (\n\t\t\t!interfacesToCheck.includes(networksKey) &&\n\t\t\t!interfacesStartsWithCheck.some((prefix) =>\n\t\t\t\tnetworksKey.startsWith(prefix),\n\t\t\t)\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst hasValidNetwork = networks.some((network) => {\n\t\t\tif (!network.internal && network.family === 'IPv4') {\n\t\t\t\thasValidInterface = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\n\t\treturn hasValidNetwork;\n\t});\n\n\treturn hasValidInterface;\n}\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "// override console early to catch all logs\nimport {\n\toverrideGlobalConsole,\n\tstartConsoleRateLimiting,\n} from '../common/rateLimitedConsole';\noverrideGlobalConsole();\nstartConsoleRateLimiting();\n\nimport { app, shell, BrowserWindow, Notification } from 'electron';\nimport { join } from 'path';\nimport { is, optimizer } from '@electron-toolkit/utils';\nimport icon from '../../resources/icon.png?asset';\n\n// function createWindow(): void {\n//   // Create the browser window.\n//   const mainWindow = new BrowserWindow({\n//     width: 900,\n//     height: 670,\n//     show: false,\n//     autoHideMenuBar: true,\n//     ...(process.platform === 'linux' ? { icon } : {}),\n//     webPreferences: {\n//       preload: join(__dirname, '../preload/index.js'),\n//       sandbox: false,\n//     },\n//   });\n//\n//   mainWindow.on('ready-to-show', () => {\n//     mainWindow.show();\n//   });\n//\n//   mainWindow.webContents.setWindowOpenHandler((details) => {\n//     shell.openExternal(details.url);\n//     return { action: 'deny' };\n//   });\n//\n//   // HMR for renderer base on electron-vite cli.\n//   // Load the remote URL for development or the local html file for production.\n//   if (is.dev && process.env['ELECTRON_RENDERER_URL']) {\n//     mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);\n//   } else {\n//     mainWindow.loadFile(join(__dirname, '../renderer/index.html'));\n//   }\n// }\n//\n// // This method will be called when Electron has finished\n// // initialization and is ready to create browser windows.\n// // Some APIs can only be used after this event occurs.\n// app.whenReady().then(() => {\n//   // Set app user model id for windows\n//   electronApp.setAppUserModelId('com.deskreen');\n//\n//   // Default open or close DevTools by F12 in development\n//   // and ignore CommandOrControl + R in production.\n//   // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils\n//   app.on('browser-window-created', (_, window) => {\n//     optimizer.watchWindowShortcuts(window);\n//   });\n//\n//   // IPC test\n//   ipcMain.on('ping', () => console.log('pong'));\n//\n//   createWindow();\n//\n//   app.on('activate', function () {\n//     // On macOS it's common to re-create a window in the app when the\n//     // dock icon is clicked and there are no other windows open.\n//     if (BrowserWindow.getAllWindows().length === 0) createWindow();\n//   });\n// });\n//\n// // Quit when all windows are closed, except on macOS. There, it's common\n// // for applications and their menu bar to stay active until the user quits\n// // explicitly with Cmd + Q.\n// app.on('window-all-closed', () => {\n//   if (process.platform !== 'darwin') {\n//     app.quit();\n//   }\n// });\n\n// In this file you can include the rest of your app's specific main process\n// code. You can also put them in separate files and require them here.\n\n// import path from 'path';\n// import { app, BrowserWindow } from 'electron';\nimport { store } from '../common/deskreen-electron-store';\n// import i18n from './i18next.config';\nimport i18n from './configs/i18next.config';\nimport { signalingServer } from '../server';\nimport MenuBuilder from './menu';\nimport installExtensions from './utils/installExtensions';\nimport getNewVersionTag from './utils/getNewVersionTag';\nimport { initIpcMainHandlers } from './helpers/ipcMainHandlers';\nimport { initGlobals } from './helpers/initGlobals';\nimport { ElectronStoreKeys } from '../common/ElectronStoreKeys.enum';\nimport { getDeskreenGlobal } from './helpers/getDeskreenGlobal';\nimport { startLogBufferCleanup } from './utils/LoggerWithFilePrefix';\n\nexport default class DeskreenApp {\n\tmainWindow: BrowserWindow | null = null;\n\n\tmenuBuilder: MenuBuilder | null = null;\n\n\tlatestAppVersion = '';\n\n\tinitElectronAppObject(): void {\n\t\t/**\n\t\t * Add event listeners...\n\t\t */\n\t\tapp.on('window-all-closed', () => {\n\t\t\t// TODO: when app will be set to auto start on login, this will be not required,\n\t\t\t// TODO: the app will run until user didn't kill it in system tray\n\t\t\t// Respect the OSX convention of having the application in memory even\n\t\t\t// after all windows have been closed\n\t\t\tif (process.platform !== 'darwin') {\n\t\t\t\tapp.quit();\n\t\t\t}\n\t\t});\n\n\t\tapp.whenReady().then(async () => {\n\t\t\tapp.setAppUserModelId('com.deskreen-ce.app');\n\t\t\tif (process.platform === 'darwin') {\n\t\t\t\tapp.setActivationPolicy('regular');\n\t\t\t}\n\n\t\t\t// start log buffer cleanup to prevent memory bloat\n\t\t\tstartLogBufferCleanup();\n\n\t\t\tawait this.createWindow();\n\n\t\t\tvoid this.checkForLatestVersionAndNotify();\n\t\t});\n\n\t\tapp.on('browser-window-created', (_, window) => {\n\t\t\toptimizer.watchWindowShortcuts(window);\n\t\t});\n\n\t\tapp.on('activate', (e) => {\n\t\t\te.preventDefault();\n\t\t\t// On macOS it's common to re-create a window in the app when the\n\t\t\t// dock icon is clicked and there are no other windows open.\n\t\t\tif (this.mainWindow === null) {\n\t\t\t\tthis.createWindow();\n\t\t\t}\n\t\t});\n\n\t\tapp.commandLine.appendSwitch(\n\t\t\t'webrtc-max-cpu-consumption-percentage',\n\t\t\t'100',\n\t\t);\n\t}\n\n\tprivate async checkForLatestVersionAndNotify(): Promise<void> {\n\t\ttry {\n\t\t\tconst latestAppVersion = await getNewVersionTag();\n\t\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\t\tdeskreenGlobal.latestAppVersion = latestAppVersion;\n\t\t\tthis.latestAppVersion = latestAppVersion;\n\n\t\t\tif (\n\t\t\t\tlatestAppVersion === '' ||\n\t\t\t\tlatestAppVersion === deskreenGlobal.currentAppVersion ||\n\t\t\t\t!Notification.isSupported()\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.showUpdateNotification(latestAppVersion);\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to check for Deskreen updates', error);\n\t\t}\n\t}\n\n\tprivate showUpdateNotification(latestAppVersion: string): void {\n\t\tconst deskreenGlobal = getDeskreenGlobal();\n\t\tconst notification = new Notification({\n\t\t\ttitle: i18n.t('deskreen-ce-update-is-available'),\n\t\t\tbody: `${i18n.t('your-current-version-is')} ${deskreenGlobal.currentAppVersion} | ${i18n.t(\n\t\t\t\t'click-to-download-new-updated-version',\n\t\t\t)} ${latestAppVersion}`,\n\t\t});\n\n\t\tnotification.on('click', () => {\n\t\t\tvoid shell.openExternal('https://deskreen.com/download');\n\t\t});\n\n\t\tnotification.show();\n\t}\n\n\tasync createWindow(): Promise<void> {\n\t\tif (\n\t\t\tprocess.env.NODE_ENV === 'development' ||\n\t\t\tprocess.env.DEBUG_PROD === 'true'\n\t\t) {\n\t\t\tawait installExtensions();\n\t\t}\n\n\t\tthis.mainWindow = new BrowserWindow({\n\t\t\tshow: false,\n\t\t\twidth: 940,\n\t\t\theight: 640,\n\t\t\tminHeight: 460,\n\t\t\tminWidth: 640,\n\t\t\ttitleBarStyle: 'hiddenInset',\n\t\t\tframe: process.platform === 'darwin' ? false : true,\n\t\t\tuseContentSize: true,\n\t\t\ttitle: 'Deskreen CE',\n\t\t\t// useContentSize: true,\n\t\t\tautoHideMenuBar: true,\n\t\t\t...(process.platform === 'linux' ? { icon } : {}),\n\t\t\twebPreferences: {\n\t\t\t\tpreload: join(__dirname, '../preload/index.js'),\n\t\t\t\tsandbox: false,\n\t\t\t},\n\t\t});\n\n\t\t// this.mainWindow.loadURL(`file://${__dirname}/app.html`);\n\n\t\t// @TODO: Use 'ready-to-show' event\n\t\t//        https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event\n\t\tthis.mainWindow.on('ready-to-show', () => {\n\t\t\t// this.mainWindow.webContents.on('did-finish-load', () => {\n\t\t\tif (!this.mainWindow) {\n\t\t\t\tthrow new Error('\"mainWindow\" is not defined');\n\t\t\t}\n\t\t\tif (process.env.START_MINIMIZED === 'true') {\n\t\t\t\tthis.mainWindow.minimize();\n\t\t\t} else {\n\t\t\t\tthis.mainWindow.show();\n\t\t\t\tthis.mainWindow.focus();\n\t\t\t}\n\t\t\t// });\n\t\t});\n\n\t\tthis.mainWindow.webContents.setWindowOpenHandler((details) => {\n\t\t\tshell.openExternal(details.url);\n\t\t\treturn { action: 'deny' };\n\t\t});\n\n\t\t// HMR for renderer base on electron-vite cli.\n\t\t// Load the remote URL for development or the local html file for production.\n\t\tif (is.dev && process.env['ELECTRON_RENDERER_URL']) {\n\t\t\tthis.mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);\n\t\t} else {\n\t\t\tthis.mainWindow.loadFile(join(__dirname, '../renderer/index.html'));\n\t\t}\n\n\t\tthis.mainWindow.on('closed', () => {\n\t\t\tthis.mainWindow = null;\n\t\t\t// TODO: when app will be set to auto start on login, this will be not required,\n\t\t\t// TODO: the app will run until user didn't kill it in system tray\n\t\t\tif (process.platform !== 'darwin') {\n\t\t\t\tapp.quit();\n\t\t\t}\n\t\t});\n\n\t\tif (process.env.NODE_ENV === 'dev') {\n\t\t\tthis.mainWindow.webContents.toggleDevTools();\n\t\t}\n\n\t\tthis.menuBuilder = new MenuBuilder(this.mainWindow, i18n);\n\t\tthis.menuBuilder.buildMenu();\n\n\t\tthis.initI18n();\n\n\t\tinitIpcMainHandlers(this.mainWindow);\n\t}\n\n\tinitI18n(): void {\n\t\ti18n.on('loaded', () => {\n\t\t\ti18n.changeLanguage('en');\n\t\t\ti18n.off('loaded');\n\t\t});\n\n\t\ti18n.on('languageChanged', (lng) => {\n\t\t\tif (this.mainWindow === null) return;\n\t\t\tthis.menuBuilder = new MenuBuilder(this.mainWindow, i18n);\n\t\t\tthis.menuBuilder.buildMenu();\n\t\t\tsetTimeout(async () => {\n\t\t\t\tif (lng !== 'en' && i18n.language !== lng) {\n\t\t\t\t\ti18n.changeLanguage(lng);\n\t\t\t\t\tif (store.has(ElectronStoreKeys.AppLanguage)) {\n\t\t\t\t\t\tstore.delete(ElectronStoreKeys.AppLanguage);\n\t\t\t\t\t}\n\t\t\t\t\tstore.set(ElectronStoreKeys.AppLanguage, lng);\n\t\t\t\t}\n\t\t\t}, 400);\n\t\t});\n\t}\n\n\tstart(): void {\n\t\t// ensure only one instance of the app can run\n\t\tconst gotTheLock = app.requestSingleInstanceLock();\n\n\t\tif (!gotTheLock) {\n\t\t\t// another instance is already running, quit this one\n\t\t\tapp.quit();\n\t\t\treturn;\n\t\t}\n\n\t\t// handle second instance attempts (e.g., clicking taskbar icon on windows)\n\t\tapp.on('second-instance', () => {\n\t\t\tif (this.mainWindow) {\n\t\t\t\tif (this.mainWindow.isMinimized()) {\n\t\t\t\t\tthis.mainWindow.restore();\n\t\t\t\t}\n\t\t\t\tthis.mainWindow.focus();\n\t\t\t\tthis.mainWindow.show();\n\t\t\t}\n\t\t});\n\n\t\tconst cliLocalIp = this.parseCliLocalIp();\n\t\tinitGlobals(join(__dirname, '..'), cliLocalIp);\n\t\tsignalingServer.start();\n\n\t\tthis.initElectronAppObject();\n\t}\n\n\tprivate parseCliLocalIp(): string | undefined {\n\t\tconst args = process.argv;\n\t\tconst localIpIndex = args.findIndex(\n\t\t\t(arg) => arg === '--local-ip' || arg === '--ip',\n\t\t);\n\t\tif (localIpIndex !== -1 && localIpIndex + 1 < args.length) {\n\t\t\tconst ip = args[localIpIndex + 1];\n\t\t\tif (ip && !ip.startsWith('--')) {\n\t\t\t\treturn ip;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n}\n\nexport const deskreenApp = new DeskreenApp();\ndeskreenApp.start();\n"
  },
  {
    "path": "src/main/menu.ts",
    "content": "import {\n\tMenu,\n\tshell,\n\tBrowserWindow,\n\tMenuItemConstructorOptions,\n\tapp,\n} from 'electron';\n\nimport i18nType from './configs/i18next.config';\n\ninterface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {\n\tselector?: string;\n\tsubmenu?: DarwinMenuItemConstructorOptions[] | Menu;\n}\n\nexport default class MenuBuilder {\n\tmainWindow: BrowserWindow;\n\n\ti18n: typeof i18nType;\n\n\tconstructor(mainWindow: BrowserWindow, i18n: typeof i18nType) {\n\t\tthis.mainWindow = mainWindow;\n\t\tthis.i18n = i18n;\n\t}\n\n\tbuildMenu(): void {\n\t\tif (\n\t\t\tprocess.env.NODE_ENV === 'development' ||\n\t\t\tprocess.env.DEBUG_PROD === 'true'\n\t\t) {\n\t\t\tthis.setupDevelopmentEnvironment();\n\t\t}\n\n\t\tif (process.platform === 'darwin') {\n\t\t\tconst menu = Menu.buildFromTemplate(this.buildDarwinTemplate());\n\t\t\tMenu.setApplicationMenu(menu);\n\t\t} else {\n\t\t\t// for production, no menu for non MacOS app\n\t\t\tMenu.setApplicationMenu(null);\n\t\t}\n\t}\n\n\tsetupDevelopmentEnvironment(): void {\n\t\tthis.mainWindow.webContents.on('context-menu', (_, props) => {\n\t\t\tconst { x, y } = props;\n\n\t\t\tMenu.buildFromTemplate([\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Inspect element',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tthis.mainWindow.webContents.inspectElement(x, y);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]).popup({ window: this.mainWindow });\n\t\t});\n\t}\n\n\tbuildDarwinTemplate(): MenuItemConstructorOptions[] {\n\t\tconst subMenuAbout: DarwinMenuItemConstructorOptions = {\n\t\t\tlabel: 'Deskreen CE',\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('about-deskreen'),\n\t\t\t\t\tselector: 'orderFrontStandardAboutPanel:',\n\t\t\t\t},\n\t\t\t\t{ type: 'separator' },\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('hide-deskreen'),\n\t\t\t\t\taccelerator: 'Command+H',\n\t\t\t\t\tselector: 'hide:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('hide-others'),\n\t\t\t\t\taccelerator: 'Command+Shift+H',\n\t\t\t\t\tselector: 'hideOtherApplications:',\n\t\t\t\t},\n\t\t\t\t{ label: this.i18n.t('show-all'), selector: 'unhideAllApplications:' },\n\t\t\t\t{ type: 'separator' },\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('quit'),\n\t\t\t\t\taccelerator: 'Command+Q',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tapp.quit();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst subMenuEdit: DarwinMenuItemConstructorOptions = {\n\t\t\tlabel: this.i18n.t('edit'),\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('undo'),\n\t\t\t\t\taccelerator: 'Command+Z',\n\t\t\t\t\tselector: 'undo:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('redo'),\n\t\t\t\t\taccelerator: 'Shift+Command+Z',\n\t\t\t\t\tselector: 'redo:',\n\t\t\t\t},\n\t\t\t\t{ type: 'separator' },\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('cut'),\n\t\t\t\t\taccelerator: 'Command+X',\n\t\t\t\t\tselector: 'cut:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('copy'),\n\t\t\t\t\taccelerator: 'Command+C',\n\t\t\t\t\tselector: 'copy:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('paste'),\n\t\t\t\t\taccelerator: 'Command+V',\n\t\t\t\t\tselector: 'paste:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('select-all'),\n\t\t\t\t\taccelerator: 'Command+A',\n\t\t\t\t\tselector: 'selectAll:',\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst subMenuViewDev: MenuItemConstructorOptions = {\n\t\t\tlabel: this.i18n.t('view'),\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('reload'),\n\t\t\t\t\taccelerator: 'Command+R',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tthis.mainWindow.webContents.reload();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('toggle-full-screen'),\n\t\t\t\t\taccelerator: 'Ctrl+Command+F',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tthis.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('toggle-developer-tools'),\n\t\t\t\t\taccelerator: 'Alt+Command+I',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tthis.mainWindow.webContents.toggleDevTools();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst subMenuViewProd: MenuItemConstructorOptions = {\n\t\t\tlabel: this.i18n.t('view'),\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('toggle-full-screen'),\n\t\t\t\t\taccelerator: 'Ctrl+Command+F',\n\t\t\t\t\tclick: () => {\n\t\t\t\t\t\tthis.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst subMenuWindow: DarwinMenuItemConstructorOptions = {\n\t\t\tlabel: this.i18n.t('window'),\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('minimize'),\n\t\t\t\t\taccelerator: 'Command+M',\n\t\t\t\t\tselector: 'performMiniaturize:',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('close'),\n\t\t\t\t\taccelerator: 'Command+W',\n\t\t\t\t\tselector: 'performClose:',\n\t\t\t\t},\n\t\t\t\t{ type: 'separator' },\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('bring-all-to-front'),\n\t\t\t\t\tselector: 'arrangeInFront:',\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst subMenuHelp: MenuItemConstructorOptions = {\n\t\t\tlabel: this.i18n.t('help'),\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.i18n.t('learn-more'),\n\t\t\t\t\tclick() {\n\t\t\t\t\t\tshell.openExternal('https://www.deskreen.com');\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// {\n\t\t\t\t//   label: this.i18n.t('Documentation'),\n\t\t\t\t//   click() {\n\t\t\t\t//     shell.openExternal(\n\t\t\t\t//       'https://github.com/pavlobu/deskreen/blob/master/README.md'\n\t\t\t\t//     );\n\t\t\t\t//   },\n\t\t\t\t// },\n\t\t\t\t// {\n\t\t\t\t//   label: this.i18n.t('Community Discussions'),\n\t\t\t\t//   click() {\n\t\t\t\t//     shell.openExternal(\n\t\t\t\t//       'https://github.com/pavlobu/deskreen/discussions'\n\t\t\t\t//     );\n\t\t\t\t//   },\n\t\t\t\t// },\n\t\t\t\t// {\n\t\t\t\t//   label: this.i18n.t('Search Issues'),\n\t\t\t\t//   click() {\n\t\t\t\t//     shell.openExternal('https://github.com/pavlobu/deskreen/issues');\n\t\t\t\t//   },\n\t\t\t\t// },\n\t\t\t],\n\t\t};\n\n\t\tconst subMenuView =\n\t\t\tprocess.env.NODE_ENV === 'development' ||\n\t\t\tprocess.env.DEBUG_PROD === 'true'\n\t\t\t\t? subMenuViewDev\n\t\t\t\t: subMenuViewProd;\n\n\t\treturn [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];\n\t}\n}\n"
  },
  {
    "path": "src/main/utils/LoggerWithFilePrefix.ts",
    "content": "import log from 'electron-log';\n\nlog.transports.file.level = 'warn';\n\nif (process.env.NODE_ENV !== 'production') {\n\tlog.transports.console.level = 'silly';\n} else {\n\tlog.transports.console.level = 'debug'; // TODO: make false when doing release\n}\n\n// configure log file rotation and size limits to prevent memory bloat\nif (log.transports.file) {\n\t// limit individual log file size to 10MB\n\tlog.transports.file.maxSize = 10 * 1024 * 1024; // 10MB in bytes\n\t// electron-log automatically rotates and deletes old files when maxSize is exceeded\n}\n\n// periodic cleanup of log buffers to release memory\nlet logCleanupInterval: NodeJS.Timeout | null = null;\n\nfunction cleanupLogBuffers(): void {\n\ttry {\n\t\t// electron-log automatically handles file rotation based on maxSize\n\t\t// this function helps trigger memory cleanup\n\n\t\t// force garbage collection hint if available (Node.js v12.17.0+)\n\t\tif (global.gc && typeof global.gc === 'function') {\n\t\t\t// only run GC in production to avoid performance impact in dev\n\t\t\tif (process.env.NODE_ENV === 'production') {\n\t\t\t\tglobal.gc();\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// silently fail cleanup to avoid log spam\n\t}\n}\n\n// start periodic cleanup every 5 minutes\nexport function startLogBufferCleanup(): void {\n\tif (logCleanupInterval) {\n\t\treturn; // already started\n\t}\n\n\tlogCleanupInterval = setInterval(\n\t\t() => {\n\t\t\tcleanupLogBuffers();\n\t\t},\n\t\t5 * 60 * 1000,\n\t); // every 5 minutes\n}\n\n// stop periodic cleanup (useful for testing or shutdown)\nexport function stopLogBufferCleanup(): void {\n\tif (logCleanupInterval) {\n\t\tclearInterval(logCleanupInterval);\n\t\tlogCleanupInterval = null;\n\t}\n}\n\nexport default class LoggerWithFilePrefix {\n\tfilenamePath: string;\n\n\telectronLog: typeof log;\n\n\tconstructor(_filenamePath: string) {\n\t\tthis.filenamePath = _filenamePath;\n\t\tthis.electronLog = log;\n\t}\n\n\terror(...args: unknown[]): void {\n\t\tthis.electronLog.error(this.filenamePath, ':', ...args);\n\t}\n\n\twarn(...args: unknown[]): void {\n\t\tthis.electronLog.warn(this.filenamePath, ':', ...args);\n\t}\n\n\tinfo(...args: unknown[]): void {\n\t\tthis.electronLog.info(this.filenamePath, ':', ...args);\n\t}\n\n\tverbose(...args: unknown[]): void {\n\t\tthis.electronLog.verbose(this.filenamePath, ':', ...args);\n\t}\n\n\tdebug(...args: unknown[]): void {\n\t\tthis.electronLog.debug(this.filenamePath, ':', ...args);\n\t}\n\n\tsilly(...args: unknown[]): void {\n\t\tthis.electronLog.silly(this.filenamePath, ':', ...args);\n\t}\n}\n"
  },
  {
    "path": "src/main/utils/getNewVersionTag.ts",
    "content": "import axios from 'axios';\n\nconst githubApiRepoLatestReleaseUrl =\n\t'https://api.github.com/repos/pavlobu/deskreen/releases/latest';\n\nexport default async function getNewVersionTag(): Promise<string> {\n\ttry {\n\t\tconst response = await axios({\n\t\t\turl: githubApiRepoLatestReleaseUrl,\n\t\t\tmethod: 'get',\n\t\t\theaders: { 'User-Agent': 'node.js' },\n\t\t});\n\n\t\tconst tagName = response.data?.tag_name;\n\t\tif (typeof tagName !== 'string' || tagName.length === 0) {\n\t\t\treturn '';\n\t\t}\n\n\t\treturn tagName.startsWith('v') ? tagName.slice(1) : tagName;\n\t} catch (error) {\n\t\tconsole.error('Failed to fetch latest Deskreen release tag', error);\n\t\treturn '';\n\t}\n}\n"
  },
  {
    "path": "src/main/utils/installExtensions.ts",
    "content": "import {\n\tinstallExtension,\n\tREACT_DEVELOPER_TOOLS,\n} from 'electron-devtools-installer';\n\nexport default async function installExtensions(): Promise<void> {\n\t// const forceDownload = !!process.env.UPGRADE_EXTENSIONS;\n\t// const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];\n\t//\n\t// return Promise.all(\n\t//   extensions.map((name) => installer.default(installer[name], forceDownload)),\n\t// ).catch(console.log);\n\n\tinstallExtension([REACT_DEVELOPER_TOOLS])\n\t\t.then(([react]) => console.log(`Added Extensions: ${react.name}`))\n\t\t.catch((err) => console.log('An error occurred: ', err));\n}\n"
  },
  {
    "path": "src/main/utils/isLinuxWaylandSession.ts",
    "content": "const isLinuxWaylandSession =\n\tprocess.platform === 'linux' &&\n\t(process.env.XDG_SESSION_TYPE?.toLowerCase() === 'wayland' ||\n\t\tprocess.env.WAYLAND_DISPLAY != null);\n\nexport default isLinuxWaylandSession;\n"
  },
  {
    "path": "src/preload/index.d.ts",
    "content": "import { ElectronAPI } from '@electron-toolkit/preload';\nimport forge from 'node-forge';\n\ndeclare global {\n\tinterface Window {\n\t\telectron: ElectronAPI;\n\t\tapi: {\n\t\t\tforge: typeof forge;\n\t\t\tBuffer: typeof Buffer;\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "src/preload/index.ts",
    "content": "import { contextBridge } from 'electron';\nimport { electronAPI } from '@electron-toolkit/preload';\nimport forge from 'node-forge';\n// import SimplePeer from 'simple-peer';\n// import wrtc from '@roamhq/wrtc';\n// import * as SimplePeerMin from './simplepeer.min.js';\n\n// Custom APIs for renderer\nconst api = {\n\t// SimplePeer: SimplePeer,\n\t// createSimplePeer: (opts) => {\n\t//   // Merge user-provided options with the wrtc module\n\t//   // This ensures SimplePeer uses the wrtc loaded in the preload process.\n\t//   return new SimplePeer({ ...opts });\n\t// },\n\t// SimplePeerMin: SimplePeerMin,\n\t// wrtc: wrtc,\n\tforge: forge,\n\tBuffer: Buffer,\n};\n\n// Use `contextBridge` APIs to expose Electron APIs to\n// renderer only if context isolation is enabled, otherwise\n// just add to the DOM global.\nif (process.contextIsolated) {\n\ttry {\n\t\tcontextBridge.exposeInMainWorld('electron', electronAPI);\n\t\tcontextBridge.exposeInMainWorld('api', api);\n\t} catch (error) {\n\t\tconsole.error(error);\n\t}\n} else {\n\t// @ts-ignore (define in dts)\n\twindow.electron = electronAPI;\n\t// @ts-ignore (define in dts)\n\twindow.api = api;\n}\n"
  },
  {
    "path": "src/renderer/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n\t  <title>Deskreen CE</title>\n    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->\n    <meta\n      http-equiv=\"Content-Security-Policy\"\n      content=\"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://127.0.0.1:* http://localhost:*\"\n    />\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/peerConnectionHelperRendererWindowIndex.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Electron Helper Renderer</title>\n    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->\n<!--    <meta-->\n<!--      http-equiv=\"Content-Security-Policy\"-->\n<!--      content=\"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:\"-->\n<!--    />-->\n    <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; connect-src *;\"/>\n    <script src=\"./assets/simplepeer.min.js\"></script>\n  </head>\n  <body>\n    <div id=\"root\">\n    </div>\n\n    <script type=\"module\" src=\"/src/peerConnectionHelperRendererWindowIndex.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/src/assets/base.css",
    "content": "/*:root {*/\n/*  --ev-c-white: #ffffff;*/\n/*  --ev-c-white-soft: #f8f8f8;*/\n/*  --ev-c-white-mute: #f2f2f2;*/\n\n/*  --ev-c-black: #1b1b1f;*/\n/*  --ev-c-black-soft: #222222;*/\n/*  --ev-c-black-mute: #282828;*/\n\n/*  --ev-c-gray-1: #515c67;*/\n/*  --ev-c-gray-2: #414853;*/\n/*  --ev-c-gray-3: #32363f;*/\n\n/*  --ev-c-text-1: rgba(255, 255, 245, 0.86);*/\n/*  --ev-c-text-2: rgba(235, 235, 245, 0.6);*/\n/*  --ev-c-text-3: rgba(235, 235, 245, 0.38);*/\n\n/*  --ev-button-alt-border: transparent;*/\n/*  --ev-button-alt-text: var(--ev-c-text-1);*/\n/*  --ev-button-alt-bg: var(--ev-c-gray-3);*/\n/*  --ev-button-alt-hover-border: transparent;*/\n/*  --ev-button-alt-hover-text: var(--ev-c-text-1);*/\n/*  --ev-button-alt-hover-bg: var(--ev-c-gray-2);*/\n/*}*/\n\n/*:root {*/\n/*  --color-background: var(--ev-c-black);*/\n/*  --color-background-soft: var(--ev-c-black-soft);*/\n/*  --color-background-mute: var(--ev-c-black-mute);*/\n\n/*  --color-text: var(--ev-c-text-1);*/\n/*}*/\n\n/**,*/\n/**::before,*/\n/**::after {*/\n/*  box-sizing: border-box;*/\n/*  margin: 0;*/\n/*  font-weight: normal;*/\n/*}*/\n\n/*ul {*/\n/*  list-style: none;*/\n/*}*/\n\n/*body {*/\n/*  min-height: 100vh;*/\n/*  color: var(--color-text);*/\n/*  background: var(--color-background);*/\n/*  line-height: 1.6;*/\n/*  font-family:*/\n/*    Inter,*/\n/*    -apple-system,*/\n/*    BlinkMacSystemFont,*/\n/*    'Segoe UI',*/\n/*    Roboto,*/\n/*    Oxygen,*/\n/*    Ubuntu,*/\n/*    Cantarell,*/\n/*    'Fira Sans',*/\n/*    'Droid Sans',*/\n/*    'Helvetica Neue',*/\n/*    sans-serif;*/\n/*  text-rendering: optimizeLegibility;*/\n/*  -webkit-font-smoothing: antialiased;*/\n/*  -moz-osx-font-smoothing: grayscale;*/\n/*}*/\n\n/*\n * @NOTE: Prepend a `~` to css file paths that are in your node_modules\n *        See https://github.com/webpack-contrib/sass-loader#imports\n */\n@import \"@fortawesome/fontawesome-free/css/all.css\";\n@import \"normalize.css/normalize.css\";\n@import \"@blueprintjs/core/lib/css/blueprint.css\";\n@import \"react-flexbox-grid/dist/react-flexbox-grid.css\";\n@import \"fontsource-lexend-peta/index.css\";\n\n:root {\n\t--light-bg-color: rgba(240, 248, 250, 1);\n\t--light-btn-no-intent-color: rgb(218, 238, 243);\n\t--custom-scrollbar-webkit-scrollbar-thumb-border-radius: 10px;\n\t--custom-scrollbar-webkit-scrollbar-thumb-background-color: #8a9ba8;\n\t--custom-scrollbar-webkit-scrollbar_background-color: rgba(0, 0, 0, 0);\n\t--custom-scrollbar-webkit-scrollbar-width: 12px;\n\t--custom-scrollbar-webkit-scrollbar-track-border-radius: 10px;\n\t--custom-scrollbar-webkit-scrollbar-track-background-color: rgba(0, 0, 0, 0);\n}\n\nbody {\n\tposition: relative;\n\theight: 100vh;\n\tfont-family:\n\t\tArial,\n\t\tHelvetica,\n\t\tHelvetica Neue,\n\t\tserif;\n\toverflow: hidden;\n\tbackground-color: var(--light-bg-color);\n}\n\n.bp3-alert-footer > button {\n\tborder-radius: 100px;\n}\n\n#intermediate-step-container > .react-reveal {\n\twidth: 100%;\n}\n\n#choose-app-or-screen-overlay-container > .react-reveal {\n\theight: 0%;\n}\n\n/* UI colors START */\n.bp3-button:not([class*=\"bp3-intent-\"]) {\n\tbackground-color: var(--light-btn-no-intent-color);\n}\n\n.bp3-dialog {\n\tbackground-color: var(--light-bg-color) !important;\n}\n\n.bp3-popover .bp3-popover-arrow-fill {\n\tfill: var(--light-bg-color);\n}\n\n.bp3-popover .bp3-popover-content {\n\tbackground-color: var(--light-bg-color);\n\tcolor: black;\n}\n\n.bp3-html-select > select {\n\tbackground-color: var(--light-btn-no-intent-color);\n}\n\n.bp3-drawer {\n\tbackground-color: var(--light-bg-color);\n}\n\n.bp3-card {\n\tbackground-color: var(--light-bg-color);\n}\n\n/* for really small screen sizes (ex. Raspberry PI display etc. */\n@media screen and (max-height: 419px) {\n\tbody {\n\t\toverflow-y: scroll;\n\t}\n}\n\n.react-toast-notifications__container {\n\toverflow: hidden !important;\n}\n\n/* Connected Devices List button pulse START */\n#top-panel-connected-devices-list-button.pulsing {\n\ttransform: scale(1);\n\tanimation: pulse-black-devices-list-button 0.75s infinite;\n}\n\n#top-panel-connected-devices-list-button.pulse-not-infinite {\n\ttransform: scale(1);\n\tanimation: pulse-black-devices-list-button 0.75s 4;\n}\n\n@keyframes pulse-black-devices-list-button {\n\t0% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 0 rgba(115, 134, 148, 0.7);\n\t}\n\n\t60% {\n\t\ttransform: scale(0.85);\n\t\tbox-shadow: 0 0 0 15px rgba(115, 134, 148, 0.3);\n\t}\n\n\t100% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 5px rgba(0, 0, 0, 0);\n\t}\n}\n\n/* Connected Devices List button pulse END */\n\n/* For choose app or screen overlay popup without scrollbars! */\n.bp3-overlay-scroll-container {\n\toverflow-y: hidden !important;\n}\n\n/* help cursor when text hovered Connected Devices List */\n#connected-devices-list-text-success:hover {\n\tcursor: help;\n}\n\n/* react-toast-notifications progress bar more obvious look */\nbody > div.react-toast-notifications__container\n\t> div\n\t> div\n\t> div.react-toast-notifications__toast__icon-wrapper\n\t> div {\n\tbackground-color: rgba(0, 0, 0, 0.4);\n}\n\n.hide-toaster-progress {\n\theight: 5px;\n\twidth: calc(100% + 87px) !important;\n\tbottom: -11px !important;\n\tleft: -40px !important;\n}\n\ndiv.class-allow-device-to-connect-alert {\n\tz-index: 9999;\n}\n\n.rounded-pill-alert .bp3-alert-footer .bp3-button,\n.rounded-pill-alert .bp5-alert-footer .bp5-button,\n.rounded-pill-alert .bp6-alert-footer .bp6-button {\n\tborder-radius: 9999px;\n}\n\n/* ALLOW CONNECTION ALERT BLINK ANIMATION START */\ndiv.class-allow-device-to-connect-alert > div.bp3-alert-body\n\t> span\n\t> svg\n\t> path {\n\tcolor: #a82a2a;\n\t-webkit-animation: blink 0.75s infinite alternate;\n\n\t/* to blink 3 times instead of infinite write just 3 */\n\t-moz-animation: blink 0.75s infinite alternate;\n\t-ms-animation: blink 0.75s infinite alternate;\n\t-o-animation: blink 0.75s infinite alternate;\n\tanimation: blink 0.75s infinite alternate;\n}\n\n@-webkit-keyframes blink {\n\tfrom {\n\t\tcolor: #a82a2a;\n\t}\n\n\tto {\n\t\tcolor: #f55656;\n\t}\n}\n\n@-moz-keyframes blink {\n\tfrom {\n\t\tcolor: #a82a2a;\n\t}\n\n\tto {\n\t\tcolor: #f55656;\n\t}\n}\n\n@-ms-keyframes blink {\n\tfrom {\n\t\tcolor: #a82a2a;\n\t}\n\n\tto {\n\t\tcolor: #f55656;\n\t}\n}\n\n@-o-keyframes blink {\n\tfrom {\n\t\tcolor: #a82a2a;\n\t}\n\n\tto {\n\t\tcolor: #f55656;\n\t}\n}\n\n@keyframes blink {\n\tfrom {\n\t\tcolor: #a82a2a;\n\t}\n\n\tto {\n\t\tcolor: #f55656;\n\t}\n}\n\n/* ALLOW CONNECTION ALERT BLINK ANIMATION END */\n\n/* Connected Device Info Button pulse animation START */\n\n#connected-device-info-stepper-button {\n\ttransform: scale(1);\n\tanimation: pulse-black-connected-device 0.75s 3;\n}\n\n@keyframes pulse-black-connected-device {\n\t0% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 0 rgba(61, 204, 145, 0.7);\n\t}\n\n\t60% {\n\t\ttransform: scale(0.75);\n\t\tbox-shadow: 0 0 0 15px rgba(61, 204, 145, 0.3);\n\t}\n\n\t100% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 5px rgba(0, 0, 0, 0);\n\t}\n}\n\n/* Connected Device Info Button pulse animation END */\n\n#settings-overlay-inner > div > div.bp3-tab-panel {\n\twidth: 100% !important;\n}\n\n/* settings panel tabs button left styles */\n#settings-overlay-inner > div > div.bp3-tab-list {\n\tbackground-color: rgba(0, 0, 0, 0.1);\n\tpadding: 8px;\n}\n\n/* settings inner 100% height regardless tab content height */\n#settings-overlay-inner > div {\n\theight: 100%;\n}\n\n.bp3-overlay-settings {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n/* TODO: move to appropriate style file in ShareEntireScreenOrAppWindowControlGroup */\n#share-screen-or-app-btn-group > button > span {\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tflex-direction: column;\n}\n\n.active-stepper-pulse-icon {\n\ttransform: scale(1);\n\tanimation: pulse-black 3s infinite;\n}\n\n@keyframes pulse-black {\n\t0% {\n\t\ttransform: scale(0.9);\n\t\tbox-shadow: 0 0 0 0 rgba(191, 115, 38, 0.7);\n\t}\n\n\t60% {\n\t\ttransform: scale(1);\n\t\tbox-shadow: 0 0 0 12px rgba(255, 179, 102, 0.3);\n\t}\n\n\t100% {\n\t\ttransform: scale(0.9);\n\t\tbox-shadow: 0 0 0 20px rgba(0, 0, 0, 0);\n\t}\n}\n\n/* TODO: move it to DeskreenStepper.css ! */\n#step-label-deskreen > span.MuiStepLabel-labelContainer > span {\n\tmargin-top: 8px;\n}\n\n#share-screen-or-app-btn-group > button:nth-child(1):hover {\n\tborder-width: 10px;\n}\n\n.bp3-overlay::-webkit-scrollbar-track {\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-track-border-radius);\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-track-background-color\n\t);\n}\n\n.bp3-overlay::-webkit-scrollbar {\n\twidth: var(--custom-scrollbar-webkit-scrollbar-width);\n\tbackground-color: var(--custom-scrollbar-webkit-scrollbar_background-color);\n}\n\n.bp3-overlay::-webkit-scrollbar-thumb {\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-thumb-border-radius);\n\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); */\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-thumb-background-color\n\t);\n}\n\n.bp3-drawer::-webkit-scrollbar-track {\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-track-border-radius);\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-track-background-color\n\t);\n}\n\n.bp3-drawer::-webkit-scrollbar {\n\twidth: var(--custom-scrollbar-webkit-scrollbar-width);\n\tbackground-color: var(--custom-scrollbar-webkit-scrollbar_background-color);\n}\n\n.bp3-drawer::-webkit-scrollbar-thumb {\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-thumb-border-radius);\n\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); */\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-thumb-background-color\n\t);\n}\n\nbody::-webkit-scrollbar-track {\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-track-border-radius);\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-track-background-color\n\t);\n}\n\nbody::-webkit-scrollbar {\n\twidth: var(--custom-scrollbar-webkit-scrollbar-width);\n\tbackground-color: var(--custom-scrollbar-webkit-scrollbar_background-color);\n}\n\nbody::-webkit-scrollbar-thumb {\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-thumb-border-radius);\n\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); */\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-thumb-background-color\n\t);\n}\n\n.choose-app-or-screen-dialog::-webkit-scrollbar-track {\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-track-border-radius);\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-track-background-color\n\t);\n}\n\n.choose-app-or-screen-dialog::-webkit-scrollbar {\n\twidth: var(--custom-scrollbar-webkit-scrollbar-width);\n\tbackground-color: var(--custom-scrollbar-webkit-scrollbar_background-color);\n}\n\n.choose-app-or-screen-dialog::-webkit-scrollbar-thumb {\n\tborder-radius: var(--custom-scrollbar-webkit-scrollbar-thumb-border-radius);\n\n\t/* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); */\n\tbackground-color: var(\n\t\t--custom-scrollbar-webkit-scrollbar-thumb-background-color\n\t);\n}\n\n/* --custom-scrollbar-webkit-scrollbar-thumb-border-radius: 10px;\n--custom-scrollbar-webkit-scrollbar-thumb-background-color: #8A9BA8;\n--custom-scrollbar-webkit-scrollbar_background-color: rgba(0,0,0,0);\n--custom-scrollbar-webkit-scrollbar-width: 12px;\n--custom-scrollbar-webkit-scrollbar-track-border-radius: 10px;\n--custom-scrollbar-webkit-scrollbar-track-background-color: rgba(0,0,0,0); */\n\nh2 {\n\tmargin: 0;\n\tfont-size: 2.25rem;\n\tfont-weight: bold;\n\tletter-spacing: -0.025em;\n\tcolor: #fff;\n}\n\np {\n\tfont-size: 24px;\n}\n\nli {\n\tlist-style: none;\n}\n\na {\n\tcolor: white;\n\topacity: 0.75;\n\ttext-decoration: none;\n}\n\na:hover {\n\topacity: 1;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n#new-version-header {\n\tbackground: rgba(0, 255, 54, 0.48);\n\twidth: fit-content;\n\tborder-radius: 100px;\n\tpadding: 5px;\n}\n\n#new-version-header:hover {\n\tbackground: rgba(0, 255, 54, 0.78);\n\tcursor: pointer;\n}\n\n.bp3-tab-list {\n\theight: calc(100vh - 30%);\n}\n"
  },
  {
    "path": "src/renderer/src/assets/main.css",
    "content": "@import \"./base.css\";\n\n/*body {*/\n/*  display: flex;*/\n/*  align-items: center;*/\n/*  justify-content: center;*/\n/*  overflow: hidden;*/\n/*  background-image: url('./wavy-lines.svg');*/\n/*  background-size: cover;*/\n/*  user-select: none;*/\n/*}*/\n\n/*code {*/\n/*  font-weight: 600;*/\n/*  padding: 3px 5px;*/\n/*  border-radius: 2px;*/\n/*  background-color: var(--color-background-mute);*/\n/*  font-family:*/\n/*    ui-monospace,*/\n/*    SFMono-Regular,*/\n/*    SF Mono,*/\n/*    Menlo,*/\n/*    Consolas,*/\n/*    Liberation Mono,*/\n/*    monospace;*/\n/*  font-size: 85%;*/\n/*}*/\n\n/*#root {*/\n/*  display: flex;*/\n/*  align-items: center;*/\n/*  justify-content: center;*/\n/*  flex-direction: column;*/\n/*  margin-bottom: 80px;*/\n/*}*/\n\n/*.logo {*/\n/*  margin-bottom: 20px;*/\n/*  -webkit-user-drag: none;*/\n/*  height: 128px;*/\n/*  width: 128px;*/\n/*  will-change: filter;*/\n/*  transition: filter 300ms;*/\n/*}*/\n\n/*.logo:hover {*/\n/*  filter: drop-shadow(0 0 1.2em #6988e6aa);*/\n/*}*/\n\n/*.creator {*/\n/*  font-size: 14px;*/\n/*  line-height: 16px;*/\n/*  color: var(--ev-c-text-2);*/\n/*  font-weight: 600;*/\n/*  margin-bottom: 10px;*/\n/*}*/\n\n/*.text {*/\n/*  font-size: 28px;*/\n/*  color: var(--ev-c-text-1);*/\n/*  font-weight: 700;*/\n/*  line-height: 32px;*/\n/*  text-align: center;*/\n/*  margin: 0 10px;*/\n/*  padding: 16px 0;*/\n/*}*/\n\n/*.tip {*/\n/*  font-size: 16px;*/\n/*  line-height: 24px;*/\n/*  color: var(--ev-c-text-2);*/\n/*  font-weight: 600;*/\n/*}*/\n\n/*.react {*/\n/*  background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee);*/\n/*  background-clip: text;*/\n/*  -webkit-background-clip: text;*/\n/*  -webkit-text-fill-color: transparent;*/\n/*  font-weight: 700;*/\n/*}*/\n\n/*.ts {*/\n/*  background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e);*/\n/*  background-clip: text;*/\n/*  -webkit-background-clip: text;*/\n/*  -webkit-text-fill-color: transparent;*/\n/*  font-weight: 700;*/\n/*}*/\n\n/*.actions {*/\n/*  display: flex;*/\n/*  padding-top: 32px;*/\n/*  margin: -6px;*/\n/*  flex-wrap: wrap;*/\n/*  justify-content: flex-start;*/\n/*}*/\n\n/*.action {*/\n/*  flex-shrink: 0;*/\n/*  padding: 6px;*/\n/*}*/\n\n/*.action a {*/\n/*  cursor: pointer;*/\n/*  text-decoration: none;*/\n/*  display: inline-block;*/\n/*  border: 1px solid transparent;*/\n/*  text-align: center;*/\n/*  font-weight: 600;*/\n/*  white-space: nowrap;*/\n/*  border-radius: 20px;*/\n/*  padding: 0 20px;*/\n/*  line-height: 38px;*/\n/*  font-size: 14px;*/\n/*  border-color: var(--ev-button-alt-border);*/\n/*  color: var(--ev-button-alt-text);*/\n/*  background-color: var(--ev-button-alt-bg);*/\n/*}*/\n\n/*.action a:hover {*/\n/*  border-color: var(--ev-button-alt-hover-border);*/\n/*  color: var(--ev-button-alt-hover-text);*/\n/*  background-color: var(--ev-button-alt-hover-bg);*/\n/*}*/\n\n/*.versions {*/\n/*  position: absolute;*/\n/*  bottom: 30px;*/\n/*  margin: 0 auto;*/\n/*  padding: 15px 0;*/\n/*  font-family: 'Menlo', 'Lucida Console', monospace;*/\n/*  display: inline-flex;*/\n/*  overflow: hidden;*/\n/*  align-items: center;*/\n/*  border-radius: 22px;*/\n/*  background-color: #202127;*/\n/*  backdrop-filter: blur(24px);*/\n/*}*/\n\n/*.versions li {*/\n/*  display: block;*/\n/*  float: left;*/\n/*  border-right: 1px solid var(--ev-c-gray-1);*/\n/*  padding: 0 20px;*/\n/*  font-size: 14px;*/\n/*  line-height: 14px;*/\n/*  opacity: 0.8;*/\n/*  &:last-child {*/\n/*    border: none;*/\n/*  }*/\n/*}*/\n\n/*@media (max-width: 720px) {*/\n/*  .text {*/\n/*    font-size: 20px;*/\n/*  }*/\n/*}*/\n\n/*@media (max-width: 620px) {*/\n/*  .versions {*/\n/*    display: none;*/\n/*  }*/\n/*}*/\n\n/*@media (max-width: 350px) {*/\n/*  .tip,*/\n/*  .actions {*/\n/*    display: none;*/\n/*  }*/\n/*}*/\n"
  },
  {
    "path": "src/renderer/src/components/AllowConnectionForDeviceAlert.tsx",
    "content": "import React from 'react';\nimport { Intent, Alert, H4 } from '@blueprintjs/core';\nimport DeviceInfoCallout from './DeviceInfoCallout';\nimport { Device } from '../../../common/Device';\nimport { useTranslation } from 'react-i18next';\n\ninterface AllowConnectionForDeviceAlertProps {\n\tdevice: Device | null;\n\tisOpen: boolean;\n\tonCancel: () => void;\n\tonConfirm: () => void;\n}\n\nconst AllowConnectionForDeviceAlert: React.FC<\n\tAllowConnectionForDeviceAlertProps\n> = (props) => {\n\tconst { device, isOpen, onCancel, onConfirm } = props;\n\tconst { t } = useTranslation();\n\tconst denyText = t('deny');\n\tconst allowText = t('allow');\n\n\treturn (\n\t\t<Alert\n\t\t\tclassName=\"class-allow-device-to-connect-alert rounded-pill-alert\"\n\t\t\tcancelButtonText={denyText}\n\t\t\tconfirmButtonText={allowText}\n\t\t\ticon=\"feed\"\n\t\t\tintent={Intent.DANGER}\n\t\t\tisOpen={isOpen}\n\t\t\tonCancel={onCancel}\n\t\t\tonConfirm={onConfirm}\n\t\t\ttransitionDuration={0}\n\t\t\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t\t\t// @ts-ignore\n\t\t\tusePortal={false}\n\t\t>\n\t\t\t<H4>{t('someone-is-trying-to-connect-do-you-allow')}</H4>\n\t\t\t<DeviceInfoCallout\n\t\t\t\tdeviceType={device?.deviceType}\n\t\t\t\tdeviceIP={device?.deviceIP}\n\t\t\t\tdeviceOS={device?.deviceOS}\n\t\t\t\tdeviceBrowser={device?.deviceBrowser}\n\t\t\t\tdeviceRoomId={device?.deviceRoomId}\n\t\t\t/>\n\t\t</Alert>\n\t);\n};\n\nexport default AllowConnectionForDeviceAlert;\n"
  },
  {
    "path": "src/renderer/src/components/CloseOverlayButton.tsx",
    "content": "import React from 'react';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport { Button, Icon } from '@blueprintjs/core';\n\nclass CloseOverlayButtonProps {\n\tonClick = () => {\n\t\t// noop default handler\n\t};\n\n\tstyle? = {};\n\n\tisDefaultStyles? = false;\n\n\tclassName? = '';\n}\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tcloseButton: {\n\t\t\tposition: 'relative',\n\t\t\twidth: '40px',\n\t\t\theight: '40px',\n\t\t\tleft: 'calc(100% - 55px)',\n\t\t\tborderRadius: '100px',\n\t\t\tzIndex: 9999,\n\t\t},\n\t}),\n);\n\nconst CloseOverlayButton: React.FC<CloseOverlayButtonProps> = (\n\tprops: CloseOverlayButtonProps,\n) => {\n\tconst { className, isDefaultStyles, style, onClick } = props;\n\tconst classes = useStyles();\n\treturn (\n\t\t<Button\n\t\t\tid=\"close-overlay-button\"\n\t\t\tclassName={isDefaultStyles ? `${classes.closeButton} ${className}` : ''}\n\t\t\tonClick={onClick}\n\t\t\tstyle={style}\n\t\t>\n\t\t\t<Icon icon=\"cross\" size={30} />\n\t\t</Button>\n\t);\n};\n\nexport default CloseOverlayButton;\n"
  },
  {
    "path": "src/renderer/src/components/ConnectedDevicesListDrawer.tsx",
    "content": "import { useEffect, useState, useCallback } from 'react';\nimport {\n\tButton,\n\tText,\n\tPosition,\n\tDrawer,\n\tCard,\n\tAlert,\n\tH4,\n\tDrawerSize,\n} from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport CloseOverlayButton from './CloseOverlayButton';\nimport DeviceInfoCallout from './DeviceInfoCallout';\nimport SharingSourcePreviewCard from './SharingSourcePreviewCard';\nimport { Device } from '../../../common/Device';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\nimport isProduction from '../../../common/isProduction';\nimport { useTranslation } from 'react-i18next';\n\ntype DeviceWithDesktopCapturerSourceId = Device & {\n\tdesktopCapturerSourceId: string;\n};\n\ninterface ConnectedDevicesListDrawerProps {\n\tisOpen: boolean;\n\thandleToggle: () => void;\n\thandleReset: () => void;\n}\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tdrawerRoot: { overflowY: 'scroll', overflowX: 'hidden' },\n\t\tdrawerInnerTopPanel: { padding: '20px 10px 0px 30px' },\n\t\tconnectedDevicesRoot: { padding: '10px 20px' },\n\t\ttopHeader: {\n\t\t\tmarginRight: '20px',\n\t\t\tfontSize: '20px',\n\t\t\tfontWeight: 900,\n\t\t},\n\t\tzoomFullWidth: {\n\t\t\twidth: '100%',\n\t\t},\n\t}),\n);\n\nexport default function ConnectedDevicesListDrawer(\n\tprops: ConnectedDevicesListDrawerProps,\n) {\n\tconst classes = useStyles();\n\tconst { t } = useTranslation();\n\n\tconst [isAlertDisconectAllOpen, setIsAlertDisconectAllOpen] = useState(false);\n\tconst [connectedDevices, setConnectedDevices] = useState<\n\t\tDeviceWithDesktopCapturerSourceId[]\n\t>([]);\n\tconst [devicesDisplayed, setDevicesDisplayed] = useState(new Map());\n\n\tuseEffect(() => {\n\t\tfunction getConnectedDevicesCallback() {\n\t\t\twindow.electron.ipcRenderer\n\t\t\t\t.invoke(IpcEvents.GetConnectedDevices)\n\t\t\t\t.then(async (devices: Device[]) => {\n\t\t\t\t\tconst devicesWithSourceIds: DeviceWithDesktopCapturerSourceId[] = [];\n\n\t\t\t\t\tfor await (const device of devices) {\n\t\t\t\t\t\tconst sharingSourceId = await window.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\tIpcEvents.GetDesktopCapturerSourceIdBySharingSessionId,\n\t\t\t\t\t\t\tdevice.sharingSessionID,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdevicesWithSourceIds.push({\n\t\t\t\t\t\t\t...device,\n\t\t\t\t\t\t\tdesktopCapturerSourceId: sharingSourceId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tsetConnectedDevices(devicesWithSourceIds);\n\n\t\t\t\t\tconst map = new Map();\n\t\t\t\t\tdevicesWithSourceIds.forEach((el) => {\n\t\t\t\t\t\tmap.set(el.id, true);\n\t\t\t\t\t});\n\t\t\t\t\tsetDevicesDisplayed(map);\n\t\t\t\t})\n\n\t\t\t\t.catch((e) => console.error(e));\n\t\t}\n\n\t\tgetConnectedDevicesCallback();\n\n\t\tconst connectedDevicesInterval = setInterval(\n\t\t\tgetConnectedDevicesCallback,\n\t\t\t4000,\n\t\t);\n\n\t\treturn () => {\n\t\t\tclearInterval(connectedDevicesInterval);\n\t\t};\n\t}, []);\n\n\tconst handleDisconnectOneDevice = useCallback(\n\t\tasync (id: string) => {\n\t\t\tconst device = connectedDevices.find((d: Device) => d.id === id);\n\t\t\tif (!device) return;\n\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.DisconnectPeerAndDestroySharingSessionBySessionID,\n\t\t\t\tdevice.sharingSessionID,\n\t\t\t);\n\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.DisconnectDeviceById,\n\t\t\t\tdevice.id,\n\t\t\t);\n\t\t\tsetConnectedDevices(connectedDevices.filter((d: Device) => d.id !== id));\n\t\t},\n\t\t[connectedDevices, setConnectedDevices],\n\t);\n\n\tconst handleDisconnectAll = useCallback(() => {\n\t\tconnectedDevices.forEach((device: Device) => {\n\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.DisconnectPeerAndDestroySharingSessionBySessionID,\n\t\t\t\tdevice.sharingSessionID,\n\t\t\t);\n\t\t});\n\t\twindow.electron.ipcRenderer.invoke(IpcEvents.DisconnectAllDevices);\n\t}, [connectedDevices]);\n\n\tconst hideOneDeviceInDevicesDisplayed = useCallback(\n\t\t(id) => {\n\t\t\tconst newDevicesDisplayed = new Map(devicesDisplayed);\n\t\t\tnewDevicesDisplayed.set(id, false);\n\t\t\tsetDevicesDisplayed(newDevicesDisplayed);\n\t\t\tnewDevicesDisplayed.delete(id);\n\t\t\tsetDevicesDisplayed(newDevicesDisplayed);\n\t\t\tsetConnectedDevices(\n\t\t\t\tconnectedDevices.filter((device) => device.id !== id),\n\t\t\t);\n\t\t},\n\t\t[\n\t\t\tconnectedDevices,\n\t\t\tsetConnectedDevices,\n\t\t\tdevicesDisplayed,\n\t\t\tsetDevicesDisplayed,\n\t\t],\n\t);\n\n\tconst hideAllDevicesInDevicesDisplayed = useCallback(() => {\n\t\tconst newDevicesDisplayed = new Map(devicesDisplayed);\n\t\t[...newDevicesDisplayed.keys()].forEach((key) => {\n\t\t\tnewDevicesDisplayed.set(key, false);\n\t\t});\n\t\tsetDevicesDisplayed(newDevicesDisplayed);\n\t}, [devicesDisplayed, setDevicesDisplayed]);\n\n\tconst handleDisconnectAndHideOneDevice = useCallback(\n\t\t(id) => {\n\t\t\thideOneDeviceInDevicesDisplayed(id);\n\t\t\thandleDisconnectOneDevice(id);\n\t\t},\n\t\t[handleDisconnectOneDevice, hideOneDeviceInDevicesDisplayed],\n\t);\n\n\tconst handleDisconnectAndHideAllDevices = useCallback(() => {\n\t\thideAllDevicesInDevicesDisplayed();\n\t\tsetTimeout(\n\t\t\t() => {\n\t\t\t\thandleDisconnectAll();\n\t\t\t\tprops.handleToggle();\n\t\t\t\tprops.handleReset();\n\t\t\t},\n\t\t\tisProduction() ? 1000 : 0,\n\t\t);\n\t}, [handleDisconnectAll, hideAllDevicesInDevicesDisplayed, props]);\n\n\tconst disconnectAllCancelButtonText = t('no-cancel');\n\tconst disconnectAllConfirmButtonText = t('yes-disconnect-all');\n\n\treturn (\n\t\t<>\n\t\t\t<Drawer\n\t\t\t\tclassName={classes.drawerRoot}\n\t\t\t\tposition={Position.BOTTOM}\n\t\t\t\tsize={DrawerSize.LARGE}\n\t\t\t\tisOpen={props.isOpen}\n\t\t\t\tonClose={props.handleToggle}\n\t\t\t\ttransitionDuration={0}\n\t\t\t>\n\t\t\t\t<Row between=\"xs\" middle=\"xs\" className={classes.drawerInnerTopPanel}>\n\t\t\t\t\t<Col xs={11}>\n\t\t\t\t\t\t<Row middle=\"xs\">\n\t\t\t\t\t\t\t<div className={classes.topHeader}>\n\t\t\t\t\t\t\t\t<Text className=\"bp3-text-muted\">{t('connected-devices')}</Text>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tintent=\"danger\"\n\t\t\t\t\t\t\t\tdisabled={connectedDevices.length === 0}\n\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\tsetIsAlertDisconectAllOpen(true);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\ticon=\"disable\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{t('disconnect-all-devices')}\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col xs={1}>\n\t\t\t\t\t\t<CloseOverlayButton onClick={props.handleToggle} isDefaultStyles />\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t\t<Row className={classes.connectedDevicesRoot}>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<div className={classes.zoomFullWidth}>\n\t\t\t\t\t\t\t{connectedDevices.map((device) => {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<div key={device.id}>\n\t\t\t\t\t\t\t\t\t\t<Card className=\"connected-device-card\">\n\t\t\t\t\t\t\t\t\t\t\t<Row middle=\"xs\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Col xs={6}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<DeviceInfoCallout\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdeviceType={device.deviceType}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdeviceOS={device.deviceOS}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdeviceIP={device.deviceIP}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdeviceBrowser={device.deviceBrowser}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdeviceRoomId={device.deviceRoomId}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t\t\t\t\t\t<Col xs={6}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SharingSourcePreviewCard\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsharingSourceID={device.desktopCapturerSourceId}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tid={`disconnect-device-${device.deviceIP}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tintent=\"danger\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(): void => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thandleDisconnectAndHideOneDevice(device.id);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\ticon=\"disable\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{t('disconnect')}\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Drawer>\n\t\t\t<Alert\n\t\t\t\tisOpen={isAlertDisconectAllOpen}\n\t\t\t\tonClose={() => {\n\t\t\t\t\tsetIsAlertDisconectAllOpen(false);\n\t\t\t\t}}\n\t\t\t\ticon=\"warning-sign\"\n\t\t\t\tcancelButtonText={disconnectAllCancelButtonText}\n\t\t\t\tconfirmButtonText={disconnectAllConfirmButtonText}\n\t\t\t\tintent=\"danger\"\n\t\t\t\tcanEscapeKeyCancel\n\t\t\t\tcanOutsideClickCancel\n\t\t\t\tonCancel={() => {\n\t\t\t\t\tsetIsAlertDisconectAllOpen(false);\n\t\t\t\t}}\n\t\t\t\tonConfirm={handleDisconnectAndHideAllDevices}\n\t\t\t\ttransitionDuration={0}\n\t\t\t>\n\t\t\t\t<H4>\n\t\t\t\t\t{t(\n\t\t\t\t\t\t'are-you-sure-you-want-to-disconnect-all-connected-viewing-devices',\n\t\t\t\t\t)}\n\t\t\t\t</H4>\n\t\t\t\t<Text>{t('this-step-can-not-be-undone')}</Text>\n\t\t\t\t<Text>{t('you-will-have-to-connect-all-devices-manually-again')}</Text>\n\t\t\t</Alert>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/DeviceInfoCallout/index.tsx",
    "content": "import React from 'react';\nimport { Callout, Text, H4, Tooltip, Position } from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport { TFunction } from 'i18next';\n\nfunction getContentOfTooltip(t: TFunction) {\n\treturn (\n\t\t<>\n\t\t\t<Text>\n\t\t\t\t{t(\n\t\t\t\t\t'this-should-match-with-device-ip-displayed-on-the-screen-of-device-that-is-trying-to-connect',\n\t\t\t\t)}\n\t\t\t</Text>\n\t\t\t<span style={{ fontWeight: 900 }}>\n\t\t\t\t<Text>{t('if-ip-addresses-dont-match-click-disconnect-button')}</Text>\n\t\t\t</span>\n\t\t</>\n\t);\n}\n\ninterface DeviceInfoCalloutProps {\n\tdeviceType: string | undefined;\n\tdeviceIP: string | undefined;\n\tdeviceOS: string | undefined;\n\tdeviceBrowser: string | undefined;\n\tdeviceRoomId: string | undefined;\n}\n\nconst DeviceInfoCallout: React.FC<DeviceInfoCalloutProps> = (props) => {\n\tconst { t } = useTranslation();\n\tconst { deviceType, deviceIP, deviceOS, deviceRoomId, deviceBrowser } = props;\n\n\treturn (\n\t\t<>\n\t\t\t<H4 style={{ margin: '0 auto', textAlign: 'center' }}>\n\t\t\t\t{t('partner-device-info')}\n\t\t\t</H4>\n\t\t\t<Callout id=\"device-info-callout\" style={{ borderRadius: '8px' }}>\n\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t{t('device-type')}: <span>{deviceType}</span>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t<Tooltip content={getContentOfTooltip(t)} position={Position.TOP}>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tfontWeight: 900,\n\t\t\t\t\t\t\t\t\tbackgroundColor: '#00f99273',\n\t\t\t\t\t\t\t\t\tpaddingLeft: '10px',\n\t\t\t\t\t\t\t\t\tpaddingRight: '10px',\n\t\t\t\t\t\t\t\t\tborderRadius: '20px',\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Text className=\"bp3-text-large\">\n\t\t\t\t\t\t\t\t\t{t('device-ip')}:{' '}\n\t\t\t\t\t\t\t\t\t<span className=\"device-ip-span\">{deviceIP}</span>\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t{t('device-browser')}: <span>{deviceBrowser}</span>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t{t('device-os')}: <span>{deviceOS}</span>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t{t('device-connection-id')}: <span>{deviceRoomId}</span>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Callout>\n\t\t</>\n\t);\n};\n\nexport default DeviceInfoCallout;\n"
  },
  {
    "path": "src/renderer/src/components/LanguageSelector/index.tsx",
    "content": "import React, { useContext, useEffect, useState } from 'react';\nimport { HTMLSelect } from '@blueprintjs/core';\nimport { SettingsContext } from '@renderer/contexts/SettingsContext';\nimport i18n_client, {\n\tgetLangFullNameToLangISOKeyMap,\n\tgetLangISOKeyToLangFullNameMap,\n} from '../../configs/i18next.config.client';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport config from '../../../../common/app.lang.config';\n\nexport default function LanguageSelector() {\n\tconst { setCurrentLanguageHook } = useContext(SettingsContext);\n\n\tconst [languagesList, setLanguagesList] = useState([] as string[]);\n\n\tuseEffect(() => {\n\t\tconst tmp: string[] = [];\n\t\tconst langISOKeyToLangFullNameMap = getLangISOKeyToLangFullNameMap();\n\t\tconfig.languages.forEach((langISOKey) => {\n\t\t\tconst langFullName = langISOKeyToLangFullNameMap.get(langISOKey);\n\t\t\tif (langFullName) {\n\t\t\t\ttmp.push(langFullName);\n\t\t\t}\n\t\t});\n\t\tsetLanguagesList(tmp);\n\t\tsetCurrentLanguageHook(i18n_client.language);\n\t}, [setCurrentLanguageHook]);\n\n\tconst onChangeLanguageHTMLSelectHandler = (\n\t\tevent: React.ChangeEvent<HTMLSelectElement>,\n\t) => {\n\t\tif (\n\t\t\tevent.currentTarget &&\n\t\t\tgetLangFullNameToLangISOKeyMap().has(event.currentTarget.value)\n\t\t) {\n\t\t\tconst newLang =\n\t\t\t\tgetLangFullNameToLangISOKeyMap().get(event.currentTarget.value) ||\n\t\t\t\t'English';\n\t\t\ti18n_client.changeLanguage(newLang);\n\t\t\twindow.electron.ipcRenderer.invoke(IpcEvents.AppLanguageChanged, newLang);\n\t\t}\n\t};\n\n\treturn (\n\t\t<HTMLSelect\n\t\t\tvalue={getLangISOKeyToLangFullNameMap().get(i18n_client.language)}\n\t\t\toptions={languagesList}\n\t\t\tonChange={onChangeLanguageHTMLSelectHandler}\n\t\t\tstyle={{\n\t\t\t\tborderRadius: '50px',\n\t\t\t\twidth: '120px',\n\t\t\t}}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/SettingsOverlay/SettingRowLabelAndInput.tsx",
    "content": "import React from 'react';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { Icon, Text } from '@blueprintjs/core';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\toneSettingRow: {\n\t\t\tcolor: '#5C7080 !important',\n\t\t\tfontSize: '18px',\n\t\t\tfontWeight: 900,\n\t\t\tdisplay: 'flex',\n\t\t\tflexDirection: 'row',\n\t\t\talignItems: 'center',\n\t\t},\n\t\tsettingRowIcon: {\n\t\t\tmargin: '10px',\n\t\t\tcolor: '#8A9BA8',\n\t\t},\n\t}),\n);\n\ninterface SettingRowLabelAndInput {\n\ticon: string;\n\tlabel: string;\n\tinput: React.ReactNode;\n}\n\nexport default function SettingRowLabelAndInput(\n\tprops: SettingRowLabelAndInput,\n) {\n\tconst { icon, label, input } = props;\n\tconst classes = useStyles();\n\n\treturn (\n\t\t<Row middle=\"xs\" between=\"xs\" style={{ display: 'flex', width: '100%' }}>\n\t\t\t<div style={{ flex: 8 }}>\n\t\t\t\t<div className={classes.oneSettingRow}>\n\t\t\t\t\t<Col>\n\t\t\t\t\t\t<Icon\n\t\t\t\t\t\t\t// @ts-ignore: ok here\n\t\t\t\t\t\t\ticon={icon}\n\t\t\t\t\t\t\tsize={25}\n\t\t\t\t\t\t\tclassName={classes.settingRowIcon}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col>\n\t\t\t\t\t\t<Text>{label}</Text>\n\t\t\t\t\t</Col>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div style={{ flex: 1 }}>\n\t\t\t\t<Row>{input}</Row>\n\t\t\t</div>\n\t\t</Row>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/SettingsOverlay/SettingsOverlay.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react';\nimport {\n\tOverlay2,\n\tClasses,\n\tH3,\n\tTabs,\n\tTab,\n\tIcon,\n\tText,\n\tTabsExpander,\n\tCallout,\n} from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport { LIGHT_UI_BACKGROUND } from '../../containers/SettingsProvider';\nimport CloseOverlayButton from '../CloseOverlayButton';\nimport SettingRowLabelAndInput from './SettingRowLabelAndInput';\nimport LanguageSelector from '../LanguageSelector';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport { useTranslation } from 'react-i18next';\nimport './settings-overlay.css';\n\ninterface SettingsOverlayProps {\n\tisSettingsOpen: boolean;\n\thandleClose: () => void;\n}\n\ntype SettingsOverlayClassKey =\n\t| 'checkboxSettings'\n\t| 'overlayInnerRoot'\n\t| 'overlayInsideFade'\n\t| 'absoluteCloseButton'\n\t| 'tabNavigationRowButton'\n\t| 'iconInTablLeftButton'\n\t| 'updateCalloutWrapper'\n\t| 'updateCallout';\n\ntype SettingsOverlayClassMap = Record<SettingsOverlayClassKey, string>;\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tcheckboxSettings: { margin: '0' },\n\t\toverlayInnerRoot: { width: '90%' },\n\t\toverlayInsideFade: {\n\t\t\theight: '90vh',\n\t\t\tbackgroundColor: LIGHT_UI_BACKGROUND,\n\t\t},\n\t\tabsoluteCloseButton: { position: 'absolute', left: 'calc(100% - 65px)' },\n\t\ttabNavigationRowButton: {\n\t\t\tfontWeight: 700,\n\t\t\tpadding: '6px 10px',\n\t\t\tborderRadius: '100px',\n\t\t},\n\t\ticonInTablLeftButton: { marginRight: '5px' },\n\t\tupdateCalloutWrapper: {\n\t\t\tdisplay: 'flex',\n\t\t\tjustifyContent: 'center',\n\t\t\tmarginBottom: '16px',\n\t\t\twidth: '100%',\n\t\t},\n\t\tupdateCallout: {\n\t\t\tcursor: 'pointer',\n\t\t\tboxShadow: 'none',\n\t\t\tdisplay: 'inline-flex',\n\t\t\tflexDirection: 'column',\n\t\t\tgap: '4px',\n\t\t\twidth: 'auto',\n\t\t\tmaxWidth: '420px',\n\t\t\tborderRadius: '8px',\n\t\t},\n\t}),\n);\n\nexport default function SettingsOverlay(\n\tprops: SettingsOverlayProps,\n): React.ReactElement {\n\tconst [clientViewerPort, setClientViewerPort] = useState('80'); // Default port, can be changed later\n\n\tconst { handleClose, isSettingsOpen } = props;\n\tconst [latestVersion, setLatestVersion] = useState('');\n\tconst [currentVersion, setCurrentVersion] = useState('');\n\n\tconst { t } = useTranslation();\n\n\tconst classes = useStyles() as SettingsOverlayClassMap;\n\n\tconst handleOpenDownload = useCallback((): void => {\n\t\tvoid window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.OpenExternalLink,\n\t\t\t'https://deskreen.com/download',\n\t\t);\n\t}, []);\n\n\tconst handleUpdateCalloutKeyDown = useCallback(\n\t\t(event: React.KeyboardEvent<HTMLDivElement>): void => {\n\t\t\tif (event.key === 'Enter' || event.key === ' ') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\thandleOpenDownload();\n\t\t\t}\n\t\t},\n\t\t[handleOpenDownload],\n\t);\n\n\tuseEffect(() => {\n\t\twindow.electron.ipcRenderer\n\t\t\t.invoke(IpcEvents.GetPort)\n\t\t\t.then((port) => {\n\t\t\t\treturn setClientViewerPort(port);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tconsole.error('Error getting port:', error);\n\t\t\t});\n\n\t\treturn () => {\n\t\t\twindow.electron.ipcRenderer.removeListener(\n\t\t\t\t'settings-overlay-close',\n\t\t\t\thandleClose,\n\t\t\t);\n\t\t};\n\t}, [handleClose]);\n\n\tuseEffect(() => {\n\t\tconst getLatestVersion = async (): Promise<void> => {\n\t\t\tconst gotLatestVersion =\n\t\t\t\tawait window.electron.ipcRenderer.invoke('get-latest-version');\n\t\t\tif (gotLatestVersion !== '') {\n\t\t\t\tsetLatestVersion(gotLatestVersion);\n\t\t\t}\n\t\t};\n\t\tgetLatestVersion();\n\t\tconst getCurrentVersion = async (): Promise<void> => {\n\t\t\tconst gotCurrentVersion = await window.electron.ipcRenderer.invoke(\n\t\t\t\t'get-current-version',\n\t\t\t);\n\t\t\tif (gotCurrentVersion !== '') {\n\t\t\t\tsetCurrentVersion(gotCurrentVersion);\n\t\t\t}\n\t\t};\n\t\tgetCurrentVersion();\n\t}, []);\n\n\tconst hasUpdate =\n\t\tlatestVersion !== '' &&\n\t\tcurrentVersion !== '' &&\n\t\tlatestVersion !== currentVersion;\n\n\tconst GeneralSettingsPanel: React.FC = () => {\n\t\treturn (\n\t\t\t<div style={{ width: '100%' }}>\n\t\t\t\t{hasUpdate ? (\n\t\t\t\t\t<div className={classes.updateCalloutWrapper}>\n\t\t\t\t\t\t<Callout\n\t\t\t\t\t\t\tclassName={classes.updateCallout}\n\t\t\t\t\t\t\ticon=\"automatic-updates\"\n\t\t\t\t\t\t\tintent=\"success\"\n\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t\tonClick={handleOpenDownload}\n\t\t\t\t\t\t\tonKeyDown={handleUpdateCalloutKeyDown}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Text style={{ fontWeight: 600 }}>\n\t\t\t\t\t\t\t\t{t('deskreen-ce-update-is-available')}\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t<Text>{`${t('your-current-version-is')} ${currentVersion}`}</Text>\n\t\t\t\t\t\t\t<Text>{`${t('click-to-download-new-updated-version')} ${latestVersion}`}</Text>\n\t\t\t\t\t\t</Callout>\n\t\t\t\t\t</div>\n\t\t\t\t) : null}\n\t\t\t\t<Row middle=\"xs\">\n\t\t\t\t\t<H3 className=\"bp3-text-muted\">{t('general-settings')}</H3>\n\t\t\t\t</Row>\n\n\t\t\t\t{/*<SettingRowLabelAndInput*/}\n\t\t\t\t{/*  icon=\"style\"*/}\n\t\t\t\t{/*  label={t('color-theme')}*/}\n\t\t\t\t{/*  input={<ToggleThemeBtnGroup />}*/}\n\t\t\t\t{/*/>*/}\n\t\t\t\t<div style={{ marginTop: '24px' }}>\n\t\t\t\t\t<SettingRowLabelAndInput\n\t\t\t\t\t\ticon=\"translate\"\n\t\t\t\t\t\tlabel={t('language')}\n\t\t\t\t\t\tinput={<LanguageSelector />}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t<Row\n\t\t\t\t\tcenter=\"xs\"\n\t\t\t\t\tmiddle=\"xs\"\n\t\t\t\t\tstyle={{ marginTop: '40px', width: '100%' }}\n\t\t\t\t>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={`http://127.0.0.1:${clientViewerPort}/logo512.png`}\n\t\t\t\t\t\t\t\talt=\"logo\"\n\t\t\t\t\t\t\t\tstyle={{ width: '100px' }}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t<H3>{t('about-deskreen')}</H3>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t<Text>{`${t('version')}: ${currentVersion} (${currentVersion})`}</Text>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t\t{`${t('copyright')} © ${new Date().getFullYear()} `}\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"https://www.linkedin.com/in/pavlobu/\"\n\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\tclassName=\"bp3-link\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tcolor: '#106ba3',\n\t\t\t\t\t\t\t\t\t\ttextDecoration: 'none',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tPavlo Buidenkov\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t\t{`${t('website')}: `}\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"https://www.deskreen.com\"\n\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\tclassName=\"bp3-link\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tcolor: '#106ba3',\n\t\t\t\t\t\t\t\t\t\ttextDecoration: 'none',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\thttps://www.deskreen.com\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t</Col>\n\t\t\t\t\t</div>\n\t\t\t\t</Row>\n\t\t\t</div>\n\t\t);\n\t};\n\n\tconst getTabNavGeneralSettingsButton = (): React.ReactElement => {\n\t\treturn (\n\t\t\t<Row middle=\"xs\" className={classes.tabNavigationRowButton}>\n\t\t\t\t<Icon icon=\"wrench\" className={classes.iconInTablLeftButton} />\n\t\t\t\t<Text className=\"bp3-text-large\">{t('general')}</Text>\n\t\t\t</Row>\n\t\t);\n\t};\n\n\treturn (\n\t\t<Overlay2\n\t\t\tonClose={handleClose}\n\t\t\tclassName={`${Classes.OVERLAY_SCROLL_CONTAINER} bp3-overlay-settings`}\n\t\t\tautoFocus\n\t\t\tcanEscapeKeyClose\n\t\t\tcanOutsideClickClose\n\t\t\tenforceFocus\n\t\t\thasBackdrop\n\t\t\tisOpen={isSettingsOpen}\n\t\t\tusePortal\n\t\t\ttransitionDuration={0}\n\t\t>\n\t\t\t<div className={classes.overlayInnerRoot}>\n\t\t\t\t<div\n\t\t\t\t\tid=\"settings-overlay-inner\"\n\t\t\t\t\tclassName={`${classes.overlayInsideFade} ${Classes.CARD}`}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tborderRadius: '8px',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CloseOverlayButton\n\t\t\t\t\t\tclassName={classes.absoluteCloseButton}\n\t\t\t\t\t\tonClick={handleClose}\n\t\t\t\t\t\tisDefaultStyles\n\t\t\t\t\t/>\n\t\t\t\t\t<Tabs\n\t\t\t\t\t\tanimate\n\t\t\t\t\t\tid=\"TabsExample\"\n\t\t\t\t\t\tkey=\"vertical\"\n\t\t\t\t\t\trenderActiveTabPanelOnly\n\t\t\t\t\t\tvertical\n\t\t\t\t\t>\n\t\t\t\t\t\t<Tab\n\t\t\t\t\t\t\tid=\"rx\"\n\t\t\t\t\t\t\ttitle=\"\"\n\t\t\t\t\t\t\tpanel={<GeneralSettingsPanel />}\n\t\t\t\t\t\t\tpanelClassName={'tab-panel-wide-custom-style'}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{getTabNavGeneralSettingsButton()}\n\t\t\t\t\t\t</Tab>\n\t\t\t\t\t\t<TabsExpander />\n\t\t\t\t\t</Tabs>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Overlay2>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/SettingsOverlay/settings-overlay.css",
    "content": ".tab-panel-wide-custom-style {\n\twidth: 100%;\n}\n"
  },
  {
    "path": "src/renderer/src/components/ShareAppOrScreenControlGroup.tsx",
    "content": "import { useState, useCallback, useEffect } from 'react';\nimport { Button, Icon, ControlGroup, Text } from '@blueprintjs/core';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport ChooseAppOrScreenOverlay from './StepsOfStepper/ChooseAppOrScreenOverlay/ChooseAppOrScreenOverlay';\nimport { useTranslation } from 'react-i18next';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\n\ninterface ShareAppOrScreenControlGroupProps {\n\thandleNextEntireScreen: () => void;\n\thandleNextApplicationWindow: () => void;\n}\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tcontrolGroupRoot: {\n\t\t\twidth: '500px',\n\t\t\tdisplay: 'flex',\n\t\t\tposition: 'relative',\n\t\t\tleft: '20px',\n\t\t},\n\t\tshareEntireScreenButton: {\n\t\t\theight: '180px',\n\t\t\twidth: '50%',\n\t\t\tcolor: 'white',\n\t\t\tfontSize: '20px',\n\t\t\tborderRadius: '20px 0px 0px 20px !important',\n\t\t\ttextAlign: 'center',\n\t\t},\n\t\tshareEntireScreenButtonIcon: { marginBottom: '20px' },\n\t\tshareAppButton: {\n\t\t\theight: '180px',\n\t\t\twidth: '50%',\n\t\t\tborderRadius: '0px 20px 20px 0px !important',\n\t\t\tcolor: 'white',\n\t\t\tfontSize: '20px',\n\t\t\ttextAlign: 'center',\n\t\t\tbackgroundColor: '#48AFF0 !important',\n\t\t\t'&:hover': {\n\t\t\t\tbackgroundColor: '#4097ce !important',\n\t\t\t},\n\t\t},\n\t\tshareAppButtonIcon: { marginBottom: '20px' },\n\t\torDecorationButton: {\n\t\t\theight: '38px',\n\t\t\twidth: '40px',\n\t\t\tborderRadius: '100px !important',\n\t\t\tposition: 'relative',\n\t\t\ttop: '70px',\n\t\t\tleft: '-190px !important',\n\t\t\tcursor: 'default',\n\t\t},\n\t}),\n);\n\nexport default function ShareAppOrScreenControlGroup(\n\tprops: ShareAppOrScreenControlGroupProps,\n) {\n\tconst { handleNextEntireScreen, handleNextApplicationWindow } = props;\n\tconst classes = useStyles();\n\tconst { t } = useTranslation();\n\n\tconst [isChooseAppOrScreenOverlayOpen, setChooseAppOrScreenOverlayOpen] =\n\t\tuseState(false);\n\n\tconst [isEntireScreenToShareChosen, setEntireScreenToShareChosen] =\n\t\tuseState(false);\n\n\tconst [isWaylandSession, setIsWaylandSession] = useState(false);\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\n\t\twindow.electron.ipcRenderer\n\t\t\t.invoke(IpcEvents.GetIsLinuxWaylandSession)\n\t\t\t.then((value: boolean) => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetIsWaylandSession(Boolean(value));\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tconsole.error('failed to detect session environment', error);\n\t\t\t});\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t};\n\t}, []);\n\n\tconst handleOpenChooseAppOrScreenOverlay = useCallback(() => {\n\t\tsetChooseAppOrScreenOverlayOpen(true);\n\t}, []);\n\n\tconst handleCloseChooseAppOrScreenOverlay = useCallback(() => {\n\t\tsetChooseAppOrScreenOverlayOpen(false);\n\t}, []);\n\n\tconst handleWaylandShare = useCallback(\n\t\tasync (mode: 'screen' | 'window') => {\n\t\t\ttry {\n\t\t\t\tconst sourceId: string | null =\n\t\t\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\t\t\tIpcEvents.RequestDesktopCapturerPortalSource,\n\t\t\t\t\t\t{ mode },\n\t\t\t\t\t);\n\t\t\t\tif (!sourceId) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\t\tIpcEvents.SetDesktopCapturerSourceId,\n\t\t\t\t\tsourceId,\n\t\t\t\t);\n\t\t\t\tif (mode === 'screen') {\n\t\t\t\t\thandleNextEntireScreen();\n\t\t\t\t} else {\n\t\t\t\t\thandleNextApplicationWindow();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t'failed to acquire desktop capture source via portal',\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\t[handleNextApplicationWindow, handleNextEntireScreen],\n\t);\n\n\tconst handleChooseAppOverlayOpen = useCallback(() => {\n\t\tif (isWaylandSession) {\n\t\t\tvoid handleWaylandShare('window');\n\t\t\treturn;\n\t\t}\n\t\tsetEntireScreenToShareChosen(false);\n\t\thandleOpenChooseAppOrScreenOverlay();\n\t}, [\n\t\thandleOpenChooseAppOrScreenOverlay,\n\t\thandleWaylandShare,\n\t\tisWaylandSession,\n\t]);\n\n\tconst handleChooseEntireScreenOverlayOpen = useCallback(() => {\n\t\tif (isWaylandSession) {\n\t\t\tvoid handleWaylandShare('screen');\n\t\t\treturn;\n\t\t}\n\t\tsetEntireScreenToShareChosen(true);\n\t\thandleOpenChooseAppOrScreenOverlay();\n\t}, [\n\t\thandleOpenChooseAppOrScreenOverlay,\n\t\thandleWaylandShare,\n\t\tisWaylandSession,\n\t]);\n\n\treturn (\n\t\t<>\n\t\t\t<ControlGroup\n\t\t\t\tid=\"share-screen-or-app-btn-group\"\n\t\t\t\tclassName={classes.controlGroupRoot}\n\t\t\t\tfill\n\t\t\t\tvertical={false}\n\t\t\t\tstyle={{ width: '380px' }}\n\t\t\t>\n\t\t\t\t<Button\n\t\t\t\t\tclassName={classes.shareEntireScreenButton}\n\t\t\t\t\tintent=\"primary\"\n\t\t\t\t\tonClick={handleChooseEntireScreenOverlayOpen}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.shareEntireScreenButtonIcon}\n\t\t\t\t\t\ticon=\"desktop\"\n\t\t\t\t\t\tsize={100}\n\t\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Text className=\"bp3-running-text\">{t('entire-screen')}</Text>\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tclassName={classes.shareAppButton}\n\t\t\t\t\tintent=\"primary\"\n\t\t\t\t\tonClick={handleChooseAppOverlayOpen}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.shareAppButtonIcon}\n\t\t\t\t\t\ticon=\"application\"\n\t\t\t\t\t\tsize={100}\n\t\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Text className=\"bp3-running-text\">{t('application-window')}</Text>\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tactive\n\t\t\t\t\tclassName={classes.orDecorationButton}\n\t\t\t\t\tstyle={{ zIndex: 999 }}\n\t\t\t\t>\n\t\t\t\t\t{t('or')}\n\t\t\t\t</Button>\n\t\t\t</ControlGroup>\n\t\t\t<ChooseAppOrScreenOverlay\n\t\t\t\tisEntireScreenToShareChosen={isEntireScreenToShareChosen}\n\t\t\t\tisChooseAppOrScreenOverlayOpen={isChooseAppOrScreenOverlayOpen}\n\t\t\t\thandleClose={handleCloseChooseAppOrScreenOverlay}\n\t\t\t\thandleNextEntireScreen={handleNextEntireScreen}\n\t\t\t\thandleNextApplicationWindow={handleNextApplicationWindow}\n\t\t\t\tisWaylandSession={isWaylandSession}\n\t\t\t/>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/SharingSourcePreviewCard/index.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { Text, Card, Spinner } from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport { useTranslation } from 'react-i18next';\n\nclass SharingSourcePreviewCardProps {\n\tsharingSourceID: string | undefined = '';\n\n\tonClickCard? = (): void => {\n\t\t// noop default handler\n\t};\n\n\tisChangeAppearanceOnHover? = false;\n}\n\nconst SharingSourcePreviewCard: React.FC<SharingSourcePreviewCardProps> = (\n\tprops,\n) => {\n\tconst { isChangeAppearanceOnHover, onClickCard, sharingSourceID } = props;\n\tconst [sourceImage, setSourceImage] = useState('');\n\tconst [sourceName, setSourceName] = useState('');\n\tconst [appIconSourceImage, setAppIconSourceImage] = useState('');\n\tconst [isHovered, setIsHovered] = useState(false);\n\tconst { t } = useTranslation();\n\tconst rootRef = useRef<HTMLDivElement | null>(null);\n\tconst [isVisible, setIsVisible] = useState(false);\n\n\tuseEffect(() => {\n\t\tif (!rootRef.current) return;\n\t\tconst observer = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\tentries.forEach((entry) => {\n\t\t\t\t\tif (entry.isIntersecting) {\n\t\t\t\t\t\tsetIsVisible(true);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ root: null, threshold: 0.1 },\n\t\t);\n\t\tobserver.observe(rootRef.current);\n\t\treturn () => observer.disconnect();\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (!isVisible) return;\n\t\tconst timer = setTimeout(async () => {\n\t\t\tif (!sharingSourceID) return;\n\t\t\tconst sources = await window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.GetDesktopCapturerServiceSourcesByIds,\n\t\t\t\t[sharingSourceID],\n\t\t\t);\n\n\t\t\tconst data = sources?.[sharingSourceID];\n\t\t\tif (data) {\n\t\t\t\tsetSourceImage((data?.source.thumbnail as unknown as string) || '');\n\t\t\t\tif (data?.source.appIcon != null) {\n\t\t\t\t\tsetAppIconSourceImage(\n\t\t\t\t\t\t(data?.source.appIcon as unknown as string) || '',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsetSourceName(data?.source.name || t('failed-to-get-source-name'));\n\t\t\t}\n\t\t}, 200);\n\n\t\treturn () => clearTimeout(timer);\n\t}, [isVisible, sharingSourceID]);\n\n\treturn (\n\t\t<div ref={rootRef}>\n\t\t\t<Card\n\t\t\t\tclassName=\"preview-share-thumb-container\"\n\t\t\t\tonClick={onClickCard ? () => onClickCard() : undefined}\n\t\t\t\tstyle={{\n\t\t\t\t\theight: '200px',\n\t\t\t\t\tminWidth: '250px',\n\t\t\t\t\tbackgroundColor:\n\t\t\t\t\t\tisHovered && isChangeAppearanceOnHover\n\t\t\t\t\t\t\t? '#2B95D6'\n\t\t\t\t\t\t\t: 'rgba(0,0,0,0.0)',\n\t\t\t\t}}\n\t\t\t\tonMouseEnter={() => setIsHovered(true)}\n\t\t\t\tonMouseOver={() => setIsHovered(true)}\n\t\t\t\tonMouseLeave={() => setIsHovered(false)}\n\t\t\t>\n\t\t\t\t<Row\n\t\t\t\t\tcenter=\"xs\"\n\t\t\t\t\tmiddle=\"xs\"\n\t\t\t\t\tstyle={{ height: '95%', minWidth: '200px' }}\n\t\t\t\t>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t{sourceImage !== '' ? (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tsrc={sourceImage}\n\t\t\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\t\t\tstyle={{ height: '143px', maxWidth: '100%' }}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{appIconSourceImage !== '' ? (\n\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\t\t\t\t\twidth: '40px',\n\t\t\t\t\t\t\t\t\t\t\theight: '40px',\n\t\t\t\t\t\t\t\t\t\t\ttransform: 'translate(0px, -45px)',\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: '500px',\n\t\t\t\t\t\t\t\t\t\t\tpadding: '0px',\n\t\t\t\t\t\t\t\t\t\t\tmargin: '0px',\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\televation={4}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Row center=\"xs\" middle=\"xs\" style={{ height: '100%' }}>\n\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\tsrc={appIconSourceImage}\n\t\t\t\t\t\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\twidth: '25px',\n\t\t\t\t\t\t\t\t\t\t\t\t\theight: '25px',\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<> </>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<Spinner size={60} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t<Col\n\t\t\t\t\t\txs={12}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tbackgroundColor:\n\t\t\t\t\t\t\t\tisHovered && isChangeAppearanceOnHover\n\t\t\t\t\t\t\t\t\t? 'rgba(0,0,0,0.8)'\n\t\t\t\t\t\t\t\t\t: 'rgba(0,0,0,0.45)',\n\t\t\t\t\t\t\tcolor: 'white',\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text ellipsize>{sourceName}</Text>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Card>\n\t\t</div>\n\t);\n};\n\nexport default SharingSourcePreviewCard;\n"
  },
  {
    "path": "src/renderer/src/components/StepperPanel/ColorlibConnector.tsx",
    "content": "import { withStyles } from '@material-ui/core/styles';\nimport StepConnector from '@material-ui/core/StepConnector';\n\nconst ColorlibConnector = withStyles({\n\talternativeLabel: {\n\t\ttop: 43,\n\t},\n\tactive: {\n\t\t'& $line': {\n\t\t\tbackgroundImage:\n\t\t\t\t'linear-gradient( 95deg, #3DCC91 0%, #15B371 50%, #FFB366 100%)',\n\t\t},\n\t},\n\tcompleted: {\n\t\t'& $line': {\n\t\t\tbackgroundImage:\n\t\t\t\t'linear-gradient( 95deg, #3DCC91 0%, #15B371 50%, #3DCC91 100%)',\n\t\t},\n\t},\n\tline: {\n\t\theight: 2,\n\t\tborder: 0,\n\t\tbackgroundColor: '#CED9E0',\n\t\tborderRadius: 1,\n\t},\n})(StepConnector);\n\nexport default ColorlibConnector;\n"
  },
  {
    "path": "src/renderer/src/components/StepperPanel/ColorlibStepIcon.tsx",
    "content": "import React, { ReactNode } from 'react';\nimport clsx from 'clsx';\nimport { makeStyles } from '@material-ui/core/styles';\nimport { StepIconProps } from '@material-ui/core/StepIcon';\nimport { Icon } from '@blueprintjs/core';\n\nexport interface StepIconPropsDeskreen extends StepIconProps {\n\tisEntireScreenSelected: boolean;\n\tisApplicationWindowSelected: boolean;\n}\n\nconst useColorlibStepIconStyles = makeStyles({\n\troot: {\n\t\tbackgroundColor: '#BFCCD6',\n\t\tzIndex: 1,\n\t\tcolor: '#5C7080',\n\t\twidth: 65,\n\t\theight: 65,\n\t\tdisplay: 'flex',\n\t\tborderRadius: '50%',\n\t\tjustifyContent: 'center',\n\t\talignItems: 'center',\n\t},\n\tactive: {\n\t\tbackgroundImage:\n\t\t\t'linear-gradient( 136deg, #FFB366 0%, #F29D49 50%, #A66321 100%)',\n\t\tboxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',\n\t},\n\tcompleted: {\n\t\tbackgroundImage:\n\t\t\t'linear-gradient( 136deg, #3DCC91 0%, #15B371 50%, #0E5A8A 100%)',\n\t},\n\tstepContent: {},\n});\n\nconst getDesktopOrAppIcon = (isDesktop: boolean, color: string): ReactNode => {\n\tif (isDesktop) {\n\t\treturn <Icon icon=\"desktop\" size={25} color={color} />;\n\t}\n\treturn <Icon icon=\"application\" size={25} color={color} />;\n};\n\nexport default function ColorlibStepIcon(\n\tprops: StepIconPropsDeskreen,\n): ReactNode {\n\tconst { icon } = props;\n\tconst classes = useColorlibStepIconStyles();\n\tconst { active, completed, isEntireScreenSelected } = props;\n\n\tconst color = active || completed ? '#fff' : '#5C7080';\n\n\tconst icons: { [index: string]: React.ReactNode } = {\n\t\t1: completed ? (\n\t\t\t<Icon icon=\"feed-subscribed\" size={25} color={color} />\n\t\t) : (\n\t\t\t<Icon icon=\"feed\" size={25} color={color} />\n\t\t),\n\t\t2: completed ? (\n\t\t\tgetDesktopOrAppIcon(isEntireScreenSelected, color)\n\t\t) : (\n\t\t\t<Icon icon=\"flow-branch\" size={25} color={color} />\n\t\t),\n\t\t3: completed ? (\n\t\t\t<Icon icon=\"tick-circle\" size={25} color={color} />\n\t\t) : (\n\t\t\t<Icon icon=\"confirm\" size={25} color={color} />\n\t\t),\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tclassName={`${clsx(classes.root, {\n\t\t\t\t[classes.active]: active,\n\t\t\t\t[classes.completed]: completed,\n\t\t\t})}`}\n\t\t>\n\t\t\t{icons[String(icon)]}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepperPanel/DeviceConnectedInfoButton.tsx",
    "content": "import { Row, Col } from 'react-flexbox-grid';\nimport { Icon, Text, Button, Popover, Tooltip } from '@blueprintjs/core';\nimport DeviceInfoCallout from '../DeviceInfoCallout';\nimport { Device } from '../../../../common/Device';\nimport { useTranslation } from 'react-i18next';\nimport { TFunction } from 'i18next';\n\ninterface DeviceConnectedInfoButtonProps {\n\tdevice: Device;\n\tonDisconnect: () => void;\n}\n\nconst getDeviceConnectedPopoverContent = (\n\tpendingConnectionDevice: Device,\n\thandleDisconnect: () => void,\n\tt: TFunction,\n) => {\n\tconst disconnectButtonText = t('disconnect');\n\n\treturn (\n\t\t<Row>\n\t\t\t<div style={{ padding: '20px', borderRadius: '100px' }}>\n\t\t\t\t<Row style={{ margin: '0 px 10px 10px 10px' }}>\n\t\t\t\t\t<DeviceInfoCallout\n\t\t\t\t\t\tdeviceType={pendingConnectionDevice?.deviceType}\n\t\t\t\t\t\tdeviceIP={pendingConnectionDevice?.deviceIP}\n\t\t\t\t\t\tdeviceOS={pendingConnectionDevice?.deviceOS}\n\t\t\t\t\t\tdeviceBrowser={pendingConnectionDevice?.deviceBrowser}\n\t\t\t\t\t\tdeviceRoomId={pendingConnectionDevice?.deviceRoomId}\n\t\t\t\t\t/>\n\t\t\t\t</Row>\n\t\t\t\t<Row>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tintent=\"danger\"\n\t\t\t\t\t\t\ticon=\"disable\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\thandleDisconnect();\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tstyle={{ width: '100%', borderRadius: '100px' }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{disconnectButtonText}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</div>\n\t\t</Row>\n\t);\n};\n\nexport default function DeviceConnectedInfoButton(\n\tprops: DeviceConnectedInfoButtonProps,\n) {\n\tconst { device, onDisconnect } = props;\n\tconst { t } = useTranslation();\n\n\treturn (\n\t\t<>\n\t\t\t<Popover\n\t\t\t\tcontent={getDeviceConnectedPopoverContent(device, onDisconnect, t)}\n\t\t\t\tposition=\"bottom\"\n\t\t\t\ttransitionDuration={0}\n\t\t\t>\n\t\t\t\t<Tooltip\n\t\t\t\t\tcontent={<Text>Click to see more</Text>}\n\t\t\t\t\tposition=\"right\"\n\t\t\t\t\thoverOpenDelay={400}\n\t\t\t\t>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tid=\"connected-device-info-stepper-button\"\n\t\t\t\t\t\tintent=\"success\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: '150px',\n\t\t\t\t\t\t\theight: '10px !important',\n\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\t\tmargin: '0 auto',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Row>\n\t\t\t\t\t\t\t<Col xs={1}>\n\t\t\t\t\t\t\t\t<Icon icon=\"info-sign\" />\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t<Col xs>\n\t\t\t\t\t\t\t\t<Text>{t('connected')}</Text>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Button>\n\t\t\t\t</Tooltip>\n\t\t\t</Popover>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ChooseAppOrScreenOverlay/ChooseAppOrScreenOverlay.tsx",
    "content": "import { useCallback, useEffect, useState } from 'react';\nimport { H3, Dialog, Button, Spinner } from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport CloseOverlayButton from '../../CloseOverlayButton';\nimport PreviewGridList from './PreviewGridList';\nimport { IpcEvents } from '../../../../../common/IpcEvents.enum';\nimport { useTranslation } from 'react-i18next';\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tdialogRoot: {\n\t\t\twidth: '90%',\n\t\t\theight: '87vh !important',\n\t\t\toverflowY: 'scroll',\n\t\t},\n\t\tcloseButton: {\n\t\t\tposition: 'relative',\n\t\t\twidth: '40px',\n\t\t\theight: '40px',\n\t\t\tleft: 'calc(100% - 55px)',\n\t\t\tborderRadius: '100px',\n\t\t\tzIndex: 9999,\n\t\t},\n\t\toverlayInnerRoot: { width: '90%', height: '90%' },\n\t\tsharePreviewsContainer: {\n\t\t\ttop: '60px',\n\t\t\tposition: 'relative',\n\t\t\theight: '100%',\n\t\t},\n\t}),\n);\n\ninterface ChooseAppOrScreenOverlayProps {\n\tisEntireScreenToShareChosen: boolean;\n\tisChooseAppOrScreenOverlayOpen: boolean;\n\thandleNextEntireScreen: () => void;\n\thandleNextApplicationWindow: () => void;\n\thandleClose: () => void;\n\tisWaylandSession: boolean;\n}\n\nexport default function ChooseAppOrScreenOverlay(\n\tprops: ChooseAppOrScreenOverlayProps,\n) {\n\tconst {\n\t\thandleClose,\n\t\tisChooseAppOrScreenOverlayOpen,\n\t\tisEntireScreenToShareChosen,\n\t\thandleNextEntireScreen,\n\t\thandleNextApplicationWindow,\n\t\tisWaylandSession,\n\t} = props;\n\tconst classes = useStyles();\n\tconst { t } = useTranslation();\n\n\tconst [viewSharingIds, setViewSharingIds] = useState<string[]>([]);\n\tconst [isLoading, setIsLoading] = useState<boolean>(false);\n\n\tconst handleRefreshSources = useCallback(async (): Promise<string[]> => {\n\t\tif (isWaylandSession) {\n\t\t\tsetViewSharingIds([]);\n\t\t\treturn [];\n\t\t}\n\t\tconst ids = await window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.GetDesktopSharingSourceIds,\n\t\t\t{\n\t\t\t\tisEntireScreenToShareChosen,\n\t\t\t},\n\t\t);\n\t\tsetViewSharingIds(ids);\n\t\treturn ids;\n\t}, [isEntireScreenToShareChosen, isWaylandSession]);\n\n\tconst handleRefreshSourcesWithLoading = useCallback(async (): Promise<\n\t\tstring[]\n\t> => {\n\t\tsetIsLoading(true);\n\t\ttry {\n\t\t\tconst ids = await handleRefreshSources();\n\t\t\treturn ids;\n\t\t} finally {\n\t\t\tsetIsLoading(false);\n\t\t}\n\t}, [handleRefreshSources]);\n\n\tuseEffect(() => {\n\t\tif (!isChooseAppOrScreenOverlayOpen || isWaylandSession) {\n\t\t\tsetIsLoading(false);\n\t\t\tsetViewSharingIds([]);\n\t\t\treturn;\n\t\t}\n\n\t\tlet cancelled = false;\n\t\tlet attempts = 0;\n\t\tconst maxAttempts = 8; // ~3.2s total if retryDelayMs = 400\n\t\tconst retryDelayMs = 400;\n\n\t\tsetIsLoading(true);\n\n\t\tconst attemptLoad = async () => {\n\t\t\tconst ids = await handleRefreshSources();\n\t\t\tif (cancelled) return;\n\t\t\tif (ids.length > 0 || attempts >= maxAttempts) {\n\t\t\t\tsetIsLoading(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tattempts += 1;\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tattemptLoad();\n\t\t\t\t}\n\t\t\t}, retryDelayMs);\n\t\t};\n\n\t\tattemptLoad();\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tsetIsLoading(false);\n\t\t};\n\t}, [isChooseAppOrScreenOverlayOpen, handleRefreshSources, isWaylandSession]);\n\n\treturn (\n\t\t<Dialog\n\t\t\tonClose={handleClose}\n\t\t\tclassName={`${classes.dialogRoot} choose-app-or-screen-dialog`}\n\t\t\tautoFocus\n\t\t\tcanEscapeKeyClose\n\t\t\tcanOutsideClickClose\n\t\t\tenforceFocus\n\t\t\tisOpen={isChooseAppOrScreenOverlayOpen}\n\t\t\tusePortal\n\t\t\ttransitionDuration={0}\n\t\t\tstyle={{\n\t\t\t\tborderRadius: '8px',\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tid=\"choose-app-or-screen-overlay-container\"\n\t\t\t\tstyle={{ minHeight: '95%', overflowX: 'hidden' }}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: 'fixed',\n\t\t\t\t\t\tzIndex: 99999,\n\t\t\t\t\t\twidth: '90%',\n\t\t\t\t\t\tpaddingTop: '0px',\n\t\t\t\t\t\tpaddingLeft: '15px',\n\t\t\t\t\t\tpaddingRight: '15px',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tpadding: '10px',\n\t\t\t\t\t\t\tborderRadius: '5px',\n\t\t\t\t\t\t\theight: '60px',\n\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Row\n\t\t\t\t\t\t\tbetween=\"xs\"\n\t\t\t\t\t\t\tmiddle=\"xs\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\tbackgroundColor: '#f6f7f9',\n\t\t\t\t\t\t\t\tborderRadius: '8px',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Col xs={9}>\n\t\t\t\t\t\t\t\t{isEntireScreenToShareChosen ? (\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<H3 style={{ marginBottom: '0px' }}>\n\t\t\t\t\t\t\t\t\t\t\t{t('select-entire-screen-to-share')}\n\t\t\t\t\t\t\t\t\t\t</H3>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<H3 style={{ marginBottom: '0px' }}>\n\t\t\t\t\t\t\t\t\t\t\t{t('select-app-window-to-share')}\n\t\t\t\t\t\t\t\t\t\t</H3>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t<Col xs={2}>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ticon=\"refresh\"\n\t\t\t\t\t\t\t\t\tintent=\"warning\"\n\t\t\t\t\t\t\t\t\tonClick={handleRefreshSourcesWithLoading}\n\t\t\t\t\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\t\t\twidth: 'max-content',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{t('refresh')}\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Col>\n\n\t\t\t\t\t\t\t<Col xs={1}>\n\t\t\t\t\t\t\t\t<CloseOverlayButton\n\t\t\t\t\t\t\t\t\tonClick={handleClose}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\t\t\twidth: '40px',\n\t\t\t\t\t\t\t\t\t\theight: '40px',\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\tzIndex: '1',\n\t\t\t\t\t\theight: 'calc(87vh - 80px)',\n\t\t\t\t\t\tminHeight: '400px',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Spinner size={60} />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Row>\n\t\t\t\t\t\t\t\t<div className={classes.sharePreviewsContainer}>\n\t\t\t\t\t\t\t\t\t<PreviewGridList\n\t\t\t\t\t\t\t\t\t\tviewSharingIds={viewSharingIds}\n\t\t\t\t\t\t\t\t\t\tisEntireScreen={isEntireScreenToShareChosen}\n\t\t\t\t\t\t\t\t\t\thandleNextEntireScreen={() => {\n\t\t\t\t\t\t\t\t\t\t\thandleNextEntireScreen();\n\t\t\t\t\t\t\t\t\t\t\thandleClose();\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\thandleNextApplicationWindow={() => {\n\t\t\t\t\t\t\t\t\t\t\thandleNextApplicationWindow();\n\t\t\t\t\t\t\t\t\t\t\thandleClose();\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Dialog>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ChooseAppOrScreenOverlay/PreviewGridList.tsx",
    "content": "import { Row, Col } from 'react-flexbox-grid';\nimport SharingSourcePreviewCard from '../../SharingSourcePreviewCard';\nimport { IpcEvents } from '../../../../../common/IpcEvents.enum';\n\ninterface PreviewGridListProps {\n\tviewSharingIds: string[];\n\tisEntireScreen: boolean;\n\thandleNextEntireScreen: () => void;\n\thandleNextApplicationWindow: () => void;\n}\n\nexport default function PreviewGridList(props: PreviewGridListProps) {\n\tconst {\n\t\tviewSharingIds,\n\t\tisEntireScreen,\n\t\thandleNextEntireScreen,\n\t\thandleNextApplicationWindow,\n\t} = props;\n\n\treturn (\n\t\t<Row\n\t\t\tcenter=\"xs\"\n\t\t\taround=\"xs\"\n\t\t\tstyle={{\n\t\t\t\theight: '90%',\n\t\t\t}}\n\t\t>\n\t\t\t{viewSharingIds.map((id) => {\n\t\t\t\treturn (\n\t\t\t\t\t<Col xs={12} md={6} key={id}>\n\t\t\t\t\t\t<SharingSourcePreviewCard\n\t\t\t\t\t\t\tsharingSourceID={id}\n\t\t\t\t\t\t\tisChangeAppearanceOnHover\n\t\t\t\t\t\t\tonClickCard={async () => {\n\t\t\t\t\t\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\t\t\tIpcEvents.SetDesktopCapturerSourceId,\n\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tif (isEntireScreen) {\n\t\t\t\t\t\t\t\t\thandleNextEntireScreen();\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\thandleNextApplicationWindow();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Col>\n\t\t\t\t);\n\t\t\t})}\n\t\t</Row>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ChooseAppOrScreenOverlay/ViewSharingObject.d.ts",
    "content": "type ViewSharingObject = { thumbnailUrl: string; name: string };\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ChooseAppOrScreenStep.tsx",
    "content": "import React from 'react';\nimport { Row, Col } from 'react-flexbox-grid';\nimport ShareEntireScreenOrAppWindowControlGroup from '../ShareAppOrScreenControlGroup';\n\ninterface ChooseAppOrScreeenStepProps {\n\thandleNextEntireScreen: () => void;\n\thandleNextApplicationWindow: () => void;\n}\n\nconst ChooseAppOrScreenStep: React.FC<ChooseAppOrScreeenStepProps> = ({\n\thandleNextEntireScreen,\n\thandleNextApplicationWindow,\n}: ChooseAppOrScreeenStepProps) => {\n\treturn (\n\t\t<Row style={{ width: '100%' }}>\n\t\t\t<Col xs={12}>\n\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t<Col xs={6}>\n\t\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t\t<Col>\n\t\t\t\t\t\t\t\t<ShareEntireScreenOrAppWindowControlGroup\n\t\t\t\t\t\t\t\t\thandleNextEntireScreen={handleNextEntireScreen}\n\t\t\t\t\t\t\t\t\thandleNextApplicationWindow={handleNextApplicationWindow}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Col>\n\t\t</Row>\n\t);\n};\n\nexport default ChooseAppOrScreenStep;\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ConfirmStep.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport { Text } from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport SharingSourcePreviewCard from '../SharingSourcePreviewCard';\nimport DeviceInfoCallout from '../DeviceInfoCallout';\nimport { Device } from '../../../../common/Device';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport { useTranslation } from 'react-i18next';\n\ninterface ConfirmStepProps {\n\tdevice: Device | null;\n}\n\nexport default function ConfirmStep(props: ConfirmStepProps) {\n\tconst { device } = props;\n\tconst [\n\t\twaitingForConnectionSharingSessionSourceId,\n\t\tsetWaitingForConnectionSharingSessionSourceId,\n\t] = useState<string | undefined>();\n\tconst { t } = useTranslation();\n\n\tuseEffect(() => {\n\t\twindow.electron.ipcRenderer\n\t\t\t.invoke(IpcEvents.GetWaitingForConnectionSharingSessionSourceId)\n\t\t\t.then((id) => {\n\t\t\t\tsetWaitingForConnectionSharingSessionSourceId(id);\n\t\t\t})\n\t\t\t.catch((e) => console.error(e));\n\t}, []);\n\n\treturn (\n\t\t<div style={{ width: '80%', marginTop: '50px' }}>\n\t\t\t<Row style={{ marginBottom: '10px' }}>\n\t\t\t\t<Col xs={12} style={{ textAlign: 'center' }}>\n\t\t\t\t\t<Text>{t('check-if-all-is-ok-and-click-confirm')}</Text>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Row middle=\"xs\" center=\"xs\">\n\t\t\t\t<Col xs={5}>\n\t\t\t\t\t<DeviceInfoCallout\n\t\t\t\t\t\tdeviceType={device?.deviceType}\n\t\t\t\t\t\tdeviceIP={device?.deviceIP}\n\t\t\t\t\t\tdeviceOS={device?.deviceOS}\n\t\t\t\t\t\tdeviceBrowser={device?.deviceBrowser}\n\t\t\t\t\t\tdeviceRoomId={device?.deviceRoomId}\n\t\t\t\t\t/>\n\t\t\t\t</Col>\n\t\t\t\t<Col xs={5}>\n\t\t\t\t\t<Text>{t('this-screen-source-will-be-seen-by-the-client')}</Text>\n\t\t\t\t\t<SharingSourcePreviewCard\n\t\t\t\t\t\tsharingSourceID={waitingForConnectionSharingSessionSourceId}\n\t\t\t\t\t/>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/IntermediateStep.tsx",
    "content": "import React from 'react';\nimport { Button, Text } from '@blueprintjs/core';\nimport { Col, Row } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\nimport ScanQRStep from './ScanQRStep';\nimport ChooseAppOrScreenStep from './ChooseAppOrScreenStep';\nimport ConfirmStep from './ConfirmStep';\nimport { Device } from '../../../../common/Device';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\n\ninterface IntermediateStepProps {\n\tactiveStep: number;\n\tsteps: string[];\n\thandleBack: () => void;\n\thandleNextEntireScreen: () => void;\n\thandleNextApplicationWindow: () => void;\n\tresetPendingConnectionDevice: () => void;\n\tresetUserAllowedConnection: () => void;\n\tconnectedDevice: Device | null;\n\thandleReset: () => void;\n}\n\nfunction getStepContent(\n\tt: ReturnType<typeof useTranslation>['t'],\n\tstepIndex: number,\n\thandleNextEntireScreen: () => void,\n\thandleNextApplicationWindow: () => void,\n\tconnectedDevice: Device | null,\n): React.ReactNode {\n\tswitch (stepIndex) {\n\t\tcase 0:\n\t\t\treturn <ScanQRStep />;\n\t\tcase 1:\n\t\t\treturn (\n\t\t\t\t<>\n\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t<div style={{ marginBottom: '10px' }}>\n\t\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t\t{t('choose-entire-screen-or-app-window-you-want-to-share')}\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Row>\n\t\t\t\t\t<ChooseAppOrScreenStep\n\t\t\t\t\t\thandleNextEntireScreen={handleNextEntireScreen}\n\t\t\t\t\t\thandleNextApplicationWindow={handleNextApplicationWindow}\n\t\t\t\t\t/>\n\t\t\t\t</>\n\t\t\t);\n\t\tcase 2:\n\t\t\treturn <ConfirmStep device={connectedDevice} />;\n\t\tdefault:\n\t\t\treturn 'Unknown stepIndex';\n\t}\n}\n\nfunction isConfirmStep(activeStep: number, steps: string[]): boolean {\n\treturn activeStep === steps.length - 1;\n}\n\nexport default function IntermediateStep(\n\tprops: IntermediateStepProps,\n): React.ReactElement {\n\tconst { t } = useTranslation();\n\n\tconst {\n\t\tactiveStep,\n\t\tsteps,\n\t\thandleBack,\n\t\thandleNextEntireScreen,\n\t\thandleNextApplicationWindow,\n\t\tresetPendingConnectionDevice,\n\t\tresetUserAllowedConnection,\n\t\tconnectedDevice,\n\t\thandleReset,\n\t} = props;\n\n\treturn (\n\t\t<Col\n\t\t\txs={12}\n\t\t\tstyle={{\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\talignItems: 'center',\n\t\t\t\theight: '260px',\n\t\t\t\twidth: '100%',\n\t\t\t}}\n\t\t>\n\t\t\t{getStepContent(\n\t\t\t\tt,\n\t\t\t\tactiveStep,\n\t\t\t\thandleNextEntireScreen,\n\t\t\t\thandleNextApplicationWindow,\n\t\t\t\tconnectedDevice,\n\t\t\t)}\n\t\t\t{process.env.NODE_ENV === 'production' &&\n\t\t\tprocess.env.RUN_MODE !== 'dev' &&\n\t\t\tprocess.env.RUN_MODE !== 'test' ? (\n\t\t\t\t<></>\n\t\t\t) : activeStep === 0 ? (\n\t\t\t\t<Button\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t// connectedDevicesService.setPendingConnectionDevice(DEVICES[Math.floor(Math.random() * DEVICES.length)]);\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tConnect Test Device\n\t\t\t\t</Button>\n\t\t\t) : (\n\t\t\t\t<></>\n\t\t\t)}\n\t\t\t{activeStep !== 0 ? (\n\t\t\t\t<Row>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tintent={activeStep === 2 ? 'success' : 'none'}\n\t\t\t\t\t\t\tonClick={async () => {\n\t\t\t\t\t\t\t\tif (isConfirmStep(activeStep, steps)) {\n\t\t\t\t\t\t\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\t\t\t\tIpcEvents.StartSharingOnWaitingForConnectionSharingSession,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tresetPendingConnectionDevice();\n\t\t\t\t\t\t\t\t\tresetUserAllowedConnection();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\t\t\thandleReset();\n\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: activeStep === 1 ? 'none' : 'inline',\n\t\t\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t\t\t\twidth: '300px',\n\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\trightIcon={\n\t\t\t\t\t\t\t\tisConfirmStep(activeStep, steps)\n\t\t\t\t\t\t\t\t\t? 'small-tick'\n\t\t\t\t\t\t\t\t\t: 'chevron-right'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isConfirmStep(activeStep, steps)\n\t\t\t\t\t\t\t\t? t('confirm-button-text')\n\t\t\t\t\t\t\t\t: t('next')}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t) : (\n\t\t\t\t<></>\n\t\t\t)}\n\t\t\t<Row style={{ display: activeStep === 2 ? 'inline-block' : 'none' }}>\n\t\t\t\t<Button\n\t\t\t\t\tintent=\"danger\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginTop: '10px',\n\t\t\t\t\t\tborderRadius: '100px',\n\t\t\t\t\t}}\n\t\t\t\t\tonClick={handleBack}\n\t\t\t\t\ticon=\"chevron-left\"\n\t\t\t\t\ttext={t('no-i-need-to-choose-other')}\n\t\t\t\t/>\n\t\t\t</Row>\n\t\t</Col>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/ScanQRStep.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react';\nimport {\n\tButton,\n\tText,\n\tTooltip,\n\tPosition,\n\tDialog,\n\tClasses,\n\tH3,\n} from '@blueprintjs/core';\nimport { QRCodeSVG } from 'qrcode.react';\nimport { makeStyles, createStyles } from '@material-ui/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport isProduction from '../../../../common/isProduction';\nimport config from '../../../../common/config';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport { useTranslation } from 'react-i18next';\nimport Logo192 from '../../assets/logo192.png';\n\nconst { hostname } = config;\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tsmallQRCode: {\n\t\t\theight: '100%',\n\t\t\tborder: '1px solid',\n\t\t\tborderColor: 'rgba(0,0,0,0.0)',\n\t\t\tpadding: '10px',\n\t\t\tborderRadius: '10px',\n\t\t\tmargin: '0 auto',\n\t\t\t'&:hover': {\n\t\t\t\tbackgroundColor: 'rgba(0,0,0,0.12)',\n\t\t\t\tborder: '1px solid #8A9BA8',\n\t\t\t\tcursor: 'zoom-in',\n\t\t\t},\n\t\t},\n\t\tdialogQRWrapper: {\n\t\t\tbackgroundColor: 'white',\n\t\t\tpadding: '20px',\n\t\t\tborderRadius: '10px',\n\t\t},\n\t\tbigQRCodeDialogRoot: {\n\t\t\t'&:hover': {\n\t\t\t\tcursor: 'zoom-out',\n\t\t\t},\n\t\t\tpaddingBottom: '0px',\n\t\t},\n\t}),\n);\n\nconst ScanQRStep: React.FC = () => {\n\tconst { t } = useTranslation();\n\tconst [clientViewerPort, setClientViewerPort] = useState('80'); // Default port, can be changed later\n\tconst classes = useStyles();\n\n\tconst [isViewerSlotAvailable, setIsViewerSlotAvailable] = useState(true);\n\tconst [roomID, setRoomID] = useState('');\n\tconst [LOCAL_LAN_IP, setLocalLanIP] = useState('');\n\tconst [isQRCodeMagnified, setIsQRCodeMagnified] = useState(false);\n\n\tuseEffect(() => {\n\t\twindow.electron.ipcRenderer\n\t\t\t.invoke(IpcEvents.GetPort)\n\t\t\t.then((port) => {\n\t\t\t\treturn setClientViewerPort(port);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tconsole.error('Failed to get port:', error);\n\t\t\t});\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\n\t\tconst handleAvailabilityChange = (\n\t\t\t_: unknown,\n\t\t\tpayload: { isAvailable: boolean },\n\t\t): void => {\n\t\t\tif (cancelled) return;\n\t\t\tconst isAvailable = Boolean(payload?.isAvailable);\n\t\t\tsetIsViewerSlotAvailable(isAvailable);\n\t\t\tif (!isAvailable) {\n\t\t\t\tsetRoomID('');\n\t\t\t\tsetIsQRCodeMagnified(false);\n\t\t\t}\n\t\t};\n\n\t\twindow.electron.ipcRenderer\n\t\t\t.invoke(IpcEvents.GetViewerConnectionAvailability)\n\t\t\t.then((availability) => {\n\t\t\t\tif (cancelled) return;\n\t\t\t\tconst isAvailable = Boolean(availability);\n\t\t\t\tsetIsViewerSlotAvailable(isAvailable);\n\t\t\t\tif (!isAvailable) {\n\t\t\t\t\tsetRoomID('');\n\t\t\t\t\tsetIsQRCodeMagnified(false);\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tconsole.error('Failed to get viewer slot availability:', error);\n\t\t\t});\n\n\t\twindow.electron.ipcRenderer.on(\n\t\t\tIpcEvents.ViewerConnectionAvailabilityChanged,\n\t\t\thandleAvailabilityChange,\n\t\t);\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\twindow.electron.ipcRenderer.removeListener(\n\t\t\t\tIpcEvents.ViewerConnectionAvailabilityChanged,\n\t\t\t\thandleAvailabilityChange,\n\t\t\t);\n\t\t};\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\t\tconst fetchRoomId = async (): Promise<void> => {\n\t\t\tconst roomId = await window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.GetWaitingForConnectionSharingSessionRoomId,\n\t\t\t);\n\t\t\tif (cancelled) return;\n\t\t\tif (\n\t\t\t\ttypeof roomId === 'string' &&\n\t\t\t\troomId !== '' &&\n\t\t\t\tisViewerSlotAvailable\n\t\t\t) {\n\t\t\t\tsetRoomID(roomId);\n\t\t\t} else {\n\t\t\t\tsetRoomID('');\n\t\t\t}\n\t\t};\n\n\t\tconst fetchLocalIp = async (): Promise<void> => {\n\t\t\tconst gotIP =\n\t\t\t\tawait window.electron.ipcRenderer.invoke('get-local-lan-ip');\n\t\t\tif (!cancelled && gotIP) {\n\t\t\t\tsetLocalLanIP(gotIP);\n\t\t\t}\n\t\t};\n\n\t\tvoid fetchRoomId();\n\t\tvoid fetchLocalIp();\n\t\tconst roomInterval = setInterval(() => {\n\t\t\tvoid fetchRoomId();\n\t\t}, 1000);\n\t\tconst ipInterval = setInterval(() => {\n\t\t\tvoid fetchLocalIp();\n\t\t}, 1000);\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tclearInterval(roomInterval);\n\t\t\tclearInterval(ipInterval);\n\t\t};\n\t}, [isViewerSlotAvailable]);\n\n\tconst portString = useMemo(() => {\n\t\treturn `:${clientViewerPort}`;\n\t}, [clientViewerPort]);\n\tconst roomPath = useMemo(() => {\n\t\treturn roomID !== '' ? `/${roomID}` : '';\n\t}, [roomID]);\n\tconst shareUrl = useMemo(() => {\n\t\tif (!isViewerSlotAvailable) return '';\n\t\tif (LOCAL_LAN_IP === '') return '';\n\t\tif (roomPath === '') return '';\n\t\treturn `http://${LOCAL_LAN_IP}${portString}${roomPath}`;\n\t}, [LOCAL_LAN_IP, portString, roomPath, isViewerSlotAvailable]);\n\tconst isQrInteractive = shareUrl !== '';\n\tconst connectionLimitTooltip = t('connection-limit-reached-tooltip');\n\tconst qrTooltipContent = isQrInteractive\n\t\t? t('click-to-make-bigger')\n\t\t: connectionLimitTooltip;\n\tconst copyTooltipContent = isQrInteractive\n\t\t? t('click-to-copy')\n\t\t: connectionLimitTooltip;\n\n\treturn (\n\t\t<>\n\t\t\t<div style={{ textAlign: 'center' }}>\n\t\t\t\t<Text>\n\t\t\t\t\t<span\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tbackgroundColor: '#00f99273',\n\t\t\t\t\t\t\tfontWeight: 900,\n\t\t\t\t\t\t\tpaddingRight: '8px',\n\t\t\t\t\t\t\tpaddingLeft: '8px',\n\t\t\t\t\t\t\tborderRadius: '20px',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\n\t\t\t\t\t\t\t'make-sure-your-computer-and-screen-viewing-device-are-connected-to-same-wi-fi',\n\t\t\t\t\t\t)}\n\t\t\t\t\t</span>\n\t\t\t\t</Text>\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<Row>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<div style={{ textAlign: 'center' }}>\n\t\t\t\t\t\t\t<Text className=\"bp3-text\">\n\t\t\t\t\t\t\t\t{t('scan-the-qr-code-to-connect')}\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\t\t\tmarginBottom: '16px',\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Tooltip content={qrTooltipContent} position={Position.LEFT}>\n\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t{isQrInteractive ? (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tid=\"magnify-qr-code-button\"\n\t\t\t\t\t\t\t\t\t\t\tclassName={classes.smallQRCode}\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tif (!isQrInteractive) return;\n\t\t\t\t\t\t\t\t\t\t\t\tsetIsQRCodeMagnified(true);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tdisabled={!isQrInteractive}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<QRCodeSVG\n\t\t\t\t\t\t\t\t\t\t\t\tvalue={shareUrl}\n\t\t\t\t\t\t\t\t\t\t\t\tlevel=\"H\"\n\t\t\t\t\t\t\t\t\t\t\t\tbgColor=\"rgba(0,0,0,0.0)\"\n\t\t\t\t\t\t\t\t\t\t\t\tfgColor=\"#000000\"\n\t\t\t\t\t\t\t\t\t\t\t\timageSettings={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t// src: `http://127.0.0.1${portString}/logo192.png`,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc: Logo192,\n\t\t\t\t\t\t\t\t\t\t\t\t\twidth: 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\theight: 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\texcavate: true,\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tclassName={classes.smallQRCode}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ cursor: 'not-allowed' }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\tsrc={Logo192}\n\t\t\t\t\t\t\t\t\t\t\t\talt={t('deskreen-logo')}\n\t\t\t\t\t\t\t\t\t\t\t\twidth={64}\n\t\t\t\t\t\t\t\t\t\t\t\theight={64}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</div>\n\t\t\t<Row\n\t\t\t\tstyle={{\n\t\t\t\t\tmarginBottom: '10px',\n\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\tflexDirection: 'row',\n\t\t\t\t\talignItems: 'center',\n\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Text className=\"bp3-text-muted\">\n\t\t\t\t\t{isQrInteractive\n\t\t\t\t\t\t? t(\n\t\t\t\t\t\t\t\t'enter-the-following-address-in-browser-address-bar-on-any-device',\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: t('one-viewing-client-is-connected-already')}\n\t\t\t\t</Text>\n\t\t\t</Row>\n\n\t\t\t<Row\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\tflexDirection: 'row',\n\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Tooltip content={copyTooltipContent} position={Position.TOP}>\n\t\t\t\t\t<span>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tintent={isQrInteractive ? 'primary' : 'none'}\n\t\t\t\t\t\t\ticon=\"duplicate\"\n\t\t\t\t\t\t\tstyle={{ borderRadius: '100px' }}\n\t\t\t\t\t\t\tdisabled={!isQrInteractive}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tif (!isQrInteractive) return;\n\t\t\t\t\t\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\t\t\tIpcEvents.WriteTextToClipboard,\n\t\t\t\t\t\t\t\t\tshareUrl,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isQrInteractive ? shareUrl : t('viewing-client-connected-label')}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</span>\n\t\t\t\t</Tooltip>\n\t\t\t</Row>\n\t\t\t{!isQrInteractive && (\n\t\t\t\t<>\n\t\t\t\t\t<Row\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginTop: '12px',\n\t\t\t\t\t\t\tmarginBottom: '6px',\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tflexDirection: 'row',\n\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text className=\"bp3-text-muted\">\n\t\t\t\t\t\t\t{t('deskreen-ce-allows-only-one-client-at-same-time')}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Row>\n\t\t\t\t\t<Row\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginBottom: '10px',\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tflexDirection: 'row',\n\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text className=\"bp3-text-muted\">\n\t\t\t\t\t\t\t{t('this-will-be-available-only-in-pro-version')}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Row>\n\t\t\t\t</>\n\t\t\t)}\n\n\t\t\t<Dialog\n\t\t\t\tclassName={classes.bigQRCodeDialogRoot}\n\t\t\t\tisOpen={isQrInteractive && isQRCodeMagnified}\n\t\t\t\tonClose={() => setIsQRCodeMagnified(false)}\n\t\t\t\tcanEscapeKeyClose\n\t\t\t\tcanOutsideClickClose\n\t\t\t\ttransitionDuration={isProduction() ? 700 : 0}\n\t\t\t\tstyle={{ position: 'relative', top: '0px' }}\n\t\t\t\tusePortal={false}\n\t\t\t>\n\t\t\t\t<Row\n\t\t\t\t\tid=\"qr-code-dialog-inner\"\n\t\t\t\t\tclassName={Classes.DIALOG_BODY}\n\t\t\t\t\tcenter=\"xs\"\n\t\t\t\t\tmiddle=\"xs\"\n\t\t\t\t\tonClick={() => setIsQRCodeMagnified(false)}\n\t\t\t\t>\n\t\t\t\t\t<Col xs={11} className={classes.dialogQRWrapper}>\n\t\t\t\t\t\t{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}\n\t\t\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t\t\t<QRCodeSVG\n\t\t\t\t\t\t\tvalue={isQrInteractive ? shareUrl : 'INACTIVE'}\n\t\t\t\t\t\t\tlevel=\"H\"\n\t\t\t\t\t\t\timageSettings={{\n\t\t\t\t\t\t\t\t// src: `http://127.0.0.1${portString}/logo192.png`,\n\t\t\t\t\t\t\t\tsrc: Logo192,\n\t\t\t\t\t\t\t\twidth: 25,\n\t\t\t\t\t\t\t\theight: 25,\n\t\t\t\t\t\t\t\texcavate: true,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\twidth=\"390px\"\n\t\t\t\t\t\t\theight=\"390px\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col>\n\t\t\t\t\t\t<H3>\n\t\t\t\t\t\t\t{isQrInteractive\n\t\t\t\t\t\t\t\t? `${hostname}${portString}${roomPath}`\n\t\t\t\t\t\t\t\t: t('waiting-for-connection')}\n\t\t\t\t\t\t</H3>\n\t\t\t\t\t\t<H3>{isQrInteractive ? shareUrl : t('waiting-for-connection')}</H3>\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n};\n\nexport default ScanQRStep;\n"
  },
  {
    "path": "src/renderer/src/components/StepsOfStepper/SuccessStep.tsx",
    "content": "import React, { useCallback, useEffect } from 'react';\nimport { Button, H5, Icon, Text } from '@blueprintjs/core';\nimport { Row, Col } from 'react-flexbox-grid';\nimport { useTranslation } from 'react-i18next';\n\ninterface SuccessStepProps {\n\thandleReset: () => void;\n}\n\nconst SuccessStep: React.FC<SuccessStepProps> = (props: SuccessStepProps) => {\n\tconst { t } = useTranslation();\n\n\tuseEffect(() => {\n\t\tdocument\n\t\t\t.querySelector('#top-panel-connected-devices-list-button')\n\t\t\t?.classList.remove('pulse-not-infinite');\n\n\t\tdocument\n\t\t\t.querySelector('#top-panel-connected-devices-list-button')\n\t\t\t?.classList.add('pulse-not-infinite');\n\n\t\tsetTimeout(() => {\n\t\t\tdocument\n\t\t\t\t.querySelector('#top-panel-connected-devices-list-button')\n\t\t\t\t?.classList.remove('pulse-not-infinite');\n\t\t}, 4000);\n\t}, []);\n\n\tconst handleTextConnectedListMouseEnter = useCallback(() => {\n\t\tdocument\n\t\t\t.querySelector('#top-panel-connected-devices-list-button')\n\t\t\t?.classList.add('pulsing');\n\t}, []);\n\n\tconst handleTextConnectedListMouseLeave = useCallback(() => {\n\t\tdocument\n\t\t\t.querySelector('#top-panel-connected-devices-list-button')\n\t\t\t?.classList.remove('pulsing');\n\t}, []);\n\n\treturn (\n\t\t<Col\n\t\t\txs={8}\n\t\t\tmd={6}\n\t\t\tstyle={{\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t}}\n\t\t>\n\t\t\t<Row center=\"xs\">\n\t\t\t\t<Col xs={12}>\n\t\t\t\t\t<Icon icon=\"endorsed\" size={35} color=\"#0F9960\" />\n\t\t\t\t\t<H5>Done!</H5>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Row center=\"xs\">\n\t\t\t\t<Col xs={10}>\n\t\t\t\t\t<div style={{ marginBottom: '10px' }}>\n\t\t\t\t\t\t<Text>Now you can see your screen on other device</Text>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tid=\"connected-devices-list-text-success\"\n\t\t\t\t\t\tonMouseEnter={handleTextConnectedListMouseEnter}\n\t\t\t\t\t\tonMouseLeave={handleTextConnectedListMouseLeave}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginBottom: '25px',\n\t\t\t\t\t\t\ttextDecoration: 'underline dotted',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text className=\"\">\n\t\t\t\t\t\t\t{t(\n\t\t\t\t\t\t\t\t'you-can-manage-connected-devices-by-clicking-connected-devices-button-in-top-panel',\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</div>\n\t\t\t\t</Col>\n\t\t\t</Row>\n\t\t\t<Button\n\t\t\t\tintent=\"primary\"\n\t\t\t\tonClick={props.handleReset}\n\t\t\t\ticon=\"repeat\"\n\t\t\t\tstyle={{ borderRadius: '100px' }}\n\t\t\t>\n\t\t\t\t{t('connect-new-device')}\n\t\t\t</Button>\n\t\t</Col>\n\t);\n};\n\nexport default SuccessStep;\n"
  },
  {
    "path": "src/renderer/src/components/TopPanel.tsx",
    "content": "import React from 'react';\nimport { Button, H3, Icon, Position, Tag, Tooltip } from '@blueprintjs/core';\nimport { createStyles, makeStyles } from '@material-ui/core/styles';\nimport { Col, Row } from 'react-flexbox-grid';\nimport SettingsOverlay from './SettingsOverlay/SettingsOverlay';\nimport ConnectedDevicesListDrawer from './ConnectedDevicesListDrawer';\nimport { useTranslation } from 'react-i18next';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\ttopPanelRoot: {\n\t\t\tdisplay: 'flex',\n\t\t\tflexDirection: 'column',\n\t\t\talignItems: 'center',\n\t\t\tpaddingTop: '15px',\n\t\t\tmarginBottom: '20px',\n\t\t\tposition: 'relative',\n\t\t\tgap: '12px',\n\t\t},\n\t\tdonateButtonRoot: {\n\t\t\tdisplay: 'flex',\n\t\t\tjustifyContent: 'center',\n\t\t\twidth: '100%',\n\t\t\tmarginTop: '4px',\n\t\t},\n\t\tlogoWithAppName: { margin: '0 auto' },\n\t\tappNameHeader: {\n\t\t\tmargin: '0 auto',\n\t\t\tpaddingTop: '5px',\n\t\t\tfontFamily: 'Lexend Peta',\n\t\t\tfontSize: '20px',\n\t\t\tcolor: '#e2791b',\n\t\t\tcursor: 'default !important',\n\t\t},\n\t\tdonateButton: {\n\t\t\tborderRadius: '100px',\n\t\t\tpadding: '0',\n\t\t\theight: '40px',\n\t\t\tbackground:\n\t\t\t\t'linear-gradient(135deg, hsl(258, 90%, 66%) 0%, hsl(210, 96%, 62%) 30%, hsl(192, 94%, 44%) 70%, hsl(28, 96%, 58%) 100%)',\n\t\t\tborder: 'none',\n\t\t\tboxShadow:\n\t\t\t\t'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)',\n\t\t\ttransition: 'all 0.2s ease',\n\t\t\t'&:hover': {\n\t\t\t\ttransform: 'translateY(-1px)',\n\t\t\t\tboxShadow:\n\t\t\t\t\t'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)',\n\t\t\t},\n\t\t},\n\t\tdonateButtonContent: {\n\t\t\tdisplay: 'flex',\n\t\t\talignItems: 'center',\n\t\t\tjustifyContent: 'center',\n\t\t\theight: '100%',\n\t\t\tpadding: '0 16px',\n\t\t\tgap: '8px',\n\t\t},\n\t\tdonateButtonIcon: {\n\t\t\twidth: '20px',\n\t\t\theight: '20px',\n\t\t\tdisplay: 'block',\n\t\t\tverticalAlign: 'middle',\n\t\t\tflexShrink: 0,\n\t\t\tfilter: 'brightness(1.1) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))',\n\t\t},\n\t\tdonateButtonLabel: {\n\t\t\tdisplay: 'flex',\n\t\t\talignItems: 'center',\n\t\t\tlineHeight: '1',\n\t\t\tfontSize: '14px',\n\t\t\tfontWeight: 600,\n\t\t\tcolor: '#ffffff',\n\t\t\ttextShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',\n\t\t},\n\t\ttopPanelControlButtonsRoot: {\n\t\t\tdisplay: 'flex',\n\t\t\talignItems: 'center',\n\t\t\tgap: '12px',\n\t\t},\n\t\ttopPanelControlsWrapper: {\n\t\t\tposition: 'absolute',\n\t\t\tright: '15px',\n\t\t\ttop: '15px',\n\t\t\tdisplay: 'flex',\n\t\t\tflexDirection: 'column',\n\t\t\talignItems: 'flex-end',\n\t\t\tgap: '6px',\n\t\t},\n\t\ttopPanelControlButton: {\n\t\t\twidth: '40px',\n\t\t\theight: '40px',\n\t\t\tborderRadius: '50px',\n\t\t\tcursor: 'default !important',\n\t\t},\n\t\ttopPanelControlButtonMargin: {\n\t\t\tcursor: 'default !important',\n\t\t\tposition: 'relative',\n\t\t},\n\t\tupdateBadge: {\n\t\t\tborderRadius: '12px',\n\t\t\tcursor: 'pointer',\n\t\t\tboxShadow: 'none',\n\t\t},\n\t\ttopPanelIconOfControlButton: {\n\t\t\tcursor: 'default !important',\n\t\t},\n\t\tconnectedDevicesBadge: {\n\t\t\tposition: 'absolute',\n\t\t\ttop: '-4px',\n\t\t\tright: '-4px',\n\t\t\tbackgroundColor: '#ff3b30',\n\t\t\tcolor: '#ffffff',\n\t\t\tborderRadius: '10px',\n\t\t\tminWidth: '20px',\n\t\t\theight: '20px',\n\t\t\tdisplay: 'flex',\n\t\t\talignItems: 'center',\n\t\t\tjustifyContent: 'center',\n\t\t\tfontSize: '12px',\n\t\t\tfontWeight: 600,\n\t\t\tpadding: '0 6px',\n\t\t\tboxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',\n\t\t\tzIndex: 10,\n\t\t\tlineHeight: '1',\n\t\t},\n\t}),\n);\n\ninterface Props {\n\thandleReset: () => void;\n}\n\nexport default function TopPanel({ handleReset }: Props): React.ReactElement {\n\tconst { t } = useTranslation();\n\tconst classes = useStyles();\n\n\tconst [isSettingsOpen, setIsSettingsOpen] = React.useState(false);\n\tconst [isConnectedDevicesDrawerOpen, setIsConnectedDevicesDrawerOpen] =\n\t\tReact.useState(false);\n\tconst [latestVersion, setLatestVersion] = React.useState('');\n\tconst [currentVersion, setCurrentVersion] = React.useState('');\n\tconst [connectedDevicesCount, setConnectedDevicesCount] = React.useState(0);\n\n\tconst handleSettingsOpen = React.useCallback(() => {\n\t\tsetIsSettingsOpen(true);\n\t}, []);\n\n\tconst handleSettingsClose = React.useCallback(() => {\n\t\tsetIsSettingsOpen(false);\n\t}, []);\n\n\tconst handleToggleConnectedDevicesListDrawer = React.useCallback(() => {\n\t\tsetIsConnectedDevicesDrawerOpen(!isConnectedDevicesDrawerOpen);\n\t}, [isConnectedDevicesDrawerOpen]);\n\n\tconst donateTooltipContent = t('get-deskreen-pro-tooltip');\n\n\tconst handleDonateButtonClick = React.useCallback(() => {\n\t\twindow.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.OpenExternalLink,\n\t\t\t'https://deskreen.com/download',\n\t\t);\n\t}, []);\n\n\tconst handleTutorialButtonClick = React.useCallback(() => {\n\t\twindow.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.OpenExternalLink,\n\t\t\t'https://deskreen.com/howto',\n\t\t);\n\t}, []);\n\n\tconst handleOpenDownloadPage = React.useCallback((): void => {\n\t\tvoid window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.OpenExternalLink,\n\t\t\t'https://deskreen.com/download',\n\t\t);\n\t}, []);\n\n\tReact.useEffect(() => {\n\t\tconst fetchVersions = async (): Promise<void> => {\n\t\t\tconst [latest, current] = await Promise.all([\n\t\t\t\twindow.electron.ipcRenderer.invoke('get-latest-version'),\n\t\t\t\twindow.electron.ipcRenderer.invoke('get-current-version'),\n\t\t\t]);\n\t\t\tif (typeof latest === 'string') {\n\t\t\t\tsetLatestVersion(latest);\n\t\t\t}\n\t\t\tif (typeof current === 'string') {\n\t\t\t\tsetCurrentVersion(current);\n\t\t\t}\n\t\t};\n\n\t\tvoid fetchVersions();\n\t}, []);\n\n\tReact.useEffect(() => {\n\t\tconst fetchConnectedDevicesCount = async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\tconst devices = await window.electron.ipcRenderer.invoke(\n\t\t\t\t\tIpcEvents.GetConnectedDevices,\n\t\t\t\t);\n\t\t\t\tif (Array.isArray(devices)) {\n\t\t\t\t\tsetConnectedDevicesCount(devices.length);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t}\n\t\t};\n\n\t\tfetchConnectedDevicesCount();\n\n\t\tconst connectedDevicesInterval = setInterval(\n\t\t\tfetchConnectedDevicesCount,\n\t\t\t2000,\n\t\t);\n\n\t\treturn () => {\n\t\t\tclearInterval(connectedDevicesInterval);\n\t\t};\n\t}, []);\n\n\tconst hasUpdate =\n\t\tlatestVersion !== '' &&\n\t\tcurrentVersion !== '' &&\n\t\tlatestVersion !== currentVersion;\n\n\tconst renderDonateButton = (\n\t\t<Tooltip content={donateTooltipContent} position={Position.BOTTOM}>\n\t\t\t<Button\n\t\t\t\tid=\"top-panel-donate-button\"\n\t\t\t\tclassName={classes.donateButton}\n\t\t\t\tonClick={handleDonateButtonClick}\n\t\t\t>\n\t\t\t\t<div className={classes.donateButtonContent}>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.donateButtonIcon}\n\t\t\t\t\t\ticon=\"clean\"\n\t\t\t\t\t\tsize={20}\n\t\t\t\t\t\tcolor=\"#D4AF37\"\n\t\t\t\t\t/>\n\t\t\t\t\t<span className={classes.donateButtonLabel}>\n\t\t\t\t\t\t{t('get-deskreen-pro')}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</Button>\n\t\t</Tooltip>\n\t);\n\n\tconst renderConnectedDevicesListButton = (\n\t\t<div className={classes.topPanelControlButtonMargin}>\n\t\t\t<Tooltip content={t('connected-devices')} position={Position.BOTTOM}>\n\t\t\t\t<Button\n\t\t\t\t\tid=\"top-panel-connected-devices-list-button\"\n\t\t\t\t\tintent=\"primary\"\n\t\t\t\t\tclassName={classes.topPanelControlButton}\n\t\t\t\t\tonClick={handleToggleConnectedDevicesListDrawer}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.topPanelIconOfControlButton}\n\t\t\t\t\t\ticon=\"th-list\"\n\t\t\t\t\t\tsize={20}\n\t\t\t\t\t/>\n\t\t\t\t</Button>\n\t\t\t</Tooltip>\n\t\t\t{connectedDevicesCount > 0 && (\n\t\t\t\t<span className={classes.connectedDevicesBadge}>\n\t\t\t\t\t{connectedDevicesCount}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</div>\n\t);\n\n\tconst renderTutorialButton = (\n\t\t<div className={classes.topPanelControlButtonMargin}>\n\t\t\t<Tooltip content={t('tutorial')} position={Position.BOTTOM}>\n\t\t\t\t<Button\n\t\t\t\t\tid=\"top-panel-tutorial-button\"\n\t\t\t\t\tclassName={classes.topPanelControlButton}\n\t\t\t\t\tonClick={handleTutorialButtonClick}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.topPanelIconOfControlButton}\n\t\t\t\t\t\ticon=\"learning\"\n\t\t\t\t\t\tsize={22}\n\t\t\t\t\t/>\n\t\t\t\t</Button>\n\t\t\t</Tooltip>\n\t\t</div>\n\t);\n\n\tconst renderHelpButton = (\n\t\t<div className={classes.topPanelControlButtonMargin}>\n\t\t\t<Tooltip content={t('fix-reset-tooltip')} position={Position.BOTTOM}>\n\t\t\t\t<Button\n\t\t\t\t\tid=\"top-panel-help-button\"\n\t\t\t\t\tintent=\"danger\"\n\t\t\t\t\tclassName={classes.topPanelControlButton}\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\tPromise.resolve(handleReset()).then(() => {\n\t\t\t\t\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\t\tIpcEvents.CreateWaitingForConnectionSharingSession,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t});\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.topPanelIconOfControlButton}\n\t\t\t\t\t\ticon=\"lifesaver\"\n\t\t\t\t\t\tsize={22}\n\t\t\t\t\t/>\n\t\t\t\t</Button>\n\t\t\t</Tooltip>\n\t\t</div>\n\t);\n\n\tconst renderSettingsButton = (\n\t\t<div className={classes.topPanelControlButtonMargin}>\n\t\t\t<Tooltip content={t('settings')} position={Position.BOTTOM}>\n\t\t\t\t<Button\n\t\t\t\t\tid=\"top-panel-settings-button\"\n\t\t\t\t\tonClick={handleSettingsOpen}\n\t\t\t\t\tclassName={classes.topPanelControlButton}\n\t\t\t\t>\n\t\t\t\t\t<Icon\n\t\t\t\t\t\tclassName={classes.topPanelIconOfControlButton}\n\t\t\t\t\t\ticon=\"cog\"\n\t\t\t\t\t\tsize={22}\n\t\t\t\t\t/>\n\t\t\t\t</Button>\n\t\t\t</Tooltip>\n\t\t</div>\n\t);\n\n\tconst renderLogoWithAppName = (\n\t\t<div\n\t\t\tid=\"logo-with-popover-visit-website\"\n\t\t\tclassName={classes.logoWithAppName}\n\t\t>\n\t\t\t<H3>Deskreen Community Edition</H3>\n\t\t</div>\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<div className={classes.topPanelRoot}>\n\t\t\t\t<Row middle=\"xs\" center=\"xs\" style={{ width: '100%' }}>\n\t\t\t\t\t<Col>{renderLogoWithAppName}</Col>\n\t\t\t\t</Row>\n\t\t\t\t<div className={classes.donateButtonRoot}>{renderDonateButton}</div>\n\t\t\t\t<div className={classes.topPanelControlsWrapper}>\n\t\t\t\t\t<div className={classes.topPanelControlButtonsRoot}>\n\t\t\t\t\t\t{renderConnectedDevicesListButton}\n\t\t\t\t\t\t{renderHelpButton}\n\t\t\t\t\t\t{renderTutorialButton}\n\t\t\t\t\t\t{renderSettingsButton}\n\t\t\t\t\t</div>\n\t\t\t\t\t{hasUpdate ? (\n\t\t\t\t\t\t<Tag\n\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\tintent=\"success\"\n\t\t\t\t\t\t\tround\n\t\t\t\t\t\t\tclassName={classes.updateBadge}\n\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\tonClick={handleOpenDownloadPage}\n\t\t\t\t\t\t\tonKeyDown={(event) => {\n\t\t\t\t\t\t\t\tif (event.key === 'Enter' || event.key === ' ') {\n\t\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\t\thandleOpenDownloadPage();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{t('new-version-available')}\n\t\t\t\t\t\t</Tag>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t{isSettingsOpen ? (\n\t\t\t\t<SettingsOverlay\n\t\t\t\t\tisSettingsOpen={isSettingsOpen}\n\t\t\t\t\thandleClose={handleSettingsClose}\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<></>\n\t\t\t)}\n\t\t\t{isConnectedDevicesDrawerOpen ? (\n\t\t\t\t<ConnectedDevicesListDrawer\n\t\t\t\t\tisOpen={isConnectedDevicesDrawerOpen}\n\t\t\t\t\thandleToggle={handleToggleConnectedDevicesListDrawer}\n\t\t\t\t\thandleReset={handleReset}\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<></>\n\t\t\t)}\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/components/css.d.ts",
    "content": "declare module '*.scss' {\n\tconst content: { [className: string]: string };\n\texport default content;\n}\n\ndeclare module '*.css' {\n\tconst content: { [className: string]: string };\n\texport default content;\n}\n"
  },
  {
    "path": "src/renderer/src/configs/i18next.config.client.ts",
    "content": "import i18next, { TFunction } from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport config from '../../../common/app.lang.config';\nimport translationEN from '../../../common/locales/en/translation.json';\nimport translationES from '../../../common/locales/es/translation.json';\nimport translationKO from '../../../common/locales/ko/translation.json';\nimport translationUA from '../../../common/locales/ua/translation.json';\nimport translationRU from '../../../common/locales/ru/translation.json';\nimport translationZH_CN from '../../../common/locales/zh_CN/translation.json';\nimport translationZH_TW from '../../../common/locales/zh_TW/translation.json';\nimport translationDA from '../../../common/locales/da/translation.json';\nimport translationDE from '../../../common/locales/de/translation.json';\nimport translationFI from '../../../common/locales/fi/translation.json';\nimport translationIT from '../../../common/locales/it/translation.json';\nimport translationJA from '../../../common/locales/ja/translation.json';\nimport translationNL from '../../../common/locales/nl/translation.json';\nimport translationFR from '../../../common/locales/fr/translation.json';\nimport translationSV from '../../../common/locales/sv/translation.json';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\n// import { store } from '../../../common/deskreen-electron-store';\n// import { ElectronStoreKeys } from '../../../common/ElectronStoreKeys.enum';\n\nconst i18n = i18next.createInstance(); // Create a new instance\n\nexport const getLangFullNameToLangISOKeyMap = (): Map<string, string> => {\n\tconst res = new Map<string, string>();\n\n\tfor (const [key, value] of Object.entries(\n\t\tconfig.langISOKeyToLangFullNameMap,\n\t)) {\n\t\tres.set(value, key);\n\t}\n\treturn res;\n};\n\nexport const getLangISOKeyToLangFullNameMap = (): Map<string, string> => {\n\tconst res = new Map<string, string>();\n\n\tfor (const [key, value] of Object.entries(\n\t\tconfig.langISOKeyToLangFullNameMap,\n\t)) {\n\t\tres.set(key, value);\n\t}\n\treturn res;\n};\n\nfunction shuffleArray(array: unknown[]): void {\n\tfor (let i = array.length - 1; i > 0; i--) {\n\t\tconst j = Math.floor(Math.random() * (i + 1));\n\t\t[array[i], array[j]] = [array[j], array[i]];\n\t}\n}\n\nexport let t: TFunction;\n\nexport const getShuffledArrayOfHello = (): string[] => {\n\tconst res: string[] = [];\n\n\tres.push(translationES.hello);\n\tres.push(translationUA.hello);\n\tres.push(translationKO.hello);\n\tres.push(translationRU.hello);\n\tres.push(translationZH_CN.hello);\n\tres.push(translationZH_TW.hello);\n\tres.push(translationDA.hello);\n\tres.push(translationDE.hello);\n\tres.push(translationFI.hello);\n\tres.push(translationIT.hello);\n\tres.push(translationJA.hello);\n\tres.push(translationNL.hello);\n\tres.push(translationFR.hello);\n\tres.push(translationSV.hello);\n\n\tshuffleArray(res);\n\n\tres.unshift(translationEN.hello);\n\n\treturn res;\n};\n\nasync function initI18NextOptions(): Promise<void> {\n\tconst appLanguage = await window.electron.ipcRenderer.invoke(\n\t\tIpcEvents.GetAppLanguage,\n\t);\n\n\ti18n.use(initReactI18next);\n\tconst i18nextOptions = {\n\t\tdebug: true,\n\t\tinterpolation: {\n\t\t\tescapeValue: false,\n\t\t},\n\t\tsaveMissing: true,\n\t\tlng: config.languages.includes(appLanguage) ? appLanguage : 'en',\n\t\tfallbackLng: config.fallbackLng,\n\t\twhitelist: config.languages,\n\t\treact: {},\n\t\tresources: {\n\t\t\ten: {\n\t\t\t\ttranslation: translationEN,\n\t\t\t},\n\t\t\tes: {\n\t\t\t\ttranslation: translationES,\n\t\t\t},\n\t\t\tko: {\n\t\t\t\ttranslation: translationKO,\n\t\t\t},\n\t\t\tua: {\n\t\t\t\ttranslation: translationUA,\n\t\t\t},\n\t\t\tru: {\n\t\t\t\ttranslation: translationRU,\n\t\t\t},\n\t\t\tzh_CN: {\n\t\t\t\ttranslation: translationZH_CN,\n\t\t\t},\n\t\t\tzh_TW: {\n\t\t\t\ttranslation: translationZH_TW,\n\t\t\t},\n\t\t\tda: {\n\t\t\t\ttranslation: translationDA,\n\t\t\t},\n\t\t\tde: {\n\t\t\t\ttranslation: translationDE,\n\t\t\t},\n\t\t\tfi: {\n\t\t\t\ttranslation: translationFI,\n\t\t\t},\n\t\t\tit: {\n\t\t\t\ttranslation: translationIT,\n\t\t\t},\n\t\t\tja: {\n\t\t\t\ttranslation: translationJA,\n\t\t\t},\n\t\t\tnl: {\n\t\t\t\ttranslation: translationNL,\n\t\t\t},\n\t\t\tfr: {\n\t\t\t\ttranslation: translationFR,\n\t\t\t},\n\t\t\tsv: {\n\t\t\t\ttranslation: translationSV,\n\t\t\t},\n\t\t},\n\t};\n\n\tif (!i18n.isInitialized) {\n\t\tt = await i18n.init(i18nextOptions);\n\t}\n}\n\nexport const i18nInitPromise = initI18NextOptions();\n\ni18n.on('languageChanged', () => {\n\twindow.electron.ipcRenderer.send('client-changed-language', i18n.language);\n});\n\nexport default i18n;\n"
  },
  {
    "path": "src/renderer/src/containers/App.tsx",
    "content": "import { ReactNode } from 'react';\n\ntype Props = {\n\tchildren: ReactNode;\n};\n\nconst App: React.FC<Props> = (props: Props) => {\n\tconst { children } = props;\n\treturn <>{children}</>;\n};\n\nexport default App;\n"
  },
  {
    "path": "src/renderer/src/containers/DeskreenStepper.tsx",
    "content": "// import SuccessStep from '../components/StepsOfStepper/SuccessStep';\nimport React, { useState, useCallback, useEffect, ReactNode } from 'react';\nimport { makeStyles, createStyles } from '@material-ui/core/styles';\nimport Stepper from '@material-ui/core/Stepper';\nimport Step from '@material-ui/core/Step';\nimport StepLabel from '@material-ui/core/StepLabel';\nimport { Row, Col, Grid } from 'react-flexbox-grid';\nimport {\n\tButton,\n\tDialog,\n\tH1,\n\tH3,\n\tH4,\n\tH5,\n\tIcon,\n\tSpinner,\n\tText,\n} from '@blueprintjs/core';\nimport IntermediateStep from '@renderer/components/StepsOfStepper/IntermediateStep';\nimport ColorlibConnector from '@renderer/components/StepperPanel/ColorlibConnector';\nimport { Device } from '../../../common/Device';\nimport ColorlibStepIcon, {\n\tStepIconPropsDeskreen,\n} from '@renderer/components/StepperPanel/ColorlibStepIcon';\nimport LanguageSelector from '@renderer/components/LanguageSelector';\nimport { getShuffledArrayOfHello } from '@renderer/configs/i18next.config.client';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\nimport DeviceConnectedInfoButton from '@renderer/components/StepperPanel/DeviceConnectedInfoButton';\nimport AllowConnectionForDeviceAlert from '@renderer/components/AllowConnectionForDeviceAlert';\nimport { useTranslation } from 'react-i18next';\nimport { TFunction } from 'i18next';\nimport { showMessageFromNewToaster } from '@renderer/utils/showMessageFromNewToaster';\n\nconst useStyles = makeStyles(() =>\n\tcreateStyles({\n\t\tstepContent: {\n\t\t\tdisplay: 'flex',\n\t\t\tflexDirection: 'column',\n\t\t\tjustifyContent: 'center',\n\t\t\talignItems: 'center',\n\t\t},\n\t\tstepLabelContent: {\n\t\t\tmarginTop: '10px !important',\n\t\t\theight: '110px',\n\t\t},\n\t\tstepperComponent: {\n\t\t\tpaddingBottom: '0px',\n\t\t},\n\t}),\n);\n\nfunction getSteps(t: TFunction): string[] {\n\treturn [t('connect'), t('select'), t('confirm')];\n}\n\ninterface Props {\n\tactiveStep: number;\n\tsetActiveStep: React.Dispatch<React.SetStateAction<number>>;\n\tisAllowDeviceAlertOpen: boolean;\n\tsetIsAllowDeviceAlertOpen: (isOpen: boolean) => void;\n\tisUserAllowedConnection: boolean;\n\tsetIsUserAllowedConnection: (isAllowed: boolean) => void;\n\tpendingConnectionDevice: Device | null;\n\tsetPendingConnectionDevice: (device: Device | null) => void;\n\thandleReset: () => void;\n}\n\nconst DeskreenStepper = ({\n\tactiveStep,\n\tsetActiveStep,\n\tisAllowDeviceAlertOpen,\n\tsetIsAllowDeviceAlertOpen,\n\tisUserAllowedConnection,\n\tsetIsUserAllowedConnection,\n\tpendingConnectionDevice,\n\tsetPendingConnectionDevice,\n\thandleReset,\n}: Props): ReactNode => {\n\tconst classes = useStyles();\n\tconst { t } = useTranslation();\n\n\tconst [isEntireScreenSelected, setIsEntireScreenSelected] = useState(false);\n\tconst [isApplicationWindowSelected, setIsApplicationWindowSelected] =\n\t\tuseState(false);\n\tconst [isNoWiFiError, setisNoWiFiError] = useState(false);\n\tconst [isSelectLanguageDialogOpen, setIsSelectLanguageDialogOpen] =\n\t\tuseState(false);\n\tconst [helloWord, setHelloWord] = useState('Hello');\n\n\tuseEffect(() => {\n\t\tlet helloInterval: NodeJS.Timeout;\n\t\tasync function stepperOpenedCallback(): Promise<void> {\n\t\t\tconst isFirstTimeStart = await window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.GetIsFirstTimeAppStart,\n\t\t\t);\n\t\t\tsetIsSelectLanguageDialogOpen(isFirstTimeStart);\n\t\t\tif (!isFirstTimeStart) return;\n\t\t\tconst helloWords = getShuffledArrayOfHello();\n\t\t\tlet pos = 0;\n\t\t\thelloInterval = setInterval(() => {\n\t\t\t\tif (pos + 1 === helloWords.length) {\n\t\t\t\t\tpos = 0;\n\t\t\t\t} else {\n\t\t\t\t\tpos += 1;\n\t\t\t\t}\n\t\t\t\tsetHelloWord(helloWords[pos]);\n\t\t\t}, 4000);\n\t\t}\n\t\tstepperOpenedCallback();\n\n\t\treturn () => {\n\t\t\tclearInterval(helloInterval);\n\t\t};\n\t}, []);\n\n\tuseEffect(() => {\n\t\tconst wifiCheckInterval = setInterval(async () => {\n\t\t\tconst isConnected = await window.electron.ipcRenderer.invoke(\n\t\t\t\t'check-wifi-connection',\n\t\t\t);\n\t\t\tif (!isConnected) {\n\t\t\t\tsetisNoWiFiError(true);\n\t\t\t} else {\n\t\t\t\tsetisNoWiFiError(false);\n\t\t\t}\n\t\t}, 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(wifiCheckInterval);\n\t\t};\n\t}, []);\n\n\tconst steps = getSteps(t);\n\n\tconst handleNext = useCallback((): void => {\n\t\tif (activeStep === steps.length - 1) {\n\t\t\tsetIsEntireScreenSelected(false);\n\t\t\tsetIsApplicationWindowSelected(false);\n\t\t}\n\t\tsetActiveStep((prevActiveStep: number): number => prevActiveStep + 1);\n\t}, [activeStep, setActiveStep, steps]);\n\n\tconst handleNextEntireScreen = useCallback((): void => {\n\t\tsetActiveStep((prevActiveStep: number): number => prevActiveStep + 1);\n\t\tsetIsEntireScreenSelected(true);\n\t}, [setActiveStep]);\n\n\tconst handleNextApplicationWindow = useCallback((): void => {\n\t\tsetActiveStep((prevActiveStep: number): number => prevActiveStep + 1);\n\t\tsetIsApplicationWindowSelected(true);\n\t}, [setActiveStep]);\n\n\tconst handleBack = useCallback((): void => {\n\t\tsetActiveStep((prevActiveStep: number) => prevActiveStep - 1);\n\t}, [setActiveStep]);\n\n\tconst handleCancelAlert = async (): Promise<void> => {\n\t\tsetIsAllowDeviceAlertOpen(false);\n\t\tsetActiveStep(0);\n\t\tsetPendingConnectionDevice(null);\n\t\tsetIsUserAllowedConnection(false);\n\n\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.ResetWaitingForConnectionSharingSession,\n\t\t);\n\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.CreateWaitingForConnectionSharingSession,\n\t\t);\n\t};\n\n\tconst handleConfirmAlert = useCallback(async () => {\n\t\tsetIsAllowDeviceAlertOpen(false);\n\t\tsetIsUserAllowedConnection(true);\n\t\thandleNext();\n\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.SetDeviceConnectedStatus,\n\t\t);\n\t}, [handleNext, setIsAllowDeviceAlertOpen, setIsUserAllowedConnection]);\n\n\tuseEffect(() => {\n\t\twindow.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.CreateWaitingForConnectionSharingSession,\n\t\t);\n\n\t\tconst handlePendingConnectionDevice = (\n\t\t\t_: unknown,\n\t\t\tdevice: Device,\n\t\t): void => {\n\t\t\tsetPendingConnectionDevice(device);\n\t\t\tsetIsAllowDeviceAlertOpen(true);\n\t\t};\n\n\t\twindow.electron.ipcRenderer.on(\n\t\t\tIpcEvents.SetPendingConnectionDevice,\n\t\t\thandlePendingConnectionDevice,\n\t\t);\n\n\t\treturn () => {\n\t\t\twindow.electron.ipcRenderer.removeListener(\n\t\t\t\tIpcEvents.SetPendingConnectionDevice,\n\t\t\t\thandlePendingConnectionDevice,\n\t\t\t);\n\t\t};\n\t}, [setIsAllowDeviceAlertOpen, setPendingConnectionDevice]);\n\n\tconst handleUserClickedDeviceDisconnectButton =\n\t\tuseCallback(async (): Promise<void> => {\n\t\t\thandleReset();\n\n\t\t\tawait showMessageFromNewToaster(\n\t\t\t\tt(\n\t\t\t\t\t'device-is-successfully-disconnected-by-you-you-can-connect-a-new-device',\n\t\t\t\t),\n\t\t\t);\n\t\t}, [handleReset, t]);\n\n\tconst renderIntermediateOrSuccessStepContent = useCallback(() => {\n\t\treturn (\n\t\t\t<div id=\"intermediate-step-container\" style={{ width: '100%' }}>\n\t\t\t\t<IntermediateStep\n\t\t\t\t\tactiveStep={activeStep}\n\t\t\t\t\tsteps={steps}\n\t\t\t\t\thandleBack={handleBack}\n\t\t\t\t\thandleNextEntireScreen={handleNextEntireScreen}\n\t\t\t\t\thandleNextApplicationWindow={handleNextApplicationWindow}\n\t\t\t\t\tresetPendingConnectionDevice={() => setPendingConnectionDevice(null)}\n\t\t\t\t\tresetUserAllowedConnection={() => setIsUserAllowedConnection(false)}\n\t\t\t\t\tconnectedDevice={pendingConnectionDevice}\n\t\t\t\t\thandleReset={handleReset}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t);\n\t}, [\n\t\tactiveStep,\n\t\tsteps,\n\t\thandleReset,\n\t\thandleBack,\n\t\thandleNextEntireScreen,\n\t\thandleNextApplicationWindow,\n\t\tpendingConnectionDevice,\n\t\tsetIsUserAllowedConnection,\n\t\tsetPendingConnectionDevice,\n\t]);\n\n\tconst renderStepLabelContent = useCallback(\n\t\t(label, idx) => {\n\t\t\treturn (\n\t\t\t\t<StepLabel\n\t\t\t\t\tid=\"step-label-deskreen\"\n\t\t\t\t\tclassName={classes.stepLabelContent}\n\t\t\t\t\tStepIconComponent={ColorlibStepIcon}\n\t\t\t\t\tStepIconProps={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tisEntireScreenSelected,\n\t\t\t\t\t\t\tisApplicationWindowSelected,\n\t\t\t\t\t\t} as StepIconPropsDeskreen\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{pendingConnectionDevice && idx === 0 && isUserAllowedConnection ? (\n\t\t\t\t\t\t<DeviceConnectedInfoButton\n\t\t\t\t\t\t\tdevice={pendingConnectionDevice}\n\t\t\t\t\t\t\tonDisconnect={handleUserClickedDeviceDisconnectButton}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<Text className=\"bp3-text-muted\">{label}</Text>\n\t\t\t\t\t)}\n\t\t\t\t</StepLabel>\n\t\t\t);\n\t\t},\n\t\t[\n\t\t\tclasses.stepLabelContent,\n\t\t\thandleUserClickedDeviceDisconnectButton,\n\t\t\tisApplicationWindowSelected,\n\t\t\tisEntireScreenSelected,\n\t\t\tisUserAllowedConnection,\n\t\t\tpendingConnectionDevice,\n\t\t],\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<>\n\t\t\t\t<Row style={{ width: '100%' }}>\n\t\t\t\t\t<Col xs={12}>\n\t\t\t\t\t\t<Stepper\n\t\t\t\t\t\t\tclassName={classes.stepperComponent}\n\t\t\t\t\t\t\tactiveStep={activeStep}\n\t\t\t\t\t\t\talternativeLabel\n\t\t\t\t\t\t\tstyle={{ background: 'transparent' }}\n\t\t\t\t\t\t\tconnector={<ColorlibConnector />}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{steps.map((label, idx) => (\n\t\t\t\t\t\t\t\t<Step key={label}>{renderStepLabelContent(label, idx)}</Step>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Stepper>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col className={classes.stepContent} xs={12}>\n\t\t\t\t\t\t{renderIntermediateOrSuccessStepContent()}\n\t\t\t\t\t</Col>\n\t\t\t\t</Row>\n\t\t\t\t<AllowConnectionForDeviceAlert\n\t\t\t\t\tdevice={pendingConnectionDevice}\n\t\t\t\t\tisOpen={isAllowDeviceAlertOpen}\n\t\t\t\t\tonCancel={handleCancelAlert}\n\t\t\t\t\tonConfirm={handleConfirmAlert}\n\t\t\t\t/>\n\t\t\t</>\n\t\t\t<Dialog isOpen={isNoWiFiError} autoFocus usePortal>\n\t\t\t\t<Grid>\n\t\t\t\t\t<div style={{ padding: '10px' }}>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t\t\t\t<Icon icon=\"offline\" size={50} color=\"#8A9BA8\" />\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t\t\t\t<H3>{t('no-wifi-and-lan-connection')}</H3>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t\t<H5>{t('deskreen-ce-works-only-with-wifi-and-lan-networks')}</H5>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row center=\"xs\">\n\t\t\t\t\t\t\t<Spinner size={50} />\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t\t\t\t<H4>{t('waiting-for-connection')}</H4>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t</Grid>\n\t\t\t</Dialog>\n\t\t\t<Dialog isOpen={isSelectLanguageDialogOpen} autoFocus usePortal>\n\t\t\t\t<Grid>\n\t\t\t\t\t<div style={{ padding: '10px' }}>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t\t\t\t<H1>{helloWord}</H1>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row>\n\t\t\t\t\t\t\t<Col xs>\n\t\t\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '20px' }}>\n\t\t\t\t\t\t\t\t\t<Icon icon=\"translate\" size={50} color=\"#8A9BA8\" />\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '20px' }}>\n\t\t\t\t\t\t\t\t\t<H5>{t('language')}</H5>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '10px' }}>\n\t\t\t\t\t\t\t\t\t<LanguageSelector />\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</Col>\n\t\t\t\t\t\t\t{/*<Col xs>*/}\n\t\t\t\t\t\t\t{/*  <Row center=\"xs\" style={{ marginTop: '20px' }}>*/}\n\t\t\t\t\t\t\t{/*    <Icon icon=\"style\" size={50} color=\"#8A9BA8\" />*/}\n\t\t\t\t\t\t\t{/*  </Row>*/}\n\t\t\t\t\t\t\t{/*  <Row center=\"xs\" style={{ marginTop: '20px' }}>*/}\n\t\t\t\t\t\t\t{/*    <H5>{t('color-theme')}</H5>*/}\n\t\t\t\t\t\t\t{/*  </Row>*/}\n\t\t\t\t\t\t\t{/*  <Row center=\"xs\" style={{ marginTop: '10px' }}>*/}\n\t\t\t\t\t\t\t{/*    <ToggleThemeBtnGroup />*/}\n\t\t\t\t\t\t\t{/*  </Row>*/}\n\t\t\t\t\t\t\t{/*</Col>*/}\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Row center=\"xs\" style={{ marginTop: '20px' }}>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tminimal\n\t\t\t\t\t\t\t\trightIcon=\"chevron-right\"\n\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\tsetIsSelectLanguageDialogOpen(false);\n\t\t\t\t\t\t\t\t\twindow.electron.ipcRenderer.invoke(\n\t\t\t\t\t\t\t\t\t\tIpcEvents.SetAppStartedOnce,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tstyle={{ borderRadius: '50px' }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{t('continue')}\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t</Grid>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n};\n\nexport default DeskreenStepper;\n"
  },
  {
    "path": "src/renderer/src/containers/HomePage.tsx",
    "content": "import React, { useCallback, useState } from 'react';\nimport { Classes } from '@blueprintjs/core';\nimport { ToastProvider, DefaultToast } from 'react-toast-notifications';\n\nimport { LIGHT_UI_BACKGROUND } from './SettingsProvider';\nimport DeskreenStepper from './DeskreenStepper';\nimport { Device } from '../../../common/Device';\nimport TopPanel from '@renderer/components/TopPanel';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\n\n// @ts-ignore: it is ok here, be like js it is fine\n// eslint-disable-next-line react/prop-types\nexport const CustomToastWithTheme = ({\n\tchildren,\n\t...props\n}): React.ReactElement => {\n\treturn (\n\t\t<DefaultToast\n\t\t\tcomponents={{ Toast: CustomToastWithTheme }}\n\t\t\t{...props}\n\t\t\t// @ts-ignore: some minor type complain, it is fine here\n\t\t\tstyle={{\n\t\t\t\tcolor: '#293742',\n\t\t\t\tbackgroundColor: LIGHT_UI_BACKGROUND,\n\t\t\t}}\n\t\t>\n\t\t\t<>{children}</>\n\t\t</DefaultToast>\n\t);\n};\n\nexport default function HomePage(): React.ReactElement {\n\tconsole.log('window.api', window.api);\n\tconst [activeStep, setActiveStep] = useState(0);\n\tconst [isAllowDeviceAlertOpen, setIsAllowDeviceAlertOpen] = useState(false);\n\tconst [isUserAllowedConnection, setIsUserAllowedConnection] = useState(false);\n\tconst [pendingConnectionDevice, setPendingConnectionDevice] =\n\t\tuseState<Device | null>(null);\n\n\tconst handleResetWithSharingSessionRestart =\n\t\tuseCallback(async (): Promise<void> => {\n\t\t\tsetActiveStep(0);\n\t\t\tsetPendingConnectionDevice(null);\n\t\t\tsetIsUserAllowedConnection(false);\n\t\t\tsetIsAllowDeviceAlertOpen(false);\n\n\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.ResetWaitingForConnectionSharingSession,\n\t\t\t);\n\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\tIpcEvents.CreateWaitingForConnectionSharingSession,\n\t\t\t);\n\t\t}, []);\n\n\treturn (\n\t\t<ToastProvider\n\t\t\tplacement=\"top-center\"\n\t\t\tautoDismissTimeout={5000}\n\t\t\tcomponents={{ Toast: CustomToastWithTheme }}\n\t\t>\n\t\t\t<div className={Classes.TREE}>\n\t\t\t\t<TopPanel handleReset={handleResetWithSharingSessionRestart} />\n\t\t\t\t<DeskreenStepper\n\t\t\t\t\tactiveStep={activeStep}\n\t\t\t\t\tsetActiveStep={setActiveStep}\n\t\t\t\t\tisAllowDeviceAlertOpen={isAllowDeviceAlertOpen}\n\t\t\t\t\tsetIsAllowDeviceAlertOpen={setIsAllowDeviceAlertOpen}\n\t\t\t\t\tisUserAllowedConnection={isUserAllowedConnection}\n\t\t\t\t\tsetIsUserAllowedConnection={setIsUserAllowedConnection}\n\t\t\t\t\tpendingConnectionDevice={pendingConnectionDevice}\n\t\t\t\t\tsetPendingConnectionDevice={setPendingConnectionDevice}\n\t\t\t\t\thandleReset={handleResetWithSharingSessionRestart}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</ToastProvider>\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/containers/Root.tsx",
    "content": "import { FocusStyleManager } from '@blueprintjs/core';\nimport { SettingsProvider } from './SettingsProvider';\nimport HomePage from '@renderer/containers/HomePage';\n\nFocusStyleManager.onlyShowFocusOnTabs();\n\nconst Root = () => {\n\treturn (\n\t\t<SettingsProvider>\n\t\t\t<HomePage />\n\t\t</SettingsProvider>\n\t);\n};\n\nexport default Root;\n"
  },
  {
    "path": "src/renderer/src/containers/SettingsProvider.tsx",
    "content": "import React, { useState } from 'react';\nimport { SettingsContext } from '@renderer/contexts/SettingsContext';\n\n// TODO: move to 'constants' tsx file ?\nexport const LIGHT_UI_BACKGROUND = 'rgba(240, 248, 250, 1)';\n\nexport const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({\n\tchildren,\n}) => {\n\tconst [currentLanguage, setCurrentLanguage] = useState('en');\n\n\tconst setCurrentLanguageHook = (newLang: string): void => {\n\t\tsetCurrentLanguage(newLang);\n\t};\n\n\tconst value = {\n\t\tcurrentLanguage,\n\t\tsetCurrentLanguageHook,\n\t};\n\n\treturn (\n\t\t<SettingsContext.Provider value={value}>\n\t\t\t{children}\n\t\t</SettingsContext.Provider>\n\t);\n};\n"
  },
  {
    "path": "src/renderer/src/contexts/SettingsContext.tsx",
    "content": "import React from 'react';\n\nexport interface SettingsContextInterface {\n\tcurrentLanguage: string;\n\tsetCurrentLanguageHook: (newLang: string) => void;\n}\n\nexport const defaultSettingsContextValue = {\n\tsetCurrentLanguageHook: () => {\n\t\t// noop default\n\t},\n\tcurrentLanguage: 'en',\n};\n\nexport const SettingsContext = React.createContext<SettingsContextInterface>(\n\tdefaultSettingsContextValue,\n);\n"
  },
  {
    "path": "src/renderer/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/NullSimplePeer.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nconst NullSimplePeer = new SimplePeer();\n\nexport default NullSimplePeer;\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/NullUser.ts",
    "content": "export default { username: '', id: '' };\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/PartnerPeerUser.d.ts",
    "content": "interface PartnerPeerUser {\n\tusername: string;\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/PeerConnection.d.ts",
    "content": "// use import() to prevent cycle import!\n// From here: https://stackoverflow.com/questions/39040108/import-class-in-definition-file-d-ts\ntype PeerConnection = import('./index').default;\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts",
    "content": "interface ReceiveEncryptedMessagePayload {\n\tfromSocketID: string;\n\ttype: string;\n\tpayload: Record<string, unknown>;\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/createDesktopCapturerStream.ts",
    "content": "import getDesktopSourceStreamBySourceID from './getDesktopSourceStreamBySourceID';\nimport DesktopCapturerSourceType from '../../../../common/DesktopCapturerSourceType';\n\nexport default async function createDesktopCapturerStream(\n\tpeerConnection: PeerConnection,\n\tsourceID: string,\n): Promise<void> {\n\ttry {\n\t\tif (process.env.RUN_MODE === 'test') return;\n\n\t\tif (sourceID.includes(DesktopCapturerSourceType.SCREEN)) {\n\t\t\tconst stream = await getDesktopSourceStreamBySourceID(\n\t\t\t\tsourceID,\n\t\t\t\tpeerConnection.sourceDisplaySize?.width,\n\t\t\t\tpeerConnection.sourceDisplaySize?.height,\n\t\t\t\t0.5,\n\t\t\t\t1,\n\t\t\t);\n\t\t\tpeerConnection.localStream = stream;\n\t\t} else {\n\t\t\t// when source is app window\n\t\t\tconst stream = await getDesktopSourceStreamBySourceID(sourceID);\n\t\t\tpeerConnection.localStream = stream;\n\t\t}\n\t} catch (e) {\n\t\tconsole.error(e);\n\t}\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/getDesktopSourceStreamBySourceID.ts",
    "content": "async function getStreamWithSource(\n\tchromeMediaSource: 'desktop' | 'screen',\n\tsourceID: string,\n\twidth: number | null | undefined,\n\theight: number | null | undefined,\n\tminSizeMultiplier: number,\n\tmaxSizeMultiplier: number,\n\tminFrameRate: number,\n\tmaxFrameRate: number,\n): Promise<MediaStream> {\n\tif (width && height) {\n\t\treturn navigator.mediaDevices.getUserMedia({\n\t\t\taudio: false,\n\t\t\tvideo: {\n\t\t\t\t// @ts-ignore: mandatory is supported in chromium but missing in types\n\t\t\t\tmandatory: {\n\t\t\t\t\tchromeMediaSource,\n\t\t\t\t\tchromeMediaSourceId: sourceID,\n\t\t\t\t\tminWidth: width * minSizeMultiplier,\n\t\t\t\t\tmaxWidth: width * maxSizeMultiplier,\n\t\t\t\t\tminHeight: height * minSizeMultiplier,\n\t\t\t\t\tmaxHeight: height * maxSizeMultiplier,\n\t\t\t\t\tminFrameRate,\n\t\t\t\t\tmaxFrameRate,\n\t\t\t\t},\n\t\t\t},\n\t\t});\n\t}\n\n\treturn navigator.mediaDevices.getUserMedia({\n\t\taudio: false,\n\t\tvideo: {\n\t\t\t// @ts-ignore: mandatory is supported in chromium but missing in types\n\t\t\tmandatory: {\n\t\t\t\tchromeMediaSource,\n\t\t\t\tchromeMediaSourceId: sourceID,\n\t\t\t\tminFrameRate,\n\t\t\t\tmaxFrameRate,\n\t\t\t},\n\t\t},\n\t});\n}\n\nexport default async (\n\tsourceID: string,\n\twidth: number | null | undefined = undefined,\n\theight: number | null | undefined = undefined,\n\tminSizeMultiplier = 1,\n\tmaxSizeMultiplier = 1,\n\tminFrameRate = 15,\n\tmaxFrameRate = 60,\n): Promise<MediaStream> => {\n\ttry {\n\t\treturn await getStreamWithSource(\n\t\t\t'desktop',\n\t\t\tsourceID,\n\t\t\twidth,\n\t\t\theight,\n\t\t\tminSizeMultiplier,\n\t\t\tmaxSizeMultiplier,\n\t\t\tminFrameRate,\n\t\t\tmaxFrameRate,\n\t\t);\n\t} catch (desktopError) {\n\t\tconsole.warn(\n\t\t\t'failed to capture desktop stream, retrying with screen source',\n\t\t\tdesktopError,\n\t\t);\n\t\treturn getStreamWithSource(\n\t\t\t'screen',\n\t\t\tsourceID,\n\t\t\twidth,\n\t\t\theight,\n\t\t\tminSizeMultiplier,\n\t\t\tmaxSizeMultiplier,\n\t\t\tminFrameRate,\n\t\t\tmaxFrameRate,\n\t\t);\n\t}\n};\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleCreatePeer.ts",
    "content": "// import SimplePeer from 'simple-peer';\nimport createDesktopCapturerStream from './createDesktopCapturerStream';\nimport handlePeerOnData from './handlePeerOnData';\nimport NullSimplePeer from './NullSimplePeer';\n// import simplePeerHandleSdpTransform from './simplePeerHandleSdpTransform';\n\nexport default function handleCreatePeer(\n\tpeerConnection: PeerConnection,\n): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\t// cleanup existing peer before creating new one\n\t\tif (peerConnection.peer !== NullSimplePeer) {\n\t\t\ttry {\n\t\t\t\tpeerConnection.peer.removeAllListeners();\n\t\t\t\tpeerConnection.peer.destroy();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Error cleaning up existing peer:', error);\n\t\t\t}\n\t\t\tpeerConnection.peer = NullSimplePeer;\n\t\t}\n\n\t\t// cleanup existing stream before creating new one\n\t\tif (peerConnection.localStream) {\n\t\t\tpeerConnection.localStream.getTracks().forEach((track) => {\n\t\t\t\ttrack.stop();\n\t\t\t});\n\t\t\tpeerConnection.localStream = null;\n\t\t}\n\n\t\t// clear old signals and reset call state when recreating peer\n\t\tpeerConnection.signalsDataToCallUser = [];\n\t\tpeerConnection.isCallStarted = false;\n\n\t\tcreateDesktopCapturerStream(\n\t\t\tpeerConnection,\n\t\t\tpeerConnection.desktopCapturerSourceID,\n\t\t)\n\t\t\t.then(() => {\n\t\t\t\t// if (peerConnection.peer === NullSimplePeer) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n\t\t\t\t// @ts-ignore\n\t\t\t\tpeerConnection.peer = new SimplePeer({\n\t\t\t\t\tinitiator: true,\n\t\t\t\t\t// trickle: false,\n\t\t\t\t\t// wrtc: window.api.wrtc,\n\t\t\t\t\tconfig: { iceServers: [] },\n\t\t\t\t\t// sdpTransform: simplePeerHandleSdpTransform,\n\t\t\t\t});\n\t\t\t\t// }\n\n\t\t\t\t// TODO: basically here we need a client side simple peer, but we get a nodejs side simple peer\n\t\t\t\tif (peerConnection.localStream !== null) {\n\t\t\t\t\tpeerConnection.peer.addStream(peerConnection.localStream);\n\t\t\t\t}\n\n\t\t\t\tpeerConnection.peer.on('signal', (data: string) => {\n\t\t\t\t\t// fired when simple peer and webrtc done preparation to start call on peerConnection machine\n\t\t\t\t\tpeerConnection.signalsDataToCallUser.push(data);\n\t\t\t\t});\n\n\t\t\t\tpeerConnection.peer.on('data', (data) => {\n\t\t\t\t\thandlePeerOnData(peerConnection, data);\n\t\t\t\t});\n\n\t\t\t\t// ensure cleanup on peer end/error to prevent dangling helper window\n\t\t\t\tpeerConnection.peer.on('close', () => {\n\t\t\t\t\tpeerConnection.selfDestroy();\n\t\t\t\t});\n\n\t\t\t\tpeerConnection.peer.on('error', (e: Error) => {\n\t\t\t\t\tconsole.error('peerConnection peer error', e);\n\t\t\t\t\tpeerConnection.selfDestroy();\n\t\t\t\t});\n\t\t\t\tresolve(undefined);\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tconsole.error(e);\n\t\t\t\treject();\n\t\t\t});\n\t});\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handlePeerOnData.ts",
    "content": "import DesktopCapturerSourceType from '../../../../common/DesktopCapturerSourceType';\nimport getDesktopSourceStreamBySourceID from './getDesktopSourceStreamBySourceID';\nimport prepareDataMessageToSendScreenSourceType from './prepareDataMessageToSendScreenSourceType';\nimport NullSimplePeer from './NullSimplePeer';\n\nexport default async function handlePeerOnData(\n\tpeerConnection: PeerConnection,\n\tdata: string,\n): Promise<void> {\n\tconst dataJSON = JSON.parse(data);\n\n\tif (dataJSON.type === 'set_video_quality') {\n\t\tconst maxVideoQualityMultiplier = dataJSON.payload.value;\n\t\tconst minVideoQualityMultiplier =\n\t\t\tmaxVideoQualityMultiplier === 1 ? 0.5 : maxVideoQualityMultiplier;\n\n\t\tif (\n\t\t\t!peerConnection.desktopCapturerSourceID.includes(\n\t\t\t\tDesktopCapturerSourceType.SCREEN,\n\t\t\t)\n\t\t)\n\t\t\treturn;\n\n\t\tconst newStream = await getDesktopSourceStreamBySourceID(\n\t\t\tpeerConnection.desktopCapturerSourceID,\n\t\t\tpeerConnection.sourceDisplaySize?.width,\n\t\t\tpeerConnection.sourceDisplaySize?.height,\n\t\t\tminVideoQualityMultiplier,\n\t\t\tmaxVideoQualityMultiplier,\n\t\t\t60,\n\t\t\t60,\n\t\t);\n\t\tconst newVideoTrack = newStream.getVideoTracks()[0];\n\t\tconst oldStream = peerConnection.localStream;\n\t\tconst oldTrack = oldStream?.getVideoTracks()[0];\n\n\t\tif (oldTrack && oldStream && peerConnection.peer !== NullSimplePeer) {\n\t\t\tawait peerConnection.peer.replaceTrack(\n\t\t\t\toldTrack,\n\t\t\t\tnewVideoTrack,\n\t\t\t\toldStream,\n\t\t\t);\n\t\t\t// stop only the old track (it's already removed from the stream by replaceTrack)\n\t\t\toldTrack.stop();\n\t\t\t// stop any remaining tracks in the old stream, but don't stop the new track\n\t\t\toldStream.getTracks().forEach((track) => {\n\t\t\t\tif (track.id !== newVideoTrack.id) {\n\t\t\t\t\ttrack.stop();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// update local stream reference to new stream\n\t\tpeerConnection.localStream = newStream;\n\t}\n\n\tif (dataJSON.type === 'get_sharing_source_type') {\n\t\tconst sourceType = peerConnection.desktopCapturerSourceID.includes(\n\t\t\tDesktopCapturerSourceType.SCREEN,\n\t\t)\n\t\t\t? DesktopCapturerSourceType.SCREEN\n\t\t\t: DesktopCapturerSourceType.WINDOW;\n\n\t\tpeerConnection.peer.send(\n\t\t\tprepareDataMessageToSendScreenSourceType(sourceType),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleSelfDestroy.ts",
    "content": "import { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport NullSimplePeer from './NullSimplePeer';\nimport NullUser from './NullUser';\n\nexport default function handleSelfDestroy(\n\tpeerConnection: PeerConnection,\n): void {\n\tpeerConnection.partner = NullUser;\n\twindow.electron.ipcRenderer.invoke(\n\t\tIpcEvents.DisconnectDeviceById,\n\t\tpeerConnection.partnerDeviceDetails.id,\n\t);\n\n\t// remove window event listener\n\tif (peerConnection.beforeunloadHandler) {\n\t\twindow.removeEventListener(\n\t\t\t'beforeunload',\n\t\t\tpeerConnection.beforeunloadHandler,\n\t\t);\n\t\tpeerConnection.beforeunloadHandler = null;\n\t}\n\n\t// cleanup peer connection and remove all event listeners\n\tif (peerConnection.peer !== NullSimplePeer) {\n\t\ttry {\n\t\t\t// remove all event listeners before destroying\n\t\t\tpeerConnection.peer.removeAllListeners();\n\t\t\tpeerConnection.peer.destroy();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error destroying peer:', error);\n\t\t}\n\t\tpeerConnection.peer = NullSimplePeer;\n\t}\n\n\t// cleanup media stream\n\tif (peerConnection.localStream) {\n\t\tpeerConnection.localStream.getTracks().forEach((track) => {\n\t\t\ttrack.stop();\n\t\t});\n\t\tpeerConnection.localStream = null;\n\t}\n\n\t// cleanup socket\n\tpeerConnection.socket.removeAllListeners();\n\tpeerConnection.socket.disconnect();\n\n\twindow.electron.ipcRenderer.invoke(\n\t\tIpcEvents.DestroySharingSessionById,\n\t\tpeerConnection.sharingSessionID,\n\t);\n\tpeerConnection.onDeviceConnectedCallback = () => {\n\t\t// reset callback after destruction\n\t};\n\tpeerConnection.isCallStarted = false;\n\n\twindow.electron.ipcRenderer.invoke(\n\t\tIpcEvents.UnmarkRoomIDAsTaken,\n\t\tpeerConnection.roomID,\n\t);\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleSetDisplaySizeFromLocalStream.ts",
    "content": "export default function setDisplaySizeFromLocalStream(\n\tpeerConnection: PeerConnection,\n): void {\n\tif (\n\t\t!peerConnection.localStream ||\n\t\t!peerConnection.localStream.getVideoTracks()[0]\n\t)\n\t\treturn;\n\tif (!peerConnection.localStream.getVideoTracks()[0].getSettings().width)\n\t\treturn;\n\tif (!peerConnection.localStream.getVideoTracks()[0].getSettings().height)\n\t\treturn;\n\tpeerConnection.sourceDisplaySize = {\n\t\twidth: peerConnection.localStream.getVideoTracks()[0].getSettings().width\n\t\t\t? (peerConnection.localStream.getVideoTracks()[0].getSettings()\n\t\t\t\t\t.width as number)\n\t\t\t: 640,\n\t\theight: peerConnection.localStream.getVideoTracks()[0].getSettings().height\n\t\t\t? (peerConnection.localStream.getVideoTracks()[0].getSettings()\n\t\t\t\t\t.height as number)\n\t\t\t: 480,\n\t};\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleSocket.ts",
    "content": "import handleSocketUserEnter from './handleSocketUserEnter';\nimport handleSocketUserExit from './handleSocketUserExit';\n\nexport default function handleSocket(peerConnection: PeerConnection): void {\n\tpeerConnection.socket.removeAllListeners();\n\n\tpeerConnection.socket.on('disconnect', () => {\n\t\tpeerConnection.selfDestroy();\n\t});\n\n\tpeerConnection.socket.on('error', (error: Error) => {\n\t\tconsole.error('peerConnection socket error', error);\n\t\tpeerConnection.selfDestroy();\n\t});\n\n\tpeerConnection.socket.on('connect', () => {\n\t\tpeerConnection.emitUserEnter();\n\t});\n\n\tpeerConnection.socket.on(\n\t\t'USER_ENTER',\n\t\t(payload: { users: PartnerPeerUser[] }) => {\n\t\t\thandleSocketUserEnter(peerConnection, payload);\n\t\t},\n\t);\n\n\tpeerConnection.socket.on('USER_EXIT', () => {\n\t\thandleSocketUserExit(peerConnection);\n\t});\n\n\tpeerConnection.socket.on(\n\t\t'MESSAGE',\n\t\t(payload: ReceiveEncryptedMessagePayload) => {\n\t\t\tpeerConnection.receiveEncryptedMessage(payload);\n\t\t},\n\t);\n\n\tpeerConnection.socket.on('USER_DISCONNECT', () => {\n\t\tpeerConnection.toggleLockRoom(false);\n\t});\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleSocketUserEnter.ts",
    "content": "export default (\n\tpeerConnection: PeerConnection,\n\tpayload: { users: PartnerPeerUser[] },\n): void => {\n\tconst filteredPartner = payload.users.filter((user: PartnerPeerUser) => {\n\t\treturn peerConnection.user.username !== user.username;\n\t});\n\n\tif (filteredPartner[0] === undefined) return;\n\n\t[peerConnection.partner] = filteredPartner;\n\n\tif (peerConnection.partner.username !== '') {\n\t\tpeerConnection.toggleLockRoom(true);\n\t\tpeerConnection.emitUserEnter();\n\t}\n};\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/handleSocketUserExit.ts",
    "content": "export default (peerConnection: PeerConnection): void => {\n\tif (peerConnection.isSocketRoomLocked) {\n\t\tpeerConnection.toggleLockRoom(false);\n\t\tif (peerConnection.isCallStarted) {\n\t\t\t// TODO: display toast device is gone ....\n\t\t\tpeerConnection.selfDestroy();\n\t\t}\n\t}\n};\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/index.ts",
    "content": "import { prepare as prepareMessage } from '../../utils/message';\nimport { connectSocket } from '../../../../common/connectSocket';\nimport handleCreatePeer from './handleCreatePeer';\nimport handleSocket from './handleSocket';\nimport { handleRecieveEncryptedMessage } from '../../utils/handleRecieveEncryptedMessage';\nimport handleSelfDestroy from './handleSelfDestroy';\nimport NullUser from './NullUser';\nimport NullSimplePeer from './NullSimplePeer';\nimport setDisplaySizeFromLocalStream from './handleSetDisplaySizeFromLocalStream';\nimport DesktopCapturerSourceType from '../../../../common/DesktopCapturerSourceType';\nimport getAppLanguage from '../../../../common/getAppLanguage';\nimport { IpcEvents } from '../../../../common/IpcEvents.enum';\nimport getDesktopSourceStreamBySourceID from './getDesktopSourceStreamBySourceID';\n\nimport { Device } from '../../../../common/Device';\nimport { LocalPeerUser } from '../../../../common/LocalPeerUser';\nimport type { SendEncryptedMessagePayload } from '../../../../common/SendEncryptedMessagePayload';\nimport { Socket } from 'socket.io-client';\n\ntype DisplaySize = { width: number; height: number };\n\nexport interface PartnerPeerUser {\n\tusername: string;\n}\n\nexport default class PeerConnection {\n\tsharingSessionID: string;\n\troomID: string;\n\tsocket: Socket;\n\tuser: LocalPeerUser;\n\tpartner: PartnerPeerUser;\n\tpeer = NullSimplePeer;\n\tdesktopCapturerSourceID: string;\n\tlocalStream: MediaStream | null;\n\tisSocketRoomLocked: boolean;\n\tpartnerDeviceDetails = {\n\t\tid: '',\n\t\tsharingSessionID: '',\n\t\tdeviceOS: '',\n\t\tdeviceType: '',\n\t\tdeviceIP: '',\n\t\tdeviceBrowser: '',\n\t\tdeviceScreenWidth: 0,\n\t\tdeviceScreenHeight: 0,\n\t} as Device;\n\tsignalsDataToCallUser: string[];\n\tisCallStarted: boolean;\n\tonDeviceConnectedCallback: (device: Device) => void;\n\tdisplayID: string;\n\tsourceDisplaySize: DisplaySize | undefined;\n\tbeforeunloadHandler: (() => void) | null = null;\n\n\tconstructor(\n\t\troomID: string,\n\t\tsharingSessionID: string,\n\t\tuser: LocalPeerUser,\n\t\tport: string,\n\t) {\n\t\tthis.sharingSessionID = sharingSessionID;\n\t\tthis.isSocketRoomLocked = false;\n\t\tthis.roomID = encodeURI(roomID);\n\t\tthis.socket = connectSocket(port, this.roomID);\n\t\tthis.user = user;\n\t\tthis.partner = NullUser;\n\t\tthis.desktopCapturerSourceID = '';\n\t\tthis.signalsDataToCallUser = [];\n\t\tthis.isCallStarted = false;\n\t\tthis.localStream = null;\n\t\tthis.displayID = '';\n\t\tthis.sourceDisplaySize = undefined;\n\t\tthis.onDeviceConnectedCallback = () => {\n\t\t\t// noop until UI layer registers callback\n\t\t};\n\n\t\thandleSocket(this);\n\n\t\tthis.beforeunloadHandler = () => {\n\t\t\tthis.socket.emit('USER_DISCONNECT');\n\t\t};\n\t\twindow.addEventListener('beforeunload', this.beforeunloadHandler);\n\t}\n\n\tnotifyClientWithNewLanguage(): void {\n\t\tsetTimeout(async () => {\n\t\t\tthis.sendEncryptedMessage({\n\t\t\t\ttype: 'APP_LANGUAGE',\n\t\t\t\tpayload: {\n\t\t\t\t\tvalue: await getAppLanguage(),\n\t\t\t\t},\n\t\t\t});\n\t\t}, 1000);\n\t}\n\n\tasync setDesktopCapturerSourceID(id: string): Promise<void> {\n\t\tthis.desktopCapturerSourceID = id;\n\t\tif (process.env.RUN_MODE === 'test') return;\n\n\t\t// clear old display size when switching sources to ensure new source uses correct dimensions\n\t\tthis.sourceDisplaySize = undefined;\n\t\tthis.displayID = '';\n\n\t\tawait this.setDisplayIDByDesktopCapturerSourceID();\n\n\t\tawait this.handleCreatePeerAfterDesktopCapturerSourceIDWasSet();\n\t}\n\n\tasync setDisplayIDByDesktopCapturerSourceID(): Promise<void> {\n\t\tif (\n\t\t\t!this.desktopCapturerSourceID.includes(DesktopCapturerSourceType.SCREEN)\n\t\t) {\n\t\t\t// clear display size for window sources\n\t\t\tthis.sourceDisplaySize = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.displayID = await window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.GetSourceDisplayIDByDesktopCapturerSourceID,\n\t\t\tthis.desktopCapturerSourceID,\n\t\t);\n\n\t\tif (this.displayID !== '') {\n\t\t\t// await to ensure sourceDisplaySize is set before creating stream\n\t\t\tawait this.setDisplaySizeRetreivedFromMainProcess();\n\t\t}\n\t}\n\n\tasync setDisplaySizeRetreivedFromMainProcess(): Promise<void> {\n\t\tconst size: DisplaySize | 'undefined' =\n\t\t\tawait window.electron.ipcRenderer.invoke(\n\t\t\t\t'get-display-size-by-display-id',\n\t\t\t\tthis.displayID,\n\t\t\t);\n\t\tif (size !== 'undefined') {\n\t\t\tthis.sourceDisplaySize = size;\n\t\t}\n\t}\n\n\tasync handleCreatePeerAfterDesktopCapturerSourceIDWasSet(): Promise<void> {\n\t\t// if peer already exists, replace the track instead of creating a new peer\n\t\tif (this.peer !== NullSimplePeer && this.localStream) {\n\t\t\ttry {\n\t\t\t\tconst oldTrack = this.localStream.getVideoTracks()[0];\n\t\t\t\tif (!oldTrack) {\n\t\t\t\t\tawait this.createPeer();\n\t\t\t\t\tif (!this.sourceDisplaySize) {\n\t\t\t\t\t\tsetDisplaySizeFromLocalStream(this);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst newStream = await getDesktopSourceStreamBySourceID(\n\t\t\t\t\tthis.desktopCapturerSourceID,\n\t\t\t\t\tthis.sourceDisplaySize?.width,\n\t\t\t\t\tthis.sourceDisplaySize?.height,\n\t\t\t\t\t0.5,\n\t\t\t\t\t1,\n\t\t\t\t);\n\t\t\t\tconst newVideoTrack = newStream.getVideoTracks()[0];\n\n\t\t\t\tif (!newVideoTrack) {\n\t\t\t\t\tawait this.createPeer();\n\t\t\t\t\tif (!this.sourceDisplaySize) {\n\t\t\t\t\t\tsetDisplaySizeFromLocalStream(this);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// store reference to old stream before replacement\n\t\t\t\tconst oldStream = this.localStream;\n\n\t\t\t\t// replace the track in the existing peer\n\t\t\t\t// replaceTrack will add the new track to the old stream\n\t\t\t\tawait this.peer.replaceTrack(oldTrack, newVideoTrack, oldStream);\n\n\t\t\t\t// stop only the old track (it's already removed from the stream by replaceTrack)\n\t\t\t\toldTrack.stop();\n\n\t\t\t\t// stop any remaining tracks in the old stream (should be none, but just in case)\n\t\t\t\t// but don't stop the new track that was just added by replaceTrack\n\t\t\t\toldStream.getTracks().forEach((track) => {\n\t\t\t\t\tif (track.id !== newVideoTrack.id) {\n\t\t\t\t\t\ttrack.stop();\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\t// update local stream reference to the new stream\n\t\t\t\t// the new stream's track is now being used in the peer connection\n\t\t\t\tthis.localStream = newStream;\n\n\t\t\t\t// update sourceDisplaySize from actual stream to ensure correct resolution\n\t\t\t\t// this is critical when switching sources to get the actual stream dimensions\n\t\t\t\tif (\n\t\t\t\t\tthis.desktopCapturerSourceID.includes(\n\t\t\t\t\t\tDesktopCapturerSourceType.SCREEN,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tsetDisplaySizeFromLocalStream(this);\n\t\t\t\t} else {\n\t\t\t\t\t// clear for window sources\n\t\t\t\t\tthis.sourceDisplaySize = undefined;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to replace stream track:', error);\n\t\t\t\t// fallback to creating a new peer if replacement fails\n\t\t\t\tawait this.createPeer();\n\t\t\t\tif (!this.sourceDisplaySize) {\n\t\t\t\t\tsetDisplaySizeFromLocalStream(this);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tawait this.createPeer();\n\t\t\tif (!this.sourceDisplaySize) {\n\t\t\t\tsetDisplaySizeFromLocalStream(this);\n\t\t\t}\n\t\t}\n\t}\n\n\tsetOnDeviceConnectedCallback(callback: (device: Device) => void): void {\n\t\tthis.onDeviceConnectedCallback = callback;\n\t}\n\n\tasync denyConnectionForPartner(): Promise<void> {\n\t\tawait this.sendEncryptedMessage({\n\t\t\ttype: 'DENY_TO_CONNECT',\n\t\t\tpayload: {},\n\t\t});\n\t\tthis.disconnectPartner();\n\t}\n\n\tsendUserAllowedToConnect(): void {\n\t\tthis.sendEncryptedMessage({\n\t\t\ttype: 'ALLOWED_TO_CONNECT',\n\t\t\tpayload: {},\n\t\t});\n\t}\n\n\tasync disconnectByHostMachineUser(deviceId: string): Promise<void> {\n\t\tif (this.partnerDeviceDetails.id !== deviceId) {\n\t\t\treturn;\n\t\t}\n\t\tawait this.sendEncryptedMessage({\n\t\t\ttype: 'DISCONNECT_BY_HOST_MACHINE_USER',\n\t\t\tpayload: {},\n\t\t});\n\t\tthis.disconnectPartner();\n\t\tthis.selfDestroy();\n\t}\n\n\tdisconnectPartner(): void {\n\t\tthis.socket.emit('DISCONNECT_SOCKET_BY_DEVICE_IP', {\n\t\t\tip: this.partnerDeviceDetails.deviceIP,\n\t\t});\n\n\t\tthis.partnerDeviceDetails = {} as Device;\n\t}\n\n\tselfDestroy(): void {\n\t\thandleSelfDestroy(this);\n\t}\n\n\temitUserEnter(): void {\n\t\tthis.socket.emit('USER_ENTER', {\n\t\t\tusername: this.user.username,\n\t\t});\n\t}\n\n\tasync sendEncryptedMessage(\n\t\tpayload: SendEncryptedMessagePayload,\n\t): Promise<void> {\n\t\tif (!this.socket) return;\n\t\tif (!this.user) return;\n\t\tif (!this.partner) return;\n\t\tif (!this.partner.username) return;\n\t\tconst msg = await prepareMessage(payload, this.user);\n\t\tthis.socket.emit('MESSAGE', msg.toSend);\n\t}\n\n\treceiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload): void {\n\t\tif (!this.user) return;\n\t\thandleRecieveEncryptedMessage(this, payload);\n\t}\n\n\tcallPeer(): void {\n\t\tif (process.env.RUN_MODE === 'test') return;\n\t\tif (this.isCallStarted) return;\n\t\tthis.isCallStarted = true;\n\n\t\tthis.signalsDataToCallUser.forEach((data: string) => {\n\t\t\tthis.sendEncryptedMessage({\n\t\t\t\ttype: 'CALL_USER',\n\t\t\t\tpayload: {\n\t\t\t\t\tsignalData: data,\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t}\n\n\tcreatePeer(): Promise<void> {\n\t\treturn handleCreatePeer(this);\n\t}\n\n\ttoggleLockRoom(isConnected: boolean): void {\n\t\tthis.socket.emit('TOGGLE_LOCK_ROOM');\n\t\tthis.isSocketRoomLocked = isConnected;\n\t}\n}\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/prepareDataMessageToSendScreenSourceType.ts",
    "content": "export default (s: string): string => {\n\treturn `\n    {\n      \"type\": \"screen_sharing_source_type\",\n      \"payload\": {\n        \"value\": \"${s}\"\n      }\n    }\n  `;\n};\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/setSdpMediaBitrate.ts",
    "content": "export default (sdp: string, mediaType: string, bitrate: number): string => {\n\tconst sdpLines = sdp.split('\\n');\n\tlet mediaLineIndex = -1;\n\tconst mediaLine = `m=${mediaType}`;\n\tlet bitrateLineIndex = -1;\n\tconst bitrateLine = `b=AS:${bitrate}`;\n\tmediaLineIndex = sdpLines.findIndex((line) => line.startsWith(mediaLine));\n\n\t// If we find a line matching “m={mediaType}”\n\tif (mediaLineIndex && mediaLineIndex < sdpLines.length) {\n\t\t// Skip the media line\n\t\tbitrateLineIndex = mediaLineIndex + 1;\n\n\t\t// Skip both i=* and c=* lines (bandwidths limiters have to come afterwards)\n\t\twhile (\n\t\t\tsdpLines[bitrateLineIndex].startsWith('i=') ||\n\t\t\tsdpLines[bitrateLineIndex].startsWith('c=')\n\t\t) {\n\t\t\tbitrateLineIndex += 1;\n\t\t}\n\n\t\tif (sdpLines[bitrateLineIndex].startsWith('b=')) {\n\t\t\t// If the next line is a b=* line, replace it with our new bandwidth\n\t\t\tsdpLines[bitrateLineIndex] = bitrateLine;\n\t\t} else {\n\t\t\t// Otherwise insert a new bitrate line.\n\t\t\tsdpLines.splice(bitrateLineIndex, 0, bitrateLine);\n\t\t}\n\t}\n\n\t// Then return the updated sdp content as a string\n\treturn sdpLines.join('\\n');\n};\n"
  },
  {
    "path": "src/renderer/src/features/PeerConnection/simplePeerHandleSdpTransform.ts",
    "content": "import setSdpMediaBitrate from './setSdpMediaBitrate';\n\nexport default (sdp: string): string => {\n\tlet newSDP = sdp;\n\tnewSDP = setSdpMediaBitrate(newSDP, 'video', 500000);\n\treturn newSDP;\n};\n"
  },
  {
    "path": "src/renderer/src/main.tsx",
    "content": "// override console early to catch all logs in renderer\nimport {\n\toverrideGlobalConsole,\n\tstartConsoleRateLimiting,\n} from '../../common/rateLimitedConsole';\noverrideGlobalConsole();\nstartConsoleRateLimiting();\n\nimport './assets/main.css';\nimport { createRoot } from 'react-dom/client';\nimport { StrictMode, Suspense } from 'react';\nimport Root from './containers/Root';\nimport { i18nInitPromise } from './configs/i18next.config.client';\n\ndocument.addEventListener('DOMContentLoaded', () => {\n\tconst windowTopBar = document.createElement('div');\n\twindowTopBar.style.width = '75%';\n\twindowTopBar.style.height = '50px';\n\twindowTopBar.style.position = 'absolute';\n\twindowTopBar.style.top = '0';\n\twindowTopBar.style.left = '0';\n\t// @ts-ignore: all good here\n\twindowTopBar.style.webkitAppRegion = 'drag';\n\twindowTopBar.style.pointerEvents = 'none';\n\n\tdocument.body.appendChild(windowTopBar);\n});\n\ni18nInitPromise.then(() => {\n\tcreateRoot(document.getElementById('root')!).render(\n\t\t<StrictMode>\n\t\t\t<Suspense fallback=\"loading\">\n\t\t\t\t<Root />\n\t\t\t</Suspense>\n\t\t</StrictMode>,\n\t);\n});\n// });\n\nwindow.onbeforeunload = () => {\n\twindow.electron.ipcRenderer.invoke('main-window-onbeforeunload');\n};\n"
  },
  {
    "path": "src/renderer/src/peerConnectionHelperRendererWindowIndex.tsx",
    "content": "// override console early to catch all logs in helper renderer\nimport {\n\toverrideGlobalConsole,\n\tstartConsoleRateLimiting,\n} from '../../common/rateLimitedConsole';\noverrideGlobalConsole();\nstartConsoleRateLimiting();\n\nimport { IpcEvents } from '../../common/IpcEvents.enum';\nimport PeerConnection from './features/PeerConnection';\n\nconst loadDevelopmentText = (): void => {\n\tconst root = document.getElementById('root');\n\tif (root) {\n\t\tconst h1 = document.createElement('h1');\n\t\th1.textContent =\n\t\t\t'This is a client connection WebRTC electron renderer helper window.';\n\t\troot.appendChild(h1);\n\n\t\tconst h2Mode = document.createElement('h2');\n\t\th2Mode.textContent = 'It is shown only in Dev mode';\n\t\troot.appendChild(h2Mode);\n\n\t\tconst h2F12 = document.createElement('h2');\n\t\th2F12.textContent =\n\t\t\t'Press F12 to open Development Tools for this renderer window to debug webrtc with one connected client.';\n\t\troot.appendChild(h2F12);\n\t} else {\n\t\tconsole.error('Root element not found.');\n\t}\n};\n\nexport function handleIpcRenderer(): void {\n\twindow.electron.ipcRenderer.on('start-peer-connection', () => {\n\t\tlet peerConnection: PeerConnection | undefined;\n\n\t\twindow.electron.ipcRenderer.on(\n\t\t\t'create-peer-connection-with-data',\n\t\t\tasync (_, data) => {\n\t\t\t\t// cleanup existing peer connection before creating new one\n\t\t\t\tif (peerConnection) {\n\t\t\t\t\tpeerConnection.selfDestroy();\n\t\t\t\t\tpeerConnection = undefined;\n\t\t\t\t}\n\n\t\t\t\tconst port = await window.electron.ipcRenderer.invoke(\n\t\t\t\t\tIpcEvents.GetPort,\n\t\t\t\t);\n\t\t\t\tpeerConnection = new PeerConnection(\n\t\t\t\t\tdata.roomID,\n\t\t\t\t\tdata.sharingSessionID,\n\t\t\t\t\tdata.user,\n\t\t\t\t\tport,\n\t\t\t\t);\n\n\t\t\t\tpeerConnection.setOnDeviceConnectedCallback((deviceData) => {\n\t\t\t\t\twindow.electron.ipcRenderer.send('peer-connected', deviceData);\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\n\t\twindow.electron.ipcRenderer.on(\n\t\t\t'set-desktop-capturer-source-id',\n\t\t\t(_, id) => {\n\t\t\t\tif (peerConnection) {\n\t\t\t\t\tpeerConnection.setDesktopCapturerSourceID(id);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\twindow.electron.ipcRenderer.on('call-peer', () => {\n\t\t\tif (peerConnection) {\n\t\t\t\tpeerConnection.callPeer();\n\t\t\t}\n\t\t});\n\n\t\twindow.electron.ipcRenderer.on(\n\t\t\t'disconnect-by-host-machine-user',\n\t\t\t(_, deviceId: string) => {\n\t\t\t\tif (peerConnection) {\n\t\t\t\t\tpeerConnection.disconnectByHostMachineUser(deviceId);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\twindow.electron.ipcRenderer.on('deny-connection-for-partner', () => {\n\t\t\tif (peerConnection) {\n\t\t\t\tpeerConnection.denyConnectionForPartner();\n\t\t\t}\n\t\t});\n\n\t\twindow.electron.ipcRenderer.on('send-user-allowed-to-connect', () => {\n\t\t\tif (peerConnection) {\n\t\t\t\tpeerConnection.sendUserAllowedToConnect();\n\t\t\t}\n\t\t});\n\n\t\twindow.electron.ipcRenderer.on('app-language-changed', () => {\n\t\t\tif (peerConnection) {\n\t\t\t\tpeerConnection.notifyClientWithNewLanguage();\n\t\t\t}\n\t\t});\n\t});\n}\n\nhandleIpcRenderer();\n\nloadDevelopmentText();\n"
  },
  {
    "path": "src/renderer/src/utils/handleRecieveEncryptedMessage.ts",
    "content": "import { process as processMessage } from './message';\nimport { IpcEvents } from '../../../common/IpcEvents.enum';\n\nexport type CallAcceptedMessageWithPayload = {\n\ttype: 'CALL_ACCEPTED';\n\tpayload: {\n\t\tsignalData: string;\n\t};\n};\n\nexport type CallUserMessageWithPayload = {\n\ttype: 'CALL_USER';\n\tpayload: {\n\t\tsignalData: string;\n\t};\n};\n\nexport type DeviceDetailsMessageWithPayload = {\n\ttype: 'DEVICE_DETAILS';\n\tpayload: {\n\t\tdeviceType: string;\n\t\tos: string;\n\t\tbrowser: string;\n\t\tdeviceScreenWidth: number;\n\t\tdeviceScreenHeight: number;\n\t};\n};\n\nexport type GetAppLanguageMessageWithPayload = {\n\ttype: 'GET_APP_LANGUAGE';\n\tpayload: Record<string, unknown>;\n};\n\nexport type AppLanguageMessageWithPayload = {\n\ttype: 'APP_LANGUAGE';\n\tpayload: {\n\t\tvalue: string;\n\t};\n};\n\nexport type DenyToConnectMessageWithPayload = {\n\ttype: 'DENY_TO_CONNECT';\n\tpayload: Record<string, unknown>;\n};\n\nexport type AllowedToConnectMessageWithPayload = {\n\ttype: 'ALLOWED_TO_CONNECT';\n\tpayload: Record<string, unknown>;\n};\n\nexport type DisconnectByHostMachineUserMessageWithPayload = {\n\ttype: 'DISCONNECT_BY_HOST_MACHINE_USER';\n\tpayload: Record<string, unknown>;\n};\n\nexport type ProcessedMessage =\n\t| CallAcceptedMessageWithPayload\n\t| CallUserMessageWithPayload\n\t| DeviceDetailsMessageWithPayload\n\t| GetAppLanguageMessageWithPayload\n\t| AppLanguageMessageWithPayload\n\t| DenyToConnectMessageWithPayload\n\t| AllowedToConnectMessageWithPayload\n\t| DisconnectByHostMachineUserMessageWithPayload;\n\nexport function handleDeviceIPMessage(\n\tdeviceIP: string,\n\tpeerConnection: PeerConnection,\n\tmessage: ProcessedMessage,\n): void {\n\tif (message.type !== 'DEVICE_DETAILS') return;\n\tconst device = {\n\t\tid: Math.random().toString(),\n\t\tdeviceIP,\n\t\tdeviceType: message.payload.deviceType,\n\t\tdeviceOS: message.payload.os,\n\t\tdeviceBrowser: message.payload.browser,\n\t\tdeviceScreenWidth: message.payload.deviceScreenWidth,\n\t\tdeviceScreenHeight: message.payload.deviceScreenHeight,\n\t\tsharingSessionID: peerConnection.sharingSessionID,\n\t\tdeviceRoomId: peerConnection.roomID,\n\t};\n\tpeerConnection.partnerDeviceDetails = device;\n\tpeerConnection.onDeviceConnectedCallback(device);\n}\n\nexport const handleRecieveEncryptedMessage = async (\n\tpeerConnection: PeerConnection,\n\tpayload: ReceiveEncryptedMessagePayload,\n): Promise<void> => {\n\tlet message: ProcessedMessage;\n\ttry {\n\t\tmessage = await processMessage(payload);\n\t} catch (e) {\n\t\tconsole.error('failed to process incoming message', e, payload);\n\t\treturn;\n\t}\n\t// const message = payload as any;\n\tif (message.type === 'CALL_ACCEPTED') {\n\t\tpeerConnection.peer.signal(message.payload.signalData);\n\t}\n\tif (message.type === 'DEVICE_DETAILS') {\n\t\tpeerConnection.socket.emit(\n\t\t\t'GET_IP_BY_SOCKET_ID',\n\t\t\tpayload.fromSocketID,\n\t\t\t(deviceIP: string) => {\n\t\t\t\t// TODO: need to add myIP in client message.payload.myIP, then if retrieved deviceIP and myIP from client don't match, we were spoofed, then we can interrupt connection immediately!\n\t\t\t\thandleDeviceIPMessage(deviceIP, peerConnection, message);\n\t\t\t},\n\t\t);\n\t}\n\tif (message.type === 'GET_APP_LANGUAGE') {\n\t\tconst appLanguage = await window.electron.ipcRenderer.invoke(\n\t\t\tIpcEvents.GetAppLanguage,\n\t\t);\n\t\tpeerConnection.sendEncryptedMessage({\n\t\t\ttype: 'APP_LANGUAGE',\n\t\t\tpayload: {\n\t\t\t\tvalue: appLanguage,\n\t\t\t},\n\t\t});\n\t}\n};\n"
  },
  {
    "path": "src/renderer/src/utils/message.ts",
    "content": "import { ProcessedMessage } from './handleRecieveEncryptedMessage';\nimport { LocalPeerUser } from '../../../common/LocalPeerUser';\nimport type { SendEncryptedMessagePayload } from '../../../common/SendEncryptedMessagePayload';\n\ninterface ProcessedPayload {\n\ttoSend: ProcessedMessage;\n\toriginal: ProcessedMessage;\n}\n\nexport const process = (\n\tpayload: ReceiveEncryptedMessagePayload,\n): Promise<ProcessedMessage> => {\n\treturn Promise.resolve(payload as ProcessedMessage);\n};\n\nexport const prepare = (\n\tpayload: SendEncryptedMessagePayload,\n\tuser: LocalPeerUser,\n): Promise<ProcessedPayload> =>\n\tnew Promise<ProcessedPayload>((resolve): void => {\n\t\tconst myUsername = user.username;\n\t\tconst myId = user.id;\n\t\tconst innerPayload = { ...payload.payload } as Record<string, unknown>;\n\t\tif (typeof (innerPayload as { text?: unknown }).text === 'string') {\n\t\t\t(innerPayload as { text?: string }).text = encodeURI(\n\t\t\t\t(innerPayload as { text?: string }).text as string,\n\t\t\t);\n\t\t}\n\t\tconst jsonToSend = {\n\t\t\t...payload,\n\t\t\tpayload: {\n\t\t\t\t...innerPayload,\n\t\t\t\tsender: myId,\n\t\t\t\tusername: myUsername,\n\t\t\t},\n\t\t} as ProcessedMessage;\n\n\t\tresolve({\n\t\t\ttoSend: jsonToSend,\n\t\t\toriginal: jsonToSend,\n\t\t});\n\t});\n"
  },
  {
    "path": "src/renderer/src/utils/showMessageFromNewToaster.ts",
    "content": "import { Intent, OverlayToaster } from '@blueprintjs/core';\nimport { createRoot, Root } from 'react-dom/client';\n\nexport async function showMessageFromNewToaster(\n\tmsg: string,\n\tintent: Intent = Intent.PRIMARY,\n): Promise<void> {\n\tconst container = document.createElement('div');\n\tlet root: Root | undefined;\n\t// Since this toaster isn't created in a portal, a fixed position container\n\t// is required for it to show at the top of the viewport. Otherwise the\n\t// toaster won't be visible until the user scrolls upward.\n\tcontainer.style.position = 'fixed';\n\tcontainer.style.top = '0';\n\tcontainer.style.width = '100%';\n\n\tdocument.body.appendChild(container);\n\n\treturn new Promise<void>((resolve, reject) => {\n\t\tconst onDismiss = async () => {\n\t\t\tresolve();\n\n\t\t\t// Wait for the message to fade out before completely unmounting the OverlayToaster.\n\t\t\tawait new Promise((resolveClose) => setTimeout(resolveClose, 5000));\n\n\t\t\troot?.unmount();\n\t\t\troot = undefined;\n\t\t\tdocument.body.removeChild(container);\n\t\t};\n\n\t\tOverlayToaster.create(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tcontainer,\n\t\t\t\tdomRenderer: (element, domContainer) => {\n\t\t\t\t\troot = createRoot(domContainer);\n\t\t\t\t\troot.render(element);\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\t\t.then((toaster) => {\n\t\t\t\ttoaster.show({ intent, message: msg, onDismiss });\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tdocument.body.removeChild(container);\n\t\t\t\treject(error);\n\t\t\t});\n\t});\n}\n"
  },
  {
    "path": "src/server/Room.d.ts",
    "content": "interface Room {\n\tid: string;\n\tusers: User[];\n\tisLocked: boolean;\n\tcreatedAt: number;\n}\n"
  },
  {
    "path": "src/server/RoomIDService/index.ts",
    "content": "import crypto from 'crypto';\nimport shortID from 'shortid';\n\nexport default class RoomIDService {\n\ttakenRoomIDs: Set<string>;\n\n\tnextSimpleRoomID: number;\n\n\tconstructor() {\n\t\tthis.takenRoomIDs = new Set<string>();\n\t\tthis.nextSimpleRoomID = 1;\n\t\t// TODO: load saved taken room ids from local storage, will be useful for saved devices feature in FUTURE\n\t}\n\n\tgetSimpleAvailableRoomID(): Promise<string> {\n\t\tthis.nextSimpleRoomID += 1;\n\t\treturn new Promise<string>((resolve, reject) => {\n\t\t\tcrypto.randomBytes(3, (error, buffer) => {\n\t\t\t\tif (error) {\n\t\t\t\t\treject(error);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst decimalString = parseInt(buffer.toString('hex'), 16)\n\t\t\t\t\t.toString()\n\t\t\t\t\t.padStart(6, '0');\n\t\t\t\tresolve(decimalString.substring(0, 6));\n\t\t\t});\n\t\t});\n\t}\n\n\tgetShortIDStringOfAvailableRoom(): Promise<string> {\n\t\treturn new Promise<string>((resolve) => {\n\t\t\tlet newID = shortID();\n\t\t\twhile (this.takenRoomIDs.has(newID)) {\n\t\t\t\tnewID = shortID();\n\t\t\t}\n\t\t\tresolve(newID);\n\t\t});\n\t}\n\n\tmarkRoomIDAsTaken(id: string): void {\n\t\tthis.takenRoomIDs.add(id);\n\t}\n\n\tunmarkRoomIDAsTaken(id: string): void {\n\t\tthis.takenRoomIDs.delete(id);\n\t}\n\n\tisRoomIDTaken(id: string): boolean {\n\t\treturn this.takenRoomIDs.has(id);\n\t}\n}\n"
  },
  {
    "path": "src/server/darkwireSocket.ts",
    "content": "/*\n * original JS code from darkwire.io\n * translated and adapted to typescript for Deskreen CE app\n * */\n\n/* eslint-disable no-async-promise-executor */\nimport _ from 'lodash';\nimport Io from 'socket.io';\nimport socketsIPService from './socketsIPService';\nimport getStore from './store';\nimport socketIOServerStore from './store/socketIOServerStore';\n\nconst LOCALHOST_SOCKET_IP = '127.0.0.1';\n\ninterface SocketOPTS {\n\troomId: string;\n\tsocket: Io.Socket;\n\troom: Room;\n\troomIdOriginal: string;\n}\n\nfunction isLocalhostSocket(socket: Io.Socket) {\n\tconst remoteAddress = socket.request.socket.remoteAddress ?? '';\n\treturn remoteAddress.includes(LOCALHOST_SOCKET_IP);\n}\n\nexport default class Socket implements SocketOPTS {\n\troomId: string;\n\n\tsocket: Io.Socket;\n\n\troom: Room;\n\n\troomIdOriginal: string;\n\n\tconstructor(opts: SocketOPTS) {\n\t\tconst { roomId, socket, room, roomIdOriginal } = opts;\n\n\t\tthis.roomId = roomId;\n\t\tthis.socket = socket;\n\t\tthis.roomIdOriginal = roomIdOriginal;\n\t\tthis.room = room;\n\t\tif (room.isLocked) {\n\t\t\tthis.sendRoomLocked();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.init();\n\t}\n\n\tasync init(): Promise<void> {\n\t\tawait this.joinRoom();\n\t\tthis.handleSocket();\n\t}\n\n\tsendRoomLocked(): void {\n\t\tthis.socket.emit('ROOM_LOCKED');\n\t}\n\n\tasync saveRoom(room: Room): Promise<number> {\n\t\tconst json = {\n\t\t\t...room,\n\t\t\tupdatedAt: Date.now(),\n\t\t};\n\t\treturn getStore().set('rooms', this.roomId, JSON.stringify(json));\n\t}\n\n\tasync destroyRoom(): Promise<0 | 1> {\n\t\treturn getStore().del('rooms', this.roomId);\n\t}\n\n\tasync fetchRoom(): Promise<unknown> {\n\t\tconst res = await getStore().get('rooms', this.roomId);\n\t\tif (typeof res !== 'string') {\n\t\t\treturn {};\n\t\t}\n\t\treturn JSON.parse(res);\n\t}\n\n\tjoinRoom(): Promise<unknown> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.socket.join(this.roomId);\n\t\t\tresolve(undefined);\n\t\t});\n\t}\n\n\thandleSocket(): void {\n\t\tthis.socket.on('GET_MY_IP', (acknowledgeFunction) => {\n\t\t\tacknowledgeFunction(socketsIPService.getSocketIPByID(this.socket.id));\n\t\t});\n\n\t\tthis.socket.on('GET_IP_BY_SOCKET_ID', (socketID, acknowledgeFunction) => {\n\t\t\tif (!isLocalhostSocket(this.socket)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tacknowledgeFunction(socketsIPService.getSocketIPByID(socketID));\n\t\t});\n\n\t\tthis.socket.on('IS_ROOM_LOCKED', async (acknowledgeFunction) => {\n\t\t\tconst room: Room = (await this.fetchRoom()) as Room;\n\t\t\tacknowledgeFunction(room.isLocked);\n\t\t});\n\n\t\tthis.socket.on('PING', (acknowledgeFunction) => {\n\t\t\tacknowledgeFunction('PONG');\n\t\t});\n\n\t\tthis.socket.on('MESSAGE', (payload) => {\n\t\t\tpayload.fromSocketID = this.socket.id;\n\t\t\tthis.socket.to(this.roomId).emit('MESSAGE', payload);\n\t\t});\n\n\t\tthis.socket.on('DISCONNECT_SOCKET_BY_DEVICE_IP', async (payload) => {\n\t\t\tconst room: Room = (await this.fetchRoom()) as Room;\n\t\t\tconst ownerUser = (room.users || []).find(\n\t\t\t\t(u) => u.socketId === this.socket.id && u.isOwner,\n\t\t\t);\n\t\t\tif (!ownerUser) return;\n\t\t\tconst socketIDToDisconnect = socketsIPService.getSocketIDByIP(payload.ip);\n\t\t\tif (!socketIDToDisconnect) return;\n\n\t\t\tthis.handleDisconnect(\n\t\t\t\tsocketIOServerStore.getServer().sockets.sockets[socketIDToDisconnect],\n\t\t\t);\n\t\t});\n\n\t\tthis.socket.on('USER_ENTER', async (payload) => {\n\t\t\tlet room: Room = (await this.fetchRoom()) as Room;\n\t\t\tif (_.isEmpty(room)) {\n\t\t\t\troom = {\n\t\t\t\t\tid: this.roomId,\n\t\t\t\t\tusers: [],\n\t\t\t\t\tisLocked: false,\n\t\t\t\t\tcreatedAt: Date.now(),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tconst userFound = room.users.find(\n\t\t\t\t\t(r) => r.username === payload.username,\n\t\t\t\t);\n\t\t\t\tif (userFound) return;\n\t\t\t}\n\n\t\t\tconst isOwnerSocket = isLocalhostSocket(this.socket);\n\t\t\tif (!isOwnerSocket) {\n\t\t\t\tconst connectedViewers = (room.users || []).filter((user) => {\n\t\t\t\t\treturn !user.isOwner;\n\t\t\t\t});\n\t\t\t\tif (connectedViewers.length >= 1) {\n\t\t\t\t\tthis.socket.emit('NOT_ALLOWED');\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tthis.socket.disconnect(true);\n\t\t\t\t\t}, 0);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst newRoom: Room = {\n\t\t\t\t...room,\n\t\t\t\tusers: [\n\t\t\t\t\t...(room.users || []),\n\t\t\t\t\t{\n\t\t\t\t\t\tsocketId: this.socket.id,\n\t\t\t\t\t\tusername: payload.username,\n\t\t\t\t\t\tisOwner: isOwnerSocket,\n\t\t\t\t\t\tip: payload.ip ? payload.ip : '', // TODO: remove as it is not used\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t\tawait this.saveRoom(newRoom);\n\n\t\t\tsocketIOServerStore\n\t\t\t\t.getServer()\n\t\t\t\t.to(this.roomId)\n\t\t\t\t.emit('USER_ENTER', {\n\t\t\t\t\t...newRoom,\n\t\t\t\t\tid: this.roomIdOriginal,\n\t\t\t\t});\n\t\t});\n\n\t\tthis.socket.on('TOGGLE_LOCK_ROOM', async () => {\n\t\t\t// TODO: in here if there is somehow already more than ONE client connected, then we were spoofed! Need to add code to interrupt connection immediately.\n\t\t\tconst room: Room = (await this.fetchRoom()) as Room;\n\t\t\tconst user = (room.users || []).find(\n\t\t\t\t(u) => u.socketId === this.socket.id && u.isOwner,\n\t\t\t);\n\n\t\t\tif (!user) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.saveRoom({\n\t\t\t\t...room,\n\t\t\t\tisLocked: !room.isLocked,\n\t\t\t});\n\t\t});\n\n\t\tthis.socket.on('disconnect', () => {\n\t\t\tthis.handleDisconnect(this.socket);\n\t\t});\n\n\t\tthis.socket.on('USER_DISCONNECT', () => {\n\t\t\tthis.handleDisconnect(this.socket);\n\t\t});\n\t}\n\n\tasync handleDisconnect(socket: Io.Socket): Promise<void> {\n\t\tconst room: Room = (await this.fetchRoom()) as Room;\n\t\tconst isOwnerUser = !!(room.users || []).find(\n\t\t\t(u) => u.socketId === socket.id && u.isOwner,\n\t\t);\n\n\t\tconst newRoom = {\n\t\t\t...room,\n\t\t\tusers: (room.users || [])\n\t\t\t\t.filter((u) => u.socketId !== socket.id)\n\t\t\t\t.map((u, index) => ({\n\t\t\t\t\t...u,\n\t\t\t\t\tisOwner: index === 0,\n\t\t\t\t})),\n\t\t};\n\n\t\tif (isOwnerUser) {\n\t\t\tthis.disconnectAllUsers(newRoom);\n\t\t\tawait this.destroyRoom();\n\t\t} else {\n\t\t\tawait this.saveRoom(newRoom);\n\t\t}\n\n\t\tsocketIOServerStore\n\t\t\t.getServer()\n\t\t\t.to(this.roomId)\n\t\t\t.emit('USER_EXIT', newRoom.users);\n\n\t\tsocket.disconnect(true);\n\t}\n\n\tdisconnectAllUsers(room: Room): void {\n\t\troom.users.forEach((u) => {\n\t\t\tif (socketIOServerStore.getServer().sockets.sockets[u.socketId]) {\n\t\t\t\tsocketIOServerStore\n\t\t\t\t\t.getServer()\n\t\t\t\t\t.sockets.sockets[u.socketId].disconnect();\n\t\t\t}\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/server/getClientViewerDistPath.ts",
    "content": "import { existsSync } from 'fs';\nimport { join, resolve } from 'path';\nimport { app } from 'electron';\n\nconst hasClientViewerBundle = (directory: string): boolean => {\n\tif (!directory) {\n\t\treturn false;\n\t}\n\n\tconst indexFile = join(directory, 'index.html');\n\treturn existsSync(indexFile);\n};\n\nconst normalizeCandidates = (candidates: string[]): string[] => {\n\tconst normalized = new Set<string>();\n\n\tcandidates.forEach((candidate) => {\n\t\tif (!candidate) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst resolvedCandidate = resolve(candidate);\n\t\tnormalized.add(resolvedCandidate);\n\t});\n\n\treturn [...normalized];\n};\n\nexport const getClientViewerDistPath = (): string => {\n\tconst resourcesPath = process.resourcesPath ?? '';\n\tconst appPath = app.getAppPath();\n\n\tconst candidates = normalizeCandidates([\n\t\tjoin(__dirname, '../client-viewer'),\n\t\tjoin(appPath, 'client-viewer'),\n\t\tjoin(appPath, 'out/client-viewer'),\n\t\tjoin(resourcesPath, 'client-viewer'),\n\t\tjoin(resourcesPath, 'app.asar.unpacked/client-viewer'),\n\t\tjoin(process.cwd(), 'out/client-viewer'),\n\t\tjoin(process.cwd(), 'src/client-viewer/dist'),\n\t]);\n\n\tfor (const candidate of candidates) {\n\t\tif (hasClientViewerBundle(candidate)) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\n\treturn '';\n};\n"
  },
  {
    "path": "src/server/index.ts",
    "content": "/*\n * original JS code from darkwire.io\n * translated to typescript for Deskreen app\n * by Pavlo (Paul) Buidenkov\n * */\n\nimport http from 'http';\nimport Koa from 'koa';\nimport crypto from 'crypto';\nimport { Server } from 'socket.io';\nimport cors from 'kcors';\nimport Router from 'koa-router';\nimport koaStatic from 'koa-static';\nimport koaSend from 'koa-send';\nimport detectPort from 'detect-port';\nimport config from '../common/config';\nimport startPollForInactiveRooms from './startPollForInactiveRooms';\nimport Logger from '../main/utils/LoggerWithFilePrefix';\nimport SocketsIPService from './socketsIPService';\nimport socketIOServerStore from './store/socketIOServerStore';\nimport DarkwireSocket from './darkwireSocket';\nimport getStore from './store';\nimport { getDeskreenGlobal } from '../main/helpers/getDeskreenGlobal';\nimport getMyLocalIpV4 from '../main/helpers/getMyLocalIpV4';\nimport { getClientViewerDistPath } from './getClientViewerDistPath';\n\nconst { hostname, primaryPort, backupPort } = config;\n\nconst getRoomIdHash = (id: string): string => {\n\treturn crypto.createHash('sha256').update(id).digest('hex');\n};\n\nconst ioHandleOnConnection = (socket): void => {\n\tconst { roomId } = socket.handshake.query;\n\tconst store = getStore();\n\n\tsetTimeout(async () => {\n\t\tif (!getDeskreenGlobal().roomIDService.isRoomIDTaken(roomId)) {\n\t\t\tsocket.emit('NOT_ALLOWED');\n\t\t\tsetTimeout(() => {\n\t\t\t\tsocket.disconnect(true);\n\t\t\t}, 1000);\n\t\t\treturn;\n\t\t}\n\t\tconst roomIdHash = getRoomIdHash(roomId);\n\n\t\tconst storedRoom = await store.get('rooms', roomIdHash);\n\t\tconst parsedRoom =\n\t\t\ttypeof storedRoom === 'string' ? JSON.parse(storedRoom) : {};\n\n\t\tnew DarkwireSocket({\n\t\t\troomIdOriginal: roomId,\n\t\t\troomId: roomIdHash,\n\t\t\tsocket,\n\t\t\troom: parsedRoom as Room,\n\t\t});\n\t\t// }\n\t}, 500); // timeout 500 millisecond for throttling malicious connections\n};\n\nfunction setStaticFileHeaders(\n\tctx: Koa.ParameterizedContext<Koa.DefaultState, Koa.DefaultContext>,\n): void {\n\tctx.set({\n\t\t'strict-transport-security': 'max-age=31536000',\n\t\t'X-Frame-Options': 'deny',\n\t\t'X-XSS-Protection': '1; mode=block',\n\t\t'X-Content-Type-Options': 'nosniff',\n\t\t'Referrer-Policy': 'no-referrer',\n\t\t'Feature-Policy':\n\t\t\t\"geolocation 'none'; vr 'none'; payment 'none'; microphone 'none'\",\n\t\t// 'Cache-Control': 'max-age=0', // make browser get fresh files and make new connection when client connected\n\t});\n}\n\nclass DeskreenSignalingServer {\n\tlog = new Logger(__filename);\n\n\tserver = {} as unknown as http.Server;\n\n\thostname: string;\n\n\tprimaryPort: number;\n\n\tbackupPort: number;\n\n\tport: number;\n\n\tapp: Koa | undefined;\n\n\tclientDistDirectory: string;\n\n\tconstructor() {\n\t\tconst localIp = getMyLocalIpV4();\n\t\tthis.hostname = localIp || String(hostname);\n\t\tthis.primaryPort = parseInt(primaryPort as unknown as string, 10);\n\t\tthis.backupPort = parseInt(backupPort as unknown as string, 10);\n\n\t\tthis.port = this.primaryPort;\n\t\tthis.clientDistDirectory = getClientViewerDistPath();\n\n\t\tif (!this.clientDistDirectory) {\n\t\t\tthis.log.error(\n\t\t\t\t'Client viewer bundle is missing. Remote connections will fail.',\n\t\t\t);\n\t\t}\n\n\t\tthis.init();\n\t}\n\n\tinit(): void {\n\t\tthis.app = new Koa();\n\t\tconst router = new Router();\n\n\t\tthis.app.use(cors());\n\t\tthis.app.use(router.routes());\n\n\t\tconst clientDistDirectory = this.clientDistDirectory;\n\n\t\tif (clientDistDirectory) {\n\t\t\tthis.app.use(async (ctx, next) => {\n\t\t\t\tsetStaticFileHeaders(ctx);\n\t\t\t\tawait koaStatic(clientDistDirectory)(ctx, next);\n\t\t\t});\n\n\t\t\tthis.app.use(async (ctx) => {\n\t\t\t\tsetStaticFileHeaders(ctx);\n\t\t\t\tawait koaSend(ctx, 'index.html', { root: clientDistDirectory });\n\t\t\t});\n\t\t} else {\n\t\t\tthis.app.use(async (ctx) => {\n\t\t\t\tctx.body = { ready: true };\n\t\t\t});\n\t\t}\n\n\t\tconst protocol = http;\n\n\t\tthis.server = protocol.createServer(this.app.callback());\n\t\tconst io = new Server(this.server, {\n\t\t\tpingInterval: 20000,\n\t\t\tpingTimeout: 5000,\n\t\t\tserveClient: false,\n\t\t});\n\n\t\tio.sockets.on('connection', (socket) => {\n\t\t\tconst socketId = socket.id;\n\n\t\t\tconst clientIp = socket.request.socket.remoteAddress;\n\t\t\tSocketsIPService.setIPOfSocketID(socketId, clientIp || '');\n\t\t});\n\n\t\tio.on('connection', (socket) => {\n\t\t\tioHandleOnConnection(socket);\n\t\t});\n\n\t\tsocketIOServerStore.setServer(io);\n\t}\n\n\tasync start(): Promise<http.Server> {\n\t\tstartPollForInactiveRooms();\n\t\tthis.server = await this.callListenOnHttpServer();\n\t\treturn this.server;\n\t}\n\n\tlistenCallback() {\n\t\treturn () => {\n\t\t\tthis.log.info(\n\t\t\t\t`Deskreen CE signaling server is online at port ${this.port}`,\n\t\t\t);\n\t\t\tthis.log.info(\n\t\t\t\t`🌐 Server available at http://${this.hostname}:${this.port}`,\n\t\t\t);\n\t\t};\n\t}\n\n\tasync callListenOnHttpServer(): Promise<http.Server> {\n\t\treturn new Promise<http.Server>((resolve, reject) => {\n\t\t\tconst tryListen = (port: number): void => {\n\t\t\t\t// Remove any previous error listeners\n\t\t\t\tthis.server.removeAllListeners('error');\n\n\t\t\t\t// Set up error handler\n\t\t\t\tthis.server.once('error', async (error: NodeJS.ErrnoException) => {\n\t\t\t\t\tif (\n\t\t\t\t\t\terror.code === 'EADDRINUSE' &&\n\t\t\t\t\t\t(port === this.primaryPort || port === this.backupPort)\n\t\t\t\t\t) {\n\t\t\t\t\t\t// Primary port is already in use, try backup\n\t\t\t\t\t\tthis.log.error(`Port ${port} is already in use`);\n\t\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t\t`Port ${primaryPort} is in use. Trying backup port ${backupPort}...`,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst detectedBackupPort = await detectPort(backupPort);\n\n\t\t\t\t\t\t\tif (backupPort === detectedBackupPort) {\n\t\t\t\t\t\t\t\tthis.log.info(`Backup port ${backupPort} is available.`);\n\t\t\t\t\t\t\t\tthis.port = backupPort;\n\t\t\t\t\t\t\t\ttryListen(backupPort);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tconst errorMsg = `Both primary port ${primaryPort} and backup port ${backupPort} are in use`;\n\t\t\t\t\t\t\t\tthis.log.error(`Error: ${errorMsg}`);\n\t\t\t\t\t\t\t\t// reject(new Error(errorMsg));\n\t\t\t\t\t\t\t\tthis.port = await detectPort();\n\t\t\t\t\t\t\t\ttryListen(this.port);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tthis.log.error(\n\t\t\t\t\t\t\t\t'An unexpected error occurred while detecting ports:',\n\t\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Some other error or backup port is also in use\n\t\t\t\t\t\tthis.log.error(`Failed to start server on port ${port}:`, error);\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\t// Attempt to listen on all interfaces (0.0.0.0) to allow both local and local network access\n\t\t\t\tthis.server.listen(port, '0.0.0.0', () => {\n\t\t\t\t\tthis.listenCallback()();\n\t\t\t\t\tresolve(this.server);\n\t\t\t\t});\n\t\t\t};\n\n\t\t\t// Start with the primary port\n\t\t\ttryListen(this.port);\n\t\t});\n\t}\n\n\tstop(): void {\n\t\tthis.server.close();\n\t}\n}\n\nexport const signalingServer = new DeskreenSignalingServer();\n"
  },
  {
    "path": "src/server/onDeviceConnectedCallback.ts",
    "content": "import { IpcEvents } from '../common/IpcEvents.enum';\nimport { getDeskreenGlobal } from '../main/helpers/getDeskreenGlobal';\nimport { deskreenApp } from '../main';\nimport { Device } from '../common/Device';\nimport SharingSessionStatusEnum from '../features/SharingSessionService/SharingSessionStatusEnum';\n\nexport function onDeviceConnectedCallback(device: Device): void {\n\tconst deskreenGlobal = getDeskreenGlobal();\n\tconst { connectedDevicesService, sharingSessionService } = deskreenGlobal;\n\tif (!connectedDevicesService.isSlotAvailable()) {\n\t\tconst waitingSession =\n\t\t\tsharingSessionService.waitingForConnectionSharingSession;\n\t\twaitingSession?.denyConnectionForPartner();\n\t\twaitingSession?.setStatus(SharingSessionStatusEnum.NOT_CONNECTED);\n\t\tsharingSessionService.waitingForConnectionSharingSession = null;\n\t\tconnectedDevicesService.resetPendingConnectionDevice();\n\t\treturn;\n\t}\n\tconnectedDevicesService.setPendingConnectionDevice(device);\n\tdeskreenApp.mainWindow?.webContents.send(\n\t\tIpcEvents.SetPendingConnectionDevice,\n\t\tdevice,\n\t);\n}\n"
  },
  {
    "path": "src/server/socketsIPService.ts",
    "content": "class SocketsIPService {\n\tprivate static instance: SocketsIPService;\n\n\tidToIpMap: Map<string, string>;\n\n\tipToIdMap: Map<string, string>;\n\n\tconstructor() {\n\t\tthis.idToIpMap = new Map<string, string>();\n\t\tthis.ipToIdMap = new Map<string, string>();\n\t}\n\n\tsetIPOfSocketID(id: string, ip: string): void {\n\t\tthis.idToIpMap.set(id, ip);\n\t\tthis.ipToIdMap.set(ip, id);\n\t}\n\n\tsetSocketIDOfIP(ip: string, id: string): void {\n\t\tthis.idToIpMap.set(id, ip);\n\t\tthis.ipToIdMap.set(ip, id);\n\t}\n\n\tgetSocketIPByID(id: string): string | undefined {\n\t\treturn this.idToIpMap.get(id);\n\t}\n\n\tgetSocketIDByIP(ip: string): string | undefined {\n\t\treturn this.ipToIdMap.get(ip);\n\t}\n\n\tisIPExists(ip: string): boolean {\n\t\treturn this.ipToIdMap.has(ip);\n\t}\n\n\tstatic getInstance(): SocketsIPService {\n\t\tif (!this.instance) {\n\t\t\tthis.instance = new SocketsIPService();\n\t\t}\n\t\treturn this.instance;\n\t}\n}\n\nexport default SocketsIPService.getInstance();\n"
  },
  {
    "path": "src/server/startPollForInactiveRooms.ts",
    "content": "/*\n * original JS code from darkwire.io\n * translated to typescript for Deskreen app\n * */\nimport getStore from './store';\n\nexport default async function startPollForInactiveRooms(): Promise<void> {\n\tconst store = getStore();\n\tconst rooms = (await store.getAll('rooms')) || {};\n\n\tfor (const roomId of Object.keys(rooms)) {\n\t\tconst serializedRoom = rooms[roomId];\n\t\tif (typeof serializedRoom !== 'string') {\n\t\t\tcontinue;\n\t\t}\n\t\tconst room = JSON.parse(serializedRoom) as { updatedAt?: number };\n\t\tif (typeof room.updatedAt !== 'number') {\n\t\t\tcontinue;\n\t\t}\n\t\tconst timeSinceUpdatedInSeconds = (Date.now() - room.updatedAt) / 1000;\n\t\tconst timeSinceUpdatedInDays = Math.round(\n\t\t\ttimeSinceUpdatedInSeconds / 60 / 60 / 24,\n\t\t);\n\t\tif (timeSinceUpdatedInDays > 7) {\n\t\t\tawait store.del('rooms', roomId);\n\t\t}\n\t}\n\n\tsetTimeout(startPollForInactiveRooms, 1000 * 60 * 60); // every hour\n}\n"
  },
  {
    "path": "src/server/store/MemoryStore.ts",
    "content": "/*\n * original JS code from darkwire.io\n * translated to typescript for Deskreen app\n * */\n\ntype MemoryStoreBucket = Record<string, unknown>;\ntype MemoryStoreData = Record<string, MemoryStoreBucket>;\n\ninterface MemoryStoreParams {\n\tstore: MemoryStoreData;\n}\n\nclass MemoryStore implements MemoryStoreParams {\n\tstore: MemoryStoreData;\n\n\tconstructor() {\n\t\tthis.store = {};\n\t}\n\n\tasync get(key: string, field: string): Promise<unknown | null> {\n\t\tconst bucket = this.store[key];\n\t\tif (bucket === undefined || bucket[field] === undefined) {\n\t\t\treturn null;\n\t\t}\n\t\treturn bucket[field];\n\t}\n\n\tasync getAll(key: string): Promise<MemoryStoreBucket> {\n\t\treturn this.store[key] ?? {};\n\t}\n\n\tasync set(key: string, field: string, value: unknown): Promise<number> {\n\t\tconst bucket = this.ensureBucket(key);\n\t\tbucket[field] = value;\n\t\treturn 1;\n\t}\n\n\tasync del(key: string, field: string): Promise<0 | 1> {\n\t\tconst bucket = this.store[key];\n\t\tif (bucket === undefined || bucket[field] === undefined) {\n\t\t\treturn 0;\n\t\t}\n\t\tdelete bucket[field];\n\t\treturn 1;\n\t}\n\n\tasync inc(key: string, field: string, inc = 1): Promise<number> {\n\t\tconst bucket = this.ensureBucket(key);\n\t\tconst currentValue = bucket[field];\n\t\tconst nextValue =\n\t\t\ttypeof currentValue === 'number' ? currentValue + inc : inc;\n\t\tbucket[field] = nextValue;\n\t\treturn nextValue;\n\t}\n\n\tprivate ensureBucket(key: string): MemoryStoreBucket {\n\t\tif (this.store[key] === undefined) {\n\t\t\tthis.store[key] = {};\n\t\t}\n\t\treturn this.store[key];\n\t}\n}\n\nexport default MemoryStore;\n"
  },
  {
    "path": "src/server/store/index.ts",
    "content": "import MemoryStore from './MemoryStore';\n\nlet store: MemoryStore;\n\nconst getStore = (): MemoryStore => {\n\tif (store === undefined) {\n\t\tstore = new MemoryStore();\n\t}\n\treturn store;\n};\n\nexport default getStore;\n"
  },
  {
    "path": "src/server/store/socketIOServerStore.ts",
    "content": "import Io from 'socket.io';\n\nclass SocketIOServerStore {\n\tioServer = {} as unknown as Io.Server;\n\n\tsetServer(server: Io.Server): void {\n\t\tthis.ioServer = server;\n\t}\n\n\tgetServer(): Io.Server {\n\t\treturn this.ioServer;\n\t}\n}\n\nconst store = new SocketIOServerStore();\n\nexport default store;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"files\": [],\n\t\"references\": [\n\t\t{ \"path\": \"./tsconfig.node.json\" },\n\t\t{ \"path\": \"./tsconfig.web.json\" }\n\t]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n\t\"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n\t\"include\": [\n\t\t\"electron.vite.config.*\",\n\t\t\"src/main/**/*\",\n\t\t\"src/preload/**/*\",\n\t\t\"src/server/**/*\",\n\t\t\"src/features/**/*\",\n\t\t\"src/common/**/*\"\n\t],\n\t\"compilerOptions\": {\n\t\t\"composite\": true,\n\t\t\"types\": [\"electron-vite/node\"]\n\t}\n}\n"
  },
  {
    "path": "tsconfig.web.json",
    "content": "{\n\t\"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n\t\"include\": [\n\t\t\"src/renderer/src/env.d.ts\",\n\t\t\"src/renderer/src/**/*\",\n\t\t\"src/renderer/src/**/*.tsx\",\n\t\t\"src/rendererHelper/**/*\",\n\t\t\"src/rendererHelper/**/*.tsx\",\n\t\t\"src/preload/*.d.ts\",\n\t\t\"src/common/**/*\"\n\t],\n\t\"compilerOptions\": {\n\t\t\"composite\": true,\n\t\t\"jsx\": \"react-jsx\",\n\t\t\"baseUrl\": \".\",\n\t\t\"paths\": {\n\t\t\t\"@renderer/*\": [\"src/renderer/src/*\"],\n\t\t\t\"@common/*\": [\"src/common/*\"]\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "wiki/Home.md",
    "content": "Welcome to the deskreen wiki!\n\nTo edit [Deskreen wiki](https://github.com/pavlobu/deskreen/wiki) please open your PR to [Deskreen Repo](https://github.com/pavlobu/deskreen) with changes in `wiki/` folder.\n"
  }
]