Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8802ea62ca | |||
| 3c86b014de | |||
| c6f06dd1fc | |||
| 922e28a198 | |||
| 49363afac1 | |||
| 28a1b9a787 | |||
| 9cfab40343 | |||
| 1d7c5705b2 | |||
| 40cd4caeec |
+6
-6
@@ -1,10 +1,10 @@
|
||||
*
|
||||
!.env
|
||||
!.eslintrc.json
|
||||
!.npmrc
|
||||
!.prettierrc
|
||||
!package.json
|
||||
!public/
|
||||
!src/
|
||||
!.npmrc
|
||||
!.eslintrc.json
|
||||
!.prettierrc
|
||||
!package-lock.json
|
||||
!package.json
|
||||
!tsconfig.json
|
||||
!yarn.lock
|
||||
!.env
|
||||
|
||||
+1
-1
@@ -2,7 +2,6 @@
|
||||
"extends": ["prettier", "react-app"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"curly": "warn",
|
||||
"dot-notation": "warn",
|
||||
"import/no-anonymous-default-export": "off",
|
||||
@@ -23,6 +22,7 @@
|
||||
],
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"no-useless-return": "warn",
|
||||
"no-var": "warn",
|
||||
"object-shorthand": "warn",
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
open-pull-requests-limit: 99
|
||||
reviewers:
|
||||
- lipis
|
||||
assignees:
|
||||
- lipis
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /src/packages/excalidraw/
|
||||
directory: "/src/packages/excalidraw/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
open-pull-requests-limit: 99
|
||||
reviewers:
|
||||
- ad1992
|
||||
assignees:
|
||||
- ad1992
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /src/packages/utils/
|
||||
directory: "/src/packages/utils/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
open-pull-requests-limit: 99
|
||||
reviewers:
|
||||
- ad1992
|
||||
assignees:
|
||||
|
||||
@@ -6,8 +6,9 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1
|
||||
- run: docker build -t excalidraw .
|
||||
|
||||
@@ -7,23 +7,27 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
packages:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn --cwd src/packages/excalidraw
|
||||
yarn --cwd src/packages/utils
|
||||
npm ci
|
||||
npm ci --prefix src/packages/excalidraw
|
||||
npm ci --prefix src/packages/utils
|
||||
|
||||
- name: Build @excalidraw/excalidraw
|
||||
run: |
|
||||
yarn --cwd src/packages/excalidraw run pack
|
||||
npm run pack --prefix src/packages/excalidraw
|
||||
|
||||
- name: Build @excalidraw/utils
|
||||
run: |
|
||||
yarn --cwd src/packages/utils run pack
|
||||
npm run pack --prefix src/packages/utils
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
name: Cancel previous runs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.6.0
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
name: Lint
|
||||
|
||||
on: pull_request
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn test:other
|
||||
yarn test:code
|
||||
yarn test:typecheck
|
||||
npm ci
|
||||
npm run test:other
|
||||
npm run test:code
|
||||
npm run test:typecheck
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Build locales coverage
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- l10n_master
|
||||
- "l10n_master"
|
||||
|
||||
jobs:
|
||||
locales:
|
||||
@@ -15,13 +15,13 @@ jobs:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Create report file
|
||||
run: |
|
||||
yarn locales-coverage
|
||||
npm run locales-coverage
|
||||
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
||||
if [ ! -z "${FILE_CHANGED}" ]; then
|
||||
git config --global user.name 'Excalidraw Bot'
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Construct comment body
|
||||
id: getCommentBody
|
||||
run: |
|
||||
body=$(yarn locales-coverage:description | grep '^[^>]')
|
||||
body=$(npm run locales-coverage:description | grep '^[^>]')
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Semantic PR title
|
||||
name: "Semantic PR title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -8,8 +8,9 @@ on:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
semantic:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||
env:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: New Sentry production release
|
||||
name: New Sentry Production Release
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,23 +6,28 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
sentry:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1.0.0
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and build
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn build:app
|
||||
npm ci
|
||||
npm run build:app
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Install Sentry
|
||||
run: |
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
|
||||
- name: Create new Sentry release
|
||||
run: |
|
||||
export SENTRY_RELEASE=$(sentry-cli releases propose-version)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
name: Tests
|
||||
|
||||
on: pull_request
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and test
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn test:app
|
||||
npm ci
|
||||
npm run test:app
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ firebase
|
||||
logs
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
static
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"proseWrap": "never",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
+2
-1
@@ -19,7 +19,7 @@
|
||||
### Option 2 - CodeSandbox
|
||||
|
||||
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. Tap on `Fork Sandbox`
|
||||
1. Write your code
|
||||
@@ -35,6 +35,7 @@ Make sure the title starts with a semantic prefix:
|
||||
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **improvement**: An improvement to a current feature
|
||||
- **docs**: Documentation only changes
|
||||
- **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
|
||||
|
||||
+3
-3
@@ -2,13 +2,13 @@ FROM node:14-alpine AS build
|
||||
|
||||
WORKDIR /opt/node_app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --ignore-optional
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm i --no-optional
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
COPY . .
|
||||
RUN yarn build:app:docker
|
||||
RUN npm run build:app:docker
|
||||
|
||||
FROM nginx:1.17-alpine
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a href="https://excalidraw.com">
|
||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||
</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>
|
||||
<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">
|
||||
@@ -11,7 +11,6 @@
|
||||
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
|
||||
</a>
|
||||
</p>
|
||||
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
|
||||
</div>
|
||||
|
||||
## Try it now
|
||||
@@ -20,14 +19,6 @@ 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/).
|
||||
|
||||
## 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
|
||||
|
||||
### Shortcuts
|
||||
@@ -50,7 +41,7 @@ Translations will be available on the app if they exceed a certain threshold of
|
||||
|
||||
### 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}
|
||||
@@ -70,28 +61,18 @@ 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).
|
||||
|
||||
## 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
|
||||
## Developement
|
||||
|
||||
### Code Sandbox
|
||||
|
||||
- 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!
|
||||
|
||||
### Local Installation
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
@@ -100,26 +81,26 @@ git clone https://github.com/excalidraw/excalidraw.git
|
||||
|
||||
#### Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------- |
|
||||
| `yarn` | Install the dependencies |
|
||||
| `yarn start` | Run the project |
|
||||
| `yarn fix` | Reformat all files with Prettier |
|
||||
| `yarn test` | Run tests |
|
||||
| `yarn test:update` | Update test snapshots |
|
||||
| `yarn test:code` | Test for formatting with Prettier |
|
||||
| Command | Description |
|
||||
| --------------------- | --------------------------------- |
|
||||
| `npm install` | Install the dependencies |
|
||||
| `npm start` | Run the project |
|
||||
| `npm run fix` | Reformat all files with Prettier |
|
||||
| `npm test` | Run tests |
|
||||
| `npm run test:update` | Update test snapshots |
|
||||
| `npm run test:code` | Test for formatting with Prettier |
|
||||
|
||||
#### 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
|
||||
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
|
||||
docker build -t excalidraw/excalidraw .
|
||||
@@ -130,7 +111,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.**
|
||||
|
||||
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
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ services:
|
||||
volumes:
|
||||
- ./:/opt/node_app/app:delegated
|
||||
- ./package.json:/opt/node_app/package.json
|
||||
- ./yarn.lock:/opt/node_app/yarn.lock
|
||||
- ./package-lock.json:/opt/node_app/package-lock.json
|
||||
- notused:/opt/node_app/app/node_modules
|
||||
|
||||
volumes:
|
||||
|
||||
Generated
+23407
File diff suppressed because it is too large
Load Diff
+23
-26
@@ -19,20 +19,21 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "6.2.0",
|
||||
"@sentry/integrations": "6.2.0",
|
||||
"@sentry/browser": "6.0.3",
|
||||
"@sentry/integrations": "6.0.3",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.5",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.1",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/socket.io-client": "1.4.35",
|
||||
"browser-fs-access": "0.14.0",
|
||||
"browser-fs-access": "0.13.0",
|
||||
"clsx": "1.1.1",
|
||||
"firebase": "8.2.9",
|
||||
"firebase": "8.2.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.1.20",
|
||||
"node-sass": "4.14.1",
|
||||
"open-color": "1.8.0",
|
||||
"pako": "1.0.11",
|
||||
"png-chunk-text": "1.0.0",
|
||||
@@ -42,29 +43,26 @@
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scripts": "4.0.1",
|
||||
"roughjs": "4.3.1",
|
||||
"sass": "1.32.8",
|
||||
"socket.io-client": "2.3.1",
|
||||
"typescript": "4.1.5"
|
||||
"typescript": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@types/lodash.throttle": "4.1.6",
|
||||
"@types/pako": "1.0.1",
|
||||
"@types/resize-observer-browser": "0.1.5",
|
||||
"eslint-config-prettier": "8.1.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"firebase-tools": "9.5.0",
|
||||
"firebase-tools": "9.2.2",
|
||||
"husky": "4.3.8",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"lint-staged": "10.5.4",
|
||||
"jest-canvas-mock": "2.3.0",
|
||||
"lint-staged": "10.5.3",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"rewire": "5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"homepage": ".",
|
||||
"husky": {
|
||||
@@ -79,29 +77,28 @@
|
||||
"resetMocks": false
|
||||
},
|
||||
"name": "excalidraw",
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"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": "yarn build:app && yarn build:version",
|
||||
"build": "npm run build:app && npm run build:version",
|
||||
"eject": "react-scripts eject",
|
||||
"fix:code": "yarn test:code --fix",
|
||||
"fix:other": "yarn prettier --write",
|
||||
"fix": "yarn fix:other && yarn fix:code",
|
||||
"fix:code": "npm run test:code -- --fix",
|
||||
"fix:other": "npm run prettier -- --write",
|
||||
"fix": "npm run fix:other && npm run fix:code",
|
||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "react-scripts start",
|
||||
"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:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||
"test:other": "yarn prettier --list-different",
|
||||
"test:other": "npm run prettier -- --list-different",
|
||||
"test:typecheck": "tsc",
|
||||
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||
"test": "yarn test:app"
|
||||
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
|
||||
"test": "npm run 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 */
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("Virgil.woff2");
|
||||
src: url("FG_Virgil.woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -57,10 +57,9 @@
|
||||
|
||||
<!-- Excalidraw version -->
|
||||
<meta name="version" content="{version}" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="Virgil.woff2"
|
||||
href="FG_Virgil.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
@@ -86,9 +85,7 @@
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||
</script>
|
||||
|
||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||
<script
|
||||
async
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// In order to run:
|
||||
// npm install canvas # please do not check it in
|
||||
// yarn build-node
|
||||
// npm run build-node
|
||||
// node build/static/js/build-node.js
|
||||
// open test.png
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { ZOOM_STEP } from "../constants";
|
||||
import { GRID_SIZE, ZOOM_STEP } from "../constants";
|
||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
@@ -52,7 +52,7 @@ export const actionClearCanvas = register({
|
||||
elementLocked: appState.elementLocked,
|
||||
exportBackground: appState.exportBackground,
|
||||
exportEmbedScene: appState.exportEmbedScene,
|
||||
gridSize: appState.gridSize,
|
||||
gridSize: appState.gridSize || GRID_SIZE,
|
||||
shouldAddWatermark: appState.shouldAddWatermark,
|
||||
showStats: appState.showStats,
|
||||
pasteDialog: appState.pasteDialog,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ProjectName } from "../components/ProjectName";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import "../components/ToolIcon.scss";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||
import { loadFromJSON, saveAsJSON } from "../data";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
@@ -97,24 +96,9 @@ export const actionChangeShouldAddWatermark = register({
|
||||
export const actionSaveScene = register({
|
||||
name: "saveScene",
|
||||
perform: async (elements, appState, value) => {
|
||||
const fileHandleExists = !!appState.fileHandle;
|
||||
try {
|
||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
fileHandle,
|
||||
toastMessage: fileHandleExists
|
||||
? fileHandle.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved")
|
||||
: null,
|
||||
},
|
||||
};
|
||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError") {
|
||||
console.error(error);
|
||||
@@ -205,31 +189,3 @@ 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,
|
||||
// set the last point to first point.
|
||||
// This ensures that loop remains closed at different scales.
|
||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||
const isLoop = isPathALoop(multiPointElement.points);
|
||||
if (
|
||||
multiPointElement.type === "line" ||
|
||||
multiPointElement.type === "draw"
|
||||
|
||||
@@ -42,7 +42,7 @@ export const actionGoToCollaborator = register({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { background, stroke } = getClientColors(clientId, appState);
|
||||
const { background, stroke } = getClientColors(clientId);
|
||||
const shortName = getClientInitials(collaborator.username);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { GRID_SIZE } from "../constants";
|
||||
import { AppState } from "../types";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { AppState } from "../types";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleGridMode = register({
|
||||
name: "gridMode",
|
||||
@@ -11,12 +10,12 @@ export const actionToggleGridMode = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||
showGrid: !appState.showGrid,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState: AppState) => appState.gridSize !== null,
|
||||
contextItemLabel: "labels.showGrid",
|
||||
checked: (appState: AppState) => appState.showGrid,
|
||||
contextItemLabel: "labels.gridMode",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/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
|
||||
// `canvas` state.
|
||||
@@ -69,7 +68,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
}
|
||||
const { viewModeEnabled } = this.getAppState();
|
||||
if (viewModeEnabled) {
|
||||
if (!Object.values(MODES).includes(data[0].name)) {
|
||||
if (data[0].name !== "viewMode") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,7 @@ export type ActionName =
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode";
|
||||
| "viewMode";
|
||||
|
||||
export interface Action {
|
||||
name: ActionName;
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
GRID_SIZE,
|
||||
} from "./constants";
|
||||
import { t } from "./i18n";
|
||||
import { AppState, NormalizedZoomValue } from "./types";
|
||||
@@ -40,9 +41,8 @@ export const getDefaultAppState = (): Omit<
|
||||
errorMessage: null,
|
||||
exportBackground: true,
|
||||
exportEmbedScene: false,
|
||||
exportWithDarkMode: false,
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
gridSize: GRID_SIZE,
|
||||
height: window.innerHeight,
|
||||
isBindingEnabled: true,
|
||||
isLibraryOpen: false,
|
||||
@@ -64,6 +64,7 @@ export const getDefaultAppState = (): Omit<
|
||||
selectionElement: null,
|
||||
shouldAddWatermark: false,
|
||||
shouldCacheIgnoreZoom: false,
|
||||
showGrid: false,
|
||||
showHelpDialog: false,
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
@@ -119,9 +120,9 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
errorMessage: { browser: false, export: false },
|
||||
exportBackground: { browser: true, export: false },
|
||||
exportEmbedScene: { browser: true, export: false },
|
||||
exportWithDarkMode: { browser: true, export: false },
|
||||
fileHandle: { browser: false, export: false },
|
||||
gridSize: { browser: true, export: true },
|
||||
showGrid: { browser: true, export: false },
|
||||
height: { browser: false, export: false },
|
||||
isBindingEnabled: { browser: false, export: false },
|
||||
isLibraryOpen: { browser: false, export: false },
|
||||
|
||||
+1
-8
@@ -1,13 +1,6 @@
|
||||
import colors from "./colors";
|
||||
import { AppState } from "./types";
|
||||
|
||||
export const getClientColors = (clientId: string, appState: AppState) => {
|
||||
if (appState?.collaborators) {
|
||||
const currentUser = appState.collaborators.get(clientId);
|
||||
if (currentUser?.color) {
|
||||
return currentUser.color;
|
||||
}
|
||||
}
|
||||
export const getClientColors = (clientId: string) => {
|
||||
// Naive way of getting an integer out of the clientId
|
||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
||||
|
||||
|
||||
+52
-84
@@ -4,6 +4,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "../actions";
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
actionBringForward,
|
||||
@@ -47,11 +48,9 @@ import {
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
ENV,
|
||||
EVENT,
|
||||
GRID_SIZE,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MIME_TYPES,
|
||||
POINTER_BUTTON,
|
||||
SCROLL_TIMEOUT,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
@@ -300,8 +299,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
offsetTop,
|
||||
excalidrawRef,
|
||||
viewModeEnabled = false,
|
||||
zenModeEnabled = false,
|
||||
gridModeEnabled = false,
|
||||
} = props;
|
||||
this.state = {
|
||||
...defaultAppState,
|
||||
@@ -310,8 +307,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
height,
|
||||
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||
};
|
||||
if (excalidrawRef) {
|
||||
const readyPromise =
|
||||
@@ -458,15 +453,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderCustomFooter={renderFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
||||
}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
appState={this.state}
|
||||
elements={this.scene.getElements()}
|
||||
onClose={this.toggleStats}
|
||||
/>
|
||||
@@ -519,21 +510,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
|
||||
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(
|
||||
(state) => ({
|
||||
...actionResult.appState,
|
||||
@@ -544,8 +525,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
offsetTop: state.offsetTop,
|
||||
offsetLeft: state.offsetLeft,
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
gridSize,
|
||||
}),
|
||||
() => {
|
||||
if (actionResult.syncHistory) {
|
||||
@@ -672,24 +651,19 @@ class App extends React.Component<ExcalidrawProps, 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,
|
||||
};
|
||||
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.syncActionResult({
|
||||
@@ -831,7 +805,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
|
||||
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);
|
||||
@@ -871,15 +844,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
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
|
||||
.querySelector(".excalidraw")
|
||||
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
|
||||
@@ -919,7 +883,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
|
||||
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
|
||||
const pointerUsernames: { [id: string]: string } = {};
|
||||
const pointerUserStates: { [id: string]: string } = {};
|
||||
this.state.collaborators.forEach((user, socketId) => {
|
||||
if (user.selectedElementIds) {
|
||||
for (const id of Object.keys(user.selectedElementIds)) {
|
||||
@@ -935,9 +898,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (user.username) {
|
||||
pointerUsernames[socketId] = user.username;
|
||||
}
|
||||
if (user.userState) {
|
||||
pointerUserStates[socketId] = user.userState;
|
||||
}
|
||||
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: user.pointer.x,
|
||||
@@ -972,12 +932,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
remotePointerButton: cursorButton,
|
||||
remoteSelectedElementIds,
|
||||
remotePointerUsernames: pointerUsernames,
|
||||
remotePointerUserStates: pointerUserStates,
|
||||
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
||||
},
|
||||
{
|
||||
renderOptimizations: true,
|
||||
renderScrollbars: !isMobile(),
|
||||
},
|
||||
);
|
||||
if (scrollBars) {
|
||||
@@ -1006,10 +964,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
private onScroll = debounce(() => {
|
||||
this.setState({ ...this.getCanvasOffsets() });
|
||||
}, SCROLL_TIMEOUT);
|
||||
|
||||
// Copy/paste
|
||||
|
||||
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||
@@ -1134,7 +1088,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const dy = y - elementsCenterY;
|
||||
const groupIdMap = new Map();
|
||||
|
||||
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
dx,
|
||||
dy,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
|
||||
const oldIdToDuplicatedId = new Map();
|
||||
const newElements = clipboardElements.map((element) => {
|
||||
@@ -1242,6 +1201,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.actionManager.executeAction(actionToggleZenMode);
|
||||
};
|
||||
|
||||
toggleGridMode = () => {
|
||||
this.actionManager.executeAction(actionToggleGridMode);
|
||||
};
|
||||
|
||||
toggleStats = () => {
|
||||
if (!this.state.showStats) {
|
||||
trackEvent("dialog", "stats");
|
||||
@@ -1354,7 +1317,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
|
||||
if (isArrowKey(event.key)) {
|
||||
const step =
|
||||
(this.state.gridSize &&
|
||||
(this.state.showGrid &&
|
||||
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
|
||||
(event.shiftKey
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
@@ -1562,7 +1525,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
textWysiwyg({
|
||||
id: element.id,
|
||||
appState: this.state,
|
||||
canvas: this.canvas,
|
||||
getViewportCoords: (x, y) => {
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{
|
||||
@@ -1871,11 +1833,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPointerOverScrollBars = isOverScrollBars(
|
||||
currentScrollBars,
|
||||
event.clientX - this.state.offsetLeft,
|
||||
event.clientY - this.state.offsetTop,
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
);
|
||||
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
||||
if (!this.state.draggingElement && !this.state.multiElement) {
|
||||
@@ -1898,6 +1859,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.state.editingLinearElement,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
if (editingLinearElement !== this.state.editingLinearElement) {
|
||||
@@ -1969,7 +1931,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
points: points.slice(0, -1),
|
||||
});
|
||||
} else {
|
||||
if (isPathALoop(points, this.state.zoom.value)) {
|
||||
if (isPathALoop(points)) {
|
||||
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
||||
}
|
||||
// update last uncommitted point
|
||||
@@ -2327,12 +2289,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
return {
|
||||
origin,
|
||||
originInGrid: tupleToCoors(
|
||||
getGridPoint(origin.x, origin.y, this.state.gridSize),
|
||||
getGridPoint(
|
||||
origin.x,
|
||||
origin.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
),
|
||||
),
|
||||
scrollbars: isOverScrollBars(
|
||||
currentScrollBars,
|
||||
event.clientX - this.state.offsetLeft,
|
||||
event.clientY - this.state.offsetTop,
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
),
|
||||
// we need to duplicate because we'll be updating this state
|
||||
lastCoords: { ...origin },
|
||||
@@ -2641,10 +2608,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const { multiElement } = this.state;
|
||||
|
||||
// finalize if completing a loop
|
||||
if (
|
||||
multiElement.type === "line" &&
|
||||
isPathALoop(multiElement.points, this.state.zoom.value)
|
||||
) {
|
||||
if (multiElement.type === "line" && isPathALoop(multiElement.points)) {
|
||||
mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
@@ -2686,7 +2650,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
elementType === "draw" ? null : this.state.gridSize,
|
||||
elementType === "draw" ? false : this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
|
||||
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
||||
@@ -2748,6 +2713,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
const element = newElement({
|
||||
@@ -2837,6 +2803,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
|
||||
@@ -2909,6 +2876,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [dragX, dragY] = getGridPoint(
|
||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
|
||||
@@ -2961,6 +2929,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [originDragX, originDragY] = getGridPoint(
|
||||
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
||||
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
mutateElement(duplicatedElement, {
|
||||
@@ -3617,6 +3586,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
dragNewElement(
|
||||
@@ -3655,6 +3625,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
const [resizeX, resizeY] = getGridPoint(
|
||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||
this.state.showGrid,
|
||||
this.state.gridSize,
|
||||
);
|
||||
if (
|
||||
@@ -3715,16 +3686,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
options.push(actionCopyAsSvg);
|
||||
}
|
||||
if (!element) {
|
||||
const viewModeOptions = [
|
||||
const viewModeOptions: ContextMenuOption[] = [
|
||||
...options,
|
||||
typeof this.props.gridModeEnabled === "undefined" &&
|
||||
actionToggleGridMode,
|
||||
typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode,
|
||||
typeof this.props.viewModeEnabled === "undefined" &&
|
||||
actionToggleViewMode,
|
||||
actionToggleStats,
|
||||
];
|
||||
|
||||
if (typeof this.props.viewModeEnabled === "undefined") {
|
||||
viewModeOptions.push(actionToggleViewMode);
|
||||
}
|
||||
|
||||
ContextMenu.push({
|
||||
options: viewModeOptions,
|
||||
top: clientY,
|
||||
@@ -3762,10 +3732,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
separator,
|
||||
actionSelectAll,
|
||||
separator,
|
||||
typeof this.props.gridModeEnabled === "undefined" &&
|
||||
actionToggleGridMode,
|
||||
typeof this.props.zenModeEnabled === "undefined" &&
|
||||
actionToggleZenMode,
|
||||
actionToggleGridMode,
|
||||
actionToggleZenMode,
|
||||
typeof this.props.viewModeEnabled === "undefined" &&
|
||||
actionToggleViewMode,
|
||||
actionToggleStats,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
.excalidraw {
|
||||
.CollabButton.is-collaborating {
|
||||
background-color: var(--button-special-active-bg-color);
|
||||
background-color: var(--button-special-active-background-color);
|
||||
|
||||
.ToolIcon__icon svg,
|
||||
.ToolIcon__label {
|
||||
.ToolIcon__icon svg {
|
||||
color: var(--icon-green-fill-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ const CollabButton = ({
|
||||
onClick={onClick}
|
||||
icon={users}
|
||||
type="button"
|
||||
title={t("labels.liveCollaboration")}
|
||||
aria-label={t("labels.liveCollaboration")}
|
||||
title={t("buttons.roomDialog")}
|
||||
aria-label={t("buttons.roomDialog")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
>
|
||||
{collaboratorCount > 0 && (
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
.excalidraw {
|
||||
.color-picker {
|
||||
background: var(--popup-bg-color);
|
||||
border: 0 solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0 1px 4px;
|
||||
background: var(--popup-background-color);
|
||||
border: 0px solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
}
|
||||
|
||||
.color-picker-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-style: solid;
|
||||
border-width: 0 9px 10px;
|
||||
border-color: transparent transparent var(--popup-bg-color);
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent var(--popup-background-color);
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
|
||||
@@ -84,12 +84,12 @@
|
||||
|
||||
.color-picker-transparent {
|
||||
border-radius: 4px;
|
||||
box-shadow: transparentize($oc-black, 0.9) 0 0 0 1px inset;
|
||||
box-shadow: transparentize($oc-black, 0.9) 0px 0px 0px 1px inset;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.color-picker-transparent,
|
||||
@@ -104,11 +104,11 @@
|
||||
width: 1.875rem;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: 4px 0 0 4px;
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
|
||||
color: var(--input-label-color);
|
||||
@@ -144,7 +144,7 @@
|
||||
}
|
||||
|
||||
.color-input-container:focus-within .color-picker-hash::after {
|
||||
background: var(--input-bg-color);
|
||||
background: var(--input-background-color);
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: -2px;
|
||||
@@ -163,19 +163,19 @@
|
||||
width: 12ch; /* length of `transparent` + 1 */
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
background-color: var(--input-bg-color);
|
||||
color: var(--text-primary-color);
|
||||
border: 0;
|
||||
background-color: var(--input-background-color);
|
||||
color: var(--text-color-primary);
|
||||
border: 0px;
|
||||
outline: none;
|
||||
height: 1.75em;
|
||||
box-shadow: var(--input-border-color) 0 0 0 1px inset;
|
||||
box-shadow: var(--input-border-color) 0px 0px 0px 1px inset;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: 4px 0 0 4px;
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
float: left;
|
||||
@@ -228,7 +228,7 @@
|
||||
}
|
||||
|
||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||
color: $oc-white;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||
@@ -241,10 +241,10 @@
|
||||
|
||||
&.Appearance_dark {
|
||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||
color: $oc-black;
|
||||
color: #000;
|
||||
}
|
||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||
color: $oc-black;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
.context-menu {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 3px 10px transparentize($oc-black, 0.8);
|
||||
box-shadow: 0px 3px 10px transparentize($oc-black, 0.8);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
margin: -0.25rem 0 0 0.125rem;
|
||||
padding: 0.5rem 0;
|
||||
background-color: var(--popup-secondary-bg-color);
|
||||
background-color: var(--popup-secondary-background-color);
|
||||
border: 1px solid var(--button-gray-3);
|
||||
cursor: default;
|
||||
}
|
||||
@@ -61,12 +61,12 @@
|
||||
}
|
||||
|
||||
.context-menu-option:hover {
|
||||
color: var(--popup-bg-color);
|
||||
color: var(--popup-background-color);
|
||||
background-color: var(--select-highlight-color);
|
||||
|
||||
&.dangerous {
|
||||
.context-menu-option__label {
|
||||
color: var(--popup-bg-color);
|
||||
color: var(--popup-background-color);
|
||||
}
|
||||
background-color: $oc-red-6;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,13 @@ export type Appearence = "light" | "dark";
|
||||
export const DarkModeToggle = (props: {
|
||||
value: Appearence;
|
||||
onChange: (value: Appearence) => void;
|
||||
title?: string;
|
||||
}) => {
|
||||
const title = props.title
|
||||
? props.title
|
||||
: props.value === "dark"
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode");
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
|
||||
title={title}
|
||||
title={
|
||||
props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode")
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
@@ -30,7 +25,11 @@ export const DarkModeToggle = (props: {
|
||||
props.onChange(event.target.checked ? "dark" : "light")
|
||||
}
|
||||
checked={props.value === "dark"}
|
||||
aria-label={title}
|
||||
aria-label={
|
||||
props.value === "dark"
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode")
|
||||
}
|
||||
/>
|
||||
<div className="ToolIcon__icon">
|
||||
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
background: var(--island-bg-color);
|
||||
background: var(--bg-color-island);
|
||||
font-size: 1.25em;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -19,9 +19,6 @@ import { ToolButton } from "./ToolButton";
|
||||
const scales = [1, 2, 3];
|
||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||
|
||||
const supportsContextFilters =
|
||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||
|
||||
export const ErrorCanvasPreview = () => {
|
||||
return (
|
||||
<div>
|
||||
@@ -131,8 +128,6 @@ const ExportModal = ({
|
||||
return (
|
||||
<div className="ExportDialog">
|
||||
<div className="ExportDialog__preview" ref={previewRef} />
|
||||
{supportsContextFilters &&
|
||||
actionManager.renderAction("exportWithDarkMode")}
|
||||
<Stack.Col gap={2} align="center">
|
||||
<div className="ExportDialog__actions">
|
||||
<Stack.Row gap={2}>
|
||||
|
||||
@@ -224,7 +224,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.showGrid")}
|
||||
label={t("labels.gridMode")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||
/>
|
||||
<Shortcut
|
||||
|
||||
@@ -26,7 +26,7 @@ $wide-viewport-width: 1000px;
|
||||
|
||||
> span {
|
||||
padding: 0.2rem 0.4rem;
|
||||
background-color: var(--overlay-bg-color);
|
||||
background-color: var(--overlay-background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
}
|
||||
|
||||
.picker {
|
||||
background: var(--popup-bg-color);
|
||||
border: 0 solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0 1px 4px;
|
||||
background: var(--popup-background-color);
|
||||
border: 0px solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -56,8 +56,8 @@
|
||||
}
|
||||
|
||||
.picker-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
:root[dir="ltr"] & {
|
||||
@@ -73,7 +73,7 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-style: solid;
|
||||
border-width: 0 9px 10px;
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent transparentize($oc-black, 0.9);
|
||||
top: -1px;
|
||||
}
|
||||
@@ -82,8 +82,8 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-style: solid;
|
||||
border-width: 0 9px 10px;
|
||||
border-color: transparent transparent var(--popup-bg-color);
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent var(--popup-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
|
||||
.picker-type-elementBackground .picker-keybinding {
|
||||
color: $oc-white;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||
@@ -134,10 +134,10 @@
|
||||
|
||||
&.Appearance_dark {
|
||||
.picker-type-elementBackground .picker-keybinding {
|
||||
color: $oc-black;
|
||||
color: #000;
|
||||
}
|
||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||
color: $oc-black;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
||||
import {
|
||||
defaultLang,
|
||||
Language,
|
||||
languages,
|
||||
setLanguageFirstTime,
|
||||
} from "../i18n";
|
||||
|
||||
interface Props {
|
||||
langCode: Language["code"];
|
||||
@@ -18,7 +23,7 @@ export class InitializeApp extends React.Component<Props, State> {
|
||||
const currentLang =
|
||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||
defaultLang;
|
||||
await setLanguage(currentLang);
|
||||
await setLanguageFirstTime(currentLang);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.excalidraw {
|
||||
.Island {
|
||||
--padding: 0;
|
||||
background-color: var(--island-bg-color);
|
||||
background-color: var(--bg-color-island);
|
||||
backdrop-filter: saturate(100%) blur(10px);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
}
|
||||
|
||||
a {
|
||||
margin-inline-start: auto;
|
||||
margin-left: auto;
|
||||
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
|
||||
padding-inline-end: 18px;
|
||||
padding-right: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,6 @@
|
||||
}
|
||||
|
||||
.layer-ui__wrapper {
|
||||
z-index: var(--zIndex-layerUI);
|
||||
|
||||
.encrypted-icon {
|
||||
position: relative;
|
||||
margin-inline-start: 15px;
|
||||
@@ -73,7 +71,7 @@
|
||||
&__footer {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
bottom: 0;
|
||||
bottom: 0px;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: 0;
|
||||
@@ -94,7 +92,7 @@
|
||||
}
|
||||
|
||||
:root[dir="ltr"] &.transition-right {
|
||||
transform: translate(999px, 0);
|
||||
transform: translate(999px, 0px);
|
||||
}
|
||||
|
||||
:root[dir="rtl"] &.transition-left {
|
||||
|
||||
+28
-58
@@ -11,7 +11,7 @@ import { CLASSES } from "../constants";
|
||||
import { exportCanvas } from "../data";
|
||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
@@ -27,7 +27,7 @@ import { ExportCB, ExportDialog } from "./ExportDialog";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { GitHubCorner } from "./GitHubCorner";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import { exportFile, load, shield, trash } from "./icons";
|
||||
import { exportFile, load, shield } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import "./LayerUI.scss";
|
||||
import { LibraryUnit } from "./LibraryUnit";
|
||||
@@ -52,7 +52,6 @@ interface LayerUIProps {
|
||||
onLockToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
showExitZenModeBtn: boolean;
|
||||
toggleZenMode: () => void;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
@@ -100,7 +99,6 @@ const LibraryMenuItems = ({
|
||||
onInsertShape,
|
||||
pendingElements,
|
||||
setAppState,
|
||||
setLibraryItems,
|
||||
}: {
|
||||
library: LibraryItems;
|
||||
pendingElements: LibraryItem;
|
||||
@@ -108,7 +106,6 @@ const LibraryMenuItems = ({
|
||||
onInsertShape: (elements: LibraryItem) => void;
|
||||
onAddToLibrary: (elements: LibraryItem) => void;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
setLibraryItems: (library: LibraryItems) => void;
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
||||
@@ -152,19 +149,6 @@ 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">
|
||||
{t("labels.libraries")}
|
||||
@@ -296,7 +280,6 @@ const LibraryMenu = ({
|
||||
onInsertShape={onInsertShape}
|
||||
pendingElements={pendingElements}
|
||||
setAppState={setAppState}
|
||||
setLibraryItems={setLibraryItems}
|
||||
/>
|
||||
)}
|
||||
</Island>
|
||||
@@ -313,7 +296,6 @@ const LayerUI = ({
|
||||
onLockToggle,
|
||||
onInsertElements,
|
||||
zenModeEnabled,
|
||||
showExitZenModeBtn,
|
||||
toggleZenMode,
|
||||
isCollaborating,
|
||||
onExportToBackend,
|
||||
@@ -442,15 +424,7 @@ const LayerUI = ({
|
||||
"transition-left": zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<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`,
|
||||
}}
|
||||
>
|
||||
<Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={2}>
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
@@ -539,18 +513,17 @@ const LayerUI = ({
|
||||
"transition-right": zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{appState.collaborators.size > 0 &&
|
||||
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]) => (
|
||||
<Tooltip
|
||||
label={client.username || "Unknown user"}
|
||||
key={clientId}
|
||||
>
|
||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
||||
</Tooltip>
|
||||
))}
|
||||
{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]) => (
|
||||
<Tooltip
|
||||
label={client.username || "Unknown user"}
|
||||
key={clientId}
|
||||
>
|
||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
||||
</Tooltip>
|
||||
))}
|
||||
</UserList>
|
||||
</div>
|
||||
</FixedSideContainer>
|
||||
@@ -605,12 +578,24 @@ const LayerUI = ({
|
||||
</div>
|
||||
<button
|
||||
className={clsx("disable-zen-mode", {
|
||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||
"disable-zen-mode--visible": zenModeEnabled,
|
||||
})}
|
||||
onClick={toggleZenMode}
|
||||
>
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -662,10 +647,7 @@ const LayerUI = ({
|
||||
) : (
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper", {
|
||||
"disable-pointerEvents":
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
(appState.editingElement && !isTextElement(appState.editingElement)),
|
||||
"disable-pointerEvents": appState.cursorButton === "down",
|
||||
})}
|
||||
>
|
||||
{dialogs}
|
||||
@@ -673,18 +655,6 @@ const LayerUI = ({
|
||||
{renderBottomAppMenu()}
|
||||
{renderGitHubCorner()}
|
||||
{renderFooter()}
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,9 +46,9 @@ export const MobileMenu = ({
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
const renderFixedSideContainer = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
<FixedSideContainer side="top">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
@@ -136,7 +136,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!viewModeEnabled && renderToolbar()}
|
||||
{!viewModeEnabled && renderFixedSideContainer()}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
style={{
|
||||
@@ -152,26 +152,24 @@ export const MobileMenu = ({
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
&.excalidraw-modal-container {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -20,7 +15,7 @@
|
||||
}
|
||||
|
||||
.Modal__background {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -44,10 +39,10 @@
|
||||
overflow-y: auto;
|
||||
|
||||
// for modals, reset blurry bg
|
||||
background: var(--island-bg-color);
|
||||
background: var(--bg-color-island);
|
||||
backdrop-filter: none;
|
||||
|
||||
border: 1px solid var(--dialog-border-color);
|
||||
border: 1px solid var(--dialog-border);
|
||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -87,7 +82,7 @@
|
||||
}
|
||||
|
||||
.Modal__content {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -54,7 +54,7 @@ const useBodyRoot = () => {
|
||||
?.classList.contains("Appearance_dark");
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||
div.classList.add("excalidraw");
|
||||
|
||||
if (isDarkTheme) {
|
||||
div.classList.add("Appearance_dark");
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
.excalidraw {
|
||||
.popover {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.popover .cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-42
@@ -1,53 +1,51 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Stats {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
z-index: 999;
|
||||
.Stats {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
z-index: 999;
|
||||
|
||||
h3 {
|
||||
margin: 0 24px 8px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 24px 8px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
.close {
|
||||
float: right;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 100%;
|
||||
th {
|
||||
border-bottom: 1px solid var(--input-border-color);
|
||||
padding: 4px;
|
||||
}
|
||||
tr {
|
||||
td:nth-child(2) {
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
left: 12px;
|
||||
right: initial;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 24px;
|
||||
}
|
||||
.close {
|
||||
float: left;
|
||||
table {
|
||||
width: 100%;
|
||||
th {
|
||||
border-bottom: 1px solid var(--input-border-color);
|
||||
padding: 4px;
|
||||
}
|
||||
tr {
|
||||
td:nth-child(2) {
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
left: 12px;
|
||||
right: initial;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 24px;
|
||||
}
|
||||
.close {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-37
@@ -1,6 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { copyTextToSystemClipboard } from "../clipboard";
|
||||
import { DEFAULT_VERSION } from "../constants";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
@@ -11,7 +9,7 @@ import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getTargetElements } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { debounce, getVersion, nFormatter } from "../utils";
|
||||
import { debounce, nFormatter } from "../utils";
|
||||
import { close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import "./Stats.scss";
|
||||
@@ -26,8 +24,8 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
||||
}, 500);
|
||||
|
||||
export const Stats = (props: {
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
@@ -49,21 +47,16 @@ export const Stats = (props: {
|
||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||
|
||||
const onGridSizeChange = () => {
|
||||
props.setAppState({
|
||||
gridSize: ((props.appState.gridSize - 5) % 50) + 10,
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile && props.appState.openMenu) {
|
||||
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 (
|
||||
<div className="Stats">
|
||||
<Island padding={2}>
|
||||
@@ -170,28 +163,17 @@ export const Stats = (props: {
|
||||
</td>
|
||||
</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>
|
||||
{props.appState.showGrid && (
|
||||
<>
|
||||
<tr>
|
||||
<th colSpan={2}>{"Misc"}</th>
|
||||
</tr>
|
||||
<tr onClick={onGridSizeChange} style={{ cursor: "pointer" }}>
|
||||
<td>{"Grid size"}</td>
|
||||
<td>{props.appState.gridSize}</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Island>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
.excalidraw {
|
||||
.TextInput {
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-color-primary);
|
||||
display: inline-block;
|
||||
border: 1.5px solid var(--button-gray-1);
|
||||
line-height: 1;
|
||||
padding: 0.75rem;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--space-factor);
|
||||
background-color: var(--input-bg-color);
|
||||
background-color: var(--input-background-color);
|
||||
|
||||
&:not(:focus) {
|
||||
&:hover {
|
||||
background-color: var(--input-hover-bg-color);
|
||||
background-color: var(--input-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
padding: 4px 0;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
z-index: 999999;
|
||||
|
||||
+2
-19
@@ -1,10 +1,9 @@
|
||||
import { FontFamily } from "./element/types";
|
||||
import cssVariables from "./css/variables.module.scss";
|
||||
|
||||
export const APP_NAME = "Excalidraw";
|
||||
|
||||
export const DRAGGING_THRESHOLD = 10; // px
|
||||
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
||||
export const DRAGGING_THRESHOLD = 10; // 10px
|
||||
export const LINE_CONFIRM_THRESHOLD = 10; // 10px
|
||||
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
||||
export const ELEMENT_TRANSLATE_AMOUNT = 1;
|
||||
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
|
||||
@@ -47,8 +46,6 @@ export enum EVENT {
|
||||
TOUCH_START = "touchstart",
|
||||
TOUCH_END = "touchend",
|
||||
HASHCHANGE = "hashchange",
|
||||
VISIBILITY_CHANGE = "visibilitychange",
|
||||
SCROLL = "scroll",
|
||||
}
|
||||
|
||||
export const ENV = {
|
||||
@@ -94,19 +91,5 @@ export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||
export const TITLE_TIMEOUT = 10000;
|
||||
export const TOAST_TIMEOUT = 5000;
|
||||
export const VERSION_TIMEOUT = 30000;
|
||||
export const SCROLL_TIMEOUT = 500;
|
||||
|
||||
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;
|
||||
|
||||
+15
-28
@@ -1,15 +1,10 @@
|
||||
@import "./variables.module";
|
||||
@import "./theme";
|
||||
|
||||
:root {
|
||||
--zIndex-canvas: 1;
|
||||
--zIndex-wysiwyg: 2;
|
||||
--zIndex-layerUI: 3;
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-color-primary);
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@@ -35,8 +30,6 @@
|
||||
image-rendering: pixelated; // chromium
|
||||
// NOTE: must be declared *after* the above
|
||||
image-rendering: -moz-crisp-edges; // FF
|
||||
|
||||
z-index: var(--zIndex-canvas);
|
||||
}
|
||||
|
||||
&.Appearance_dark {
|
||||
@@ -50,10 +43,10 @@
|
||||
}
|
||||
|
||||
.FixedSideContainer {
|
||||
padding-top: var(--sat, 0);
|
||||
padding-right: var(--sar, 0);
|
||||
padding-bottom: var(--sab, 0);
|
||||
padding-left: var(--sal, 0);
|
||||
padding-top: var(--sat, 0px);
|
||||
padding-right: var(--sar, 0px);
|
||||
padding-bottom: var(--sab, 0px);
|
||||
padding-left: var(--sal, 0px);
|
||||
}
|
||||
|
||||
.panelRow {
|
||||
@@ -71,7 +64,7 @@
|
||||
margin-top: 0.333rem;
|
||||
margin-bottom: 0.333rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-color-primary);
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
@@ -223,13 +216,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.App-top-bar {
|
||||
z-index: var(--zIndex-layerUI);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.App-bottom-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -237,10 +223,10 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
--bar-padding: calc(4 * var(--space-factor));
|
||||
padding-top: #{"max(var(--bar-padding), var(--sat,0))"};
|
||||
padding-right: var(--sar, 0);
|
||||
padding-bottom: var(--sab, 0);
|
||||
padding-left: var(--sal, 0);
|
||||
padding-top: #{"max(var(--bar-padding), var(--sat, 0px))"};
|
||||
padding-right: var(--sar, 0px);
|
||||
padding-bottom: var(--sab, 0px);
|
||||
padding-left: var(--sal, 0px);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -257,7 +243,7 @@
|
||||
pointer-events: initial;
|
||||
|
||||
.panelColumn {
|
||||
padding: 8px 8px 0 8px;
|
||||
padding: 8px 8px 0px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +347,7 @@
|
||||
|
||||
.App-menu__left {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 236px);
|
||||
}
|
||||
|
||||
.dropdown-select {
|
||||
@@ -432,7 +419,7 @@
|
||||
|
||||
.scroll-back-to-content {
|
||||
color: var(--popup-text-color);
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 30px;
|
||||
transform: translateX(-50%);
|
||||
@@ -460,7 +447,7 @@
|
||||
display: none;
|
||||
}
|
||||
.scroll-back-to-content {
|
||||
bottom: calc(80px + var(--sab, 0));
|
||||
bottom: calc(80px + var(--sab, 0px));
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
+41
-42
@@ -1,44 +1,43 @@
|
||||
@import "open-color/open-color.scss";
|
||||
@import "./variables.module.scss";
|
||||
|
||||
:root {
|
||||
--appearance-filter: none;
|
||||
--button-destructive-bg-color: #{$oc-red-1};
|
||||
--button-destructive-color: #{$oc-red-9};
|
||||
--bg-color-island: rgba(255, 255, 255, 0.9);
|
||||
--popup-background-color: #{$oc-white};
|
||||
--space-factor: 0.25rem;
|
||||
--button-gray-1: #{$oc-gray-2};
|
||||
--button-gray-2: #{$oc-gray-4};
|
||||
--button-gray-3: #{$oc-gray-5};
|
||||
--button-special-active-bg-color: #{$oc-green-0};
|
||||
--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>');
|
||||
--focus-highlight-color: #{$oc-blue-2};
|
||||
--input-border-color: #{$oc-gray-3};
|
||||
--input-background-color: #{$oc-white};
|
||||
--input-hover-background-color: #{$oc-gray-1};
|
||||
--input-label-color: #{$oc-gray-7};
|
||||
--icon-fill-color: #{$oc-black};
|
||||
--icon-green-fill-color: #{$oc-green-9};
|
||||
--input-bg-color: #{$oc-white};
|
||||
--input-border-color: #{$oc-gray-3};
|
||||
--input-hover-bg-color: #{$oc-gray-1};
|
||||
--input-label-color: #{$oc-gray-7};
|
||||
--island-bg-color: rgba(255, 255, 255, 0.9);
|
||||
--keybinding-color: #{$oc-gray-5};
|
||||
--link-color: #{$oc-blue-7};
|
||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||
--popup-bg-color: #{$oc-white};
|
||||
--popup-secondary-bg-color: #{$oc-gray-1};
|
||||
--popup-text-color: #{$oc-black};
|
||||
--popup-text-inverted-color: #{$oc-white};
|
||||
--sat: env(safe-area-inset-top);
|
||||
--sab: env(safe-area-inset-bottom);
|
||||
--sal: env(safe-area-inset-left);
|
||||
--sar: env(safe-area-inset-right);
|
||||
--sat: env(safe-area-inset-top);
|
||||
--select-highlight-color: #{$oc-blue-5};
|
||||
--text-color-primary: #{$oc-gray-8};
|
||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
|
||||
--space-factor: 0.25rem;
|
||||
--text-primary-color: #{$oc-gray-8};
|
||||
--overlay-background-color: #{transparentize($oc-white, 0.12)};
|
||||
--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};
|
||||
--select-highlight-color: #{$oc-blue-5};
|
||||
--appearance-filter: none;
|
||||
--button-special-active-background-color: #{$oc-green-0};
|
||||
--button-destructive-color: #{$oc-red-9};
|
||||
--button-destructive-background-color: #{$oc-red-1};
|
||||
--popup-secondary-background-color: #{$oc-gray-1};
|
||||
--popup-text-color: #{$oc-black};
|
||||
--popup-text-inverted-color: #{$oc-white};
|
||||
--dialog-border: #{$oc-gray-6};
|
||||
--link-color: #{$oc-blue-7};
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
&.Appearance_dark {
|
||||
background: $oc-black;
|
||||
background: #000;
|
||||
|
||||
&.Appearance_dark-background-none {
|
||||
background: none;
|
||||
@@ -46,31 +45,31 @@
|
||||
}
|
||||
|
||||
&.Appearance_dark {
|
||||
--appearance-filter: #{$appearance-filter};
|
||||
--button-destructive-bg-color: #5a0000;
|
||||
--button-destructive-color: #{$oc-red-3};
|
||||
--text-color-primary: #{$oc-gray-4};
|
||||
--bg-color-island: #1e1e1e;
|
||||
--popup-background-color: #2c2c2c;
|
||||
--button-gray-1: #363636;
|
||||
--button-gray-2: #272727;
|
||||
--button-gray-3: #222;
|
||||
--button-special-active-bg-color: #204624;
|
||||
--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>');
|
||||
--focus-highlight-color: #{$oc-blue-6};
|
||||
--input-border-color: #2e2e2e;
|
||||
--input-background-color: #121212;
|
||||
--input-hover-background-color: #181818;
|
||||
--input-label-color: #{$oc-gray-2};
|
||||
--icon-fill-color: #{$oc-gray-4};
|
||||
--icon-green-fill-color: #{$oc-green-4};
|
||||
--input-bg-color: #121212;
|
||||
--input-border-color: #2e2e2e;
|
||||
--input-hover-bg-color: #181818;
|
||||
--input-label-color: #{$oc-gray-2};
|
||||
--island-bg-color: #1e1e1e;
|
||||
--keybinding-color: #{$oc-gray-6};
|
||||
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
||||
--popup-bg-color: #2c2c2c;
|
||||
--popup-secondary-bg-color: #222;
|
||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
|
||||
--overlay-background-color: rgba(30, 30, 30, 0.88);
|
||||
--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};
|
||||
--select-highlight-color: #{$oc-blue-4};
|
||||
--appearance-filter: invert(93%) hue-rotate(180deg);
|
||||
--button-special-active-background-color: #204624;
|
||||
--button-destructive-color: #{$oc-red-3};
|
||||
--button-destructive-background-color: #5a0000;
|
||||
--popup-secondary-background-color: #222;
|
||||
--popup-text-color: #{$oc-gray-4};
|
||||
--popup-text-inverted-color: #2c2c2c;
|
||||
--select-highlight-color: #{$oc-blue-4};
|
||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
|
||||
--text-primary-color: #{$oc-gray-4};
|
||||
--dialog-border: #{$oc-gray-9};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
// keep up to date with is-mobile.tsx
|
||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||
$appearance-filter: "invert(93%) hue-rotate(180deg)";
|
||||
|
||||
:export {
|
||||
isMobileQuery: unquote($is-mobile-query);
|
||||
appearanceFilter: unquote($appearance-filter);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export const exportCanvas = async (
|
||||
if (type === "svg" || type === "clipboard-svg") {
|
||||
const tempSvg = exportToSvg(elements, {
|
||||
exportBackground,
|
||||
exportWithDarkMode: appState.exportWithDarkMode,
|
||||
viewBackgroundColor,
|
||||
exportPadding,
|
||||
scale,
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ export const restoreElements = (
|
||||
}, [] as ExcalidrawElement[]);
|
||||
};
|
||||
|
||||
export const restoreAppState = (
|
||||
const restoreAppState = (
|
||||
appState: ImportedDataState["appState"],
|
||||
localAppState: Partial<AppState> | null,
|
||||
): AppState => {
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface ImportedDataState {
|
||||
source?: string;
|
||||
elements?: DataState["elements"] | null;
|
||||
appState?: Partial<DataState["appState"]> | null;
|
||||
scrollToCenter?: boolean;
|
||||
}
|
||||
|
||||
export interface LibraryData {
|
||||
|
||||
@@ -102,6 +102,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||
appState.showGrid,
|
||||
appState.gridSize,
|
||||
);
|
||||
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
|
||||
@@ -129,7 +130,7 @@ export class LinearElementEditor {
|
||||
isDragging &&
|
||||
(activePointIndex === 0 || activePointIndex === element.points.length - 1)
|
||||
) {
|
||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||
if (isPathALoop(element.points)) {
|
||||
LinearElementEditor.movePoint(
|
||||
element,
|
||||
activePointIndex,
|
||||
@@ -198,6 +199,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
appState.showGrid,
|
||||
appState.gridSize,
|
||||
),
|
||||
],
|
||||
@@ -282,7 +284,8 @@ export class LinearElementEditor {
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
editingLinearElement: LinearElementEditor,
|
||||
gridSize: number | null,
|
||||
isGridOn: boolean,
|
||||
gridSize: number,
|
||||
): LinearElementEditor {
|
||||
const { elementId, lastUncommittedPoint } = editingLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
@@ -304,6 +307,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||
isGridOn,
|
||||
gridSize,
|
||||
);
|
||||
|
||||
@@ -398,9 +402,15 @@ export class LinearElementEditor {
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
gridSize: number | null,
|
||||
isGridOn: boolean,
|
||||
gridSize: number,
|
||||
): Point {
|
||||
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
|
||||
const pointerOnGrid = getGridPoint(
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
isGridOn,
|
||||
gridSize,
|
||||
);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
||||
@@ -38,7 +38,6 @@ export const textWysiwyg = ({
|
||||
onSubmit,
|
||||
getViewportCoords,
|
||||
element,
|
||||
canvas,
|
||||
}: {
|
||||
id: ExcalidrawElement["id"];
|
||||
appState: AppState;
|
||||
@@ -46,7 +45,6 @@ export const textWysiwyg = ({
|
||||
onSubmit: (text: string) => void;
|
||||
getViewportCoords: (x: number, y: number) => [number, number];
|
||||
element: ExcalidrawElement;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
}) => {
|
||||
const updateWysiwygStyle = () => {
|
||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||
@@ -91,6 +89,9 @@ export const textWysiwyg = ({
|
||||
editable.dataset.type = "wysiwyg";
|
||||
// prevent line wrapping on Safari
|
||||
editable.wrap = "off";
|
||||
editable.className = `excalidraw ${
|
||||
appState.appearance === "dark" ? "Appearance_dark" : ""
|
||||
}`;
|
||||
|
||||
Object.assign(editable.style, {
|
||||
position: "fixed",
|
||||
@@ -106,8 +107,6 @@ export const textWysiwyg = ({
|
||||
overflow: "hidden",
|
||||
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
|
||||
whiteSpace: "pre",
|
||||
// must be specified because in dark mode canvas creates a stacking context
|
||||
zIndex: "var(--zIndex-wysiwyg)",
|
||||
});
|
||||
|
||||
updateWysiwygStyle();
|
||||
@@ -153,10 +152,6 @@ export const textWysiwyg = ({
|
||||
editable.oninput = null;
|
||||
editable.onkeydown = null;
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updateWysiwygStyle);
|
||||
window.removeEventListener("wheel", stopEvent, true);
|
||||
window.removeEventListener("pointerdown", onPointerDown);
|
||||
@@ -165,7 +160,7 @@ export const textWysiwyg = ({
|
||||
|
||||
unbindUpdate();
|
||||
|
||||
editable.remove();
|
||||
document.body.removeChild(editable);
|
||||
};
|
||||
|
||||
const rebindBlur = () => {
|
||||
@@ -203,27 +198,15 @@ export const textWysiwyg = ({
|
||||
let isDestroyed = false;
|
||||
|
||||
editable.onblur = handleSubmit;
|
||||
|
||||
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
||||
// 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);
|
||||
}
|
||||
|
||||
// reposition wysiwyg in case of window resize. Happens on mobile when
|
||||
// device keyboard is opened.
|
||||
window.addEventListener("resize", updateWysiwygStyle);
|
||||
window.addEventListener("pointerdown", onPointerDown);
|
||||
window.addEventListener("wheel", stopEvent, {
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
document
|
||||
.querySelector(".excalidraw-textEditorContainer")!
|
||||
.appendChild(editable);
|
||||
document.body.appendChild(editable);
|
||||
editable.focus();
|
||||
editable.select();
|
||||
};
|
||||
|
||||
@@ -19,16 +19,12 @@ import {
|
||||
} from "../app_constants";
|
||||
import {
|
||||
decryptAESGEM,
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
generateCollaborationLink,
|
||||
getCollaborationLinkData,
|
||||
SocketUpdateDataSource,
|
||||
SOCKET_SERVER,
|
||||
} from "../data";
|
||||
import {
|
||||
isSavedToFirebase,
|
||||
loadFromFirebase,
|
||||
saveToFirebase,
|
||||
} from "../data/firebase";
|
||||
import { isSavedToFirebase, saveToFirebase } from "../data/firebase";
|
||||
import {
|
||||
importUsernameFromLocalStorage,
|
||||
saveUsernameToLocalStorage,
|
||||
@@ -37,25 +33,20 @@ import {
|
||||
import Portal from "./Portal";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
import { createInverseContext } from "../../createInverseContext";
|
||||
import { t } from "../../i18n";
|
||||
import { UserIdleState } from "./types";
|
||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||
|
||||
interface CollabState {
|
||||
isCollaborating: boolean;
|
||||
modalIsShown: boolean;
|
||||
errorMessage: string;
|
||||
username: string;
|
||||
userState: UserIdleState;
|
||||
activeRoomLink: string;
|
||||
}
|
||||
|
||||
type CollabInstance = InstanceType<typeof CollabWrapper>;
|
||||
|
||||
export interface CollabAPI {
|
||||
/** function so that we can access the latest value from stale callbacks */
|
||||
isCollaborating: () => boolean;
|
||||
isCollaborating: CollabState["isCollaborating"];
|
||||
username: CollabState["username"];
|
||||
userState: CollabState["userState"];
|
||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
||||
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
||||
@@ -81,10 +72,6 @@ export { CollabContext, CollabContextConsumer };
|
||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
portal: Portal;
|
||||
excalidrawAPI: Props["excalidrawAPI"];
|
||||
isCollaborating: boolean = false;
|
||||
activeIntervalId: number | null;
|
||||
idleTimeoutId: number | null;
|
||||
|
||||
private socketInitializationTimer?: NodeJS.Timeout;
|
||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||
private collaborators = new Map<string, Collaborator>();
|
||||
@@ -92,16 +79,14 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isCollaborating: false,
|
||||
modalIsShown: false,
|
||||
errorMessage: "",
|
||||
username: importUsernameFromLocalStorage() || "",
|
||||
userState: UserIdleState.ACTIVE,
|
||||
activeRoomLink: "",
|
||||
};
|
||||
this.portal = new Portal(this);
|
||||
this.excalidrawAPI = props.excalidrawAPI;
|
||||
this.activeIntervalId = null;
|
||||
this.idleTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -125,32 +110,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||
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 = () => {
|
||||
this.destroySocketClient({ isUnload: true });
|
||||
this.destroySocketClient();
|
||||
};
|
||||
|
||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||
const syncableElements = getSyncableElements(
|
||||
this.getSceneElementsIncludingDeleted(),
|
||||
);
|
||||
|
||||
if (
|
||||
this.isCollaborating &&
|
||||
this.state.isCollaborating &&
|
||||
!isSavedToFirebase(this.portal, syncableElements)
|
||||
) {
|
||||
// this won't run in time if user decides to leave the site, but
|
||||
@@ -162,7 +133,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
event.returnValue = "";
|
||||
}
|
||||
|
||||
if (this.isCollaborating || this.portal.roomId) {
|
||||
if (this.state.isCollaborating || this.portal.roomId) {
|
||||
try {
|
||||
localStorage?.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
||||
@@ -188,192 +159,143 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
};
|
||||
|
||||
openPortal = async () => {
|
||||
return this.initializeSocketClient(null);
|
||||
window.history.pushState({}, APP_NAME, await generateCollaborationLink());
|
||||
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 = () => {
|
||||
this.saveCollabRoomToFirebase();
|
||||
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||
this.destroySocketClient();
|
||||
}
|
||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||
this.destroySocketClient();
|
||||
};
|
||||
|
||||
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||
if (!opts?.isUnload) {
|
||||
this.collaborators = new Map();
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
});
|
||||
this.setState({
|
||||
activeRoomLink: "",
|
||||
});
|
||||
this.isCollaborating = false;
|
||||
}
|
||||
private destroySocketClient = () => {
|
||||
this.collaborators = new Map();
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
});
|
||||
this.setState({
|
||||
isCollaborating: false,
|
||||
activeRoomLink: "",
|
||||
});
|
||||
this.portal.close();
|
||||
};
|
||||
|
||||
private initializeSocketClient = async (
|
||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||
): Promise<ImportedDataState | null> => {
|
||||
private initializeSocketClient = async (): Promise<ImportedDataState | null> => {
|
||||
if (this.portal.socket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let roomId;
|
||||
let roomKey;
|
||||
|
||||
if (existingRoomLinkData) {
|
||||
({ roomId, roomKey } = existingRoomLinkData);
|
||||
} else {
|
||||
({ roomId, roomKey } = await generateCollaborationLinkData());
|
||||
window.history.pushState(
|
||||
{},
|
||||
APP_NAME,
|
||||
getCollaborationLink({ roomId, roomKey }),
|
||||
);
|
||||
}
|
||||
|
||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||
|
||||
this.isCollaborating = true;
|
||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
||||
|
||||
const { default: socketIOClient }: any = await import(
|
||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||
);
|
||||
if (roomMatch) {
|
||||
const roomId = roomMatch[1];
|
||||
const roomKey = roomMatch[2];
|
||||
|
||||
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey);
|
||||
// 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);
|
||||
|
||||
if (existingRoomLinkData) {
|
||||
this.excalidrawAPI.resetScene();
|
||||
const { default: socketIOClient }: any = await import(
|
||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||
);
|
||||
|
||||
try {
|
||||
const elements = await loadFromFirebase(
|
||||
roomId,
|
||||
roomKey,
|
||||
this.portal.socket,
|
||||
);
|
||||
if (elements) {
|
||||
scenePromise.resolve({
|
||||
elements,
|
||||
scrollToCenter: true,
|
||||
});
|
||||
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");
|
||||
}
|
||||
} 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,
|
||||
this.initializeSocket();
|
||||
scenePromise.resolve(null);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isCollaborating: true,
|
||||
activeRoomLink: window.location.href,
|
||||
});
|
||||
|
||||
return scenePromise;
|
||||
}
|
||||
|
||||
// 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;
|
||||
return null;
|
||||
};
|
||||
|
||||
private initializeSocket = () => {
|
||||
@@ -437,7 +359,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
// Avoid broadcasting to the rest of the collaborators the scene
|
||||
// we just received!
|
||||
// Note: this needs to be set before updating the scene as it
|
||||
// synchronously calls render.
|
||||
// syncronously calls render.
|
||||
this.setLastBroadcastedOrReceivedSceneVersion(getSceneVersion(newElements));
|
||||
|
||||
return newElements as ReconciledElements;
|
||||
@@ -466,58 +388,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
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[]) {
|
||||
this.setState((state) => {
|
||||
const collaborators: InstanceType<
|
||||
@@ -557,11 +427,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.portal.broadcastMouseLocation(payload);
|
||||
};
|
||||
|
||||
onIdleStateChange = (userState: UserIdleState) => {
|
||||
this.setState({ userState });
|
||||
this.portal.broadcastIdleChange(userState);
|
||||
};
|
||||
|
||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
if (
|
||||
getSceneVersion(elements) >
|
||||
@@ -615,11 +480,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
/** Getter of context value. Returned object is stable. */
|
||||
getContextValue = (): CollabAPI => {
|
||||
if (!this.contextValue) {
|
||||
this.contextValue = {} as CollabAPI;
|
||||
}
|
||||
this.contextValue = this.contextValue || ({} as CollabAPI);
|
||||
|
||||
this.contextValue.isCollaborating = () => this.isCollaborating;
|
||||
this.contextValue.isCollaborating = this.state.isCollaborating;
|
||||
this.contextValue.username = this.state.username;
|
||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||
|
||||
@@ -9,7 +9,6 @@ import CollabWrapper from "./CollabWrapper";
|
||||
import { getSyncableElements } from "../../packages/excalidraw/index";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { BROADCAST, SCENE } from "../app_constants";
|
||||
import { UserIdleState } from "./types";
|
||||
|
||||
class Portal {
|
||||
collab: CollabWrapper;
|
||||
@@ -123,7 +122,7 @@ class Portal {
|
||||
data as SocketUpdateData,
|
||||
);
|
||||
|
||||
if (syncAll && this.collab.isCollaborating) {
|
||||
if (syncAll && this.collab.state.isCollaborating) {
|
||||
await Promise.all([
|
||||
broadcastPromise,
|
||||
this.collab.saveCollabRoomToFirebase(syncableElements),
|
||||
@@ -133,23 +132,6 @@ 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: {
|
||||
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
||||
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.RoomDialog-link {
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-color-primary);
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
@@ -32,29 +32,15 @@
|
||||
display: flex;
|
||||
align-items: 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 {
|
||||
background-color: var(--input-bg-color);
|
||||
background-color: var(--input-background-color);
|
||||
border-color: var(--input-border-color);
|
||||
appearance: none;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
@media #{$is-mobile-query} {
|
||||
margin-top: 0.5em;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
height: 2.5rem;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
@@ -67,7 +53,7 @@
|
||||
}
|
||||
|
||||
.Modal .RoomDialog-stopSession {
|
||||
background-color: var(--button-destructive-bg-color);
|
||||
background-color: var(--button-destructive-background-color);
|
||||
|
||||
.ToolIcon__label,
|
||||
.ToolIcon__icon svg {
|
||||
|
||||
@@ -119,11 +119,7 @@ const RoomDialog = ({
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title={t("labels.liveCollaboration")}
|
||||
>
|
||||
<Dialog small onCloseRequest={handleClose} title={t("labels.createRoom")}>
|
||||
{renderRoomDialog()}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum UserIdleState {
|
||||
ACTIVE = "active",
|
||||
AWAY = "away",
|
||||
IDLE = "idle",
|
||||
}
|
||||
@@ -148,7 +148,6 @@ export const saveToFirebase = async (
|
||||
export const loadFromFirebase = async (
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
socket: SocketIOClient.Socket | null,
|
||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||
const firebase = await getFirebase();
|
||||
const db = firebase.firestore();
|
||||
@@ -161,12 +160,5 @@ export const loadFromFirebase = async (
|
||||
const storedScene = doc.data() as FirebaseStoredScene;
|
||||
const ciphertext = storedScene.ciphertext.toUint8Array();
|
||||
const iv = storedScene.iv.toUint8Array();
|
||||
|
||||
const elements = await decryptElements(roomKey, iv, ciphertext);
|
||||
|
||||
if (socket) {
|
||||
firebaseSceneVersionCache.set(socket, getSceneVersion(elements));
|
||||
}
|
||||
|
||||
return restoreElements(elements);
|
||||
return restoreElements(await decryptElements(roomKey, iv, ciphertext));
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ImportedDataState } from "../../data/types";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { t } from "../../i18n";
|
||||
import { AppState } from "../../types";
|
||||
import { UserIdleState } from "../collab/types";
|
||||
|
||||
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
||||
|
||||
@@ -60,14 +59,6 @@ export type SocketUpdateDataSource = {
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
IDLE_STATUS: {
|
||||
type: "IDLE_STATUS";
|
||||
payload: {
|
||||
socketId: string;
|
||||
userState: UserIdleState;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type SocketUpdateDataIncoming =
|
||||
@@ -134,27 +125,17 @@ export const decryptAESGEM = async (
|
||||
};
|
||||
|
||||
export const getCollaborationLinkData = (link: string) => {
|
||||
const hash = new URL(link).hash;
|
||||
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
||||
return match ? { roomId: match[1], roomKey: match[2] } : null;
|
||||
};
|
||||
|
||||
export const generateCollaborationLinkData = async () => {
|
||||
const roomId = await generateRandomID();
|
||||
const roomKey = await generateEncryptionKey();
|
||||
|
||||
if (!roomKey) {
|
||||
throw new Error("Couldn't generate room key");
|
||||
if (link.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { roomId, roomKey };
|
||||
const hash = new URL(link).hash;
|
||||
return hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
||||
};
|
||||
|
||||
export const getCollaborationLink = (data: {
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
}) => {
|
||||
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
||||
export const generateCollaborationLink = async () => {
|
||||
const id = await generateRandomID();
|
||||
const key = await generateEncryptionKey();
|
||||
return `${window.location.origin}${window.location.pathname}#room=${id},${key}`;
|
||||
};
|
||||
|
||||
export const getImportedKey = (key: string, usage: KeyUsage) =>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ExcalidrawImperativeAPI } from "../components/App";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
||||
import { DataState, ImportedDataState } from "../data/types";
|
||||
import { ImportedDataState } from "../data/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
@@ -39,9 +39,11 @@ import CollabWrapper, {
|
||||
} from "./collab/CollabWrapper";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||
import { loadFromFirebase } from "./data/firebase";
|
||||
import {
|
||||
importFromLocalStorage,
|
||||
saveToLocalStorage,
|
||||
STORAGE_KEYS,
|
||||
} from "./data/localStorage";
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
@@ -64,9 +66,50 @@ const onBlur = () => {
|
||||
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: {
|
||||
collabAPI: CollabAPI;
|
||||
}): Promise<ImportedDataState | null> => {
|
||||
resetScene: ExcalidrawImperativeAPI["resetScene"];
|
||||
initializeSocketClient: CollabAPI["initializeSocketClient"];
|
||||
}): Promise<Scene | null> => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
const jsonMatch = window.location.hash.match(
|
||||
@@ -75,21 +118,13 @@ const initializeScene = async (opts: {
|
||||
|
||||
const initialData = importFromLocalStorage();
|
||||
|
||||
let scene: DataState & { scrollToCenter?: boolean } = await loadScene(
|
||||
null,
|
||||
null,
|
||||
initialData,
|
||||
);
|
||||
let scene = await loadScene(null, null, initialData);
|
||||
|
||||
let roomLinkData = getCollaborationLinkData(window.location.href);
|
||||
const isExternalScene = !!(id || jsonMatch || roomLinkData);
|
||||
let isCollabScene = !!getCollaborationLinkData(window.location.href);
|
||||
const isExternalScene = !!(id || jsonMatch || isCollabScene);
|
||||
if (isExternalScene) {
|
||||
if (
|
||||
// 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
|
||||
shouldForceLoadScene(scene) ||
|
||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
||||
) {
|
||||
// Backwards compatibility with legacy url format
|
||||
@@ -98,8 +133,7 @@ const initializeScene = async (opts: {
|
||||
} else if (jsonMatch) {
|
||||
scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
|
||||
}
|
||||
scene.scrollToCenter = true;
|
||||
if (!roomLinkData) {
|
||||
if (!isCollabScene) {
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
}
|
||||
} else {
|
||||
@@ -116,12 +150,38 @@ const initializeScene = async (opts: {
|
||||
});
|
||||
}
|
||||
|
||||
roomLinkData = null;
|
||||
isCollabScene = false;
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
}
|
||||
}
|
||||
if (roomLinkData) {
|
||||
return opts.collabAPI.initializeSocketClient(roomLinkData);
|
||||
if (isCollabScene) {
|
||||
// when joining a room we don't want user's local scene data to be merged
|
||||
// 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) {
|
||||
return scene;
|
||||
}
|
||||
@@ -182,16 +242,24 @@ function ExcalidrawWrapper() {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeScene({ collabAPI }).then((scene) => {
|
||||
initializeScene({
|
||||
resetScene: excalidrawAPI.resetScene,
|
||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||
}).then((scene) => {
|
||||
initialStatePromiseRef.current.promise.resolve(scene);
|
||||
});
|
||||
|
||||
const onHashChange = (_: HashChangeEvent) => {
|
||||
initializeScene({ collabAPI }).then((scene) => {
|
||||
if (scene) {
|
||||
excalidrawAPI.updateScene(scene);
|
||||
}
|
||||
});
|
||||
if (window.location.hash.length > 1) {
|
||||
initializeScene({
|
||||
resetScene: excalidrawAPI.resetScene,
|
||||
initializeSocketClient: collabAPI.initializeSocketClient,
|
||||
}).then((scene) => {
|
||||
if (scene) {
|
||||
excalidrawAPI.updateScene(scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const titleTimeout = setTimeout(
|
||||
@@ -217,13 +285,9 @@ function ExcalidrawWrapper() {
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (collabAPI?.isCollaborating()) {
|
||||
saveDebounced(elements, appState);
|
||||
if (collabAPI?.isCollaborating) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,8 +350,9 @@ function ExcalidrawWrapper() {
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
user={{ name: collabAPI?.username }}
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating()}
|
||||
isCollaborating={collabAPI?.isCollaborating}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderFooter={renderFooter}
|
||||
|
||||
Vendored
-1
@@ -12,7 +12,6 @@ interface Document {
|
||||
interface Window {
|
||||
ClipboardItem: any;
|
||||
__EXCALIDRAW_SHA__: string | undefined;
|
||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||
gtag: Function;
|
||||
}
|
||||
|
||||
|
||||
+12
-28
@@ -1,6 +1,5 @@
|
||||
import fallbackLangData from "./locales/en.json";
|
||||
import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
@@ -56,33 +55,25 @@ export const languages: Language[] = allLanguages
|
||||
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 currentLangData = {};
|
||||
|
||||
export const setLanguage = async (lang: Language) => {
|
||||
currentLang = lang;
|
||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||
document.documentElement.lang = currentLang.code;
|
||||
|
||||
if (lang.code.startsWith(TEST_LANG_CODE)) {
|
||||
currentLangData = {};
|
||||
} else {
|
||||
currentLangData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
||||
);
|
||||
}
|
||||
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;
|
||||
@@ -102,13 +93,6 @@ const findPartsForData = (data: any, parts: 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(".");
|
||||
let translation =
|
||||
findPartsForData(currentLangData, parts) ||
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ const elements = [
|
||||
},
|
||||
];
|
||||
|
||||
registerFont("./public/Virgil.woff2", { family: "Virgil" });
|
||||
registerFont("./public/Cascadia.woff2", { family: "Cascadia" });
|
||||
registerFont("./public/FG_Virgil.ttf", { family: "Virgil" });
|
||||
registerFont("./public/Cascadia.ttf", { family: "Cascadia" });
|
||||
|
||||
const canvas = exportToCanvas(
|
||||
elements as any,
|
||||
|
||||
@@ -4,6 +4,7 @@ import ExcalidrawApp from "./excalidraw-app";
|
||||
|
||||
import "./excalidraw-app/pwa";
|
||||
import "./excalidraw-app/sentry";
|
||||
|
||||
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
||||
|
||||
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "الطبقات",
|
||||
"actions": "الإجراءات",
|
||||
"language": "اللغة",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "مشاركة الجلسة مباشرة",
|
||||
"duplicateSelection": "تكرار",
|
||||
"untitled": "غير معنون",
|
||||
"name": "الاسم",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "تحديد مجموعة",
|
||||
"ungroup": "إلغاء تحديد مجموعة",
|
||||
"collaborators": "المتعاونون",
|
||||
"showGrid": "",
|
||||
"gridMode": "وضع الشبكة",
|
||||
"addToLibrary": "أضف إلى المكتبة",
|
||||
"removeFromLibrary": "حذف من المكتبة",
|
||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "تعديل",
|
||||
"undo": "تراجع",
|
||||
"redo": "إعادة تنفيذ",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "بدء المشاركة الحية",
|
||||
"createNewRoom": "إنشاء غرفة جديدة",
|
||||
"fullScreen": "شاشة كاملة",
|
||||
"darkMode": "الوضع المظلم",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "تعذر فك تشفير البيانات.",
|
||||
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "تحديد",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "التخزين",
|
||||
"title": "إحصائيات للمهووسين",
|
||||
"total": "المجموع",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "العرض"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+22
-30
@@ -8,11 +8,11 @@
|
||||
"cut": "Изрежи",
|
||||
"copy": "Копирай",
|
||||
"copyAsPng": "Копиране в клипборда",
|
||||
"copyAsSvg": "Копирано в клипборда като SVG",
|
||||
"bringForward": "Преместване напред",
|
||||
"copyAsSvg": "Копиране в клипборда",
|
||||
"bringForward": "Преместване на~пред",
|
||||
"sendToBack": "Изнасяне назад",
|
||||
"bringToFront": "Изнасяне отпред",
|
||||
"sendBackward": "Изпрати отзад",
|
||||
"bringToFront": "~Изнасяне отпред",
|
||||
"sendBackward": "Изпрати назад",
|
||||
"delete": "Изтрий",
|
||||
"copyStyles": "Копирайте стилове",
|
||||
"pasteStyles": "Постави стилове",
|
||||
@@ -68,7 +68,7 @@
|
||||
"layers": "Слоеве",
|
||||
"actions": "Действия",
|
||||
"language": "Език",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "Споделете сесия за сътрудничество на живо",
|
||||
"duplicateSelection": "Дублирай",
|
||||
"untitled": "Неозаглавено",
|
||||
"name": "Име",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Групирай селекцията",
|
||||
"ungroup": "Спри групирането на селекцията",
|
||||
"collaborators": "Сътрудници",
|
||||
"showGrid": "Показване на мрежа",
|
||||
"gridMode": "Решетъчен режим",
|
||||
"addToLibrary": "Добавяне към библиотеката",
|
||||
"removeFromLibrary": "Премахване от библиотеката",
|
||||
"libraryLoadingMessage": "Зареждане на библиотеката…",
|
||||
@@ -92,7 +92,7 @@
|
||||
"centerHorizontally": "Центрирай хоризонтално",
|
||||
"distributeHorizontally": "Разпредели хоризонтално",
|
||||
"distributeVertically": "Разпредели вертикално",
|
||||
"viewMode": "Изглед"
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Нулиране на платно",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Редактиране",
|
||||
"undo": "Отмяна",
|
||||
"redo": "Повтори",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "Започнете сътрудничество на живо",
|
||||
"createNewRoom": "Създай нова стая",
|
||||
"fullScreen": "На цял екран",
|
||||
"darkMode": "Тъмен режим",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "Данните не можаха да се дешифрират.",
|
||||
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
||||
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
||||
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
||||
"errorLoadingLibrary": "Възникна грешка при зареждането на външна библиотека.",
|
||||
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
||||
"imageDoesNotContainScene": "Импортирането на картинки не се поддържва в момента.\n\nИскате да импортнете сцена? Тази картинка не съдържа данни от сцена. Разрешили ли сте последното при експортирането?",
|
||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Селекция",
|
||||
@@ -203,24 +201,24 @@
|
||||
"title": "Грешка"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Прочетете нашия блог",
|
||||
"blog": "",
|
||||
"click": "клик",
|
||||
"curvedArrow": "Извита стрелка",
|
||||
"curvedLine": "Извита линия",
|
||||
"documentation": "Документация",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "плъзнете",
|
||||
"editor": "Редактор",
|
||||
"github": "Намерихте проблем? Изпратете",
|
||||
"howto": "Следвайте нашите ръководства",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "или",
|
||||
"preventBinding": "Спри прилепяне на стрелките",
|
||||
"preventBinding": "",
|
||||
"shapes": "Фигури",
|
||||
"shortcuts": "Клавиши за бърз достъп",
|
||||
"textFinish": "Завършете редактирането (текст)",
|
||||
"textNewLine": "Добавяне на нов ред (текст)",
|
||||
"title": "Помощ",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "Преглед",
|
||||
"zoomToFit": "Приближи докато се виждат всички елементи",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": "Приближи селекцията"
|
||||
},
|
||||
"encrypted": {
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Съхранение на данни",
|
||||
"title": "Статистика за хакери",
|
||||
"total": "Общо",
|
||||
"version": "Версия",
|
||||
"versionCopy": "Настисни за да копираш",
|
||||
"versionNotAvailable": "Версията не е налична",
|
||||
"width": "Широчина"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Копирани стилове.",
|
||||
"copyToClipboard": "Копирано в клипборда.",
|
||||
"copyToClipboardAsPng": "Копирано в клипборда като PNG.",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+6
-14
@@ -68,7 +68,7 @@
|
||||
"layers": "Capes",
|
||||
"actions": "Accions",
|
||||
"language": "Llengua",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "Compartir una sessió de col·laboració en directe",
|
||||
"duplicateSelection": "Duplicar",
|
||||
"untitled": "Sense títol",
|
||||
"name": "Nom",
|
||||
@@ -77,10 +77,10 @@
|
||||
"group": "Agrupar la selecció",
|
||||
"ungroup": "Desagrupar la selecció",
|
||||
"collaborators": "Col·laboradors",
|
||||
"showGrid": "",
|
||||
"gridMode": "Mode quadrícula",
|
||||
"addToLibrary": "Afegir a la biblioteca",
|
||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||
"libraryLoadingMessage": "Carregant la biblioteca…",
|
||||
"libraryLoadingMessage": "Carregant la biblioteca...",
|
||||
"libraries": "Explorar biblioteques",
|
||||
"loadingScene": "Carregant escena…",
|
||||
"align": "Alinear",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Editar",
|
||||
"undo": "Desfer",
|
||||
"redo": "Refer",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "Començar col·laboració en directe",
|
||||
"createNewRoom": "Crear sala nova",
|
||||
"fullScreen": "Pantalla completa",
|
||||
"darkMode": "Mode fosc",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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ó?",
|
||||
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selecció",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Emmagatzematge",
|
||||
"title": "Estadístiques per nerds",
|
||||
"total": "Total",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "Amplada"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+7
-15
@@ -68,7 +68,7 @@
|
||||
"layers": "Ebenen",
|
||||
"actions": "Aktionen",
|
||||
"language": "Sprache",
|
||||
"liveCollaboration": "Live-Zusammenarbeit",
|
||||
"createRoom": "Live-Kollaborationssitzung teilen",
|
||||
"duplicateSelection": "Duplizieren",
|
||||
"untitled": "Unbenannt",
|
||||
"name": "Name",
|
||||
@@ -76,8 +76,8 @@
|
||||
"madeWithExcalidraw": "Made with Excalidraw",
|
||||
"group": "Auswahl gruppieren",
|
||||
"ungroup": "Gruppierung aufheben",
|
||||
"collaborators": "Kollaboratoren",
|
||||
"showGrid": "Raster anzeigen",
|
||||
"collaborators": "Mitarbeitende",
|
||||
"gridMode": "Rastermodus",
|
||||
"addToLibrary": "Zur Bibliothek hinzufügen",
|
||||
"removeFromLibrary": "Aus Bibliothek entfernen",
|
||||
"libraryLoadingMessage": "Lade Bibliothek…",
|
||||
@@ -101,7 +101,7 @@
|
||||
"exportToSvg": "Als SVG exportieren",
|
||||
"copyToClipboard": "In Zwischenablage kopieren",
|
||||
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
||||
"scale": "Skalierung",
|
||||
"scale": "Skalieren",
|
||||
"save": "Speichern",
|
||||
"saveAs": "Speichern unter",
|
||||
"load": "Laden",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Bearbeiten",
|
||||
"undo": "Rückgängig machen",
|
||||
"redo": "Wiederholen",
|
||||
"resetLibrary": "Bibliothek zurücksetzen",
|
||||
"roomDialog": "Live-Kollaborationssitzung starten",
|
||||
"createNewRoom": "Neuen Raum erstellen",
|
||||
"fullScreen": "Vollbildanzeige",
|
||||
"darkMode": "Dunkles Design",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
|
||||
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?"
|
||||
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Auswahl",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Speicher",
|
||||
"title": "Statistiken für Nerds",
|
||||
"total": "Gesamt",
|
||||
"version": "Version",
|
||||
"versionCopy": "Zum Kopieren klicken",
|
||||
"versionNotAvailable": "Version nicht verfügbar",
|
||||
"width": "Breite"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Formatierung kopiert.",
|
||||
"copyToClipboard": "In die Zwischenablage kopiert.",
|
||||
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert.",
|
||||
"fileSaved": "Datei gespeichert.",
|
||||
"fileSavedToFilename": "Als {filename} gespeichert"
|
||||
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert."
|
||||
}
|
||||
}
|
||||
|
||||
+10
-18
@@ -68,7 +68,7 @@
|
||||
"layers": "Στρώματα",
|
||||
"actions": "Ενέργειες",
|
||||
"language": "Γλώσσα",
|
||||
"liveCollaboration": "Ζωντανή συνεργασία",
|
||||
"createRoom": "Έναρξη ζωντανής συνεδρίας",
|
||||
"duplicateSelection": "Δημιουργία αντιγράφου",
|
||||
"untitled": "Χωρίς τίτλο",
|
||||
"name": "Όνομα",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Δημιουργία ομάδας από επιλογή",
|
||||
"ungroup": "Κατάργηση ομάδας από επιλογή",
|
||||
"collaborators": "Συνεργάτες",
|
||||
"showGrid": "Προβολή πλέγματος",
|
||||
"gridMode": "Εμφάνιση σε πλέγμα",
|
||||
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
|
||||
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
|
||||
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης…",
|
||||
@@ -117,13 +117,13 @@
|
||||
"edit": "Επεξεργασία",
|
||||
"undo": "Αναίρεση",
|
||||
"redo": "Επαναφορά",
|
||||
"resetLibrary": "Καθαρισμός βιβλιοθήκης",
|
||||
"roomDialog": "Έναρξη ζωντανής συνεργασίας",
|
||||
"createNewRoom": "Δημιουργία νέου χώρου",
|
||||
"fullScreen": "Πλήρης οθόνη",
|
||||
"darkMode": "Σκοτεινή λειτουργία",
|
||||
"lightMode": "Φωτεινή λειτουργία",
|
||||
"zenMode": "Λειτουργία Zεν",
|
||||
"exitZenMode": "Έξοδος από την λειτουργία Zen"
|
||||
"exitZenMode": "Έξοδος απο την λειτουργία Zen"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
||||
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
||||
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
||||
"collabStopOverridePrompt": "Η διακοπή της συνεδρίας θα αντικαταστήσει το προηγούμενο, τοπικά αποθηκευμένο σχέδιο. Είστε σίγουροι?\n\n(Αν θέλετε να διατηρήσετε το τοπικό σας σχέδιο, απλά κλείστε την καρτέλα του προγράμματος περιήγησης.)",
|
||||
"errorLoadingLibrary": "Υπήρξε ένα σφάλμα κατά τη φόρτωση της βιβλιοθήκης τρίτου μέρους.",
|
||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβιλιοθήκη σας. Είστε σίγουροι;",
|
||||
"imageDoesNotContainScene": "Η εισαγωγή εικόνων δεν υποστηρίζεται αυτή τη στιγμή.\n\nΜήπως θέλετε να εισαγάγετε μια σκηνή; Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει αυτό κατά την εξαγωγή;",
|
||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;"
|
||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Επιλογή",
|
||||
@@ -162,7 +160,7 @@
|
||||
},
|
||||
"hints": {
|
||||
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
||||
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||
"freeDraw": "Κάντε κλικ και σύρατε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
||||
@@ -185,7 +183,7 @@
|
||||
"clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ",
|
||||
"trackedToSentry_pre": "Το σφάλμα με αναγνωριστικό ",
|
||||
"trackedToSentry_post": " παρακολουθήθηκε στο σύστημά μας.",
|
||||
"openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας ",
|
||||
"openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψουν να ακολουθήσεις το δικό μας ",
|
||||
"openIssueMessage_button": "ανιχνευτής σφαλμάτων.",
|
||||
"openIssueMessage_post": " Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.",
|
||||
"sceneContent": "Περιεχόμενο σκηνής:"
|
||||
@@ -224,7 +222,7 @@
|
||||
"zoomToSelection": "Ζουμ στην επιλογή"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Γωνία",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Χώρος",
|
||||
"title": "Στατιστικά για σπασίκλες",
|
||||
"total": "Σύνολο ",
|
||||
"version": "Έκδοση",
|
||||
"versionCopy": "Κάνε κλικ για αντιγραφή",
|
||||
"versionNotAvailable": "Έκδοση μη διαθέσιμη",
|
||||
"width": "Πλάτος"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Αντιγράφηκαν στυλ.",
|
||||
"copyToClipboard": "Αντιγράφηκε στο πρόχειρο.",
|
||||
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG.",
|
||||
"fileSaved": "Το αρχείο αποθηκεύτηκε.",
|
||||
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}"
|
||||
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+6
-15
@@ -68,7 +68,7 @@
|
||||
"layers": "Layers",
|
||||
"actions": "Actions",
|
||||
"language": "Language",
|
||||
"liveCollaboration": "Live collaboration",
|
||||
"createRoom": "Share a live-collaboration session",
|
||||
"duplicateSelection": "Duplicate",
|
||||
"untitled": "Untitled",
|
||||
"name": "Name",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Group selection",
|
||||
"ungroup": "Ungroup selection",
|
||||
"collaborators": "Collaborators",
|
||||
"showGrid": "Show grid",
|
||||
"gridMode": "Grid mode",
|
||||
"addToLibrary": "Add to library",
|
||||
"removeFromLibrary": "Remove from library",
|
||||
"libraryLoadingMessage": "Loading library…",
|
||||
@@ -92,8 +92,7 @@
|
||||
"centerHorizontally": "Center horizontally",
|
||||
"distributeHorizontally": "Distribute horizontally",
|
||||
"distributeVertically": "Distribute vertically",
|
||||
"viewMode": "View mode",
|
||||
"toggleExportColorScheme": "Toggle export color scheme"
|
||||
"viewMode": "View mode"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
@@ -118,7 +117,7 @@
|
||||
"edit": "Edit",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"resetLibrary": "Reset library",
|
||||
"roomDialog": "Start live collaboration",
|
||||
"createNewRoom": "Create new room",
|
||||
"fullScreen": "Full screen",
|
||||
"darkMode": "Dark mode",
|
||||
@@ -137,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Scene couldn't be restored from this image file",
|
||||
"resetLibrary": "This will clear your library. Are you sure?"
|
||||
"cannotRestoreFromImage": "Scene couldn't be restored from this image file"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selection",
|
||||
@@ -237,16 +234,10 @@
|
||||
"storage": "Storage",
|
||||
"title": "Stats for nerds",
|
||||
"total": "Total",
|
||||
"version": "Version",
|
||||
"versionCopy": "Click to copy",
|
||||
"versionNotAvailable": "Version not available",
|
||||
"width": "Width"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Copied styles.",
|
||||
"copyToClipboard": "Copied to clipboard.",
|
||||
"copyToClipboardAsPng": "Copied to clipboard as PNG.",
|
||||
"fileSaved": "File saved.",
|
||||
"fileSavedToFilename": "Saved to {filename}"
|
||||
"copyToClipboardAsPng": "Copied to clipboard as PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+7
-15
@@ -68,7 +68,7 @@
|
||||
"layers": "Capas",
|
||||
"actions": "Acciones",
|
||||
"language": "Idioma",
|
||||
"liveCollaboration": "Colaboración en directo",
|
||||
"createRoom": "Compartir una sesión de colaboración en vivo",
|
||||
"duplicateSelection": "Duplicar",
|
||||
"untitled": "Sin título",
|
||||
"name": "Nombre",
|
||||
@@ -77,12 +77,12 @@
|
||||
"group": "Agrupar selección",
|
||||
"ungroup": "Desagrupar selección",
|
||||
"collaborators": "Colaboradores",
|
||||
"showGrid": "Mostrar cuadrícula",
|
||||
"gridMode": "Modo cuadrícula",
|
||||
"addToLibrary": "Añadir a la biblioteca",
|
||||
"removeFromLibrary": "Eliminar de la biblioteca",
|
||||
"libraryLoadingMessage": "Cargando librería…",
|
||||
"libraryLoadingMessage": "Cargando biblioteca...",
|
||||
"libraries": "Explorar bibliotecas",
|
||||
"loadingScene": "Cargando escena…",
|
||||
"loadingScene": "Cargando escena...",
|
||||
"align": "Alinear",
|
||||
"alignTop": "Alineación superior",
|
||||
"alignBottom": "Alineación inferior",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Editar",
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer",
|
||||
"resetLibrary": "Resetear librería",
|
||||
"roomDialog": "Iniciar colaboración en vivo",
|
||||
"createNewRoom": "Crear nueva sala",
|
||||
"fullScreen": "Pantalla completa",
|
||||
"darkMode": "Modo oscuro",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen",
|
||||
"resetLibrary": "Esto eliminará tu librería. ¿Estás seguro?"
|
||||
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selección",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Almacenamiento",
|
||||
"title": "Estadísticas para nerds",
|
||||
"total": "Total",
|
||||
"version": "Versión",
|
||||
"versionCopy": "Clic para copiar",
|
||||
"versionNotAvailable": "Versión no disponible",
|
||||
"width": "Ancho"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Estilos copiados.",
|
||||
"copyToClipboard": "Copiado en el portapapeles.",
|
||||
"copyToClipboardAsPng": "Copiado al portapapeles como PNG.",
|
||||
"fileSaved": "Archivo guardado.",
|
||||
"fileSavedToFilename": "Guardado en {filename}"
|
||||
"copyToClipboardAsPng": "Copiado al portapapeles como PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "لایه ها",
|
||||
"actions": "عملیات",
|
||||
"language": "زبان",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "اشتراک گذاری جلسه همکاری زنده",
|
||||
"duplicateSelection": "تکرار",
|
||||
"untitled": "بدون عنوان",
|
||||
"name": "نام",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "گروهبندی انتخابها",
|
||||
"ungroup": "حذف گروهبندی انتخابها",
|
||||
"collaborators": "همکاران",
|
||||
"showGrid": "",
|
||||
"gridMode": "حالت شبکه ای",
|
||||
"addToLibrary": "افزودن به کتابخانه",
|
||||
"removeFromLibrary": "حذف از کتابخانه",
|
||||
"libraryLoadingMessage": "بارگذاری کتابخانه…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "ویرایش",
|
||||
"undo": "بازگرد",
|
||||
"redo": "از سر",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "همکاری آنلاین را شروع کنید",
|
||||
"createNewRoom": "ایجاد یک اتاق جدید",
|
||||
"fullScreen": "تمامصفحه",
|
||||
"darkMode": "حالت تیره",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
||||
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
||||
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "خطایی در بارگذاری کتابخانه ثالث وجود داشت.",
|
||||
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
||||
"imageDoesNotContainScene": "وارد کردن تصویر در این لحظه امکان پذیر نمی باشد.\nآیا مایل به وارد کردن یک صحنه هستید؟ این تصویر به نظر می رسد که فاقد هرگونه اطلاعاتی مربوط به صحنه باشد. آیا این گزینه را در زمان وارد کردن تصویر فعال کرده اید؟",
|
||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "گزینش",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "حافظه",
|
||||
"title": "آمار برای نردها",
|
||||
"total": "مجموع",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "عرض"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "کپی سبک.",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG.",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "Tasot",
|
||||
"actions": "Toiminnot",
|
||||
"language": "Kieli",
|
||||
"liveCollaboration": "Live-yhteistyö",
|
||||
"createRoom": "Jaa yhteistyöistunto",
|
||||
"duplicateSelection": "Monista",
|
||||
"untitled": "Nimetön",
|
||||
"name": "Nimi",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Ryhmitä valinta",
|
||||
"ungroup": "Pura valittu ryhmä",
|
||||
"collaborators": "Yhteistyökumppanit",
|
||||
"showGrid": "Näytä ruudukko",
|
||||
"gridMode": "Ruudukkotila",
|
||||
"addToLibrary": "Lisää kirjastoon",
|
||||
"removeFromLibrary": "Poista kirjastosta",
|
||||
"libraryLoadingMessage": "Ladataan kirjastoa…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Muokkaa",
|
||||
"undo": "Kumoa",
|
||||
"redo": "Tee uudelleen",
|
||||
"resetLibrary": "Tyhjennä kirjasto",
|
||||
"roomDialog": "Aloita live-yhteistyö",
|
||||
"createNewRoom": "Luo huone",
|
||||
"fullScreen": "Koko näyttö",
|
||||
"darkMode": "Tumma tila",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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öä.",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta",
|
||||
"resetLibrary": "Tämä tyhjentää kirjastosi. Oletko varma?"
|
||||
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Valinta",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Tallennustila",
|
||||
"title": "Nörttien tilastot",
|
||||
"total": "Yhteensä",
|
||||
"version": "Versio",
|
||||
"versionCopy": "Klikkaa kopioidaksesi",
|
||||
"versionNotAvailable": "Versio ei saatavilla",
|
||||
"width": "Leveys"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Tyylit kopioitu.",
|
||||
"copyToClipboard": "Kopioitu leikepöydälle.",
|
||||
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona.",
|
||||
"fileSaved": "Tiedosto tallennettu.",
|
||||
"fileSavedToFilename": "Tallennettu kohteeseen {filename}"
|
||||
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona."
|
||||
}
|
||||
}
|
||||
|
||||
+7
-15
@@ -68,7 +68,7 @@
|
||||
"layers": "Calques",
|
||||
"actions": "Actions",
|
||||
"language": "Langue",
|
||||
"liveCollaboration": "Collaboration en direct",
|
||||
"createRoom": "Partager une session de collaboration en direct",
|
||||
"duplicateSelection": "Dupliquer",
|
||||
"untitled": "Sans-titre",
|
||||
"name": "Nom",
|
||||
@@ -77,12 +77,12 @@
|
||||
"group": "Grouper la sélection",
|
||||
"ungroup": "Dégrouper la sélection",
|
||||
"collaborators": "Collaborateurs",
|
||||
"showGrid": "Afficher la grille",
|
||||
"gridMode": "Mode grille",
|
||||
"addToLibrary": "Ajouter à 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",
|
||||
"loadingScene": "Chargement de la scène…",
|
||||
"loadingScene": "Chargement de la scène...",
|
||||
"align": "Aligner",
|
||||
"alignTop": "Aligner en haut",
|
||||
"alignBottom": "Aligner en bas",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Modifier",
|
||||
"undo": "Annuler",
|
||||
"redo": "Rétablir",
|
||||
"resetLibrary": "Réinitialiser la bibliothèque",
|
||||
"roomDialog": "Démarrer la collaboration en direct",
|
||||
"createNewRoom": "Créer une nouvelle salle",
|
||||
"fullScreen": "Plein écran",
|
||||
"darkMode": "Mode sombre",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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 ?",
|
||||
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image",
|
||||
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?"
|
||||
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Sélection",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Stockage",
|
||||
"title": "Stats pour les nerds",
|
||||
"total": "Total",
|
||||
"version": "Version",
|
||||
"versionCopy": "Cliquer pour copier",
|
||||
"versionNotAvailable": "Version non disponible",
|
||||
"width": "Largeur"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Styles copiés.",
|
||||
"copyToClipboard": "Copié vers le presse-papiers.",
|
||||
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG.",
|
||||
"fileSaved": "Fichier enregistré.",
|
||||
"fileSavedToFilename": "Enregistré sous {filename}"
|
||||
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "שכבות",
|
||||
"actions": "פעולות",
|
||||
"language": "שפה",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "התחל שיתוף פעולה חי",
|
||||
"duplicateSelection": "שכפל",
|
||||
"untitled": "ללא כותרת",
|
||||
"name": "שם",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "אחד לקבוצה",
|
||||
"ungroup": "פרק קבוצה",
|
||||
"collaborators": "שותפים",
|
||||
"showGrid": "",
|
||||
"gridMode": "מצב רשת",
|
||||
"addToLibrary": "הוסף לספריה",
|
||||
"removeFromLibrary": "הסר מספריה",
|
||||
"libraryLoadingMessage": "טוען ספריה…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "ערוך",
|
||||
"undo": "בטל",
|
||||
"redo": "בצע מחדש",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "התחל שיתוף חי",
|
||||
"createNewRoom": "צור חדר",
|
||||
"fullScreen": "מסך מלא",
|
||||
"darkMode": "מצב כהה",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "לא ניתן לפענח מידע.",
|
||||
"uploadedSecurly": "ההעלאה הוצפנה מקצה לקצה, ולכן שרת Excalidraw וצד שלישי לא יכולים לקרוא את התוכן.",
|
||||
"loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "קרתה שגיאה בטעינת הספריה החיצונית.",
|
||||
"confirmAddLibrary": "הפעולה תוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?",
|
||||
"imageDoesNotContainScene": "אין תמיכה בייבוא תמונות כעת.\n\nהאם אתה רוצה לייבא תצוגה? התמונה הזאת אינה מכילה מידע על תצוגה. האם הפעלת את האפשרות הזאת בזמן הוצאת המידע?",
|
||||
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "בחירה",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "אחסון",
|
||||
"title": "סטטיסטיקות לחנונים",
|
||||
"total": "סה״כ",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "רוחב"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+24
-32
@@ -68,7 +68,7 @@
|
||||
"layers": "परतें",
|
||||
"actions": "कार्रवाई",
|
||||
"language": "भाषा",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "अधिवेशन",
|
||||
"duplicateSelection": "डुप्लिकेट",
|
||||
"untitled": "अशीर्षित",
|
||||
"name": "नाम",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "समूह चयन",
|
||||
"ungroup": "समूह चयन असमूहीकृत करें",
|
||||
"collaborators": "सहयोगी",
|
||||
"showGrid": "",
|
||||
"gridMode": "ग्रिड मॉड",
|
||||
"addToLibrary": "लाइब्रेरी से जोड़ें",
|
||||
"removeFromLibrary": "लाइब्रेरी से निकालें",
|
||||
"libraryLoadingMessage": "लाइब्रेरी खुल रही है",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "संशोधन करें",
|
||||
"undo": "पूर्ववत् करें",
|
||||
"redo": "फिर से करें",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "लाइव सहयोग शुरू करें",
|
||||
"createNewRoom": "एक नया कमरा बनाएं",
|
||||
"fullScreen": "पूरी स्क्रीन",
|
||||
"darkMode": "डार्क मोड",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "डेटा को डिक्रिप्ट नहीं किया जा सका।",
|
||||
"uploadedSecurly": "अपलोड को एंड-टू-एंड एन्क्रिप्शन के साथ सुरक्षित किया गया है, जिसका मतलब है कि एक्सक्लूसिव सर्वर और थर्ड पार्टी कंटेंट नहीं पढ़ सकते हैं।",
|
||||
"loadSceneOverridePrompt": "लोड हो रहा है बाहरी ड्राइंग आपके मौजूदा सामग्री को बदल देगा। क्या आप जारी रखना चाहते हैं?",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "लाइब्रेरी लोड करने में त्रुटि",
|
||||
"confirmAddLibrary": "लाइब्रेरी जोड़ें पुष्टि करें आकार संख्या",
|
||||
"imageDoesNotContainScene": "दृश्य में छवि नहीं है",
|
||||
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "चयन",
|
||||
@@ -203,25 +201,25 @@
|
||||
"title": "गलती"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "हमारा ब्लॉग पढे",
|
||||
"click": "क्लिक करें",
|
||||
"curvedArrow": "वक्र तीर",
|
||||
"curvedLine": "वक्र रेखा",
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "खींचें",
|
||||
"editor": "संपादक",
|
||||
"github": "मुद्दा मिला? प्रस्तुत करें",
|
||||
"howto": "हमारे गाइड का पालन करें",
|
||||
"or": "या",
|
||||
"preventBinding": "तीर बंधन रोकें",
|
||||
"shapes": "आकृतियाँ",
|
||||
"shortcuts": "कीबोर्ड के शॉर्टकट्स",
|
||||
"textFinish": "संपादन समाप्त करें (पाठ)",
|
||||
"textNewLine": "नई पंक्ति जोड़ें (पाठ)",
|
||||
"title": "मदद",
|
||||
"view": "दृश्य",
|
||||
"zoomToFit": "सभी तत्वों को फिट करने के लिए ज़ूम करें",
|
||||
"zoomToSelection": "चयन तक ज़ूम करे"
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "संग्रह",
|
||||
"title": "बेवकूफ के लिए आँकड़े",
|
||||
"total": "कुल",
|
||||
"version": "संस्करण",
|
||||
"versionCopy": "काॅपी करने के लिए क्लिक करें",
|
||||
"versionNotAvailable": "संस्करण उपलब्ध नहीं है",
|
||||
"width": "चौड़ाई"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "काॅपी कीए स्टाइल",
|
||||
"copyToClipboard": "क्लिपबोर्ड में कॉपी कीए",
|
||||
"copyToClipboardAsPng": "क्लिपबोर्ड में PNG के रूप में कॉपी किए",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "Rétegek",
|
||||
"actions": "Műveletek",
|
||||
"language": "Nyelv",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "Élő együttmüködés megosztása",
|
||||
"duplicateSelection": "Duplikálás",
|
||||
"untitled": "Névtelen",
|
||||
"name": "Név",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Csoportosítás",
|
||||
"ungroup": "Csoportbontás",
|
||||
"collaborators": "Közreműködők",
|
||||
"showGrid": "",
|
||||
"gridMode": "Hálómód",
|
||||
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
||||
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
||||
"libraryLoadingMessage": "Könyvtár betöltése…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Szerkesztés",
|
||||
"undo": "Vissza",
|
||||
"redo": "Újra",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "Élő együttműködés indítása",
|
||||
"createNewRoom": "Új szoba létrehozása",
|
||||
"fullScreen": "Teljes képernyő",
|
||||
"darkMode": "Sötét mód",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Kijelölés",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Tárhely",
|
||||
"title": "Statisztikák",
|
||||
"total": "Összesen",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "Szélesség"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "Lapisan",
|
||||
"actions": "Aksi",
|
||||
"language": "Bahasa",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "Bagikan sesi kolaborasi langsung",
|
||||
"duplicateSelection": "Duplikat",
|
||||
"untitled": "Tanpa judul",
|
||||
"name": "Nama",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Kelompokan pilihan",
|
||||
"ungroup": "Pisahkan pilihan",
|
||||
"collaborators": "Kolaborator",
|
||||
"showGrid": "Tampilkan grid",
|
||||
"gridMode": "Mode grid",
|
||||
"addToLibrary": "Tambahkan ke pustaka",
|
||||
"removeFromLibrary": "Hapus dari pustaka",
|
||||
"libraryLoadingMessage": "Memuat pustaka…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Edit",
|
||||
"undo": "Urungkan",
|
||||
"redo": "Ulangi",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "Mulai kolaborasi langsung",
|
||||
"createNewRoom": "Buat ruang baru",
|
||||
"fullScreen": "Layar penuh",
|
||||
"darkMode": "Mode gelap",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Pilihan",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Penyimpanan",
|
||||
"title": "Statistik untuk nerd",
|
||||
"total": "Total",
|
||||
"version": "Versi",
|
||||
"versionCopy": "Klik untuk salin",
|
||||
"versionNotAvailable": "Versi tidak tersedia",
|
||||
"width": "Lebar"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Gaya tersalin.",
|
||||
"copyToClipboard": "Tersalin ke papan klip.",
|
||||
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG.",
|
||||
"fileSaved": "File tersimpan.",
|
||||
"fileSavedToFilename": "Disimpan ke {filename}"
|
||||
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+6
-14
@@ -68,7 +68,7 @@
|
||||
"layers": "Livelli",
|
||||
"actions": "Azioni",
|
||||
"language": "Lingua",
|
||||
"liveCollaboration": "Collaborazione live",
|
||||
"createRoom": "Condividi una sessione di collaborazione in diretta",
|
||||
"duplicateSelection": "Duplica",
|
||||
"untitled": "Senza titolo",
|
||||
"name": "Nome",
|
||||
@@ -77,10 +77,10 @@
|
||||
"group": "Crea gruppo da selezione",
|
||||
"ungroup": "Dividi gruppo da selezione",
|
||||
"collaborators": "Collaboratori",
|
||||
"showGrid": "Visualizza griglia",
|
||||
"gridMode": "Modalità griglia",
|
||||
"addToLibrary": "Aggiungi alla libreria",
|
||||
"removeFromLibrary": "Rimuovi dalla libreria",
|
||||
"libraryLoadingMessage": "Caricamento libreria…",
|
||||
"libraryLoadingMessage": "Caricamento della biblioteca…",
|
||||
"libraries": "Sfoglia librerie",
|
||||
"loadingScene": "Caricamento della scena…",
|
||||
"align": "Allinea",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Modifica",
|
||||
"undo": "Annulla",
|
||||
"redo": "Ripeti",
|
||||
"resetLibrary": "Ripristina libreria",
|
||||
"roomDialog": "Inizia collaborazione in diretta",
|
||||
"createNewRoom": "Crea nuova stanza",
|
||||
"fullScreen": "Schermo intero",
|
||||
"darkMode": "Tema scuro",
|
||||
@@ -136,12 +136,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine",
|
||||
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?"
|
||||
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selezione",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Memoria",
|
||||
"title": "Statistiche per nerd",
|
||||
"total": "Totale",
|
||||
"version": "Versione",
|
||||
"versionCopy": "Clicca per copiare",
|
||||
"versionNotAvailable": "Versione non disponibile",
|
||||
"width": "Larghezza"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Stili copiati.",
|
||||
"copyToClipboard": "Copiato negli appunti.",
|
||||
"copyToClipboardAsPng": "Copiato negli appunti come PNG.",
|
||||
"fileSaved": "File salvato.",
|
||||
"fileSavedToFilename": "Salvato in {filename}"
|
||||
"copyToClipboardAsPng": "Copiato negli appunti come PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+43
-51
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "貼り付け",
|
||||
"pasteCharts": "チャートの貼り付け",
|
||||
"pasteCharts": "",
|
||||
"selectAll": "すべて選択",
|
||||
"multiSelect": "複数選択",
|
||||
"moveCanvas": "キャンバスを移動",
|
||||
"cut": "切り取り",
|
||||
"cut": "",
|
||||
"copy": "コピー",
|
||||
"copyAsPng": "PNGとしてクリップボードへコピー",
|
||||
"copyAsSvg": "SVGとしてクリップボードへコピー",
|
||||
@@ -38,7 +38,7 @@
|
||||
"fontSize": "フォントの大きさ",
|
||||
"fontFamily": "フォントの種類",
|
||||
"onlySelected": "選択中のみ",
|
||||
"withBackground": "背景を含める",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "エクスポートされたファイルにシーンを埋め込みます",
|
||||
"exportEmbedScene_details": "シーンデータはエクスポートされたPNG/SVGファイルに保存され、シーンを復元することができます。\nエクスポートされたファイルのサイズは増加します。",
|
||||
"addWatermark": "\"Made with Excalidraw\"と表示",
|
||||
@@ -68,20 +68,20 @@
|
||||
"layers": "レイヤー",
|
||||
"actions": "操作",
|
||||
"language": "言語",
|
||||
"liveCollaboration": "ライブ連携",
|
||||
"createRoom": "共同編集セッションの共有",
|
||||
"duplicateSelection": "複製",
|
||||
"untitled": "無題",
|
||||
"untitled": "",
|
||||
"name": "名前",
|
||||
"yourName": "あなたの名前",
|
||||
"madeWithExcalidraw": "Excalidrawで作成",
|
||||
"group": "図形のグループ化",
|
||||
"ungroup": "グループ化を解除",
|
||||
"collaborators": "共同編集者",
|
||||
"showGrid": "グリッドを表示",
|
||||
"gridMode": "",
|
||||
"addToLibrary": "ライブラリに追加",
|
||||
"removeFromLibrary": "ライブラリから削除",
|
||||
"libraryLoadingMessage": "ライブラリを読み込み中…",
|
||||
"libraries": "ライブラリを参照する",
|
||||
"libraries": "",
|
||||
"loadingScene": "シーンを読み込み中…",
|
||||
"align": "整列",
|
||||
"alignTop": "上揃え",
|
||||
@@ -90,9 +90,9 @@
|
||||
"alignRight": "右揃え",
|
||||
"centerVertically": "縦方向に中央揃え",
|
||||
"centerHorizontally": "横方向に中央揃え",
|
||||
"distributeHorizontally": "水平方向に分散配置",
|
||||
"distributeVertically": "垂直方向に分散配置",
|
||||
"viewMode": "閲覧モード"
|
||||
"distributeHorizontally": "",
|
||||
"distributeVertically": "",
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "キャンバスのリセット",
|
||||
@@ -117,12 +117,12 @@
|
||||
"edit": "編集",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直し",
|
||||
"resetLibrary": "ライブラリをリセット",
|
||||
"roomDialog": "共同編集を開始する",
|
||||
"createNewRoom": "新しい部屋を作成する",
|
||||
"fullScreen": "全画面表示",
|
||||
"darkMode": "ダークモード",
|
||||
"lightMode": "ライトモード",
|
||||
"zenMode": "Zenモード",
|
||||
"zenMode": "",
|
||||
"exitZenMode": "集中モードをやめる"
|
||||
},
|
||||
"alerts": {
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "データを復号できませんでした。",
|
||||
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
|
||||
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
|
||||
"collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザタブを閉じてください。)",
|
||||
"errorLoadingLibrary": "サードパーティライブラリの読み込み中にエラーが発生しました。",
|
||||
"confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?",
|
||||
"imageDoesNotContainScene": "現在、画像のインポートはサポートされていません。\n\nシーンをインポートしようとしましたか?この画像にはシーンデータが含まれていないようです。エクスポート中に有効にしていましたか?",
|
||||
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした",
|
||||
"resetLibrary": "ライブラリを消去します。本当によろしいですか?"
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "選択",
|
||||
@@ -165,7 +163,7 @@
|
||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||
"linearElementMulti": "最後のポイントをクリックするか、エスケープまたはEnterを押して終了します",
|
||||
"lockAngle": "SHIFTを押したままにすると、角度を制限することができます",
|
||||
"lockAngle": "",
|
||||
"resize": "サイズを変更中にSHIFTを押しすと比率を制御できます。Altを押すと中央からサイズを変更できます。",
|
||||
"rotate": "回転中にSHIFT キーを押すと角度を制限することができます",
|
||||
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
|
||||
@@ -203,49 +201,43 @@
|
||||
"title": "エラー"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "公式ブログを読む",
|
||||
"click": "クリック",
|
||||
"curvedArrow": "カーブした矢印",
|
||||
"curvedLine": "曲線",
|
||||
"documentation": "ドキュメント",
|
||||
"drag": "ドラッグ",
|
||||
"editor": "エディタ",
|
||||
"github": "不具合報告はこちら",
|
||||
"howto": "ヘルプ・マニュアル",
|
||||
"or": "または",
|
||||
"preventBinding": "矢印を結合しない",
|
||||
"shapes": "図形",
|
||||
"shortcuts": "キーボードショートカット",
|
||||
"textFinish": "編集を終了する (テキスト)",
|
||||
"textNewLine": "新しい行を追加 (テキスト)",
|
||||
"title": "ヘルプ",
|
||||
"view": "表示",
|
||||
"zoomToFit": "すべての要素が収まるようにズーム",
|
||||
"zoomToSelection": "選択要素にズーム"
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "角度",
|
||||
"element": "要素",
|
||||
"elements": "要素",
|
||||
"angle": "",
|
||||
"element": "",
|
||||
"elements": "",
|
||||
"height": "高さ",
|
||||
"scene": "シーン",
|
||||
"selected": "選択済み",
|
||||
"storage": "ストレージ",
|
||||
"title": "マニア向け統計情報",
|
||||
"scene": "",
|
||||
"selected": "",
|
||||
"storage": "",
|
||||
"title": "",
|
||||
"total": "合計",
|
||||
"version": "バージョン",
|
||||
"versionCopy": "クリックしてコピー",
|
||||
"versionNotAvailable": "利用できないバージョン",
|
||||
"width": "幅"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "スタイルをコピー",
|
||||
"copyToClipboard": "クリップボードにコピー",
|
||||
"copyToClipboardAsPng": "PNG形式でクリップボードにコピー",
|
||||
"fileSaved": "ファイルを保存しました",
|
||||
"fileSavedToFilename": "{filename} に保存しました"
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"strokeStyle_solid": "Aččuran",
|
||||
"strokeStyle_dashed": "S tjerriḍin",
|
||||
"strokeStyle_dotted": "S tenqiḍin",
|
||||
"sloppiness": "Astehzi",
|
||||
"sloppiness": "",
|
||||
"opacity": "Tiḍullest",
|
||||
"textAlign": "Areyyec n uḍris",
|
||||
"edges": "Leryuf",
|
||||
@@ -68,7 +68,7 @@
|
||||
"layers": "Tissiyin",
|
||||
"actions": "Tigawin",
|
||||
"language": "Tutlayt",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "Bḍu tiɣimit n umɛawen s srid",
|
||||
"duplicateSelection": "Sisleg",
|
||||
"untitled": "War azwel",
|
||||
"name": "Isem",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Segrew tafrayt",
|
||||
"ungroup": "Kkess asegrew i tefrayt",
|
||||
"collaborators": "Imɛiwnen",
|
||||
"showGrid": "Beqqeḍ aferrug",
|
||||
"gridMode": "Askar n uferrug",
|
||||
"addToLibrary": "Rnu ɣer temkarḍit",
|
||||
"removeFromLibrary": "Kkes si temkarḍit",
|
||||
"libraryLoadingMessage": "Asali n temkarḍit…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Ẓreg",
|
||||
"undo": "Sefsex",
|
||||
"redo": "Err-d",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "Bdu amɛawen s srid",
|
||||
"createNewRoom": "Snulfu-d taxxamt tamaynutt",
|
||||
"fullScreen": "Agdil aččuran",
|
||||
"darkMode": "Askar imsulles",
|
||||
@@ -130,18 +130,16 @@
|
||||
"couldNotCreateShareableLink": "D awezɣi asnulfu n useɣwen n beṭṭu.",
|
||||
"couldNotCreateShareableLinkTooBig": "D awezɣi asnulfu n useɣwen n beṭṭu. Asayes ɣezzif aṭas",
|
||||
"couldNotLoadInvalidFile": "D awezɣi asali n ufaylu armeɣtu",
|
||||
"importBackendFailed": "Takterḍ seg uɣawas n deffir ur teddi ara.",
|
||||
"importBackendFailed": "",
|
||||
"cannotExportEmptyCanvas": "D awezɣi asifeḍ n teɣzut n usuneɣ tilemt.",
|
||||
"couldNotCopyToClipboard": "D awezɣi anɣal ɣer tecfawit. Eɛreḍ ad tesqedceḍ iminig Chrome.",
|
||||
"decryptFailed": "D awezɣi tukksa n uwgelhen i yisefka.",
|
||||
"uploadedSecurly": "Asili yettwasɣelles s uwgelhen ixef s ixef, ayagi yebɣa ad d-yini belli aqeddac n Excalidraw akked medden ur zmiren ara ad ɣren agbur.",
|
||||
"loadSceneOverridePrompt": "Asali n wunuɣ uffiɣ ad isemselsi agbur-inek (m) yellan. Tebɣiḍ ad tkemmeleḍ?",
|
||||
"collabStopOverridePrompt": "Aḥbas n tɣimit ad yesefsex unuɣ-inek (m) yettwaḥerzen yakan s wudem adigan. Tetḥeqqeḍ?\n(Ma tebɣiḍ ad teǧǧeḍ unuɣ-inek (m) adigan, mdel iccer n yiminig, deg umḍiq.)",
|
||||
"errorLoadingLibrary": "Teḍra-d tuccḍa deg usali n temkarḍit n wis kraḍ.",
|
||||
"confirmAddLibrary": "Ayagi adirnu talɣa (win) {{numShapes}} ɣer temkarḍit-inek (m). Tetḥeqqeḍ?",
|
||||
"imageDoesNotContainScene": "Taktert n tugniwin ur tettwadhel ara akka tura.\nTebɣiḍ ad tketreḍ asayes? Tugna-agi tettban-d ur tegbir ara isefka n usnas. Tesremdeḍ ayagi deg usifeḍ?",
|
||||
"cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Tafrayt",
|
||||
@@ -185,7 +183,7 @@
|
||||
"clearCanvasCaveat": " Ayagi ad d-iglu s usṛuḥu n umahil ",
|
||||
"trackedToSentry_pre": "Tuccḍa akked umesmagi ",
|
||||
"trackedToSentry_post": " tettwasekles deg unagraw-nneɣ.",
|
||||
"openIssueMessage_pre": "Nḥuder aṭas akken ur nseddu ara talɣut n usayes-inek (m) di tuccḍa. Ma yella asayes-inek (m) mačči d amaẓlay, ttxil-k (m) xemmem ad ḍefreḍ ",
|
||||
"openIssueMessage_pre": "",
|
||||
"openIssueMessage_button": "afecku n weḍfar n yibugen.",
|
||||
"openIssueMessage_post": " Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.",
|
||||
"sceneContent": "Agbur n usayes:"
|
||||
@@ -213,7 +211,7 @@
|
||||
"github": "Tufiḍ-d ugur? Azen-aɣ-d",
|
||||
"howto": "Ḍfer imniren-nneɣ",
|
||||
"or": "neɣ",
|
||||
"preventBinding": "Seḥbes tuqqna n tneccabin",
|
||||
"preventBinding": "",
|
||||
"shapes": "Talɣiwin",
|
||||
"shortcuts": "Inegzumen n unasiw",
|
||||
"textFinish": "Fak asiẓreg (aḍris)",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Aḥraz",
|
||||
"title": "",
|
||||
"total": "Aɣrud",
|
||||
"version": "Alqem",
|
||||
"versionCopy": "Sit ad tneɣleḍ",
|
||||
"versionNotAvailable": "Ur inuḥ ulqem",
|
||||
"width": "Tehri"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Iɣunab yettwaneɣlen.",
|
||||
"copyToClipboard": "Yettwaɣel ɣer tecfawit.",
|
||||
"copyToClipboardAsPng": "Yettwanɣel ɣer tecfawit am PNG.",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": "Yettwanɣel ɣer tecfawit am PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+28
-36
@@ -68,7 +68,7 @@
|
||||
"layers": "레이어",
|
||||
"actions": "동작",
|
||||
"language": "언어",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "실시간 협업 세션 공유",
|
||||
"duplicateSelection": "복제",
|
||||
"untitled": "제목 없음",
|
||||
"name": "이름",
|
||||
@@ -77,12 +77,12 @@
|
||||
"group": "그룹 생성",
|
||||
"ungroup": "그룹 해제",
|
||||
"collaborators": "공동 작업자",
|
||||
"showGrid": "",
|
||||
"gridMode": "격자 방식",
|
||||
"addToLibrary": "라이브러리에 추가",
|
||||
"removeFromLibrary": "라이브러리에서 제거",
|
||||
"libraryLoadingMessage": "라이브러리 불러오는 중…",
|
||||
"libraryLoadingMessage": "라이브러리 불러오는 중...",
|
||||
"libraries": "라이브러리 찾기",
|
||||
"loadingScene": "화면 불러오는 중…",
|
||||
"loadingScene": "화면 불러오는 중...",
|
||||
"align": "정렬",
|
||||
"alignTop": "상단 정렬",
|
||||
"alignBottom": "하단 정렬",
|
||||
@@ -92,7 +92,7 @@
|
||||
"centerHorizontally": "수평으로 중앙 정렬",
|
||||
"distributeHorizontally": "수평으로 분배",
|
||||
"distributeVertically": "수직으로 분배",
|
||||
"viewMode": "보기 모드"
|
||||
"viewMode": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "캔버스 초기화",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "수정",
|
||||
"undo": "실행 취소",
|
||||
"redo": "다시 실행",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "실시간 협업 시작하기",
|
||||
"createNewRoom": "방 만들기",
|
||||
"fullScreen": "전체화면",
|
||||
"darkMode": "다크 모드",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "데이터를 복호화하지 못했습니다.",
|
||||
"uploadedSecurly": "업로드는 종단 간 암호화로 보호되므로 Excalidraw 서버 및 타사가 콘텐츠를 읽을 수 없습니다.",
|
||||
"loadSceneOverridePrompt": "외부 파일을 불러 오면 기존 콘텐츠가 대체됩니다. 계속 진행할까요?",
|
||||
"collabStopOverridePrompt": "협업 세션을 종료하면 로컬 저장소에 있는 그림이 협업 세션의 그림으로 대체됩니다. 진행하겠습니까?\n\n(로컬 저장소에 있는 그림을 유지하려면 현재 브라우저 탭을 닫아주세요.)",
|
||||
"errorLoadingLibrary": "외부 라이브러리를 불러오는 중에 문제가 발생했습니다.",
|
||||
"confirmAddLibrary": "{{numShapes}}개의 모양이 라이브러리에 추가됩니다. 계속하시겠어요?",
|
||||
"imageDoesNotContainScene": "이미지에서 불러오기는 현재 지원되지 않습니다.\n\n화면을 불러오려고 하셨나요? 이미지에 화면 정보가 없는 것 같습니다. 내보낼 때 화면을 포함했나요?",
|
||||
"cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "선택",
|
||||
@@ -203,25 +201,25 @@
|
||||
"title": "오류"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "블로그 읽어보기",
|
||||
"click": "클릭",
|
||||
"curvedArrow": "곡선 화살표",
|
||||
"curvedLine": "곡선",
|
||||
"documentation": "설명서",
|
||||
"drag": "드래그",
|
||||
"editor": "에디터",
|
||||
"github": "문제 제보하기",
|
||||
"howto": "가이드 참고하기",
|
||||
"or": "또는",
|
||||
"preventBinding": "화살표가 붙지 않게 하기",
|
||||
"shapes": "도형",
|
||||
"shortcuts": "키보드 단축키",
|
||||
"textFinish": "편집 완료 (텍스트)",
|
||||
"textNewLine": "줄바꿈 (텍스트)",
|
||||
"title": "도움말",
|
||||
"view": "보기",
|
||||
"zoomToFit": "모든 요소가 보이도록 확대/축소",
|
||||
"zoomToSelection": "선택 영역으로 확대/축소"
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "저장공간",
|
||||
"title": "덕후들을 위한 통계",
|
||||
"total": "합계",
|
||||
"version": "버전",
|
||||
"versionCopy": "복사하려면 클릭",
|
||||
"versionNotAvailable": "해당 버전 사용 불가능",
|
||||
"width": "너비"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "스타일 복사.",
|
||||
"copyToClipboard": "클립보드로 복사.",
|
||||
"copyToClipboardAsPng": "클립보드로 PNG 이미지 복사.",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyStyles": "",
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "အလွှာများ",
|
||||
"actions": "လုပ်ဆောင်ချက်များ",
|
||||
"language": "ဘာသာစကား",
|
||||
"liveCollaboration": "",
|
||||
"createRoom": "တိုက်ရိုက်ပူးပေါင်းဆောင်ရွက်ရန်အဖွဲ့ဖွဲ့",
|
||||
"duplicateSelection": "ပွား",
|
||||
"untitled": "အမည်မရှိ",
|
||||
"name": "အမည်",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "အုပ်စုဖွဲ့",
|
||||
"ungroup": "အုပ်စုဖျက်သိမ်း",
|
||||
"collaborators": "ပူးပေါင်းပါဝင်သူများ",
|
||||
"showGrid": "",
|
||||
"gridMode": "",
|
||||
"addToLibrary": "မှတ်တမ်းတင်",
|
||||
"removeFromLibrary": "မှတ်တမ်းမှထုတ်",
|
||||
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "ပြင်ဆင်",
|
||||
"undo": "ပြန်ထား",
|
||||
"redo": "ထပ်လုပ်",
|
||||
"resetLibrary": "",
|
||||
"roomDialog": "တိုက်ရိုက်ပူးပေါင်းမှုစတင်",
|
||||
"createNewRoom": "အခန်းသစ်ဖွဲ့",
|
||||
"fullScreen": "",
|
||||
"darkMode": "",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "အချက်အလက်ဖော်ယူ၍မရပါ။",
|
||||
"uploadedSecurly": "တင်သွင်းအချက်အလက်များအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်စနစ်အသုံးပြု၍လုံခြုံစွာထိန်းသိမ်းထားပါသဖြင့် Excalidraw ဆာဗာနှင့်ဆက်စပ်အဖွဲ့အစည်းများပင်လျှင်မဖတ်ရှုနိုင်ပါ။",
|
||||
"loadSceneOverridePrompt": "လက်ရှိရေးဆွဲထားသမျှအား ပြင်ပမှတင်သွင်းသောပုံနှင့်အစားထိုးပါမည်။ ဆက်လက်ဆောင်ရွက်လိုပါသလား။",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "ပြင်ပမှမှတ်တမ်းအားတင်သွင်းရာတွင်အမှားအယွင်းရှိနေသည်။",
|
||||
"confirmAddLibrary": "{{numShapes}} ခုသောပုံသဏ္ဌာန်အားမှတ်တမ်းတင်ပါမည်။ အတည်ပြုပါ။",
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။",
|
||||
"resetLibrary": ""
|
||||
"cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "ရွေးချယ်",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "သိုလှောင်ခန်း",
|
||||
"title": "အက္ခရာများအတွက်အချက်အလက်များ",
|
||||
"total": "စုစုပေါင်း",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "အကျယ်"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": ""
|
||||
"copyToClipboardAsPng": ""
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -68,7 +68,7 @@
|
||||
"layers": "Lag",
|
||||
"actions": "Handlinger",
|
||||
"language": "Språk",
|
||||
"liveCollaboration": "Sanntids-samarbeid",
|
||||
"createRoom": "Del en sanntids-samarbeidsøkt",
|
||||
"duplicateSelection": "Dupliser",
|
||||
"untitled": "Uten navn",
|
||||
"name": "Navn",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Gruppér utvalg",
|
||||
"ungroup": "Avgruppér utvalg",
|
||||
"collaborators": "Samarbeidspartnere",
|
||||
"showGrid": "Vis rutenett",
|
||||
"gridMode": "Rutevisning",
|
||||
"addToLibrary": "Legg til i bibliotek",
|
||||
"removeFromLibrary": "Fjern fra bibliotek",
|
||||
"libraryLoadingMessage": "Laster bibliotek…",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Rediger",
|
||||
"undo": "Angre",
|
||||
"redo": "Gjør om",
|
||||
"resetLibrary": "Nullstill bibliotek",
|
||||
"roomDialog": "Start sanntids-samarbeid",
|
||||
"createNewRoom": "Opprett et nytt rom",
|
||||
"fullScreen": "Fullskjerm",
|
||||
"darkMode": "Mørk modus",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "Kunne ikke dekryptere data.",
|
||||
"uploadedSecurly": "Opplastingen er kryptert og kan ikke leses av Excalidraw-serveren eller tredjeparter.",
|
||||
"loadSceneOverridePrompt": "Å laste inn ekstern tegning vil erstatte det eksisterende innholdet. Ønsker du å fortsette?",
|
||||
"collabStopOverridePrompt": "Hvis du slutter økten, overskrives din forrige, lokalt lagrede tegning. Er du sikker?\n\n(Hvis du ønsker å beholde din lokale tegning, bare lukk nettleserfanen i stedet.)",
|
||||
"errorLoadingLibrary": "Det oppstod en feil under lasting av tredjepartsbiblioteket.",
|
||||
"confirmAddLibrary": "Dette vil legge til {{numShapes}} figur(er) i biblioteket ditt. Er du sikker?",
|
||||
"imageDoesNotContainScene": "Importering av bilder støttes ikke for øyeblikket.\n\nVil du importere en scene? Dette bildet ser ikke ut til å inneholde noen scene-data. Har du aktivert dette under eksporten?",
|
||||
"cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen",
|
||||
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?"
|
||||
"cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Velg",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Lagring",
|
||||
"title": "Statistikk for nerder",
|
||||
"total": "Totalt",
|
||||
"version": "Versjon",
|
||||
"versionCopy": "Klikk for å kopiere",
|
||||
"versionNotAvailable": "Versjon ikke tilgjengelig",
|
||||
"width": "Bredde"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Kopierte stiler.",
|
||||
"copyToClipboard": "Kopiert til utklippstavlen.",
|
||||
"copyToClipboardAsPng": "Kopiert til utklippstavlen som PNG.",
|
||||
"fileSaved": "Fil lagret.",
|
||||
"fileSavedToFilename": "Lagret til {filename}"
|
||||
"copyToClipboardAsPng": "Kopiert til utklippstavlen som PNG."
|
||||
}
|
||||
}
|
||||
|
||||
+7
-15
@@ -68,7 +68,7 @@
|
||||
"layers": "Lagen",
|
||||
"actions": "Acties",
|
||||
"language": "Taal",
|
||||
"liveCollaboration": "Live Samenwerking",
|
||||
"createRoom": "Deel een live-samenwerkingssessie",
|
||||
"duplicateSelection": "Dupliceer",
|
||||
"untitled": "Naamloos",
|
||||
"name": "Naam",
|
||||
@@ -77,12 +77,12 @@
|
||||
"group": "Groeperen",
|
||||
"ungroup": "Groep opheffen",
|
||||
"collaborators": "Deelnemers",
|
||||
"showGrid": "Raster weergeven",
|
||||
"gridMode": "Rasterweergave",
|
||||
"addToLibrary": "Voeg toe aan bibliotheek",
|
||||
"removeFromLibrary": "Verwijder uit bibliotheek",
|
||||
"libraryLoadingMessage": "Bibliotheek laden…",
|
||||
"libraryLoadingMessage": "Bibliotheek laden...",
|
||||
"libraries": "Blader door bibliotheken",
|
||||
"loadingScene": "Scène laden…",
|
||||
"loadingScene": "Scène laden...",
|
||||
"align": "Uitlijnen",
|
||||
"alignTop": "Boven uitlijnen",
|
||||
"alignBottom": "Onder uitlijnen",
|
||||
@@ -117,7 +117,7 @@
|
||||
"edit": "Bewerken",
|
||||
"undo": "Ongedaan maken",
|
||||
"redo": "Herstel ongedaan maken",
|
||||
"resetLibrary": "Bibliotheek Resetten",
|
||||
"roomDialog": "Live-samenwerkingssessie starten",
|
||||
"createNewRoom": "Creëer live-samenwerkingssessie",
|
||||
"fullScreen": "Volledig scherm",
|
||||
"darkMode": "Donkere modus",
|
||||
@@ -136,12 +136,10 @@
|
||||
"decryptFailed": "Kan gegevens niet decoderen.",
|
||||
"uploadedSecurly": "De upload is beveiligd met end-to-end encryptie, wat betekent dat de Excalidraw server en derden de inhoud niet kunnen lezen.",
|
||||
"loadSceneOverridePrompt": "Het laden van externe tekening zal uw bestaande inhoud vervangen. Wil je doorgaan?",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "Bij het laden van de externe bibliotheek is een fout opgetreden.",
|
||||
"confirmAddLibrary": "Hiermee worden {{numShapes}} vorm(n) aan uw bibliotheek toegevoegd. Ben je het zeker?",
|
||||
"imageDoesNotContainScene": "Afbeeldingen importeren wordt op dit moment niet ondersteund.\n\nWil je een scène importeren? Deze afbeelding lijkt geen scène gegevens te bevatten. Heb je dit geactiveerd tijdens het exporteren?",
|
||||
"cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand",
|
||||
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?"
|
||||
"cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selectie",
|
||||
@@ -236,16 +234,10 @@
|
||||
"storage": "Opslag",
|
||||
"title": "Statistieken voor nerds",
|
||||
"total": "Totaal",
|
||||
"version": "Versie",
|
||||
"versionCopy": "Klik om te kopiëren",
|
||||
"versionNotAvailable": "Versie niet beschikbaar",
|
||||
"width": "Breedte"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Stijlen gekopieerd.",
|
||||
"copyToClipboard": "Gekopieerd naar het klembord.",
|
||||
"copyToClipboardAsPng": "Gekopieerd naar klembord als PNG.",
|
||||
"fileSaved": "Bestand opgeslagen.",
|
||||
"fileSavedToFilename": "Opgeslagen als {filename}"
|
||||
"copyToClipboardAsPng": "Gekopieerd naar klembord als PNG."
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user