├── .env.example ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml └── workflows │ ├── appstore-build-publish.yml │ ├── dependabot-approve-merge.yml │ ├── docker.yml │ ├── fixup.yml │ ├── lint-eslint.yml │ ├── lint-php-cs.yml │ ├── lint-php.yml │ ├── lint-stylelint.yml │ ├── node-test.yml │ ├── node.yml │ ├── npm-audit-fix.yml │ ├── phpunit-sqlite.yml │ ├── playwright.yml │ ├── pr-feedback.yml │ ├── psalm.yml │ ├── reuse.yml │ └── update-nextcloud-ocp.yml ├── .gitignore ├── .l10nignore ├── .nextcloudignore ├── .php-cs-fixer.dist.php ├── .stylelintrc.cjs ├── .tx └── config ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── LICENSES ├── AGPL-3.0-or-later.txt ├── Apache-2.0.txt ├── CC0-1.0.txt └── MIT.txt ├── README.md ├── REUSE.toml ├── appinfo ├── info.xml └── routes.php ├── composer.json ├── composer.lock ├── docker-compose.yml ├── img ├── app-dark.svg ├── app-filetype.svg └── app.svg ├── krankerl.toml ├── l10n ├── .gitkeep ├── ar.js ├── ar.json ├── ast.js ├── ast.json ├── bg.js ├── bg.json ├── ca.js ├── ca.json ├── cs.js ├── cs.json ├── da.js ├── da.json ├── de.js ├── de.json ├── de_DE.js ├── de_DE.json ├── el.js ├── el.json ├── en_GB.js ├── en_GB.json ├── eo.js ├── eo.json ├── es.js ├── es.json ├── es_419.js ├── es_419.json ├── es_CL.js ├── es_CL.json ├── es_CO.js ├── es_CO.json ├── es_CR.js ├── es_CR.json ├── es_DO.js ├── es_DO.json ├── es_EC.js ├── es_EC.json ├── es_GT.js ├── es_GT.json ├── es_HN.js ├── es_HN.json ├── es_MX.js ├── es_MX.json ├── es_NI.js ├── es_NI.json ├── es_PA.js ├── es_PA.json ├── es_PE.js ├── es_PE.json ├── es_PR.js ├── es_PR.json ├── es_PY.js ├── es_PY.json ├── es_SV.js ├── es_SV.json ├── es_UY.js ├── es_UY.json ├── et_EE.js ├── et_EE.json ├── eu.js ├── eu.json ├── fa.js ├── fa.json ├── fi.js ├── fi.json ├── fr.js ├── fr.json ├── ga.js ├── ga.json ├── gl.js ├── gl.json ├── he.js ├── he.json ├── hr.js ├── hr.json ├── hu.js ├── hu.json ├── is.js ├── is.json ├── it.js ├── it.json ├── ja.js ├── ja.json ├── ka.js ├── ka.json ├── ka_GE.js ├── ka_GE.json ├── ko.js ├── ko.json ├── lt_LT.js ├── lt_LT.json ├── lv.js ├── lv.json ├── mk.js ├── mk.json ├── mn.js ├── mn.json ├── nb.js ├── nb.json ├── nl.js ├── nl.json ├── oc.js ├── oc.json ├── pl.js ├── pl.json ├── pt_BR.js ├── pt_BR.json ├── pt_PT.js ├── pt_PT.json ├── ro.js ├── ro.json ├── ru.js ├── ru.json ├── sc.js ├── sc.json ├── sk.js ├── sk.json ├── sl.js ├── sl.json ├── sq.js ├── sq.json ├── sr.js ├── sr.json ├── sv.js ├── sv.json ├── tr.js ├── tr.json ├── ug.js ├── ug.json ├── uk.js ├── uk.json ├── vi.js ├── vi.json ├── zh_CN.js ├── zh_CN.json ├── zh_HK.js ├── zh_HK.json ├── zh_TW.js └── zh_TW.json ├── lib ├── AppInfo │ └── Application.php ├── Consts │ └── JWTConsts.php ├── Controller │ ├── JWTController.php │ ├── SettingsController.php │ └── WhiteboardController.php ├── Exception │ ├── InvalidUserException.php │ └── UnauthorizedException.php ├── Listener │ ├── AddContentSecurityPolicyListener.php │ ├── BeforeTemplateRenderedListener.php │ ├── LoadViewerListener.php │ └── RegisterTemplateCreatorListener.php ├── Model │ ├── AuthenticatedUser.php │ ├── PublicSharingUser.php │ └── User.php ├── Service │ ├── Authentication │ │ ├── AuthenticatePublicSharingUserService.php │ │ ├── AuthenticateSessionUserService.php │ │ ├── AuthenticateUserService.php │ │ ├── AuthenticateUserServiceFactory.php │ │ ├── ChainAuthenticateUserService.php │ │ ├── GetPublicSharingUserFromIdService.php │ │ ├── GetSessionUserFromIdService.php │ │ ├── GetUserFromIdService.php │ │ └── GetUserFromIdServiceFactory.php │ ├── ConfigService.php │ ├── ExceptionService.php │ ├── File │ │ ├── GetFileFromIdService.php │ │ ├── GetFileFromPublicSharingTokenService.php │ │ ├── GetFileService.php │ │ └── GetFileServiceFactory.php │ ├── JWTService.php │ └── WhiteboardContentService.php └── Settings │ ├── Admin.php │ ├── Section.php │ └── SetupCheck.php ├── package-lock.json ├── package.json ├── playwright.config.ts ├── playwright ├── e2e │ └── viewer.spec.ts ├── start-nextcloud-server.mjs └── support │ ├── fixtures │ └── random-user.ts │ └── setup.ts ├── psalm.xml ├── screenshots └── screenshot1.png ├── src ├── App.scss ├── App.tsx ├── Embeddable.tsx ├── VueWrapper.tsx ├── components │ ├── ExcalidrawMenu.tsx │ ├── FileDownloadButton.tsx │ └── NetworkStatusIndicator.tsx ├── database │ └── db.ts ├── hooks │ ├── useBoardDataManager.ts │ ├── useCollaboration.ts │ ├── useFiles.ts │ ├── useReadOnlyState.ts │ ├── useSidebarDownload.ts │ ├── useSmartPicker.tsx │ ├── useSync.ts │ └── useThemeHandling.ts ├── main.tsx ├── settings.js ├── settings │ └── Settings.vue ├── sidebar │ └── ExampleSidebar.scss ├── stores │ ├── useCollaborationStore.ts │ ├── useExcalidrawStore.ts │ ├── useJwtStore.ts │ ├── useLangStore.ts │ ├── useSyncStore.ts │ └── useWhiteboardConfigStore.ts ├── util.ts ├── utils.ts ├── viewer.css └── workers │ └── syncWorker.ts ├── templates └── admin.php ├── tests ├── Unit │ └── AppInfo │ │ └── ApplicationTest.php ├── bootstrap.php ├── integration │ ├── configMock.js │ ├── metrics.spec.mjs │ └── socket.spec.mjs ├── phpunit.xml ├── psalm-baseline.xml └── stub.phpstub ├── tsconfig.json ├── vite.config.ts ├── vitest.config.js └── websocket_server ├── .gitignore ├── AppManager.js ├── Config.js ├── Constants.js ├── InMemoryStrategy.js ├── LRUStrategy.js ├── PrometheusDataManager.js ├── RedisStrategy.js ├── ServerManager.js ├── SocketManager.js ├── StorageManager.js ├── StorageStrategy.js ├── SystemMonitor.js ├── Utils.js └── main.js /.env.example: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | # The URL of the Nextcloud instance 5 | # This is used to read and write the file content of the whiteboard files 6 | NEXTCLOUD_URL=http://nextcloud.local 7 | 8 | # The port running the whiteboard backend 9 | PORT=3002 10 | 11 | # The secret key used to sign the JWT tokens to secure the communication 12 | # between the Nextcloud app and the whiteboard backend 13 | JWT_SECRET_KEY=your_secret_key 14 | 15 | # For development purposes it can be useful to configure TLS and provide keys 16 | # We recommend to use a reverse proxy for production 17 | TLS=false 18 | TLS_KEY= 19 | TLS_CERT= 20 | 21 | # Storage strategy for whiteboard data and socket-related temporary data 22 | # Valid values are: 'redis' or 'lru' (Least Recently Used cache) 23 | # This strategy is used for: 24 | # 1. Whiteboard data storage 25 | # 2. Socket-related temporary data (e.g., cached tokens, bound data for each socket ID) 26 | # 3. Scaling the socket server across multiple nodes (when using 'redis') 27 | # We strongly recommend using 'redis' for production environments 28 | # 'lru' provides a balance of performance and memory usage for single-node setups 29 | STORAGE_STRATEGY=lru 30 | 31 | # Redis connection URL for data storage and socket server scaling 32 | # Required when STORAGE_STRATEGY is set to 'redis' 33 | # This URL is used for both persistent data and temporary socket-related data 34 | # Format: redis://[username:password@]host[:port][/database_number] 35 | # Example: redis://user:password@redis.example.com:6379/0 36 | REDIS_URL=redis://localhost:6379 37 | 38 | # Timeout in milliseconds for the whiteboard server's graceful shutdown process 39 | # If the server cannot close all connections within this time, it will force shutdown 40 | # Default: 3600000 (1 hour) 41 | FORCE_CLOSE_TIMEOUT=3600000 42 | 43 | # Maximum file upload size in megabytes 44 | # This limits the size of files that can be uploaded to the whiteboard 45 | # Default: 2 (MB) 46 | MAX_UPLOAD_FILE_SIZE=2 47 | 48 | # Prometheus metrics endpoint 49 | # Set this to access the monitoring endpoint at /metrics 50 | # either providing it as Bearer token or as ?token= query parameter 51 | METRICS_TOKEN= 52 | 53 | # WebSocket compression setting 54 | # Enable or disable per-message deflate compression for WebSocket messages 55 | # Default: true 56 | COMPRESSION_ENABLED=true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | # App maintainers 5 | * @hweihwang @grnd-alt @juliushaertl 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | contact_links: 4 | - name: 🚨 Report a security or privacy issue 5 | url: https://hackerone.com/nextcloud 6 | about: Report security and privacy related issues privately to the Nextcloud team, so we can coordinate the fix and release without potentially exposing all Nextcloud servers and users in the meantime. 7 | - name: ❓ Community Support and Help 8 | url: https://help.nextcloud.com/ 9 | about: Configuration, webserver/proxy or performance issues and other questions 10 | - name: 💼 Nextcloud Enterprise 11 | url: https://portal.nextcloud.com/ 12 | about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: npm 7 | directory: "/" 8 | target-branch: "main" 9 | schedule: 10 | interval: weekly 11 | day: saturday 12 | time: "03:00" 13 | timezone: Europe/Paris 14 | open-pull-requests-limit: 10 15 | labels: 16 | - 3. to review 17 | - dependencies 18 | reviewers: 19 | - juliushaertl 20 | - hweihwang 21 | - grnd-alt 22 | 23 | - package-ecosystem: composer 24 | directory: "/" 25 | schedule: 26 | interval: weekly 27 | day: saturday 28 | time: "03:00" 29 | timezone: Europe/Paris 30 | open-pull-requests-limit: 10 31 | labels: 32 | - 3. to review 33 | - dependencies 34 | reviewers: 35 | - juliushaertl 36 | - hweihwang 37 | - grnd-alt 38 | 39 | - package-ecosystem: "docker" 40 | directory: "/" 41 | schedule: 42 | interval: "weekly" 43 | day: saturday 44 | time: "03:00" 45 | timezone: Europe/Paris 46 | open-pull-requests-limit: 10 47 | labels: 48 | - 3. to review 49 | - dependencies 50 | reviewers: 51 | - juliushaertl 52 | - hweihwang 53 | - grnd-alt 54 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-merge.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Dependabot 10 | 11 | on: 12 | pull_request_target: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | permissions: 19 | contents: read 20 | 21 | concurrency: 22 | group: dependabot-approve-merge-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | auto-approve-merge: 27 | if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]' 28 | runs-on: ubuntu-latest-low 29 | permissions: 30 | # for hmarr/auto-approve-action to approve PRs 31 | pull-requests: write 32 | 33 | steps: 34 | - name: Disabled on forks 35 | if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} 36 | run: | 37 | echo 'Can not approve PRs from forks' 38 | exit 1 39 | 40 | # GitHub actions bot approve 41 | - uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Nextcloud bot approve and merge request 46 | - uses: ahmadnassri/action-dependabot-auto-merge@45fc124d949b19b6b8bf6645b6c9d55f4f9ac61a # v2 47 | with: 48 | target: minor 49 | github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | name: Docker image 5 | 6 | on: 7 | release: 8 | types: [published] 9 | push: 10 | branches: 11 | - main 12 | - master 13 | - stable* 14 | 15 | env: 16 | GHCR_REPO: ghcr.io/${{ github.repository_owner }}/whiteboard 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-22.04 21 | timeout-minutes: 30 22 | 23 | strategy: 24 | matrix: 25 | platform: 26 | - linux/amd64 27 | - linux/arm64 28 | 29 | steps: 30 | - name: Set vars 31 | id: vars 32 | run: | 33 | echo "version_channel=${{ github.event_name == 'release' && 'release' || 'daily' }}" >> $GITHUB_OUTPUT 34 | echo "version=${{ github.sha }}" >> $GITHUB_OUTPUT 35 | echo "platform=$(echo -n ${{ matrix.platform }} | sed 's/\//-/g')" >> $GITHUB_OUTPUT 36 | 37 | - uses: actions/checkout@v3 38 | - uses: docker/setup-qemu-action@v3 39 | - uses: docker/setup-buildx-action@v3 40 | 41 | - name: Login to Github Container Registry 42 | uses: docker/login-action@v2 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Build docker images 49 | uses: docker/build-push-action@v3 50 | with: 51 | context: . 52 | push: true 53 | platforms: ${{ matrix.platform }} 54 | provenance: false 55 | tags: | 56 | ${{ env.GHCR_REPO}}:${{ steps.vars.outputs.version }}-${{ steps.vars.outputs.platform }} 57 | 58 | release: 59 | runs-on: ubuntu-22.04 60 | timeout-minutes: 10 61 | needs: build 62 | 63 | steps: 64 | - name: Set vars 65 | id: vars 66 | run: | 67 | echo "version_channel=${{ github.event_name == 'release' && 'release' || 'daily' }}" >> $GITHUB_OUTPUT 68 | echo "version_ref=${{ github.event_name == 'release' && github.ref_name || github.sha }}" >> $GITHUB_OUTPUT 69 | echo "version=${{ github.sha }}" >> $GITHUB_OUTPUT 70 | 71 | - name: Login to Github Container Registry 72 | uses: docker/login-action@v2 73 | with: 74 | registry: ghcr.io 75 | username: ${{ github.actor }} 76 | password: ${{ secrets.GITHUB_TOKEN }} 77 | 78 | - name: Create GHCR manifest 79 | run: | 80 | docker manifest create $GHCR_REPO:${{ steps.vars.outputs.version_ref }} \ 81 | $GHCR_REPO:${{ steps.vars.outputs.version }}-linux-amd64 \ 82 | $GHCR_REPO:${{ steps.vars.outputs.version }}-linux-arm64 83 | 84 | docker manifest create $GHCR_REPO:${{ steps.vars.outputs.version_channel }} \ 85 | $GHCR_REPO:${{ steps.vars.outputs.version }}-linux-amd64 \ 86 | $GHCR_REPO:${{ steps.vars.outputs.version }}-linux-arm64 87 | 88 | - name: Push manifests 89 | run: | 90 | docker manifest push $GHCR_REPO:${{ steps.vars.outputs.version_ref }} 91 | docker manifest push $GHCR_REPO:${{ steps.vars.outputs.version_channel }} -------------------------------------------------------------------------------- /.github/workflows/fixup.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block fixup and squash commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: fixup-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | commit-message-check: 24 | if: github.event.pull_request.draft == false 25 | 26 | permissions: 27 | pull-requests: write 28 | name: Block fixup and squash commits 29 | 30 | runs-on: ubuntu-latest-low 31 | 32 | steps: 33 | - name: Run check 34 | uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/lint-eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint eslint 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-eslint-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | changes: 22 | runs-on: ubuntu-latest-low 23 | permissions: 24 | contents: read 25 | pull-requests: read 26 | 27 | outputs: 28 | src: ${{ steps.changes.outputs.src}} 29 | 30 | steps: 31 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 32 | id: changes 33 | continue-on-error: true 34 | with: 35 | filters: | 36 | src: 37 | - '.github/workflows/**' 38 | - 'src/**' 39 | - 'appinfo/info.xml' 40 | - 'package.json' 41 | - 'package-lock.json' 42 | - 'tsconfig.json' 43 | - '.eslintrc.*' 44 | - '.eslintignore' 45 | - '**.js' 46 | - '**.ts' 47 | - '**.vue' 48 | 49 | lint: 50 | runs-on: ubuntu-latest 51 | 52 | needs: changes 53 | if: needs.changes.outputs.src != 'false' 54 | 55 | name: NPM lint 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 60 | 61 | - name: Read package.json node and npm engines version 62 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 63 | id: versions 64 | with: 65 | fallbackNode: '^20' 66 | fallbackNpm: '^10' 67 | 68 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 69 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 70 | with: 71 | node-version: ${{ steps.versions.outputs.nodeVersion }} 72 | 73 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 74 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 75 | 76 | - name: Install dependencies 77 | env: 78 | CYPRESS_INSTALL_BINARY: 0 79 | PUPPETEER_SKIP_DOWNLOAD: true 80 | run: npm ci 81 | 82 | - name: Lint 83 | run: npm run lint 84 | 85 | summary: 86 | permissions: 87 | contents: none 88 | runs-on: ubuntu-latest-low 89 | needs: [changes, lint] 90 | 91 | if: always() 92 | 93 | # This is the summary, we just avoid to rename it so that branch protection rules still match 94 | name: eslint 95 | 96 | steps: 97 | - name: Summary status 98 | run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi 99 | -------------------------------------------------------------------------------- /.github/workflows/lint-php-cs.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php-cs 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-cs-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | 24 | name: php-cs 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 29 | 30 | - name: Get php version 31 | id: versions 32 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 33 | 34 | - name: Set up php${{ steps.versions.outputs.php-available }} 35 | uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 36 | with: 37 | php-version: ${{ steps.versions.outputs.php-available }} 38 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 39 | coverage: none 40 | ini-file: development 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Install dependencies 45 | run: composer i 46 | 47 | - name: Lint 48 | run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) 49 | -------------------------------------------------------------------------------- /.github/workflows/lint-php.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | matrix: 22 | runs-on: ubuntu-latest-low 23 | outputs: 24 | php-versions: ${{ steps.versions.outputs.php-versions }} 25 | steps: 26 | - name: Checkout app 27 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 28 | - name: Get version matrix 29 | id: versions 30 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.0.0 31 | 32 | php-lint: 33 | runs-on: ubuntu-latest 34 | needs: matrix 35 | strategy: 36 | matrix: 37 | php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}} 38 | 39 | name: php-lint 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 44 | 45 | - name: Set up php ${{ matrix.php-versions }} 46 | uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 47 | with: 48 | php-version: ${{ matrix.php-versions }} 49 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 50 | coverage: none 51 | ini-file: development 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: Lint 56 | run: composer run lint 57 | 58 | summary: 59 | permissions: 60 | contents: none 61 | runs-on: ubuntu-latest-low 62 | needs: php-lint 63 | 64 | if: always() 65 | 66 | name: php-lint-summary 67 | 68 | steps: 69 | - name: Summary status 70 | run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi 71 | -------------------------------------------------------------------------------- /.github/workflows/lint-stylelint.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint stylelint 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-stylelint-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | 24 | name: stylelint 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 29 | 30 | - name: Read package.json node and npm engines version 31 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 32 | id: versions 33 | with: 34 | fallbackNode: '^20' 35 | fallbackNpm: '^10' 36 | 37 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 38 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 39 | with: 40 | node-version: ${{ steps.versions.outputs.nodeVersion }} 41 | 42 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 43 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 44 | 45 | - name: Install dependencies 46 | env: 47 | CYPRESS_INSTALL_BINARY: 0 48 | run: npm ci 49 | 50 | - name: Lint 51 | run: npm run stylelint 52 | -------------------------------------------------------------------------------- /.github/workflows/npm-audit-fix.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Npm audit fix and compile 10 | 11 | on: 12 | workflow_dispatch: 13 | schedule: 14 | # At 2:30 on Sundays 15 | - cron: '30 2 * * 0' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | branches: ['main', 'master', 'stable29', 'stable28', 'stable27'] 25 | 26 | name: npm-audit-fix-${{ matrix.branches }} 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 31 | with: 32 | ref: ${{ matrix.branches }} 33 | 34 | - name: Read package.json node and npm engines version 35 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 36 | id: versions 37 | with: 38 | fallbackNode: '^20' 39 | fallbackNpm: '^10' 40 | 41 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 42 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 43 | with: 44 | node-version: ${{ steps.versions.outputs.nodeVersion }} 45 | 46 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 47 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 48 | 49 | - name: Fix npm audit 50 | id: npm-audit 51 | uses: nextcloud-libraries/npm-audit-action@2a60bd2e79cc77f2cc4d9a3fe40f1a69896f3a87 # v0.1.0 52 | 53 | - name: Run npm ci and npm run build 54 | if: always() 55 | env: 56 | CYPRESS_INSTALL_BINARY: 0 57 | run: | 58 | npm ci 59 | npm run build --if-present 60 | 61 | - name: Create Pull Request 62 | if: always() 63 | uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 64 | with: 65 | token: ${{ secrets.COMMAND_BOT_PAT }} 66 | commit-message: 'fix(deps): Fix npm audit' 67 | committer: GitHub 68 | author: nextcloud-command 69 | signoff: true 70 | branch: automated/noid/${{ matrix.branches }}-fix-npm-audit 71 | title: '[${{ matrix.branches }}] Fix npm audit' 72 | body: ${{ steps.npm-audit.outputs.markdown }} 73 | labels: | 74 | dependencies 75 | 3. to review 76 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Playwright Tests 4 | on: 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | timeout-minutes: 60 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout app 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Check composer.json 17 | id: check_composer 18 | uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v2 19 | with: 20 | files: 'composer.json' 21 | 22 | - name: Install composer dependencies 23 | if: steps.check_composer.outputs.files_exists == 'true' 24 | run: composer install --no-dev 25 | 26 | - name: Read package.json node and npm engines version 27 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 28 | id: versions 29 | with: 30 | fallbackNode: '^20' 31 | fallbackNpm: '^10' 32 | 33 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 34 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 35 | with: 36 | node-version: ${{ steps.versions.outputs.nodeVersion }} 37 | 38 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 39 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 40 | 41 | - name: Install node dependencies & build app 42 | run: | 43 | npm ci 44 | TESTING=true npm run build --if-present 45 | 46 | - name: Install Playwright Browsers 47 | run: npx playwright install chromium --with-deps 48 | 49 | - name: Run Playwright tests 50 | run: | 51 | npx playwright test 52 | 53 | - uses: actions/upload-artifact@v4 54 | if: always() 55 | with: 56 | name: playwright-report 57 | path: playwright-report/ 58 | retention-days: 30 59 | -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-FileCopyrightText: 2023 Marcel Klehr 8 | # SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> 9 | # SPDX-FileCopyrightText: 2023 Daniel Kesselberg 10 | # SPDX-FileCopyrightText: 2023 Florian Steffens 11 | # SPDX-License-Identifier: MIT 12 | 13 | name: 'Ask for feedback on PRs' 14 | on: 15 | schedule: 16 | - cron: '30 1 * * *' 17 | 18 | jobs: 19 | pr-feedback: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: The get-github-handles-from-website action 23 | uses: marcelklehr/get-github-handles-from-website-action@a739600f6b91da4957f51db0792697afbb2f143c # v1.0.0 24 | id: scrape 25 | with: 26 | website: 'https://nextcloud.com/team/' 27 | 28 | - name: Get blocklist 29 | id: blocklist 30 | run: | 31 | blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) 32 | echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" 33 | 34 | - uses: marcelklehr/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 35 | with: 36 | feedback-message: | 37 | Hello there, 38 | Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. 39 | 40 | We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. 41 | 42 | Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 43 | 44 | Thank you for contributing to Nextcloud and we hope to hear from you soon! 45 | 46 | (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) 47 | days-before-feedback: 14 48 | start-date: '2024-04-30' 49 | exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' 50 | exempt-bots: true 51 | -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Static analysis 10 | 11 | on: pull_request 12 | 13 | concurrency: 14 | group: psalm-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | static-analysis: 19 | runs-on: ubuntu-latest 20 | 21 | name: static-psalm-analysis 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 25 | 26 | - name: Get php version 27 | id: versions 28 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 29 | 30 | - name: Set up php${{ steps.versions.outputs.php-available }} 31 | uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 32 | with: 33 | php-version: ${{ steps.versions.outputs.php-available }} 34 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 35 | coverage: none 36 | ini-file: development 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Install dependencies 41 | run: composer i 42 | 43 | - name: Run coding standards check 44 | run: composer run psalm 45 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | jobs: 15 | reuse-compliance-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: REUSE Compliance Check 24 | uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | \.idea/ 3 | 4 | /build/ 5 | /backup/ 6 | /js/ 7 | /dist/ 8 | /css/ 9 | /vendor/ 10 | /node_modules/ 11 | /backup/ 12 | 13 | .php-cs-fixer.cache 14 | .phpunit.result.cache 15 | .env 16 | -------------------------------------------------------------------------------- /.l10nignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: none 2 | # SPDX-License-Identifier: CC0-1.0 3 | js/ 4 | websocket_server/ 5 | -------------------------------------------------------------------------------- /.nextcloudignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /.git 3 | /.github 4 | /docs/ 5 | /tests 6 | /babel.config.js 7 | /.editorconfig 8 | /.eslintrc.js 9 | /.nextcloudignore 10 | /webpack.*.js 11 | /.codecov.yml 12 | /composer.json 13 | /composer.lock 14 | /_config.yml 15 | /.drone.yml 16 | /.travis.yml 17 | /.eslintignore 18 | /.eslintrc.yml 19 | /.gitignore 20 | /issue_template.md 21 | /krankerl.toml 22 | /Makefile 23 | /mkdocs.yml 24 | /run-eslint.sh 25 | /package.json 26 | /package-lock.json 27 | /node_modules/ 28 | /screenshots/ 29 | /src/ 30 | /websocket_server/ 31 | /psalm.xml0i 32 | /tsconfig.json 33 | /Dockerfile 34 | /.stylelintrc.cjs 35 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | getFinder() 12 | // ->ignoreVCSIgnored(true) 13 | ->notPath('build') 14 | ->notPath('l10n') 15 | ->notPath('src') 16 | ->notPath('node_modules') 17 | ->notPath('vendor') 18 | ->in(__DIR__); 19 | return $config; 20 | -------------------------------------------------------------------------------- /.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const stylelintConfig = require('@nextcloud/stylelint-config') 7 | 8 | stylelintConfig.rules['no-invalid-position-at-import-rule'] = null 9 | 10 | module.exports = stylelintConfig 11 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = hu_HU: hu, nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi 4 | 5 | [o:nextcloud:p:nextcloud:r:whiteboard] 6 | file_filter = translationfiles//whiteboard.po 7 | source_file = translationfiles/templates/whiteboard.pot 8 | source_lang = en 9 | type = PO 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:latest 2 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | FROM node:23.11.0-alpine AS build 6 | SHELL ["/bin/ash", "-eo", "pipefail", "-c"] 7 | ARG NODE_ENV=production 8 | COPY . /app 9 | WORKDIR /app 10 | RUN apk upgrade --no-cache -a && \ 11 | apk add --no-cache ca-certificates && \ 12 | npm install --global clean-modules && \ 13 | npm clean-install && \ 14 | clean-modules --yes && \ 15 | npm cache clean --force 16 | 17 | FROM node:23.11.0-alpine 18 | COPY --from=build --chown=nobody:nobody /app /app 19 | WORKDIR /app 20 | RUN apk upgrade --no-cache -a && \ 21 | apk add --no-cache ca-certificates tzdata netcat-openbsd 22 | USER nobody 23 | EXPOSE 3002 24 | ENTRYPOINT ["npm", "run", "server:start"] 25 | HEALTHCHECK CMD nc -z 127.0.0.1 3002 || exit 1 26 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "whiteboard" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/whiteboard" 7 | 8 | [[annotations]] 9 | path = [".gitattributes", ".editorconfig", "babel.config.js", ".php-cs-fixer.dist.php", "package-lock.json", "package.json", "composer.json", "composer.lock", "webpack.js", "stylelint.config.js", ".eslintrc.js", "cypress/.eslintrc.json", ".gitignore", ".jshintrc", ".l10nignore", "action/.gitignore", "action/package.json", "action/package-lock.json", "action/dist/index.js", "tests/**", "psalm.xml", "vendor-bin/**/composer.json", "vendor-bin/**/composer.lock", ".tx/config", "webpack.config.js", "js/vendor.LICENSE.txt", ".github/CODEOWNERS", "vite.config.js", "stylelint.config.cjs", "composer/**.php", "composer/composer.**", "tsconfig.json", "jsconfig.json", "krankerl.toml", "renovate.json", ".github/ISSUE_TEMPLATE/**", ".nextcloudignore", "CHANGELOG.md", ".tsconfig.json"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "none" 12 | SPDX-License-Identifier = "CC0-1.0" 13 | 14 | [[annotations]] 15 | path = ["l10n/**.js", "l10n/**.json", "js/**.mjs.map", "js/**.mjs", "css/**", "screenshots/**"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2019-2024 Nextcloud GmbH and Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | [[annotations]] 21 | path = ["img/app.svg", "img/app-dark.svg", "img/app-filetype.svg"] 22 | precedence = "aggregate" 23 | SPDX-FileCopyrightText = "2019 Simran-B " 24 | SPDX-License-Identifier = "Apache-2.0" 25 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | whiteboard 8 | Whiteboard 9 | Whiteboard app 10 | 25 | 26 | 1.1.0-beta.1 27 | agpl 28 | Julius Härtl 29 | Whiteboard 30 | 31 | https://github.com/nextcloud/whiteboard/blob/main/README.md 32 | 33 | tools 34 | files 35 | office 36 | 37 | https://github.com/nextcloud/whiteboard 38 | https://github.com/nextcloud/whiteboard/issues 39 | https://github.com/nextcloud/whiteboard.git 40 | https://raw.githubusercontent.com/nextcloud/whiteboard/main/screenshots/screenshot1.png 41 | 42 | 43 | 44 | 45 | 46 | 47 | OCA\Whiteboard\Settings\Admin 48 | OCA\Whiteboard\Settings\Section 49 | 50 | 51 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | [ 16 | /** @see JWTController::getJWT() */ 17 | ['name' => 'JWT#getJWT', 'url' => '{fileId}/token', 'verb' => 'GET'], 18 | /** @see WhiteboardController::update() */ 19 | ['name' => 'Whiteboard#update', 'url' => '{fileId}', 'verb' => 'PUT'], 20 | /** @see WhiteboardController::show() */ 21 | ['name' => 'Whiteboard#show', 'url' => '{fileId}', 'verb' => 'GET'], 22 | /** @see SettingsController::update() */ 23 | ['name' => 'Settings#update', 'url' => 'settings', 'verb' => 'POST'], 24 | ] 25 | ]; 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcloud/whiteboard", 3 | "config": { 4 | "autoloader-suffix": "Whiteboard", 5 | "optimize-autoloader": true, 6 | "platform": { 7 | "php": "8.4" 8 | }, 9 | "sort-packages": true 10 | }, 11 | "license": "AGPL", 12 | "require": { 13 | "php": "^8.0", 14 | "firebase/php-jwt": "^6.10" 15 | }, 16 | "require-dev": { 17 | "nextcloud/coding-standard": "^1.3.2", 18 | "nextcloud/ocp": "dev-master", 19 | "phpunit/phpunit": "^12", 20 | "psalm/phar": "^6.0", 21 | "psr/log": "^3.0.2", 22 | "roave/security-advisories": "dev-latest", 23 | "sabre/dav": "^4.3" 24 | }, 25 | "scripts": { 26 | "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l", 27 | "cs:check": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run", 28 | "cs:fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix", 29 | "psalm": "psalm.phar", 30 | "test:unit": "phpunit -c tests/phpunit.xml" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | version: '3.7' 5 | services: 6 | nextcloud-whiteboard-server: 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | ports: 11 | - 3002:3002 12 | environment: 13 | - NEXTCLOUD_URL 14 | - JWT_SECRET_KEY 15 | ## if you run this rootess, backup_dir needs to be in a place writeable to the non-root process, for example: 16 | - BACKUP_DIR=/tmp/backup 17 | -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/app-filetype.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /krankerl.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | before_cmds = [ 3 | 'composer install --no-dev', 4 | 'npm ci', 5 | 'npm run build' 6 | ] 7 | -------------------------------------------------------------------------------- /l10n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/whiteboard/924fc4a300b6a37ef39a9c91e5c4f053eaace1f3/l10n/.gitkeep -------------------------------------------------------------------------------- /l10n/ast.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Pizarra", 5 | "Advanced settings" : "Configuración avanzada" 6 | }, 7 | "nplurals=2; plural=(n != 1);"); 8 | -------------------------------------------------------------------------------- /l10n/ast.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Pizarra", 3 | "Advanced settings" : "Configuración avanzada" 4 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 5 | } -------------------------------------------------------------------------------- /l10n/bg.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Табло", 5 | "Shared secret" : "Споделена тайна", 6 | "Save settings" : "Запазване на настройките", 7 | "Advanced settings" : "Допълнителни настройки" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/bg.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Табло", 3 | "Shared secret" : "Споделена тайна", 4 | "Save settings" : "Запазване на настройките", 5 | "Advanced settings" : "Допълнителни настройки" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/ca.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Pissarra blanca", 5 | "Shared secret" : "Clau secreta compartida", 6 | "Save settings" : "Desa els paràmetres", 7 | "Advanced settings" : "Paràmetres avançats" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/ca.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Pissarra blanca", 3 | "Shared secret" : "Clau secreta compartida", 4 | "Save settings" : "Desa els paràmetres", 5 | "Advanced settings" : "Paràmetres avançats" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/cs.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Nová tabule", 5 | "Create new whiteboard" : "Vytvořit novou tabuli", 6 | "Whiteboard" : "Tabule", 7 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "URL serveru, poskytujícího tabule, není nastavená. Tabule vyžadují samostatný server pro spolupráci, který je připojený k Nextcloud.", 8 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud serveru se nepodařilo připojit k serveru s tabulí: %s", 9 | "No version provided by /status enpdoint" : "Koncovým bodem /status neposkytnuta žádná verze", 10 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "Na serveru s podpůrnou vrstvou je provozována odlišná verze – aktualizujte obě na stejnou verzi. Aplikace: %s Podpůrná vrstva: %s", 11 | "Whiteboard backend server could not reach Nextcloud: %s" : "Serveru podpůrné vrstvy pro Nástěnku se nepodařilo zkontaktovat Nextcloud: %s", 12 | "Failed to connect to whiteboard server status endpoint: %s" : "Nepodařilo se spojit s koncovým bodem serveru, poskytujícího tabuli: %s", 13 | "Whiteboard server configured properly" : "Server, poskytující tabuli, nastavený správně", 14 | "Whiteboard app" : "Aplikace Tabule", 15 | "Whiteboard server" : "Server poskytující tabuli", 16 | "Whiteboard backend server is configured and connected." : "Server podpůrné vrstvy pro Tabuli je nastavený a připojený.", 17 | "Failed to verify the connection:" : "Nepodařilo se ověřit připojení:", 18 | "Verifying connection…" : "Ověřování připojení…", 19 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Tabule vyžaduje oddělený server pro spolupráci, který je připojený k Nextcloud.", 20 | "See the documentation on how to install it." : "Pokyny k jeho instalaci naleznete v dokumentaci.", 21 | "Whiteboard server URL" : "URL whiteboard serveru", 22 | "This URL is used by the browser to connect to the whiteboard server." : "Tato URL slouží prohlížeči pro spojení se se serverem, poskytujícím tabuli.", 23 | "Internal whiteboard server URL" : "Interní URL serveru, poskytujícího tabuli", 24 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Tato URL slouží Nextcloud serveru pro spojení se se serverem, poskytujícím tabuli.", 25 | "Skip TLS certificate validation (not recommended)" : "Přeskočit ověřování TLS certifikátu (nedoporučeno)", 26 | "Shared secret" : "Předsdílené heslo", 27 | "Save settings" : "Uložit nastavení", 28 | "Advanced settings" : "Pokročilá nastavení", 29 | "Max file size" : "Nejvyšší umožněná velikost souboru" 30 | }, 31 | "nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"); 32 | -------------------------------------------------------------------------------- /l10n/cs.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Nová tabule", 3 | "Create new whiteboard" : "Vytvořit novou tabuli", 4 | "Whiteboard" : "Tabule", 5 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "URL serveru, poskytujícího tabule, není nastavená. Tabule vyžadují samostatný server pro spolupráci, který je připojený k Nextcloud.", 6 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud serveru se nepodařilo připojit k serveru s tabulí: %s", 7 | "No version provided by /status enpdoint" : "Koncovým bodem /status neposkytnuta žádná verze", 8 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "Na serveru s podpůrnou vrstvou je provozována odlišná verze – aktualizujte obě na stejnou verzi. Aplikace: %s Podpůrná vrstva: %s", 9 | "Whiteboard backend server could not reach Nextcloud: %s" : "Serveru podpůrné vrstvy pro Nástěnku se nepodařilo zkontaktovat Nextcloud: %s", 10 | "Failed to connect to whiteboard server status endpoint: %s" : "Nepodařilo se spojit s koncovým bodem serveru, poskytujícího tabuli: %s", 11 | "Whiteboard server configured properly" : "Server, poskytující tabuli, nastavený správně", 12 | "Whiteboard app" : "Aplikace Tabule", 13 | "Whiteboard server" : "Server poskytující tabuli", 14 | "Whiteboard backend server is configured and connected." : "Server podpůrné vrstvy pro Tabuli je nastavený a připojený.", 15 | "Failed to verify the connection:" : "Nepodařilo se ověřit připojení:", 16 | "Verifying connection…" : "Ověřování připojení…", 17 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Tabule vyžaduje oddělený server pro spolupráci, který je připojený k Nextcloud.", 18 | "See the documentation on how to install it." : "Pokyny k jeho instalaci naleznete v dokumentaci.", 19 | "Whiteboard server URL" : "URL whiteboard serveru", 20 | "This URL is used by the browser to connect to the whiteboard server." : "Tato URL slouží prohlížeči pro spojení se se serverem, poskytujícím tabuli.", 21 | "Internal whiteboard server URL" : "Interní URL serveru, poskytujícího tabuli", 22 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Tato URL slouží Nextcloud serveru pro spojení se se serverem, poskytujícím tabuli.", 23 | "Skip TLS certificate validation (not recommended)" : "Přeskočit ověřování TLS certifikátu (nedoporučeno)", 24 | "Shared secret" : "Předsdílené heslo", 25 | "Save settings" : "Uložit nastavení", 26 | "Advanced settings" : "Pokročilá nastavení", 27 | "Max file size" : "Nejvyšší umožněná velikost souboru" 28 | },"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;" 29 | } -------------------------------------------------------------------------------- /l10n/eo.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Save settings" : "Konservi agordojn", 5 | "Advanced settings" : "Altanivela agordo" 6 | }, 7 | "nplurals=2; plural=(n != 1);"); 8 | -------------------------------------------------------------------------------- /l10n/eo.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Save settings" : "Konservi agordojn", 3 | "Advanced settings" : "Altanivela agordo" 4 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 5 | } -------------------------------------------------------------------------------- /l10n/es.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Nueva pizarra", 5 | "Whiteboard" : "Pizarra blanca", 6 | "Failed to verify the connection:" : "No se pudo verificar la conexión:", 7 | "Verifying connection…" : "Verificando conexión…", 8 | "See the documentation on how to install it." : "Consulte la documentación sobre cómo instalarlo.", 9 | "Shared secret" : "Secreto compartido", 10 | "Save settings" : "Guardar configuración", 11 | "Advanced settings" : "Configuración avanzada" 12 | }, 13 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 14 | -------------------------------------------------------------------------------- /l10n/es.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Nueva pizarra", 3 | "Whiteboard" : "Pizarra blanca", 4 | "Failed to verify the connection:" : "No se pudo verificar la conexión:", 5 | "Verifying connection…" : "Verificando conexión…", 6 | "See the documentation on how to install it." : "Consulte la documentación sobre cómo instalarlo.", 7 | "Shared secret" : "Secreto compartido", 8 | "Save settings" : "Guardar configuración", 9 | "Advanced settings" : "Configuración avanzada" 10 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 11 | } -------------------------------------------------------------------------------- /l10n/es_419.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_419.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_CL.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_CL.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_CO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_CO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_CR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_CR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_DO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_DO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_EC.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Pizarra", 5 | "Shared secret" : "Secreto compartido", 6 | "Save settings" : "Guardar configuraciones", 7 | "Advanced settings" : "Configuraciones avanzados" 8 | }, 9 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 10 | -------------------------------------------------------------------------------- /l10n/es_EC.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Pizarra", 3 | "Shared secret" : "Secreto compartido", 4 | "Save settings" : "Guardar configuraciones", 5 | "Advanced settings" : "Configuraciones avanzados" 6 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 7 | } -------------------------------------------------------------------------------- /l10n/es_GT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_GT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_HN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_HN.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_MX.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Pizarrón", 5 | "Shared secret" : "Secreto compartido", 6 | "Save settings" : "Guardar configuraciones", 7 | "Advanced settings" : "Configuraciones avanzadas" 8 | }, 9 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 10 | -------------------------------------------------------------------------------- /l10n/es_MX.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Pizarrón", 3 | "Shared secret" : "Secreto compartido", 4 | "Save settings" : "Guardar configuraciones", 5 | "Advanced settings" : "Configuraciones avanzadas" 6 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 7 | } -------------------------------------------------------------------------------- /l10n/es_NI.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_NI.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_PA.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_PA.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_PE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_PE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_PR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_PR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_PY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_PY.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_SV.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_SV.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/es_UY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secreto compartido", 5 | "Save settings" : "Guardar configuraciones", 6 | "Advanced settings" : "Configuraciones avanzados" 7 | }, 8 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/es_UY.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secreto compartido", 3 | "Save settings" : "Guardar configuraciones", 4 | "Advanced settings" : "Configuraciones avanzados" 5 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/et_EE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Valgetahvel", 5 | "Shared secret" : "Jagatud saladus", 6 | "Save settings" : "Salvesta seaded", 7 | "Advanced settings" : "Lisavalikud" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/et_EE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Valgetahvel", 3 | "Shared secret" : "Jagatud saladus", 4 | "Save settings" : "Salvesta seaded", 5 | "Advanced settings" : "Lisavalikud" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/eu.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Arbel zuria", 5 | "Shared secret" : "Partekatutako sekretua", 6 | "Save settings" : "Gorde ezarpenak", 7 | "Advanced settings" : "Ezarpen aurreratuak" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/eu.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Arbel zuria", 3 | "Shared secret" : "Partekatutako sekretua", 4 | "Save settings" : "Gorde ezarpenak", 5 | "Advanced settings" : "Ezarpen aurreratuak" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/fa.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Whiteboard", 5 | "Shared secret" : "Shared secret", 6 | "Advanced settings" : "تنظیمات پیشرفته" 7 | }, 8 | "nplurals=2; plural=(n > 1);"); 9 | -------------------------------------------------------------------------------- /l10n/fa.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Whiteboard", 3 | "Shared secret" : "Shared secret", 4 | "Advanced settings" : "تنظیمات پیشرفته" 5 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 6 | } -------------------------------------------------------------------------------- /l10n/fi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Uusi valkotaulu", 5 | "Create new whiteboard" : "Luo uusi valkotaulu", 6 | "Whiteboard" : "Valkotaulu", 7 | "Whiteboard app" : "Valkotaulusovellus", 8 | "Whiteboard backend server is configured and connected." : "Valkotaulun taustaosan palvelin on määritetty ja yhteys muodostettu.", 9 | "Failed to verify the connection:" : "Yhteyden vahvistaminen epäonnistui:", 10 | "Verifying connection…" : "Vahvistetaan yhteyttä…", 11 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Valkotaulu vaatii erillisen yhteistyöpalvelimen, joka on yhdistetty Nextcloudiin.", 12 | "See the documentation on how to install it." : "Lue dokumentaatiosta tietoa miten asentaa se.", 13 | "Whiteboard server URL" : "Valkotaulupalvelimen URL-osoite", 14 | "Shared secret" : "Jaettu salaisuus", 15 | "Save settings" : "Tallenna asetukset", 16 | "Advanced settings" : "Lisäasetukset" 17 | }, 18 | "nplurals=2; plural=(n != 1);"); 19 | -------------------------------------------------------------------------------- /l10n/fi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Uusi valkotaulu", 3 | "Create new whiteboard" : "Luo uusi valkotaulu", 4 | "Whiteboard" : "Valkotaulu", 5 | "Whiteboard app" : "Valkotaulusovellus", 6 | "Whiteboard backend server is configured and connected." : "Valkotaulun taustaosan palvelin on määritetty ja yhteys muodostettu.", 7 | "Failed to verify the connection:" : "Yhteyden vahvistaminen epäonnistui:", 8 | "Verifying connection…" : "Vahvistetaan yhteyttä…", 9 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Valkotaulu vaatii erillisen yhteistyöpalvelimen, joka on yhdistetty Nextcloudiin.", 10 | "See the documentation on how to install it." : "Lue dokumentaatiosta tietoa miten asentaa se.", 11 | "Whiteboard server URL" : "Valkotaulupalvelimen URL-osoite", 12 | "Shared secret" : "Jaettu salaisuus", 13 | "Save settings" : "Tallenna asetukset", 14 | "Advanced settings" : "Lisäasetukset" 15 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 16 | } -------------------------------------------------------------------------------- /l10n/fr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Nouveau tableau blanc", 5 | "Create new whiteboard" : "Créer un nouveau tableau blanc", 6 | "Whiteboard" : "Tableau blanc", 7 | "Whiteboard app" : "App tableau blanc", 8 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "L'application de tableau blanc officielle pour Nextcloud. Elle permet aux utilisateurs de créer et de partager des tableaux blancs avec d'autres utilisateurs et de collaborer en temps réel.\n\n**Le tableau blanc nécessite un serveur de collaboration distinct pour fonctionner.** Veuillez consulter la [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) pour savoir comment l'installer.\n\n- 🎨 Dessiner des formes, écrire du texte, connecter des éléments\n- 📝 Collaboration en temps réel\n- 🖼️ Ajouter des images par glisser-déposer\n- 📊 Ajouter facilement des diagrammes de sirène\n- ✨ Utiliser le Smart Picker pour intégrer d'autres éléments de Nextcloud\n- 📦 Export d'images\n- 💪 Base solide : nous utilisons Excalidraw comme bibliothèque de base", 9 | "Whiteboard backend server is configured and connected." : "Le serveur backend du tableau blanc est configuré et connecté.", 10 | "Failed to verify the connection:" : "Erreur lors de la vérification de la connexion.", 11 | "Verifying connection…" : "Vérification de la connexion ...", 12 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Le tableau blanc nécessite un serveur de collaboration distinct qui est connecté à Nextcloud.", 13 | "See the documentation on how to install it." : "Consultez la documentation d'installation.", 14 | "Whiteboard server URL" : "URL du serveur du tableau blanc", 15 | "Shared secret" : "Secret partagé", 16 | "Save settings" : "Enregistrer les paramètres", 17 | "Advanced settings" : "Paramètres avancés" 18 | }, 19 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 20 | -------------------------------------------------------------------------------- /l10n/fr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Nouveau tableau blanc", 3 | "Create new whiteboard" : "Créer un nouveau tableau blanc", 4 | "Whiteboard" : "Tableau blanc", 5 | "Whiteboard app" : "App tableau blanc", 6 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "L'application de tableau blanc officielle pour Nextcloud. Elle permet aux utilisateurs de créer et de partager des tableaux blancs avec d'autres utilisateurs et de collaborer en temps réel.\n\n**Le tableau blanc nécessite un serveur de collaboration distinct pour fonctionner.** Veuillez consulter la [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) pour savoir comment l'installer.\n\n- 🎨 Dessiner des formes, écrire du texte, connecter des éléments\n- 📝 Collaboration en temps réel\n- 🖼️ Ajouter des images par glisser-déposer\n- 📊 Ajouter facilement des diagrammes de sirène\n- ✨ Utiliser le Smart Picker pour intégrer d'autres éléments de Nextcloud\n- 📦 Export d'images\n- 💪 Base solide : nous utilisons Excalidraw comme bibliothèque de base", 7 | "Whiteboard backend server is configured and connected." : "Le serveur backend du tableau blanc est configuré et connecté.", 8 | "Failed to verify the connection:" : "Erreur lors de la vérification de la connexion.", 9 | "Verifying connection…" : "Vérification de la connexion ...", 10 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Le tableau blanc nécessite un serveur de collaboration distinct qui est connecté à Nextcloud.", 11 | "See the documentation on how to install it." : "Consultez la documentation d'installation.", 12 | "Whiteboard server URL" : "URL du serveur du tableau blanc", 13 | "Shared secret" : "Secret partagé", 14 | "Save settings" : "Enregistrer les paramètres", 15 | "Advanced settings" : "Paramètres avancés" 16 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 17 | } -------------------------------------------------------------------------------- /l10n/he.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "סוד משותף", 5 | "Advanced settings" : "הגדרות מתקדמות" 6 | }, 7 | "nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"); 8 | -------------------------------------------------------------------------------- /l10n/he.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "סוד משותף", 3 | "Advanced settings" : "הגדרות מתקדמות" 4 | },"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;" 5 | } -------------------------------------------------------------------------------- /l10n/hr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Ploča za pisanje", 5 | "Shared secret" : "Dijeljen tajni ključ", 6 | "Advanced settings" : "Napredne postavke" 7 | }, 8 | "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"); 9 | -------------------------------------------------------------------------------- /l10n/hr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Ploča za pisanje", 3 | "Shared secret" : "Dijeljen tajni ključ", 4 | "Advanced settings" : "Napredne postavke" 5 | },"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" 6 | } -------------------------------------------------------------------------------- /l10n/hu.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Tábla", 5 | "Shared secret" : "Megosztott titok", 6 | "Save settings" : "Beállítások mentése", 7 | "Advanced settings" : "Speciális beállítások" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/hu.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Tábla", 3 | "Shared secret" : "Megosztott titok", 4 | "Save settings" : "Beállítások mentése", 5 | "Advanced settings" : "Speciális beállítások" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/is.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Teiknitafla", 5 | "Shared secret" : "Sameiginlegur leynilykill", 6 | "Save settings" : "Vista stillingar", 7 | "Advanced settings" : "Ítarlegri valkostir" 8 | }, 9 | "nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); 10 | -------------------------------------------------------------------------------- /l10n/is.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Teiknitafla", 3 | "Shared secret" : "Sameiginlegur leynilykill", 4 | "Save settings" : "Vista stillingar", 5 | "Advanced settings" : "Ítarlegri valkostir" 6 | },"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);" 7 | } -------------------------------------------------------------------------------- /l10n/ja.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "新規 ホワイトボード", 5 | "Create new whiteboard" : "新しいホワイトボードを作成する", 6 | "Whiteboard" : "ホワイトボード", 7 | "Whiteboard app" : "ホワイトボード アプリ", 8 | "Whiteboard backend server is configured and connected." : "ホワイトボードバックエンドサーバーが設定され、接続されています。", 9 | "Failed to verify the connection:" : "接続の検証に失敗しました:", 10 | "Verifying connection…" : "接続の確認...", 11 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "ホワイトボードにはNextcloudに接続された別のコラボレーションサーバーが必要です。", 12 | "See the documentation on how to install it." : "インストール方法については、ドキュメントを参照してください。", 13 | "Whiteboard server URL" : "ホワイトボード サーバー URL", 14 | "Shared secret" : "共有秘密鍵", 15 | "Save settings" : "設定を保存", 16 | "Advanced settings" : "詳細設定" 17 | }, 18 | "nplurals=1; plural=0;"); 19 | -------------------------------------------------------------------------------- /l10n/ja.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "新規 ホワイトボード", 3 | "Create new whiteboard" : "新しいホワイトボードを作成する", 4 | "Whiteboard" : "ホワイトボード", 5 | "Whiteboard app" : "ホワイトボード アプリ", 6 | "Whiteboard backend server is configured and connected." : "ホワイトボードバックエンドサーバーが設定され、接続されています。", 7 | "Failed to verify the connection:" : "接続の検証に失敗しました:", 8 | "Verifying connection…" : "接続の確認...", 9 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "ホワイトボードにはNextcloudに接続された別のコラボレーションサーバーが必要です。", 10 | "See the documentation on how to install it." : "インストール方法については、ドキュメントを参照してください。", 11 | "Whiteboard server URL" : "ホワイトボード サーバー URL", 12 | "Shared secret" : "共有秘密鍵", 13 | "Save settings" : "設定を保存", 14 | "Advanced settings" : "詳細設定" 15 | },"pluralForm" :"nplurals=1; plural=0;" 16 | } -------------------------------------------------------------------------------- /l10n/ka.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Whiteboard", 5 | "Shared secret" : "Shared secret", 6 | "Advanced settings" : "Advanced settings" 7 | }, 8 | "nplurals=2; plural=(n!=1);"); 9 | -------------------------------------------------------------------------------- /l10n/ka.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Whiteboard", 3 | "Shared secret" : "Shared secret", 4 | "Advanced settings" : "Advanced settings" 5 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 6 | } -------------------------------------------------------------------------------- /l10n/ka_GE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "გაზიარებული საიდუმლო", 5 | "Save settings" : "პარამეტრების შენახვა", 6 | "Advanced settings" : "დამატებითი პარამეტრები" 7 | }, 8 | "nplurals=2; plural=(n!=1);"); 9 | -------------------------------------------------------------------------------- /l10n/ka_GE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "გაზიარებული საიდუმლო", 3 | "Save settings" : "პარამეტრების შენახვა", 4 | "Advanced settings" : "დამატებითი პარამეტრები" 5 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 6 | } -------------------------------------------------------------------------------- /l10n/ko.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "새 화이트보드", 5 | "Create new whiteboard" : "새 화이트보드 생성", 6 | "Whiteboard" : "화이트보드", 7 | "Whiteboard app" : "화이트보드 앱", 8 | "Whiteboard backend server is configured and connected." : "화이트보드 백엔드 서버가 설정되었고 연결되었습니다.", 9 | "Failed to verify the connection:" : "연결을 확인하는데 실패하였습니다:", 10 | "Verifying connection…" : "연결을 확인하는 중입니다...", 11 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "화이트보드는 Nextcloud와 연결된 별도의 협업 서버가 필요합니다.", 12 | "See the documentation on how to install it." : "인스톨 방법에 관해서는 문서를 참조해주시기 바랍니다.", 13 | "Whiteboard server URL" : "화이트보드 서버 URL", 14 | "Shared secret" : "공유된 비밀 값", 15 | "Save settings" : "설정 저장", 16 | "Advanced settings" : "고급 설정" 17 | }, 18 | "nplurals=1; plural=0;"); 19 | -------------------------------------------------------------------------------- /l10n/ko.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "새 화이트보드", 3 | "Create new whiteboard" : "새 화이트보드 생성", 4 | "Whiteboard" : "화이트보드", 5 | "Whiteboard app" : "화이트보드 앱", 6 | "Whiteboard backend server is configured and connected." : "화이트보드 백엔드 서버가 설정되었고 연결되었습니다.", 7 | "Failed to verify the connection:" : "연결을 확인하는데 실패하였습니다:", 8 | "Verifying connection…" : "연결을 확인하는 중입니다...", 9 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "화이트보드는 Nextcloud와 연결된 별도의 협업 서버가 필요합니다.", 10 | "See the documentation on how to install it." : "인스톨 방법에 관해서는 문서를 참조해주시기 바랍니다.", 11 | "Whiteboard server URL" : "화이트보드 서버 URL", 12 | "Shared secret" : "공유된 비밀 값", 13 | "Save settings" : "설정 저장", 14 | "Advanced settings" : "고급 설정" 15 | },"pluralForm" :"nplurals=1; plural=0;" 16 | } -------------------------------------------------------------------------------- /l10n/lt_LT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Nauja rašymo lenta", 5 | "Create new whiteboard" : "Sukurti naują rašymo lentą", 6 | "Whiteboard" : "Rašymo lenta", 7 | "Whiteboard app" : "Rašymo lentos programėlė", 8 | "Failed to verify the connection:" : "Nepavyko patikrinti ryšio:", 9 | "Verifying connection…" : "Tikrinamas ryšys…", 10 | "Save settings" : "Įrašyti nustatymus", 11 | "Advanced settings" : "Išplėstiniai nustatymai" 12 | }, 13 | "nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); 14 | -------------------------------------------------------------------------------- /l10n/lt_LT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Nauja rašymo lenta", 3 | "Create new whiteboard" : "Sukurti naują rašymo lentą", 4 | "Whiteboard" : "Rašymo lenta", 5 | "Whiteboard app" : "Rašymo lentos programėlė", 6 | "Failed to verify the connection:" : "Nepavyko patikrinti ryšio:", 7 | "Verifying connection…" : "Tikrinamas ryšys…", 8 | "Save settings" : "Įrašyti nustatymus", 9 | "Advanced settings" : "Išplėstiniai nustatymai" 10 | },"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" 11 | } -------------------------------------------------------------------------------- /l10n/lv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Tāfele", 5 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "Aizmugures serveris darbojas ar citu versiju, jānodrošina abu jaunināšana uz vienu un to pašu versiju. Lietotne: %s, aizmugures versija: %s", 6 | "Shared secret" : "Koplietojams noslēpums", 7 | "Save settings" : "Saglabāt iestatījumus", 8 | "Advanced settings" : "Paplašināti iestatījumi" 9 | }, 10 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"); 11 | -------------------------------------------------------------------------------- /l10n/lv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Tāfele", 3 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "Aizmugures serveris darbojas ar citu versiju, jānodrošina abu jaunināšana uz vienu un to pašu versiju. Lietotne: %s, aizmugures versija: %s", 4 | "Shared secret" : "Koplietojams noslēpums", 5 | "Save settings" : "Saglabāt iestatījumus", 6 | "Advanced settings" : "Paplašināti iestatījumi" 7 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" 8 | } -------------------------------------------------------------------------------- /l10n/mk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Табла", 5 | "Save settings" : "Зачувај параметри", 6 | "Advanced settings" : "Напредни параметри" 7 | }, 8 | "nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); 9 | -------------------------------------------------------------------------------- /l10n/mk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Табла", 3 | "Save settings" : "Зачувај параметри", 4 | "Advanced settings" : "Напредни параметри" 5 | },"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" 6 | } -------------------------------------------------------------------------------- /l10n/mn.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Save settings" : "тохиргоог хадгалах", 5 | "Advanced settings" : "Нарийвчилсан тохиргоо" 6 | }, 7 | "nplurals=2; plural=(n != 1);"); 8 | -------------------------------------------------------------------------------- /l10n/mn.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Save settings" : "тохиргоог хадгалах", 3 | "Advanced settings" : "Нарийвчилсан тохиргоо" 4 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 5 | } -------------------------------------------------------------------------------- /l10n/nb.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Ny tavle", 5 | "Create new whiteboard" : "Opprett ny tavle", 6 | "Whiteboard" : "Tavle", 7 | "Whiteboard app" : "Whiteboard-app", 8 | "Whiteboard backend server is configured and connected." : "Whiteboard-backendserveren er konfigurert og tilkoblet.", 9 | "Failed to verify the connection:" : "Verifisering av forbindelsen feilet:", 10 | "Verifying connection…" : "Verifiserer forbindelse...", 11 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Whiteboard krever en egen samarbeidsserver som er koblet til Nextcloud.", 12 | "See the documentation on how to install it." : "Se dokumentasjonen for hvordan du installerer den.", 13 | "Whiteboard server URL" : "Whiteboard server URL", 14 | "Shared secret" : "Delt hemmelighet", 15 | "Save settings" : "Lagre innstillinger", 16 | "Advanced settings" : "Avanserte innstillinger" 17 | }, 18 | "nplurals=2; plural=(n != 1);"); 19 | -------------------------------------------------------------------------------- /l10n/nb.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Ny tavle", 3 | "Create new whiteboard" : "Opprett ny tavle", 4 | "Whiteboard" : "Tavle", 5 | "Whiteboard app" : "Whiteboard-app", 6 | "Whiteboard backend server is configured and connected." : "Whiteboard-backendserveren er konfigurert og tilkoblet.", 7 | "Failed to verify the connection:" : "Verifisering av forbindelsen feilet:", 8 | "Verifying connection…" : "Verifiserer forbindelse...", 9 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Whiteboard krever en egen samarbeidsserver som er koblet til Nextcloud.", 10 | "See the documentation on how to install it." : "Se dokumentasjonen for hvordan du installerer den.", 11 | "Whiteboard server URL" : "Whiteboard server URL", 12 | "Shared secret" : "Delt hemmelighet", 13 | "Save settings" : "Lagre innstillinger", 14 | "Advanced settings" : "Avanserte innstillinger" 15 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 16 | } -------------------------------------------------------------------------------- /l10n/nl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Whiteboard", 5 | "Shared secret" : "Gedeeld geheim", 6 | "Save settings" : "Opslaan instellingen", 7 | "Advanced settings" : "Geavanceerde instellingen" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/nl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Whiteboard", 3 | "Shared secret" : "Gedeeld geheim", 4 | "Save settings" : "Opslaan instellingen", 5 | "Advanced settings" : "Geavanceerde instellingen" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/oc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Secret partejat", 5 | "Save settings" : "Salvar paramètres", 6 | "Advanced settings" : "Paramètres avançats" 7 | }, 8 | "nplurals=2; plural=(n > 1);"); 9 | -------------------------------------------------------------------------------- /l10n/oc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Secret partejat", 3 | "Save settings" : "Salvar paramètres", 4 | "Advanced settings" : "Paramètres avançats" 5 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 6 | } -------------------------------------------------------------------------------- /l10n/pl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "Nowa tablica", 5 | "Create new whiteboard" : "Utwórz nową tablicę", 6 | "Whiteboard" : "Tablica", 7 | "Whiteboard app" : "Aplikacja tablicowa", 8 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Oficjalna aplikacja tablicowa dla Nextcloud. Umożliwia użytkownikom tworzenie i udostępnianie tablic innym użytkownikom oraz współpracę w czasie rzeczywistym.\n\n**Tablica wymaga do działania oddzielnego serwera współpracy.** Zapoznaj się z [dokumentacją](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend), jak ją zainstalować.\n\n- 🎨 Rysowanie kształtów, pisanie tekstu, łączenie elementów\n- 📝 Współpraca w czasie rzeczywistym\n- 🖼️ Dodawaj obrazy metodą przeciągnij i upuść\n- 📊 Z łatwością dodawaj syrenie diagramy\n- ✨ Użyj inteligentnego wyboru, aby osadzić inne elementy z Nextcloud\n- 📦 Eksport obrazu\n- 💪 Mocny fundament: Używamy Excalidraw jako naszej podstawowej biblioteki", 9 | "Whiteboard backend server is configured and connected." : "Serwer zaplecza tablicy jest skonfigurowany i podłączony.", 10 | "Failed to verify the connection:" : "Nie udało się zweryfikować połączenia:", 11 | "Verifying connection…" : "Weryfikacja połączenia…", 12 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Tablica wymaga osobnego serwera współpracy połączonego z Nextcloud.", 13 | "See the documentation on how to install it." : "Zobacz dokumentację dotyczącą sposobu instalacji.", 14 | "Whiteboard server URL" : "Adres URL serwera tablicy", 15 | "Shared secret" : "Klucz współdzielony", 16 | "Save settings" : "Zapisz ustawienia", 17 | "Advanced settings" : "Ustawienia zaawansowane" 18 | }, 19 | "nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); 20 | -------------------------------------------------------------------------------- /l10n/pl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "Nowa tablica", 3 | "Create new whiteboard" : "Utwórz nową tablicę", 4 | "Whiteboard" : "Tablica", 5 | "Whiteboard app" : "Aplikacja tablicowa", 6 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Oficjalna aplikacja tablicowa dla Nextcloud. Umożliwia użytkownikom tworzenie i udostępnianie tablic innym użytkownikom oraz współpracę w czasie rzeczywistym.\n\n**Tablica wymaga do działania oddzielnego serwera współpracy.** Zapoznaj się z [dokumentacją](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend), jak ją zainstalować.\n\n- 🎨 Rysowanie kształtów, pisanie tekstu, łączenie elementów\n- 📝 Współpraca w czasie rzeczywistym\n- 🖼️ Dodawaj obrazy metodą przeciągnij i upuść\n- 📊 Z łatwością dodawaj syrenie diagramy\n- ✨ Użyj inteligentnego wyboru, aby osadzić inne elementy z Nextcloud\n- 📦 Eksport obrazu\n- 💪 Mocny fundament: Używamy Excalidraw jako naszej podstawowej biblioteki", 7 | "Whiteboard backend server is configured and connected." : "Serwer zaplecza tablicy jest skonfigurowany i podłączony.", 8 | "Failed to verify the connection:" : "Nie udało się zweryfikować połączenia:", 9 | "Verifying connection…" : "Weryfikacja połączenia…", 10 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "Tablica wymaga osobnego serwera współpracy połączonego z Nextcloud.", 11 | "See the documentation on how to install it." : "Zobacz dokumentację dotyczącą sposobu instalacji.", 12 | "Whiteboard server URL" : "Adres URL serwera tablicy", 13 | "Shared secret" : "Klucz współdzielony", 14 | "Save settings" : "Zapisz ustawienia", 15 | "Advanced settings" : "Ustawienia zaawansowane" 16 | },"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);" 17 | } -------------------------------------------------------------------------------- /l10n/pt_PT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Segredo partilhado", 5 | "Advanced settings" : "Definições avançadas" 6 | }, 7 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 8 | -------------------------------------------------------------------------------- /l10n/pt_PT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Segredo partilhado", 3 | "Advanced settings" : "Definições avançadas" 4 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 5 | } -------------------------------------------------------------------------------- /l10n/ro.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Tablă de scris", 5 | "Save settings" : "Salvează setări", 6 | "Advanced settings" : "Setări avansate" 7 | }, 8 | "nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"); 9 | -------------------------------------------------------------------------------- /l10n/ro.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Tablă de scris", 3 | "Save settings" : "Salvează setări", 4 | "Advanced settings" : "Setări avansate" 5 | },"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));" 6 | } -------------------------------------------------------------------------------- /l10n/ru.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Доска", 5 | "Shared secret" : "Общая секретная фраза", 6 | "Save settings" : "Сохранить изменения", 7 | "Advanced settings" : "Расширенные параметры" 8 | }, 9 | "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); 10 | -------------------------------------------------------------------------------- /l10n/ru.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Доска", 3 | "Shared secret" : "Общая секретная фраза", 4 | "Save settings" : "Сохранить изменения", 5 | "Advanced settings" : "Расширенные параметры" 6 | },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" 7 | } -------------------------------------------------------------------------------- /l10n/sc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Shared secret" : "Segretu cumpartzidu", 5 | "Advanced settings" : "Cunfiguratzione avantzada" 6 | }, 7 | "nplurals=2; plural=(n != 1);"); 8 | -------------------------------------------------------------------------------- /l10n/sc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Shared secret" : "Segretu cumpartzidu", 3 | "Advanced settings" : "Cunfiguratzione avantzada" 4 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 5 | } -------------------------------------------------------------------------------- /l10n/sl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Bela tabla", 5 | "Shared secret" : "Skrivna koda", 6 | "Save settings" : "Shrani nastavitve", 7 | "Advanced settings" : "Napredne nastavitve" 8 | }, 9 | "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"); 10 | -------------------------------------------------------------------------------- /l10n/sl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Bela tabla", 3 | "Shared secret" : "Skrivna koda", 4 | "Save settings" : "Shrani nastavitve", 5 | "Advanced settings" : "Napredne nastavitve" 6 | },"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" 7 | } -------------------------------------------------------------------------------- /l10n/sq.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Save settings" : "Ruaj konfigurimet", 5 | "Advanced settings" : "Rregullime të mëtejshme" 6 | }, 7 | "nplurals=2; plural=(n != 1);"); 8 | -------------------------------------------------------------------------------- /l10n/sq.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Save settings" : "Ruaj konfigurimet", 3 | "Advanced settings" : "Rregullime të mëtejshme" 4 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 5 | } -------------------------------------------------------------------------------- /l10n/ug.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "يېڭى ئاق دوسكا", 5 | "Create new whiteboard" : "يېڭى ئاق دوسكا ياساڭ", 6 | "Whiteboard" : "ئاق دوسكا", 7 | "Whiteboard app" : "ئاق دوسكا دېتالى", 8 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud نىڭ رەسمىي ئاق دوسكا دېتالى. ئۇ ئىشلەتكۈچىلەرنىڭ ئاق دوسكىلارنى باشقا ئىشلەتكۈچىلەر بىلەن ئورتاقلىشىشى ۋە ئورتاقلىشىشى ۋە دەل ۋاقتىدا ھەمكارلىشىشىغا يول قويىدۇ.\n\n** ئاق دوسكا ئىشلەش ئۈچۈن ئايرىم ھەمكارلىق مۇلازىمېتىرى تەلەپ قىلىدۇ. ** ئۇنى قانداق ئورنىتىش توغرىسىدىكى [ھۆججەتلەر] (https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) نى كۆرۈڭ.\n\n- sh شەكىل سىزىش ، تېكىست يېزىش ، ئۇلىنىش ئېلېمېنتلىرى\n- real ھەقىقىي ھەمكارلىق\n- سۆرەپ تاشلاش ئارقىلىق رەسىم قوشۇڭ\n- mer سۇ پەرىسى دىئاگراممىسىنى ئاسانلا قوشۇڭ\n- Smart Smart Picker نى ئىشلىتىپ Nextcloud دىن باشقا ئېلېمېنتلارنى قىستۇرۇڭ\n- export رەسىم ئېكسپورتى\n- rong مۇستەھكەم ئاساس: بىز Excalidraw نى ئاساسى كۇتۇپخانا قىلىپ ئىشلىتىمىز", 9 | "Whiteboard backend server is configured and connected." : "ئاق دوسكا ئارقا مۇلازىمىتىرى سەپلەنگەن ۋە ئۇلانغان.", 10 | "Failed to verify the connection:" : "ئۇلىنىشنى دەلىللىيەلمىدى:", 11 | "Verifying connection…" : "ئۇلىنىشنى دەلىللەۋاتىدۇ…", 12 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "ئاق دوسكا Nextcloud غا ئۇلانغان ئايرىم ھەمكارلىق مۇلازىمېتىرىنى تەلەپ قىلىدۇ.", 13 | "See the documentation on how to install it." : "ئۇنى قانداق ئورنىتىش توغرىسىدىكى ھۆججەتلەرنى كۆرۈڭ.", 14 | "Whiteboard server URL" : "ئاق دوسكا مۇلازىمېتىرى URL", 15 | "Shared secret" : "ئورتاق مەخپىيەتلىك", 16 | "Save settings" : "تەڭشەكلەرنى ساقلاڭ", 17 | "Advanced settings" : "ئىلغار تەڭشەكلەر" 18 | }, 19 | "nplurals=2; plural=(n != 1);"); 20 | -------------------------------------------------------------------------------- /l10n/ug.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "يېڭى ئاق دوسكا", 3 | "Create new whiteboard" : "يېڭى ئاق دوسكا ياساڭ", 4 | "Whiteboard" : "ئاق دوسكا", 5 | "Whiteboard app" : "ئاق دوسكا دېتالى", 6 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud نىڭ رەسمىي ئاق دوسكا دېتالى. ئۇ ئىشلەتكۈچىلەرنىڭ ئاق دوسكىلارنى باشقا ئىشلەتكۈچىلەر بىلەن ئورتاقلىشىشى ۋە ئورتاقلىشىشى ۋە دەل ۋاقتىدا ھەمكارلىشىشىغا يول قويىدۇ.\n\n** ئاق دوسكا ئىشلەش ئۈچۈن ئايرىم ھەمكارلىق مۇلازىمېتىرى تەلەپ قىلىدۇ. ** ئۇنى قانداق ئورنىتىش توغرىسىدىكى [ھۆججەتلەر] (https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) نى كۆرۈڭ.\n\n- sh شەكىل سىزىش ، تېكىست يېزىش ، ئۇلىنىش ئېلېمېنتلىرى\n- real ھەقىقىي ھەمكارلىق\n- سۆرەپ تاشلاش ئارقىلىق رەسىم قوشۇڭ\n- mer سۇ پەرىسى دىئاگراممىسىنى ئاسانلا قوشۇڭ\n- Smart Smart Picker نى ئىشلىتىپ Nextcloud دىن باشقا ئېلېمېنتلارنى قىستۇرۇڭ\n- export رەسىم ئېكسپورتى\n- rong مۇستەھكەم ئاساس: بىز Excalidraw نى ئاساسى كۇتۇپخانا قىلىپ ئىشلىتىمىز", 7 | "Whiteboard backend server is configured and connected." : "ئاق دوسكا ئارقا مۇلازىمىتىرى سەپلەنگەن ۋە ئۇلانغان.", 8 | "Failed to verify the connection:" : "ئۇلىنىشنى دەلىللىيەلمىدى:", 9 | "Verifying connection…" : "ئۇلىنىشنى دەلىللەۋاتىدۇ…", 10 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "ئاق دوسكا Nextcloud غا ئۇلانغان ئايرىم ھەمكارلىق مۇلازىمېتىرىنى تەلەپ قىلىدۇ.", 11 | "See the documentation on how to install it." : "ئۇنى قانداق ئورنىتىش توغرىسىدىكى ھۆججەتلەرنى كۆرۈڭ.", 12 | "Whiteboard server URL" : "ئاق دوسكا مۇلازىمېتىرى URL", 13 | "Shared secret" : "ئورتاق مەخپىيەتلىك", 14 | "Save settings" : "تەڭشەكلەرنى ساقلاڭ", 15 | "Advanced settings" : "ئىلغار تەڭشەكلەر" 16 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 17 | } -------------------------------------------------------------------------------- /l10n/uk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Біла дошка", 5 | "Shared secret" : "Спільний секрет", 6 | "Advanced settings" : "Розширені" 7 | }, 8 | "nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); 9 | -------------------------------------------------------------------------------- /l10n/uk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Біла дошка", 3 | "Shared secret" : "Спільний секрет", 4 | "Advanced settings" : "Розширені" 5 | },"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" 6 | } -------------------------------------------------------------------------------- /l10n/vi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "Whiteboard" : "Bảng viết", 5 | "Shared secret" : "Chia sẽ mật khẩu", 6 | "Advanced settings" : "Cài đặt nâng cao" 7 | }, 8 | "nplurals=1; plural=0;"); 9 | -------------------------------------------------------------------------------- /l10n/vi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Whiteboard" : "Bảng viết", 3 | "Shared secret" : "Chia sẽ mật khẩu", 4 | "Advanced settings" : "Cài đặt nâng cao" 5 | },"pluralForm" :"nplurals=1; plural=0;" 6 | } -------------------------------------------------------------------------------- /l10n/zh_CN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "新建白板", 5 | "Create new whiteboard" : "创建新白板", 6 | "Whiteboard" : "白板", 7 | "Whiteboard app" : "白板应用", 8 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板应用程序。它允许用户创建白板并与其他用户共享,并实时协作。\n\n**白板需要单独的协作服务器才能工作。** 有关如何安装它请参阅 [文档](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)\n\n- 🎨 绘制形状、书写文字、连接元素\n- 📝 实时协作\n- 🖼️ 通过拖放添加图像\n- 📊 轻松添加 Mermaid 流程图\n- ✨ 使用智能选择器嵌入 Nextcloud 中的其他元素\n- 📦 图像导出\n- 💪 坚实的基础:我们使用 Excalidraw 作为我们的基础库", 9 | "Whiteboard backend server is configured and connected." : "白板后端服务器已配置并连接。", 10 | "Failed to verify the connection:" : "验证连接已失败:", 11 | "Verifying connection…" : "正在验证连接……", 12 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要一个连接到 Nextcloud 的单独协作服务器。", 13 | "See the documentation on how to install it." : "请参阅有关如何安装它的文档。", 14 | "Whiteboard server URL" : "白板服务器 URL", 15 | "Shared secret" : "已分享密码", 16 | "Save settings" : "保存设置", 17 | "Advanced settings" : "高级选项" 18 | }, 19 | "nplurals=1; plural=0;"); 20 | -------------------------------------------------------------------------------- /l10n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "新建白板", 3 | "Create new whiteboard" : "创建新白板", 4 | "Whiteboard" : "白板", 5 | "Whiteboard app" : "白板应用", 6 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板应用程序。它允许用户创建白板并与其他用户共享,并实时协作。\n\n**白板需要单独的协作服务器才能工作。** 有关如何安装它请参阅 [文档](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)\n\n- 🎨 绘制形状、书写文字、连接元素\n- 📝 实时协作\n- 🖼️ 通过拖放添加图像\n- 📊 轻松添加 Mermaid 流程图\n- ✨ 使用智能选择器嵌入 Nextcloud 中的其他元素\n- 📦 图像导出\n- 💪 坚实的基础:我们使用 Excalidraw 作为我们的基础库", 7 | "Whiteboard backend server is configured and connected." : "白板后端服务器已配置并连接。", 8 | "Failed to verify the connection:" : "验证连接已失败:", 9 | "Verifying connection…" : "正在验证连接……", 10 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要一个连接到 Nextcloud 的单独协作服务器。", 11 | "See the documentation on how to install it." : "请参阅有关如何安装它的文档。", 12 | "Whiteboard server URL" : "白板服务器 URL", 13 | "Shared secret" : "已分享密码", 14 | "Save settings" : "保存设置", 15 | "Advanced settings" : "高级选项" 16 | },"pluralForm" :"nplurals=1; plural=0;" 17 | } -------------------------------------------------------------------------------- /l10n/zh_HK.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "新白板", 5 | "Create new whiteboard" : "建立新白板", 6 | "Whiteboard" : "白板", 7 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "尚未設定白板伺服器 URL。白板需要連線至 Nextcloud 的獨立協作伺服器。", 8 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud 伺服器尚未連線至白板伺服器:%s", 9 | "No version provided by /status enpdoint" : "/status 端點未提供版本", 10 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "後端伺服器正執行不同的版本,請確保兩者都升級到同一個版本。應用程式:%s 後端版本:%s", 11 | "Whiteboard backend server could not reach Nextcloud: %s" : "白板後端伺服器無法連線至 Nextcloud:%s", 12 | "Failed to connect to whiteboard server status endpoint: %s" : "連線至白板伺服器狀態端點失敗:%s", 13 | "Whiteboard server configured properly" : "白板伺服器設定正確", 14 | "Whiteboard app" : "白板應用程式", 15 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板應用程式。它可讓使用者建立白板並與其他使用者分享,以及進行即時協作。\n\n**白板需要獨立的協作伺服器才能運作。**安裝方法請參閱[文件](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)。\n\n- 🎨 繪製圖形、書寫文字、連結元素\n- 📝 即時協作\n- 🖼️ 使用拖放功能新增圖片\n- 📊 輕鬆新增 mermaid 圖表\n- ✨ 使用智慧型挑選程式從 Nextcloud 嵌入其他元素\n- 📦 匯出影像\n- 💪 強大的基礎:我們使用 Excalidraw 作為基礎函式庫", 16 | "Whiteboard server" : "白板伺服器", 17 | "Whiteboard backend server is configured and connected." : "已設定好白板後端伺服器並連線。", 18 | "Failed to verify the connection:" : "驗證連線失敗:", 19 | "Verifying connection…" : "正在驗證連線 …", 20 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要連線至 Nextcloud 的獨立協作伺服器。", 21 | "See the documentation on how to install it." : "有關如何安裝,請參閱用戶手冊。", 22 | "Whiteboard server URL" : "白板伺服器 URL", 23 | "This URL is used by the browser to connect to the whiteboard server." : "瀏覽器使用此 URL 連線至白板伺服器。", 24 | "Internal whiteboard server URL" : "內部白板伺服器 URL", 25 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Nextcloud 伺服器使用此 URL 連線至白板伺服器。", 26 | "Skip TLS certificate validation (not recommended)" : "略過 TLS 憑證驗證(不建議)", 27 | "Shared secret" : "分享了密碼", 28 | "Save settings" : "保存設置", 29 | "Advanced settings" : "進階設定", 30 | "Max file size" : "最大檔案大小" 31 | }, 32 | "nplurals=1; plural=0;"); 33 | -------------------------------------------------------------------------------- /l10n/zh_HK.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "新白板", 3 | "Create new whiteboard" : "建立新白板", 4 | "Whiteboard" : "白板", 5 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "尚未設定白板伺服器 URL。白板需要連線至 Nextcloud 的獨立協作伺服器。", 6 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud 伺服器尚未連線至白板伺服器:%s", 7 | "No version provided by /status enpdoint" : "/status 端點未提供版本", 8 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "後端伺服器正執行不同的版本,請確保兩者都升級到同一個版本。應用程式:%s 後端版本:%s", 9 | "Whiteboard backend server could not reach Nextcloud: %s" : "白板後端伺服器無法連線至 Nextcloud:%s", 10 | "Failed to connect to whiteboard server status endpoint: %s" : "連線至白板伺服器狀態端點失敗:%s", 11 | "Whiteboard server configured properly" : "白板伺服器設定正確", 12 | "Whiteboard app" : "白板應用程式", 13 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板應用程式。它可讓使用者建立白板並與其他使用者分享,以及進行即時協作。\n\n**白板需要獨立的協作伺服器才能運作。**安裝方法請參閱[文件](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)。\n\n- 🎨 繪製圖形、書寫文字、連結元素\n- 📝 即時協作\n- 🖼️ 使用拖放功能新增圖片\n- 📊 輕鬆新增 mermaid 圖表\n- ✨ 使用智慧型挑選程式從 Nextcloud 嵌入其他元素\n- 📦 匯出影像\n- 💪 強大的基礎:我們使用 Excalidraw 作為基礎函式庫", 14 | "Whiteboard server" : "白板伺服器", 15 | "Whiteboard backend server is configured and connected." : "已設定好白板後端伺服器並連線。", 16 | "Failed to verify the connection:" : "驗證連線失敗:", 17 | "Verifying connection…" : "正在驗證連線 …", 18 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要連線至 Nextcloud 的獨立協作伺服器。", 19 | "See the documentation on how to install it." : "有關如何安裝,請參閱用戶手冊。", 20 | "Whiteboard server URL" : "白板伺服器 URL", 21 | "This URL is used by the browser to connect to the whiteboard server." : "瀏覽器使用此 URL 連線至白板伺服器。", 22 | "Internal whiteboard server URL" : "內部白板伺服器 URL", 23 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Nextcloud 伺服器使用此 URL 連線至白板伺服器。", 24 | "Skip TLS certificate validation (not recommended)" : "略過 TLS 憑證驗證(不建議)", 25 | "Shared secret" : "分享了密碼", 26 | "Save settings" : "保存設置", 27 | "Advanced settings" : "進階設定", 28 | "Max file size" : "最大檔案大小" 29 | },"pluralForm" :"nplurals=1; plural=0;" 30 | } -------------------------------------------------------------------------------- /l10n/zh_TW.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "whiteboard", 3 | { 4 | "New whiteboard" : "新白板", 5 | "Create new whiteboard" : "建立新白板", 6 | "Whiteboard" : "白板", 7 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "尚未設定白板伺服器 URL。白板需要連線至 Nextcloud 的獨立協作伺服器。", 8 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud 伺服器尚未連線至白板伺服器:%s", 9 | "No version provided by /status enpdoint" : "/status 端點未提供版本", 10 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "後端伺服器正執行不同的版本,請確保兩者都升級到同一個版本。應用程式:%s 後端版本:%s", 11 | "Whiteboard backend server could not reach Nextcloud: %s" : "白板後端伺服器無法連線至 Nextcloud:%s", 12 | "Failed to connect to whiteboard server status endpoint: %s" : "連線至白板伺服器狀態端點失敗:%s", 13 | "Whiteboard server configured properly" : "白板伺服器設定正確", 14 | "Whiteboard app" : "白板應用程式", 15 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板應用程式。它可讓使用者建立白板並與其他使用者分享,以及進行即時協作。\n\n**白板需要獨立的協作伺服器才能運作。**安裝方法請參閱[文件](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)。\n\n- 🎨 繪製圖形、書寫文字、連結元素\n- 📝 即時協作\n- 🖼️ 使用拖放功能新增圖片\n- 📊 輕鬆新增 mermaid 圖表\n- ✨ 使用智慧型挑選程式從 Nextcloud 嵌入其他元素\n- 📦 匯出影像\n- 💪 強大的基礎:我們使用 Excalidraw 作為基礎函式庫", 16 | "Whiteboard server" : "白板伺服器", 17 | "Whiteboard backend server is configured and connected." : "已設定好白板後端伺服器並連線。", 18 | "Failed to verify the connection:" : "驗證連線失敗:", 19 | "Verifying connection…" : "正在驗證連線……", 20 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要連線至 Nextcloud 的獨立協作伺服器。", 21 | "See the documentation on how to install it." : "請參閱如何安裝它的文件。", 22 | "Whiteboard server URL" : "白板伺服器 URL", 23 | "This URL is used by the browser to connect to the whiteboard server." : "瀏覽器使用此 URL 連線至白板伺服器。", 24 | "Internal whiteboard server URL" : "內部白板伺服器 URL", 25 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Nextcloud 伺服器使用此 URL 連線至白板伺服器。", 26 | "Skip TLS certificate validation (not recommended)" : "略過 TLS 憑證驗證(不建議)", 27 | "Shared secret" : "已分享密碼", 28 | "Save settings" : "儲存設定", 29 | "Advanced settings" : "進階設定", 30 | "Max file size" : "最大檔案大小" 31 | }, 32 | "nplurals=1; plural=0;"); 33 | -------------------------------------------------------------------------------- /l10n/zh_TW.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "New whiteboard" : "新白板", 3 | "Create new whiteboard" : "建立新白板", 4 | "Whiteboard" : "白板", 5 | "Whiteboard server URL is not configured. Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "尚未設定白板伺服器 URL。白板需要連線至 Nextcloud 的獨立協作伺服器。", 6 | "Nextcloud server could not connect to whiteboard server: %s" : "Nextcloud 伺服器尚未連線至白板伺服器:%s", 7 | "No version provided by /status enpdoint" : "/status 端點未提供版本", 8 | "Backend server is running a different version, make sure to upgrade both to the same version. App: %s Backend version: %s" : "後端伺服器正執行不同的版本,請確保兩者都升級到同一個版本。應用程式:%s 後端版本:%s", 9 | "Whiteboard backend server could not reach Nextcloud: %s" : "白板後端伺服器無法連線至 Nextcloud:%s", 10 | "Failed to connect to whiteboard server status endpoint: %s" : "連線至白板伺服器狀態端點失敗:%s", 11 | "Whiteboard server configured properly" : "白板伺服器設定正確", 12 | "Whiteboard app" : "白板應用程式", 13 | "The official whiteboard app for Nextcloud. It allows users to create and share whiteboards with other users and collaborate in real-time.\n\n**Whiteboard requires a separate collaboration server to work.** Please see the [documentation](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend) on how to install it.\n\n- 🎨 Drawing shapes, writing text, connecting elements\n- 📝 Real-time collaboration\n- 🖼️ Add images with drag and drop\n- 📊 Easily add mermaid diagrams\n- ✨ Use the Smart Picker to embed other elements from Nextcloud\n- 📦 Image export\n- 💪 Strong foundation: We use Excalidraw as our base library" : "Nextcloud 的官方白板應用程式。它可讓使用者建立白板並與其他使用者分享,以及進行即時協作。\n\n**白板需要獨立的協作伺服器才能運作。**安裝方法請參閱[文件](https://github.com/nextcloud/whiteboard?tab=readme-ov-file#backend)。\n\n- 🎨 繪製圖形、書寫文字、連結元素\n- 📝 即時協作\n- 🖼️ 使用拖放功能新增圖片\n- 📊 輕鬆新增 mermaid 圖表\n- ✨ 使用智慧型挑選程式從 Nextcloud 嵌入其他元素\n- 📦 匯出影像\n- 💪 強大的基礎:我們使用 Excalidraw 作為基礎函式庫", 14 | "Whiteboard server" : "白板伺服器", 15 | "Whiteboard backend server is configured and connected." : "已設定好白板後端伺服器並連線。", 16 | "Failed to verify the connection:" : "驗證連線失敗:", 17 | "Verifying connection…" : "正在驗證連線……", 18 | "Whiteboard requires a separate collaboration server that is connected to Nextcloud." : "白板需要連線至 Nextcloud 的獨立協作伺服器。", 19 | "See the documentation on how to install it." : "請參閱如何安裝它的文件。", 20 | "Whiteboard server URL" : "白板伺服器 URL", 21 | "This URL is used by the browser to connect to the whiteboard server." : "瀏覽器使用此 URL 連線至白板伺服器。", 22 | "Internal whiteboard server URL" : "內部白板伺服器 URL", 23 | "This URL is used by the Nextcloud server to connect to the whiteboard server." : "Nextcloud 伺服器使用此 URL 連線至白板伺服器。", 24 | "Skip TLS certificate validation (not recommended)" : "略過 TLS 憑證驗證(不建議)", 25 | "Shared secret" : "已分享密碼", 26 | "Save settings" : "儲存設定", 27 | "Advanced settings" : "進階設定", 28 | "Max file size" : "最大檔案大小" 29 | },"pluralForm" :"nplurals=1; plural=0;" 30 | } -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | registerEventListener(AddContentSecurityPolicyEvent::class, AddContentSecurityPolicyListener::class); 46 | $context->registerEventListener(LoadViewer::class, LoadViewerListener::class); 47 | $context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class); 48 | $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); 49 | $context->registerSetupCheck(SetupCheck::class); 50 | } 51 | 52 | #[\Override] 53 | public function boot(IBootContext $context): void { 54 | [$major] = Util::getVersion(); 55 | if ($major < 30) { 56 | $context->injectFn(function (ITemplateManager $templateManager, IL10N $l10n) { 57 | $templateManager->registerTemplateFileCreator(function () use ($l10n) { 58 | return RegisterTemplateCreatorListener::getTemplateFileCreator($l10n); 59 | }); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Consts/JWTConsts.php: -------------------------------------------------------------------------------- 1 | request->getParam('publicSharingToken'); 44 | 45 | $user = $this->authenticateUserServiceFactory->create($publicSharingToken)->authenticate(); 46 | 47 | $fileService = $this->getFileServiceFactory->create($user, $fileId); 48 | 49 | $file = $fileService->getFile(); 50 | 51 | $isFileReadOnly = $fileService->isFileReadOnly(); 52 | 53 | $jwt = $this->jwtService->generateJWT($user, $file, $isFileReadOnly); 54 | 55 | return new DataResponse(['token' => $jwt]); 56 | } catch (Exception $e) { 57 | return $this->exceptionService->handleException($e); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/Controller/SettingsController.php: -------------------------------------------------------------------------------- 1 | request->getParam('serverUrl'); 39 | $serverUrlInternal = $this->request->getParam('serverUrlInternal'); 40 | $secret = $this->request->getParam('secret'); 41 | $maxFileSize = $this->request->getParam('maxFileSize'); 42 | $skipTlsVerify = $this->request->getParam('skipTlsVerify'); 43 | 44 | if ($serverUrl !== null) { 45 | $this->configService->setCollabBackendUrl($serverUrl); 46 | } 47 | 48 | if ($serverUrlInternal !== null) { 49 | $this->configService->setInternalCollabBackendUrl($serverUrlInternal); 50 | } 51 | 52 | if ($secret !== null) { 53 | $this->configService->setWhiteboardSharedSecret($secret); 54 | } 55 | 56 | if ($maxFileSize !== null) { 57 | $this->configService->setMaxFileSize(intval($maxFileSize)); 58 | } 59 | 60 | if ($skipTlsVerify !== null) { 61 | $this->configService->setSkipTlsVerify(filter_var($skipTlsVerify, FILTER_VALIDATE_BOOLEAN)); 62 | } 63 | 64 | $result = null; 65 | if ($serverUrl !== null || $serverUrlInternal !== null || $secret !== null || $maxFileSize !== null || $skipTlsVerify !== null) { 66 | $result = $this->setupCheck->run(); 67 | } 68 | 69 | return new DataResponse([ 70 | 'jwt' => $this->jwtService->generateJWTFromPayload([ 'serverUrl' => $serverUrl ?: $this->configService->getCollabBackendUrl() ]), 71 | 'check' => $result?->jsonSerialize(), 72 | ]); 73 | } catch (Exception $e) { 74 | return $this->exceptionService->handleException($e); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Exception/InvalidUserException.php: -------------------------------------------------------------------------------- 1 | */ 17 | class AddContentSecurityPolicyListener implements IEventListener { 18 | public function __construct( 19 | private IRequest $request, 20 | ) { 21 | } 22 | 23 | #[\Override] 24 | public function handle(Event $event): void { 25 | if (!$event instanceof AddContentSecurityPolicyEvent) { 26 | return; 27 | } 28 | 29 | $policy = new EmptyContentSecurityPolicy(); 30 | 31 | $policy->addAllowedConnectDomain('*'); 32 | $policy->addAllowedWorkerSrcDomain('*'); 33 | 34 | $event->addPolicy($policy); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Listener/BeforeTemplateRenderedListener.php: -------------------------------------------------------------------------------- 1 | */ 20 | /** 21 | * @psalm-suppress UndefinedClass 22 | * @psalm-suppress MissingTemplateParam 23 | */ 24 | class BeforeTemplateRenderedListener implements IEventListener { 25 | public function __construct( 26 | private IInitialState $initialState, 27 | ) { 28 | } 29 | 30 | /** 31 | * @throws NotFoundException 32 | */ 33 | #[\Override] 34 | public function handle(Event $event): void { 35 | if (!($event instanceof BeforeTemplateRenderedEvent)) { 36 | return; 37 | } 38 | 39 | $this->initialState->provideInitialState( 40 | 'file_id', 41 | $event->getShare()->getNodeId() 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/Listener/LoadViewerListener.php: -------------------------------------------------------------------------------- 1 | */ 21 | class LoadViewerListener implements IEventListener { 22 | public function __construct( 23 | private IInitialState $initialState, 24 | private ConfigService $configService, 25 | ) { 26 | } 27 | 28 | #[\Override] 29 | public function handle(Event $event): void { 30 | if (!($event instanceof LoadViewer)) { 31 | return; 32 | } 33 | 34 | Util::addScript('whiteboard', 'whiteboard-main'); 35 | Util::addStyle('whiteboard', 'whiteboard-main'); 36 | 37 | $this->initialState->provideInitialState( 38 | 'collabBackendUrl', 39 | $this->configService->getCollabBackendUrl() 40 | ); 41 | $this->initialState->provideInitialState( 42 | 'maxFileSize', 43 | $this->configService->getMaxFileSize() 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/Listener/RegisterTemplateCreatorListener.php: -------------------------------------------------------------------------------- 1 | */ 18 | /** 19 | * @psalm-suppress UndefinedClass 20 | * @psalm-suppress MissingTemplateParam 21 | */ 22 | final class RegisterTemplateCreatorListener implements IEventListener { 23 | public function __construct( 24 | private IL10N $l10n, 25 | ) { 26 | } 27 | 28 | #[\Override] 29 | public function handle(Event $event): void { 30 | if (!($event instanceof RegisterTemplateCreatorEvent)) { 31 | return; 32 | } 33 | 34 | 35 | $event->getTemplateManager()->registerTemplateFileCreator(function () { 36 | return self::getTemplateFileCreator($this->l10n); 37 | }); 38 | } 39 | 40 | public static function getTemplateFileCreator(IL10N $l10n): TemplateFileCreator { 41 | $whiteboard = new TemplateFileCreator(Application::APP_ID, $l10n->t('New whiteboard'), '.whiteboard'); 42 | $whiteboard->addMimetype('application/vnd.excalidraw+json'); 43 | $iconContent = file_get_contents(__DIR__ . '/../../img/app-filetype.svg'); 44 | if ($iconContent !== false) { 45 | $whiteboard->setIconSvgInline($iconContent); 46 | } 47 | $whiteboard->setActionLabel($l10n->t('Create new whiteboard')); 48 | return $whiteboard; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Model/AuthenticatedUser.php: -------------------------------------------------------------------------------- 1 | user->getUID(); 23 | } 24 | 25 | #[\Override] 26 | public function getDisplayName(): string { 27 | return $this->user->getDisplayName(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/Model/PublicSharingUser.php: -------------------------------------------------------------------------------- 1 | generateRandomUID(); 21 | } 22 | 23 | #[\Override] 24 | public function getDisplayName(): string { 25 | return $this->generateRandomDisplayName(); 26 | } 27 | 28 | public function getPublicSharingToken(): string { 29 | return $this->publicSharingToken; 30 | } 31 | 32 | private function generateRandomUID(): string { 33 | return 'shared_' . $this->publicSharingToken . '_' . bin2hex(random_bytes(8)); 34 | } 35 | 36 | private function generateRandomDisplayName(): string { 37 | $adjectives = ['Anonymous', 'Mysterious', 'Incognito', 'Unknown', 'Unnamed']; 38 | $nouns = ['User', 'Visitor', 'Guest', 'Collaborator', 'Participant']; 39 | 40 | $adjective = $adjectives[array_rand($adjectives)]; 41 | $noun = $nouns[array_rand($nouns)]; 42 | 43 | return $adjective . ' ' . $noun; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Model/User.php: -------------------------------------------------------------------------------- 1 | publicSharingToken) { 28 | throw new UnauthorizedException('Public sharing token not provided'); 29 | } 30 | 31 | try { 32 | $this->shareManager->getShareByToken($this->publicSharingToken); 33 | return new PublicSharingUser($this->publicSharingToken); 34 | } catch (Exception) { 35 | throw new UnauthorizedException('Invalid sharing token'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/Service/Authentication/AuthenticateSessionUserService.php: -------------------------------------------------------------------------------- 1 | userSession->isLoggedIn()) { 26 | throw new UnauthorizedException('User not logged in'); 27 | } 28 | 29 | $user = $this->userSession->getUser(); 30 | if ($user === null) { 31 | throw new UnauthorizedException('User session invalid'); 32 | } 33 | 34 | return new AuthenticatedUser($user); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Service/Authentication/AuthenticateUserService.php: -------------------------------------------------------------------------------- 1 | shareManager, $publicSharingToken), 27 | new AuthenticateSessionUserService($this->userSession), 28 | ]; 29 | 30 | return new ChainAuthenticateUserService($authServices); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Service/Authentication/ChainAuthenticateUserService.php: -------------------------------------------------------------------------------- 1 | strategies as $strategy) { 25 | try { 26 | return $strategy->authenticate(); 27 | } catch (Exception) { 28 | continue; 29 | } 30 | } 31 | 32 | throw new InvalidUserException('No valid authentication method found'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/Service/Authentication/GetPublicSharingUserFromIdService.php: -------------------------------------------------------------------------------- 1 | userId); 29 | if (count($parts) < 3) { 30 | throw new InvalidUserException('Invalid public sharing user ID format'); 31 | } 32 | $publicSharingToken = $parts[1]; 33 | 34 | try { 35 | $this->shareManager->getShareByToken($publicSharingToken); 36 | return new PublicSharingUser($publicSharingToken); 37 | } catch (Exception) { 38 | throw new UnauthorizedException('Invalid sharing token'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Service/Authentication/GetSessionUserFromIdService.php: -------------------------------------------------------------------------------- 1 | userManager->get($this->userId); 29 | if (!$user) { 30 | throw new UnauthorizedException('User not found'); 31 | } 32 | $this->userSession->setVolatileActiveUser($user); 33 | 34 | return new AuthenticatedUser($user); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Service/Authentication/GetUserFromIdService.php: -------------------------------------------------------------------------------- 1 | shareManager, $userId); 27 | } 28 | 29 | return new GetSessionUserFromIdService($this->userManager, $this->userSession, $userId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/Service/ConfigService.php: -------------------------------------------------------------------------------- 1 | appConfig, 'getAppValueString')) { 22 | return $this->appConfig->getAppValue('jwt_secret_key'); 23 | } 24 | 25 | return $this->appConfig->getAppValueString('jwt_secret_key'); 26 | } 27 | 28 | public function getMaxFileSize(): int { 29 | return $this->appConfig->getAppValueInt('max_file_size', 10); 30 | } 31 | 32 | public function setMaxFileSize(int $maxFileSize): void { 33 | $this->appConfig->setAppValueInt('max_file_size', $maxFileSize); 34 | } 35 | 36 | public function getCollabBackendUrl(): string { 37 | if (!method_exists($this->appConfig, 'getAppValueString')) { 38 | return $this->trimUrl($this->appConfig->getAppValue('collabBackendUrl')); 39 | } 40 | 41 | return $this->trimUrl($this->appConfig->getAppValueString('collabBackendUrl')); 42 | } 43 | 44 | public function setCollabBackendUrl(string $collabBackendUrl): void { 45 | if (!method_exists($this->appConfig, 'setAppValueString')) { 46 | $this->appConfig->setAppValue('collabBackendUrl', $collabBackendUrl); 47 | return; 48 | } 49 | 50 | $this->appConfig->setAppValueString('collabBackendUrl', $collabBackendUrl); 51 | } 52 | 53 | public function getInternalCollabBackendUrl(bool $fallback = true): string { 54 | if (!method_exists($this->appConfig, 'getAppValueString')) { 55 | // Get internal URL from app config 56 | $this->appConfig->getAppValue('collabBackendUrlInternal'); 57 | } 58 | 59 | $internalUrl = $this->appConfig->getAppValueString('collabBackendUrlInternal'); 60 | 61 | if ($internalUrl !== '' || !$fallback) { 62 | return $this->trimUrl($internalUrl); 63 | } 64 | 65 | return $this->getCollabBackendUrl(); 66 | } 67 | 68 | private function trimUrl(string $url): string { 69 | return rtrim(trim($url), '/'); 70 | } 71 | 72 | public function setInternalCollabBackendUrl(string $collabBackendUrl): void { 73 | if (!method_exists($this->appConfig, 'setAppValueString')) { 74 | $this->appConfig->setAppValue('collabBackendUrlInternal', $collabBackendUrl); 75 | return; 76 | } 77 | 78 | $this->appConfig->setAppValueString('collabBackendUrlInternal', $collabBackendUrl); 79 | } 80 | 81 | public function getSkipTlsVerify(): bool { 82 | return $this->appConfig->getAppValueBool('skip_tls_verify', false); 83 | } 84 | 85 | public function setSkipTlsVerify(bool $skip): void { 86 | $this->appConfig->setAppValueBool('skip_tls_verify', $skip); 87 | } 88 | 89 | public function getWhiteboardSharedSecret(): string { 90 | return $this->appConfig->getAppValueString('jwt_secret_key'); 91 | } 92 | 93 | public function setWhiteboardSharedSecret(string $jwtSecretKey): void { 94 | if (!method_exists($this->appConfig, 'setAppValueString')) { 95 | $this->appConfig->setAppValue('jwt_secret_key', $jwtSecretKey); 96 | return; 97 | } 98 | 99 | $this->appConfig->setAppValueString('jwt_secret_key', $jwtSecretKey); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/Service/ExceptionService.php: -------------------------------------------------------------------------------- 1 | getStatusCode($e); 32 | $message = $this->getMessage($e); 33 | 34 | return new DataResponse(['message' => $message], $statusCode); 35 | } 36 | 37 | private function getStatusCode(Exception $e): int { 38 | return match (true) { 39 | $e instanceof NotFoundException => Http::STATUS_NOT_FOUND, 40 | $e instanceof NotPermittedException => Http::STATUS_FORBIDDEN, 41 | $e instanceof UnauthorizedException => Http::STATUS_UNAUTHORIZED, 42 | $e instanceof InvalidUserException => Http::STATUS_BAD_REQUEST, 43 | default => (int)($e->getCode() ?: Http::STATUS_INTERNAL_SERVER_ERROR), 44 | }; 45 | } 46 | 47 | private function getMessage(Exception $e): string { 48 | return match (true) { 49 | $e instanceof NotFoundException => 'File not found', 50 | $e instanceof NotPermittedException => 'Permission denied', 51 | $e instanceof UnauthorizedException => 'Unauthorized', 52 | $e instanceof InvalidUserException => 'Invalid user', 53 | default => $e->getMessage() ?: 'An error occurred', 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Service/File/GetFileFromIdService.php: -------------------------------------------------------------------------------- 1 | rootFolder->getUserFolder($this->userId); 45 | 46 | $file = $userFolder->getFirstNodeById($this->fileId); 47 | if ($file instanceof File && $file->getPermissions() & Constants::PERMISSION_UPDATE) { 48 | $this->file = $file; 49 | 50 | return $file; 51 | } 52 | 53 | $files = $userFolder->getById($this->fileId); 54 | if (empty($files)) { 55 | throw new NotFoundException('File not found'); 56 | } 57 | 58 | usort($files, static function (Node $a, Node $b) { 59 | return ($b->getPermissions() & Constants::PERMISSION_UPDATE) <=> ($a->getPermissions() & Constants::PERMISSION_UPDATE); 60 | }); 61 | 62 | $file = $files[0]; 63 | if (!$file instanceof File) { 64 | throw new NotFoundException('Not a file'); 65 | } 66 | 67 | if (!($file->getPermissions() & Constants::PERMISSION_READ)) { 68 | throw new NotPermittedException('No read permission'); 69 | } 70 | 71 | $this->file = $file; 72 | 73 | return $this->file; 74 | } 75 | 76 | /** 77 | * @throws NotFoundException 78 | * @throws InvalidPathException 79 | */ 80 | #[\Override] 81 | public function isFileReadOnly(): bool { 82 | if ($this->file === null) { 83 | throw new NotFoundException('File not found'); 84 | } 85 | 86 | return !($this->file->getPermissions() & Constants::PERMISSION_UPDATE); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/Service/File/GetFileFromPublicSharingTokenService.php: -------------------------------------------------------------------------------- 1 | shareManager->getShareByToken($this->publicSharingToken); 42 | } catch (ShareNotFound) { 43 | throw new NotFoundException(); 44 | } 45 | 46 | $this->share = $share; 47 | 48 | $node = $share->getNode(); 49 | 50 | if ($node instanceof File) { 51 | return $node; 52 | } 53 | 54 | $node = $node->getFirstNodeById($this->fileId); 55 | 56 | if ($node instanceof File) { 57 | return $node; 58 | } 59 | 60 | throw new InvalidArgumentException('No proper share data'); 61 | } 62 | 63 | #[\Override] 64 | public function isFileReadOnly(): bool { 65 | if ($this->share === null) { 66 | throw new InvalidArgumentException('No share data'); 67 | } 68 | 69 | return !($this->share->getPermissions() & Constants::PERMISSION_UPDATE); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/Service/File/GetFileService.php: -------------------------------------------------------------------------------- 1 | rootFolder, $user->getUID(), $fileId); 34 | } 35 | 36 | if ($user instanceof PublicSharingUser) { 37 | return new GetFileFromPublicSharingTokenService($this->shareManager, $user->getPublicSharingToken(), $fileId); 38 | } 39 | 40 | throw new InvalidUserException(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/Service/JWTService.php: -------------------------------------------------------------------------------- 1 | $user->getUID(), 37 | 'fileId' => $file->getId(), 38 | 'isFileReadOnly' => $isFileReadOnly, 39 | 'user' => [ 40 | 'id' => $user->getUID(), 41 | 'name' => $user->getDisplayName() 42 | ], 43 | 'iat' => $issuedAt, 44 | 'exp' => $expirationTime 45 | ]; 46 | 47 | return $this->generateJWTFromPayload($payload); 48 | } 49 | 50 | /** 51 | * @throws InvalidPathException 52 | * @throws NotFoundException 53 | */ 54 | public function generateJWTFromPayload(array $payload): string { 55 | $key = $this->configService->getJwtSecretKey(); 56 | return JWT::encode($payload, $key, JWTConsts::JWT_ALGORITHM); 57 | } 58 | 59 | public function getUserIdFromJWT(string $jwt): string { 60 | try { 61 | $key = $this->configService->getJwtSecretKey(); 62 | return JWT::decode($jwt, new Key($key, JWTConsts::JWT_ALGORITHM))->userid; 63 | } catch (Exception) { 64 | throw new UnauthorizedException(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/Service/WhiteboardContentService.php: -------------------------------------------------------------------------------- 1 | getContent(); 27 | if ($fileContent === '') { 28 | $fileContent = '{"elements":[],"scrollToContent":true}'; 29 | } 30 | 31 | return json_decode($fileContent, true, 512, JSON_THROW_ON_ERROR); 32 | } 33 | 34 | /** 35 | * @throws NotPermittedException 36 | * @throws GenericFileException 37 | * @throws LockedException 38 | * @throws JsonException 39 | */ 40 | public function updateContent(File $file, array $data): void { 41 | if (empty($data)) { 42 | $data = ['elements' => [], 'scrollToContent' => true]; 43 | } 44 | 45 | $file->putContent(json_encode($data, JSON_THROW_ON_ERROR)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Settings/Admin.php: -------------------------------------------------------------------------------- 1 | initialState->provideInitialState('url', $this->configService->getCollabBackendUrl()); 28 | $this->initialState->provideInitialState('urlInternal', $this->configService->getInternalCollabBackendUrl(false)); 29 | $this->initialState->provideInitialState('secret', $this->configService->getWhiteboardSharedSecret()); 30 | $this->initialState->provideInitialState('jwt', $this->jwtService->generateJWTFromPayload([])); 31 | $this->initialState->provideInitialState('maxFileSize', $this->configService->getMaxFileSize()); 32 | $this->initialState->provideInitialState('skipTlsVerify', $this->configService->getSkipTlsVerify()); 33 | $response = new TemplateResponse( 34 | 'whiteboard', 35 | 'admin', 36 | [], 37 | 'blank' 38 | ); 39 | $csp = new ContentSecurityPolicy(); 40 | $csp->addAllowedConnectDomain('*'); 41 | $response->setContentSecurityPolicy($csp); 42 | return $response; 43 | } 44 | 45 | #[\Override] 46 | public function getSection() { 47 | return 'whiteboard'; 48 | } 49 | 50 | #[\Override] 51 | public function getPriority() { 52 | return 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/Settings/Section.php: -------------------------------------------------------------------------------- 1 | l10n->t('Whiteboard'); 29 | } 30 | 31 | #[\Override] 32 | public function getPriority() { 33 | return 75; 34 | } 35 | 36 | #[\Override] 37 | public function getIcon() { 38 | return $this->url->imagePath('whiteboard', 'app-dark.svg'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { defineConfig, devices } from '@playwright/test' 7 | 8 | /** 9 | * See https://playwright.dev/docs/test-configuration. 10 | */ 11 | export default defineConfig({ 12 | testDir: './playwright', 13 | 14 | /* Directory for test artifacts like videos and screenshots */ 15 | outputDir: 'test-results', 16 | 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: process.env.CI ? [ 27 | ['github'], 28 | ['html', { outputFolder: 'playwright-report' }] 29 | ] : 'html', 30 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 31 | use: { 32 | /* Base URL to use in actions like `await page.goto('./')`. */ 33 | baseURL: 'http://localhost:8089/index.php/', 34 | 35 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 36 | trace: 'on', 37 | 38 | /* Capture video of test runs - only keep videos for failed tests */ 39 | video: 'on', 40 | }, 41 | 42 | projects: [ 43 | // Our global setup to configure the Nextcloud docker container 44 | { 45 | name: 'setup', 46 | testMatch: /setup\.ts$/, 47 | }, 48 | 49 | { 50 | name: 'chromium', 51 | use: { 52 | ...devices['Desktop Chrome'], 53 | }, 54 | dependencies: ['setup'], 55 | }, 56 | ], 57 | 58 | webServer: [ 59 | { 60 | // Starts the Nextcloud docker container 61 | command: 'npm run start:nextcloud', 62 | reuseExistingServer: !process.env.CI, 63 | url: 'http://127.0.0.1:8089', 64 | stderr: 'pipe', 65 | stdout: 'pipe', 66 | timeout: 5 * 60 * 1000, // max. 5 minutes for creating the container 67 | }, 68 | { 69 | // Starts the Nextcloud docker container 70 | command: 'METRICS_TOKEN=secret JWT_SECRET_KEY=secret NEXTCLOUD_URL=http://127.0.0.1:8089 npm run server:start', 71 | reuseExistingServer: !process.env.CI, 72 | url: 'http://127.0.0.1:3002', 73 | stderr: 'pipe', 74 | stdout: 'pipe', 75 | timeout: 5 * 60 * 1000, // max. 5 minutes for creating the container 76 | } 77 | ], 78 | }) -------------------------------------------------------------------------------- /playwright/e2e/viewer.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { expect } from '@playwright/test' 7 | import { test } from '../support/fixtures/random-user' 8 | 9 | test.beforeEach(async ({ page }) => { 10 | await page.goto('apps/files') 11 | await page.waitForURL(/apps\/files/) 12 | }) 13 | 14 | test('test whiteboard server is reachable', async ({ page }) => { 15 | await page.goto('http://localhost:3002') 16 | await expect(page.locator('body')).toContainText('Nextcloud Whiteboard Collaboration Server') 17 | }) 18 | 19 | test('open a whiteboard', async ({ page }) => { 20 | await page.getByRole('button', { name: 'New' }).click() 21 | await page.getByRole('menuitem', { name: 'New whiteboard' }).click() 22 | await page.getByRole('button', { name: 'Create' }).click() 23 | await expect(page.getByText('Drawing canvas')).toBeVisible() 24 | 25 | await page.getByTitle('Text — T or').locator('div').click(); 26 | await page.getByText('Drawing canvas').click({ 27 | position: { 28 | x: 534, 29 | y: 249 30 | } 31 | }); 32 | await page.locator('textarea').fill('Test'); 33 | await page.getByText('Drawing canvas').click({ 34 | position: { 35 | x: 683, 36 | y: 214 37 | } 38 | }); 39 | await page.getByTestId('main-menu-trigger').click(); 40 | await expect(page.getByText('Canvas backgroundExport image')).toBeVisible(); 41 | await page.getByTestId('main-menu-trigger').click(); 42 | await page.getByTestId('dropdown-menu-button').click(); 43 | }) -------------------------------------------------------------------------------- /playwright/start-nextcloud-server.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { startNextcloud, stopNextcloud } from '@nextcloud/e2e-test-server/docker' 7 | import { readFileSync } from 'fs' 8 | 9 | const start = async () => { 10 | return await startNextcloud(getBranch(), true, { 11 | exposePort: 8089, 12 | }) 13 | } 14 | 15 | const getBranch = () => { 16 | try { 17 | const appinfo = readFileSync('appinfo/info.xml').toString() 18 | const maxVersion = appinfo.match( 19 | //, 20 | )?.[1] 21 | return maxVersion ? `stable${maxVersion}` : undefined 22 | } catch (err) { 23 | if (err.code === 'ENOENT') { 24 | console.warn('No appinfo/info.xml found. Using default server banch.') 25 | } 26 | } 27 | } 28 | 29 | // Start the Nextcloud docker container 30 | await start() 31 | // Listen for process to exit (tests done) and shut down the docker container 32 | process.on('beforeExit', (code) => { 33 | stopNextcloud() 34 | }) 35 | 36 | // Idle to wait for shutdown 37 | while (true) { 38 | await new Promise((resolve) => setTimeout(resolve, 5000)) 39 | } -------------------------------------------------------------------------------- /playwright/support/fixtures/random-user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { test as base } from '@playwright/test' 7 | import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright' 8 | 9 | interface RandomUserFixture { 10 | user: User 11 | } 12 | 13 | /** 14 | * This test fixture ensures a new random user is created and used for the test (current page) 15 | */ 16 | export const test = base.extend({ 17 | user: async ({ }, use) => { 18 | const user = await createRandomUser() 19 | await use(user) 20 | }, 21 | page: async ({ browser, baseURL, user }, use) => { 22 | // Important: make sure we authenticate in a clean environment by unsetting storage state. 23 | const page = await browser.newPage({ 24 | storageState: undefined, 25 | baseURL, 26 | }) 27 | 28 | await login(page.request, user) 29 | 30 | await use(page) 31 | await page.close() 32 | }, 33 | }) -------------------------------------------------------------------------------- /playwright/support/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { test as setup } from '@playwright/test' 7 | import { configureNextcloud, runOcc } from '@nextcloud/e2e-test-server' 8 | 9 | /** 10 | * We use this to ensure Nextcloud is configured correctly before running our tests 11 | * 12 | * This can not be done in the webserver startup process, 13 | * as that only checks for the URL to be accessible which happens already before everything is configured. 14 | */ 15 | setup('Configure Nextcloud', async () => { 16 | setup.slow() 17 | const appsToInstall = [ 18 | 'whiteboard', 19 | 'viewer', 20 | ] 21 | await configureNextcloud(appsToInstall) 22 | await runOcc(['config:app:set', 'whiteboard', 'collabBackendUrl', '--value', 'http://localhost:3002']) 23 | await runOcc(['config:app:set', 'whiteboard', 'jwt_secret_key', '--value', 'secret']) 24 | }) -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/whiteboard/924fc4a300b6a37ef39a9c91e5c4f053eaace1f3/screenshots/screenshot1.png -------------------------------------------------------------------------------- /src/Embeddable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | import { NcReferenceList } from '@nextcloud/vue/dist/Components/NcRichText.js' 6 | 7 | import VueWrapper from './VueWrapper' 8 | 9 | /** 10 | * 11 | * @param props props 12 | * @param props.link link to display in embedable 13 | */ 14 | export default function(props: { link: string }) { 15 | const referenceProps = { text: props.link, limit: 1, interactive: true } 16 | return React.createElement(VueWrapper, { componentProps: referenceProps, component: NcReferenceList }) 17 | } 18 | -------------------------------------------------------------------------------- /src/VueWrapper.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | import Vue from 'vue' 6 | 7 | const VueWrapper = function( 8 | { componentProps, component }) { 9 | const vueRef = React.useRef(null) 10 | const [vueInstance, setVueInstance] = React.useState(undefined) 11 | 12 | React.useEffect(() => { 13 | /** 14 | * 15 | */ 16 | async function createVueInstance() { 17 | } 18 | 19 | createVueInstance() 20 | 21 | setVueInstance(new Vue({ 22 | el: vueRef.current, 23 | data() { 24 | return { 25 | props: componentProps, 26 | } 27 | }, 28 | render(h) { 29 | return h(component, { 30 | props: this.props, 31 | }) 32 | }, 33 | })) 34 | 35 | return () => { 36 | vueInstance?.$destroy() 37 | } 38 | }, []) 39 | 40 | React.useEffect(() => { 41 | if (vueInstance) { 42 | const keys = Object.keys(componentProps) 43 | keys.forEach(key => { vueInstance.props[key] = componentProps[key] }) 44 | } 45 | }, [Object.values(componentProps)]) 46 | 47 | return
48 | } 49 | 50 | export default VueWrapper 51 | -------------------------------------------------------------------------------- /src/components/ExcalidrawMenu.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { useCallback, memo } from 'react' 7 | import { Icon } from '@mdi/react' 8 | import { mdiMonitorScreenshot } from '@mdi/js' 9 | import { MainMenu } from '@excalidraw/excalidraw' 10 | 11 | interface ExcalidrawMenuProps { 12 | fileNameWithoutExtension: string 13 | } 14 | 15 | export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExtension }: ExcalidrawMenuProps) { 16 | const takeScreenshot = useCallback(() => { 17 | const canvas = document.querySelector('.excalidraw__canvas') as HTMLCanvasElement 18 | if (canvas) { 19 | const dataUrl = canvas.toDataURL('image/png') 20 | const downloadLink = document.createElement('a') 21 | downloadLink.href = dataUrl 22 | downloadLink.download = `${fileNameWithoutExtension} Screenshot.png` 23 | document.body.appendChild(downloadLink) 24 | downloadLink.click() 25 | } 26 | }, [fileNameWithoutExtension]) 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | } 36 | onSelect={takeScreenshot}> 37 | {'Download screenshot'} 38 | 39 | 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/FileDownloadButton.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | import { FC, useEffect, useState } from 'react' 6 | import { createPortal } from 'react-dom' 7 | import { Icon } from '@mdi/react' 8 | import { mdiDownloadBox } from '@mdi/js' 9 | import type { Meta } from '../hooks/useFiles' 10 | 11 | interface FileDownloadButtonProps { 12 | meta: Meta 13 | onDownload: (meta: Meta) => void 14 | } 15 | 16 | export const FileDownloadButton: FC = ({ meta, onDownload }) => { 17 | const [container, setContainer] = useState(null) 18 | const [, setPanelColumnElement] = useState(null) 19 | 20 | useEffect(() => { 21 | // Find the sidebar container 22 | const sideBar = document.querySelector('.App-menu__left') 23 | if (!sideBar) return 24 | 25 | // Find the panel column that needs to be hidden 26 | const panelColumn = sideBar.querySelector('.panelColumn') as HTMLElement 27 | if (panelColumn) { 28 | // Save original display style to restore later 29 | panelColumn.dataset.originalDisplay = panelColumn.style.display 30 | panelColumn.style.display = 'none' 31 | setPanelColumnElement(panelColumn) 32 | } 33 | 34 | // Create container for our download button 35 | const downloadContainer = document.createElement('div') 36 | downloadContainer.classList.add('nc-download') 37 | sideBar.appendChild(downloadContainer) 38 | 39 | setContainer(downloadContainer) 40 | 41 | // Cleanup on unmount 42 | return () => { 43 | if (panelColumn && panelColumn.dataset.originalDisplay !== undefined) { 44 | panelColumn.style.display = panelColumn.dataset.originalDisplay 45 | delete panelColumn.dataset.originalDisplay 46 | } 47 | 48 | if (downloadContainer && downloadContainer.parentNode) { 49 | downloadContainer.parentNode.removeChild(downloadContainer) 50 | } 51 | } 52 | }, []) 53 | 54 | // Only render when we have a container 55 | if (!container) return null 56 | 57 | // Use createPortal to render into our container 58 | return createPortal( 59 |
65 | 73 | 79 | {meta.name} 80 | 81 | 91 |
, 92 | container, 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/database/db.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import * as Dexie from 'dexie' 7 | import type { Table } from 'dexie' 8 | import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types' 9 | import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types/types' 10 | 11 | export interface WhiteboardData { 12 | id: number 13 | elements: ExcalidrawElement[] 14 | files: BinaryFiles 15 | appState?: AppState 16 | savedAt?: number 17 | } 18 | 19 | export class WhiteboardDatabase extends Dexie.Dexie { 20 | 21 | whiteboards!: Table 22 | 23 | constructor() { 24 | super('WhiteboardDatabase') 25 | 26 | this.version(1).stores({ 27 | whiteboards: '++id, savedAt', 28 | }) 29 | } 30 | 31 | async get( 32 | fileId: number, 33 | ): Promise { 34 | return this.whiteboards.get(fileId) 35 | } 36 | 37 | async put( 38 | fileId: number, 39 | elements: ExcalidrawElement[], 40 | files: BinaryFiles, 41 | appState?: AppState, 42 | ): Promise { 43 | const data = { 44 | id: fileId, 45 | elements, 46 | files, 47 | appState, 48 | savedAt: Date.now(), 49 | } 50 | 51 | return this.whiteboards.put(data) 52 | } 53 | 54 | async delete(fileId: number): Promise { 55 | return this.whiteboards.delete(fileId) 56 | } 57 | 58 | async clear(): Promise { 59 | return this.whiteboards.clear() 60 | } 61 | 62 | } 63 | 64 | export const db = new WhiteboardDatabase() 65 | -------------------------------------------------------------------------------- /src/hooks/useSidebarDownload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | import { useState, useCallback } from 'react' 6 | import type { Meta } from './useFiles' 7 | 8 | export function useSidebarDownload(downloadFile: (meta: Meta) => void) { 9 | const [activeMeta, setActiveMeta] = useState(null) 10 | 11 | const showDownloadButton = useCallback((meta: Meta) => { 12 | setActiveMeta(meta) 13 | }, []) 14 | 15 | const hideDownloadButton = useCallback(() => { 16 | setActiveMeta(null) 17 | }, []) 18 | 19 | const handleDownload = useCallback( 20 | (meta: Meta) => { 21 | downloadFile(meta) 22 | 23 | // hideDownloadButton() 24 | }, 25 | [downloadFile], 26 | ) 27 | 28 | return { 29 | activeMeta, 30 | showDownloadButton, 31 | hideDownloadButton, 32 | handleDownload, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/useThemeHandling.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { useState, useEffect } from 'react' 7 | import type { Theme } from '@excalidraw/excalidraw/types/types' 8 | 9 | export function useThemeHandling() { 10 | const [theme, setTheme] = useState('light') 11 | 12 | const isDarkMode = () => { 13 | const ncThemes = document.body.dataset?.themes 14 | return ( 15 | (window.matchMedia('(prefers-color-scheme: dark)').matches 16 | && (ncThemes === undefined 17 | || ncThemes?.indexOf('light') === -1)) 18 | || ncThemes?.indexOf('dark') > -1 19 | ) 20 | } 21 | 22 | useEffect(() => { 23 | setTheme(isDarkMode() ? 'dark' : 'light') 24 | }, []) 25 | 26 | useEffect(() => { 27 | const themeChangeListener = () => 28 | setTheme(isDarkMode() ? 'dark' : 'light') 29 | const mq = window.matchMedia('(prefers-color-scheme: dark)') 30 | mq.addEventListener('change', themeChangeListener) 31 | return () => { 32 | mq.removeEventListener('change', themeChangeListener) 33 | } 34 | }, []) 35 | 36 | return { theme } 37 | } 38 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | import Vue from 'vue' 6 | import AdminSettings from './settings/Settings.vue' 7 | import { t, n } from '@nextcloud/l10n' 8 | 9 | Vue.prototype.t = t 10 | Vue.prototype.n = n 11 | 12 | /* eslint-disable-next-line no-new */ 13 | new Vue({ 14 | render: h => h(AdminSettings, {}), 15 | }).$mount('#admin-vue') 16 | -------------------------------------------------------------------------------- /src/sidebar/ExampleSidebar.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2020 Excalidraw 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | // https://github.com/excalidraw/excalidraw/blob/4dc4590f247a0a0d9c3f5d39fe09c00c5cef87bf/examples/excalidraw 7 | 8 | .sidebar { 9 | height: 100%; 10 | width: 0; 11 | position: absolute; 12 | z-index: 1; 13 | top: 0; 14 | left: 0; 15 | background-color: #111; 16 | overflow-x: hidden; 17 | transition: 0.5s; 18 | padding-top: 60px; 19 | 20 | &.open { 21 | width: 300px; 22 | } 23 | 24 | } 25 | 26 | .sidebar a { 27 | padding: 8px 8px 8px 32px; 28 | text-decoration: none; 29 | font-size: 25px; 30 | color: #818181; 31 | display: block; 32 | transition: 0.3s; 33 | } 34 | 35 | .sidebar a:hover { 36 | color: #f1f1f1; 37 | } 38 | 39 | .sidebar .closebtn { 40 | position: absolute; 41 | top: 0; 42 | right: 0; 43 | font-size: 36px; 44 | margin-left: 50px; 45 | } 46 | 47 | .openbtn { 48 | font-size: 20px; 49 | cursor: pointer; 50 | background-color: #111; 51 | color: white; 52 | padding: 10px 15px; 53 | border: none; 54 | display: flex; 55 | margin-left: 50px; 56 | } 57 | 58 | .sidebar-open { 59 | margin-left: 300px; 60 | } -------------------------------------------------------------------------------- /src/stores/useCollaborationStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { create } from 'zustand' 7 | import type { Socket } from 'socket.io-client' 8 | 9 | export type CollaborationConnectionStatus = 'online' | 'offline' | 'connecting' | 'reconnecting' 10 | 11 | interface CollaborationStore { 12 | status: CollaborationConnectionStatus 13 | socket: Socket | null 14 | isDedicatedSyncer: boolean // Is this client responsible for syncing to server/broadcasting? 15 | 16 | // Actions 17 | setStatus: (status: CollaborationConnectionStatus) => void 18 | setSocket: (socket: Socket | null) => void 19 | setDedicatedSyncer: (isSyncer: boolean) => void 20 | resetStore: () => void 21 | } 22 | 23 | const initialState: Omit = { 24 | status: 'offline', 25 | socket: null, 26 | isDedicatedSyncer: false, 27 | } 28 | 29 | export const useCollaborationStore = create()((set) => ({ 30 | ...initialState, 31 | 32 | setStatus: (status) => set((state) => (state.status === status ? {} : { status })), 33 | setSocket: (socket) => set({ socket }), 34 | setDedicatedSyncer: (isSyncer) => set({ isDedicatedSyncer: isSyncer }), 35 | resetStore: () => set(initialState), 36 | })) 37 | -------------------------------------------------------------------------------- /src/stores/useExcalidrawStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { create } from 'zustand' 7 | import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types' 8 | 9 | interface ExcalidrawStore { 10 | excalidrawAPI: ExcalidrawImperativeAPI | null 11 | 12 | setExcalidrawAPI: (api: ExcalidrawImperativeAPI) => void 13 | resetExcalidrawAPI: () => void 14 | scrollToContent: () => void 15 | } 16 | 17 | export const useExcalidrawStore = create((set, get) => ({ 18 | excalidrawAPI: null, 19 | 20 | setExcalidrawAPI: (api) => set({ excalidrawAPI: api }), 21 | resetExcalidrawAPI: () => set({ excalidrawAPI: null }), 22 | scrollToContent: () => { 23 | const { excalidrawAPI } = get() 24 | if (!excalidrawAPI) return 25 | 26 | const elements = excalidrawAPI.getSceneElements() 27 | excalidrawAPI.scrollToContent(elements, { 28 | fitToContent: true, 29 | animate: true, 30 | duration: 500, 31 | }) 32 | }, 33 | })) 34 | -------------------------------------------------------------------------------- /src/stores/useLangStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { create } from 'zustand' 7 | import { getLanguage } from '@nextcloud/l10n' 8 | import { languages } from '@excalidraw/excalidraw' 9 | 10 | const languageMap = new Map( 11 | languages.map((lang) => [lang.code.toLowerCase(), lang.code]), 12 | ) 13 | 14 | function mapNextcloudToExcalidrawLang(nextcloudLang: string): string { 15 | const lowerNextcloudLang = nextcloudLang.toLowerCase() 16 | 17 | if (languageMap.has(lowerNextcloudLang)) { 18 | return languageMap.get(lowerNextcloudLang)! 19 | } 20 | 21 | const hyphenatedLang = lowerNextcloudLang.replace('_', '-') 22 | if (languageMap.has(hyphenatedLang)) { 23 | return languageMap.get(hyphenatedLang)! 24 | } 25 | 26 | for (const [excalidrawLang, originalCode] of languageMap) { 27 | if ( 28 | excalidrawLang.startsWith(lowerNextcloudLang) 29 | || lowerNextcloudLang.startsWith(excalidrawLang.split('-')[0]) 30 | ) { 31 | return originalCode 32 | } 33 | } 34 | 35 | return 'en' 36 | } 37 | 38 | interface ExcalidrawLangStore { 39 | lang: string 40 | updateLang: () => void 41 | setLang: (lang: string) => void 42 | } 43 | 44 | export const useLangStore = create()((set) => ({ 45 | lang: mapNextcloudToExcalidrawLang(getLanguage()), 46 | 47 | updateLang: () => { 48 | const nextcloudLang = getLanguage() 49 | set({ lang: mapNextcloudToExcalidrawLang(nextcloudLang) }) 50 | }, 51 | 52 | setLang: (lang) => set({ lang }), 53 | })) 54 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2020 Excalidraw 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | // https://github.com/excalidraw/excalidraw/blob/4dc4590f247a0a0d9c3f5d39fe09c00c5cef87bf/examples/excalidraw/utils.ts 7 | 8 | /* eslint-disable @typescript-eslint/no-explicit-any */ 9 | 10 | /* eslint-disable-next-line camelcase */ 11 | import { unstable_batchedUpdates } from 'react-dom' 12 | 13 | export const throttleRAF = ( 14 | fn: (...args: T) => void, 15 | opts?: { trailing?: boolean }, 16 | ) => { 17 | let timerId: number | null = null 18 | let lastArgs: T | null = null 19 | let lastArgsTrailing: T | null = null 20 | 21 | const scheduleFunc = (args: T) => { 22 | timerId = window.requestAnimationFrame(() => { 23 | timerId = null 24 | fn(...args) 25 | lastArgs = null 26 | if (lastArgsTrailing) { 27 | lastArgs = lastArgsTrailing 28 | lastArgsTrailing = null 29 | scheduleFunc(lastArgs) 30 | } 31 | }) 32 | } 33 | 34 | const ret = (...args: T) => { 35 | if (process.env.NODE_ENV === 'test') { 36 | fn(...args) 37 | return 38 | } 39 | lastArgs = args 40 | if (timerId === null) { 41 | scheduleFunc(lastArgs) 42 | } else if (opts?.trailing) { 43 | lastArgsTrailing = args 44 | } 45 | } 46 | ret.flush = () => { 47 | if (timerId !== null) { 48 | cancelAnimationFrame(timerId) 49 | timerId = null 50 | } 51 | if (lastArgs) { 52 | fn(...(lastArgsTrailing || lastArgs)) 53 | lastArgs = lastArgsTrailing = null 54 | } 55 | } 56 | ret.cancel = () => { 57 | lastArgs = lastArgsTrailing = null 58 | if (timerId !== null) { 59 | cancelAnimationFrame(timerId) 60 | timerId = null 61 | } 62 | } 63 | return ret 64 | } 65 | 66 | export const withBatchedUpdates = < 67 | TFunction extends ((event: any) => void) | (() => void) 68 | >( 69 | func: Parameters['length'] extends 0 | 1 ? TFunction : never, 70 | ) => 71 | ((event) => { 72 | unstable_batchedUpdates(func as TFunction, event) 73 | }) as TFunction 74 | 75 | export const withBatchedUpdatesThrottled = < 76 | TFunction extends ((event: any) => void) | (() => void) 77 | >( 78 | func: Parameters['length'] extends 0 | 1 ? TFunction : never, 79 | ) => { 80 | // @ts-ingore 81 | return throttleRAF>(((event) => { 82 | unstable_batchedUpdates(func, event) 83 | }) as TFunction) 84 | } 85 | 86 | export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { 87 | const xd = x2 - x1 88 | const yd = y2 - y1 89 | return Math.hypot(xd, yd) 90 | } 91 | 92 | export const resolvablePromise = () => { 93 | let resolve!: any 94 | let reject!: any 95 | const promise = new Promise((_resolve, _reject) => { 96 | resolve = _resolve 97 | reject = _reject 98 | }); 99 | (promise as any).resolve = resolve; 100 | (promise as any).reject = reject 101 | return promise as ResolvablePromise 102 | } 103 | -------------------------------------------------------------------------------- /src/viewer.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | .whiteboard { 7 | width: 100%; 8 | height: calc(100% + 50px); 9 | position: absolute; 10 | top: 0; 11 | } 12 | 13 | .whiteboard-viewer__embedding .App { 14 | min-height: max(400px, 50vh); 15 | 16 | .excalidraw-wrapper { 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/admin.php: -------------------------------------------------------------------------------- 1 | 9 |
10 | -------------------------------------------------------------------------------- /tests/Unit/AppInfo/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | createMock(\OCP\AppFramework\Bootstrap\IRegistrationContext::class); 17 | $app = new Application(); 18 | $app->register($registrationContext); 19 | self::assertTrue(true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | ({ 8 | default: createConfigMock({ 9 | NEXTCLOUD_URL: 'http://localhost:3008', 10 | NEXTCLOUD_WEBSOCKET_URL: 'http://localhost:3008', 11 | PORT: '3008', 12 | METRICS_TOKEN: 'secret', 13 | }), 14 | })) 15 | 16 | const Config = ConfigModule 17 | const ServerManager = ServerManagerModule 18 | 19 | describe('Metrics endpoint', () => { 20 | let serverManager 21 | 22 | beforeAll(async () => { 23 | serverManager = new ServerManager() 24 | await serverManager.start() 25 | }) 26 | 27 | afterAll(async () => { 28 | await serverManager.gracefulShutdown() 29 | }) 30 | 31 | it('should work with bearer auth', async () => { 32 | const response = await axios.get(`${Config.NEXTCLOUD_URL}/metrics`, { 33 | headers: { 34 | Authorization: `Bearer ${Config.METRICS_TOKEN}`, 35 | }, 36 | }) 37 | expect(response.status).toBe(200) 38 | // Check for memory metrics 39 | expect(response.data).toContain('whiteboard_memory_usage{type="rss"}') 40 | // Check for room metrics 41 | expect(response.data).toContain('whiteboard_room_stats{stat="connectedClients"}') 42 | expect(response.data).toContain('whiteboard_room_stats{stat="activeRooms"}') 43 | // Check for cache metrics 44 | expect(response.data).toContain('whiteboard_cache_stats{stat="size"}') 45 | // Check for socket.io metrics 46 | expect(response.data).toContain('socket_io_connected') 47 | }) 48 | 49 | it('should work with token param', async () => { 50 | const response = await axios.get(`${Config.NEXTCLOUD_URL}/metrics?token=${Config.METRICS_TOKEN}`) 51 | expect(response.status).toBe(200) 52 | expect(response.data).toContain('whiteboard_room_stats{stat="activeRooms"}') 53 | expect(response.data).toContain('whiteboard_memory_usage') 54 | expect(response.data).toContain('whiteboard_cache_stats') 55 | }) 56 | 57 | it('Not return on invalid auth', async () => { 58 | try { 59 | await axios.get(`${Config.NEXTCLOUD_URL}/metrics`, { 60 | headers: { 61 | Authorization: 'Bearer wrongtoken', 62 | }, 63 | }) 64 | expect(true).toBe(false) 65 | } catch (error) { 66 | expect(error.response.status).toBe(403) 67 | } 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./Unit 6 | 7 | 8 | 9 | 10 | ./lib 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /tests/stub.phpstub: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | import { createAppConfig } from '@nextcloud/vite-config' 5 | import react from '@vitejs/plugin-react' 6 | import { viteStaticCopy } from 'vite-plugin-static-copy' 7 | import { defineConfig } from 'vite' 8 | import { join, resolve } from 'path' 9 | 10 | const AppConfig = createAppConfig({ 11 | main: resolve(join('src', 'main.tsx')), 12 | settings: resolve(join('src', 'settings.js')), 13 | }, { 14 | config: defineConfig({ 15 | build: { 16 | cssCodeSplit: true, 17 | chunkSizeWarningLimit: 3000, 18 | minify: 'esbuild', 19 | target: 'es2020', 20 | rollupOptions: { 21 | output: { 22 | manualChunks: { 23 | vendor: ['react', 'react-dom'], 24 | }, 25 | // assetFileNames: 'js/[name]-[hash].[ext]', 26 | }, 27 | }, 28 | }, 29 | worker: { 30 | format: 'es', 31 | rollupOptions: { 32 | output: { 33 | entryFileNames: 'js/[name]-[hash].js', 34 | }, 35 | }, 36 | }, 37 | css: { 38 | modules: { 39 | localsConvention: 'camelCase', 40 | }, 41 | }, 42 | optimizeDeps: { 43 | esbuildOptions: { 44 | jsx: 'automatic', 45 | }, 46 | }, 47 | esbuild: { 48 | jsxInject: 'import React from \'react\'', 49 | }, 50 | plugins: [ 51 | react({ 52 | jsxRuntime: 'classic', 53 | }), 54 | viteStaticCopy({ 55 | targets: [ 56 | { 57 | src: './node_modules/@excalidraw/excalidraw/dist/excalidraw-assets/*', 58 | dest: './dist/excalidraw-assets', 59 | }, 60 | ], 61 | }), 62 | ], 63 | resolve: { 64 | alias: { 65 | '@excalidraw/excalidraw/types': resolve(__dirname, 'node_modules/@excalidraw/excalidraw/types'), 66 | }, 67 | }, 68 | }), 69 | }) 70 | 71 | export default AppConfig 72 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { defineConfig } from 'vitest/config' 7 | 8 | export default defineConfig({ 9 | test: { 10 | environment: 'node', 11 | include: [ 12 | 'tests/integration/*.spec.?(c|m)[jt]s?(x)' 13 | ], 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /websocket_server/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | *.pem 5 | -------------------------------------------------------------------------------- /websocket_server/Config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | /* eslint-disable no-console */ 7 | 8 | import dotenv from 'dotenv' 9 | import crypto from 'crypto' 10 | import { 11 | DEFAULT_NEXTCLOUD_URL, 12 | DEFAULT_PORT, 13 | DEFAULT_STORAGE_STRATEGY, 14 | DEFAULT_FORCE_CLOSE_TIMEOUT, 15 | DEFAULT_REDIS_URL, 16 | DEFAULT_CACHED_TOKEN_TTL, 17 | DEFAULT_COMPRESSION_ENABLED, 18 | } from './Constants.js' 19 | import Utils from './Utils.js' 20 | 21 | dotenv.config() 22 | 23 | const Config = { 24 | IS_TEST_ENV: process.env.NODE_ENV === 'test', 25 | 26 | PORT: process.env.PORT || DEFAULT_PORT, 27 | 28 | USE_TLS: Utils.parseBooleanFromEnv(process.env.TLS), 29 | 30 | TLS_KEY_PATH: process.env.TLS_KEY || null, 31 | 32 | TLS_CERT_PATH: process.env.TLS_CERT || null, 33 | 34 | STORAGE_STRATEGY: process.env.STORAGE_STRATEGY || DEFAULT_STORAGE_STRATEGY, 35 | 36 | REDIS_URL: process.env.REDIS_URL || DEFAULT_REDIS_URL, 37 | 38 | FORCE_CLOSE_TIMEOUT: process.env.FORCE_CLOSE_TIMEOUT || DEFAULT_FORCE_CLOSE_TIMEOUT, 39 | 40 | METRICS_TOKEN: process.env.METRICS_TOKEN || null, 41 | 42 | MAX_UPLOAD_FILE_SIZE: process.env.MAX_UPLOAD_FILE_SIZE * (1e6) || 2e6, 43 | 44 | CACHED_TOKEN_TTL: process.env.CACHED_TOKEN_TTL || DEFAULT_CACHED_TOKEN_TTL, 45 | 46 | // WebSocket compression setting 47 | COMPRESSION_ENABLED: process.env.COMPRESSION_ENABLED !== undefined 48 | ? Utils.parseBooleanFromEnv(process.env.COMPRESSION_ENABLED) 49 | : DEFAULT_COMPRESSION_ENABLED, 50 | 51 | get JWT_SECRET_KEY() { 52 | if (!process.env.JWT_SECRET_KEY) { 53 | const newSecret = crypto.randomBytes(32).toString('hex') 54 | process.env.JWT_SECRET_KEY = newSecret 55 | } 56 | 57 | return process.env.JWT_SECRET_KEY 58 | }, 59 | 60 | get NEXTCLOUD_WEBSOCKET_URL() { 61 | return Utils.getOriginFromUrl(process.env.NEXTCLOUD_URL || DEFAULT_NEXTCLOUD_URL) 62 | }, 63 | 64 | get NEXTCLOUD_URL() { 65 | return this.NEXTCLOUD_WEBSOCKET_URL 66 | }, 67 | } 68 | 69 | export default Config 70 | -------------------------------------------------------------------------------- /websocket_server/Constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | export const DEFAULT_NEXTCLOUD_URL = 'http://nextcloud.local' 7 | 8 | export const DEFAULT_PORT = 3002 9 | 10 | export const DEFAULT_STORAGE_STRATEGY = 'lru' 11 | 12 | export const DEFAULT_FORCE_CLOSE_TIMEOUT = 60 * 60 * 1000 13 | 14 | export const DEFAULT_REDIS_URL = 'redis://localhost:6379' 15 | 16 | export const DEFAULT_CACHED_TOKEN_TTL = 10 * 60 * 1000 17 | 18 | // WebSocket compression default 19 | export const DEFAULT_COMPRESSION_ENABLED = true 20 | -------------------------------------------------------------------------------- /websocket_server/InMemoryStrategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import StorageStrategy from './StorageStrategy.js' 7 | 8 | export default class InMemoryStrategy extends StorageStrategy { 9 | 10 | constructor() { 11 | super() 12 | this.store = new Map() 13 | } 14 | 15 | async get(key) { 16 | return this.store.get(key) 17 | } 18 | 19 | async set(key, value) { 20 | this.store.set(key, value) 21 | } 22 | 23 | async delete(key) { 24 | this.store.delete(key) 25 | } 26 | 27 | async clear() { 28 | this.store.clear() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /websocket_server/LRUStrategy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | import { LRUCache } from 'lru-cache' 9 | import StorageStrategy from './StorageStrategy.js' 10 | 11 | export default class LRUStrategy extends StorageStrategy { 12 | 13 | constructor(options = {}) { 14 | const { max = 1000, ttl = 1000 * 60 * 60 * 24, ttlAutopurge = true } = options 15 | super() 16 | this.cache = new LRUCache({ 17 | max, 18 | ttl, 19 | ttlAutopurge, 20 | }) 21 | } 22 | 23 | async get(key) { 24 | return this.cache.get(key) 25 | } 26 | 27 | async set(key, value) { 28 | this.cache.set(key, value) 29 | } 30 | 31 | async delete(key) { 32 | this.cache.delete(key) 33 | } 34 | 35 | async clear() { 36 | this.cache.clear() 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /websocket_server/RedisStrategy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | import StorageStrategy from './StorageStrategy.js' 9 | 10 | export default class RedisStrategy extends StorageStrategy { 11 | 12 | constructor(redisClient, options = {}) { 13 | const { prefix = 'general_', ttl = null } = options 14 | super() 15 | this.prefix = prefix 16 | this.ttl = ttl 17 | this.client = redisClient 18 | } 19 | 20 | async get(key) { 21 | try { 22 | const data = await this.client.get(`${this.prefix}${key}`) 23 | if (!data) return null 24 | return JSON.parse(data) 25 | } catch (error) { 26 | console.error(`Error getting data for key ${key}:`, error) 27 | return null 28 | } 29 | } 30 | 31 | async set(key, value) { 32 | try { 33 | const serializedData = JSON.stringify(value) 34 | if (this.ttl) { 35 | await this.client.set(`${this.prefix}${key}`, serializedData, { 36 | EX: this.ttl, 37 | }) 38 | } else { 39 | await this.client.set(`${this.prefix}${key}`, serializedData) 40 | } 41 | } catch (error) { 42 | console.error(`Error setting data for key ${key}:`, error) 43 | } 44 | } 45 | 46 | async delete(key) { 47 | try { 48 | await this.client.del(`${this.prefix}${key}`) 49 | } catch (error) { 50 | console.error(`Error deleting key ${key}:`, error) 51 | } 52 | } 53 | 54 | async clear() { 55 | try { 56 | const keys = await this.client.keys(`${this.prefix}*`) 57 | if (keys.length > 0) { 58 | await this.client.del(keys) 59 | } 60 | } catch (error) { 61 | console.error('Error clearing general data:', error) 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /websocket_server/StorageManager.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | import StorageStrategy from './StorageStrategy.js' 9 | import LRUStrategy from './LRUStrategy.js' 10 | import RedisStrategy from './RedisStrategy.js' 11 | import InMemoryStrategy from './InMemoryStrategy.js' 12 | 13 | export default class StorageManager { 14 | 15 | constructor(strategy) { 16 | this.setStrategy(strategy) 17 | } 18 | 19 | setStrategy(strategy) { 20 | if (strategy instanceof StorageStrategy) { 21 | this.strategy = strategy 22 | } else { 23 | throw new Error('Invalid strategy') 24 | } 25 | } 26 | 27 | async get(key) { 28 | return this.strategy.get(key) 29 | } 30 | 31 | async set(key, value) { 32 | await this.strategy.set(key, value) 33 | } 34 | 35 | async delete(key) { 36 | await this.strategy.delete(key) 37 | } 38 | 39 | async clear() { 40 | await this.strategy.clear() 41 | } 42 | 43 | static create(strategyType = 'lru', redisClient = null, options = {}) { 44 | let strategy 45 | 46 | switch (strategyType) { 47 | case 'lru': 48 | strategy = new LRUStrategy(options) 49 | break 50 | case 'redis': 51 | strategy = new RedisStrategy(redisClient, options) 52 | break 53 | case 'in-mem': 54 | strategy = new InMemoryStrategy() 55 | break 56 | default: 57 | throw new Error('Invalid storage strategy type') 58 | } 59 | 60 | return new StorageManager(strategy) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /websocket_server/StorageStrategy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | export default class StorageStrategy { 9 | 10 | async get(key) { 11 | throw new Error('Method not implemented.') 12 | } 13 | 14 | async set(key, value) { 15 | throw new Error('Method not implemented.') 16 | } 17 | 18 | async delete(key) { 19 | throw new Error('Method not implemented.') 20 | } 21 | 22 | async clear() { 23 | throw new Error('Method not implemented.') 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /websocket_server/Utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | /* eslint-disable no-console */ 7 | 8 | export default class Utils { 9 | 10 | static convertStringToArrayBuffer(string) { 11 | return new TextEncoder().encode(string).buffer 12 | } 13 | 14 | static convertArrayBufferToString(arrayBuffer) { 15 | return new TextDecoder().decode(arrayBuffer) 16 | } 17 | 18 | static parseBooleanFromEnv(value) { 19 | return value === 'true' 20 | } 21 | 22 | static getOriginFromUrl(url) { 23 | try { 24 | return new URL(url).origin 25 | } catch (error) { 26 | console.error('Invalid URL:', url) 27 | return null 28 | } 29 | } 30 | 31 | /** 32 | * Logs operation details 33 | * @param {string} context - Context identifier 34 | * @param {string} message - Log message 35 | * @param {object} [data] - Additional data to log 36 | */ 37 | static logOperation(context, message, data = {}) { 38 | console.log(`[${context}] ${message}:`, data) 39 | } 40 | 41 | /** 42 | * Logs error details 43 | * @param {string} context - Context identifier 44 | * @param {string} message - Error message 45 | * @param {Error} error - Error object 46 | */ 47 | static logError(context, message, error) { 48 | console.error(`[${context}] ${message}:`, error) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /websocket_server/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable n/no-process-exit */ 3 | 4 | /** 5 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 6 | * SPDX-License-Identifier: AGPL-3.0-or-later 7 | */ 8 | import ServerManager from './ServerManager.js' 9 | import Config from './Config.js' 10 | 11 | async function main() { 12 | try { 13 | const serverManager = new ServerManager() 14 | 15 | await serverManager.start() 16 | 17 | console.log(`Server started successfully on port ${Config.PORT}`) 18 | 19 | process.on('SIGTERM', () => serverManager.gracefulShutdown()) 20 | process.on('SIGINT', () => serverManager.gracefulShutdown()) 21 | } catch (error) { 22 | console.error('Failed to start server:', error) 23 | process.exit(1) 24 | } 25 | } 26 | 27 | main() 28 | --------------------------------------------------------------------------------