Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 105f7fdaad | |||
| 053353841a | |||
| 286642ffcf | |||
| 7090938ec1 | |||
| 6233bc52e3 | |||
| 7f8ee06710 | |||
| 3e200634e0 | |||
| 701b02d6df | |||
| 535b2b682e | |||
| 1a4a736d94 | |||
| bb207ef861 | |||
| 2fcbf8658f | |||
| 6a8c6e7f47 | |||
| f962503425 | |||
| 02fdc506ee | |||
| 2ba6088e97 | |||
| 4e421e6e9e | |||
| d213dbb42d | |||
| a5779dd5d8 | |||
| 628b4c1eec | |||
| 31333e597b | |||
| 872b340f1b | |||
| 83e8167adf | |||
| baea88942c | |||
| ae35037a3b | |||
| 4f31ae1e4b | |||
| 704ee30ae6 | |||
| 176baef9c2 | |||
| 3558f07fe0 | |||
| fd24c74ab1 | |||
| ae6892501d | |||
| 1e6adaf0b5 | |||
| 7ccd38a37f | |||
| e6ce9e0ea7 | |||
| c0e05445b1 | |||
| b44531d94a | |||
| 464c2cc05e | |||
| bd13c2ed48 | |||
| 2e58aaae66 | |||
| d4c14d484c | |||
| 06d1871640 | |||
| e7a59335e4 | |||
| 0dbef18044 | |||
| e16c2d592f | |||
| 1f4cf4610f | |||
| 1bee959660 | |||
| f609a4ac3a | |||
| eee9d1bc16 | |||
| ecdc2582e9 | |||
| 245e13a884 | |||
| 8a322b22ed | |||
| 3f784d76fc | |||
| ba5afe9139 | |||
| 9e6f351672 | |||
| fcd10a6a43 | |||
| 3bc18f6aed | |||
| 7c5481b877 | |||
| d17464fbaa | |||
| baf9da2b83 | |||
| 4bfcf105a5 | |||
| 74e82d0d7c | |||
| 9dd2257932 | |||
| 9c0f832a41 | |||
| 6cafb6bb90 | |||
| e6cd97c4f2 | |||
| ba9b65b051 | |||
| 830fb64a25 | |||
| 5b343a9d46 | |||
| f8beb305de | |||
| 4b4eecbd27 | |||
| e8bd910b9b | |||
| a9a3e1bca5 | |||
| 2a922dd477 | |||
| 07dab85ebf | |||
| d2ce4a7523 | |||
| dc73f3a9eb | |||
| d100f38750 | |||
| 503500cc74 | |||
| 1a828a43d9 | |||
| d88884466b | |||
| d3d470ac3d | |||
| 54df521a78 | |||
| c5557b5cc1 | |||
| 51875fd627 | |||
| b2ba61bbcf | |||
| 3b7f62c9a0 | |||
| aeafb81479 | |||
| dfe81bf6b2 | |||
| 1d332d597a | |||
| b5fc8757a4 | |||
| ecbd5ba55d | |||
| 6967d8c985 | |||
| 4b253c7362 | |||
| 4fdddb518a | |||
| 0b2e4dd60b | |||
| f162512988 | |||
| 2f7154cdf2 | |||
| 5ab0ce5a33 | |||
| 073f4032f3 | |||
| 73cba59d2d | |||
| 4085071347 | |||
| 8a63187d4f | |||
| 9c51ba6067 | |||
| bf50c9cae7 | |||
| 1801048763 | |||
| 414deea084 | |||
| 979d28d5c6 | |||
| 1f8b7e417f | |||
| 8c968cd13e | |||
| f798000006 | |||
| 7ff3a71179 | |||
| 6c0804d4c3 | |||
| ae8e7aca16 | |||
| 842b185aa6 | |||
| f59387471e | |||
| 60eb709eb3 | |||
| 02539bbb89 | |||
| 77ae5d4605 | |||
| bdd4f69bf6 | |||
| 87ca829490 | |||
| a31a7fd766 | |||
| e70f02063f | |||
| 769f727bd4 | |||
| bc994fcbe2 | |||
| ac5e058222 | |||
| d61970cdac | |||
| 489d4b7469 | |||
| d88de08872 | |||
| 42882e2a93 | |||
| f6374e5bde | |||
| aac9d4e837 | |||
| d9103b8b24 | |||
| 8b56346011 | |||
| e63a0ec5be | |||
| 86222662f2 | |||
| 066560311b | |||
| d6ca981f7a | |||
| f7f98d9dda | |||
| 1a67642fd1 | |||
| 6aa22bada8 | |||
| 00209ef9c3 | |||
| b79ef0d428 | |||
| dc25fe06d0 | |||
| 1e17c1967b | |||
| 23a8891e0e | |||
| 6c81a32d62 | |||
| f0f5430313 | |||
| e18e945cd3 | |||
| bd0c6e63ff | |||
| dbae33e4f8 | |||
| 0f4a053759 | |||
| 1837147c55 | |||
| 15f698dc21 | |||
| ce507b0a0b | |||
| 02598c6163 | |||
| 3e2890bd21 | |||
| 210649f383 | |||
| f8087e01c8 | |||
| e2522645f7 | |||
| d46a9166be | |||
| 675da16ca4 | |||
| 2b1b62d8f2 | |||
| 627c56ef1c |
+6
-6
@@ -1,10 +1,10 @@
|
|||||||
*
|
*
|
||||||
|
!.env
|
||||||
|
!.eslintrc.json
|
||||||
|
!.npmrc
|
||||||
|
!.prettierrc
|
||||||
|
!package.json
|
||||||
!public/
|
!public/
|
||||||
!src/
|
!src/
|
||||||
!.npmrc
|
|
||||||
!.eslintrc.json
|
|
||||||
!.prettierrc
|
|
||||||
!package-lock.json
|
|
||||||
!package.json
|
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
!.env
|
!yarn.lock
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-level EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
+1
-1
@@ -2,6 +2,7 @@
|
|||||||
"extends": ["prettier", "react-app"],
|
"extends": ["prettier", "react-app"],
|
||||||
"plugins": ["prettier"],
|
"plugins": ["prettier"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
"curly": "warn",
|
"curly": "warn",
|
||||||
"dot-notation": "warn",
|
"dot-notation": "warn",
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
@@ -22,7 +23,6 @@
|
|||||||
],
|
],
|
||||||
"no-unneeded-ternary": "warn",
|
"no-unneeded-ternary": "warn",
|
||||||
"no-unused-expressions": "warn",
|
"no-unused-expressions": "warn",
|
||||||
"no-unused-vars": "warn",
|
|
||||||
"no-useless-return": "warn",
|
"no-useless-return": "warn",
|
||||||
"no-var": "warn",
|
"no-var": "warn",
|
||||||
"object-shorthand": "warn",
|
"object-shorthand": "warn",
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/"
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: sunday
|
day: sunday
|
||||||
time: "01:00"
|
time: "01:00"
|
||||||
open-pull-requests-limit: 99
|
|
||||||
reviewers:
|
reviewers:
|
||||||
- lipis
|
- lipis
|
||||||
assignees:
|
assignees:
|
||||||
- lipis
|
- lipis
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/src/packages/excalidraw/"
|
directory: /src/packages/excalidraw/
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: sunday
|
day: sunday
|
||||||
time: "01:00"
|
time: "01:00"
|
||||||
open-pull-requests-limit: 99
|
|
||||||
reviewers:
|
reviewers:
|
||||||
- ad1992
|
- ad1992
|
||||||
assignees:
|
assignees:
|
||||||
- ad1992
|
- ad1992
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/src/packages/utils/"
|
directory: /src/packages/utils/
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: sunday
|
day: sunday
|
||||||
time: "01:00"
|
time: "01:00"
|
||||||
open-pull-requests-limit: 99
|
|
||||||
reviewers:
|
reviewers:
|
||||||
- ad1992
|
- ad1992
|
||||||
assignees:
|
assignees:
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
- run: docker build -t excalidraw .
|
- run: docker build -t excalidraw .
|
||||||
|
|||||||
@@ -7,27 +7,23 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
packages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
yarn --frozen-lockfile
|
||||||
npm ci --prefix src/packages/excalidraw
|
yarn --cwd src/packages/excalidraw
|
||||||
npm ci --prefix src/packages/utils
|
yarn --cwd src/packages/utils
|
||||||
|
|
||||||
- name: Build @excalidraw/excalidraw
|
- name: Build @excalidraw/excalidraw
|
||||||
run: |
|
run: |
|
||||||
npm run pack --prefix src/packages/excalidraw
|
yarn --cwd src/packages/excalidraw run pack
|
||||||
|
|
||||||
- name: Build @excalidraw/utils
|
- name: Build @excalidraw/utils
|
||||||
run: |
|
run: |
|
||||||
npm run pack --prefix src/packages/utils
|
yarn --cwd src/packages/utils run pack
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
name: Cancel previous runs
|
name: Cancel previous runs
|
||||||
|
|
||||||
on: push
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cancel:
|
cancel:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
steps:
|
steps:
|
||||||
- uses: styfle/cancel-workflow-action@0.6.0
|
- uses: styfle/cancel-workflow-action@0.6.0
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on: push
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
yarn --frozen-lockfile
|
||||||
npm run test:other
|
yarn test:other
|
||||||
npm run test:code
|
yarn test:code
|
||||||
npm run test:typecheck
|
yarn test:typecheck
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Build locales coverage
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "l10n_master"
|
- l10n_master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
locales:
|
locales:
|
||||||
@@ -15,13 +15,13 @@ jobs:
|
|||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
npm run locales-coverage
|
yarn locales-coverage
|
||||||
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
||||||
if [ ! -z "${FILE_CHANGED}" ]; then
|
if [ ! -z "${FILE_CHANGED}" ]; then
|
||||||
git config --global user.name 'Excalidraw Bot'
|
git config --global user.name 'Excalidraw Bot'
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Construct comment body
|
- name: Construct comment body
|
||||||
id: getCommentBody
|
id: getCommentBody
|
||||||
run: |
|
run: |
|
||||||
body=$(npm run locales-coverage:description | grep '^[^>]')
|
body=$(yarn locales-coverage:description | grep '^[^>]')
|
||||||
body="${body//'%'/'%25'}"
|
body="${body//'%'/'%25'}"
|
||||||
body="${body//$'\n'/'%0A'}"
|
body="${body//$'\n'/'%0A'}"
|
||||||
body="${body//$'\r'/'%0D'}"
|
body="${body//$'\r'/'%0D'}"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: "Semantic PR title"
|
name: Semantic PR title
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
@@ -8,9 +8,8 @@ on:
|
|||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
semantic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: New Sentry Production Release
|
name: New Sentry production release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,28 +6,23 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
sentry:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1.0.0
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
yarn --frozen-lockfile
|
||||||
npm run build:app
|
yarn build:app
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
- name: Install Sentry
|
- name: Install Sentry
|
||||||
run: |
|
run: |
|
||||||
curl -sL https://sentry.io/get-cli/ | bash
|
curl -sL https://sentry.io/get-cli/ | bash
|
||||||
|
|
||||||
- name: Create new Sentry release
|
- name: Create new Sentry release
|
||||||
run: |
|
run: |
|
||||||
export SENTRY_RELEASE=$(sentry-cli releases propose-version)
|
export SENTRY_RELEASE=$(sentry-cli releases propose-version)
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: push
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
yarn --frozen-lockfile
|
||||||
npm run test:app
|
yarn test:app
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ firebase
|
|||||||
logs
|
logs
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
package-lock.json
|
||||||
static
|
static
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
yarn.lock
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"proseWrap": "never",
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
### Option 2 - CodeSandbox
|
### Option 2 - CodeSandbox
|
||||||
|
|
||||||
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
||||||
1. Connect your Github account
|
1. Connect your GitHub account
|
||||||
1. Go to Git tab on left side
|
1. Go to Git tab on left side
|
||||||
1. Tap on `Fork Sandbox`
|
1. Tap on `Fork Sandbox`
|
||||||
1. Write your code
|
1. Write your code
|
||||||
@@ -35,7 +35,6 @@ Make sure the title starts with a semantic prefix:
|
|||||||
|
|
||||||
- **feat**: A new feature
|
- **feat**: A new feature
|
||||||
- **fix**: A bug fix
|
- **fix**: A bug fix
|
||||||
- **improvement**: An improvement to a current feature
|
|
||||||
- **docs**: Documentation only changes
|
- **docs**: Documentation only changes
|
||||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||||
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||||
+3
-3
@@ -2,13 +2,13 @@ FROM node:14-alpine AS build
|
|||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json yarn.lock ./
|
||||||
RUN npm i --no-optional
|
RUN yarn --ignore-optional
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.17-alpine
|
FROM nginx:1.17-alpine
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<a href="https://excalidraw.com">
|
<a href="https://excalidraw.com">
|
||||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||||
</a>
|
</a>
|
||||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end to end encrypted.</h3>
|
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end-to-end encrypted.</h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://twitter.com/Excalidraw">
|
<a href="https://twitter.com/Excalidraw">
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
|
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Try it now
|
## Try it now
|
||||||
@@ -19,6 +20,14 @@ Go to [excalidraw.com](https://excalidraw.com) to start sketching.
|
|||||||
|
|
||||||
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
|
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
|
||||||
|
|
||||||
|
## Supporting Excalidraw
|
||||||
|
|
||||||
|
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
|
||||||
|
|
||||||
|
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/backers.svg?avatarHeight=32"/></a>
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### Shortcuts
|
### Shortcuts
|
||||||
@@ -41,7 +50,7 @@ Translations will be available on the app if they exceed a certain threshold of
|
|||||||
|
|
||||||
### Create a collaboration session manually
|
### Create a collaboration session manually
|
||||||
|
|
||||||
In order to create a session manually you just need to generate a link of this form:
|
In order to create a session manually, you just need to generate a link of this form:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
|
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
|
||||||
@@ -61,18 +70,28 @@ The second set of digits is the encryption key. The Excalidraw server doesn’t
|
|||||||
|
|
||||||
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||||
|
|
||||||
## Developement
|
## Embedding Excalidraw in your App?
|
||||||
|
|
||||||
|
Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/excalidraw). This package allows you to easily embed Excalidraw as a React component into your apps.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
### Code Sandbox
|
### Code Sandbox
|
||||||
|
|
||||||
- Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
- Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
||||||
- You may need to sign in with Github and reload the page
|
- You may need to sign in with GitHub and reload the page
|
||||||
- You can start coding instantly, and even send PRs from there!
|
- You can start coding instantly, and even send PRs from there!
|
||||||
|
|
||||||
### Local Installation
|
### Local Installation
|
||||||
|
|
||||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/en/)
|
||||||
|
- [Yarn](https://yarnpkg.com/getting-started/install)
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
#### Clone the repo
|
#### Clone the repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -81,26 +100,26 @@ git clone https://github.com/excalidraw/excalidraw.git
|
|||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --------------------- | --------------------------------- |
|
| ------------------ | --------------------------------- |
|
||||||
| `npm install` | Install the dependencies |
|
| `yarn` | Install the dependencies |
|
||||||
| `npm start` | Run the project |
|
| `yarn start` | Run the project |
|
||||||
| `npm run fix` | Reformat all files with Prettier |
|
| `yarn fix` | Reformat all files with Prettier |
|
||||||
| `npm test` | Run tests |
|
| `yarn test` | Run tests |
|
||||||
| `npm run test:update` | Update test snapshots |
|
| `yarn test:update` | Update test snapshots |
|
||||||
| `npm run test:code` | Test for formatting with Prettier |
|
| `yarn test:code` | Test for formatting with Prettier |
|
||||||
|
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
|
|
||||||
You can use docker-compose to work on excalidraw locally if you don't want to setup a Node.js env.
|
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker-compose up --build -d
|
docker-compose up --build -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Self hosting
|
### Self-hosting
|
||||||
|
|
||||||
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self host your own client under your own domain, on Kubernetes, AWS ECS, etc.
|
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t excalidraw/excalidraw .
|
docker build -t excalidraw/excalidraw .
|
||||||
@@ -111,7 +130,7 @@ The Docker image is free of analytics and other tracking libraries.
|
|||||||
|
|
||||||
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
|
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
|
||||||
|
|
||||||
We are working towards providing a full-fledged solution for self hosting your own Excalidraw.
|
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./:/opt/node_app/app:delegated
|
- ./:/opt/node_app/app:delegated
|
||||||
- ./package.json:/opt/node_app/package.json
|
- ./package.json:/opt/node_app/package.json
|
||||||
- ./package-lock.json:/opt/node_app/package-lock.json
|
- ./yarn.lock:/opt/node_app/yarn.lock
|
||||||
- notused:/opt/node_app/app/node_modules
|
- notused:/opt/node_app/app/node_modules
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Generated
-23391
File diff suppressed because it is too large
Load Diff
+34
-31
@@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
],
|
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
"not dead",
|
"not dead",
|
||||||
@@ -16,24 +11,28 @@
|
|||||||
"not chrome < 70",
|
"not chrome < 70",
|
||||||
"not and_uc < 13",
|
"not and_uc < 13",
|
||||||
"not samsung < 10"
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "6.0.1",
|
"@sentry/browser": "6.2.0",
|
||||||
"@sentry/integrations": "6.0.1",
|
"@sentry/integrations": "6.2.0",
|
||||||
"@testing-library/jest-dom": "5.11.9",
|
"@testing-library/jest-dom": "5.11.9",
|
||||||
"@testing-library/react": "11.2.3",
|
"@testing-library/react": "11.2.5",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/react": "17.0.0",
|
"@types/react": "17.0.2",
|
||||||
"@types/react-dom": "17.0.0",
|
"@types/react-dom": "17.0.1",
|
||||||
"@types/socket.io-client": "1.4.35",
|
"@types/socket.io-client": "1.4.35",
|
||||||
"browser-fs-access": "0.13.0",
|
"browser-fs-access": "0.14.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.5",
|
"firebase": "8.2.9",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.1.20",
|
"nanoid": "3.1.20",
|
||||||
"node-sass": "4.14.1",
|
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
@@ -43,26 +42,29 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.3.1",
|
"roughjs": "4.3.1",
|
||||||
|
"sass": "1.32.8",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"typescript": "4.1.3"
|
"typescript": "4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.6",
|
||||||
"@types/pako": "1.0.1",
|
"@types/pako": "1.0.1",
|
||||||
"eslint-config-prettier": "7.2.0",
|
"@types/resize-observer-browser": "0.1.5",
|
||||||
|
"eslint-config-prettier": "8.1.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"firebase-tools": "9.2.2",
|
"firebase-tools": "9.5.0",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"jest-canvas-mock": "2.3.0",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"lint-staged": "10.5.3",
|
"lint-staged": "10.5.4",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"rewire": "5.0.0"
|
"rewire": "5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"husky": {
|
"husky": {
|
||||||
@@ -71,34 +73,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"resetMocks": false,
|
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
||||||
]
|
],
|
||||||
|
"resetMocks": false
|
||||||
},
|
},
|
||||||
"name": "excalidraw",
|
"name": "excalidraw",
|
||||||
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:app && npm run build:version",
|
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
|
||||||
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||||
|
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "node ./scripts/build-version.js",
|
||||||
|
"build": "yarn build:app && yarn build:version",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"fix": "npm run fix:other && npm run fix:code",
|
"fix:code": "yarn test:code --fix",
|
||||||
"fix:code": "npm run test:code -- --fix",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix:other": "npm run prettier -- --write",
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"test": "npm run test:app",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
|
||||||
"test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
|
|
||||||
"test:app": "react-scripts test --passWithNoTests",
|
"test:app": "react-scripts test --passWithNoTests",
|
||||||
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||||
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||||
"test:other": "npm run prettier -- --list-different",
|
"test:other": "yarn prettier --list-different",
|
||||||
"test:typecheck": "tsc",
|
"test:typecheck": "tsc",
|
||||||
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false"
|
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||||
|
"test": "yarn test:app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */
|
/* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Virgil";
|
font-family: "Virgil";
|
||||||
src: url("FG_Virgil.woff2");
|
src: url("Virgil.woff2");
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -57,9 +57,10 @@
|
|||||||
|
|
||||||
<!-- Excalidraw version -->
|
<!-- Excalidraw version -->
|
||||||
<meta name="version" content="{version}" />
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="FG_Virgil.woff2"
|
href="Virgil.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
@@ -85,7 +86,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||||
|
<script>
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||||
|
</script>
|
||||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// In order to run:
|
// In order to run:
|
||||||
// npm install canvas # please do not check it in
|
// npm install canvas # please do not check it in
|
||||||
// npm run build-node
|
// yarn build-node
|
||||||
// node build/static/js/build-node.js
|
// node build/static/js/build-node.js
|
||||||
// open test.png
|
// open test.png
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const crowdinMap = {
|
|||||||
"id-ID": "en-id",
|
"id-ID": "en-id",
|
||||||
"it-IT": "en-it",
|
"it-IT": "en-it",
|
||||||
"ja-JP": "en-ja",
|
"ja-JP": "en-ja",
|
||||||
|
"kab-KAB": "en-kab",
|
||||||
"ko-KR": "en-ko",
|
"ko-KR": "en-ko",
|
||||||
"my-MM": "en-my",
|
"my-MM": "en-my",
|
||||||
"nb-NO": "en-nb",
|
"nb-NO": "en-nb",
|
||||||
@@ -40,7 +41,7 @@ const crowdinMap = {
|
|||||||
const flags = {
|
const flags = {
|
||||||
"ar-SA": "🇸🇦",
|
"ar-SA": "🇸🇦",
|
||||||
"bg-BG": "🇧🇬",
|
"bg-BG": "🇧🇬",
|
||||||
"ca-ES": "🇪🇸",
|
"ca-ES": "🏳",
|
||||||
"de-DE": "🇩🇪",
|
"de-DE": "🇩🇪",
|
||||||
"el-GR": "🇬🇷",
|
"el-GR": "🇬🇷",
|
||||||
"es-ES": "🇪🇸",
|
"es-ES": "🇪🇸",
|
||||||
@@ -53,6 +54,7 @@ const flags = {
|
|||||||
"id-ID": "🇮🇩",
|
"id-ID": "🇮🇩",
|
||||||
"it-IT": "🇮🇹",
|
"it-IT": "🇮🇹",
|
||||||
"ja-JP": "🇯🇵",
|
"ja-JP": "🇯🇵",
|
||||||
|
"kab-KAB": "🏳",
|
||||||
"ko-KR": "🇰🇷",
|
"ko-KR": "🇰🇷",
|
||||||
"my-MM": "🇲🇲",
|
"my-MM": "🇲🇲",
|
||||||
"nb-NO": "🇳🇴",
|
"nb-NO": "🇳🇴",
|
||||||
@@ -88,6 +90,7 @@ const languages = {
|
|||||||
"id-ID": "Bahasa Indonesia",
|
"id-ID": "Bahasa Indonesia",
|
||||||
"it-IT": "Italiano",
|
"it-IT": "Italiano",
|
||||||
"ja-JP": "日本語",
|
"ja-JP": "日本語",
|
||||||
|
"kab-KAB": "Taqbaylit",
|
||||||
"ko-KR": "한국어",
|
"ko-KR": "한국어",
|
||||||
"my-MM": "Burmese",
|
"my-MM": "Burmese",
|
||||||
"nb-NO": "Norsk bokmål",
|
"nb-NO": "Norsk bokmål",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ProjectName } from "../components/ProjectName";
|
|||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
|
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
@@ -96,9 +97,24 @@ export const actionChangeShouldAddWatermark = register({
|
|||||||
export const actionSaveScene = register({
|
export const actionSaveScene = register({
|
||||||
name: "saveScene",
|
name: "saveScene",
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
const { fileHandle } = await saveAsJSON(elements, appState);
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
fileHandle,
|
||||||
|
toastMessage: fileHandleExists
|
||||||
|
? fileHandle.name
|
||||||
|
? t("toast.fileSavedToFilename").replace(
|
||||||
|
"{filename}",
|
||||||
|
`"${fileHandle.name}"`,
|
||||||
|
)
|
||||||
|
: t("toast.fileSaved")
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -189,3 +205,31 @@ export const actionLoadScene = register({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionExportWithDarkMode = register({
|
||||||
|
name: "exportWithDarkMode",
|
||||||
|
perform: (_elements, appState, value) => {
|
||||||
|
return {
|
||||||
|
appState: { ...appState, exportWithDarkMode: value },
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
marginTop: "-45px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DarkModeToggle
|
||||||
|
value={appState.exportWithDarkMode ? "dark" : "light"}
|
||||||
|
onChange={(appearance: Appearence) => {
|
||||||
|
updateData(appearance === "dark");
|
||||||
|
}}
|
||||||
|
title={t("labels.toggleExportColorScheme")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const actionFinalize = register({
|
|||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
// set the last point to first point.
|
// set the last point to first point.
|
||||||
// This ensures that loop remains closed at different scales.
|
// This ensures that loop remains closed at different scales.
|
||||||
const isLoop = isPathALoop(multiPointElement.points);
|
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||||
if (
|
if (
|
||||||
multiPointElement.type === "line" ||
|
multiPointElement.type === "line" ||
|
||||||
multiPointElement.type === "draw"
|
multiPointElement.type === "draw"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const actionGoToCollaborator = register({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId);
|
const { background, stroke } = getClientColors(clientId, appState);
|
||||||
const shortName = getClientInitials(collaborator.username);
|
const shortName = getClientInitials(collaborator.username);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { CODES, KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const actionToggleGridMode = register({
|
export const actionToggleGridMode = register({
|
||||||
name: "gridMode",
|
name: "gridMode",
|
||||||
perform(elements, appState) {
|
perform(elements, appState) {
|
||||||
|
trackEvent("view", "mode", "grid");
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@@ -15,6 +17,6 @@ export const actionToggleGridMode = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState: AppState) => appState.gridSize !== null,
|
checked: (appState: AppState) => appState.gridSize !== null,
|
||||||
contextItemLabel: "labels.gridMode",
|
contextItemLabel: "labels.showGrid",
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
import { register } from "./register";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
|
export const actionToggleViewMode = register({
|
||||||
|
name: "viewMode",
|
||||||
|
perform(elements, appState) {
|
||||||
|
trackEvent("view", "mode", "view");
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
viewModeEnabled: !this.checked!(appState),
|
||||||
|
selectedElementIds: {},
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState) => appState.viewModeEnabled,
|
||||||
|
contextItemLabel: "labels.viewMode",
|
||||||
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R,
|
||||||
|
});
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
export const actionToggleZenMode = register({
|
export const actionToggleZenMode = register({
|
||||||
name: "zenMode",
|
name: "zenMode",
|
||||||
perform(elements, appState) {
|
perform(elements, appState) {
|
||||||
|
trackEvent("view", "mode", "zen");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
ActionResult,
|
ActionResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
|
import { MODES } from "../constants";
|
||||||
|
|
||||||
// This is the <App> component, but for now we don't care about anything but its
|
// This is the <App> component, but for now we don't care about anything but its
|
||||||
// `canvas` state.
|
// `canvas` state.
|
||||||
type App = { canvas: HTMLCanvasElement | null };
|
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@@ -66,6 +67,12 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const { viewModeEnabled } = this.getAppState();
|
||||||
|
if (viewModeEnabled) {
|
||||||
|
if (!Object.values(MODES).includes(data[0].name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.updater(
|
this.updater(
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export type ShortcutName =
|
|||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary";
|
| "addToLibrary"
|
||||||
|
| "viewMode";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
@@ -56,6 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
stats: [],
|
stats: [],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ export type ActionName =
|
|||||||
| "alignVerticallyCentered"
|
| "alignVerticallyCentered"
|
||||||
| "alignHorizontallyCentered"
|
| "alignHorizontallyCentered"
|
||||||
| "distributeHorizontally"
|
| "distributeHorizontally"
|
||||||
| "distributeVertically";
|
| "distributeVertically"
|
||||||
|
| "viewMode"
|
||||||
|
| "exportWithDarkMode";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
exportEmbedScene: false,
|
exportEmbedScene: false,
|
||||||
|
exportWithDarkMode: false,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
gridSize: null,
|
gridSize: null,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
@@ -72,6 +73,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||||
|
viewModeEnabled: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,6 +119,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
errorMessage: { browser: false, export: false },
|
errorMessage: { browser: false, export: false },
|
||||||
exportBackground: { browser: true, export: false },
|
exportBackground: { browser: true, export: false },
|
||||||
exportEmbedScene: { browser: true, export: false },
|
exportEmbedScene: { browser: true, export: false },
|
||||||
|
exportWithDarkMode: { browser: true, export: false },
|
||||||
fileHandle: { browser: false, export: false },
|
fileHandle: { browser: false, export: false },
|
||||||
gridSize: { browser: true, export: true },
|
gridSize: { browser: true, export: true },
|
||||||
height: { browser: false, export: false },
|
height: { browser: false, export: false },
|
||||||
@@ -151,6 +154,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
width: { browser: false, export: false },
|
width: { browser: false, export: false },
|
||||||
zenModeEnabled: { browser: true, export: false },
|
zenModeEnabled: { browser: true, export: false },
|
||||||
zoom: { browser: true, export: false },
|
zoom: { browser: true, export: false },
|
||||||
|
viewModeEnabled: { browser: false, export: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,13 @@
|
|||||||
import colors from "./colors";
|
import colors from "./colors";
|
||||||
|
import { AppState } from "./types";
|
||||||
|
|
||||||
export const getClientColors = (clientId: string) => {
|
export const getClientColors = (clientId: string, appState: AppState) => {
|
||||||
|
if (appState?.collaborators) {
|
||||||
|
const currentUser = appState.collaborators.get(clientId);
|
||||||
|
if (currentUser?.color) {
|
||||||
|
return currentUser.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Naive way of getting an integer out of the clientId
|
// Naive way of getting an integer out of the clientId
|
||||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
||||||
|
|
||||||
|
|||||||
+238
-78
@@ -2,7 +2,8 @@ import { Point, simplify } from "points-on-curve";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import "../actions";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
actionBringForward,
|
actionBringForward,
|
||||||
@@ -46,9 +47,11 @@ import {
|
|||||||
ELEMENT_TRANSLATE_AMOUNT,
|
ELEMENT_TRANSLATE_AMOUNT,
|
||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
|
GRID_SIZE,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
POINTER_BUTTON,
|
POINTER_BUTTON,
|
||||||
|
SCROLL_TIMEOUT,
|
||||||
TAP_TWICE_TIMEOUT,
|
TAP_TWICE_TIMEOUT,
|
||||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||||
TOUCH_CTX_MENU_TIMEOUT,
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
@@ -175,10 +178,11 @@ import {
|
|||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { isMobile } from "../is-mobile";
|
import { isMobile } from "../is-mobile";
|
||||||
import ContextMenu from "./ContextMenu";
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
|
|
||||||
@@ -295,6 +299,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
offsetLeft,
|
offsetLeft,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
excalidrawRef,
|
excalidrawRef,
|
||||||
|
viewModeEnabled = false,
|
||||||
|
zenModeEnabled = false,
|
||||||
|
gridModeEnabled = false,
|
||||||
} = props;
|
} = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
...defaultAppState,
|
...defaultAppState,
|
||||||
@@ -302,6 +309,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
||||||
|
viewModeEnabled,
|
||||||
|
zenModeEnabled,
|
||||||
|
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||||
};
|
};
|
||||||
if (excalidrawRef) {
|
if (excalidrawRef) {
|
||||||
const readyPromise =
|
const readyPromise =
|
||||||
@@ -342,6 +352,62 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.actionManager.registerAction(createRedoAction(history));
|
this.actionManager.registerAction(createRedoAction(history));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderCanvas() {
|
||||||
|
const canvasScale = window.devicePixelRatio;
|
||||||
|
const {
|
||||||
|
width: canvasDOMWidth,
|
||||||
|
height: canvasDOMHeight,
|
||||||
|
viewModeEnabled,
|
||||||
|
} = this.state;
|
||||||
|
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||||
|
const canvasHeight = canvasDOMHeight * canvasScale;
|
||||||
|
if (viewModeEnabled) {
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
id="canvas"
|
||||||
|
style={{
|
||||||
|
width: canvasDOMWidth,
|
||||||
|
height: canvasDOMHeight,
|
||||||
|
cursor: "grabbing",
|
||||||
|
}}
|
||||||
|
width={canvasWidth}
|
||||||
|
height={canvasHeight}
|
||||||
|
ref={this.handleCanvasRef}
|
||||||
|
onContextMenu={this.handleCanvasContextMenu}
|
||||||
|
onPointerMove={this.handleCanvasPointerMove}
|
||||||
|
onPointerUp={this.removePointer}
|
||||||
|
onPointerCancel={this.removePointer}
|
||||||
|
onTouchMove={this.handleTouchMove}
|
||||||
|
onPointerDown={this.handleCanvasPointerDown}
|
||||||
|
>
|
||||||
|
{t("labels.drawingCanvas")}
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
id="canvas"
|
||||||
|
style={{
|
||||||
|
width: canvasDOMWidth,
|
||||||
|
height: canvasDOMHeight,
|
||||||
|
}}
|
||||||
|
width={canvasWidth}
|
||||||
|
height={canvasHeight}
|
||||||
|
ref={this.handleCanvasRef}
|
||||||
|
onContextMenu={this.handleCanvasContextMenu}
|
||||||
|
onPointerDown={this.handleCanvasPointerDown}
|
||||||
|
onDoubleClick={this.handleCanvasDoubleClick}
|
||||||
|
onPointerMove={this.handleCanvasPointerMove}
|
||||||
|
onPointerUp={this.removePointer}
|
||||||
|
onPointerCancel={this.removePointer}
|
||||||
|
onTouchMove={this.handleTouchMove}
|
||||||
|
onDrop={this.handleCanvasOnDrop}
|
||||||
|
>
|
||||||
|
{t("labels.drawingCanvas")}
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
@@ -349,20 +415,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
height: canvasDOMHeight,
|
height: canvasDOMHeight,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
offsetLeft,
|
offsetLeft,
|
||||||
|
viewModeEnabled,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
||||||
const canvasScale = window.devicePixelRatio;
|
|
||||||
|
|
||||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
|
||||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
|
||||||
|
|
||||||
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
|
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
|
||||||
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="excalidraw"
|
className={clsx("excalidraw", {
|
||||||
|
"excalidraw--view-mode": viewModeEnabled,
|
||||||
|
})}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
style={{
|
style={{
|
||||||
width: canvasDOMWidth,
|
width: canvasDOMWidth,
|
||||||
@@ -392,10 +457,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
isCollaborating={this.props.isCollaborating || false}
|
isCollaborating={this.props.isCollaborating || false}
|
||||||
onExportToBackend={onExportToBackend}
|
onExportToBackend={onExportToBackend}
|
||||||
renderCustomFooter={renderFooter}
|
renderCustomFooter={renderFooter}
|
||||||
|
viewModeEnabled={viewModeEnabled}
|
||||||
|
showExitZenModeBtn={
|
||||||
|
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
<div className="excalidraw-textEditorContainer" />
|
||||||
{this.state.showStats && (
|
{this.state.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
|
setAppState={this.setAppState}
|
||||||
elements={this.scene.getElements()}
|
elements={this.scene.getElements()}
|
||||||
onClose={this.toggleStats}
|
onClose={this.toggleStats}
|
||||||
/>
|
/>
|
||||||
@@ -406,28 +477,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
clearToast={this.clearToast}
|
clearToast={this.clearToast}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<main>
|
<main>{this.renderCanvas()}</main>
|
||||||
<canvas
|
|
||||||
id="canvas"
|
|
||||||
style={{
|
|
||||||
width: canvasDOMWidth,
|
|
||||||
height: canvasDOMHeight,
|
|
||||||
}}
|
|
||||||
width={canvasWidth}
|
|
||||||
height={canvasHeight}
|
|
||||||
ref={this.handleCanvasRef}
|
|
||||||
onContextMenu={this.handleCanvasContextMenu}
|
|
||||||
onPointerDown={this.handleCanvasPointerDown}
|
|
||||||
onDoubleClick={this.handleCanvasDoubleClick}
|
|
||||||
onPointerMove={this.handleCanvasPointerMove}
|
|
||||||
onPointerUp={this.removePointer}
|
|
||||||
onPointerCancel={this.removePointer}
|
|
||||||
onTouchMove={this.handleTouchMove}
|
|
||||||
onDrop={this.handleCanvasOnDrop}
|
|
||||||
>
|
|
||||||
{t("labels.drawingCanvas")}
|
|
||||||
</canvas>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -467,6 +517,23 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (actionResult.commitToHistory) {
|
if (actionResult.commitToHistory) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
||||||
|
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
||||||
|
let gridSize = actionResult?.appState?.gridSize || null;
|
||||||
|
|
||||||
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.zenModeEnabled !== "undefined") {
|
||||||
|
zenModeEnabled = this.props.zenModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.gridModeEnabled !== "undefined") {
|
||||||
|
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
...actionResult.appState,
|
...actionResult.appState,
|
||||||
@@ -476,6 +543,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
height: state.height,
|
height: state.height,
|
||||||
offsetTop: state.offsetTop,
|
offsetTop: state.offsetTop,
|
||||||
offsetLeft: state.offsetLeft,
|
offsetLeft: state.offsetLeft,
|
||||||
|
viewModeEnabled,
|
||||||
|
zenModeEnabled,
|
||||||
|
gridSize,
|
||||||
}),
|
}),
|
||||||
() => {
|
() => {
|
||||||
if (actionResult.syncHistory) {
|
if (actionResult.syncHistory) {
|
||||||
@@ -602,19 +672,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
...calculateScrollCenter(
|
|
||||||
scene.elements,
|
|
||||||
{
|
|
||||||
...scene.appState,
|
|
||||||
width: this.state.width,
|
|
||||||
height: this.state.height,
|
|
||||||
offsetTop: this.state.offsetTop,
|
|
||||||
offsetLeft: this.state.offsetLeft,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
|
if (initialData?.scrollToCenter) {
|
||||||
|
scene.appState = {
|
||||||
|
...scene.appState,
|
||||||
|
...calculateScrollCenter(
|
||||||
|
scene.elements,
|
||||||
|
{
|
||||||
|
...scene.appState,
|
||||||
|
width: this.state.width,
|
||||||
|
height: this.state.height,
|
||||||
|
offsetTop: this.state.offsetTop,
|
||||||
|
offsetLeft: this.state.offsetLeft,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
@@ -658,7 +733,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scene.addCallback(this.onSceneUpdated);
|
this.scene.addCallback(this.onSceneUpdated);
|
||||||
|
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
// optim to avoid extra render on init
|
// optim to avoid extra render on init
|
||||||
@@ -725,25 +799,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addEventListeners() {
|
private addEventListeners() {
|
||||||
|
this.removeEventListeners();
|
||||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
document.addEventListener(EVENT.COPY, this.onCopy);
|
||||||
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
|
||||||
document.addEventListener(EVENT.CUT, this.onCut);
|
|
||||||
|
|
||||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||||
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.MOUSE_MOVE,
|
EVENT.MOUSE_MOVE,
|
||||||
this.updateCurrentCursorPosition,
|
this.updateCurrentCursorPosition,
|
||||||
);
|
);
|
||||||
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
|
||||||
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
|
||||||
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
|
||||||
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
|
||||||
|
|
||||||
// rerender text elements on font load to fix #637 && #1553
|
// rerender text elements on font load to fix #637 && #1553
|
||||||
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
||||||
|
|
||||||
// Safari-only desktop pinch zoom
|
// Safari-only desktop pinch zoom
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.GESTURE_START,
|
EVENT.GESTURE_START,
|
||||||
@@ -760,6 +825,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.onGestureEnd as any,
|
this.onGestureEnd as any,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
||||||
|
document.addEventListener(EVENT.CUT, this.onCut);
|
||||||
|
document.addEventListener(EVENT.SCROLL, this.onScroll);
|
||||||
|
|
||||||
|
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
||||||
|
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
||||||
|
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
||||||
|
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
||||||
|
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||||
@@ -782,6 +860,26 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
|
||||||
|
this.setState(
|
||||||
|
{ viewModeEnabled: !!this.props.viewModeEnabled },
|
||||||
|
this.addEventListeners,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
||||||
|
this.addEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
||||||
|
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) {
|
||||||
|
this.setState({
|
||||||
|
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
document
|
document
|
||||||
.querySelector(".excalidraw")
|
.querySelector(".excalidraw")
|
||||||
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
||||||
@@ -821,6 +919,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
|
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
|
||||||
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
|
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
|
||||||
const pointerUsernames: { [id: string]: string } = {};
|
const pointerUsernames: { [id: string]: string } = {};
|
||||||
|
const pointerUserStates: { [id: string]: string } = {};
|
||||||
this.state.collaborators.forEach((user, socketId) => {
|
this.state.collaborators.forEach((user, socketId) => {
|
||||||
if (user.selectedElementIds) {
|
if (user.selectedElementIds) {
|
||||||
for (const id of Object.keys(user.selectedElementIds)) {
|
for (const id of Object.keys(user.selectedElementIds)) {
|
||||||
@@ -836,6 +935,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (user.username) {
|
if (user.username) {
|
||||||
pointerUsernames[socketId] = user.username;
|
pointerUsernames[socketId] = user.username;
|
||||||
}
|
}
|
||||||
|
if (user.userState) {
|
||||||
|
pointerUserStates[socketId] = user.userState;
|
||||||
|
}
|
||||||
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
||||||
{
|
{
|
||||||
sceneX: user.pointer.x,
|
sceneX: user.pointer.x,
|
||||||
@@ -870,10 +972,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
remotePointerButton: cursorButton,
|
remotePointerButton: cursorButton,
|
||||||
remoteSelectedElementIds,
|
remoteSelectedElementIds,
|
||||||
remotePointerUsernames: pointerUsernames,
|
remotePointerUsernames: pointerUsernames,
|
||||||
|
remotePointerUserStates: pointerUserStates,
|
||||||
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
renderOptimizations: true,
|
renderOptimizations: true,
|
||||||
|
renderScrollbars: !isMobile(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
@@ -902,6 +1006,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onScroll = debounce(() => {
|
||||||
|
this.setState({ ...this.getCanvasOffsets() });
|
||||||
|
}, SCROLL_TIMEOUT);
|
||||||
|
|
||||||
// Copy/paste
|
// Copy/paste
|
||||||
|
|
||||||
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||||
@@ -1134,10 +1242,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.actionManager.executeAction(actionToggleZenMode);
|
this.actionManager.executeAction(actionToggleZenMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleGridMode = () => {
|
|
||||||
this.actionManager.executeAction(actionToggleGridMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleStats = () => {
|
toggleStats = () => {
|
||||||
if (!this.state.showStats) {
|
if (!this.state.showStats) {
|
||||||
trackEvent("dialog", "stats");
|
trackEvent("dialog", "stats");
|
||||||
@@ -1232,14 +1336,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event[KEYS.CTRL_OR_CMD]) {
|
|
||||||
this.setState({ isBindingEnabled: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.actionManager.handleKeyDown(event)) {
|
if (this.actionManager.handleKeyDown(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
|
this.setState({ isBindingEnabled: false });
|
||||||
|
}
|
||||||
|
|
||||||
if (event.code === CODES.NINE) {
|
if (event.code === CODES.NINE) {
|
||||||
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
||||||
}
|
}
|
||||||
@@ -1454,6 +1562,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
textWysiwyg({
|
textWysiwyg({
|
||||||
id: element.id,
|
id: element.id,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
|
canvas: this.canvas,
|
||||||
getViewportCoords: (x, y) => {
|
getViewportCoords: (x, y) => {
|
||||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||||
{
|
{
|
||||||
@@ -1762,10 +1871,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPointerOverScrollBars = isOverScrollBars(
|
const isPointerOverScrollBars = isOverScrollBars(
|
||||||
currentScrollBars,
|
currentScrollBars,
|
||||||
event.clientX,
|
event.clientX - this.state.offsetLeft,
|
||||||
event.clientY,
|
event.clientY - this.state.offsetTop,
|
||||||
);
|
);
|
||||||
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
||||||
if (!this.state.draggingElement && !this.state.multiElement) {
|
if (!this.state.draggingElement && !this.state.multiElement) {
|
||||||
@@ -1859,7 +1969,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
points: points.slice(0, -1),
|
points: points.slice(0, -1),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (isPathALoop(points)) {
|
if (isPathALoop(points, this.state.zoom.value)) {
|
||||||
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
||||||
}
|
}
|
||||||
// update last uncommitted point
|
// update last uncommitted point
|
||||||
@@ -2046,14 +2156,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
if (!this.state.viewModeEnabled) {
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||||
pointerDownState.eventListeners.onMove = onPointerMove;
|
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
||||||
pointerDownState.eventListeners.onUp = onPointerUp;
|
pointerDownState.eventListeners.onMove = onPointerMove;
|
||||||
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
pointerDownState.eventListeners.onUp = onPointerUp;
|
||||||
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
||||||
|
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
||||||
@@ -2103,7 +2215,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
!(
|
!(
|
||||||
gesture.pointers.size === 0 &&
|
gesture.pointers.size === 0 &&
|
||||||
(event.button === POINTER_BUTTON.WHEEL ||
|
(event.button === POINTER_BUTTON.WHEEL ||
|
||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
|
this.state.viewModeEnabled)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2218,8 +2331,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
),
|
),
|
||||||
scrollbars: isOverScrollBars(
|
scrollbars: isOverScrollBars(
|
||||||
currentScrollBars,
|
currentScrollBars,
|
||||||
event.clientX,
|
event.clientX - this.state.offsetLeft,
|
||||||
event.clientY,
|
event.clientY - this.state.offsetTop,
|
||||||
),
|
),
|
||||||
// we need to duplicate because we'll be updating this state
|
// we need to duplicate because we'll be updating this state
|
||||||
lastCoords: { ...origin },
|
lastCoords: { ...origin },
|
||||||
@@ -2528,7 +2641,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { multiElement } = this.state;
|
const { multiElement } = this.state;
|
||||||
|
|
||||||
// finalize if completing a loop
|
// finalize if completing a loop
|
||||||
if (multiElement.type === "line" && isPathALoop(multiElement.points)) {
|
if (
|
||||||
|
multiElement.type === "line" &&
|
||||||
|
isPathALoop(multiElement.points, this.state.zoom.value)
|
||||||
|
) {
|
||||||
mutateElement(multiElement, {
|
mutateElement(multiElement, {
|
||||||
lastCommittedPoint:
|
lastCommittedPoint:
|
||||||
multiElement.points[multiElement.points.length - 1],
|
multiElement.points[multiElement.points.length - 1],
|
||||||
@@ -3590,7 +3706,37 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
const element = this.getElementAtPosition(x, y);
|
const element = this.getElementAtPosition(x, y);
|
||||||
|
const options: ContextMenuOption[] = [];
|
||||||
|
if (probablySupportsClipboardBlob && elements.length > 0) {
|
||||||
|
options.push(actionCopyAsPng);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (probablySupportsClipboardWriteText && elements.length > 0) {
|
||||||
|
options.push(actionCopyAsSvg);
|
||||||
|
}
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
const viewModeOptions = [
|
||||||
|
...options,
|
||||||
|
typeof this.props.gridModeEnabled === "undefined" &&
|
||||||
|
actionToggleGridMode,
|
||||||
|
typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode,
|
||||||
|
typeof this.props.viewModeEnabled === "undefined" &&
|
||||||
|
actionToggleViewMode,
|
||||||
|
actionToggleStats,
|
||||||
|
];
|
||||||
|
|
||||||
|
ContextMenu.push({
|
||||||
|
options: viewModeOptions,
|
||||||
|
top: clientY,
|
||||||
|
left: clientX,
|
||||||
|
actionManager: this.actionManager,
|
||||||
|
appState: this.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
_isMobile &&
|
_isMobile &&
|
||||||
@@ -3616,8 +3762,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
separator,
|
separator,
|
||||||
actionSelectAll,
|
actionSelectAll,
|
||||||
separator,
|
separator,
|
||||||
actionToggleGridMode,
|
typeof this.props.gridModeEnabled === "undefined" &&
|
||||||
actionToggleZenMode,
|
actionToggleGridMode,
|
||||||
|
typeof this.props.zenModeEnabled === "undefined" &&
|
||||||
|
actionToggleZenMode,
|
||||||
|
typeof this.props.viewModeEnabled === "undefined" &&
|
||||||
|
actionToggleViewMode,
|
||||||
actionToggleStats,
|
actionToggleStats,
|
||||||
],
|
],
|
||||||
top: clientY,
|
top: clientY,
|
||||||
@@ -3632,6 +3782,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
ContextMenu.push({
|
||||||
|
options: [navigator.clipboard && actionCopy, ...options],
|
||||||
|
top: clientY,
|
||||||
|
left: clientX,
|
||||||
|
actionManager: this.actionManager,
|
||||||
|
appState: this.state,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
_isMobile && actionCut,
|
_isMobile && actionCut,
|
||||||
@@ -3648,8 +3809,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
},
|
},
|
||||||
_isMobile && separator,
|
_isMobile && separator,
|
||||||
probablySupportsClipboardBlob && actionCopyAsPng,
|
...options,
|
||||||
probablySupportsClipboardWriteText && actionCopyAsSvg,
|
|
||||||
separator,
|
separator,
|
||||||
actionCopyStyles,
|
actionCopyStyles,
|
||||||
actionPasteStyles,
|
actionPasteStyles,
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ export const ButtonIconCycle = <T extends any>({
|
|||||||
}) => {
|
}) => {
|
||||||
const current = options.find((op) => op.value === value);
|
const current = options.find((op) => op.value === value);
|
||||||
|
|
||||||
const cycle = () => {
|
function cycle() {
|
||||||
const index = options.indexOf(current!);
|
const index = options.indexOf(current!);
|
||||||
const next = (index + 1) % options.length;
|
const next = (index + 1) % options.length;
|
||||||
onChange(options[next].value);
|
onChange(options[next].value);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label key={group} className={clsx({ active: current!.value !== null })}>
|
<label key={group} className={clsx({ active: current!.value !== null })}>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
.CollabButton.is-collaborating {
|
.CollabButton.is-collaborating {
|
||||||
background-color: var(--button-special-active-bg-color);
|
background-color: var(--button-special-active-bg-color);
|
||||||
|
|
||||||
.ToolIcon__icon svg {
|
.ToolIcon__icon svg,
|
||||||
|
.ToolIcon__label {
|
||||||
color: var(--icon-green-fill-color);
|
color: var(--icon-green-fill-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const CollabButton = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
icon={users}
|
icon={users}
|
||||||
type="button"
|
type="button"
|
||||||
title={t("buttons.roomDialog")}
|
title={t("labels.liveCollaboration")}
|
||||||
aria-label={t("buttons.roomDialog")}
|
aria-label={t("labels.liveCollaboration")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
>
|
>
|
||||||
{collaboratorCount > 0 && (
|
{collaboratorCount > 0 && (
|
||||||
|
|||||||
@@ -164,7 +164,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: var(--input-bg-color);
|
background-color: var(--input-bg-color);
|
||||||
color: var(--text-color-primary);
|
color: var(--text-primary-color);
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 1.75em;
|
height: 1.75em;
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||||
color: #fff;
|
color: $oc-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||||
@@ -241,10 +241,10 @@
|
|||||||
|
|
||||||
&.Appearance_dark {
|
&.Appearance_dark {
|
||||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||||
color: #000;
|
color: $oc-black;
|
||||||
}
|
}
|
||||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||||
color: #000;
|
color: $oc-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Action } from "../actions/types";
|
|||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
|
||||||
type ContextMenuOption = "separator" | Action;
|
export type ContextMenuOption = "separator" | Action;
|
||||||
|
|
||||||
type ContextMenuProps = {
|
type ContextMenuProps = {
|
||||||
options: ContextMenuOption[];
|
options: ContextMenuOption[];
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ export type Appearence = "light" | "dark";
|
|||||||
export const DarkModeToggle = (props: {
|
export const DarkModeToggle = (props: {
|
||||||
value: Appearence;
|
value: Appearence;
|
||||||
onChange: (value: Appearence) => void;
|
onChange: (value: Appearence) => void;
|
||||||
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const title = props.title
|
||||||
|
? props.title
|
||||||
|
: props.value === "dark"
|
||||||
|
? t("buttons.lightMode")
|
||||||
|
: t("buttons.darkMode");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
|
className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
|
||||||
title={
|
title={title}
|
||||||
props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||||
@@ -25,11 +30,7 @@ export const DarkModeToggle = (props: {
|
|||||||
props.onChange(event.target.checked ? "dark" : "light")
|
props.onChange(event.target.checked ? "dark" : "light")
|
||||||
}
|
}
|
||||||
checked={props.value === "dark"}
|
checked={props.value === "dark"}
|
||||||
aria-label={
|
aria-label={title}
|
||||||
props.value === "dark"
|
|
||||||
? t("buttons.lightMode")
|
|
||||||
: t("buttons.darkMode")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="ToolIcon__icon">
|
<div className="ToolIcon__icon">
|
||||||
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import { ToolButton } from "./ToolButton";
|
|||||||
const scales = [1, 2, 3];
|
const scales = [1, 2, 3];
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||||
|
|
||||||
|
const supportsContextFilters =
|
||||||
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
|
|
||||||
export const ErrorCanvasPreview = () => {
|
export const ErrorCanvasPreview = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -128,6 +131,8 @@ const ExportModal = ({
|
|||||||
return (
|
return (
|
||||||
<div className="ExportDialog">
|
<div className="ExportDialog">
|
||||||
<div className="ExportDialog__preview" ref={previewRef} />
|
<div className="ExportDialog__preview" ref={previewRef} />
|
||||||
|
{supportsContextFilters &&
|
||||||
|
actionManager.renderAction("exportWithDarkMode")}
|
||||||
<Stack.Col gap={2} align="center">
|
<Stack.Col gap={2} align="center">
|
||||||
<div className="ExportDialog__actions">
|
<div className="ExportDialog__actions">
|
||||||
<Stack.Row gap={2}>
|
<Stack.Row gap={2}>
|
||||||
|
|||||||
@@ -224,9 +224,13 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.gridMode")}
|
label={t("labels.showGrid")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.viewMode")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+R")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
<Column>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.picker-type-elementBackground .picker-keybinding {
|
.picker-type-elementBackground .picker-keybinding {
|
||||||
color: #fff;
|
color: $oc-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||||
@@ -134,10 +134,10 @@
|
|||||||
|
|
||||||
&.Appearance_dark {
|
&.Appearance_dark {
|
||||||
.picker-type-elementBackground .picker-keybinding {
|
.picker-type-elementBackground .picker-keybinding {
|
||||||
color: #000;
|
color: $oc-black;
|
||||||
}
|
}
|
||||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||||
color: #000;
|
color: $oc-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import {
|
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
||||||
defaultLang,
|
|
||||||
Language,
|
|
||||||
languages,
|
|
||||||
setLanguageFirstTime,
|
|
||||||
} from "../i18n";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
@@ -23,7 +18,7 @@ export class InitializeApp extends React.Component<Props, State> {
|
|||||||
const currentLang =
|
const currentLang =
|
||||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||||
defaultLang;
|
defaultLang;
|
||||||
await setLanguageFirstTime(currentLang);
|
await setLanguage(currentLang);
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin-left: auto;
|
margin-inline-start: auto;
|
||||||
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
|
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
|
||||||
padding-right: 18px;
|
padding-inline-end: 18px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__wrapper {
|
.layer-ui__wrapper {
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
.encrypted-icon {
|
.encrypted-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-inline-start: 15px;
|
margin-inline-start: 15px;
|
||||||
|
|||||||
+136
-70
@@ -11,7 +11,7 @@ import { CLASSES } from "../constants";
|
|||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||||
import { Library } from "../data/library";
|
import { Library } from "../data/library";
|
||||||
import { showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
@@ -27,7 +27,7 @@ import { ExportCB, ExportDialog } from "./ExportDialog";
|
|||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { GitHubCorner } from "./GitHubCorner";
|
import { GitHubCorner } from "./GitHubCorner";
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { exportFile, load, shield } from "./icons";
|
import { exportFile, load, shield, trash } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
@@ -52,6 +52,7 @@ interface LayerUIProps {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
zenModeEnabled: boolean;
|
zenModeEnabled: boolean;
|
||||||
|
showExitZenModeBtn: boolean;
|
||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
@@ -61,6 +62,7 @@ interface LayerUIProps {
|
|||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
) => void;
|
) => void;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||||
|
viewModeEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@@ -98,6 +100,7 @@ const LibraryMenuItems = ({
|
|||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
setAppState,
|
setAppState,
|
||||||
|
setLibraryItems,
|
||||||
}: {
|
}: {
|
||||||
library: LibraryItems;
|
library: LibraryItems;
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
@@ -105,6 +108,7 @@ const LibraryMenuItems = ({
|
|||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: (elements: LibraryItem) => void;
|
onAddToLibrary: (elements: LibraryItem) => void;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
setLibraryItems: (library: LibraryItems) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
||||||
@@ -148,6 +152,19 @@ const LibraryMenuItems = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ToolButton
|
||||||
|
key="reset"
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.resetLibrary")}
|
||||||
|
aria-label={t("buttons.resetLibrary")}
|
||||||
|
icon={trash}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||||
|
Library.resetLibrary();
|
||||||
|
setLibraryItems([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<a href="https://libraries.excalidraw.com" target="_excalidraw_libraries">
|
<a href="https://libraries.excalidraw.com" target="_excalidraw_libraries">
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
@@ -279,6 +296,7 @@ const LibraryMenu = ({
|
|||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
|
setLibraryItems={setLibraryItems}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@@ -295,10 +313,12 @@ const LayerUI = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onInsertElements,
|
onInsertElements,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
showExitZenModeBtn,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
|
viewModeEnabled,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@@ -358,6 +378,28 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderViewModeCanvasActions = () => {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
heading="canvasActions"
|
||||||
|
className={clsx("zen-mode-transition", {
|
||||||
|
"transition-left": zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
|
<Island padding={2} style={{ zIndex: 1 }}>
|
||||||
|
<Stack.Col gap={4}>
|
||||||
|
<Stack.Row gap={1} justifyContent="space-between">
|
||||||
|
{actionManager.renderAction("saveScene")}
|
||||||
|
{actionManager.renderAction("saveAsScene")}
|
||||||
|
{renderExportDialog()}
|
||||||
|
</Stack.Row>
|
||||||
|
</Stack.Col>
|
||||||
|
</Island>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
const renderCanvasActions = () => (
|
const renderCanvasActions = () => (
|
||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
@@ -400,7 +442,15 @@ const LayerUI = ({
|
|||||||
"transition-left": zenModeEnabled,
|
"transition-left": zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={2}>
|
<Island
|
||||||
|
className={CLASSES.SHAPE_ACTIONS_MENU}
|
||||||
|
padding={2}
|
||||||
|
style={{
|
||||||
|
// we want to make sure this doesn't overflow so substracting 200
|
||||||
|
// which is approximately height of zoom footer and top left menu items with some buffer
|
||||||
|
maxHeight: `${appState.height - 200}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
@@ -448,54 +498,59 @@ const LayerUI = ({
|
|||||||
gap={4}
|
gap={4}
|
||||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
||||||
>
|
>
|
||||||
{renderCanvasActions()}
|
{viewModeEnabled
|
||||||
|
? renderViewModeCanvasActions()
|
||||||
|
: renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
<Section heading="shapes">
|
{!viewModeEnabled && (
|
||||||
{(heading) => (
|
<Section heading="shapes">
|
||||||
<Stack.Col gap={4} align="start">
|
{(heading) => (
|
||||||
<Stack.Row gap={1}>
|
<Stack.Col gap={4} align="start">
|
||||||
<Island
|
<Stack.Row gap={1}>
|
||||||
padding={1}
|
<Island
|
||||||
className={clsx({ "zen-mode": zenModeEnabled })}
|
padding={1}
|
||||||
>
|
className={clsx({ "zen-mode": zenModeEnabled })}
|
||||||
<HintViewer appState={appState} elements={elements} />
|
>
|
||||||
{heading}
|
<HintViewer appState={appState} elements={elements} />
|
||||||
<Stack.Row gap={1}>
|
{heading}
|
||||||
<ShapesSwitcher
|
<Stack.Row gap={1}>
|
||||||
elementType={appState.elementType}
|
<ShapesSwitcher
|
||||||
setAppState={setAppState}
|
elementType={appState.elementType}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
setAppState={setAppState}
|
||||||
/>
|
isLibraryOpen={appState.isLibraryOpen}
|
||||||
</Stack.Row>
|
/>
|
||||||
</Island>
|
</Stack.Row>
|
||||||
<LockIcon
|
</Island>
|
||||||
zenModeEnabled={zenModeEnabled}
|
<LockIcon
|
||||||
checked={appState.elementLocked}
|
zenModeEnabled={zenModeEnabled}
|
||||||
onChange={onLockToggle}
|
checked={appState.elementLocked}
|
||||||
title={t("toolBar.lock")}
|
onChange={onLockToggle}
|
||||||
/>
|
title={t("toolBar.lock")}
|
||||||
</Stack.Row>
|
/>
|
||||||
{libraryMenu}
|
</Stack.Row>
|
||||||
</Stack.Col>
|
{libraryMenu}
|
||||||
)}
|
</Stack.Col>
|
||||||
</Section>
|
)}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
<UserList
|
<UserList
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-right": zenModeEnabled,
|
"transition-right": zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{Array.from(appState.collaborators)
|
{appState.collaborators.size > 0 &&
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
Array.from(appState.collaborators)
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
// Collaborator is either not initialized or is actually the current user.
|
||||||
.map(([clientId, client]) => (
|
.filter(([_, client]) => Object.keys(client).length !== 0)
|
||||||
<Tooltip
|
.map(([clientId, client]) => (
|
||||||
label={client.username || "Unknown user"}
|
<Tooltip
|
||||||
key={clientId}
|
label={client.username || "Unknown user"}
|
||||||
>
|
key={clientId}
|
||||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
>
|
||||||
</Tooltip>
|
{actionManager.renderAction("goToCollaborator", clientId)}
|
||||||
))}
|
</Tooltip>
|
||||||
|
))}
|
||||||
</UserList>
|
</UserList>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
@@ -524,6 +579,20 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderGitHubCorner = () => {
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__github-corner zen-mode-transition",
|
||||||
|
{
|
||||||
|
"transition-right": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<GitHubCorner appearance={appState.appearance} />
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
const renderFooter = () => (
|
const renderFooter = () => (
|
||||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
||||||
<div
|
<div
|
||||||
@@ -536,24 +605,12 @@ const LayerUI = ({
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={clsx("disable-zen-mode", {
|
className={clsx("disable-zen-mode", {
|
||||||
"disable-zen-mode--visible": zenModeEnabled,
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
})}
|
})}
|
||||||
onClick={toggleZenMode}
|
onClick={toggleZenMode}
|
||||||
>
|
>
|
||||||
{t("buttons.exitZenMode")}
|
{t("buttons.exitZenMode")}
|
||||||
</button>
|
</button>
|
||||||
{appState.scrolledOutside && (
|
|
||||||
<button
|
|
||||||
className="scroll-back-to-content"
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({
|
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("buttons.scrollBackToContent")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -599,26 +656,35 @@ const LayerUI = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
renderCustomFooter={renderCustomFooter}
|
renderCustomFooter={renderCustomFooter}
|
||||||
|
viewModeEnabled={viewModeEnabled}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="layer-ui__wrapper">
|
<div
|
||||||
|
className={clsx("layer-ui__wrapper", {
|
||||||
|
"disable-pointerEvents":
|
||||||
|
appState.draggingElement ||
|
||||||
|
appState.resizingElement ||
|
||||||
|
(appState.editingElement && !isTextElement(appState.editingElement)),
|
||||||
|
})}
|
||||||
|
>
|
||||||
{dialogs}
|
{dialogs}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
{renderBottomAppMenu()}
|
{renderBottomAppMenu()}
|
||||||
{
|
{renderGitHubCorner()}
|
||||||
<aside
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<GitHubCorner appearance={appState.appearance} />
|
|
||||||
</aside>
|
|
||||||
}
|
|
||||||
{renderFooter()}
|
{renderFooter()}
|
||||||
|
{appState.scrolledOutside && (
|
||||||
|
<button
|
||||||
|
className="scroll-back-to-content"
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+162
-116
@@ -29,6 +29,7 @@ type MobileMenuProps = {
|
|||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||||
|
viewModeEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -43,121 +44,166 @@ export const MobileMenu = ({
|
|||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
}: MobileMenuProps) => (
|
viewModeEnabled,
|
||||||
<>
|
}: MobileMenuProps) => {
|
||||||
<FixedSideContainer side="top">
|
const renderToolbar = () => {
|
||||||
<Section heading="shapes">
|
return (
|
||||||
{(heading) => (
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
<Stack.Col gap={4} align="center">
|
<Section heading="shapes">
|
||||||
<Stack.Row gap={1}>
|
{(heading) => (
|
||||||
<Island padding={1}>
|
<Stack.Col gap={4} align="center">
|
||||||
{heading}
|
<Stack.Row gap={1}>
|
||||||
<Stack.Row gap={1}>
|
<Island padding={1}>
|
||||||
<ShapesSwitcher
|
{heading}
|
||||||
elementType={appState.elementType}
|
<Stack.Row gap={1}>
|
||||||
setAppState={setAppState}
|
<ShapesSwitcher
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
elementType={appState.elementType}
|
||||||
/>
|
setAppState={setAppState}
|
||||||
</Stack.Row>
|
isLibraryOpen={appState.isLibraryOpen}
|
||||||
</Island>
|
/>
|
||||||
<LockIcon
|
</Stack.Row>
|
||||||
checked={appState.elementLocked}
|
</Island>
|
||||||
onChange={onLockToggle}
|
<LockIcon
|
||||||
title={t("toolBar.lock")}
|
checked={appState.elementLocked}
|
||||||
/>
|
onChange={onLockToggle}
|
||||||
</Stack.Row>
|
title={t("toolBar.lock")}
|
||||||
{libraryMenu}
|
|
||||||
</Stack.Col>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
<HintViewer appState={appState} elements={elements} />
|
|
||||||
</FixedSideContainer>
|
|
||||||
<div
|
|
||||||
className="App-bottom-bar"
|
|
||||||
style={{
|
|
||||||
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Island padding={0}>
|
|
||||||
{appState.openMenu === "canvas" ? (
|
|
||||||
<Section className="App-mobile-menu" heading="canvasActions">
|
|
||||||
<div className="panelColumn">
|
|
||||||
<Stack.Col gap={4}>
|
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{actionManager.renderAction("saveAsScene")}
|
|
||||||
{exportButton}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
|
||||||
{onCollabButtonClick && (
|
|
||||||
<CollabButton
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
collaboratorCount={appState.collaborators.size}
|
|
||||||
onClick={onCollabButtonClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BackgroundPickerAndDarkModeToggle
|
|
||||||
actionManager={actionManager}
|
|
||||||
appState={appState}
|
|
||||||
setAppState={setAppState}
|
|
||||||
/>
|
/>
|
||||||
{renderCustomFooter?.(true)}
|
</Stack.Row>
|
||||||
<fieldset>
|
{libraryMenu}
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
</Stack.Col>
|
||||||
<UserList mobile>
|
|
||||||
{Array.from(appState.collaborators)
|
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
|
||||||
.map(([clientId, client]) => (
|
|
||||||
<React.Fragment key={clientId}>
|
|
||||||
{actionManager.renderAction(
|
|
||||||
"goToCollaborator",
|
|
||||||
clientId,
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</UserList>
|
|
||||||
</fieldset>
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
) : appState.openMenu === "shape" &&
|
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
|
||||||
<SelectedShapeActions
|
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
renderAction={actionManager.renderAction}
|
|
||||||
elementType={appState.elementType}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
) : null}
|
|
||||||
<footer className="App-toolbar">
|
|
||||||
<div className="App-toolbar-content">
|
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
|
||||||
{actionManager.renderAction("undo")}
|
|
||||||
{actionManager.renderAction("redo")}
|
|
||||||
{actionManager.renderAction(
|
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
|
||||||
)}
|
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
|
||||||
</div>
|
|
||||||
{appState.scrolledOutside && !appState.openMenu && (
|
|
||||||
<button
|
|
||||||
className="scroll-back-to-content"
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({
|
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("buttons.scrollBackToContent")}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</footer>
|
</Section>
|
||||||
</Island>
|
<HintViewer appState={appState} elements={elements} />
|
||||||
</div>
|
</FixedSideContainer>
|
||||||
</>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const renderAppToolbar = () => {
|
||||||
|
if (viewModeEnabled) {
|
||||||
|
return (
|
||||||
|
<div className="App-toolbar-content">
|
||||||
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="App-toolbar-content">
|
||||||
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
|
{actionManager.renderAction("undo")}
|
||||||
|
{actionManager.renderAction("redo")}
|
||||||
|
{actionManager.renderAction(
|
||||||
|
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||||
|
)}
|
||||||
|
{actionManager.renderAction("deleteSelectedElements")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCanvasActions = () => {
|
||||||
|
if (viewModeEnabled) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{actionManager.renderAction("saveScene")}
|
||||||
|
{actionManager.renderAction("saveAsScene")}
|
||||||
|
{exportButton}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{actionManager.renderAction("saveScene")}
|
||||||
|
{actionManager.renderAction("saveAsScene")}
|
||||||
|
{exportButton}
|
||||||
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
{onCollabButtonClick && (
|
||||||
|
<CollabButton
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
collaboratorCount={appState.collaborators.size}
|
||||||
|
onClick={onCollabButtonClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
<BackgroundPickerAndDarkModeToggle
|
||||||
|
actionManager={actionManager}
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!viewModeEnabled && renderToolbar()}
|
||||||
|
<div
|
||||||
|
className="App-bottom-bar"
|
||||||
|
style={{
|
||||||
|
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Island padding={0}>
|
||||||
|
{appState.openMenu === "canvas" ? (
|
||||||
|
<Section className="App-mobile-menu" heading="canvasActions">
|
||||||
|
<div className="panelColumn">
|
||||||
|
<Stack.Col gap={4}>
|
||||||
|
{renderCanvasActions()}
|
||||||
|
{renderCustomFooter?.(true)}
|
||||||
|
{appState.collaborators.size > 0 && (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
|
<UserList mobile>
|
||||||
|
{Array.from(appState.collaborators)
|
||||||
|
// Collaborator is either not initialized or is actually the current user.
|
||||||
|
.filter(
|
||||||
|
([_, client]) => Object.keys(client).length !== 0,
|
||||||
|
)
|
||||||
|
.map(([clientId, client]) => (
|
||||||
|
<React.Fragment key={clientId}>
|
||||||
|
{actionManager.renderAction(
|
||||||
|
"goToCollaborator",
|
||||||
|
clientId,
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</UserList>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
) : appState.openMenu === "shape" &&
|
||||||
|
!viewModeEnabled &&
|
||||||
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
|
<SelectedShapeActions
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
elementType={appState.elementType}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
<footer className="App-toolbar">
|
||||||
|
{renderAppToolbar()}
|
||||||
|
{appState.scrolledOutside && !appState.openMenu && (
|
||||||
|
<button
|
||||||
|
className="scroll-back-to-content"
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</Island>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
&.excalidraw-modal-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.Modal {
|
.Modal {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -15,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Modal__background {
|
.Modal__background {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -42,7 +47,7 @@
|
|||||||
background: var(--island-bg-color);
|
background: var(--island-bg-color);
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
|
|
||||||
border: 1px solid var(--dialog-border);
|
border: 1px solid var(--dialog-border-color);
|
||||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
@@ -82,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Modal__content {
|
.Modal__content {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const useBodyRoot = () => {
|
|||||||
?.classList.contains("Appearance_dark");
|
?.classList.contains("Appearance_dark");
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
div.classList.add("excalidraw");
|
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||||
|
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
div.classList.add("Appearance_dark");
|
div.classList.add("Appearance_dark");
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.popover {
|
.popover {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover .cover {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Stats {
|
.Stats {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 64px;
|
top: 64px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
import { DEFAULT_VERSION } from "../constants";
|
||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +11,7 @@ import { t } from "../i18n";
|
|||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { debounce, nFormatter } from "../utils";
|
import { debounce, getVersion, nFormatter } from "../utils";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./Stats.scss";
|
import "./Stats.scss";
|
||||||
@@ -25,6 +27,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
|||||||
|
|
||||||
export const Stats = (props: {
|
export const Stats = (props: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -50,6 +53,17 @@ export const Stats = (props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const version = getVersion();
|
||||||
|
let hash;
|
||||||
|
let timestamp;
|
||||||
|
|
||||||
|
if (version !== DEFAULT_VERSION) {
|
||||||
|
timestamp = version.slice(0, 16).replace("T", " ");
|
||||||
|
hash = version.slice(21);
|
||||||
|
} else {
|
||||||
|
timestamp = t("stats.versionNotAvailable");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
@@ -156,6 +170,28 @@ export const Stats = (props: {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{t("stats.version")}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={2}
|
||||||
|
style={{ textAlign: "center", cursor: "pointer" }}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await copyTextToSystemClipboard(getVersion());
|
||||||
|
props.setAppState({
|
||||||
|
toastMessage: t("toast.copyToClipboard"),
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}}
|
||||||
|
title={t("stats.versionCopy")}
|
||||||
|
>
|
||||||
|
{timestamp}
|
||||||
|
<br />
|
||||||
|
{hash}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.TextInput {
|
.TextInput {
|
||||||
color: var(--text-color-primary);
|
color: var(--text-primary-color);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1.5px solid var(--button-gray-1);
|
border: 1.5px solid var(--button-gray-1);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -150px;
|
margin-left: -150px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
|
|||||||
@@ -48,15 +48,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the following 3 rules ensure that the tooltip doesn't show (nor affect
|
.Tooltip:hover .Tooltip__label {
|
||||||
// the cursor) when you drag over when you draw on canvas, but at the same
|
|
||||||
// time it still works when clicking on the link/shield
|
|
||||||
|
|
||||||
body:active & .Tooltip:not(:hover) {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(:active) & .Tooltip:hover .Tooltip__label {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-24
@@ -1,51 +1,54 @@
|
|||||||
import { FontFamily } from "./element/types";
|
import { FontFamily } from "./element/types";
|
||||||
|
import cssVariables from "./css/variables.module.scss";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
export const DRAGGING_THRESHOLD = 10;
|
export const DRAGGING_THRESHOLD = 10; // px
|
||||||
export const LINE_CONFIRM_THRESHOLD = 10;
|
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
||||||
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
||||||
export const ELEMENT_TRANSLATE_AMOUNT = 1;
|
export const ELEMENT_TRANSLATE_AMOUNT = 1;
|
||||||
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
|
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
|
||||||
export const SHIFT_LOCKING_ANGLE = Math.PI / 12;
|
export const SHIFT_LOCKING_ANGLE = Math.PI / 12;
|
||||||
export const CURSOR_TYPE = {
|
export const CURSOR_TYPE = {
|
||||||
AUTO: "",
|
TEXT: "text",
|
||||||
CROSSHAIR: "crosshair",
|
CROSSHAIR: "crosshair",
|
||||||
GRABBING: "grabbing",
|
GRABBING: "grabbing",
|
||||||
MOVE: "move",
|
|
||||||
POINTER: "pointer",
|
POINTER: "pointer",
|
||||||
TEXT: "text",
|
MOVE: "move",
|
||||||
|
AUTO: "",
|
||||||
};
|
};
|
||||||
export const POINTER_BUTTON = {
|
export const POINTER_BUTTON = {
|
||||||
MAIN: 0,
|
MAIN: 0,
|
||||||
|
WHEEL: 1,
|
||||||
SECONDARY: 2,
|
SECONDARY: 2,
|
||||||
TOUCH: -1,
|
TOUCH: -1,
|
||||||
WHEEL: 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum EVENT {
|
export enum EVENT {
|
||||||
BEFORE_UNLOAD = "beforeunload",
|
|
||||||
BLUR = "blur",
|
|
||||||
COPY = "copy",
|
COPY = "copy",
|
||||||
|
PASTE = "paste",
|
||||||
CUT = "cut",
|
CUT = "cut",
|
||||||
DRAG_OVER = "dragover",
|
|
||||||
DROP = "drop",
|
|
||||||
GESTURE_CHANGE = "gesturechange",
|
|
||||||
GESTURE_END = "gestureend",
|
|
||||||
GESTURE_START = "gesturestart",
|
|
||||||
HASHCHANGE = "hashchange",
|
|
||||||
KEYDOWN = "keydown",
|
KEYDOWN = "keydown",
|
||||||
KEYUP = "keyup",
|
KEYUP = "keyup",
|
||||||
MOUSE_MOVE = "mousemove",
|
MOUSE_MOVE = "mousemove",
|
||||||
PASTE = "paste",
|
RESIZE = "resize",
|
||||||
|
UNLOAD = "unload",
|
||||||
|
BLUR = "blur",
|
||||||
|
DRAG_OVER = "dragover",
|
||||||
|
DROP = "drop",
|
||||||
|
GESTURE_END = "gestureend",
|
||||||
|
BEFORE_UNLOAD = "beforeunload",
|
||||||
|
GESTURE_START = "gesturestart",
|
||||||
|
GESTURE_CHANGE = "gesturechange",
|
||||||
POINTER_MOVE = "pointermove",
|
POINTER_MOVE = "pointermove",
|
||||||
POINTER_UP = "pointerup",
|
POINTER_UP = "pointerup",
|
||||||
RESIZE = "resize",
|
|
||||||
STATE_CHANGE = "statechange",
|
STATE_CHANGE = "statechange",
|
||||||
TOUCH_END = "touchend",
|
|
||||||
TOUCH_START = "touchstart",
|
|
||||||
UNLOAD = "unload",
|
|
||||||
WHEEL = "wheel",
|
WHEEL = "wheel",
|
||||||
|
TOUCH_START = "touchstart",
|
||||||
|
TOUCH_END = "touchend",
|
||||||
|
HASHCHANGE = "hashchange",
|
||||||
|
VISIBILITY_CHANGE = "visibilitychange",
|
||||||
|
SCROLL = "scroll",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ENV = {
|
export const ENV = {
|
||||||
@@ -66,11 +69,11 @@ export const FONT_FAMILY = {
|
|||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamily = 1;
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
|
export const DEFAULT_FONT_FAMILY: FontFamily = 1;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
|
export const DEFAULT_VERSION = "{version}";
|
||||||
|
|
||||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||||
|
|
||||||
@@ -85,11 +88,25 @@ export const STORAGE_KEYS = {
|
|||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Time in milliseconds
|
// time in milliseconds
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
export const TITLE_TIMEOUT = 10000;
|
export const TITLE_TIMEOUT = 10000;
|
||||||
export const TOAST_TIMEOUT = 5000;
|
export const TOAST_TIMEOUT = 5000;
|
||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const VERSION_TIMEOUT = 30000;
|
||||||
export const VERSION_TIMEOUT = 15000;
|
export const SCROLL_TIMEOUT = 500;
|
||||||
|
|
||||||
export const ZOOM_STEP = 0.1;
|
export const ZOOM_STEP = 0.1;
|
||||||
|
|
||||||
|
// Report a user inactive after IDLE_THRESHOLD milliseconds
|
||||||
|
export const IDLE_THRESHOLD = 60_000;
|
||||||
|
// Report a user active each ACTIVE_THRESHOLD milliseconds
|
||||||
|
export const ACTIVE_THRESHOLD = 3_000;
|
||||||
|
|
||||||
|
export const MODES = {
|
||||||
|
VIEW: "viewMode",
|
||||||
|
ZEN: "zenMode",
|
||||||
|
GRID: "gridMode",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const APPEARANCE_FILTER = cssVariables.appearanceFilter;
|
||||||
|
|||||||
+28
-8
@@ -1,10 +1,15 @@
|
|||||||
@import "./variables.module";
|
@import "./variables.module";
|
||||||
@import "./theme";
|
@import "./theme";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--zIndex-canvas: 1;
|
||||||
|
--zIndex-wysiwyg: 2;
|
||||||
|
--zIndex-layerUI: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
color: var(--text-color-primary);
|
color: var(--text-primary-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -30,6 +35,8 @@
|
|||||||
image-rendering: pixelated; // chromium
|
image-rendering: pixelated; // chromium
|
||||||
// NOTE: must be declared *after* the above
|
// NOTE: must be declared *after* the above
|
||||||
image-rendering: -moz-crisp-edges; // FF
|
image-rendering: -moz-crisp-edges; // FF
|
||||||
|
|
||||||
|
z-index: var(--zIndex-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.Appearance_dark {
|
&.Appearance_dark {
|
||||||
@@ -64,7 +71,7 @@
|
|||||||
margin-top: 0.333rem;
|
margin-top: 0.333rem;
|
||||||
margin-bottom: 0.333rem;
|
margin-bottom: 0.333rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-color-primary);
|
color: var(--text-primary-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -216,6 +223,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.App-top-bar {
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.App-bottom-bar {
|
.App-bottom-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -223,7 +237,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
--bar-padding: calc(4 * var(--space-factor));
|
--bar-padding: calc(4 * var(--space-factor));
|
||||||
padding-top: #{"max(var(--bar-padding), var(--sat, 0))"};
|
padding-top: #{"max(var(--bar-padding), var(--sat,0))"};
|
||||||
padding-right: var(--sar, 0);
|
padding-right: var(--sar, 0);
|
||||||
padding-bottom: var(--sab, 0);
|
padding-bottom: var(--sab, 0);
|
||||||
padding-left: var(--sal, 0);
|
padding-left: var(--sal, 0);
|
||||||
@@ -282,7 +296,7 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-menu_top > * {
|
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +337,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-menu_bottom > * {
|
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +361,6 @@
|
|||||||
|
|
||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(100vh - 236px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
@@ -419,7 +432,7 @@
|
|||||||
|
|
||||||
.scroll-back-to-content {
|
.scroll-back-to-content {
|
||||||
color: var(--popup-text-color);
|
color: var(--popup-text-color);
|
||||||
position: fixed;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -492,6 +505,13 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.excalidraw--view-mode {
|
||||||
|
.App-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.App-bottom-bar,
|
.App-bottom-bar,
|
||||||
.FixedSideContainer,
|
.FixedSideContainer,
|
||||||
|
|||||||
+9
-6
@@ -1,4 +1,5 @@
|
|||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
|
@import "./variables.module.scss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--appearance-filter: none;
|
--appearance-filter: none;
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
--button-gray-2: #{$oc-gray-4};
|
--button-gray-2: #{$oc-gray-4};
|
||||||
--button-gray-3: #{$oc-gray-5};
|
--button-gray-3: #{$oc-gray-5};
|
||||||
--button-special-active-bg-color: #{$oc-green-0};
|
--button-special-active-bg-color: #{$oc-green-0};
|
||||||
--dialog-border: #{$oc-gray-6};
|
--dialog-border-color: #{$oc-gray-6};
|
||||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||||
--focus-highlight-color: #{$oc-blue-2};
|
--focus-highlight-color: #{$oc-blue-2};
|
||||||
--icon-fill-color: #{$oc-black};
|
--icon-fill-color: #{$oc-black};
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
--input-border-color: #{$oc-gray-3};
|
--input-border-color: #{$oc-gray-3};
|
||||||
--input-hover-bg-color: #{$oc-gray-1};
|
--input-hover-bg-color: #{$oc-gray-1};
|
||||||
--input-label-color: #{$oc-gray-7};
|
--input-label-color: #{$oc-gray-7};
|
||||||
--island-bg-color: #{transparentize($oc-white, 0.12)};
|
--island-bg-color: rgba(255, 255, 255, 0.9);
|
||||||
--keybinding-color: #{$oc-gray-5};
|
--keybinding-color: #{$oc-gray-5};
|
||||||
--link-color: #{$oc-blue-7};
|
--link-color: #{$oc-blue-7};
|
||||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
--select-highlight-color: #{$oc-blue-5};
|
--select-highlight-color: #{$oc-blue-5};
|
||||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
|
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
|
||||||
--space-factor: 0.25rem;
|
--space-factor: 0.25rem;
|
||||||
--text-color-primary: #{$oc-gray-8};
|
--text-primary-color: #{$oc-gray-8};
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
@@ -45,14 +46,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.Appearance_dark {
|
&.Appearance_dark {
|
||||||
--appearance-filter: invert(93%) hue-rotate(180deg);
|
--appearance-filter: #{$appearance-filter};
|
||||||
--button-destructive-bg-color: #5a0000;
|
--button-destructive-bg-color: #5a0000;
|
||||||
--button-destructive-color: #{$oc-red-3};
|
--button-destructive-color: #{$oc-red-3};
|
||||||
--button-gray-1: #363636;
|
--button-gray-1: #363636;
|
||||||
--button-gray-2: #272727;
|
--button-gray-2: #272727;
|
||||||
--button-gray-3: #222;
|
--button-gray-3: #222;
|
||||||
--button-special-active-bg-color: #204624;
|
--button-special-active-bg-color: #204624;
|
||||||
--dialog-border: #{$oc-gray-9};
|
--dialog-border-color: #{$oc-gray-9};
|
||||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||||
--focus-highlight-color: #{$oc-blue-6};
|
--focus-highlight-color: #{$oc-blue-6};
|
||||||
--icon-fill-color: #{$oc-gray-4};
|
--icon-fill-color: #{$oc-gray-4};
|
||||||
@@ -63,11 +64,13 @@
|
|||||||
--input-label-color: #{$oc-gray-2};
|
--input-label-color: #{$oc-gray-2};
|
||||||
--island-bg-color: #1e1e1e;
|
--island-bg-color: #1e1e1e;
|
||||||
--keybinding-color: #{$oc-gray-6};
|
--keybinding-color: #{$oc-gray-6};
|
||||||
--overlay-bg-color: rgba(30, 30, 30, 0.88);
|
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
||||||
|
--popup-bg-color: #2c2c2c;
|
||||||
--popup-secondary-bg-color: #222;
|
--popup-secondary-bg-color: #222;
|
||||||
--popup-text-color: #{$oc-gray-4};
|
--popup-text-color: #{$oc-gray-4};
|
||||||
--popup-text-inverted-color: #2c2c2c;
|
--popup-text-inverted-color: #2c2c2c;
|
||||||
--select-highlight-color: #{$oc-blue-4};
|
--select-highlight-color: #{$oc-blue-4};
|
||||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
|
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
|
||||||
|
--text-primary-color: #{$oc-gray-4};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
|
|
||||||
// Keep up to date with is-mobile.tsx
|
// keep up to date with is-mobile.tsx
|
||||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||||
|
$appearance-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
isMobileQuery: unquote($is-mobile-query);
|
isMobileQuery: unquote($is-mobile-query);
|
||||||
|
appearanceFilter: unquote($appearance-filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const exportCanvas = async (
|
|||||||
if (type === "svg" || type === "clipboard-svg") {
|
if (type === "svg" || type === "clipboard-svg") {
|
||||||
const tempSvg = exportToSvg(elements, {
|
const tempSvg = exportToSvg(elements, {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
scale,
|
||||||
|
|||||||
+1
-1
@@ -141,7 +141,7 @@ export const restoreElements = (
|
|||||||
}, [] as ExcalidrawElement[]);
|
}, [] as ExcalidrawElement[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreAppState = (
|
export const restoreAppState = (
|
||||||
appState: ImportedDataState["appState"],
|
appState: ImportedDataState["appState"],
|
||||||
localAppState: Partial<AppState> | null,
|
localAppState: Partial<AppState> | null,
|
||||||
): AppState => {
|
): AppState => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface ImportedDataState {
|
|||||||
source?: string;
|
source?: string;
|
||||||
elements?: DataState["elements"] | null;
|
elements?: DataState["elements"] | null;
|
||||||
appState?: Partial<DataState["appState"]> | null;
|
appState?: Partial<DataState["appState"]> | null;
|
||||||
|
scrollToCenter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibraryData {
|
export interface LibraryData {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export class LinearElementEditor {
|
|||||||
isDragging &&
|
isDragging &&
|
||||||
(activePointIndex === 0 || activePointIndex === element.points.length - 1)
|
(activePointIndex === 0 || activePointIndex === element.points.length - 1)
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoint(
|
LinearElementEditor.movePoint(
|
||||||
element,
|
element,
|
||||||
activePointIndex,
|
activePointIndex,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const showSelectedShapeActions = (
|
|||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) =>
|
) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
appState.editingElement ||
|
!appState.viewModeEnabled &&
|
||||||
getSelectedElements(elements, appState).length ||
|
(appState.editingElement ||
|
||||||
appState.elementType !== "selection",
|
getSelectedElements(elements, appState).length ||
|
||||||
|
appState.elementType !== "selection"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const textWysiwyg = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
getViewportCoords,
|
getViewportCoords,
|
||||||
element,
|
element,
|
||||||
|
canvas,
|
||||||
}: {
|
}: {
|
||||||
id: ExcalidrawElement["id"];
|
id: ExcalidrawElement["id"];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -45,6 +46,7 @@ export const textWysiwyg = ({
|
|||||||
onSubmit: (text: string) => void;
|
onSubmit: (text: string) => void;
|
||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||||
@@ -89,9 +91,6 @@ export const textWysiwyg = ({
|
|||||||
editable.dataset.type = "wysiwyg";
|
editable.dataset.type = "wysiwyg";
|
||||||
// prevent line wrapping on Safari
|
// prevent line wrapping on Safari
|
||||||
editable.wrap = "off";
|
editable.wrap = "off";
|
||||||
editable.className = `excalidraw ${
|
|
||||||
appState.appearance === "dark" ? "Appearance_dark" : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
@@ -107,6 +106,8 @@ export const textWysiwyg = ({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
|
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
|
||||||
whiteSpace: "pre",
|
whiteSpace: "pre",
|
||||||
|
// must be specified because in dark mode canvas creates a stacking context
|
||||||
|
zIndex: "var(--zIndex-wysiwyg)",
|
||||||
});
|
});
|
||||||
|
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
@@ -152,6 +153,10 @@ export const textWysiwyg = ({
|
|||||||
editable.oninput = null;
|
editable.oninput = null;
|
||||||
editable.onkeydown = null;
|
editable.onkeydown = null;
|
||||||
|
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener("resize", updateWysiwygStyle);
|
window.removeEventListener("resize", updateWysiwygStyle);
|
||||||
window.removeEventListener("wheel", stopEvent, true);
|
window.removeEventListener("wheel", stopEvent, true);
|
||||||
window.removeEventListener("pointerdown", onPointerDown);
|
window.removeEventListener("pointerdown", onPointerDown);
|
||||||
@@ -160,7 +165,7 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
unbindUpdate();
|
unbindUpdate();
|
||||||
|
|
||||||
document.body.removeChild(editable);
|
editable.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebindBlur = () => {
|
const rebindBlur = () => {
|
||||||
@@ -198,15 +203,27 @@ export const textWysiwyg = ({
|
|||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
|
|
||||||
editable.onblur = handleSubmit;
|
editable.onblur = handleSubmit;
|
||||||
// reposition wysiwyg in case of window resize. Happens on mobile when
|
|
||||||
// device keyboard is opened.
|
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
||||||
window.addEventListener("resize", updateWysiwygStyle);
|
// is preferred so we catch changes from host, where window may not resize.
|
||||||
|
let observer: ResizeObserver | null = null;
|
||||||
|
if (canvas && "ResizeObserver" in window) {
|
||||||
|
observer = new window.ResizeObserver(() => {
|
||||||
|
updateWysiwygStyle();
|
||||||
|
});
|
||||||
|
observer.observe(canvas);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("resize", updateWysiwygStyle);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("pointerdown", onPointerDown);
|
window.addEventListener("pointerdown", onPointerDown);
|
||||||
window.addEventListener("wheel", stopEvent, {
|
window.addEventListener("wheel", stopEvent, {
|
||||||
passive: false,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
document.body.appendChild(editable);
|
document
|
||||||
|
.querySelector(".excalidraw-textEditorContainer")!
|
||||||
|
.appendChild(editable);
|
||||||
editable.focus();
|
editable.focus();
|
||||||
editable.select();
|
editable.select();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,12 +19,16 @@ import {
|
|||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import {
|
import {
|
||||||
decryptAESGEM,
|
decryptAESGEM,
|
||||||
generateCollaborationLink,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLinkData,
|
getCollaborationLink,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SOCKET_SERVER,
|
SOCKET_SERVER,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import { isSavedToFirebase, saveToFirebase } from "../data/firebase";
|
import {
|
||||||
|
isSavedToFirebase,
|
||||||
|
loadFromFirebase,
|
||||||
|
saveToFirebase,
|
||||||
|
} from "../data/firebase";
|
||||||
import {
|
import {
|
||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
saveUsernameToLocalStorage,
|
saveUsernameToLocalStorage,
|
||||||
@@ -33,20 +37,25 @@ import {
|
|||||||
import Portal from "./Portal";
|
import Portal from "./Portal";
|
||||||
import RoomDialog from "./RoomDialog";
|
import RoomDialog from "./RoomDialog";
|
||||||
import { createInverseContext } from "../../createInverseContext";
|
import { createInverseContext } from "../../createInverseContext";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { UserIdleState } from "./types";
|
||||||
|
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
isCollaborating: boolean;
|
|
||||||
modalIsShown: boolean;
|
modalIsShown: boolean;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
userState: UserIdleState;
|
||||||
activeRoomLink: string;
|
activeRoomLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollabInstance = InstanceType<typeof CollabWrapper>;
|
type CollabInstance = InstanceType<typeof CollabWrapper>;
|
||||||
|
|
||||||
export interface CollabAPI {
|
export interface CollabAPI {
|
||||||
isCollaborating: CollabState["isCollaborating"];
|
/** function so that we can access the latest value from stale callbacks */
|
||||||
|
isCollaborating: () => boolean;
|
||||||
username: CollabState["username"];
|
username: CollabState["username"];
|
||||||
|
userState: CollabState["userState"];
|
||||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||||
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
||||||
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
||||||
@@ -72,6 +81,10 @@ export { CollabContext, CollabContextConsumer };
|
|||||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
portal: Portal;
|
portal: Portal;
|
||||||
excalidrawAPI: Props["excalidrawAPI"];
|
excalidrawAPI: Props["excalidrawAPI"];
|
||||||
|
isCollaborating: boolean = false;
|
||||||
|
activeIntervalId: number | null;
|
||||||
|
idleTimeoutId: number | null;
|
||||||
|
|
||||||
private socketInitializationTimer?: NodeJS.Timeout;
|
private socketInitializationTimer?: NodeJS.Timeout;
|
||||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
private collaborators = new Map<string, Collaborator>();
|
private collaborators = new Map<string, Collaborator>();
|
||||||
@@ -79,14 +92,16 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isCollaborating: false,
|
|
||||||
modalIsShown: false,
|
modalIsShown: false,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
username: importUsernameFromLocalStorage() || "",
|
username: importUsernameFromLocalStorage() || "",
|
||||||
|
userState: UserIdleState.ACTIVE,
|
||||||
activeRoomLink: "",
|
activeRoomLink: "",
|
||||||
};
|
};
|
||||||
this.portal = new Portal(this);
|
this.portal = new Portal(this);
|
||||||
this.excalidrawAPI = props.excalidrawAPI;
|
this.excalidrawAPI = props.excalidrawAPI;
|
||||||
|
this.activeIntervalId = null;
|
||||||
|
this.idleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -110,18 +125,32 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||||
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
|
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
||||||
|
window.removeEventListener(
|
||||||
|
EVENT.VISIBILITY_CHANGE,
|
||||||
|
this.onVisibilityChange,
|
||||||
|
);
|
||||||
|
if (this.activeIntervalId) {
|
||||||
|
window.clearInterval(this.activeIntervalId);
|
||||||
|
this.activeIntervalId = null;
|
||||||
|
}
|
||||||
|
if (this.idleTimeoutId) {
|
||||||
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
|
this.idleTimeoutId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = () => {
|
||||||
this.destroySocketClient();
|
this.destroySocketClient({ isUnload: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||||
const syncableElements = getSyncableElements(
|
const syncableElements = getSyncableElements(
|
||||||
this.getSceneElementsIncludingDeleted(),
|
this.getSceneElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isCollaborating &&
|
this.isCollaborating &&
|
||||||
!isSavedToFirebase(this.portal, syncableElements)
|
!isSavedToFirebase(this.portal, syncableElements)
|
||||||
) {
|
) {
|
||||||
// this won't run in time if user decides to leave the site, but
|
// this won't run in time if user decides to leave the site, but
|
||||||
@@ -133,7 +162,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
event.returnValue = "";
|
event.returnValue = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isCollaborating || this.portal.roomId) {
|
if (this.isCollaborating || this.portal.roomId) {
|
||||||
try {
|
try {
|
||||||
localStorage?.setItem(
|
localStorage?.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
||||||
@@ -159,143 +188,192 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
openPortal = async () => {
|
openPortal = async () => {
|
||||||
window.history.pushState({}, APP_NAME, await generateCollaborationLink());
|
return this.initializeSocketClient(null);
|
||||||
const elements = this.excalidrawAPI.getSceneElements();
|
|
||||||
// remove deleted elements from elements array & history to ensure we don't
|
|
||||||
// expose potentially sensitive user data in case user manually deletes
|
|
||||||
// existing elements (or clears scene), which would otherwise be persisted
|
|
||||||
// to database even if deleted before creating the room.
|
|
||||||
this.excalidrawAPI.history.clear();
|
|
||||||
this.excalidrawAPI.updateScene({
|
|
||||||
elements,
|
|
||||||
commitToHistory: true,
|
|
||||||
});
|
|
||||||
return this.initializeSocketClient();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
closePortal = () => {
|
closePortal = () => {
|
||||||
this.saveCollabRoomToFirebase();
|
this.saveCollabRoomToFirebase();
|
||||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
||||||
this.destroySocketClient();
|
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||||
|
this.destroySocketClient();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private destroySocketClient = () => {
|
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||||
this.collaborators = new Map();
|
if (!opts?.isUnload) {
|
||||||
this.excalidrawAPI.updateScene({
|
this.collaborators = new Map();
|
||||||
collaborators: this.collaborators,
|
this.excalidrawAPI.updateScene({
|
||||||
});
|
collaborators: this.collaborators,
|
||||||
this.setState({
|
});
|
||||||
isCollaborating: false,
|
this.setState({
|
||||||
activeRoomLink: "",
|
activeRoomLink: "",
|
||||||
});
|
});
|
||||||
|
this.isCollaborating = false;
|
||||||
|
}
|
||||||
this.portal.close();
|
this.portal.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSocketClient = async (): Promise<ImportedDataState | null> => {
|
private initializeSocketClient = async (
|
||||||
|
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||||
|
): Promise<ImportedDataState | null> => {
|
||||||
if (this.portal.socket) {
|
if (this.portal.socket) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
let roomId;
|
||||||
|
let roomKey;
|
||||||
|
|
||||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
if (existingRoomLinkData) {
|
||||||
|
({ roomId, roomKey } = existingRoomLinkData);
|
||||||
if (roomMatch) {
|
} else {
|
||||||
const roomId = roomMatch[1];
|
({ roomId, roomKey } = await generateCollaborationLinkData());
|
||||||
const roomKey = roomMatch[2];
|
window.history.pushState(
|
||||||
|
{},
|
||||||
// fallback in case you're not alone in the room but still don't receive
|
APP_NAME,
|
||||||
// initial SCENE_UPDATE message
|
getCollaborationLink({ roomId, roomKey }),
|
||||||
this.socketInitializationTimer = setTimeout(() => {
|
|
||||||
this.initializeSocket();
|
|
||||||
scenePromise.resolve(null);
|
|
||||||
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
|
||||||
|
|
||||||
const { default: socketIOClient }: any = await import(
|
|
||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey);
|
|
||||||
|
|
||||||
// All socket listeners are moving to Portal
|
|
||||||
this.portal.socket!.on(
|
|
||||||
"client-broadcast",
|
|
||||||
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
|
||||||
if (!this.portal.roomKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const decryptedData = await decryptAESGEM(
|
|
||||||
encryptedData,
|
|
||||||
this.portal.roomKey,
|
|
||||||
iv,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (decryptedData.type) {
|
|
||||||
case "INVALID_RESPONSE":
|
|
||||||
return;
|
|
||||||
case SCENE.INIT: {
|
|
||||||
if (!this.portal.socketInitialized) {
|
|
||||||
const remoteElements = decryptedData.payload.elements;
|
|
||||||
const reconciledElements = this.reconcileElements(
|
|
||||||
remoteElements,
|
|
||||||
);
|
|
||||||
this.handleRemoteSceneUpdate(reconciledElements, {
|
|
||||||
init: true,
|
|
||||||
});
|
|
||||||
this.initializeSocket();
|
|
||||||
scenePromise.resolve({ elements: reconciledElements });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SCENE.UPDATE:
|
|
||||||
this.handleRemoteSceneUpdate(
|
|
||||||
this.reconcileElements(decryptedData.payload.elements),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "MOUSE_LOCATION": {
|
|
||||||
const {
|
|
||||||
pointer,
|
|
||||||
button,
|
|
||||||
username,
|
|
||||||
selectedElementIds,
|
|
||||||
} = decryptedData.payload;
|
|
||||||
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
|
||||||
decryptedData.payload.socketId ||
|
|
||||||
// @ts-ignore legacy, see #2094 (#2097)
|
|
||||||
decryptedData.payload.socketID;
|
|
||||||
|
|
||||||
const collaborators = new Map(this.collaborators);
|
|
||||||
const user = collaborators.get(socketId) || {}!;
|
|
||||||
user.pointer = pointer;
|
|
||||||
user.button = button;
|
|
||||||
user.selectedElementIds = selectedElementIds;
|
|
||||||
user.username = username;
|
|
||||||
collaborators.set(socketId, user);
|
|
||||||
this.excalidrawAPI.updateScene({
|
|
||||||
collaborators,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.portal.socket!.on("first-in-room", () => {
|
|
||||||
if (this.portal.socket) {
|
|
||||||
this.portal.socket.off("first-in-room");
|
|
||||||
}
|
|
||||||
this.initializeSocket();
|
|
||||||
scenePromise.resolve(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isCollaborating: true,
|
|
||||||
activeRoomLink: window.location.href,
|
|
||||||
});
|
|
||||||
|
|
||||||
return scenePromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||||
|
|
||||||
|
this.isCollaborating = true;
|
||||||
|
|
||||||
|
const { default: socketIOClient }: any = await import(
|
||||||
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey);
|
||||||
|
|
||||||
|
if (existingRoomLinkData) {
|
||||||
|
this.excalidrawAPI.resetScene();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const elements = await loadFromFirebase(
|
||||||
|
roomId,
|
||||||
|
roomKey,
|
||||||
|
this.portal.socket,
|
||||||
|
);
|
||||||
|
if (elements) {
|
||||||
|
scenePromise.resolve({
|
||||||
|
elements,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// log the error and move on. other peers will sync us the scene.
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const elements = this.excalidrawAPI.getSceneElements();
|
||||||
|
// remove deleted elements from elements array & history to ensure we don't
|
||||||
|
// expose potentially sensitive user data in case user manually deletes
|
||||||
|
// existing elements (or clears scene), which would otherwise be persisted
|
||||||
|
// to database even if deleted before creating the room.
|
||||||
|
this.excalidrawAPI.history.clear();
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
elements,
|
||||||
|
commitToHistory: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback in case you're not alone in the room but still don't receive
|
||||||
|
// initial SCENE_UPDATE message
|
||||||
|
this.socketInitializationTimer = setTimeout(() => {
|
||||||
|
this.initializeSocket();
|
||||||
|
scenePromise.resolve(null);
|
||||||
|
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
||||||
|
|
||||||
|
// All socket listeners are moving to Portal
|
||||||
|
this.portal.socket!.on(
|
||||||
|
"client-broadcast",
|
||||||
|
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
||||||
|
if (!this.portal.roomKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decryptedData = await decryptAESGEM(
|
||||||
|
encryptedData,
|
||||||
|
this.portal.roomKey,
|
||||||
|
iv,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (decryptedData.type) {
|
||||||
|
case "INVALID_RESPONSE":
|
||||||
|
return;
|
||||||
|
case SCENE.INIT: {
|
||||||
|
if (!this.portal.socketInitialized) {
|
||||||
|
this.initializeSocket();
|
||||||
|
const remoteElements = decryptedData.payload.elements;
|
||||||
|
const reconciledElements = this.reconcileElements(remoteElements);
|
||||||
|
this.handleRemoteSceneUpdate(reconciledElements, {
|
||||||
|
init: true,
|
||||||
|
});
|
||||||
|
// noop if already resolved via init from firebase
|
||||||
|
scenePromise.resolve({
|
||||||
|
elements: reconciledElements,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SCENE.UPDATE:
|
||||||
|
this.handleRemoteSceneUpdate(
|
||||||
|
this.reconcileElements(decryptedData.payload.elements),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "MOUSE_LOCATION": {
|
||||||
|
const {
|
||||||
|
pointer,
|
||||||
|
button,
|
||||||
|
username,
|
||||||
|
selectedElementIds,
|
||||||
|
} = decryptedData.payload;
|
||||||
|
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
||||||
|
decryptedData.payload.socketId ||
|
||||||
|
// @ts-ignore legacy, see #2094 (#2097)
|
||||||
|
decryptedData.payload.socketID;
|
||||||
|
|
||||||
|
const collaborators = new Map(this.collaborators);
|
||||||
|
const user = collaborators.get(socketId) || {}!;
|
||||||
|
user.pointer = pointer;
|
||||||
|
user.button = button;
|
||||||
|
user.selectedElementIds = selectedElementIds;
|
||||||
|
user.username = username;
|
||||||
|
collaborators.set(socketId, user);
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
collaborators,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "IDLE_STATUS": {
|
||||||
|
const { userState, socketId, username } = decryptedData.payload;
|
||||||
|
const collaborators = new Map(this.collaborators);
|
||||||
|
const user = collaborators.get(socketId) || {}!;
|
||||||
|
user.userState = userState;
|
||||||
|
user.username = username;
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
collaborators,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.portal.socket!.on("first-in-room", () => {
|
||||||
|
if (this.portal.socket) {
|
||||||
|
this.portal.socket.off("first-in-room");
|
||||||
|
}
|
||||||
|
this.initializeSocket();
|
||||||
|
scenePromise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initializeIdleDetector();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
activeRoomLink: window.location.href,
|
||||||
|
});
|
||||||
|
|
||||||
|
return scenePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSocket = () => {
|
private initializeSocket = () => {
|
||||||
@@ -359,7 +437,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
// Avoid broadcasting to the rest of the collaborators the scene
|
// Avoid broadcasting to the rest of the collaborators the scene
|
||||||
// we just received!
|
// we just received!
|
||||||
// Note: this needs to be set before updating the scene as it
|
// Note: this needs to be set before updating the scene as it
|
||||||
// syncronously calls render.
|
// synchronously calls render.
|
||||||
this.setLastBroadcastedOrReceivedSceneVersion(getSceneVersion(newElements));
|
this.setLastBroadcastedOrReceivedSceneVersion(getSceneVersion(newElements));
|
||||||
|
|
||||||
return newElements as ReconciledElements;
|
return newElements as ReconciledElements;
|
||||||
@@ -388,6 +466,58 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.excalidrawAPI.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onPointerMove = () => {
|
||||||
|
if (this.idleTimeoutId) {
|
||||||
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
|
this.idleTimeoutId = null;
|
||||||
|
}
|
||||||
|
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
||||||
|
if (!this.activeIntervalId) {
|
||||||
|
this.activeIntervalId = window.setInterval(
|
||||||
|
this.reportActive,
|
||||||
|
ACTIVE_THRESHOLD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (this.idleTimeoutId) {
|
||||||
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
|
this.idleTimeoutId = null;
|
||||||
|
}
|
||||||
|
if (this.activeIntervalId) {
|
||||||
|
window.clearInterval(this.activeIntervalId);
|
||||||
|
this.activeIntervalId = null;
|
||||||
|
}
|
||||||
|
this.onIdleStateChange(UserIdleState.AWAY);
|
||||||
|
} else {
|
||||||
|
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
||||||
|
this.activeIntervalId = window.setInterval(
|
||||||
|
this.reportActive,
|
||||||
|
ACTIVE_THRESHOLD,
|
||||||
|
);
|
||||||
|
this.onIdleStateChange(UserIdleState.ACTIVE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private reportIdle = () => {
|
||||||
|
this.onIdleStateChange(UserIdleState.IDLE);
|
||||||
|
if (this.activeIntervalId) {
|
||||||
|
window.clearInterval(this.activeIntervalId);
|
||||||
|
this.activeIntervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private reportActive = () => {
|
||||||
|
this.onIdleStateChange(UserIdleState.ACTIVE);
|
||||||
|
};
|
||||||
|
|
||||||
|
private initializeIdleDetector = () => {
|
||||||
|
document.addEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
||||||
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
|
||||||
|
};
|
||||||
|
|
||||||
setCollaborators(sockets: string[]) {
|
setCollaborators(sockets: string[]) {
|
||||||
this.setState((state) => {
|
this.setState((state) => {
|
||||||
const collaborators: InstanceType<
|
const collaborators: InstanceType<
|
||||||
@@ -427,6 +557,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.portal.broadcastMouseLocation(payload);
|
this.portal.broadcastMouseLocation(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onIdleStateChange = (userState: UserIdleState) => {
|
||||||
|
this.setState({ userState });
|
||||||
|
this.portal.broadcastIdleChange(userState);
|
||||||
|
};
|
||||||
|
|
||||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||||
if (
|
if (
|
||||||
getSceneVersion(elements) >
|
getSceneVersion(elements) >
|
||||||
@@ -480,9 +615,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
/** Getter of context value. Returned object is stable. */
|
/** Getter of context value. Returned object is stable. */
|
||||||
getContextValue = (): CollabAPI => {
|
getContextValue = (): CollabAPI => {
|
||||||
this.contextValue = this.contextValue || ({} as CollabAPI);
|
if (!this.contextValue) {
|
||||||
|
this.contextValue = {} as CollabAPI;
|
||||||
|
}
|
||||||
|
|
||||||
this.contextValue.isCollaborating = this.state.isCollaborating;
|
this.contextValue.isCollaborating = () => this.isCollaborating;
|
||||||
this.contextValue.username = this.state.username;
|
this.contextValue.username = this.state.username;
|
||||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import CollabWrapper from "./CollabWrapper";
|
|||||||
import { getSyncableElements } from "../../packages/excalidraw/index";
|
import { getSyncableElements } from "../../packages/excalidraw/index";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { BROADCAST, SCENE } from "../app_constants";
|
import { BROADCAST, SCENE } from "../app_constants";
|
||||||
|
import { UserIdleState } from "./types";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: CollabWrapper;
|
collab: CollabWrapper;
|
||||||
@@ -122,7 +123,7 @@ class Portal {
|
|||||||
data as SocketUpdateData,
|
data as SocketUpdateData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (syncAll && this.collab.state.isCollaborating) {
|
if (syncAll && this.collab.isCollaborating) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
broadcastPromise,
|
broadcastPromise,
|
||||||
this.collab.saveCollabRoomToFirebase(syncableElements),
|
this.collab.saveCollabRoomToFirebase(syncableElements),
|
||||||
@@ -132,6 +133,23 @@ class Portal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
broadcastIdleChange = (userState: UserIdleState) => {
|
||||||
|
if (this.socket?.id) {
|
||||||
|
const data: SocketUpdateDataSource["IDLE_STATUS"] = {
|
||||||
|
type: "IDLE_STATUS",
|
||||||
|
payload: {
|
||||||
|
socketId: this.socket.id,
|
||||||
|
userState,
|
||||||
|
username: this.collab.state.username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return this._broadcastSocketData(
|
||||||
|
data as SocketUpdateData,
|
||||||
|
true, // volatile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
broadcastMouseLocation = (payload: {
|
broadcastMouseLocation = (payload: {
|
||||||
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
||||||
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.RoomDialog-link {
|
.RoomDialog-link {
|
||||||
color: var(--text-color-primary);
|
color: var(--text-primary-color);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-inline-start: 1em;
|
margin-inline-start: 1em;
|
||||||
@@ -32,6 +32,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@media #{$is-mobile-query} {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media #{$is-mobile-query} {
|
||||||
|
.RoomDialog-usernameLabel {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDialog-username {
|
.RoomDialog-username {
|
||||||
@@ -41,6 +51,10 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-inline-start: 1em;
|
margin-inline-start: 1em;
|
||||||
|
@media #{$is-mobile-query} {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|||||||
@@ -119,7 +119,11 @@ const RoomDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog small onCloseRequest={handleClose} title={t("labels.createRoom")}>
|
<Dialog
|
||||||
|
small
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
title={t("labels.liveCollaboration")}
|
||||||
|
>
|
||||||
{renderRoomDialog()}
|
{renderRoomDialog()}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum UserIdleState {
|
||||||
|
ACTIVE = "active",
|
||||||
|
AWAY = "away",
|
||||||
|
IDLE = "idle",
|
||||||
|
}
|
||||||
@@ -148,6 +148,7 @@ export const saveToFirebase = async (
|
|||||||
export const loadFromFirebase = async (
|
export const loadFromFirebase = async (
|
||||||
roomId: string,
|
roomId: string,
|
||||||
roomKey: string,
|
roomKey: string,
|
||||||
|
socket: SocketIOClient.Socket | null,
|
||||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||||
const firebase = await getFirebase();
|
const firebase = await getFirebase();
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
@@ -160,5 +161,12 @@ export const loadFromFirebase = async (
|
|||||||
const storedScene = doc.data() as FirebaseStoredScene;
|
const storedScene = doc.data() as FirebaseStoredScene;
|
||||||
const ciphertext = storedScene.ciphertext.toUint8Array();
|
const ciphertext = storedScene.ciphertext.toUint8Array();
|
||||||
const iv = storedScene.iv.toUint8Array();
|
const iv = storedScene.iv.toUint8Array();
|
||||||
return restoreElements(await decryptElements(roomKey, iv, ciphertext));
|
|
||||||
|
const elements = await decryptElements(roomKey, iv, ciphertext);
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
firebaseSceneVersionCache.set(socket, getSceneVersion(elements));
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoreElements(elements);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ImportedDataState } from "../../data/types";
|
|||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
|
import { UserIdleState } from "../collab/types";
|
||||||
|
|
||||||
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
||||||
|
|
||||||
@@ -59,6 +60,14 @@ export type SocketUpdateDataSource = {
|
|||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
IDLE_STATUS: {
|
||||||
|
type: "IDLE_STATUS";
|
||||||
|
payload: {
|
||||||
|
socketId: string;
|
||||||
|
userState: UserIdleState;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SocketUpdateDataIncoming =
|
export type SocketUpdateDataIncoming =
|
||||||
@@ -125,17 +134,27 @@ export const decryptAESGEM = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCollaborationLinkData = (link: string) => {
|
export const getCollaborationLinkData = (link: string) => {
|
||||||
if (link.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hash = new URL(link).hash;
|
const hash = new URL(link).hash;
|
||||||
return hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
||||||
|
return match ? { roomId: match[1], roomKey: match[2] } : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateCollaborationLink = async () => {
|
export const generateCollaborationLinkData = async () => {
|
||||||
const id = await generateRandomID();
|
const roomId = await generateRandomID();
|
||||||
const key = await generateEncryptionKey();
|
const roomKey = await generateEncryptionKey();
|
||||||
return `${window.location.origin}${window.location.pathname}#room=${id},${key}`;
|
|
||||||
|
if (!roomKey) {
|
||||||
|
throw new Error("Couldn't generate room key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { roomId, roomKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCollaborationLink = (data: {
|
||||||
|
roomId: string;
|
||||||
|
roomKey: string;
|
||||||
|
}) => {
|
||||||
|
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImportedKey = (key: string, usage: KeyUsage) =>
|
export const getImportedKey = (key: string, usage: KeyUsage) =>
|
||||||
|
|||||||
+35
-100
@@ -13,7 +13,7 @@ import { ExcalidrawImperativeAPI } from "../components/App";
|
|||||||
import { ErrorDialog } from "../components/ErrorDialog";
|
import { ErrorDialog } from "../components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||||
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
||||||
import { ImportedDataState } from "../data/types";
|
import { DataState, ImportedDataState } from "../data/types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@@ -39,11 +39,9 @@ import CollabWrapper, {
|
|||||||
} from "./collab/CollabWrapper";
|
} from "./collab/CollabWrapper";
|
||||||
import { LanguageList } from "./components/LanguageList";
|
import { LanguageList } from "./components/LanguageList";
|
||||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||||
import { loadFromFirebase } from "./data/firebase";
|
|
||||||
import {
|
import {
|
||||||
importFromLocalStorage,
|
importFromLocalStorage,
|
||||||
saveToLocalStorage,
|
saveToLocalStorage,
|
||||||
STORAGE_KEYS,
|
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
@@ -66,50 +64,9 @@ const onBlur = () => {
|
|||||||
saveDebounced.flush();
|
saveDebounced.flush();
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldForceLoadScene = (
|
|
||||||
scene: ResolutionType<typeof loadScene>,
|
|
||||||
): boolean => {
|
|
||||||
if (!scene.elements.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
|
||||||
|
|
||||||
if (!roomMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomId = roomMatch[1];
|
|
||||||
|
|
||||||
let collabForceLoadFlag;
|
|
||||||
try {
|
|
||||||
collabForceLoadFlag = localStorage?.getItem(
|
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (collabForceLoadFlag) {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
room: previousRoom,
|
|
||||||
timestamp,
|
|
||||||
}: { room: string; timestamp: number } = JSON.parse(collabForceLoadFlag);
|
|
||||||
// if loading same room as the one previously unloaded within 15sec
|
|
||||||
// force reload without prompting
|
|
||||||
if (previousRoom === roomId && Date.now() - timestamp < 15000) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Scene = ImportedDataState & { commitToHistory: boolean };
|
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
resetScene: ExcalidrawImperativeAPI["resetScene"];
|
collabAPI: CollabAPI;
|
||||||
initializeSocketClient: CollabAPI["initializeSocketClient"];
|
}): Promise<ImportedDataState | null> => {
|
||||||
}): Promise<Scene | null> => {
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const id = searchParams.get("id");
|
const id = searchParams.get("id");
|
||||||
const jsonMatch = window.location.hash.match(
|
const jsonMatch = window.location.hash.match(
|
||||||
@@ -118,13 +75,21 @@ const initializeScene = async (opts: {
|
|||||||
|
|
||||||
const initialData = importFromLocalStorage();
|
const initialData = importFromLocalStorage();
|
||||||
|
|
||||||
let scene = await loadScene(null, null, initialData);
|
let scene: DataState & { scrollToCenter?: boolean } = await loadScene(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
initialData,
|
||||||
|
);
|
||||||
|
|
||||||
let isCollabScene = !!getCollaborationLinkData(window.location.href);
|
let roomLinkData = getCollaborationLinkData(window.location.href);
|
||||||
const isExternalScene = !!(id || jsonMatch || isCollabScene);
|
const isExternalScene = !!(id || jsonMatch || roomLinkData);
|
||||||
if (isExternalScene) {
|
if (isExternalScene) {
|
||||||
if (
|
if (
|
||||||
shouldForceLoadScene(scene) ||
|
// don't prompt if scene is empty
|
||||||
|
!scene.elements.length ||
|
||||||
|
// don't prompt for collab scenes because we don't override local storage
|
||||||
|
roomLinkData ||
|
||||||
|
// otherwise, prompt whether user wants to override current scene
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
||||||
) {
|
) {
|
||||||
// Backwards compatibility with legacy url format
|
// Backwards compatibility with legacy url format
|
||||||
@@ -133,7 +98,8 @@ const initializeScene = async (opts: {
|
|||||||
} else if (jsonMatch) {
|
} else if (jsonMatch) {
|
||||||
scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
|
scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
|
||||||
}
|
}
|
||||||
if (!isCollabScene) {
|
scene.scrollToCenter = true;
|
||||||
|
if (!roomLinkData) {
|
||||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -150,45 +116,19 @@ const initializeScene = async (opts: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollabScene = false;
|
roomLinkData = null;
|
||||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isCollabScene) {
|
if (roomLinkData) {
|
||||||
// when joining a room we don't want user's local scene data to be merged
|
return opts.collabAPI.initializeSocketClient(roomLinkData);
|
||||||
// into the remote scene
|
|
||||||
opts.resetScene();
|
|
||||||
const scenePromise = opts.initializeSocketClient();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [, roomId, roomKey] = getCollaborationLinkData(
|
|
||||||
window.location.href,
|
|
||||||
)!;
|
|
||||||
const elements = await loadFromFirebase(roomId, roomKey);
|
|
||||||
if (elements) {
|
|
||||||
return {
|
|
||||||
elements,
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(await scenePromise),
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// log the error and move on. other peers will sync us the scene.
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} else if (scene) {
|
} else if (scene) {
|
||||||
return scene;
|
return scene;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
function ExcalidrawWrapper() {
|
||||||
// dimensions
|
// dimensions
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -242,24 +182,16 @@ const ExcalidrawWrapper = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeScene({
|
initializeScene({ collabAPI }).then((scene) => {
|
||||||
resetScene: excalidrawAPI.resetScene,
|
|
||||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
|
||||||
}).then((scene) => {
|
|
||||||
initialStatePromiseRef.current.promise.resolve(scene);
|
initialStatePromiseRef.current.promise.resolve(scene);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHashChange = (_: HashChangeEvent) => {
|
const onHashChange = (_: HashChangeEvent) => {
|
||||||
if (window.location.hash.length > 1) {
|
initializeScene({ collabAPI }).then((scene) => {
|
||||||
initializeScene({
|
if (scene) {
|
||||||
resetScene: excalidrawAPI.resetScene,
|
excalidrawAPI.updateScene(scene);
|
||||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
}
|
||||||
}).then((scene) => {
|
});
|
||||||
if (scene) {
|
|
||||||
excalidrawAPI.updateScene(scene);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleTimeout = setTimeout(
|
const titleTimeout = setTimeout(
|
||||||
@@ -285,9 +217,13 @@ const ExcalidrawWrapper = () => {
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
saveDebounced(elements, appState);
|
if (collabAPI?.isCollaborating()) {
|
||||||
if (collabAPI?.isCollaborating) {
|
|
||||||
collabAPI.broadcastElements(elements);
|
collabAPI.broadcastElements(elements);
|
||||||
|
} else {
|
||||||
|
// collab scenes are persisted to the server, so we don't have to persist
|
||||||
|
// them locally, which has the added benefit of not overwriting whatever
|
||||||
|
// the user was working on before joining
|
||||||
|
saveDebounced(elements, appState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -350,9 +286,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
width={dimensions.width}
|
width={dimensions.width}
|
||||||
height={dimensions.height}
|
height={dimensions.height}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
user={{ name: collabAPI?.username }}
|
|
||||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||||
isCollaborating={collabAPI?.isCollaborating}
|
isCollaborating={collabAPI?.isCollaborating()}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
onExportToBackend={onExportToBackend}
|
onExportToBackend={onExportToBackend}
|
||||||
renderFooter={renderFooter}
|
renderFooter={renderFooter}
|
||||||
@@ -367,7 +302,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function ExcalidrawApp() {
|
export default function ExcalidrawApp() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Vendored
+1
@@ -12,6 +12,7 @@ interface Document {
|
|||||||
interface Window {
|
interface Window {
|
||||||
ClipboardItem: any;
|
ClipboardItem: any;
|
||||||
__EXCALIDRAW_SHA__: string | undefined;
|
__EXCALIDRAW_SHA__: string | undefined;
|
||||||
|
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||||
gtag: Function;
|
gtag: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-12
@@ -1,5 +1,6 @@
|
|||||||
import fallbackLangData from "./locales/en.json";
|
import fallbackLangData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
|
import { ENV } from "./constants";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const allLanguages: Language[] = [
|
|||||||
{ code: "id-ID", label: "Bahasa Indonesia" },
|
{ code: "id-ID", label: "Bahasa Indonesia" },
|
||||||
{ code: "it-IT", label: "Italiano" },
|
{ code: "it-IT", label: "Italiano" },
|
||||||
{ code: "ja-JP", label: "日本語" },
|
{ code: "ja-JP", label: "日本語" },
|
||||||
|
{ code: "kab-KAB", label: "Taqbaylit" },
|
||||||
{ code: "ko-KR", label: "한국어" },
|
{ code: "ko-KR", label: "한국어" },
|
||||||
{ code: "my-MM", label: "Burmese" },
|
{ code: "my-MM", label: "Burmese" },
|
||||||
{ code: "nb-NO", label: "Norsk bokmål" },
|
{ code: "nb-NO", label: "Norsk bokmål" },
|
||||||
@@ -54,25 +56,33 @@ export const languages: Language[] = allLanguages
|
|||||||
COMPLETION_THRESHOLD,
|
COMPLETION_THRESHOLD,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TEST_LANG_CODE = "__test__";
|
||||||
|
if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
|
||||||
|
languages.unshift(
|
||||||
|
{ code: TEST_LANG_CODE, label: "test language" },
|
||||||
|
{
|
||||||
|
code: `${TEST_LANG_CODE}.rtl`,
|
||||||
|
label: "\u{202a}test language (rtl)\u{202c}",
|
||||||
|
rtl: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let currentLang: Language = defaultLang;
|
let currentLang: Language = defaultLang;
|
||||||
let currentLangData = {};
|
let currentLangData = {};
|
||||||
|
|
||||||
export const setLanguage = async (lang: Language) => {
|
export const setLanguage = async (lang: Language) => {
|
||||||
currentLang = lang;
|
currentLang = lang;
|
||||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||||
|
document.documentElement.lang = currentLang.code;
|
||||||
|
|
||||||
currentLangData = await import(
|
if (lang.code.startsWith(TEST_LANG_CODE)) {
|
||||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
currentLangData = {};
|
||||||
);
|
} else {
|
||||||
};
|
currentLangData = await import(
|
||||||
|
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
||||||
export const setLanguageFirstTime = async (lang: Language) => {
|
);
|
||||||
currentLang = lang;
|
}
|
||||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
|
||||||
|
|
||||||
currentLangData = await import(
|
|
||||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = () => currentLang;
|
export const getLanguage = () => currentLang;
|
||||||
@@ -92,6 +102,13 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (path: string, replacement?: { [key: string]: string }) => {
|
export const t = (path: string, replacement?: { [key: string]: string }) => {
|
||||||
|
if (currentLang.code.startsWith(TEST_LANG_CODE)) {
|
||||||
|
const name = replacement
|
||||||
|
? `${path}(${JSON.stringify(replacement).slice(1, -1)})`
|
||||||
|
: path;
|
||||||
|
return `\u{202a}[[${name}]]\u{202c}`;
|
||||||
|
}
|
||||||
|
|
||||||
const parts = path.split(".");
|
const parts = path.split(".");
|
||||||
let translation =
|
let translation =
|
||||||
findPartsForData(currentLangData, parts) ||
|
findPartsForData(currentLangData, parts) ||
|
||||||
|
|||||||
+2
-2
@@ -54,8 +54,8 @@ const elements = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
registerFont("./public/FG_Virgil.ttf", { family: "Virgil" });
|
registerFont("./public/Virgil.woff2", { family: "Virgil" });
|
||||||
registerFont("./public/Cascadia.ttf", { family: "Cascadia" });
|
registerFont("./public/Cascadia.woff2", { family: "Cascadia" });
|
||||||
|
|
||||||
const canvas = exportToCanvas(
|
const canvas = exportToCanvas(
|
||||||
elements as any,
|
elements as any,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import ExcalidrawApp from "./excalidraw-app";
|
|||||||
|
|
||||||
import "./excalidraw-app/pwa";
|
import "./excalidraw-app/pwa";
|
||||||
import "./excalidraw-app/sentry";
|
import "./excalidraw-app/sentry";
|
||||||
|
|
||||||
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
||||||
|
|
||||||
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
|
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const CODES = {
|
|||||||
V: "KeyV",
|
V: "KeyV",
|
||||||
X: "KeyX",
|
X: "KeyX",
|
||||||
Z: "KeyZ",
|
Z: "KeyZ",
|
||||||
|
R: "KeyR",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "الطبقات",
|
"layers": "الطبقات",
|
||||||
"actions": "الإجراءات",
|
"actions": "الإجراءات",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"createRoom": "مشاركة الجلسة مباشرة",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "تكرار",
|
"duplicateSelection": "تكرار",
|
||||||
"untitled": "غير معنون",
|
"untitled": "غير معنون",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "تحديد مجموعة",
|
"group": "تحديد مجموعة",
|
||||||
"ungroup": "إلغاء تحديد مجموعة",
|
"ungroup": "إلغاء تحديد مجموعة",
|
||||||
"collaborators": "المتعاونون",
|
"collaborators": "المتعاونون",
|
||||||
"gridMode": "وضع الشبكة",
|
"showGrid": "",
|
||||||
"addToLibrary": "أضف إلى المكتبة",
|
"addToLibrary": "أضف إلى المكتبة",
|
||||||
"removeFromLibrary": "حذف من المكتبة",
|
"removeFromLibrary": "حذف من المكتبة",
|
||||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة...",
|
"libraryLoadingMessage": "جارٍ تحميل المكتبة…",
|
||||||
"libraries": "تصفح المكتبات",
|
"libraries": "تصفح المكتبات",
|
||||||
"loadingScene": "جاري تحميل المشهد...",
|
"loadingScene": "جاري تحميل المشهد…",
|
||||||
"align": "محاذاة",
|
"align": "محاذاة",
|
||||||
"alignTop": "محاذاة إلى اﻷعلى",
|
"alignTop": "محاذاة إلى اﻷعلى",
|
||||||
"alignBottom": "محاذاة إلى اﻷسفل",
|
"alignBottom": "محاذاة إلى اﻷسفل",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "توسيط عمودي",
|
"centerVertically": "توسيط عمودي",
|
||||||
"centerHorizontally": "توسيط أفقي",
|
"centerHorizontally": "توسيط أفقي",
|
||||||
"distributeHorizontally": "التوزيع الأفقي",
|
"distributeHorizontally": "التوزيع الأفقي",
|
||||||
"distributeVertically": "التوزيع عمودياً"
|
"distributeVertically": "التوزيع عمودياً",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "إعادة تعيين اللوحة",
|
"clearReset": "إعادة تعيين اللوحة",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "تعديل",
|
"edit": "تعديل",
|
||||||
"undo": "تراجع",
|
"undo": "تراجع",
|
||||||
"redo": "إعادة تنفيذ",
|
"redo": "إعادة تنفيذ",
|
||||||
"roomDialog": "بدء المشاركة الحية",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "إنشاء غرفة جديدة",
|
"createNewRoom": "إنشاء غرفة جديدة",
|
||||||
"fullScreen": "شاشة كاملة",
|
"fullScreen": "شاشة كاملة",
|
||||||
"darkMode": "الوضع المظلم",
|
"darkMode": "الوضع المظلم",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "تعذر فك تشفير البيانات.",
|
"decryptFailed": "تعذر فك تشفير البيانات.",
|
||||||
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
||||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
||||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||||
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
||||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة"
|
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "التخزين",
|
"storage": "التخزين",
|
||||||
"title": "إحصائيات للمهووسين",
|
"title": "إحصائيات للمهووسين",
|
||||||
"total": "المجموع",
|
"total": "المجموع",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
"width": "العرض"
|
"width": "العرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-24
@@ -8,11 +8,11 @@
|
|||||||
"cut": "Изрежи",
|
"cut": "Изрежи",
|
||||||
"copy": "Копирай",
|
"copy": "Копирай",
|
||||||
"copyAsPng": "Копиране в клипборда",
|
"copyAsPng": "Копиране в клипборда",
|
||||||
"copyAsSvg": "Копиране в клипборда",
|
"copyAsSvg": "Копирано в клипборда като SVG",
|
||||||
"bringForward": "Преместване на~пред",
|
"bringForward": "Преместване напред",
|
||||||
"sendToBack": "Изнасяне назад",
|
"sendToBack": "Изнасяне назад",
|
||||||
"bringToFront": "~Изнасяне отпред",
|
"bringToFront": "Изнасяне отпред",
|
||||||
"sendBackward": "Изпрати назад",
|
"sendBackward": "Изпрати отзад",
|
||||||
"delete": "Изтрий",
|
"delete": "Изтрий",
|
||||||
"copyStyles": "Копирайте стилове",
|
"copyStyles": "Копирайте стилове",
|
||||||
"pasteStyles": "Постави стилове",
|
"pasteStyles": "Постави стилове",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Слоеве",
|
"layers": "Слоеве",
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"createRoom": "Споделете сесия за сътрудничество на живо",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "Дублирай",
|
"duplicateSelection": "Дублирай",
|
||||||
"untitled": "Неозаглавено",
|
"untitled": "Неозаглавено",
|
||||||
"name": "Име",
|
"name": "Име",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Групирай селекцията",
|
"group": "Групирай селекцията",
|
||||||
"ungroup": "Спри групирането на селекцията",
|
"ungroup": "Спри групирането на селекцията",
|
||||||
"collaborators": "Сътрудници",
|
"collaborators": "Сътрудници",
|
||||||
"gridMode": "Решетъчен режим",
|
"showGrid": "Показване на мрежа",
|
||||||
"addToLibrary": "Добавяне към библиотеката",
|
"addToLibrary": "Добавяне към библиотеката",
|
||||||
"removeFromLibrary": "Премахване от библиотеката",
|
"removeFromLibrary": "Премахване от библиотеката",
|
||||||
"libraryLoadingMessage": "Зареждане на библиотеката...",
|
"libraryLoadingMessage": "Зареждане на библиотеката…",
|
||||||
"libraries": "Разглеждане на библиотеките",
|
"libraries": "Разглеждане на библиотеките",
|
||||||
"loadingScene": "Зареждане на сцена...",
|
"loadingScene": "Зареждане на сцена…",
|
||||||
"align": "Подравняване",
|
"align": "Подравняване",
|
||||||
"alignTop": "Подравняване отгоре",
|
"alignTop": "Подравняване отгоре",
|
||||||
"alignBottom": "Подравняване отдолу",
|
"alignBottom": "Подравняване отдолу",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Центрирай вертикално",
|
"centerVertically": "Центрирай вертикално",
|
||||||
"centerHorizontally": "Центрирай хоризонтално",
|
"centerHorizontally": "Центрирай хоризонтално",
|
||||||
"distributeHorizontally": "Разпредели хоризонтално",
|
"distributeHorizontally": "Разпредели хоризонтално",
|
||||||
"distributeVertically": "Разпредели вертикално"
|
"distributeVertically": "Разпредели вертикално",
|
||||||
|
"viewMode": "Изглед"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Нулиране на платно",
|
"clearReset": "Нулиране на платно",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Редактиране",
|
"edit": "Редактиране",
|
||||||
"undo": "Отмяна",
|
"undo": "Отмяна",
|
||||||
"redo": "Повтори",
|
"redo": "Повтори",
|
||||||
"roomDialog": "Започнете сътрудничество на живо",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "Създай нова стая",
|
"createNewRoom": "Създай нова стая",
|
||||||
"fullScreen": "На цял екран",
|
"fullScreen": "На цял екран",
|
||||||
"darkMode": "Тъмен режим",
|
"darkMode": "Тъмен режим",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Данните не можаха да се дешифрират.",
|
"decryptFailed": "Данните не можаха да се дешифрират.",
|
||||||
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
||||||
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
||||||
|
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
||||||
"errorLoadingLibrary": "Възникна грешка при зареждането на външна библиотека.",
|
"errorLoadingLibrary": "Възникна грешка при зареждането на външна библиотека.",
|
||||||
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
||||||
"imageDoesNotContainScene": "Импортирането на картинки не се поддържва в момента.\n\nИскате да импортнете сцена? Тази картинка не съдържа данни от сцена. Разрешили ли сте последното при експортирането?",
|
"imageDoesNotContainScene": "Импортирането на картинки не се поддържва в момента.\n\nИскате да импортнете сцена? Тази картинка не съдържа данни от сцена. Разрешили ли сте последното при експортирането?",
|
||||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл"
|
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Селекция",
|
"selection": "Селекция",
|
||||||
@@ -200,24 +203,24 @@
|
|||||||
"title": "Грешка"
|
"title": "Грешка"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Прочетете нашия блог",
|
||||||
"click": "клик",
|
"click": "клик",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Извита стрелка",
|
||||||
"curvedLine": "",
|
"curvedLine": "Извита линия",
|
||||||
"documentation": "",
|
"documentation": "Документация",
|
||||||
"drag": "плъзнете",
|
"drag": "плъзнете",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
"github": "",
|
"github": "Намерихте проблем? Изпратете",
|
||||||
"howto": "",
|
"howto": "Следвайте нашите ръководства",
|
||||||
"or": "или",
|
"or": "или",
|
||||||
"preventBinding": "",
|
"preventBinding": "Спри прилепяне на стрелките",
|
||||||
"shapes": "Фигури",
|
"shapes": "Фигури",
|
||||||
"shortcuts": "Клавиши за бърз достъп",
|
"shortcuts": "Клавиши за бърз достъп",
|
||||||
"textFinish": "",
|
"textFinish": "Завършете редактирането (текст)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Добавяне на нов ред (текст)",
|
||||||
"title": "",
|
"title": "Помощ",
|
||||||
"view": "Преглед",
|
"view": "Преглед",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Приближи докато се виждат всички елементи",
|
||||||
"zoomToSelection": "Приближи селекцията"
|
"zoomToSelection": "Приближи селекцията"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Съхранение на данни",
|
"storage": "Съхранение на данни",
|
||||||
"title": "Статистика за хакери",
|
"title": "Статистика за хакери",
|
||||||
"total": "Общо",
|
"total": "Общо",
|
||||||
|
"version": "Версия",
|
||||||
|
"versionCopy": "Настисни за да копираш",
|
||||||
|
"versionNotAvailable": "Версията не е налична",
|
||||||
"width": "Широчина"
|
"width": "Широчина"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "Копирани стилове.",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "Копирано в клипборда.",
|
||||||
|
"copyToClipboardAsPng": "Копирано в клипборда като PNG.",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Capes",
|
"layers": "Capes",
|
||||||
"actions": "Accions",
|
"actions": "Accions",
|
||||||
"language": "Llengua",
|
"language": "Llengua",
|
||||||
"createRoom": "Compartir una sessió de col·laboració en directe",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "Duplicar",
|
"duplicateSelection": "Duplicar",
|
||||||
"untitled": "Sense títol",
|
"untitled": "Sense títol",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Agrupar la selecció",
|
"group": "Agrupar la selecció",
|
||||||
"ungroup": "Desagrupar la selecció",
|
"ungroup": "Desagrupar la selecció",
|
||||||
"collaborators": "Col·laboradors",
|
"collaborators": "Col·laboradors",
|
||||||
"gridMode": "Mode quadrícula",
|
"showGrid": "",
|
||||||
"addToLibrary": "Afegir a la biblioteca",
|
"addToLibrary": "Afegir a la biblioteca",
|
||||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||||
"libraryLoadingMessage": "Carregant la biblioteca...",
|
"libraryLoadingMessage": "Carregant la biblioteca…",
|
||||||
"libraries": "Explorar biblioteques",
|
"libraries": "Explorar biblioteques",
|
||||||
"loadingScene": "Carregant escena...",
|
"loadingScene": "Carregant escena…",
|
||||||
"align": "Alinear",
|
"align": "Alinear",
|
||||||
"alignTop": "Alinear a dalt",
|
"alignTop": "Alinear a dalt",
|
||||||
"alignBottom": "Alinear a baix",
|
"alignBottom": "Alinear a baix",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrar verticalment",
|
"centerVertically": "Centrar verticalment",
|
||||||
"centerHorizontally": "Centrar horitzontalment",
|
"centerHorizontally": "Centrar horitzontalment",
|
||||||
"distributeHorizontally": "Distribuir horitzontalment",
|
"distributeHorizontally": "Distribuir horitzontalment",
|
||||||
"distributeVertically": "Distribuir verticalment"
|
"distributeVertically": "Distribuir verticalment",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Netejar el llenç",
|
"clearReset": "Netejar el llenç",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"undo": "Desfer",
|
"undo": "Desfer",
|
||||||
"redo": "Refer",
|
"redo": "Refer",
|
||||||
"roomDialog": "Començar col·laboració en directe",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "Crear sala nova",
|
"createNewRoom": "Crear sala nova",
|
||||||
"fullScreen": "Pantalla completa",
|
"fullScreen": "Pantalla completa",
|
||||||
"darkMode": "Mode fosc",
|
"darkMode": "Mode fosc",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "No s'ha pogut desencriptar.",
|
"decryptFailed": "No s'ha pogut desencriptar.",
|
||||||
"uploadedSecurly": "La càrrega s'ha assegurat amb xifratge punta a punta, cosa que significa que el servidor Excalidraw i tercers no poden llegir el contingut.",
|
"uploadedSecurly": "La càrrega s'ha assegurat amb xifratge punta a punta, cosa que significa que el servidor Excalidraw i tercers no poden llegir el contingut.",
|
||||||
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
|
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "S'ha produït un error en carregar la biblioteca de tercers.",
|
"errorLoadingLibrary": "S'ha produït un error en carregar la biblioteca de tercers.",
|
||||||
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
||||||
"imageDoesNotContainScene": "En aquest moment no s’admet la importació d’imatges.\n\nVolies importar una escena? Sembla que aquesta imatge no conté cap dada d’escena. Ho has activat durant l'exportació?",
|
"imageDoesNotContainScene": "En aquest moment no s’admet la importació d’imatges.\n\nVolies importar una escena? Sembla que aquesta imatge no conté cap dada d’escena. Ho has activat durant l'exportació?",
|
||||||
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge"
|
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Emmagatzematge",
|
"storage": "Emmagatzematge",
|
||||||
"title": "Estadístiques per nerds",
|
"title": "Estadístiques per nerds",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
"width": "Amplada"
|
"width": "Amplada"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-10
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Ebenen",
|
"layers": "Ebenen",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"createRoom": "Live-Kollaborationssitzung teilen",
|
"liveCollaboration": "Live-Zusammenarbeit",
|
||||||
"duplicateSelection": "Duplizieren",
|
"duplicateSelection": "Duplizieren",
|
||||||
"untitled": "Unbenannt",
|
"untitled": "Unbenannt",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -76,13 +76,13 @@
|
|||||||
"madeWithExcalidraw": "Made with Excalidraw",
|
"madeWithExcalidraw": "Made with Excalidraw",
|
||||||
"group": "Auswahl gruppieren",
|
"group": "Auswahl gruppieren",
|
||||||
"ungroup": "Gruppierung aufheben",
|
"ungroup": "Gruppierung aufheben",
|
||||||
"collaborators": "Mitarbeitende",
|
"collaborators": "Kollaboratoren",
|
||||||
"gridMode": "Rastermodus",
|
"showGrid": "Raster anzeigen",
|
||||||
"addToLibrary": "Zur Bibliothek hinzufügen",
|
"addToLibrary": "Zur Bibliothek hinzufügen",
|
||||||
"removeFromLibrary": "Aus Bibliothek entfernen",
|
"removeFromLibrary": "Aus Bibliothek entfernen",
|
||||||
"libraryLoadingMessage": "Lade Bibliothek...",
|
"libraryLoadingMessage": "Lade Bibliothek…",
|
||||||
"libraries": "Bibliotheken durchsuchen",
|
"libraries": "Bibliotheken durchsuchen",
|
||||||
"loadingScene": "Lade Zeichnung...",
|
"loadingScene": "Lade Zeichnung…",
|
||||||
"align": "Ausrichten",
|
"align": "Ausrichten",
|
||||||
"alignTop": "Obere Kanten",
|
"alignTop": "Obere Kanten",
|
||||||
"alignBottom": "Untere Kanten",
|
"alignBottom": "Untere Kanten",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Vertikal zentrieren",
|
"centerVertically": "Vertikal zentrieren",
|
||||||
"centerHorizontally": "Horizontal zentrieren",
|
"centerHorizontally": "Horizontal zentrieren",
|
||||||
"distributeHorizontally": "Horizontal verteilen",
|
"distributeHorizontally": "Horizontal verteilen",
|
||||||
"distributeVertically": "Vertikal verteilen"
|
"distributeVertically": "Vertikal verteilen",
|
||||||
|
"viewMode": "Ansichtsmodus"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
"exportToSvg": "Als SVG exportieren",
|
"exportToSvg": "Als SVG exportieren",
|
||||||
"copyToClipboard": "In Zwischenablage kopieren",
|
"copyToClipboard": "In Zwischenablage kopieren",
|
||||||
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
||||||
"scale": "Skalieren",
|
"scale": "Skalierung",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"saveAs": "Speichern unter",
|
"saveAs": "Speichern unter",
|
||||||
"load": "Laden",
|
"load": "Laden",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"undo": "Rückgängig machen",
|
"undo": "Rückgängig machen",
|
||||||
"redo": "Wiederholen",
|
"redo": "Wiederholen",
|
||||||
"roomDialog": "Live-Kollaborationssitzung starten",
|
"resetLibrary": "Bibliothek zurücksetzen",
|
||||||
"createNewRoom": "Neuen Raum erstellen",
|
"createNewRoom": "Neuen Raum erstellen",
|
||||||
"fullScreen": "Vollbildanzeige",
|
"fullScreen": "Vollbildanzeige",
|
||||||
"darkMode": "Dunkles Design",
|
"darkMode": "Dunkles Design",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Daten konnten nicht entschlüsselt werden.",
|
"decryptFailed": "Daten konnten nicht entschlüsselt werden.",
|
||||||
"uploadedSecurly": "Der Upload wurde mit Ende-zu-Ende-Verschlüsselung gespeichert. Weder Excalidraw noch Dritte können den Inhalt einsehen.",
|
"uploadedSecurly": "Der Upload wurde mit Ende-zu-Ende-Verschlüsselung gespeichert. Weder Excalidraw noch Dritte können den Inhalt einsehen.",
|
||||||
"loadSceneOverridePrompt": "Das Laden der externen Zeichnung ersetzt den vorhandenen Inhalt. Möchtest Du fortfahren?",
|
"loadSceneOverridePrompt": "Das Laden der externen Zeichnung ersetzt den vorhandenen Inhalt. Möchtest Du fortfahren?",
|
||||||
|
"collabStopOverridePrompt": "Das Stoppen der Sitzung wird Deine vorherige, lokal gespeicherte Zeichnung überschreiben. Bist Du sicher?\n\n(Wenn Du Deine lokale Zeichnung behalten möchtest, schließe einfach stattdessen den Browser-Tab.)",
|
||||||
"errorLoadingLibrary": "Beim Laden der Drittanbieter-Bibliothek ist ein Fehler aufgetreten.",
|
"errorLoadingLibrary": "Beim Laden der Drittanbieter-Bibliothek ist ein Fehler aufgetreten.",
|
||||||
"confirmAddLibrary": "Dieses fügt {{numShapes}} Form(en) zu deiner Bibliothek hinzu. Bist du sicher?",
|
"confirmAddLibrary": "Dieses fügt {{numShapes}} Form(en) zu deiner Bibliothek hinzu. Bist du sicher?",
|
||||||
"imageDoesNotContainScene": "Das Importieren von Bildern wird derzeit nicht unterstützt.\n\nMöchtest du eine Szene importieren? Dieses Bild scheint keine Zeichnungsdaten zu enthalten. Hast du dies beim Exportieren aktiviert?",
|
"imageDoesNotContainScene": "Das Importieren von Bildern wird derzeit nicht unterstützt.\n\nMöchtest du eine Szene importieren? Dieses Bild scheint keine Zeichnungsdaten zu enthalten. Hast du dies beim Exportieren aktiviert?",
|
||||||
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden"
|
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
|
||||||
|
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Auswahl",
|
"selection": "Auswahl",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Speicher",
|
"storage": "Speicher",
|
||||||
"title": "Statistiken für Nerds",
|
"title": "Statistiken für Nerds",
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
|
"version": "Version",
|
||||||
|
"versionCopy": "Zum Kopieren klicken",
|
||||||
|
"versionNotAvailable": "Version nicht verfügbar",
|
||||||
"width": "Breite"
|
"width": "Breite"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Formatierung kopiert.",
|
"copyStyles": "Formatierung kopiert.",
|
||||||
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert."
|
"copyToClipboard": "In die Zwischenablage kopiert.",
|
||||||
|
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert.",
|
||||||
|
"fileSaved": "Datei gespeichert.",
|
||||||
|
"fileSavedToFilename": "Als {filename} gespeichert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-13
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Στρώματα",
|
"layers": "Στρώματα",
|
||||||
"actions": "Ενέργειες",
|
"actions": "Ενέργειες",
|
||||||
"language": "Γλώσσα",
|
"language": "Γλώσσα",
|
||||||
"createRoom": "Έναρξη ζωντανής συνεδρίας",
|
"liveCollaboration": "Ζωντανή συνεργασία",
|
||||||
"duplicateSelection": "Δημιουργία αντιγράφου",
|
"duplicateSelection": "Δημιουργία αντιγράφου",
|
||||||
"untitled": "Χωρίς τίτλο",
|
"untitled": "Χωρίς τίτλο",
|
||||||
"name": "Όνομα",
|
"name": "Όνομα",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Δημιουργία ομάδας από επιλογή",
|
"group": "Δημιουργία ομάδας από επιλογή",
|
||||||
"ungroup": "Κατάργηση ομάδας από επιλογή",
|
"ungroup": "Κατάργηση ομάδας από επιλογή",
|
||||||
"collaborators": "Συνεργάτες",
|
"collaborators": "Συνεργάτες",
|
||||||
"gridMode": "Εμφάνιση σε πλέγμα",
|
"showGrid": "Προβολή πλέγματος",
|
||||||
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
||||||
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
||||||
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης...",
|
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης…",
|
||||||
"libraries": "Άλλες βιβλιοθήκες",
|
"libraries": "Άλλες βιβλιοθήκες",
|
||||||
"loadingScene": "Φόρτωση σκηνής...",
|
"loadingScene": "Φόρτωση σκηνής…",
|
||||||
"align": "Στοίχιση",
|
"align": "Στοίχιση",
|
||||||
"alignTop": "Στοίχιση πάνω",
|
"alignTop": "Στοίχιση πάνω",
|
||||||
"alignBottom": "Στοίχιση κάτω",
|
"alignBottom": "Στοίχιση κάτω",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Κέντρο κάθετα",
|
"centerVertically": "Κέντρο κάθετα",
|
||||||
"centerHorizontally": "Κέντρο οριζόντια",
|
"centerHorizontally": "Κέντρο οριζόντια",
|
||||||
"distributeHorizontally": "Οριζόντια κατανομή",
|
"distributeHorizontally": "Οριζόντια κατανομή",
|
||||||
"distributeVertically": "Κατακόρυφη κατανομή"
|
"distributeVertically": "Κατακόρυφη κατανομή",
|
||||||
|
"viewMode": "Λειτουργία προβολής"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Επαναφορά του καμβά",
|
"clearReset": "Επαναφορά του καμβά",
|
||||||
@@ -116,13 +117,13 @@
|
|||||||
"edit": "Επεξεργασία",
|
"edit": "Επεξεργασία",
|
||||||
"undo": "Αναίρεση",
|
"undo": "Αναίρεση",
|
||||||
"redo": "Επαναφορά",
|
"redo": "Επαναφορά",
|
||||||
"roomDialog": "Έναρξη ζωντανής συνεργασίας",
|
"resetLibrary": "Καθαρισμός βιβλιοθήκης",
|
||||||
"createNewRoom": "Δημιουργία νέου χώρου",
|
"createNewRoom": "Δημιουργία νέου χώρου",
|
||||||
"fullScreen": "Πλήρης οθόνη",
|
"fullScreen": "Πλήρης οθόνη",
|
||||||
"darkMode": "Σκοτεινή λειτουργία",
|
"darkMode": "Σκοτεινή λειτουργία",
|
||||||
"lightMode": "Φωτεινή λειτουργία",
|
"lightMode": "Φωτεινή λειτουργία",
|
||||||
"zenMode": "Λειτουργία Zεν",
|
"zenMode": "Λειτουργία Zεν",
|
||||||
"exitZenMode": "Έξοδος απο την λειτουργία Zen"
|
"exitZenMode": "Έξοδος από την λειτουργία Zen"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
||||||
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
||||||
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
||||||
|
"collabStopOverridePrompt": "Η διακοπή της συνεδρίας θα αντικαταστήσει το προηγούμενο, τοπικά αποθηκευμένο σχέδιο. Είστε σίγουροι?\n\n(Αν θέλετε να διατηρήσετε το τοπικό σας σχέδιο, απλά κλείστε την καρτέλα του προγράμματος περιήγησης.)",
|
||||||
"errorLoadingLibrary": "Υπήρξε ένα σφάλμα κατά τη φόρτωση της βιβλιοθήκης τρίτου μέρους.",
|
"errorLoadingLibrary": "Υπήρξε ένα σφάλμα κατά τη φόρτωση της βιβλιοθήκης τρίτου μέρους.",
|
||||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβιλιοθήκη σας. Είστε σίγουροι;",
|
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"imageDoesNotContainScene": "Η εισαγωγή εικόνων δεν υποστηρίζεται αυτή τη στιγμή.\n\nΜήπως θέλετε να εισαγάγετε μια σκηνή; Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει αυτό κατά την εξαγωγή;",
|
"imageDoesNotContainScene": "Η εισαγωγή εικόνων δεν υποστηρίζεται αυτή τη στιγμή.\n\nΜήπως θέλετε να εισαγάγετε μια σκηνή; Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει αυτό κατά την εξαγωγή;",
|
||||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας"
|
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
||||||
|
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
@@ -159,7 +162,7 @@
|
|||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
||||||
"freeDraw": "Κάντε κλικ και σύρατε, απελευθερώσατε όταν έχετε τελειώσει",
|
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||||
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
||||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||||
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
||||||
@@ -182,7 +185,7 @@
|
|||||||
"clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ",
|
"clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ",
|
||||||
"trackedToSentry_pre": "Το σφάλμα με αναγνωριστικό ",
|
"trackedToSentry_pre": "Το σφάλμα με αναγνωριστικό ",
|
||||||
"trackedToSentry_post": " παρακολουθήθηκε στο σύστημά μας.",
|
"trackedToSentry_post": " παρακολουθήθηκε στο σύστημά μας.",
|
||||||
"openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψουν να ακολουθήσεις το δικό μας ",
|
"openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας ",
|
||||||
"openIssueMessage_button": "ανιχνευτής σφαλμάτων.",
|
"openIssueMessage_button": "ανιχνευτής σφαλμάτων.",
|
||||||
"openIssueMessage_post": " Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.",
|
"openIssueMessage_post": " Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.",
|
||||||
"sceneContent": "Περιεχόμενο σκηνής:"
|
"sceneContent": "Περιεχόμενο σκηνής:"
|
||||||
@@ -221,7 +224,7 @@
|
|||||||
"zoomToSelection": "Ζουμ στην επιλογή"
|
"zoomToSelection": "Ζουμ στην επιλογή"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "Γωνία",
|
"angle": "Γωνία",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Χώρος",
|
"storage": "Χώρος",
|
||||||
"title": "Στατιστικά για σπασίκλες",
|
"title": "Στατιστικά για σπασίκλες",
|
||||||
"total": "Σύνολο ",
|
"total": "Σύνολο ",
|
||||||
|
"version": "Έκδοση",
|
||||||
|
"versionCopy": "Κάνε κλικ για αντιγραφή",
|
||||||
|
"versionNotAvailable": "Έκδοση μη διαθέσιμη",
|
||||||
"width": "Πλάτος"
|
"width": "Πλάτος"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Αντιγράφηκαν στυλ.",
|
"copyStyles": "Αντιγράφηκαν στυλ.",
|
||||||
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG."
|
"copyToClipboard": "Αντιγράφηκε στο πρόχειρο.",
|
||||||
|
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG.",
|
||||||
|
"fileSaved": "Το αρχείο αποθηκεύτηκε.",
|
||||||
|
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Layers",
|
"layers": "Layers",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"createRoom": "Share a live-collaboration session",
|
"liveCollaboration": "Live collaboration",
|
||||||
"duplicateSelection": "Duplicate",
|
"duplicateSelection": "Duplicate",
|
||||||
"untitled": "Untitled",
|
"untitled": "Untitled",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Group selection",
|
"group": "Group selection",
|
||||||
"ungroup": "Ungroup selection",
|
"ungroup": "Ungroup selection",
|
||||||
"collaborators": "Collaborators",
|
"collaborators": "Collaborators",
|
||||||
"gridMode": "Grid mode",
|
"showGrid": "Show grid",
|
||||||
"addToLibrary": "Add to library",
|
"addToLibrary": "Add to library",
|
||||||
"removeFromLibrary": "Remove from library",
|
"removeFromLibrary": "Remove from library",
|
||||||
"libraryLoadingMessage": "Loading library...",
|
"libraryLoadingMessage": "Loading library…",
|
||||||
"libraries": "Browse libraries",
|
"libraries": "Browse libraries",
|
||||||
"loadingScene": "Loading scene...",
|
"loadingScene": "Loading scene…",
|
||||||
"align": "Align",
|
"align": "Align",
|
||||||
"alignTop": "Align top",
|
"alignTop": "Align top",
|
||||||
"alignBottom": "Align bottom",
|
"alignBottom": "Align bottom",
|
||||||
@@ -91,7 +91,9 @@
|
|||||||
"centerVertically": "Center vertically",
|
"centerVertically": "Center vertically",
|
||||||
"centerHorizontally": "Center horizontally",
|
"centerHorizontally": "Center horizontally",
|
||||||
"distributeHorizontally": "Distribute horizontally",
|
"distributeHorizontally": "Distribute horizontally",
|
||||||
"distributeVertically": "Distribute vertically"
|
"distributeVertically": "Distribute vertically",
|
||||||
|
"viewMode": "View mode",
|
||||||
|
"toggleExportColorScheme": "Toggle export color scheme"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
@@ -116,7 +118,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"roomDialog": "Start live collaboration",
|
"resetLibrary": "Reset library",
|
||||||
"createNewRoom": "Create new room",
|
"createNewRoom": "Create new room",
|
||||||
"fullScreen": "Full screen",
|
"fullScreen": "Full screen",
|
||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
@@ -135,10 +137,12 @@
|
|||||||
"decryptFailed": "Couldn't decrypt data.",
|
"decryptFailed": "Couldn't decrypt data.",
|
||||||
"uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.",
|
"uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.",
|
||||||
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
|
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
|
||||||
|
"collabStopOverridePrompt": "Stopping the session will overwrite your previous, locally stored drawing. Are you sure?\n\n(If you want to keep your local drawing, simply close the browser tab instead.)",
|
||||||
"errorLoadingLibrary": "There was an error loading the third party library.",
|
"errorLoadingLibrary": "There was an error loading the third party library.",
|
||||||
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
|
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
|
||||||
"imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
|
"imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
|
||||||
"cannotRestoreFromImage": "Scene couldn't be restored from this image file"
|
"cannotRestoreFromImage": "Scene couldn't be restored from this image file",
|
||||||
|
"resetLibrary": "This will clear your library. Are you sure?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
@@ -233,10 +237,16 @@
|
|||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
"title": "Stats for nerds",
|
"title": "Stats for nerds",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"version": "Version",
|
||||||
|
"versionCopy": "Click to copy",
|
||||||
|
"versionNotAvailable": "Version not available",
|
||||||
"width": "Width"
|
"width": "Width"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Copied styles.",
|
"copyStyles": "Copied styles.",
|
||||||
"copyToClipboardAsPng": "Copied to clipboard as PNG."
|
"copyToClipboard": "Copied to clipboard.",
|
||||||
|
"copyToClipboardAsPng": "Copied to clipboard as PNG.",
|
||||||
|
"fileSaved": "File saved.",
|
||||||
|
"fileSavedToFilename": "Saved to {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Capas",
|
"layers": "Capas",
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"createRoom": "Compartir una sesión de colaboración en vivo",
|
"liveCollaboration": "Colaboración en directo",
|
||||||
"duplicateSelection": "Duplicar",
|
"duplicateSelection": "Duplicar",
|
||||||
"untitled": "Sin título",
|
"untitled": "Sin título",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Agrupar selección",
|
"group": "Agrupar selección",
|
||||||
"ungroup": "Desagrupar selección",
|
"ungroup": "Desagrupar selección",
|
||||||
"collaborators": "Colaboradores",
|
"collaborators": "Colaboradores",
|
||||||
"gridMode": "Modo cuadrícula",
|
"showGrid": "Mostrar cuadrícula",
|
||||||
"addToLibrary": "Añadir a la biblioteca",
|
"addToLibrary": "Añadir a la biblioteca",
|
||||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||||
"libraryLoadingMessage": "Cargando biblioteca...",
|
"libraryLoadingMessage": "Cargando librería…",
|
||||||
"libraries": "Explorar bibliotecas",
|
"libraries": "Explorar bibliotecas",
|
||||||
"loadingScene": "Cargando escena...",
|
"loadingScene": "Cargando escena…",
|
||||||
"align": "Alinear",
|
"align": "Alinear",
|
||||||
"alignTop": "Alineación superior",
|
"alignTop": "Alineación superior",
|
||||||
"alignBottom": "Alineación inferior",
|
"alignBottom": "Alineación inferior",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrar verticalmente",
|
"centerVertically": "Centrar verticalmente",
|
||||||
"centerHorizontally": "Centrar horizontalmente",
|
"centerHorizontally": "Centrar horizontalmente",
|
||||||
"distributeHorizontally": "Distribuir horizontalmente",
|
"distributeHorizontally": "Distribuir horizontalmente",
|
||||||
"distributeVertically": "Distribuir verticalmente"
|
"distributeVertically": "Distribuir verticalmente",
|
||||||
|
"viewMode": "Modo presentación"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"undo": "Deshacer",
|
"undo": "Deshacer",
|
||||||
"redo": "Rehacer",
|
"redo": "Rehacer",
|
||||||
"roomDialog": "Iniciar colaboración en vivo",
|
"resetLibrary": "Resetear librería",
|
||||||
"createNewRoom": "Crear nueva sala",
|
"createNewRoom": "Crear nueva sala",
|
||||||
"fullScreen": "Pantalla completa",
|
"fullScreen": "Pantalla completa",
|
||||||
"darkMode": "Modo oscuro",
|
"darkMode": "Modo oscuro",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "No se pudieron descifrar los datos.",
|
"decryptFailed": "No se pudieron descifrar los datos.",
|
||||||
"uploadedSecurly": "La carga ha sido asegurada con cifrado de principio a fin, lo que significa que el servidor de Excalidraw y terceros no pueden leer el contenido.",
|
"uploadedSecurly": "La carga ha sido asegurada con cifrado de principio a fin, lo que significa que el servidor de Excalidraw y terceros no pueden leer el contenido.",
|
||||||
"loadSceneOverridePrompt": "Si carga este dibujo externo, reemplazará el que tiene. ¿Desea continuar?",
|
"loadSceneOverridePrompt": "Si carga este dibujo externo, reemplazará el que tiene. ¿Desea continuar?",
|
||||||
|
"collabStopOverridePrompt": "Detener la sesión sobrescribirá su dibujo anterior almacenado en local. ¿Estás seguro?\n\n(Si quieres mantener tu dibujo en local, simplemente cierre la pestaña del navegador en su lugar.)",
|
||||||
"errorLoadingLibrary": "Se ha producido un error al cargar la biblioteca de terceros.",
|
"errorLoadingLibrary": "Se ha producido un error al cargar la biblioteca de terceros.",
|
||||||
"confirmAddLibrary": "Esto añadirá {{numShapes}} forma(s) a tu biblioteca. ¿Estás seguro?",
|
"confirmAddLibrary": "Esto añadirá {{numShapes}} forma(s) a tu biblioteca. ¿Estás seguro?",
|
||||||
"imageDoesNotContainScene": "La importación de imágenes no está homologada en este momento.\n\n¿Deseas importar una escena? Esta imagen no parece contener ningún dato de escena. ¿Lo has activado durante la exportación?",
|
"imageDoesNotContainScene": "La importación de imágenes no está homologada en este momento.\n\n¿Deseas importar una escena? Esta imagen no parece contener ningún dato de escena. ¿Lo has activado durante la exportación?",
|
||||||
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen"
|
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen",
|
||||||
|
"resetLibrary": "Esto eliminará tu librería. ¿Estás seguro?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selección",
|
"selection": "Selección",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Almacenamiento",
|
"storage": "Almacenamiento",
|
||||||
"title": "Estadísticas para nerds",
|
"title": "Estadísticas para nerds",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"version": "Versión",
|
||||||
|
"versionCopy": "Clic para copiar",
|
||||||
|
"versionNotAvailable": "Versión no disponible",
|
||||||
"width": "Ancho"
|
"width": "Ancho"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Estilos copiados.",
|
"copyStyles": "Estilos copiados.",
|
||||||
"copyToClipboardAsPng": "Copiado al portapapeles como PNG."
|
"copyToClipboard": "Copiado en el portapapeles.",
|
||||||
|
"copyToClipboardAsPng": "Copiado al portapapeles como PNG.",
|
||||||
|
"fileSaved": "Archivo guardado.",
|
||||||
|
"fileSavedToFilename": "Guardado en {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "لایه ها",
|
"layers": "لایه ها",
|
||||||
"actions": "عملیات",
|
"actions": "عملیات",
|
||||||
"language": "زبان",
|
"language": "زبان",
|
||||||
"createRoom": "اشتراک گذاری جلسه همکاری زنده",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "تکرار",
|
"duplicateSelection": "تکرار",
|
||||||
"untitled": "بدون عنوان",
|
"untitled": "بدون عنوان",
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "گروهبندی انتخابها",
|
"group": "گروهبندی انتخابها",
|
||||||
"ungroup": "حذف گروهبندی انتخابها",
|
"ungroup": "حذف گروهبندی انتخابها",
|
||||||
"collaborators": "همکاران",
|
"collaborators": "همکاران",
|
||||||
"gridMode": "حالت شبکه ای",
|
"showGrid": "",
|
||||||
"addToLibrary": "افزودن به کتابخانه",
|
"addToLibrary": "افزودن به کتابخانه",
|
||||||
"removeFromLibrary": "حذف از کتابخانه",
|
"removeFromLibrary": "حذف از کتابخانه",
|
||||||
"libraryLoadingMessage": "بارگذاری کتابخانه...",
|
"libraryLoadingMessage": "بارگذاری کتابخانه…",
|
||||||
"libraries": "مرور کردن کتابخانه ها",
|
"libraries": "مرور کردن کتابخانه ها",
|
||||||
"loadingScene": "باگذاری صحنه...",
|
"loadingScene": "باگذاری صحنه…",
|
||||||
"align": "تراز",
|
"align": "تراز",
|
||||||
"alignTop": "تراز به بالا",
|
"alignTop": "تراز به بالا",
|
||||||
"alignBottom": "تراز به پایین",
|
"alignBottom": "تراز به پایین",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "وسط قرار دادن به صورت عمودی",
|
"centerVertically": "وسط قرار دادن به صورت عمودی",
|
||||||
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
||||||
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
||||||
"distributeVertically": "توزیع کردن به صورت عمودی"
|
"distributeVertically": "توزیع کردن به صورت عمودی",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "پاکسازی بوم نقاشی",
|
"clearReset": "پاکسازی بوم نقاشی",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "ویرایش",
|
"edit": "ویرایش",
|
||||||
"undo": "بازگرد",
|
"undo": "بازگرد",
|
||||||
"redo": "از سر",
|
"redo": "از سر",
|
||||||
"roomDialog": "همکاری آنلاین را شروع کنید",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "ایجاد یک اتاق جدید",
|
"createNewRoom": "ایجاد یک اتاق جدید",
|
||||||
"fullScreen": "تمامصفحه",
|
"fullScreen": "تمامصفحه",
|
||||||
"darkMode": "حالت تیره",
|
"darkMode": "حالت تیره",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
||||||
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
||||||
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "خطایی در بارگذاری کتابخانه ثالث وجود داشت.",
|
"errorLoadingLibrary": "خطایی در بارگذاری کتابخانه ثالث وجود داشت.",
|
||||||
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
||||||
"imageDoesNotContainScene": "وارد کردن تصویر در این لحظه امکان پذیر نمی باشد.\nآیا مایل به وارد کردن یک صحنه هستید؟ این تصویر به نظر می رسد که فاقد هرگونه اطلاعاتی مربوط به صحنه باشد. آیا این گزینه را در زمان وارد کردن تصویر فعال کرده اید؟",
|
"imageDoesNotContainScene": "وارد کردن تصویر در این لحظه امکان پذیر نمی باشد.\nآیا مایل به وارد کردن یک صحنه هستید؟ این تصویر به نظر می رسد که فاقد هرگونه اطلاعاتی مربوط به صحنه باشد. آیا این گزینه را در زمان وارد کردن تصویر فعال کرده اید؟",
|
||||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد"
|
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "گزینش",
|
"selection": "گزینش",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "حافظه",
|
"storage": "حافظه",
|
||||||
"title": "آمار برای نردها",
|
"title": "آمار برای نردها",
|
||||||
"total": "مجموع",
|
"total": "مجموع",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
"width": "عرض"
|
"width": "عرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "کپی سبک.",
|
"copyStyles": "کپی سبک.",
|
||||||
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG."
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG.",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Tasot",
|
"layers": "Tasot",
|
||||||
"actions": "Toiminnot",
|
"actions": "Toiminnot",
|
||||||
"language": "Kieli",
|
"language": "Kieli",
|
||||||
"createRoom": "Jaa yhteistyöistunto",
|
"liveCollaboration": "Live-yhteistyö",
|
||||||
"duplicateSelection": "Monista",
|
"duplicateSelection": "Monista",
|
||||||
"untitled": "Nimetön",
|
"untitled": "Nimetön",
|
||||||
"name": "Nimi",
|
"name": "Nimi",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Ryhmitä valinta",
|
"group": "Ryhmitä valinta",
|
||||||
"ungroup": "Pura valittu ryhmä",
|
"ungroup": "Pura valittu ryhmä",
|
||||||
"collaborators": "Yhteistyökumppanit",
|
"collaborators": "Yhteistyökumppanit",
|
||||||
"gridMode": "Ruudukkotila",
|
"showGrid": "Näytä ruudukko",
|
||||||
"addToLibrary": "Lisää kirjastoon",
|
"addToLibrary": "Lisää kirjastoon",
|
||||||
"removeFromLibrary": "Poista kirjastosta",
|
"removeFromLibrary": "Poista kirjastosta",
|
||||||
"libraryLoadingMessage": "Ladataan kirjastoa...",
|
"libraryLoadingMessage": "Ladataan kirjastoa…",
|
||||||
"libraries": "Selaa kirjastoja",
|
"libraries": "Selaa kirjastoja",
|
||||||
"loadingScene": "Ladataan työtä...",
|
"loadingScene": "Ladataan työtä…",
|
||||||
"align": "Tasaa",
|
"align": "Tasaa",
|
||||||
"alignTop": "Tasaa ylös",
|
"alignTop": "Tasaa ylös",
|
||||||
"alignBottom": "Tasaa alas",
|
"alignBottom": "Tasaa alas",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Keskitä pystysuunnassa",
|
"centerVertically": "Keskitä pystysuunnassa",
|
||||||
"centerHorizontally": "Keskitä vaakasuunnassa",
|
"centerHorizontally": "Keskitä vaakasuunnassa",
|
||||||
"distributeHorizontally": "Jaa vaakasuunnassa",
|
"distributeHorizontally": "Jaa vaakasuunnassa",
|
||||||
"distributeVertically": "Jaa pystysuunnassa"
|
"distributeVertically": "Jaa pystysuunnassa",
|
||||||
|
"viewMode": "Katselutila"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tyhjennä piirtoalue",
|
"clearReset": "Tyhjennä piirtoalue",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Muokkaa",
|
"edit": "Muokkaa",
|
||||||
"undo": "Kumoa",
|
"undo": "Kumoa",
|
||||||
"redo": "Tee uudelleen",
|
"redo": "Tee uudelleen",
|
||||||
"roomDialog": "Aloita live-yhteistyö",
|
"resetLibrary": "Tyhjennä kirjasto",
|
||||||
"createNewRoom": "Luo huone",
|
"createNewRoom": "Luo huone",
|
||||||
"fullScreen": "Koko näyttö",
|
"fullScreen": "Koko näyttö",
|
||||||
"darkMode": "Tumma tila",
|
"darkMode": "Tumma tila",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Salauksen purkaminen epäonnistui.",
|
"decryptFailed": "Salauksen purkaminen epäonnistui.",
|
||||||
"uploadedSecurly": "Lähetys on turvattu päästä päähän salauksella. Excalidrawin palvelin ja kolmannet osapuolet eivät voi lukea sisältöä.",
|
"uploadedSecurly": "Lähetys on turvattu päästä päähän salauksella. Excalidrawin palvelin ja kolmannet osapuolet eivät voi lukea sisältöä.",
|
||||||
"loadSceneOverridePrompt": "Ulkopuolisen piirroksen lataaminen korvaa nykyisen sisältösi. Haluatko jatkaa?",
|
"loadSceneOverridePrompt": "Ulkopuolisen piirroksen lataaminen korvaa nykyisen sisältösi. Haluatko jatkaa?",
|
||||||
|
"collabStopOverridePrompt": "Istunnon lopettaminen korvaa aiemman, paikallisesti tallennetun piirustuksen. Oletko varma?\n\n(Jos haluat pitää paikallisen piirustuksen, sulje selaimen välilehti sen sijaan.)",
|
||||||
"errorLoadingLibrary": "Kolmannen osapuolen kirjastoa ladattaessa tapahtui virhe.",
|
"errorLoadingLibrary": "Kolmannen osapuolen kirjastoa ladattaessa tapahtui virhe.",
|
||||||
"confirmAddLibrary": "Tämä lisää {{numShapes}} muotoa kirjastoosi. Oletko varma?",
|
"confirmAddLibrary": "Tämä lisää {{numShapes}} muotoa kirjastoosi. Oletko varma?",
|
||||||
"imageDoesNotContainScene": "Kuvien lisääminen ei ole tällä hetkellä mahdollista.\n\nHaluatko tuoda piirroksen? Tämä kuva ei näytä sisältävän tarvittavia tietoja. Oletko ottanut piirrostietojen tallennuksen käyttöön viennin aikana?",
|
"imageDoesNotContainScene": "Kuvien lisääminen ei ole tällä hetkellä mahdollista.\n\nHaluatko tuoda piirroksen? Tämä kuva ei näytä sisältävän tarvittavia tietoja. Oletko ottanut piirrostietojen tallennuksen käyttöön viennin aikana?",
|
||||||
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta"
|
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta",
|
||||||
|
"resetLibrary": "Tämä tyhjentää kirjastosi. Oletko varma?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Tallennustila",
|
"storage": "Tallennustila",
|
||||||
"title": "Nörttien tilastot",
|
"title": "Nörttien tilastot",
|
||||||
"total": "Yhteensä",
|
"total": "Yhteensä",
|
||||||
|
"version": "Versio",
|
||||||
|
"versionCopy": "Klikkaa kopioidaksesi",
|
||||||
|
"versionNotAvailable": "Versio ei saatavilla",
|
||||||
"width": "Leveys"
|
"width": "Leveys"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Tyylit kopioitu.",
|
"copyStyles": "Tyylit kopioitu.",
|
||||||
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona."
|
"copyToClipboard": "Kopioitu leikepöydälle.",
|
||||||
|
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona.",
|
||||||
|
"fileSaved": "Tiedosto tallennettu.",
|
||||||
|
"fileSavedToFilename": "Tallennettu kohteeseen {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Calques",
|
"layers": "Calques",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"createRoom": "Partager une session de collaboration en direct",
|
"liveCollaboration": "Collaboration en direct",
|
||||||
"duplicateSelection": "Dupliquer",
|
"duplicateSelection": "Dupliquer",
|
||||||
"untitled": "Sans-titre",
|
"untitled": "Sans-titre",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Grouper la sélection",
|
"group": "Grouper la sélection",
|
||||||
"ungroup": "Dégrouper la sélection",
|
"ungroup": "Dégrouper la sélection",
|
||||||
"collaborators": "Collaborateurs",
|
"collaborators": "Collaborateurs",
|
||||||
"gridMode": "Mode grille",
|
"showGrid": "Afficher la grille",
|
||||||
"addToLibrary": "Ajouter à la bibliothèque",
|
"addToLibrary": "Ajouter à la bibliothèque",
|
||||||
"removeFromLibrary": "Supprimer de la bibliothèque",
|
"removeFromLibrary": "Supprimer de la bibliothèque",
|
||||||
"libraryLoadingMessage": "Chargement de la bibliothèque...",
|
"libraryLoadingMessage": "Chargement de la bibliothèque…",
|
||||||
"libraries": "Parcourir les bibliothèques",
|
"libraries": "Parcourir les bibliothèques",
|
||||||
"loadingScene": "Chargement de la scène...",
|
"loadingScene": "Chargement de la scène…",
|
||||||
"align": "Aligner",
|
"align": "Aligner",
|
||||||
"alignTop": "Aligner en haut",
|
"alignTop": "Aligner en haut",
|
||||||
"alignBottom": "Aligner en bas",
|
"alignBottom": "Aligner en bas",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centrer verticalement",
|
"centerVertically": "Centrer verticalement",
|
||||||
"centerHorizontally": "Centrer horizontalement",
|
"centerHorizontally": "Centrer horizontalement",
|
||||||
"distributeHorizontally": "Distribuer horizontalement",
|
"distributeHorizontally": "Distribuer horizontalement",
|
||||||
"distributeVertically": "Distribuer verticalement"
|
"distributeVertically": "Distribuer verticalement",
|
||||||
|
"viewMode": "Mode présentation"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Réinitialiser le canevas",
|
"clearReset": "Réinitialiser le canevas",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
"undo": "Annuler",
|
"undo": "Annuler",
|
||||||
"redo": "Rétablir",
|
"redo": "Rétablir",
|
||||||
"roomDialog": "Démarrer la collaboration en direct",
|
"resetLibrary": "Réinitialiser la bibliothèque",
|
||||||
"createNewRoom": "Créer une nouvelle salle",
|
"createNewRoom": "Créer une nouvelle salle",
|
||||||
"fullScreen": "Plein écran",
|
"fullScreen": "Plein écran",
|
||||||
"darkMode": "Mode sombre",
|
"darkMode": "Mode sombre",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Les données n'ont pas pu être déchiffrées.",
|
"decryptFailed": "Les données n'ont pas pu être déchiffrées.",
|
||||||
"uploadedSecurly": "Le téléchargement a été sécurisé avec un chiffrement de bout en bout, ce qui signifie que ni Excalidraw ni personne d'autre ne peut en lire le contenu.",
|
"uploadedSecurly": "Le téléchargement a été sécurisé avec un chiffrement de bout en bout, ce qui signifie que ni Excalidraw ni personne d'autre ne peut en lire le contenu.",
|
||||||
"loadSceneOverridePrompt": "Le chargement d'un dessin externe remplacera votre contenu actuel. Souhaitez-vous continuer ?",
|
"loadSceneOverridePrompt": "Le chargement d'un dessin externe remplacera votre contenu actuel. Souhaitez-vous continuer ?",
|
||||||
|
"collabStopOverridePrompt": "Arrêter la session écrasera votre précédent dessin stocké localement. Êtes-vous sûr·e ?\n\n(Si vous voulez garder votre dessin local, fermez simplement l'onglet du navigateur à la place.)",
|
||||||
"errorLoadingLibrary": "Une erreur s'est produite lors du chargement de la bibliothèque tierce.",
|
"errorLoadingLibrary": "Une erreur s'est produite lors du chargement de la bibliothèque tierce.",
|
||||||
"confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr·e ?",
|
"confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr·e ?",
|
||||||
"imageDoesNotContainScene": "L'importation d'images n'est pas prise en charge pour le moment.\n\nVouliez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?",
|
"imageDoesNotContainScene": "L'importation d'images n'est pas prise en charge pour le moment.\n\nVouliez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?",
|
||||||
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image"
|
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image",
|
||||||
|
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Sélection",
|
"selection": "Sélection",
|
||||||
@@ -229,14 +232,20 @@
|
|||||||
"elements": "Éléments",
|
"elements": "Éléments",
|
||||||
"height": "Hauteur",
|
"height": "Hauteur",
|
||||||
"scene": "Scène",
|
"scene": "Scène",
|
||||||
"selected": "Sélectionné",
|
"selected": "Sélection",
|
||||||
"storage": "Stockage",
|
"storage": "Stockage",
|
||||||
"title": "Stats pour les nerds",
|
"title": "Stats pour les nerds",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"version": "Version",
|
||||||
|
"versionCopy": "Cliquer pour copier",
|
||||||
|
"versionNotAvailable": "Version non disponible",
|
||||||
"width": "Largeur"
|
"width": "Largeur"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Styles copiés.",
|
"copyStyles": "Styles copiés.",
|
||||||
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG."
|
"copyToClipboard": "Copié vers le presse-papiers.",
|
||||||
|
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG.",
|
||||||
|
"fileSaved": "Fichier enregistré.",
|
||||||
|
"fileSavedToFilename": "Enregistré sous {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "שכבות",
|
"layers": "שכבות",
|
||||||
"actions": "פעולות",
|
"actions": "פעולות",
|
||||||
"language": "שפה",
|
"language": "שפה",
|
||||||
"createRoom": "התחל שיתוף פעולה חי",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "שכפל",
|
"duplicateSelection": "שכפל",
|
||||||
"untitled": "ללא כותרת",
|
"untitled": "ללא כותרת",
|
||||||
"name": "שם",
|
"name": "שם",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "אחד לקבוצה",
|
"group": "אחד לקבוצה",
|
||||||
"ungroup": "פרק קבוצה",
|
"ungroup": "פרק קבוצה",
|
||||||
"collaborators": "שותפים",
|
"collaborators": "שותפים",
|
||||||
"gridMode": "מצב רשת",
|
"showGrid": "",
|
||||||
"addToLibrary": "הוסף לספריה",
|
"addToLibrary": "הוסף לספריה",
|
||||||
"removeFromLibrary": "הסר מספריה",
|
"removeFromLibrary": "הסר מספריה",
|
||||||
"libraryLoadingMessage": "טוען ספריה...",
|
"libraryLoadingMessage": "טוען ספריה…",
|
||||||
"libraries": "דפדף בספריות",
|
"libraries": "דפדף בספריות",
|
||||||
"loadingScene": "טוען תצוגה...",
|
"loadingScene": "טוען תצוגה…",
|
||||||
"align": "יישר",
|
"align": "יישר",
|
||||||
"alignTop": "יישר למעלה",
|
"alignTop": "יישר למעלה",
|
||||||
"alignBottom": "יישר למטה",
|
"alignBottom": "יישר למטה",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "מרכז אנכית",
|
"centerVertically": "מרכז אנכית",
|
||||||
"centerHorizontally": "מרכז אופקית",
|
"centerHorizontally": "מרכז אופקית",
|
||||||
"distributeHorizontally": "חלוקה אופקית",
|
"distributeHorizontally": "חלוקה אופקית",
|
||||||
"distributeVertically": "חלוקה אנכית"
|
"distributeVertically": "חלוקה אנכית",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "אפס את הלוח",
|
"clearReset": "אפס את הלוח",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "ערוך",
|
"edit": "ערוך",
|
||||||
"undo": "בטל",
|
"undo": "בטל",
|
||||||
"redo": "בצע מחדש",
|
"redo": "בצע מחדש",
|
||||||
"roomDialog": "התחל שיתוף חי",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "צור חדר",
|
"createNewRoom": "צור חדר",
|
||||||
"fullScreen": "מסך מלא",
|
"fullScreen": "מסך מלא",
|
||||||
"darkMode": "מצב כהה",
|
"darkMode": "מצב כהה",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "לא ניתן לפענח מידע.",
|
"decryptFailed": "לא ניתן לפענח מידע.",
|
||||||
"uploadedSecurly": "ההעלאה הוצפנה מקצה לקצה, ולכן שרת Excalidraw וצד שלישי לא יכולים לקרוא את התוכן.",
|
"uploadedSecurly": "ההעלאה הוצפנה מקצה לקצה, ולכן שרת Excalidraw וצד שלישי לא יכולים לקרוא את התוכן.",
|
||||||
"loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?",
|
"loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "קרתה שגיאה בטעינת הספריה החיצונית.",
|
"errorLoadingLibrary": "קרתה שגיאה בטעינת הספריה החיצונית.",
|
||||||
"confirmAddLibrary": "הפעולה תוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?",
|
"confirmAddLibrary": "הפעולה תוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?",
|
||||||
"imageDoesNotContainScene": "אין תמיכה בייבוא תמונות כעת.\n\nהאם אתה רוצה לייבא תצוגה? התמונה הזאת אינה מכילה מידע על תצוגה. האם הפעלת את האפשרות הזאת בזמן הוצאת המידע?",
|
"imageDoesNotContainScene": "אין תמיכה בייבוא תמונות כעת.\n\nהאם אתה רוצה לייבא תצוגה? התמונה הזאת אינה מכילה מידע על תצוגה. האם הפעלת את האפשרות הזאת בזמן הוצאת המידע?",
|
||||||
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה"
|
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "בחירה",
|
"selection": "בחירה",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "אחסון",
|
"storage": "אחסון",
|
||||||
"title": "סטטיסטיקות לחנונים",
|
"title": "סטטיסטיקות לחנונים",
|
||||||
"total": "סה״כ",
|
"total": "סה״כ",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
"width": "רוחב"
|
"width": "רוחב"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-25
@@ -68,7 +68,7 @@
|
|||||||
"layers": "परतें",
|
"layers": "परतें",
|
||||||
"actions": "कार्रवाई",
|
"actions": "कार्रवाई",
|
||||||
"language": "भाषा",
|
"language": "भाषा",
|
||||||
"createRoom": "अधिवेशन",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "डुप्लिकेट",
|
"duplicateSelection": "डुप्लिकेट",
|
||||||
"untitled": "अशीर्षित",
|
"untitled": "अशीर्षित",
|
||||||
"name": "नाम",
|
"name": "नाम",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"group": "समूह चयन",
|
"group": "समूह चयन",
|
||||||
"ungroup": "समूह चयन असमूहीकृत करें",
|
"ungroup": "समूह चयन असमूहीकृत करें",
|
||||||
"collaborators": "सहयोगी",
|
"collaborators": "सहयोगी",
|
||||||
"gridMode": "ग्रिड मॉड",
|
"showGrid": "",
|
||||||
"addToLibrary": "लाइब्रेरी से जोड़ें",
|
"addToLibrary": "लाइब्रेरी से जोड़ें",
|
||||||
"removeFromLibrary": "लाइब्रेरी से निकालें",
|
"removeFromLibrary": "लाइब्रेरी से निकालें",
|
||||||
"libraryLoadingMessage": "लाइब्रेरी खुल रही है",
|
"libraryLoadingMessage": "लाइब्रेरी खुल रही है",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "लंबवत केन्द्रित",
|
"centerVertically": "लंबवत केन्द्रित",
|
||||||
"centerHorizontally": "क्षैतिज केन्द्रित",
|
"centerHorizontally": "क्षैतिज केन्द्रित",
|
||||||
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
||||||
"distributeVertically": "खड़ी रूप से वितरित करें"
|
"distributeVertically": "खड़ी रूप से वितरित करें",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "कैनवास रीसेट करें",
|
"clearReset": "कैनवास रीसेट करें",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "संशोधन करें",
|
"edit": "संशोधन करें",
|
||||||
"undo": "पूर्ववत् करें",
|
"undo": "पूर्ववत् करें",
|
||||||
"redo": "फिर से करें",
|
"redo": "फिर से करें",
|
||||||
"roomDialog": "लाइव सहयोग शुरू करें",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "एक नया कमरा बनाएं",
|
"createNewRoom": "एक नया कमरा बनाएं",
|
||||||
"fullScreen": "पूरी स्क्रीन",
|
"fullScreen": "पूरी स्क्रीन",
|
||||||
"darkMode": "डार्क मोड",
|
"darkMode": "डार्क मोड",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "डेटा को डिक्रिप्ट नहीं किया जा सका।",
|
"decryptFailed": "डेटा को डिक्रिप्ट नहीं किया जा सका।",
|
||||||
"uploadedSecurly": "अपलोड को एंड-टू-एंड एन्क्रिप्शन के साथ सुरक्षित किया गया है, जिसका मतलब है कि एक्सक्लूसिव सर्वर और थर्ड पार्टी कंटेंट नहीं पढ़ सकते हैं।",
|
"uploadedSecurly": "अपलोड को एंड-टू-एंड एन्क्रिप्शन के साथ सुरक्षित किया गया है, जिसका मतलब है कि एक्सक्लूसिव सर्वर और थर्ड पार्टी कंटेंट नहीं पढ़ सकते हैं।",
|
||||||
"loadSceneOverridePrompt": "लोड हो रहा है बाहरी ड्राइंग आपके मौजूदा सामग्री को बदल देगा। क्या आप जारी रखना चाहते हैं?",
|
"loadSceneOverridePrompt": "लोड हो रहा है बाहरी ड्राइंग आपके मौजूदा सामग्री को बदल देगा। क्या आप जारी रखना चाहते हैं?",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "लाइब्रेरी लोड करने में त्रुटि",
|
"errorLoadingLibrary": "लाइब्रेरी लोड करने में त्रुटि",
|
||||||
"confirmAddLibrary": "लाइब्रेरी जोड़ें पुष्टि करें आकार संख्या",
|
"confirmAddLibrary": "लाइब्रेरी जोड़ें पुष्टि करें आकार संख्या",
|
||||||
"imageDoesNotContainScene": "दृश्य में छवि नहीं है",
|
"imageDoesNotContainScene": "दृश्य में छवि नहीं है",
|
||||||
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है"
|
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "चयन",
|
"selection": "चयन",
|
||||||
@@ -200,25 +203,25 @@
|
|||||||
"title": "गलती"
|
"title": "गलती"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "हमारा ब्लॉग पढे",
|
||||||
"click": "",
|
"click": "क्लिक करें",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "वक्र तीर",
|
||||||
"curvedLine": "",
|
"curvedLine": "वक्र रेखा",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
"drag": "",
|
"drag": "खींचें",
|
||||||
"editor": "",
|
"editor": "संपादक",
|
||||||
"github": "",
|
"github": "मुद्दा मिला? प्रस्तुत करें",
|
||||||
"howto": "",
|
"howto": "हमारे गाइड का पालन करें",
|
||||||
"or": "",
|
"or": "या",
|
||||||
"preventBinding": "",
|
"preventBinding": "तीर बंधन रोकें",
|
||||||
"shapes": "",
|
"shapes": "आकृतियाँ",
|
||||||
"shortcuts": "",
|
"shortcuts": "कीबोर्ड के शॉर्टकट्स",
|
||||||
"textFinish": "",
|
"textFinish": "संपादन समाप्त करें (पाठ)",
|
||||||
"textNewLine": "",
|
"textNewLine": "नई पंक्ति जोड़ें (पाठ)",
|
||||||
"title": "",
|
"title": "मदद",
|
||||||
"view": "",
|
"view": "दृश्य",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "सभी तत्वों को फिट करने के लिए ज़ूम करें",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "चयन तक ज़ूम करे"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
|
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "संग्रह",
|
"storage": "संग्रह",
|
||||||
"title": "बेवकूफ के लिए आँकड़े",
|
"title": "बेवकूफ के लिए आँकड़े",
|
||||||
"total": "कुल",
|
"total": "कुल",
|
||||||
|
"version": "संस्करण",
|
||||||
|
"versionCopy": "काॅपी करने के लिए क्लिक करें",
|
||||||
|
"versionNotAvailable": "संस्करण उपलब्ध नहीं है",
|
||||||
"width": "चौड़ाई"
|
"width": "चौड़ाई"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "काॅपी कीए स्टाइल",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "क्लिपबोर्ड में कॉपी कीए",
|
||||||
|
"copyToClipboardAsPng": "क्लिपबोर्ड में PNG के रूप में कॉपी किए",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Rétegek",
|
"layers": "Rétegek",
|
||||||
"actions": "Műveletek",
|
"actions": "Műveletek",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"createRoom": "Élő együttmüködés megosztása",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "Duplikálás",
|
"duplicateSelection": "Duplikálás",
|
||||||
"untitled": "Névtelen",
|
"untitled": "Névtelen",
|
||||||
"name": "Név",
|
"name": "Név",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Csoportosítás",
|
"group": "Csoportosítás",
|
||||||
"ungroup": "Csoportbontás",
|
"ungroup": "Csoportbontás",
|
||||||
"collaborators": "Közreműködők",
|
"collaborators": "Közreműködők",
|
||||||
"gridMode": "Hálómód",
|
"showGrid": "",
|
||||||
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
||||||
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
||||||
"libraryLoadingMessage": "Könyvtár betöltése...",
|
"libraryLoadingMessage": "Könyvtár betöltése…",
|
||||||
"libraries": "Könyvtárak böngészése",
|
"libraries": "Könyvtárak böngészése",
|
||||||
"loadingScene": "Jelenet betöltése...",
|
"loadingScene": "Jelenet betöltése…",
|
||||||
"align": "Igazítás",
|
"align": "Igazítás",
|
||||||
"alignTop": "Felülre igazítás",
|
"alignTop": "Felülre igazítás",
|
||||||
"alignBottom": "Alulra igazítás",
|
"alignBottom": "Alulra igazítás",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Függőlegesen középre igazított",
|
"centerVertically": "Függőlegesen középre igazított",
|
||||||
"centerHorizontally": "Vízszintesen középre igazított",
|
"centerHorizontally": "Vízszintesen középre igazított",
|
||||||
"distributeHorizontally": "Vízszintes elosztás",
|
"distributeHorizontally": "Vízszintes elosztás",
|
||||||
"distributeVertically": "Függőleges elosztás"
|
"distributeVertically": "Függőleges elosztás",
|
||||||
|
"viewMode": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Vászon törlése",
|
"clearReset": "Vászon törlése",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Szerkesztés",
|
"edit": "Szerkesztés",
|
||||||
"undo": "Vissza",
|
"undo": "Vissza",
|
||||||
"redo": "Újra",
|
"redo": "Újra",
|
||||||
"roomDialog": "Élő együttműködés indítása",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "Új szoba létrehozása",
|
"createNewRoom": "Új szoba létrehozása",
|
||||||
"fullScreen": "Teljes képernyő",
|
"fullScreen": "Teljes képernyő",
|
||||||
"darkMode": "Sötét mód",
|
"darkMode": "Sötét mód",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.",
|
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.",
|
||||||
"uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.",
|
"uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.",
|
||||||
"loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?",
|
"loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
"errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
|
"errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
|
||||||
"confirmAddLibrary": "Ez a művelet {{numShapes}} formát fog hozzáadni a könyvtáradhoz. Biztos vagy benne?",
|
"confirmAddLibrary": "Ez a művelet {{numShapes}} formát fog hozzáadni a könyvtáradhoz. Biztos vagy benne?",
|
||||||
"imageDoesNotContainScene": "Képek importálása egyelőre nem támogatott.\n\nEgy jelenetet szeretnél betölteni? Úgy tűnik ez a kép fájl nem tartalmazza a szükséges adatokat. Exportáláskor ezt egy külön opcióval lehet beállítani.",
|
"imageDoesNotContainScene": "Képek importálása egyelőre nem támogatott.\n\nEgy jelenetet szeretnél betölteni? Úgy tűnik ez a kép fájl nem tartalmazza a szükséges adatokat. Exportáláskor ezt egy külön opcióval lehet beállítani.",
|
||||||
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból"
|
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Kijelölés",
|
"selection": "Kijelölés",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Tárhely",
|
"storage": "Tárhely",
|
||||||
"title": "Statisztikák",
|
"title": "Statisztikák",
|
||||||
"total": "Összesen",
|
"total": "Összesen",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
"width": "Szélesség"
|
"width": "Szélesség"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Lapisan",
|
"layers": "Lapisan",
|
||||||
"actions": "Aksi",
|
"actions": "Aksi",
|
||||||
"language": "Bahasa",
|
"language": "Bahasa",
|
||||||
"createRoom": "Bagikan sesi kolaborasi langsung",
|
"liveCollaboration": "",
|
||||||
"duplicateSelection": "Duplikat",
|
"duplicateSelection": "Duplikat",
|
||||||
"untitled": "Tanpa judul",
|
"untitled": "Tanpa judul",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Kelompokan pilihan",
|
"group": "Kelompokan pilihan",
|
||||||
"ungroup": "Pisahkan pilihan",
|
"ungroup": "Pisahkan pilihan",
|
||||||
"collaborators": "Kolaborator",
|
"collaborators": "Kolaborator",
|
||||||
"gridMode": "Mode grid",
|
"showGrid": "Tampilkan grid",
|
||||||
"addToLibrary": "Tambahkan ke pustaka",
|
"addToLibrary": "Tambahkan ke pustaka",
|
||||||
"removeFromLibrary": "Hapus dari pustaka",
|
"removeFromLibrary": "Hapus dari pustaka",
|
||||||
"libraryLoadingMessage": "Memuat pustaka...",
|
"libraryLoadingMessage": "Memuat pustaka…",
|
||||||
"libraries": "Telusur pustaka",
|
"libraries": "Telusur pustaka",
|
||||||
"loadingScene": "Memuat pemandangan...",
|
"loadingScene": "Memuat pemandangan…",
|
||||||
"align": "Perataan",
|
"align": "Perataan",
|
||||||
"alignTop": "Rata atas",
|
"alignTop": "Rata atas",
|
||||||
"alignBottom": "Rata bawah",
|
"alignBottom": "Rata bawah",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Pusatkan secara vertikal",
|
"centerVertically": "Pusatkan secara vertikal",
|
||||||
"centerHorizontally": "Pusatkan secara horizontal",
|
"centerHorizontally": "Pusatkan secara horizontal",
|
||||||
"distributeHorizontally": "Distribusikan horizontal",
|
"distributeHorizontally": "Distribusikan horizontal",
|
||||||
"distributeVertically": "Distribusikan vertikal"
|
"distributeVertically": "Distribusikan vertikal",
|
||||||
|
"viewMode": "Mode tampilan"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Setel Ulang Kanvas",
|
"clearReset": "Setel Ulang Kanvas",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"undo": "Urungkan",
|
"undo": "Urungkan",
|
||||||
"redo": "Ulangi",
|
"redo": "Ulangi",
|
||||||
"roomDialog": "Mulai kolaborasi langsung",
|
"resetLibrary": "",
|
||||||
"createNewRoom": "Buat ruang baru",
|
"createNewRoom": "Buat ruang baru",
|
||||||
"fullScreen": "Layar penuh",
|
"fullScreen": "Layar penuh",
|
||||||
"darkMode": "Mode gelap",
|
"darkMode": "Mode gelap",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Tidak dapat mengdekripsi data.",
|
"decryptFailed": "Tidak dapat mengdekripsi data.",
|
||||||
"uploadedSecurly": "Pengunggahan ini telah diamankan menggunakan enkripsi end-to-end, artinya server Excalidraw dan pihak ketiga tidak data membaca nya",
|
"uploadedSecurly": "Pengunggahan ini telah diamankan menggunakan enkripsi end-to-end, artinya server Excalidraw dan pihak ketiga tidak data membaca nya",
|
||||||
"loadSceneOverridePrompt": "Memuat gambar external akan mengganti konten Anda yang ada. Apakah Anda ingin melanjutkan?",
|
"loadSceneOverridePrompt": "Memuat gambar external akan mengganti konten Anda yang ada. Apakah Anda ingin melanjutkan?",
|
||||||
|
"collabStopOverridePrompt": "Menghentikan sesi akan menimpa gambar Anda yang tersimpan secara lokal. Anda yakin?\n\n(Jika Anda ingin menyimpan gambar lokal Anda, gantinya cukup tutup tab browser.)",
|
||||||
"errorLoadingLibrary": "Terdapat kesalahan dalam memuat pustaka pihak ketiga.",
|
"errorLoadingLibrary": "Terdapat kesalahan dalam memuat pustaka pihak ketiga.",
|
||||||
"confirmAddLibrary": "Ini akan menambahkan {{numShapes}} bentuk ke pustaka Anda. Anda yakin?",
|
"confirmAddLibrary": "Ini akan menambahkan {{numShapes}} bentuk ke pustaka Anda. Anda yakin?",
|
||||||
"imageDoesNotContainScene": "Mengimpor gambar tidak didukung saat ini.\n\nApakah Anda ingin impor pemandangan? Gambar ini tidak berisi data pemandangan. Sudah ka Anda aktifkan ini ketika ekspor?",
|
"imageDoesNotContainScene": "Mengimpor gambar tidak didukung saat ini.\n\nApakah Anda ingin impor pemandangan? Gambar ini tidak berisi data pemandangan. Sudah ka Anda aktifkan ini ketika ekspor?",
|
||||||
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini"
|
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
|
||||||
|
"resetLibrary": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Pilihan",
|
"selection": "Pilihan",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Penyimpanan",
|
"storage": "Penyimpanan",
|
||||||
"title": "Statistik untuk nerd",
|
"title": "Statistik untuk nerd",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"version": "Versi",
|
||||||
|
"versionCopy": "Klik untuk salin",
|
||||||
|
"versionNotAvailable": "Versi tidak tersedia",
|
||||||
"width": "Lebar"
|
"width": "Lebar"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Gaya tersalin.",
|
"copyStyles": "Gaya tersalin.",
|
||||||
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG."
|
"copyToClipboard": "Tersalin ke papan klip.",
|
||||||
|
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG.",
|
||||||
|
"fileSaved": "File tersimpan.",
|
||||||
|
"fileSavedToFilename": "Disimpan ke {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -68,7 +68,7 @@
|
|||||||
"layers": "Livelli",
|
"layers": "Livelli",
|
||||||
"actions": "Azioni",
|
"actions": "Azioni",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"createRoom": "Condividi una sessione di collaborazione in diretta",
|
"liveCollaboration": "Collaborazione live",
|
||||||
"duplicateSelection": "Duplica",
|
"duplicateSelection": "Duplica",
|
||||||
"untitled": "Senza titolo",
|
"untitled": "Senza titolo",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"group": "Crea gruppo da selezione",
|
"group": "Crea gruppo da selezione",
|
||||||
"ungroup": "Dividi gruppo da selezione",
|
"ungroup": "Dividi gruppo da selezione",
|
||||||
"collaborators": "Collaboratori",
|
"collaborators": "Collaboratori",
|
||||||
"gridMode": "Modalità griglia",
|
"showGrid": "Visualizza griglia",
|
||||||
"addToLibrary": "Aggiungi alla libreria",
|
"addToLibrary": "Aggiungi alla libreria",
|
||||||
"removeFromLibrary": "Rimuovi dalla libreria",
|
"removeFromLibrary": "Rimuovi dalla libreria",
|
||||||
"libraryLoadingMessage": "Caricamento della biblioteca...",
|
"libraryLoadingMessage": "Caricamento libreria…",
|
||||||
"libraries": "Sfoglia librerie",
|
"libraries": "Sfoglia librerie",
|
||||||
"loadingScene": "Caricamento della scena...",
|
"loadingScene": "Caricamento della scena…",
|
||||||
"align": "Allinea",
|
"align": "Allinea",
|
||||||
"alignTop": "Allinea in alto",
|
"alignTop": "Allinea in alto",
|
||||||
"alignBottom": "Allinea in basso",
|
"alignBottom": "Allinea in basso",
|
||||||
@@ -91,7 +91,8 @@
|
|||||||
"centerVertically": "Centra Verticalmente",
|
"centerVertically": "Centra Verticalmente",
|
||||||
"centerHorizontally": "Centra orizzontalmente",
|
"centerHorizontally": "Centra orizzontalmente",
|
||||||
"distributeHorizontally": "Distribuisci orizzontalmente",
|
"distributeHorizontally": "Distribuisci orizzontalmente",
|
||||||
"distributeVertically": "Distribuisci verticalmente"
|
"distributeVertically": "Distribuisci verticalmente",
|
||||||
|
"viewMode": "Modalità visualizzazione"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Svuota la tela",
|
"clearReset": "Svuota la tela",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"undo": "Annulla",
|
"undo": "Annulla",
|
||||||
"redo": "Ripeti",
|
"redo": "Ripeti",
|
||||||
"roomDialog": "Inizia collaborazione in diretta",
|
"resetLibrary": "Ripristina libreria",
|
||||||
"createNewRoom": "Crea nuova stanza",
|
"createNewRoom": "Crea nuova stanza",
|
||||||
"fullScreen": "Schermo intero",
|
"fullScreen": "Schermo intero",
|
||||||
"darkMode": "Tema scuro",
|
"darkMode": "Tema scuro",
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "Impossibile decriptare i dati.",
|
"decryptFailed": "Impossibile decriptare i dati.",
|
||||||
"uploadedSecurly": "L'upload è stato protetto con la crittografia end-to-end, il che significa che il server Excalidraw e terze parti non possono leggere il contenuto.",
|
"uploadedSecurly": "L'upload è stato protetto con la crittografia end-to-end, il che significa che il server Excalidraw e terze parti non possono leggere il contenuto.",
|
||||||
"loadSceneOverridePrompt": "Se carichi questo disegno esterno, sostituirà quello che hai. Vuoi continuare?",
|
"loadSceneOverridePrompt": "Se carichi questo disegno esterno, sostituirà quello che hai. Vuoi continuare?",
|
||||||
|
"collabStopOverridePrompt": "Interrompere la sessione sovrascriverà il precedente disegno memorizzato localmente. Sei sicuro?\n\n(Se vuoi mantenere il tuo disegno locale, chiudi semplicemente la scheda del browser.)",
|
||||||
"errorLoadingLibrary": "Si è verificato un errore nel caricamento della libreria di terze parti.",
|
"errorLoadingLibrary": "Si è verificato un errore nel caricamento della libreria di terze parti.",
|
||||||
"confirmAddLibrary": "Questo aggiungerà {{numShapes}} forma(e) alla tua libreria. Sei sicuro?",
|
"confirmAddLibrary": "Questo aggiungerà {{numShapes}} forma(e) alla tua libreria. Sei sicuro?",
|
||||||
"imageDoesNotContainScene": "L'importazione di immagini al momento non è supportata.\n\nVuoi importare una scena? Questa immagine non sembra contenere alcun dato di scena. Hai abilitato questa opzione durante l'esportazione?",
|
"imageDoesNotContainScene": "L'importazione di immagini al momento non è supportata.\n\nVuoi importare una scena? Questa immagine non sembra contenere alcun dato di scena. Hai abilitato questa opzione durante l'esportazione?",
|
||||||
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine"
|
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine",
|
||||||
|
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selezione",
|
"selection": "Selezione",
|
||||||
@@ -233,10 +236,16 @@
|
|||||||
"storage": "Memoria",
|
"storage": "Memoria",
|
||||||
"title": "Statistiche per nerd",
|
"title": "Statistiche per nerd",
|
||||||
"total": "Totale",
|
"total": "Totale",
|
||||||
|
"version": "Versione",
|
||||||
|
"versionCopy": "Clicca per copiare",
|
||||||
|
"versionNotAvailable": "Versione non disponibile",
|
||||||
"width": "Larghezza"
|
"width": "Larghezza"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Stili copiati.",
|
"copyStyles": "Stili copiati.",
|
||||||
"copyToClipboardAsPng": "Copiato negli appunti come PNG."
|
"copyToClipboard": "Copiato negli appunti.",
|
||||||
|
"copyToClipboardAsPng": "Copiato negli appunti come PNG.",
|
||||||
|
"fileSaved": "File salvato.",
|
||||||
|
"fileSavedToFilename": "Salvato in {filename}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-44
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "貼り付け",
|
"paste": "貼り付け",
|
||||||
"pasteCharts": "",
|
"pasteCharts": "チャートの貼り付け",
|
||||||
"selectAll": "すべて選択",
|
"selectAll": "すべて選択",
|
||||||
"multiSelect": "複数選択",
|
"multiSelect": "複数選択",
|
||||||
"moveCanvas": "キャンバスを移動",
|
"moveCanvas": "キャンバスを移動",
|
||||||
"cut": "",
|
"cut": "切り取り",
|
||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"copyAsPng": "PNGとしてクリップボードへコピー",
|
"copyAsPng": "PNGとしてクリップボードへコピー",
|
||||||
"copyAsSvg": "SVGとしてクリップボードへコピー",
|
"copyAsSvg": "SVGとしてクリップボードへコピー",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"fontSize": "フォントの大きさ",
|
"fontSize": "フォントの大きさ",
|
||||||
"fontFamily": "フォントの種類",
|
"fontFamily": "フォントの種類",
|
||||||
"onlySelected": "選択中のみ",
|
"onlySelected": "選択中のみ",
|
||||||
"withBackground": "",
|
"withBackground": "背景を含める",
|
||||||
"exportEmbedScene": "エクスポートされたファイルにシーンを埋め込みます",
|
"exportEmbedScene": "エクスポートされたファイルにシーンを埋め込みます",
|
||||||
"exportEmbedScene_details": "シーンデータはエクスポートされたPNG/SVGファイルに保存され、シーンを復元することができます。\nエクスポートされたファイルのサイズは増加します。",
|
"exportEmbedScene_details": "シーンデータはエクスポートされたPNG/SVGファイルに保存され、シーンを復元することができます。\nエクスポートされたファイルのサイズは増加します。",
|
||||||
"addWatermark": "\"Made with Excalidraw\"と表示",
|
"addWatermark": "\"Made with Excalidraw\"と表示",
|
||||||
@@ -68,21 +68,21 @@
|
|||||||
"layers": "レイヤー",
|
"layers": "レイヤー",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"createRoom": "共同編集セッションの共有",
|
"liveCollaboration": "ライブ連携",
|
||||||
"duplicateSelection": "複製",
|
"duplicateSelection": "複製",
|
||||||
"untitled": "",
|
"untitled": "無題",
|
||||||
"name": "名前",
|
"name": "名前",
|
||||||
"yourName": "あなたの名前",
|
"yourName": "あなたの名前",
|
||||||
"madeWithExcalidraw": "Excalidrawで作成",
|
"madeWithExcalidraw": "Excalidrawで作成",
|
||||||
"group": "図形のグループ化",
|
"group": "図形のグループ化",
|
||||||
"ungroup": "グループ化を解除",
|
"ungroup": "グループ化を解除",
|
||||||
"collaborators": "共同編集者",
|
"collaborators": "共同編集者",
|
||||||
"gridMode": "",
|
"showGrid": "グリッドを表示",
|
||||||
"addToLibrary": "ライブラリに追加",
|
"addToLibrary": "ライブラリに追加",
|
||||||
"removeFromLibrary": "ライブラリから削除",
|
"removeFromLibrary": "ライブラリから削除",
|
||||||
"libraryLoadingMessage": "ライブラリを読み込み中...",
|
"libraryLoadingMessage": "ライブラリを読み込み中…",
|
||||||
"libraries": "",
|
"libraries": "ライブラリを参照する",
|
||||||
"loadingScene": "シーンを読み込み中...",
|
"loadingScene": "シーンを読み込み中…",
|
||||||
"align": "整列",
|
"align": "整列",
|
||||||
"alignTop": "上揃え",
|
"alignTop": "上揃え",
|
||||||
"alignBottom": "下揃え",
|
"alignBottom": "下揃え",
|
||||||
@@ -90,8 +90,9 @@
|
|||||||
"alignRight": "右揃え",
|
"alignRight": "右揃え",
|
||||||
"centerVertically": "縦方向に中央揃え",
|
"centerVertically": "縦方向に中央揃え",
|
||||||
"centerHorizontally": "横方向に中央揃え",
|
"centerHorizontally": "横方向に中央揃え",
|
||||||
"distributeHorizontally": "",
|
"distributeHorizontally": "水平方向に分散配置",
|
||||||
"distributeVertically": ""
|
"distributeVertically": "垂直方向に分散配置",
|
||||||
|
"viewMode": "閲覧モード"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "キャンバスのリセット",
|
"clearReset": "キャンバスのリセット",
|
||||||
@@ -116,12 +117,12 @@
|
|||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
"undo": "元に戻す",
|
"undo": "元に戻す",
|
||||||
"redo": "やり直し",
|
"redo": "やり直し",
|
||||||
"roomDialog": "共同編集を開始する",
|
"resetLibrary": "ライブラリをリセット",
|
||||||
"createNewRoom": "新しい部屋を作成する",
|
"createNewRoom": "新しい部屋を作成する",
|
||||||
"fullScreen": "全画面表示",
|
"fullScreen": "全画面表示",
|
||||||
"darkMode": "ダークモード",
|
"darkMode": "ダークモード",
|
||||||
"lightMode": "ライトモード",
|
"lightMode": "ライトモード",
|
||||||
"zenMode": "",
|
"zenMode": "Zenモード",
|
||||||
"exitZenMode": "集中モードをやめる"
|
"exitZenMode": "集中モードをやめる"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
@@ -135,10 +136,12 @@
|
|||||||
"decryptFailed": "データを復号できませんでした。",
|
"decryptFailed": "データを復号できませんでした。",
|
||||||
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
|
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
|
||||||
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
|
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
|
||||||
|
"collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザタブを閉じてください。)",
|
||||||
"errorLoadingLibrary": "サードパーティライブラリの読み込み中にエラーが発生しました。",
|
"errorLoadingLibrary": "サードパーティライブラリの読み込み中にエラーが発生しました。",
|
||||||
"confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?",
|
"confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "現在、画像のインポートはサポートされていません。\n\nシーンをインポートしようとしましたか?この画像にはシーンデータが含まれていないようです。エクスポート中に有効にしていましたか?",
|
||||||
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした"
|
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした",
|
||||||
|
"resetLibrary": "ライブラリを消去します。本当によろしいですか?"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "選択",
|
"selection": "選択",
|
||||||
@@ -162,7 +165,7 @@
|
|||||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||||
"linearElementMulti": "最後のポイントをクリックするか、エスケープまたはEnterを押して終了します",
|
"linearElementMulti": "最後のポイントをクリックするか、エスケープまたはEnterを押して終了します",
|
||||||
"lockAngle": "",
|
"lockAngle": "SHIFTを押したままにすると、角度を制限することができます",
|
||||||
"resize": "サイズを変更中にSHIFTを押しすと比率を制御できます。Altを押すと中央からサイズを変更できます。",
|
"resize": "サイズを変更中にSHIFTを押しすと比率を制御できます。Altを押すと中央からサイズを変更できます。",
|
||||||
"rotate": "回転中にSHIFT キーを押すと角度を制限することができます",
|
"rotate": "回転中にSHIFT キーを押すと角度を制限することができます",
|
||||||
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
|
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
|
||||||
@@ -200,43 +203,49 @@
|
|||||||
"title": "エラー"
|
"title": "エラー"
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "公式ブログを読む",
|
||||||
"click": "",
|
"click": "クリック",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "カーブした矢印",
|
||||||
"curvedLine": "",
|
"curvedLine": "曲線",
|
||||||
"documentation": "",
|
"documentation": "ドキュメント",
|
||||||
"drag": "",
|
"drag": "ドラッグ",
|
||||||
"editor": "",
|
"editor": "エディタ",
|
||||||
"github": "",
|
"github": "不具合報告はこちら",
|
||||||
"howto": "",
|
"howto": "ヘルプ・マニュアル",
|
||||||
"or": "",
|
"or": "または",
|
||||||
"preventBinding": "",
|
"preventBinding": "矢印を結合しない",
|
||||||
"shapes": "",
|
"shapes": "図形",
|
||||||
"shortcuts": "",
|
"shortcuts": "キーボードショートカット",
|
||||||
"textFinish": "",
|
"textFinish": "編集を終了する (テキスト)",
|
||||||
"textNewLine": "",
|
"textNewLine": "新しい行を追加 (テキスト)",
|
||||||
"title": "",
|
"title": "ヘルプ",
|
||||||
"view": "",
|
"view": "表示",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "すべての要素が収まるようにズーム",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "選択要素にズーム"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
|
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "",
|
"angle": "角度",
|
||||||
"element": "",
|
"element": "要素",
|
||||||
"elements": "",
|
"elements": "要素",
|
||||||
"height": "高さ",
|
"height": "高さ",
|
||||||
"scene": "",
|
"scene": "シーン",
|
||||||
"selected": "",
|
"selected": "選択済み",
|
||||||
"storage": "",
|
"storage": "ストレージ",
|
||||||
"title": "",
|
"title": "マニア向け統計情報",
|
||||||
"total": "合計",
|
"total": "合計",
|
||||||
|
"version": "バージョン",
|
||||||
|
"versionCopy": "クリックしてコピー",
|
||||||
|
"versionNotAvailable": "利用できないバージョン",
|
||||||
"width": "幅"
|
"width": "幅"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "スタイルをコピー",
|
||||||
"copyToClipboardAsPng": ""
|
"copyToClipboard": "クリップボードにコピー",
|
||||||
|
"copyToClipboardAsPng": "PNG形式でクリップボードにコピー",
|
||||||
|
"fileSaved": "ファイルを保存しました",
|
||||||
|
"fileSavedToFilename": "{filename} に保存しました"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user