mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-02 21:59:59 +02:00
Compare commits
80 Commits
preserve-a
...
aakansha-r
Author | SHA1 | Date | |
---|---|---|---|
![]() |
313097233d | ||
![]() |
2b049b4a65 | ||
![]() |
339212e563 | ||
![]() |
f8b4bb66b4 | ||
![]() |
f4312bba5e | ||
![]() |
ac66665b64 | ||
![]() |
2b71a1f0bd | ||
![]() |
58845e450a | ||
![]() |
15d79f8fee | ||
![]() |
958ebeae61 | ||
![]() |
31f51ca53b | ||
![]() |
5abbf73050 | ||
![]() |
7cf766630d | ||
![]() |
59fccafeac | ||
![]() |
19a6996e6b | ||
![]() |
86c4f90910 | ||
![]() |
4d88112021 | ||
![]() |
de5c63e299 | ||
![]() |
da0853a121 | ||
![]() |
57cc4b6a29 | ||
![]() |
e2ddd7b27a | ||
![]() |
693de8501e | ||
![]() |
c6df6d444e | ||
![]() |
ad5692c5f8 | ||
![]() |
60ab3337af | ||
![]() |
dd847793d2 | ||
![]() |
6d6e9f0dd3 | ||
![]() |
0fe0d7ca6b | ||
![]() |
abcf1f1bae | ||
![]() |
7d0b03f754 | ||
![]() |
bd8931d3d1 | ||
![]() |
0d86c04939 | ||
![]() |
8436ebbf68 | ||
![]() |
824f94b3df | ||
![]() |
f9a8e686b2 | ||
![]() |
e442a44ba8 | ||
![]() |
f1fd29571a | ||
![]() |
6a482a7ba2 | ||
![]() |
bfea434a55 | ||
![]() |
acb22c5a64 | ||
![]() |
7cd1b621d1 | ||
![]() |
9c37b25bab | ||
![]() |
a8bb9a78ef | ||
![]() |
e4aff04061 | ||
![]() |
c5cadc7de3 | ||
![]() |
7dc0c0d96a | ||
![]() |
2c9c8c8e05 | ||
![]() |
b5d7ae57e5 | ||
![]() |
0f66ee3a41 | ||
![]() |
771372c66b | ||
![]() |
a7937681e9 | ||
![]() |
792f238d16 | ||
![]() |
ba16416c75 | ||
![]() |
6e0ac52a64 | ||
![]() |
5bc40402a6 | ||
![]() |
df14c69977 | ||
![]() |
1ea67ba93d | ||
![]() |
a7153d9d1d | ||
![]() |
e885057a71 | ||
![]() |
7efa081976 | ||
![]() |
5deb93a083 | ||
![]() |
e3908e6fe3 | ||
![]() |
fe3d0b5e8b | ||
![]() |
b6bb74d08d | ||
![]() |
c725f84334 | ||
![]() |
11a3380d83 | ||
![]() |
76a5bb060e | ||
![]() |
dac8dda4d4 | ||
![]() |
a1a62468a6 | ||
![]() |
ba3a723e99 | ||
![]() |
c5355c08cf | ||
![]() |
6102380051 | ||
![]() |
655e59a707 | ||
![]() |
d05745070b | ||
![]() |
88c313bf86 | ||
![]() |
a7705848ec | ||
![]() |
69e1bae8dd | ||
![]() |
d361757e4a | ||
![]() |
0ef202f2df | ||
![]() |
bbfd2b3cd3 |
@@ -11,3 +11,12 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
|
||||
REACT_APP_PORTAL_URL=
|
||||
|
||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||
|
||||
# put these in your .env.local, or make sure you don't commit!
|
||||
# must be lowercase `true` when turned on
|
||||
#
|
||||
# whether to enable Service Workers in development
|
||||
REACT_APP_DEV_ENABLE_SW=
|
||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||
# debugging Service Workers.
|
||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
|
37
.github/dependabot.yml
vendored
37
.github/dependabot.yml
vendored
@@ -1,37 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
reviewers:
|
||||
- lipis
|
||||
assignees:
|
||||
- lipis
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /src/packages/excalidraw/
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
reviewers:
|
||||
- ad1992
|
||||
assignees:
|
||||
- ad1992
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /src/packages/utils/
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
time: "01:00"
|
||||
reviewers:
|
||||
- ad1992
|
||||
assignees:
|
||||
- ad1992
|
||||
open-pull-requests-limit: 20
|
2
.github/workflows/autorelease-excalidraw.yml
vendored
2
.github/workflows/autorelease-excalidraw.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Auto release @excalidraw/excalidraw-next
|
||||
name: Auto release excalidraw next
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
2
.github/workflows/autorelease-preview.yml
vendored
2
.github/workflows/autorelease-preview.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Auto release preview @excalidraw/excalidraw-preview
|
||||
name: Auto release excalidraw preview
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
15
.github/workflows/publish-docker.yml
vendored
15
.github/workflows/publish-docker.yml
vendored
@@ -10,11 +10,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/build-push-action@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: excalidraw/excalidraw
|
||||
tag_with_ref: true
|
||||
tag_with_sha: true
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: excalidraw/excalidraw:latest
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,7 +19,6 @@ logs
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
static
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
src/packages/excalidraw/types
|
||||
|
20
dev-docs/.gitignore
vendored
Normal file
20
dev-docs/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
41
dev-docs/README.md
Normal file
41
dev-docs/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
3
dev-docs/babel.config.js
Normal file
3
dev-docs/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
};
|
6
dev-docs/docs/codebase/overview.md
Normal file
6
dev-docs/docs/codebase/overview.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Overview
|
||||
---
|
||||
|
||||
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).
|
8
dev-docs/docs/get-started.md
Normal file
8
dev-docs/docs/get-started.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
|
||||
|
||||
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).
|
6
dev-docs/docs/package/overview.md
Normal file
6
dev-docs/docs/package/overview.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Overview
|
||||
---
|
||||
|
||||
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).
|
121
dev-docs/docusaurus.config.js
Normal file
121
dev-docs/docusaurus.config.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: "Excalidraw developer docs",
|
||||
tagline:
|
||||
"For Excalidraw contributors or those integrating the Excalidraw editor",
|
||||
url: "https://docs.excalidraw.com.com",
|
||||
baseUrl: "/",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
favicon: "img/favicon.ico",
|
||||
organizationName: "Excalidraw", // Usually your GitHub org/user name.
|
||||
projectName: "excalidraw", // Usually your repo name.
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
// Please change this to your repo.
|
||||
editUrl: "https://github.com/excalidraw/docs/tree/master/",
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve("./src/css/custom.css"),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: "Excalidraw Docs",
|
||||
logo: {
|
||||
alt: "Excalidraw Logo",
|
||||
src: "img/logo.svg",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
docId: "get-started",
|
||||
position: "left",
|
||||
label: "Get started",
|
||||
},
|
||||
{
|
||||
to: "https://blog.excalidraw.com",
|
||||
label: "Blog",
|
||||
position: "left",
|
||||
},
|
||||
{
|
||||
to: "https://github.com/excalidraw/excalidraw",
|
||||
label: "GitHub",
|
||||
position: "right",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: "dark",
|
||||
links: [
|
||||
{
|
||||
title: "Docs",
|
||||
items: [
|
||||
{
|
||||
label: "Get Started",
|
||||
to: "/docs/get-started",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
items: [
|
||||
{
|
||||
label: "Discord",
|
||||
href: "https://discord.gg/UexuTaE",
|
||||
},
|
||||
{
|
||||
label: "Twitter",
|
||||
href: "https://twitter.com/excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "More",
|
||||
items: [
|
||||
{
|
||||
label: "Blog",
|
||||
to: "https://blog.excalidraw.com",
|
||||
},
|
||||
{
|
||||
label: "GitHub",
|
||||
to: "https://github.com/excalidraw/excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Made with ❤️ Built with Docusaurus`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
46
dev-docs/package.json
Normal file
46
dev-docs/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start --port 3003",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-rc.1",
|
||||
"@docusaurus/preset-classic": "2.0.0-rc.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
31
dev-docs/sidebars.js
Normal file
31
dev-docs/sidebars.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
62
dev-docs/src/components/Homepage/index.js
Normal file
62
dev-docs/src/components/Homepage/index.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: "Learn how Excalidraw works",
|
||||
Svg: require("@site/static/img/undraw_innovative.svg").default,
|
||||
description: (
|
||||
<>Want to contribute to Excalidraw but got lost in the codebase?</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Integrate Excalidraw",
|
||||
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Want to build your own app powered by Excalidraw by don't know where to
|
||||
start?
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Help us improve",
|
||||
Svg: require("@site/static/img/undraw_add_files.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Are the docs missing something? Anything you had trouble understanding
|
||||
or needs an explanation? Come contribute to the docs to make them even
|
||||
better!
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ Svg, title, description }) {
|
||||
return (
|
||||
<div className={clsx("col col--4")}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
70
dev-docs/src/components/Homepage/index.tsx
Normal file
70
dev-docs/src/components/Homepage/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
Svg: React.ComponentType<React.ComponentProps<"svg">>;
|
||||
description: JSX.Element;
|
||||
};
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: "Easy to Use",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Focus on What Matters",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Powered by React",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx("col col--4")}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures(): JSX.Element {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
43
dev-docs/src/css/custom.css
Normal file
43
dev-docs/src/css/custom.css
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #6965db;
|
||||
--ifm-color-primary-dark: #5b57d1;
|
||||
--ifm-color-primary-darker: #5b57d1;
|
||||
--ifm-color-primary-darkest: #4a47b1;
|
||||
--ifm-color-primary-light: #5b57d1;
|
||||
--ifm-color-primary-lighter: #5b57d1;
|
||||
--ifm-color-primary-lightest: #5b57d1;
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme="dark"] {
|
||||
--ifm-color-primary: #5650f0;
|
||||
--ifm-color-primary-dark: #4b46d8;
|
||||
--ifm-color-primary-darker: #4b46d8;
|
||||
--ifm-color-primary-darkest: #3e39be;
|
||||
--ifm-color-primary-light: #3f3d64;
|
||||
--ifm-color-primary-lighter: #3f3d64;
|
||||
--ifm-color-primary-lightest: #3f3d64;
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .navbar__logo {
|
||||
filter: invert(93%) hue-rotate(180deg);
|
||||
}
|
42
dev-docs/src/pages/index.js
Normal file
42
dev-docs/src/pages/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/get-started"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
27
dev-docs/src/pages/index.module.css
Normal file
27
dev-docs/src/pages/index.module.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
*/
|
||||
|
||||
.heroBanner {
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .heroBanner {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
42
dev-docs/src/pages/index.tsx
Normal file
42
dev-docs/src/pages/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/get-started"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
7
dev-docs/src/pages/markdown-page.md
Normal file
7
dev-docs/src/pages/markdown-page.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
0
dev-docs/static/.nojekyll
Normal file
0
dev-docs/static/.nojekyll
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
dev-docs/static/img/favicon.ico
Normal file
BIN
dev-docs/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
4
dev-docs/static/img/logo.svg
Normal file
4
dev-docs/static/img/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 80 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||
<path d="M22.197 150.382c-4.179-3.359-10.618-9.051-15.702-13.946l-4.01-3.813.734-5.009c.396-2.732 1.13-8.083 1.582-11.839.508-3.757 1.017-7.286 1.186-7.798.226-.683 0-1.025-.621-1.025-1.073 0-1.13.285 1.807-9.107a617.602 617.602 0 0 1 2.203-7.229c.113-.398.565-.569 1.073-.398.508.227.791.683.621 1.081-.169.455.113.911.565 1.082.621.227.565.683-.395 2.333-1.525 2.562-5.422 24.419-5.648 31.477-.17 5.009-.17 5.066 1.92 7.912 2.033 2.789 6.721 7.001 13.951 12.351 2.033 1.537 4.067 3.245 4.631 3.814.848 1.024 1.243.74 8.36-6.887 4.123-4.383 8.698-8.88 10.166-10.018l2.711-2.049-2.089-4.44c-1.13-2.391-5.705-11.612-10.223-20.377-9.433-18.442-7.513-16.678-18.47-16.849l-7.117-.056-2.372-2.733c-2.485-2.903-2.824-3.984-1.638-5.805.452-.627.791-1.651.791-2.277 0-1.025.395-1.196 2.655-1.309 1.412-.057 2.711-.228 2.88-.399.17-.171.396-3.7.565-7.855l.226-7.513-3.784-8.197C2.485 39.844 0 33.583 0 31.533c0-1.081.226-1.992.452-1.992.565 0 .565.057 23.553 48.382 10.675 22.426 20.785 43.544 22.479 47.016 1.695 3.472 3.22 6.659 3.333 7.115.113.512-3.785 4.439-9.998 9.961-5.591 5.008-10.505 9.562-10.957 10.074-1.299 1.594-3.219 1.082-6.665-1.707Zm1.921-65.458c-2.599-5.066-2.712-5.123-9.828-5.464-6.27-.342-6.383-.285-6.383.911 0 .683-.226 1.593-.508 2.049-.339.512-.113 1.423.678 2.675l1.242 1.935h5.649c3.106.057 6.664.285 7.907.512 1.243.228 2.316.342 2.429.285.113-.057-.452-1.366-1.186-2.903Zm-4.745-9.107c-.452-1.195-1.638-3.7-2.598-5.578-1.581-3.188-1.751-3.301-2.146-1.992-.226.797-.396 3.13-.452 5.236-.057 4.155-.17 4.098 4.575 4.383l1.525.057-.904-2.106Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||
<path d="M23.892 136.835c-1.017-.74-1.299-1.48-1.299-3.358 0-2.22.169-2.562 1.694-3.188 1.525-.626 1.92-.569 3.671.626 2.316 1.594 2.373 1.992.678 4.554-1.468 2.22-2.937 2.618-4.744 1.366Zm3.219-2.049c.904-1.594.339-2.789-1.355-2.789-1.525 0-2.203 1.536-1.356 3.073.678 1.253 1.977 1.139 2.711-.284ZM59.306 124.028c0-.285-.339-.569-.735-.569-.339 0-1.299-1.594-2.033-3.529-2.259-5.92-24.852-50.943-24.908-49.52 0 .74-.339 1.252-.904 1.252-.791 0-.904-.456-.565-2.675.339-2.562.113-3.131-7.907-18.841-4.519-8.936-9.376-18.271-10.788-20.775-1.469-2.619-2.598-5.465-2.711-6.66-.17-2.049.056-2.334 4.97-6.603 2.824-2.504 6.439-5.635 8.02-7.058C28.862 2.504 32.194-.114 33.098.057c1.356.228 22.31 22.369 22.367 23.622 0 .569-1.017 9.221-2.259 19.238-2.147 17.076-4.18 37.055-3.954 38.99.169 1.196-.678 7.229-1.299 9.847-.509 2.05-.283 2.903 3.784 12.238 2.372 5.521 5.479 12.295 6.834 15.027 1.299 2.732 2.429 5.123 2.429 5.294 0 .17-.395.284-.847.284-.452 0-.847-.228-.847-.569ZM46.315 81.509c.621-3.984 1.864-13.547 2.767-21.231 1.751-14.116 3.785-29.769 4.349-33.753.339-1.993.113-2.391-3.558-6.489-6.382-7.229-13.16-14.344-15.476-16.165l-2.146-1.708-11.014 10.359C11.07 21.971 10.223 22.939 10.844 24.077c.339.626 3.22 5.92 6.383 11.725 3.163 5.806 7.342 13.547 9.263 17.19 1.977 3.7 3.784 6.887 4.123 7.058.395.228.508-5.521.395-17.759-.226-18.271-.169-18.328 1.638-17.929.226 0 .396 9.221.396 20.434v20.377l5.93 11.953c3.276 6.603 5.987 11.896 6.1 11.84.113-.058.678-3.416 1.243-7.457Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
1
dev-docs/static/img/undraw_add_files.svg
Normal file
1
dev-docs/static/img/undraw_add_files.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
1
dev-docs/static/img/undraw_innovative.svg
Normal file
1
dev-docs/static/img/undraw_innovative.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
7
dev-docs/tsconfig.json
Normal file
7
dev-docs/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
7489
dev-docs/yarn.lock
Normal file
7489
dev-docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -23,17 +23,17 @@
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@tldraw/vec": "1.4.3",
|
||||
"@tldraw/vec": "1.7.1",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"clsx": "1.1.1",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
"i18next-browser-languagedetector": "6.1.2",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.6.4",
|
||||
@@ -47,8 +47,8 @@
|
||||
"png-chunks-extract": "1.0.0",
|
||||
"points-on-curve": "0.2.0",
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
@@ -59,11 +59,11 @@
|
||||
"@excalidraw/eslint-config": "1.0.0",
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/lodash.throttle": "4.1.6",
|
||||
"@types/lodash.throttle": "4.1.7",
|
||||
"@types/pako": "1.0.3",
|
||||
"@types/resize-observer-browser": "0.1.6",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "10.0.0",
|
||||
"dotenv": "16.0.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"husky": "7.0.4",
|
||||
@@ -71,7 +71,7 @@
|
||||
"lint-staged": "12.3.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "5.0.0"
|
||||
"rewire": "6.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.10.2"
|
||||
@@ -94,7 +94,8 @@
|
||||
"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:prebuild": "node ./scripts/prebuild.js",
|
||||
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
|
||||
"eject": "react-scripts eject",
|
||||
"fix:code": "yarn test:code --fix",
|
||||
"fix:other": "yarn prettier --write",
|
||||
@@ -112,6 +113,8 @@
|
||||
"test:typecheck": "tsc",
|
||||
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||
"test": "yarn test:app",
|
||||
"autorelease": "node scripts/autorelease.js"
|
||||
"autorelease": "node scripts/autorelease.js",
|
||||
"prerelease": "node scripts/prerelease.js",
|
||||
"release": "node scripts/release.js"
|
||||
}
|
||||
}
|
||||
|
@@ -98,6 +98,22 @@
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD === "true") { %>
|
||||
<script>
|
||||
{
|
||||
const _WebSocket = window.WebSocket;
|
||||
window.WebSocket = function (url) {
|
||||
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
||||
console.info(
|
||||
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||
);
|
||||
} else {
|
||||
return new _WebSocket(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<% } %>
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||
// setting this so that libraries installation reuses this window tab.
|
||||
|
@@ -17,11 +17,23 @@
|
||||
* See https://goo.gl/2aRDsh
|
||||
*/
|
||||
|
||||
importScripts("/workbox/workbox-sw.js");
|
||||
// in dev, `process` is undefined because this file is not compiled until build
|
||||
const IS_DEVELOPMENT =
|
||||
typeof process === "undefined" || process.env.NODE_ENV !== "production";
|
||||
|
||||
workbox.setConfig({
|
||||
modulePathPrefix: "/workbox/",
|
||||
});
|
||||
if (IS_DEVELOPMENT) {
|
||||
importScripts(
|
||||
"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js",
|
||||
);
|
||||
workbox.setConfig({
|
||||
debug: true,
|
||||
});
|
||||
} else {
|
||||
importScripts("/workbox/workbox-sw.js");
|
||||
workbox.setConfig({
|
||||
modulePathPrefix: "/workbox/",
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||
@@ -30,14 +42,17 @@ self.addEventListener("message", (event) => {
|
||||
});
|
||||
|
||||
workbox.core.clientsClaim();
|
||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
workbox.routing.registerNavigationRoute(
|
||||
workbox.precaching.getCacheKeyForURL("./index.html"),
|
||||
{
|
||||
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||
},
|
||||
);
|
||||
if (!IS_DEVELOPMENT) {
|
||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
workbox.routing.registerNavigationRoute(
|
||||
workbox.precaching.getCacheKeyForURL("./index.html"),
|
||||
{
|
||||
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Cache relevant font files
|
||||
workbox.routing.registerRoute(
|
@@ -5,22 +5,25 @@ const core = require("@actions/core");
|
||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
const pkg = require(excalidrawPackage);
|
||||
const isPreview = process.argv.slice(2)[0] === "preview";
|
||||
|
||||
const getShortCommitHash = () => {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
};
|
||||
|
||||
const publish = () => {
|
||||
const tag = isPreview ? "preview" : "next";
|
||||
|
||||
try {
|
||||
execSync(`yarn --frozen-lockfile`);
|
||||
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||
console.info("Published 🎉");
|
||||
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
|
||||
console.info(`Published ${pkg.name}@${tag}🎉`);
|
||||
core.setOutput(
|
||||
"result",
|
||||
`**Preview version has been shipped** :rocket:
|
||||
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
|
||||
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
|
||||
);
|
||||
} catch (error) {
|
||||
core.setOutput("result", "package couldn't be published :warning:!");
|
||||
@@ -51,27 +54,19 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||
}
|
||||
|
||||
// update package.json
|
||||
pkg.name = "@excalidraw/excalidraw-next";
|
||||
let version = `${pkg.version}-${getShortCommitHash()}`;
|
||||
|
||||
// update readme
|
||||
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
||||
|
||||
const isPreview = process.argv.slice(2)[0] === "preview";
|
||||
if (isPreview) {
|
||||
// use pullNumber-commithash as the version for preview
|
||||
const pullRequestNumber = process.argv.slice(3)[0];
|
||||
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
|
||||
// replace "excalidraw-next" with "excalidraw-preview"
|
||||
pkg.name = "@excalidraw/excalidraw-preview";
|
||||
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
|
||||
data = data.trim();
|
||||
}
|
||||
pkg.version = version;
|
||||
|
||||
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
||||
|
||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||
console.info("Publish in progress...");
|
||||
publish();
|
||||
});
|
||||
|
21
scripts/buildDocs.js
Normal file
21
scripts/buildDocs.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { exec } = require("child_process");
|
||||
|
||||
// get files changed between prev and head commit
|
||||
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||
if (error || stderr) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
const changedFiles = stdout.trim().split("\n");
|
||||
|
||||
const docFiles = changedFiles.filter((file) => {
|
||||
return file.indexOf("docs") >= 0;
|
||||
});
|
||||
|
||||
if (!docFiles.length) {
|
||||
console.info("Skipping building docs as no valid diff found");
|
||||
process.exit(0);
|
||||
}
|
||||
// Exit code 1 to build the docs in ignoredBuildStep
|
||||
process.exit(1);
|
||||
});
|
@@ -36,6 +36,7 @@ const crowdinMap = {
|
||||
"ru-RU": "en-ru",
|
||||
"si-LK": "en-silk",
|
||||
"sk-SK": "en-sk",
|
||||
"sl-SI": "en-sl",
|
||||
"sv-SE": "en-sv",
|
||||
"ta-IN": "en-ta",
|
||||
"tr-TR": "en-tr",
|
||||
@@ -47,6 +48,8 @@ const crowdinMap = {
|
||||
"lv-LV": "en-lv",
|
||||
"cs-CZ": "en-cs",
|
||||
"kk-KZ": "en-kk",
|
||||
"vi-vn": "en-vi",
|
||||
"mr-in": "en-mr",
|
||||
};
|
||||
|
||||
const flags = {
|
||||
@@ -86,6 +89,7 @@ const flags = {
|
||||
"ru-RU": "🇷🇺",
|
||||
"si-LK": "🇱🇰",
|
||||
"sk-SK": "🇸🇰",
|
||||
"sl-SI": "🇸🇮",
|
||||
"sv-SE": "🇸🇪",
|
||||
"ta-IN": "🇮🇳",
|
||||
"tr-TR": "🇹🇷",
|
||||
@@ -93,6 +97,9 @@ const flags = {
|
||||
"zh-CN": "🇨🇳",
|
||||
"zh-HK": "🇭🇰",
|
||||
"zh-TW": "🇹🇼",
|
||||
"eu-ES": "🇪🇦",
|
||||
"vi-VN": "🇻🇳",
|
||||
"mr-IN": "🇮🇳",
|
||||
};
|
||||
|
||||
const languages = {
|
||||
@@ -133,6 +140,7 @@ const languages = {
|
||||
"ru-RU": "Русский",
|
||||
"si-LK": "සිංහල",
|
||||
"sk-SK": "Slovenčina",
|
||||
"sl-SI": "Slovenščina",
|
||||
"sv-SE": "Svenska",
|
||||
"ta-IN": "Tamil",
|
||||
"tr-TR": "Türkçe",
|
||||
@@ -140,6 +148,8 @@ const languages = {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-HK": "繁體中文 (香港)",
|
||||
"zh-TW": "繁體中文",
|
||||
"vi-VN": "Tiếng Việt",
|
||||
"mr-IN": "मराठी",
|
||||
};
|
||||
|
||||
const percentages = fs.readFileSync(
|
||||
|
20
scripts/prebuild.js
Normal file
20
scripts/prebuild.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const fs = require("fs");
|
||||
|
||||
// for development purposes we want to have the service-worker.js file
|
||||
// accessible from the public folder. On build though, we need to compile it
|
||||
// and CRA expects that file to be in src/ folder.
|
||||
const moveServiceWorkerScript = () => {
|
||||
const oldPath = "./public/service-worker.js";
|
||||
const newPath = "./src/service-worker.js";
|
||||
|
||||
fs.rename(oldPath, newPath, (error) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
console.info("public/service-worker.js moved to src/");
|
||||
});
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
moveServiceWorkerScript();
|
37
scripts/prerelease.js
Normal file
37
scripts/prerelease.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const updateChangelog = require("./updateChangelog");
|
||||
|
||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
|
||||
const updatePackageVersion = (nextVersion) => {
|
||||
const pkg = require(excalidrawPackage);
|
||||
pkg.version = nextVersion;
|
||||
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
||||
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
||||
};
|
||||
|
||||
const prerelease = async (nextVersion) => {
|
||||
try {
|
||||
await updateChangelog(nextVersion);
|
||||
updatePackageVersion(nextVersion);
|
||||
await exec(`git add -u`);
|
||||
await exec(
|
||||
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||
);
|
||||
|
||||
console.info("Done!");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const nextVersion = process.argv.slice(2)[0];
|
||||
if (!nextVersion) {
|
||||
console.error("Pass the next version to release!");
|
||||
process.exit(1);
|
||||
}
|
||||
prerelease(nextVersion);
|
@@ -1,39 +1,44 @@
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const updateReadme = require("./updateReadme");
|
||||
const updateChangelog = require("./updateChangelog");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
const pkg = require(excalidrawPackage);
|
||||
|
||||
const updatePackageVersion = (nextVersion) => {
|
||||
const pkg = require(excalidrawPackage);
|
||||
pkg.version = nextVersion;
|
||||
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
||||
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
||||
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
|
||||
|
||||
const updateReadme = () => {
|
||||
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
|
||||
|
||||
// remove note for stable readme
|
||||
const data = originalReadMe.slice(excalidrawIndex);
|
||||
|
||||
// update readme
|
||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||
};
|
||||
|
||||
const release = async (nextVersion) => {
|
||||
const publish = () => {
|
||||
try {
|
||||
updateReadme();
|
||||
await updateChangelog(nextVersion);
|
||||
updatePackageVersion(nextVersion);
|
||||
await exec(`git add -u`);
|
||||
await exec(
|
||||
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||
);
|
||||
/* eslint-disable no-console */
|
||||
console.log("Done!");
|
||||
execSync(`yarn --frozen-lockfile`);
|
||||
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const nextVersion = process.argv.slice(2)[0];
|
||||
if (!nextVersion) {
|
||||
console.error("Pass the next version to release!");
|
||||
process.exit(1);
|
||||
}
|
||||
release(nextVersion);
|
||||
const release = () => {
|
||||
updateReadme();
|
||||
console.info("Note for stable readme removed");
|
||||
|
||||
publish();
|
||||
console.info(`Published ${pkg.version}!`);
|
||||
|
||||
// revert readme after release
|
||||
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
|
||||
console.info("Readme reverted");
|
||||
};
|
||||
|
||||
release();
|
||||
|
@@ -1,27 +0,0 @@
|
||||
const fs = require("fs");
|
||||
|
||||
const updateReadme = () => {
|
||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
||||
|
||||
// remove note for unstable release
|
||||
data = data.replace(
|
||||
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
|
||||
"",
|
||||
);
|
||||
|
||||
// replace "excalidraw-next" with "excalidraw"
|
||||
data = data.replace(/excalidraw-next/g, "excalidraw");
|
||||
data = data.trim();
|
||||
|
||||
const demoIndex = data.indexOf("### Demo");
|
||||
const excalidrawNextNote =
|
||||
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
|
||||
// Add excalidraw next note to try out for unreleased changes
|
||||
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
|
||||
|
||||
// update readme
|
||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||
};
|
||||
|
||||
module.exports = updateReadme;
|
@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.addedToLibrary"),
|
||||
toast: { message: t("toast.addedToLibrary") },
|
||||
},
|
||||
};
|
||||
})
|
||||
|
@@ -107,14 +107,16 @@ export const actionCopyAsPng = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyToClipboardAsPng", {
|
||||
exportSelection: selectedElements.length
|
||||
? t("toast.selection")
|
||||
: t("toast.canvas"),
|
||||
exportColorScheme: appState.exportWithDarkMode
|
||||
? t("buttons.darkMode")
|
||||
: t("buttons.lightMode"),
|
||||
}),
|
||||
toast: {
|
||||
message: t("toast.copyToClipboardAsPng", {
|
||||
exportSelection: selectedElements.length
|
||||
? t("toast.selection")
|
||||
: t("toast.canvas"),
|
||||
exportColorScheme: appState.exportWithDarkMode
|
||||
? t("buttons.darkMode")
|
||||
: t("buttons.lightMode"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@@ -128,12 +128,15 @@ const duplicateElements = (
|
||||
{
|
||||
...appState,
|
||||
selectedGroupIds: {},
|
||||
selectedElementIds: newElements.reduce((acc, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {} as any),
|
||||
selectedElementIds: newElements.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
),
|
||||
|
@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
|
||||
appState: {
|
||||
...appState,
|
||||
fileHandle,
|
||||
toastMessage: fileHandleExists
|
||||
? fileHandle?.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved")
|
||||
toast: fileHandleExists
|
||||
? {
|
||||
message: fileHandle?.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved"),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { selectGroupsForSelectedElements } from "../groups";
|
||||
import { getNonDeletedElements, isTextElement } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
export const actionSelectAll = register({
|
||||
name: "selectAll",
|
||||
@@ -15,16 +16,19 @@ export const actionSelectAll = register({
|
||||
{
|
||||
...appState,
|
||||
editingGroupId: null,
|
||||
selectedElementIds: elements.reduce((map, element) => {
|
||||
if (
|
||||
!element.isDeleted &&
|
||||
!(isTextElement(element) && element.containerId) &&
|
||||
element.locked === false
|
||||
) {
|
||||
map[element.id] = true;
|
||||
}
|
||||
return map;
|
||||
}, {} as any),
|
||||
selectedElementIds: elements.reduce(
|
||||
(map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (
|
||||
!element.isDeleted &&
|
||||
!(isTextElement(element) && element.containerId) &&
|
||||
!element.locked
|
||||
) {
|
||||
map[element.id] = true;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
),
|
||||
|
@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyStyles"),
|
||||
toast: { message: t("toast.copyStyles") },
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
290
src/appState.ts
290
src/appState.ts
@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
toastMessage: null,
|
||||
toast: null,
|
||||
viewBackgroundColor: oc.white,
|
||||
zenModeEnabled: false,
|
||||
zoom: {
|
||||
@@ -101,228 +101,90 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
Values extends {
|
||||
/** whether to keep when storing to browser storage (localStorage/IDB) */
|
||||
browser: boolean;
|
||||
/** whether to keep when exporting to a text file */
|
||||
text: boolean;
|
||||
/** whether to keep when exporting to an image file */
|
||||
image: boolean;
|
||||
/** whether to keep when exporting to file/database */
|
||||
export: boolean;
|
||||
/** server (shareLink/collab/...) */
|
||||
server: boolean;
|
||||
},
|
||||
T extends Record<keyof AppState, Values>,
|
||||
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
|
||||
config)({
|
||||
theme: { browser: true, text: false, image: false, server: false },
|
||||
collaborators: { browser: false, text: false, image: false, server: false },
|
||||
currentChartType: { browser: true, text: false, image: false, server: false },
|
||||
currentItemBackgroundColor: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemEndArrowhead: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemFillStyle: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemFontFamily: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemFontSize: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
theme: { browser: true, export: false, server: false },
|
||||
collaborators: { browser: false, export: false, server: false },
|
||||
currentChartType: { browser: true, export: false, server: false },
|
||||
currentItemBackgroundColor: { browser: true, export: false, server: false },
|
||||
currentItemEndArrowhead: { browser: true, export: false, server: false },
|
||||
currentItemFillStyle: { browser: true, export: false, server: false },
|
||||
currentItemFontFamily: { browser: true, export: false, server: false },
|
||||
currentItemFontSize: { browser: true, export: false, server: false },
|
||||
currentItemLinearStrokeSharpness: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemOpacity: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemRoughness: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemStartArrowhead: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemStrokeColor: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemStrokeSharpness: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemStrokeStyle: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemStrokeWidth: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemTextAlign: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
cursorButton: { browser: true, text: false, image: false, server: false },
|
||||
draggingElement: { browser: false, text: false, image: false, server: false },
|
||||
editingElement: { browser: false, text: false, image: false, server: false },
|
||||
editingGroupId: { browser: true, text: false, image: false, server: false },
|
||||
editingLinearElement: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
activeTool: { browser: true, text: false, image: false, server: false },
|
||||
penMode: { browser: true, text: false, image: false, server: false },
|
||||
penDetected: { browser: true, text: false, image: false, server: false },
|
||||
errorMessage: { browser: false, text: false, image: false, server: false },
|
||||
exportBackground: { browser: true, text: false, image: true, server: false },
|
||||
exportEmbedScene: { browser: true, text: false, image: true, server: false },
|
||||
exportScale: { browser: true, text: false, image: true, server: false },
|
||||
exportWithDarkMode: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: true,
|
||||
server: false,
|
||||
},
|
||||
fileHandle: { browser: false, text: false, image: false, server: false },
|
||||
gridSize: { browser: true, text: true, image: true, server: true },
|
||||
height: { browser: false, text: false, image: false, server: false },
|
||||
isBindingEnabled: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
isLibraryOpen: { browser: true, text: false, image: false, server: false },
|
||||
isLibraryMenuDocked: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
isLoading: { browser: false, text: false, image: false, server: false },
|
||||
isResizing: { browser: false, text: false, image: false, server: false },
|
||||
isRotating: { browser: false, text: false, image: false, server: false },
|
||||
lastPointerDownWith: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
multiElement: { browser: false, text: false, image: false, server: false },
|
||||
name: { browser: true, text: false, image: false, server: false },
|
||||
offsetLeft: { browser: false, text: false, image: false, server: false },
|
||||
offsetTop: { browser: false, text: false, image: false, server: false },
|
||||
openMenu: { browser: true, text: false, image: false, server: false },
|
||||
openPopup: { browser: false, text: false, image: false, server: false },
|
||||
pasteDialog: { browser: false, text: false, image: false, server: false },
|
||||
previousSelectedElementIds: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
resizingElement: { browser: false, text: false, image: false, server: false },
|
||||
scrolledOutside: { browser: true, text: false, image: false, server: false },
|
||||
scrollX: { browser: true, text: false, image: false, server: false },
|
||||
scrollY: { browser: true, text: false, image: false, server: false },
|
||||
selectedElementIds: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
selectedGroupIds: { browser: true, text: false, image: false, server: false },
|
||||
selectionElement: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
shouldCacheIgnoreZoom: {
|
||||
browser: true,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
showHelpDialog: { browser: false, text: false, image: false, server: false },
|
||||
showStats: { browser: true, text: false, image: false, server: false },
|
||||
startBoundElement: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
suggestedBindings: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
toastMessage: { browser: false, text: false, image: false, server: false },
|
||||
viewBackgroundColor: {
|
||||
browser: true,
|
||||
text: true,
|
||||
image: true,
|
||||
server: true,
|
||||
},
|
||||
width: { browser: false, text: false, image: false, server: false },
|
||||
zenModeEnabled: { browser: true, text: false, image: false, server: false },
|
||||
zoom: { browser: true, text: false, image: false, server: false },
|
||||
viewModeEnabled: { browser: false, text: false, image: false, server: false },
|
||||
pendingImageElementId: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
server: false,
|
||||
},
|
||||
showHyperlinkPopup: {
|
||||
browser: false,
|
||||
text: false,
|
||||
image: false,
|
||||
export: false,
|
||||
server: false,
|
||||
},
|
||||
currentItemOpacity: { browser: true, export: false, server: false },
|
||||
currentItemRoughness: { browser: true, export: false, server: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
||||
currentItemStrokeColor: { browser: true, export: false, server: false },
|
||||
currentItemStrokeSharpness: { browser: true, export: false, server: false },
|
||||
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
||||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||
cursorButton: { browser: true, export: false, server: false },
|
||||
draggingElement: { browser: false, export: false, server: false },
|
||||
editingElement: { browser: false, export: false, server: false },
|
||||
editingGroupId: { browser: true, export: false, server: false },
|
||||
editingLinearElement: { browser: false, export: false, server: false },
|
||||
activeTool: { browser: true, export: false, server: false },
|
||||
penMode: { browser: true, export: false, server: false },
|
||||
penDetected: { browser: true, export: false, server: false },
|
||||
errorMessage: { browser: false, export: false, server: false },
|
||||
exportBackground: { browser: true, export: false, server: false },
|
||||
exportEmbedScene: { browser: true, export: false, server: false },
|
||||
exportScale: { browser: true, export: false, server: false },
|
||||
exportWithDarkMode: { browser: true, export: false, server: false },
|
||||
fileHandle: { browser: false, export: false, server: false },
|
||||
gridSize: { browser: true, export: true, server: true },
|
||||
height: { browser: false, export: false, server: false },
|
||||
isBindingEnabled: { browser: false, export: false, server: false },
|
||||
isLibraryOpen: { browser: true, export: false, server: false },
|
||||
isLibraryMenuDocked: { browser: true, export: false, server: false },
|
||||
isLoading: { browser: false, export: false, server: false },
|
||||
isResizing: { browser: false, export: false, server: false },
|
||||
isRotating: { browser: false, export: false, server: false },
|
||||
lastPointerDownWith: { browser: true, export: false, server: false },
|
||||
multiElement: { browser: false, export: false, server: false },
|
||||
name: { browser: true, export: false, server: false },
|
||||
offsetLeft: { browser: false, export: false, server: false },
|
||||
offsetTop: { browser: false, export: false, server: false },
|
||||
openMenu: { browser: true, export: false, server: false },
|
||||
openPopup: { browser: false, export: false, server: false },
|
||||
pasteDialog: { browser: false, export: false, server: false },
|
||||
previousSelectedElementIds: { browser: true, export: false, server: false },
|
||||
resizingElement: { browser: false, export: false, server: false },
|
||||
scrolledOutside: { browser: true, export: false, server: false },
|
||||
scrollX: { browser: true, export: false, server: false },
|
||||
scrollY: { browser: true, export: false, server: false },
|
||||
selectedElementIds: { browser: true, export: false, server: false },
|
||||
selectedGroupIds: { browser: true, export: false, server: false },
|
||||
selectionElement: { browser: false, export: false, server: false },
|
||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||
showHelpDialog: { browser: false, export: false, server: false },
|
||||
showStats: { browser: true, export: false, server: false },
|
||||
startBoundElement: { browser: false, export: false, server: false },
|
||||
suggestedBindings: { browser: false, export: false, server: false },
|
||||
toast: { browser: false, export: false, server: false },
|
||||
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||
width: { browser: false, export: false, server: false },
|
||||
zenModeEnabled: { browser: true, export: false, server: false },
|
||||
zoom: { browser: true, export: false, server: false },
|
||||
viewModeEnabled: { browser: false, export: false, server: false },
|
||||
pendingImageElementId: { browser: false, export: false, server: false },
|
||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
ExportType extends "image" | "text" | "browser" | "server",
|
||||
ExportType extends "export" | "browser" | "server",
|
||||
>(
|
||||
appState: Partial<AppState>,
|
||||
exportType: ExportType,
|
||||
@@ -349,12 +211,8 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
|
||||
return _clearAppStateForStorage(appState, "browser");
|
||||
};
|
||||
|
||||
export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
|
||||
return _clearAppStateForStorage(appState, "text");
|
||||
};
|
||||
|
||||
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
|
||||
return _clearAppStateForStorage(appState, "image");
|
||||
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
|
||||
return _clearAppStateForStorage(appState, "export");
|
||||
};
|
||||
|
||||
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
||||
|
@@ -166,7 +166,7 @@ import {
|
||||
isAndroid,
|
||||
} from "../keys";
|
||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||
import { renderScene } from "../renderer";
|
||||
import { renderScene } from "../renderer/renderScene";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
calculateScrollCenter,
|
||||
@@ -286,6 +286,10 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
||||
let touchTimeout = 0;
|
||||
let invalidateContextMenu = false;
|
||||
|
||||
// remove this hack when we can sync render & resizeObserver (state update)
|
||||
// to rAF. See #5439
|
||||
let THROTTLE_NEXT_RENDER = true;
|
||||
|
||||
let lastPointerUp: ((event: any) => void) | null = null;
|
||||
const gesture: Gesture = {
|
||||
pointers: new Map(),
|
||||
@@ -380,7 +384,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
getAppState: () => this.state,
|
||||
getFiles: () => this.files,
|
||||
refresh: this.refresh,
|
||||
setToastMessage: this.setToastMessage,
|
||||
setToast: this.setToast,
|
||||
id: this.id,
|
||||
setActiveTool: this.setActiveTool,
|
||||
setCursor: this.setCursor,
|
||||
@@ -469,7 +473,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { zenModeEnabled, viewModeEnabled } = this.state;
|
||||
const selectedElement = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
@@ -484,7 +487,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return (
|
||||
<div
|
||||
className={clsx("excalidraw excalidraw-container", {
|
||||
"excalidraw--view-mode": viewModeEnabled,
|
||||
"excalidraw--view-mode": this.state.viewModeEnabled,
|
||||
"excalidraw--mobile": this.device.isMobile,
|
||||
})}
|
||||
ref={this.excalidrawContainerRef}
|
||||
@@ -515,17 +518,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
files: null,
|
||||
})
|
||||
}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
toggleZenMode={this.toggleZenMode}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomFooter={renderFooter}
|
||||
renderCustomStats={renderCustomStats}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||
zenModeEnabled
|
||||
this.state.zenModeEnabled
|
||||
}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
@@ -549,10 +549,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
/>
|
||||
)}
|
||||
{this.state.toastMessage !== null && (
|
||||
{this.state.toast !== null && (
|
||||
<Toast
|
||||
message={this.state.toastMessage}
|
||||
clearToast={this.clearToast}
|
||||
message={this.state.toast.message}
|
||||
onClose={() => this.setToast(null)}
|
||||
duration={this.state.toast.duration}
|
||||
closable={this.state.toast.closable}
|
||||
/>
|
||||
)}
|
||||
<main>{this.renderCanvas()}</main>
|
||||
@@ -766,6 +768,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? { ...scene.appState.activeTool, type: "selection" }
|
||||
: scene.appState.activeTool,
|
||||
isLoading: false,
|
||||
toast: this.state.toast,
|
||||
};
|
||||
if (initialData?.scrollToContent) {
|
||||
scene.appState = {
|
||||
@@ -859,6 +862,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
THROTTLE_NEXT_RENDER = false;
|
||||
// recompute device dimensions state
|
||||
// ---------------------------------------------------------------------
|
||||
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
||||
@@ -904,6 +908,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
} else {
|
||||
this.updateDOMRect(this.initializeScene);
|
||||
}
|
||||
this.checkIfBrowserZoomed();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@@ -916,8 +921,25 @@ class App extends React.Component<AppProps, AppState> {
|
||||
clearTimeout(touchTimeout);
|
||||
touchTimeout = 0;
|
||||
}
|
||||
|
||||
private checkIfBrowserZoomed = () => {
|
||||
if (!this.device.isMobile) {
|
||||
const scrollBarWidth = 10;
|
||||
const widthRatio =
|
||||
(window.outerWidth - scrollBarWidth) / window.innerWidth;
|
||||
const isBrowserZoomed = widthRatio < 0.75 || widthRatio > 1.1;
|
||||
if (isBrowserZoomed) {
|
||||
this.setToast({
|
||||
message: t("alerts.browserZoom"),
|
||||
closable: true,
|
||||
duration: Infinity,
|
||||
});
|
||||
} else {
|
||||
this.setToast(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
private onResize = withBatchedUpdates(() => {
|
||||
this.checkIfBrowserZoomed();
|
||||
this.scene
|
||||
.getElementsIncludingDeleted()
|
||||
.forEach((element) => invalidateShapeForElement(element));
|
||||
@@ -929,6 +951,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
document.removeEventListener(EVENT.COPY, this.onCopy);
|
||||
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
||||
document.removeEventListener(EVENT.CUT, this.onCut);
|
||||
this.excalidrawContainerRef.current?.removeEventListener(
|
||||
EVENT.WHEEL,
|
||||
this.onWheel,
|
||||
);
|
||||
this.nearestScrollableContainer?.removeEventListener(
|
||||
EVENT.SCROLL,
|
||||
this.onScroll,
|
||||
@@ -977,6 +1003,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.removeEventListeners();
|
||||
document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
|
||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
||||
this.excalidrawContainerRef.current?.addEventListener(
|
||||
EVENT.WHEEL,
|
||||
this.onWheel,
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
if (this.props.handleKeyboardGlobally) {
|
||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||
}
|
||||
@@ -1193,7 +1225,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element.id !== this.state.editingElement.id
|
||||
);
|
||||
});
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
|
||||
renderScene(
|
||||
renderingElements,
|
||||
this.state,
|
||||
this.state.selectionElement,
|
||||
@@ -1216,24 +1249,30 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isExporting: false,
|
||||
renderScrollbars: !this.device.isMobile,
|
||||
},
|
||||
({ atLeastOneVisibleElement, scrollBars }) => {
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
// hide when editing text
|
||||
isTextElement(this.state.editingElement)
|
||||
? false
|
||||
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside });
|
||||
}
|
||||
|
||||
this.scheduleImageRefresh();
|
||||
},
|
||||
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
|
||||
);
|
||||
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
// hide when editing text
|
||||
isTextElement(this.state.editingElement)
|
||||
? false
|
||||
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside });
|
||||
if (!THROTTLE_NEXT_RENDER) {
|
||||
THROTTLE_NEXT_RENDER = true;
|
||||
}
|
||||
|
||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||
|
||||
this.scheduleImageRefresh();
|
||||
|
||||
// Do not notify consumers if we're still loading the scene. Among other
|
||||
// potential issues, this fixes a case where the tab isn't focused during
|
||||
// init, which would trigger onChange with empty elements, which would then
|
||||
@@ -1501,12 +1540,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.isLibraryOpen && this.device.canDeviceFitSidebar
|
||||
? this.state.isLibraryMenuDocked
|
||||
: false,
|
||||
selectedElementIds: newElements.reduce((map, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
map[element.id] = true;
|
||||
}
|
||||
return map;
|
||||
}, {} as any),
|
||||
selectedElementIds: newElements.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
selectedGroupIds: {},
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
@@ -1599,10 +1641,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
toggleZenMode = () => {
|
||||
this.actionManager.executeAction(actionToggleZenMode);
|
||||
};
|
||||
|
||||
scrollToContent = (
|
||||
target:
|
||||
| ExcalidrawElement
|
||||
@@ -1617,12 +1655,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
clearToast = () => {
|
||||
this.setState({ toastMessage: null });
|
||||
};
|
||||
|
||||
setToastMessage = (toastMessage: string) => {
|
||||
this.setState({ toastMessage });
|
||||
setToast = (
|
||||
toast: {
|
||||
message: string;
|
||||
closable?: boolean;
|
||||
duration?: number;
|
||||
} | null,
|
||||
) => {
|
||||
this.setState({ toast });
|
||||
};
|
||||
|
||||
restoreFileFromShare = async () => {
|
||||
@@ -1708,6 +1748,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private onKeyDown = withBatchedUpdates(
|
||||
(event: React.KeyboardEvent | KeyboardEvent) => {
|
||||
// normalize `event.key` when CapsLock is pressed #2372
|
||||
|
||||
if (
|
||||
"Proxy" in window &&
|
||||
((!event.shiftKey && /^[A-Z]$/.test(event.key)) ||
|
||||
@@ -1731,6 +1772,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
// prevent browser zoom in input fields
|
||||
if (event[KEYS.CTRL_OR_CMD] && isWritableElement(event.target)) {
|
||||
if (event.code === CODES.MINUS || event.code === CODES.EQUAL) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// bail if
|
||||
if (
|
||||
// inside an input
|
||||
@@ -1823,35 +1872,32 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
isLinearElement(selectedElements[0])
|
||||
) {
|
||||
if (
|
||||
!this.state.editingLinearElement ||
|
||||
this.state.editingLinearElement.elementId !== selectedElements[0].id
|
||||
) {
|
||||
this.history.resumeRecording();
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
selectedElements.length === 1 &&
|
||||
!isLinearElement(selectedElements[0])
|
||||
) {
|
||||
const selectedElement = selectedElements[0];
|
||||
if (selectedElements.length === 1) {
|
||||
if (isLinearElement(selectedElements[0])) {
|
||||
if (
|
||||
!this.state.editingLinearElement ||
|
||||
this.state.editingLinearElement.elementId !==
|
||||
selectedElements[0].id
|
||||
) {
|
||||
this.history.resumeRecording();
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const selectedElement = selectedElements[0];
|
||||
|
||||
this.startTextEditing({
|
||||
sceneX: selectedElement.x + selectedElement.width / 2,
|
||||
sceneY: selectedElement.y + selectedElement.height / 2,
|
||||
shouldBind: true,
|
||||
});
|
||||
event.preventDefault();
|
||||
return;
|
||||
this.startTextEditing({
|
||||
sceneX: selectedElement.x + selectedElement.width / 2,
|
||||
sceneY: selectedElement.y + selectedElement.height / 2,
|
||||
shouldBind: true,
|
||||
});
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!event.ctrlKey &&
|
||||
@@ -1913,6 +1959,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
private onWheel = withBatchedUpdates((event: WheelEvent) => {
|
||||
// prevent browser pinch zoom on DOM elements
|
||||
if (!(event.target instanceof HTMLCanvasElement) && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.SPACE) {
|
||||
if (this.state.viewModeEnabled) {
|
||||
@@ -4254,10 +4307,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...prevState,
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
...elementsWithinSelection.reduce((map, element) => {
|
||||
map[element.id] = true;
|
||||
return map;
|
||||
}, {} as any),
|
||||
...elementsWithinSelection.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
...(pointerDownState.hit.element
|
||||
? {
|
||||
// if using ctrl/cmd, select the hitElement only if we
|
||||
@@ -5724,7 +5780,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isPanning) {
|
||||
return;
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import "./Card.scss";
|
||||
|
||||
export const Card: React.FC<{
|
||||
color: keyof OpenColor | "primary";
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, color }) => {
|
||||
return (
|
||||
<div
|
||||
|
@@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, checked, onChange, className }) => {
|
||||
return (
|
||||
<div
|
||||
|
@@ -18,13 +18,15 @@
|
||||
left: -5px;
|
||||
}
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: $oc-green-6;
|
||||
color: $oc-white;
|
||||
font-size: 0.7em;
|
||||
font-family: var(--ui-font);
|
||||
font-size: 0.6em;
|
||||
font-family: "Cascadia";
|
||||
}
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ const CollabButton = ({
|
||||
aria-label={t("labels.liveCollaboration")}
|
||||
showAriaLabel={useDevice().isMobile}
|
||||
>
|
||||
{collaboratorCount > 0 && (
|
||||
{isCollaborating && (
|
||||
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
||||
)}
|
||||
</ToolButton>
|
||||
|
@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
shade?: number;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -170,7 +171,9 @@ const ImageExportModal = ({
|
||||
<Stack.Row gap={2}>
|
||||
{actionManager.renderAction("changeExportScale")}
|
||||
</Stack.Row>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
||||
{t("buttons.scale")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
@@ -39,6 +39,7 @@ import { trackEvent } from "../analytics";
|
||||
import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import { actionToggleZenMode } from "../actions";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@@ -51,16 +52,13 @@ interface LayerUIProps {
|
||||
onLockToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
showExitZenModeBtn: boolean;
|
||||
showThemeBtn: boolean;
|
||||
toggleZenMode: () => void;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
viewModeEnabled: boolean;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
focusContainer: () => void;
|
||||
@@ -79,15 +77,12 @@ const LayerUI = ({
|
||||
onLockToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
zenModeEnabled,
|
||||
showExitZenModeBtn,
|
||||
showThemeBtn,
|
||||
toggleZenMode,
|
||||
isCollaborating,
|
||||
renderTopRightUI,
|
||||
renderCustomFooter,
|
||||
renderCustomStats,
|
||||
viewModeEnabled,
|
||||
libraryReturnUrl,
|
||||
UIOptions,
|
||||
focusContainer,
|
||||
@@ -171,7 +166,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
@@ -192,7 +187,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
@@ -232,7 +227,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="selectedShapeActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<Island
|
||||
@@ -302,32 +297,34 @@ const LayerUI = ({
|
||||
<div className="App-menu App-menu_top">
|
||||
<Stack.Col
|
||||
gap={4}
|
||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
||||
className={clsx({
|
||||
"disable-pointerEvents": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{viewModeEnabled
|
||||
{appState.viewModeEnabled
|
||||
? renderViewModeCanvasActions()
|
||||
: renderCanvasActions()}
|
||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||
</Stack.Col>
|
||||
{!viewModeEnabled && (
|
||||
{!appState.viewModeEnabled && (
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
className={clsx("App-toolbar-container", {
|
||||
"zen-mode": zenModeEnabled,
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<PenModeButton
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={() => onLockToggle()}
|
||||
title={t("toolBar.lock")}
|
||||
@@ -335,7 +332,7 @@ const LayerUI = ({
|
||||
<Island
|
||||
padding={1}
|
||||
className={clsx("App-toolbar", {
|
||||
"zen-mode": zenModeEnabled,
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<HintViewer
|
||||
@@ -371,7 +368,7 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||
{
|
||||
"transition-right": zenModeEnabled,
|
||||
"transition-right": appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -396,7 +393,8 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -408,12 +406,12 @@ const LayerUI = ({
|
||||
zoom={appState.zoom}
|
||||
/>
|
||||
</Island>
|
||||
{!viewModeEnabled && (
|
||||
{!appState.viewModeEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={clsx("undo-redo-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("undo", { size: "small" })}
|
||||
@@ -423,20 +421,20 @@ const LayerUI = ({
|
||||
<div
|
||||
className={clsx("eraser-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!viewModeEnabled &&
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.multiElement &&
|
||||
device.isTouchScreen && (
|
||||
<div
|
||||
className={clsx("finalize-button zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("finalize", { size: "small" })}
|
||||
@@ -450,7 +448,7 @@ const LayerUI = ({
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -460,7 +458,7 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||
{
|
||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -470,7 +468,7 @@ const LayerUI = ({
|
||||
className={clsx("disable-zen-mode", {
|
||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||
})}
|
||||
onClick={toggleZenMode}
|
||||
onClick={() => actionManager.executeAction(actionToggleZenMode)}
|
||||
>
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
@@ -543,7 +541,6 @@ const LayerUI = ({
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
showThemeBtn={showThemeBtn}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
|
@@ -224,7 +224,7 @@ export const LibraryMenu = ({
|
||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||
|
||||
const onPublishLibSuccess = useCallback(
|
||||
(data, libraryItems: LibraryItems) => {
|
||||
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
|
@@ -36,7 +36,6 @@ type MobileMenuProps = {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
viewModeEnabled: boolean;
|
||||
showThemeBtn: boolean;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
@@ -60,7 +59,6 @@ export const MobileMenu = ({
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
showThemeBtn,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
@@ -70,7 +68,7 @@ export const MobileMenu = ({
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
<Stack.Row gap={1} className="App-toolbar-container">
|
||||
<Island padding={1} className="App-toolbar">
|
||||
@@ -125,7 +123,7 @@ export const MobileMenu = ({
|
||||
!appState.editingElement &&
|
||||
getSelectedElements(elements, appState).length === 0;
|
||||
|
||||
if (viewModeEnabled) {
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
@@ -151,7 +149,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
|
||||
const renderCanvasActions = () => {
|
||||
if (viewModeEnabled) {
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<>
|
||||
{renderJSONExportDialog()}
|
||||
@@ -185,7 +183,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!viewModeEnabled && renderToolbar()}
|
||||
{!appState.viewModeEnabled && renderToolbar()}
|
||||
{renderStats()}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
@@ -216,7 +214,7 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
</Section>
|
||||
) : appState.openMenu === "shape" &&
|
||||
!viewModeEnabled &&
|
||||
!appState.viewModeEnabled &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
@@ -229,18 +227,20 @@ export const MobileMenu = ({
|
||||
) : null}
|
||||
<footer className="App-toolbar">
|
||||
{renderAppToolbar()}
|
||||
{appState.scrolledOutside && !appState.openMenu && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
{appState.scrolledOutside &&
|
||||
!appState.openMenu &&
|
||||
!appState.isLibraryOpen && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</Island>
|
||||
</div>
|
||||
|
@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
|
||||
import { AppState } from "../types";
|
||||
import { THEME } from "../constants";
|
||||
|
||||
export const Modal = (props: {
|
||||
export const Modal: React.FC<{
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: number;
|
||||
@@ -16,7 +16,7 @@ export const Modal = (props: {
|
||||
labelledBy: string;
|
||||
theme?: AppState["theme"];
|
||||
closeOnClickOutside?: boolean;
|
||||
}) => {
|
||||
}> = (props) => {
|
||||
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
||||
const modalRoot = useBodyRoot(theme);
|
||||
|
||||
|
@@ -2,5 +2,6 @@
|
||||
.popover {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 5px 0 5px;
|
||||
}
|
||||
}
|
||||
|
@@ -69,12 +69,27 @@ export const Popover = ({
|
||||
if (fitInViewport && popoverRef.current) {
|
||||
const element = popoverRef.current;
|
||||
const { x, y, width, height } = element.getBoundingClientRect();
|
||||
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
|
||||
|
||||
//Position correctly when clicked on rightmost part or the bottom part of viewport
|
||||
if (x + width - offsetLeft > viewportWidth) {
|
||||
element.style.left = `${viewportWidth - width}px`;
|
||||
element.style.left = `${viewportWidth - width - 10}px`;
|
||||
}
|
||||
if (y + height - offsetTop > viewportHeight) {
|
||||
element.style.top = `${viewportHeight - height}px`;
|
||||
}
|
||||
|
||||
//Resize to fit viewport on smaller screens
|
||||
if (height >= viewportHeight) {
|
||||
element.style.height = `${viewportHeight - 20}px`;
|
||||
element.style.top = "10px";
|
||||
element.style.overflowY = "scroll";
|
||||
}
|
||||
if (width >= viewportWidth) {
|
||||
element.style.width = `${viewportWidth}px`;
|
||||
element.style.left = "0px";
|
||||
element.style.overflowX = "scroll";
|
||||
}
|
||||
}
|
||||
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
|
||||
|
||||
|
@@ -2,12 +2,11 @@ import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
||||
export const Section: React.FC<{
|
||||
heading: string;
|
||||
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
|
||||
}
|
||||
|
||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||
className?: string;
|
||||
}> = ({ heading, children, ...props }) => {
|
||||
const { id } = useExcalidrawContainer();
|
||||
const header = (
|
||||
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||
|
@@ -2,6 +2,9 @@
|
||||
|
||||
.excalidraw {
|
||||
.Toast {
|
||||
$closeButtonSize: 1.2rem;
|
||||
$closeButtonPadding: 0.4rem;
|
||||
|
||||
animation: fade-in 0.5s;
|
||||
background-color: var(--button-gray-1);
|
||||
border-radius: 4px;
|
||||
@@ -15,11 +18,24 @@
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.Toast__message {
|
||||
color: var(--popup-text-color);
|
||||
white-space: pre-wrap;
|
||||
.Toast__message {
|
||||
padding: 0 $closeButtonSize + ($closeButtonPadding);
|
||||
color: var(--popup-text-color);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: $closeButtonPadding;
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: $closeButtonSize;
|
||||
height: $closeButtonSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
|
@@ -1,34 +1,59 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { TOAST_TIMEOUT } from "../constants";
|
||||
import { close } from "./icons";
|
||||
import "./Toast.scss";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
const DEFAULT_TOAST_TIMEOUT = 5000;
|
||||
|
||||
export const Toast = ({
|
||||
message,
|
||||
clearToast,
|
||||
onClose,
|
||||
closable = false,
|
||||
// To prevent autoclose, pass duration as Infinity
|
||||
duration = DEFAULT_TOAST_TIMEOUT,
|
||||
}: {
|
||||
message: string;
|
||||
clearToast: () => void;
|
||||
onClose: () => void;
|
||||
closable?: boolean;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const timerRef = useRef<number>(0);
|
||||
|
||||
const scheduleTimeout = useCallback(
|
||||
() =>
|
||||
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
|
||||
[clearToast],
|
||||
);
|
||||
const shouldAutoClose = duration !== Infinity;
|
||||
const scheduleTimeout = useCallback(() => {
|
||||
if (!shouldAutoClose) {
|
||||
return;
|
||||
}
|
||||
timerRef.current = window.setTimeout(() => onClose(), duration);
|
||||
}, [onClose, duration, shouldAutoClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoClose) {
|
||||
return;
|
||||
}
|
||||
scheduleTimeout();
|
||||
return () => clearTimeout(timerRef.current);
|
||||
}, [scheduleTimeout, message]);
|
||||
}, [scheduleTimeout, message, duration, shouldAutoClose]);
|
||||
|
||||
const onMouseEnter = shouldAutoClose
|
||||
? () => clearTimeout(timerRef?.current)
|
||||
: undefined;
|
||||
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
|
||||
return (
|
||||
<div
|
||||
className="Toast"
|
||||
onMouseEnter={() => clearTimeout(timerRef?.current)}
|
||||
onMouseLeave={scheduleTimeout}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<p className="Toast__message">{message}</p>
|
||||
{closable && (
|
||||
<ToolButton
|
||||
icon={close}
|
||||
aria-label="close"
|
||||
type="icon"
|
||||
onClick={onClose}
|
||||
className="close"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -212,16 +212,14 @@
|
||||
}
|
||||
}
|
||||
.ToolIcon.ToolIcon__library {
|
||||
top: 100px;
|
||||
top: calc(var(--sat) + 100px);
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: 0;
|
||||
top: 60px;
|
||||
top: calc(var(--sat) + 60px);
|
||||
}
|
||||
.ToolIcon.ToolIcon__penMode {
|
||||
margin-inline-end: 0;
|
||||
top: 140px;
|
||||
top: calc(var(--sat) + 140px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -32,7 +32,6 @@
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: var(--space-factor);
|
||||
&.ToolIcon_type_floating {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
|
@@ -116,7 +116,6 @@ export const IMAGE_RENDER_TIMEOUT = 500;
|
||||
export const TAP_TWICE_TIMEOUT = 300;
|
||||
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 = 100;
|
||||
export const ZOOM_STEP = 0.1;
|
||||
|
@@ -1,42 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const createInverseContext = <T extends unknown = null>(
|
||||
initialValue: T,
|
||||
) => {
|
||||
const Context = React.createContext(initialValue) as React.Context<T> & {
|
||||
_updateProviderValue?: (value: T) => void;
|
||||
};
|
||||
|
||||
class InverseConsumer extends React.Component {
|
||||
state = { value: initialValue };
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
Context._updateProviderValue = (value: T) => this.setState({ value });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Context.Provider value={this.state.value}>
|
||||
{this.props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InverseProvider extends React.Component<{ value: T }> {
|
||||
componentDidMount() {
|
||||
Context._updateProviderValue?.(this.props.value);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
Context._updateProviderValue?.(this.props.value);
|
||||
}
|
||||
render() {
|
||||
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Context,
|
||||
Consumer: InverseConsumer,
|
||||
Provider: InverseProvider,
|
||||
};
|
||||
};
|
@@ -1,5 +1,5 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { cleanAppStateForImageExport } from "../appState";
|
||||
import { cleanAppStateForExport } from "../appState";
|
||||
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
import { ExcalidrawElement, FileId } from "../element/types";
|
||||
@@ -143,7 +143,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
appState: {
|
||||
theme: localAppState?.theme,
|
||||
fileHandle: fileHandle || blob.handle || null,
|
||||
...cleanAppStateForImageExport(data.appState || {}),
|
||||
...cleanAppStateForExport(data.appState || {}),
|
||||
...(localAppState
|
||||
? calculateScrollCenter(
|
||||
data.elements || [],
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
FileWithHandle,
|
||||
fileOpen as _fileOpen,
|
||||
fileSave as _fileSave,
|
||||
FileSystemHandle,
|
||||
@@ -26,13 +25,9 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
extensions?: FILE_EXTENSION[];
|
||||
description: string;
|
||||
multiple?: M;
|
||||
}): Promise<
|
||||
M extends false | undefined ? FileWithHandle : FileWithHandle[]
|
||||
> => {
|
||||
}): Promise<M extends false | undefined ? File : File[]> => {
|
||||
// an unsafe TS hack, alas not much we can do AFAIK
|
||||
type RetType = M extends false | undefined
|
||||
? FileWithHandle
|
||||
: FileWithHandle[];
|
||||
type RetType = M extends false | undefined ? File : File[];
|
||||
|
||||
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||
mimeTypes.push(MIME_TYPES[type]);
|
||||
|
@@ -82,7 +82,7 @@ export const exportCanvas = async (
|
||||
await import(/* webpackChunkName: "image" */ "./image")
|
||||
).encodePngMetadata({
|
||||
blob,
|
||||
metadata: serializeAsJSON(elements, appState, files, "image"),
|
||||
metadata: serializeAsJSON(elements, appState, files, "local"),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,5 @@
|
||||
import { fileOpen, fileSave } from "./filesystem";
|
||||
import {
|
||||
cleanAppStateForImageExport,
|
||||
cleanAppStateForTextExport,
|
||||
clearAppStateForDatabase,
|
||||
} from "../appState";
|
||||
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
|
||||
import {
|
||||
EXPORT_DATA_TYPES,
|
||||
EXPORT_SOURCE,
|
||||
@@ -47,32 +43,25 @@ export const serializeAsJSON = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Partial<AppState>,
|
||||
files: BinaryFiles,
|
||||
destination: "text" | "image" | "database",
|
||||
type: "local" | "database",
|
||||
): string => {
|
||||
const cleanAppState = () => {
|
||||
switch (destination) {
|
||||
case "database":
|
||||
return clearAppStateForDatabase(appState);
|
||||
case "text":
|
||||
return cleanAppStateForTextExport(appState);
|
||||
case "image":
|
||||
return cleanAppStateForImageExport(appState);
|
||||
}
|
||||
};
|
||||
const data: ExportedDataState = {
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
version: VERSIONS.excalidraw,
|
||||
source: EXPORT_SOURCE,
|
||||
elements:
|
||||
destination === "database"
|
||||
? clearElementsForDatabase(elements)
|
||||
: clearElementsForExport(elements),
|
||||
appState: cleanAppState(),
|
||||
type === "local"
|
||||
? clearElementsForExport(elements)
|
||||
: clearElementsForDatabase(elements),
|
||||
appState:
|
||||
type === "local"
|
||||
? cleanAppStateForExport(appState)
|
||||
: clearAppStateForDatabase(appState),
|
||||
files:
|
||||
destination === "database"
|
||||
? // will be stripped from JSON
|
||||
undefined
|
||||
: filterOutDeletedFiles(elements, files),
|
||||
type === "local"
|
||||
? filterOutDeletedFiles(elements, files)
|
||||
: // will be stripped from JSON
|
||||
undefined,
|
||||
};
|
||||
|
||||
return JSON.stringify(data, null, 2);
|
||||
@@ -83,7 +72,7 @@ export const saveAsJSON = async (
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
) => {
|
||||
const serialized = serializeAsJSON(elements, appState, files, "text");
|
||||
const serialized = serializeAsJSON(elements, appState, files, "local");
|
||||
const blob = new Blob([serialized], {
|
||||
type: MIME_TYPES.excalidraw,
|
||||
});
|
||||
@@ -109,7 +98,12 @@ export const loadFromJSON = async (
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
// extensions: ["json", "excalidraw", "png", "svg"],
|
||||
});
|
||||
return loadFromBlob(await normalizeFile(file), localAppState, localElements);
|
||||
return loadFromBlob(
|
||||
await normalizeFile(file),
|
||||
localAppState,
|
||||
localElements,
|
||||
file.handle,
|
||||
);
|
||||
};
|
||||
|
||||
export const isValidExcalidrawData = (data?: {
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
LibraryItems,
|
||||
LibraryItems_anyVersion,
|
||||
} from "../types";
|
||||
import type { cleanAppStateForTextExport } from "../appState";
|
||||
import type { cleanAppStateForExport } from "../appState";
|
||||
import { VERSIONS } from "../constants";
|
||||
|
||||
export interface ExportedDataState {
|
||||
@@ -13,7 +13,7 @@ export interface ExportedDataState {
|
||||
version: number;
|
||||
source: string;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: ReturnType<typeof cleanAppStateForTextExport>;
|
||||
appState: ReturnType<typeof cleanAppStateForExport>;
|
||||
files: BinaryFiles | undefined;
|
||||
}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
||||
}, STORAGE_SIZE_TIMEOUT);
|
||||
|
||||
type Props = {
|
||||
setToastMessage: (message: string) => void;
|
||||
setToast: (message: string) => void;
|
||||
};
|
||||
const CustomStats = (props: Props) => {
|
||||
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
||||
@@ -68,7 +68,7 @@ const CustomStats = (props: Props) => {
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(getVersion());
|
||||
props.setToastMessage(t("toast.copyToClipboard"));
|
||||
props.setToast(t("toast.copyToClipboard"));
|
||||
} catch {}
|
||||
}}
|
||||
title={t("stats.versionCopy")}
|
||||
|
@@ -8,10 +8,12 @@ import {
|
||||
ExcalidrawElement,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "../../element/types";
|
||||
import { getSceneVersion } from "../../packages/excalidraw/index";
|
||||
import {
|
||||
getSceneVersion,
|
||||
restoreElements,
|
||||
} from "../../packages/excalidraw/index";
|
||||
import { Collaborator, Gesture } from "../../types";
|
||||
import {
|
||||
getFrame,
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
withBatchedUpdates,
|
||||
@@ -47,11 +49,9 @@ import {
|
||||
} from "../data/localStorage";
|
||||
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";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import {
|
||||
encodeFilesForUpload,
|
||||
FileManager,
|
||||
@@ -70,52 +70,45 @@ import {
|
||||
import { decryptData } from "../../data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiStore } from "../../jotai";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const collabDialogShownAtom = atom(false);
|
||||
export const isCollaboratingAtom = atom(false);
|
||||
|
||||
interface CollabState {
|
||||
modalIsShown: boolean;
|
||||
errorMessage: string;
|
||||
username: string;
|
||||
userState: UserIdleState;
|
||||
activeRoomLink: string;
|
||||
}
|
||||
|
||||
type CollabInstance = InstanceType<typeof CollabWrapper>;
|
||||
type CollabInstance = InstanceType<typeof Collab>;
|
||||
|
||||
export interface CollabAPI {
|
||||
/** function so that we can access the latest value from stale callbacks */
|
||||
isCollaborating: () => boolean;
|
||||
username: CollabState["username"];
|
||||
userState: CollabState["userState"];
|
||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
||||
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
||||
startCollaboration: CollabInstance["startCollaboration"];
|
||||
stopCollaboration: CollabInstance["stopCollaboration"];
|
||||
syncElements: CollabInstance["syncElements"];
|
||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||
setUsername: (username: string) => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
interface PublicProps {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
onRoomClose?: () => void;
|
||||
}
|
||||
|
||||
const {
|
||||
Context: CollabContext,
|
||||
Consumer: CollabContextConsumer,
|
||||
Provider: CollabContextProvider,
|
||||
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
|
||||
type Props = PublicProps & { modalIsShown: boolean };
|
||||
|
||||
export { CollabContext, CollabContextConsumer };
|
||||
|
||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
class Collab extends PureComponent<Props, CollabState> {
|
||||
portal: Portal;
|
||||
fileManager: FileManager;
|
||||
excalidrawAPI: Props["excalidrawAPI"];
|
||||
activeIntervalId: number | null;
|
||||
idleTimeoutId: number | null;
|
||||
|
||||
// marked as private to ensure we don't change it outside this class
|
||||
private _isCollaborating: boolean = false;
|
||||
private socketInitializationTimer?: number;
|
||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||
private collaborators = new Map<string, Collaborator>();
|
||||
@@ -123,10 +116,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
modalIsShown: false,
|
||||
errorMessage: "",
|
||||
username: importUsernameFromLocalStorage() || "",
|
||||
userState: UserIdleState.ACTIVE,
|
||||
activeRoomLink: "",
|
||||
};
|
||||
this.portal = new Portal(this);
|
||||
@@ -164,6 +155,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||
|
||||
const collabAPI: CollabAPI = {
|
||||
isCollaborating: this.isCollaborating,
|
||||
onPointerUpdate: this.onPointerUpdate,
|
||||
startCollaboration: this.startCollaboration,
|
||||
syncElements: this.syncElements,
|
||||
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
||||
stopCollaboration: this.stopCollaboration,
|
||||
setUsername: this.setUsername,
|
||||
};
|
||||
|
||||
jotaiStore.set(collabAPIAtom, collabAPI);
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
@@ -196,7 +199,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
}
|
||||
|
||||
isCollaborating = () => this._isCollaborating;
|
||||
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
||||
|
||||
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||
};
|
||||
|
||||
private onUnload = () => {
|
||||
this.destroySocketClient({ isUnload: true });
|
||||
@@ -208,7 +215,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
|
||||
if (
|
||||
this._isCollaborating &&
|
||||
this.isCollaborating() &&
|
||||
(this.fileManager.shouldPreventUnload(syncableElements) ||
|
||||
!isSavedToFirebase(this.portal, syncableElements))
|
||||
) {
|
||||
@@ -252,12 +259,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
openPortal = async () => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
return this.initializeSocketClient(null);
|
||||
};
|
||||
|
||||
closePortal = () => {
|
||||
stopCollaboration = (keepRemoteState = true) => {
|
||||
this.queueBroadcastAllElements.cancel();
|
||||
this.queueSaveToFirebase.cancel();
|
||||
this.loadImageFiles.cancel();
|
||||
@@ -267,16 +269,26 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
||||
|
||||
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||
this.portal.socket.off(
|
||||
"connect_error",
|
||||
this.fallbackInitializationHandler,
|
||||
);
|
||||
}
|
||||
|
||||
if (!keepRemoteState) {
|
||||
LocalData.fileStorage.reset();
|
||||
this.destroySocketClient();
|
||||
} else if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
||||
// hack to ensure that we prefer we disregard any new browser state
|
||||
// that could have been saved in other tabs while we were collaborating
|
||||
resetBrowserStateVersions();
|
||||
|
||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||
this.destroySocketClient();
|
||||
trackEvent("share", "room closed");
|
||||
|
||||
this.props.onRoomClose?.();
|
||||
LocalData.fileStorage.reset();
|
||||
|
||||
const elements = this.excalidrawAPI
|
||||
.getSceneElementsIncludingDeleted()
|
||||
@@ -295,20 +307,20 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
};
|
||||
|
||||
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||
this.portal.close();
|
||||
this.fileManager.reset();
|
||||
if (!opts?.isUnload) {
|
||||
this.setIsCollaborating(false);
|
||||
this.setState({
|
||||
activeRoomLink: "",
|
||||
});
|
||||
this.collaborators = new Map();
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
});
|
||||
this.setState({
|
||||
activeRoomLink: "",
|
||||
});
|
||||
this._isCollaborating = false;
|
||||
LocalData.resumeSave("collaboration");
|
||||
}
|
||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||
this.portal.close();
|
||||
this.fileManager.reset();
|
||||
};
|
||||
|
||||
private fetchImageFilesFromFirebase = async (scene: {
|
||||
@@ -349,7 +361,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
private initializeSocketClient = async (
|
||||
private fallbackInitializationHandler: null | (() => any) = null;
|
||||
|
||||
startCollaboration = async (
|
||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||
): Promise<ImportedDataState | null> => {
|
||||
if (this.portal.socket) {
|
||||
@@ -372,13 +386,23 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||
|
||||
this._isCollaborating = true;
|
||||
this.setIsCollaborating(true);
|
||||
LocalData.pauseSave("collaboration");
|
||||
|
||||
const { default: socketIOClient } = await import(
|
||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||
);
|
||||
|
||||
const fallbackInitializationHandler = () => {
|
||||
this.initializeRoom({
|
||||
roomLinkData: existingRoomLinkData,
|
||||
fetchScene: true,
|
||||
}).then((scene) => {
|
||||
scenePromise.resolve(scene);
|
||||
});
|
||||
};
|
||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||
|
||||
try {
|
||||
const socketServerData = await getCollabServer();
|
||||
|
||||
@@ -391,6 +415,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
roomId,
|
||||
roomKey,
|
||||
);
|
||||
|
||||
this.portal.socket.once("connect_error", fallbackInitializationHandler);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
@@ -419,13 +445,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
// fallback in case you're not alone in the room but still don't receive
|
||||
// initial SCENE_INIT message
|
||||
this.socketInitializationTimer = window.setTimeout(() => {
|
||||
this.initializeRoom({
|
||||
roomLinkData: existingRoomLinkData,
|
||||
fetchScene: true,
|
||||
});
|
||||
scenePromise.resolve(null);
|
||||
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
||||
this.socketInitializationTimer = window.setTimeout(
|
||||
fallbackInitializationHandler,
|
||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||
);
|
||||
|
||||
// All socket listeners are moving to Portal
|
||||
this.portal.socket.on(
|
||||
@@ -530,6 +553,12 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
| { fetchScene: false; roomLinkData?: null }) => {
|
||||
clearTimeout(this.socketInitializationTimer!);
|
||||
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||
this.portal.socket.off(
|
||||
"connect_error",
|
||||
this.fallbackInitializationHandler,
|
||||
);
|
||||
}
|
||||
if (fetchScene && roomLinkData && this.portal.socket) {
|
||||
this.excalidrawAPI.resetScene();
|
||||
|
||||
@@ -567,6 +596,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
const localElements = this.getSceneElementsIncludingDeleted();
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
remoteElements = restoreElements(remoteElements, null);
|
||||
|
||||
const reconciledElements = _reconcileElements(
|
||||
localElements,
|
||||
remoteElements,
|
||||
@@ -672,19 +703,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
};
|
||||
|
||||
setCollaborators(sockets: string[]) {
|
||||
this.setState((state) => {
|
||||
const collaborators: InstanceType<typeof CollabWrapper>["collaborators"] =
|
||||
new Map();
|
||||
for (const socketId of sockets) {
|
||||
if (this.collaborators.has(socketId)) {
|
||||
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
||||
} else {
|
||||
collaborators.set(socketId, {});
|
||||
}
|
||||
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
||||
new Map();
|
||||
for (const socketId of sockets) {
|
||||
if (this.collaborators.has(socketId)) {
|
||||
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
||||
} else {
|
||||
collaborators.set(socketId, {});
|
||||
}
|
||||
this.collaborators = collaborators;
|
||||
this.excalidrawAPI.updateScene({ collaborators });
|
||||
});
|
||||
}
|
||||
this.collaborators = collaborators;
|
||||
this.excalidrawAPI.updateScene({ collaborators });
|
||||
}
|
||||
|
||||
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
||||
@@ -713,7 +742,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
|
||||
onIdleStateChange = (userState: UserIdleState) => {
|
||||
this.setState({ userState });
|
||||
this.portal.broadcastIdleChange(userState);
|
||||
};
|
||||
|
||||
@@ -747,18 +775,22 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
|
||||
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
||||
|
||||
queueSaveToFirebase = throttle(() => {
|
||||
if (this.portal.socketInitialized) {
|
||||
this.saveCollabRoomToFirebase(
|
||||
getSyncableElements(
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
||||
queueSaveToFirebase = throttle(
|
||||
() => {
|
||||
if (this.portal.socketInitialized) {
|
||||
this.saveCollabRoomToFirebase(
|
||||
getSyncableElements(
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||
{ leading: false },
|
||||
);
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ modalIsShown: false });
|
||||
jotaiStore.set(collabDialogShownAtom, false);
|
||||
};
|
||||
|
||||
setUsername = (username: string) => {
|
||||
@@ -770,35 +802,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
saveUsernameToLocalStorage(username);
|
||||
};
|
||||
|
||||
onCollabButtonClick = () => {
|
||||
this.setState({
|
||||
modalIsShown: true,
|
||||
});
|
||||
};
|
||||
|
||||
/** PRIVATE. Use `this.getContextValue()` instead. */
|
||||
private contextValue: CollabAPI | null = null;
|
||||
|
||||
/** Getter of context value. Returned object is stable. */
|
||||
getContextValue = (): CollabAPI => {
|
||||
if (!this.contextValue) {
|
||||
this.contextValue = {} as CollabAPI;
|
||||
}
|
||||
|
||||
this.contextValue.isCollaborating = this.isCollaborating;
|
||||
this.contextValue.username = this.state.username;
|
||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
|
||||
this.contextValue.syncElements = this.syncElements;
|
||||
this.contextValue.fetchImageFilesFromFirebase =
|
||||
this.fetchImageFilesFromFirebase;
|
||||
this.contextValue.setUsername = this.setUsername;
|
||||
return this.contextValue;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
|
||||
const { username, errorMessage, activeRoomLink } = this.state;
|
||||
|
||||
const { modalIsShown } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -808,8 +815,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
activeRoomLink={activeRoomLink}
|
||||
username={username}
|
||||
onUsernameChange={this.onUsernameChange}
|
||||
onRoomCreate={this.openPortal}
|
||||
onRoomDestroy={this.closePortal}
|
||||
onRoomCreate={() => this.startCollaboration(null)}
|
||||
onRoomDestroy={this.stopCollaboration}
|
||||
setErrorMessage={(errorMessage) => {
|
||||
this.setState({ errorMessage });
|
||||
}}
|
||||
@@ -822,11 +829,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
onClose={() => this.setState({ errorMessage: "" })}
|
||||
/>
|
||||
)}
|
||||
<CollabContextProvider
|
||||
value={{
|
||||
api: this.getContextValue(),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -834,7 +836,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
collab: InstanceType<typeof CollabWrapper>;
|
||||
collab: InstanceType<typeof Collab>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,4 +847,11 @@ if (
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
}
|
||||
|
||||
export default CollabWrapper;
|
||||
const _Collab: React.FC<PublicProps> = (props) => {
|
||||
const [collabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
return <Collab {...props} modalIsShown={collabDialogShown} />;
|
||||
};
|
||||
|
||||
export default _Collab;
|
||||
|
||||
export type TCollabClass = Collab;
|
@@ -4,7 +4,7 @@ import {
|
||||
SocketUpdateDataSource,
|
||||
} from "../data";
|
||||
|
||||
import CollabWrapper from "./CollabWrapper";
|
||||
import { TCollabClass } from "./Collab";
|
||||
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import {
|
||||
@@ -20,14 +20,14 @@ import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||
import { encryptData } from "../../data/encryption";
|
||||
|
||||
class Portal {
|
||||
collab: CollabWrapper;
|
||||
collab: TCollabClass;
|
||||
socket: SocketIOClient.Socket | null = null;
|
||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||
roomId: string | null = null;
|
||||
roomKey: string | null = null;
|
||||
broadcastedElementVersions: Map<string, number> = new Map();
|
||||
|
||||
constructor(collab: CollabWrapper) {
|
||||
constructor(collab: TCollabClass) {
|
||||
this.collab = collab;
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,8 @@ import { t } from "../../i18n";
|
||||
import "./RoomDialog.scss";
|
||||
import Stack from "../../components/Stack";
|
||||
import { AppState } from "../../types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { getFrame } from "../../utils";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
@@ -95,7 +97,10 @@ const RoomDialog = ({
|
||||
title={t("roomDialog.button_startSession")}
|
||||
aria-label={t("roomDialog.button_startSession")}
|
||||
showAriaLabel={true}
|
||||
onClick={onRoomCreate}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
onRoomCreate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -160,7 +165,10 @@ const RoomDialog = ({
|
||||
title={t("roomDialog.button_stopSession")}
|
||||
aria-label={t("roomDialog.button_stopSession")}
|
||||
showAriaLabel={true}
|
||||
onClick={onRoomDestroy}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room closed");
|
||||
onRoomDestroy();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@@ -134,9 +134,16 @@ export type SocketUpdateData =
|
||||
_brand: "socketUpdateData";
|
||||
};
|
||||
|
||||
const RE_COLLAB_LINK = /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
|
||||
|
||||
export const isCollaborationLink = (link: string) => {
|
||||
const hash = new URL(link).hash;
|
||||
return RE_COLLAB_LINK.test(hash);
|
||||
};
|
||||
|
||||
export const getCollaborationLinkData = (link: string) => {
|
||||
const hash = new URL(link).hash;
|
||||
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
||||
const match = hash.match(RE_COLLAB_LINK);
|
||||
if (match && match[2].length !== 22) {
|
||||
window.alert(t("alerts.invalidEncryptionKey"));
|
||||
return null;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
@@ -45,20 +45,26 @@ import {
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
import CollabWrapper, {
|
||||
import Collab, {
|
||||
CollabAPI,
|
||||
CollabContext,
|
||||
CollabContextConsumer,
|
||||
} from "./collab/CollabWrapper";
|
||||
collabAPIAtom,
|
||||
collabDialogShownAtom,
|
||||
isCollaboratingAtom,
|
||||
} from "./collab/Collab";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
isCollaborationLink,
|
||||
loadScene,
|
||||
} from "./data";
|
||||
import {
|
||||
getLibraryItemsFromStorage,
|
||||
importFromLocalStorage,
|
||||
importUsernameFromLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
import { restoreAppState, RestoredDataState } from "../data/restore";
|
||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { shield } from "../components/icons";
|
||||
|
||||
@@ -72,8 +78,13 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import { Provider, useAtom } from "jotai";
|
||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
@@ -170,7 +181,7 @@ const initializeScene = async (opts: {
|
||||
|
||||
if (roomLinkData) {
|
||||
return {
|
||||
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
|
||||
scene: await opts.collabAPI.startCollaboration(roomLinkData),
|
||||
isExternalScene: true,
|
||||
id: roomLinkData.roomId,
|
||||
key: roomLinkData.roomKey,
|
||||
@@ -242,7 +253,11 @@ const ExcalidrawWrapper = () => {
|
||||
const [excalidrawAPI, excalidrawRefCallback] =
|
||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||
|
||||
const collabAPI = useContext(CollabContext)?.api;
|
||||
const [collabAPI] = useAtom(collabAPIAtom);
|
||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||
return isCollaborationLink(window.location.href);
|
||||
});
|
||||
|
||||
useHandleLibrary({
|
||||
excalidrawAPI,
|
||||
@@ -320,21 +335,44 @@ const ExcalidrawWrapper = () => {
|
||||
}
|
||||
};
|
||||
|
||||
initializeScene({ collabAPI }).then((data) => {
|
||||
initializeScene({ collabAPI }).then(async (data) => {
|
||||
loadImages(data, /* isInitialLoad */ true);
|
||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||
|
||||
initialStatePromiseRef.current.promise.resolve({
|
||||
...data.scene,
|
||||
// at this point the state may have already been updated (e.g. when
|
||||
// collaborating, we may have received updates from other clients)
|
||||
appState: restoreAppState(
|
||||
data.scene?.appState,
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
elements: reconcileElements(
|
||||
data.scene?.elements || [],
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const onHashChange = async (event: HashChangeEvent) => {
|
||||
event.preventDefault();
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
if (!libraryUrlTokens) {
|
||||
if (
|
||||
collabAPI.isCollaborating() &&
|
||||
!isCollaborationLink(window.location.href)
|
||||
) {
|
||||
collabAPI.stopCollaboration(false);
|
||||
}
|
||||
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
||||
|
||||
initializeScene({ collabAPI }).then((data) => {
|
||||
loadImages(data);
|
||||
if (data.scene) {
|
||||
excalidrawAPI.updateScene({
|
||||
...data.scene,
|
||||
appState: restoreAppState(data.scene.appState, null),
|
||||
...restore(data.scene, null, null),
|
||||
commitToHistory: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -622,7 +660,7 @@ const ExcalidrawWrapper = () => {
|
||||
const renderCustomStats = () => {
|
||||
return (
|
||||
<CustomStats
|
||||
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
|
||||
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -636,23 +674,19 @@ const ExcalidrawWrapper = () => {
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||
};
|
||||
|
||||
const onRoomClose = useCallback(() => {
|
||||
LocalData.fileStorage.reset();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%" }}
|
||||
className={clsx("excalidraw-app", {
|
||||
"is-collaborating": collabAPI?.isCollaborating(),
|
||||
"is-collaborating": isCollaborating,
|
||||
})}
|
||||
>
|
||||
<Excalidraw
|
||||
ref={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating()}
|
||||
onCollabButtonClick={() => setCollabDialogShown(true)}
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
@@ -686,12 +720,7 @@ const ExcalidrawWrapper = () => {
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{excalidrawAPI && (
|
||||
<CollabWrapper
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
onRoomClose={onRoomClose}
|
||||
/>
|
||||
)}
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
<ErrorDialog
|
||||
message={errorMessage}
|
||||
@@ -705,9 +734,9 @@ const ExcalidrawWrapper = () => {
|
||||
const ExcalidrawApp = () => {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<CollabContextConsumer>
|
||||
<Provider unstable_createStore={() => jotaiStore}>
|
||||
<ExcalidrawWrapper />
|
||||
</CollabContextConsumer>
|
||||
</Provider>
|
||||
</TopErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -14,6 +14,7 @@ interface Window {
|
||||
__EXCALIDRAW_SHA__: string | undefined;
|
||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||
gtag: Function;
|
||||
}
|
||||
|
||||
|
@@ -48,10 +48,13 @@ const allLanguages: Language[] = [
|
||||
{ code: "ru-RU", label: "Русский" },
|
||||
{ code: "sk-SK", label: "Slovenčina" },
|
||||
{ code: "sv-SE", label: "Svenska" },
|
||||
{ code: "sl-SI", label: "Slovenščina" },
|
||||
{ code: "tr-TR", label: "Türkçe" },
|
||||
{ code: "uk-UA", label: "Українська" },
|
||||
{ code: "zh-CN", label: "简体中文" },
|
||||
{ code: "zh-TW", label: "繁體中文" },
|
||||
{ code: "vi-VN", label: "Tiếng Việt" },
|
||||
{ code: "mr-IN", label: "मराठी" },
|
||||
].concat([defaultLang]);
|
||||
|
||||
export const languages: Language[] = allLanguages
|
||||
@@ -86,7 +89,7 @@ export const setLanguage = async (lang: Language) => {
|
||||
currentLangData = {};
|
||||
} else {
|
||||
currentLangData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
||||
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
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"));
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExcalidrawApp />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
25
src/jotai.ts
25
src/jotai.ts
@@ -1,4 +1,27 @@
|
||||
import { unstable_createStore } from "jotai";
|
||||
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export const jotaiScope = Symbol();
|
||||
export const jotaiStore = unstable_createStore();
|
||||
|
||||
export const useAtomWithInitialValue = <
|
||||
T extends unknown,
|
||||
A extends WritableAtom<T, T>,
|
||||
>(
|
||||
atom: A,
|
||||
initialValue: T | (() => T),
|
||||
) => {
|
||||
const [value, setValue] = useAtom(atom);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (typeof initialValue === "function") {
|
||||
// @ts-ignore
|
||||
setValue(initialValue());
|
||||
} else {
|
||||
setValue(initialValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return [value, setValue] as const;
|
||||
};
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "إعادة تعيين اللوحة",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
|
||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Нулиране на платно",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "Copia",
|
||||
"copyAsPng": "Copia al porta-retalls com a PNG",
|
||||
"copyAsSvg": "Copia al porta-retalls com a SVG",
|
||||
"copyText": "",
|
||||
"copyText": "Copia al porta-retalls com a text",
|
||||
"bringForward": "Porta endavant",
|
||||
"sendToBack": "Envia enrere",
|
||||
"bringToFront": "Porta al davant",
|
||||
@@ -115,12 +115,18 @@
|
||||
"label": "Enllaç"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Bloca",
|
||||
"unlock": "Desbloca",
|
||||
"lockAll": "Bloca-ho tot",
|
||||
"unlockAll": "Desbloca-ho tot"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publicat",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Neteja el llenç",
|
||||
@@ -168,7 +174,7 @@
|
||||
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
|
||||
"importBackendFailed": "Importació fallida.",
|
||||
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "No s'ha pogut copiar al porta-retalls.",
|
||||
"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?",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
|
||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
||||
@@ -190,7 +197,7 @@
|
||||
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||
"invalidSVGString": "SVG no vàlid.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "No s'ha pogut carregar la biblioteca"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selecció",
|
||||
@@ -206,7 +213,7 @@
|
||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
|
||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||
"eraser": ""
|
||||
"eraser": "Esborrador"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accions del llenç",
|
||||
@@ -232,7 +239,7 @@
|
||||
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
||||
"eraserRevert": ""
|
||||
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||
@@ -292,7 +299,7 @@
|
||||
"howto": "Seguiu les nostres guies",
|
||||
"or": "o",
|
||||
"preventBinding": "Prevenir vinculació de la fletxa",
|
||||
"tools": "",
|
||||
"tools": "Eines",
|
||||
"shortcuts": "Dreceres de teclat",
|
||||
"textFinish": "Finalitza l'edició (editor de text)",
|
||||
"textNewLine": "Afegeix una línia nova (editor de text)",
|
||||
@@ -343,7 +350,7 @@
|
||||
},
|
||||
"noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:",
|
||||
"atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Biblioteca enviada",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "Kopírovat",
|
||||
"copyAsPng": "Zkopírovat do schránky jako PNG",
|
||||
"copyAsSvg": "Zkopírovat do schránky jako SVG",
|
||||
"copyText": "",
|
||||
"copyText": "Zkopírovat do schránky jako text",
|
||||
"bringForward": "Přenést blíž",
|
||||
"sendToBack": "Přenést do pozadí",
|
||||
"bringToFront": "Přenést do popředí",
|
||||
@@ -36,14 +36,14 @@
|
||||
"arrowhead_arrow": "Šipka",
|
||||
"arrowhead_bar": "Kóta",
|
||||
"arrowhead_dot": "Tečka",
|
||||
"arrowhead_triangle": "",
|
||||
"arrowhead_triangle": "Trojúhelník",
|
||||
"fontSize": "Velikost písma",
|
||||
"fontFamily": "Písmo",
|
||||
"onlySelected": "Pouze vybrané",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"withBackground": "Pozadí",
|
||||
"exportEmbedScene": "Vložit scénu",
|
||||
"exportEmbedScene_details": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru.",
|
||||
"addWatermark": "Přidat \"Vyrobeno s Excalidraw\"",
|
||||
"handDrawn": "Od ruky",
|
||||
"normal": "Normální",
|
||||
"code": "Kód",
|
||||
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Alle sperren",
|
||||
"unlockAll": "Alle entsperren"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Veröffentlicht",
|
||||
"sidebarLock": "Seitenleiste offen lassen"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Noch keine Elemente hinzugefügt...",
|
||||
"hint_emptyLibrary": "Wähle ein Element auf der Zeichenfläche, um es hier hinzuzufügen. Oder installiere eine Bibliothek aus dem öffentlichen Verzeichnis.",
|
||||
"hint_emptyPrivateLibrary": "Wähle ein Element von der Zeichenfläche, um es hier hinzuzufügen."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
||||
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
||||
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
|
||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
|
||||
"browserZoom": "Die Zoomstufe Deines Browsers ist nicht auf 100% gesetzt, was dazu führen kann, dass der Zeichenbereich falsch angezeigt wird"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
||||
@@ -343,7 +350,7 @@
|
||||
},
|
||||
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
|
||||
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Bibliothek übermittelt",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Επαναφορά του καμβά",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||
|
@@ -187,7 +187,8 @@
|
||||
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
|
||||
"resetLibrary": "This will clear your library. Are you sure?",
|
||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
|
||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
||||
"browserZoom": "Your browser's zoom level is not set to 100% which may cause the board to display incorrectly"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Unsupported file type.",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Bloquear todo",
|
||||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Mantener barra lateral abierta"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "Seleccione un elemento en el lienzo para añadirlo aquí, o instale una biblioteca del repositorio público, a continuación.",
|
||||
"hint_emptyPrivateLibrary": "Seleccione un elemento del lienzo para añadirlo aquí."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
||||
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
||||
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
|
||||
"browserZoom": "El nivel de zoom de tu navegador no está configurado al 100%, lo que puede causar que el tablero se muestre de manera incorrecta"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tipo de archivo no admitido.",
|
||||
@@ -343,7 +350,7 @@
|
||||
},
|
||||
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
|
||||
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Biblioteca enviada",
|
||||
|
@@ -115,12 +115,18 @@
|
||||
"label": "Esteka"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Blokeatu",
|
||||
"unlock": "Desblokeatu",
|
||||
"lockAll": "Blokeatu guztiak",
|
||||
"unlockAll": "Desblokeatu guztiak"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Argitaratua",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Garbitu oihala",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
||||
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
||||
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
|
||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
||||
@@ -190,7 +197,7 @@
|
||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||
"invalidSVGString": "SVG baliogabea.",
|
||||
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "Ezin izan da liburutegia kargatu"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Hautapena",
|
||||
@@ -300,7 +307,7 @@
|
||||
"view": "Bistaratu",
|
||||
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
||||
"zoomToSelection": "Zooma hautapenera",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "Blokeatu/desbloketatu hautapena"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Garbitu oihala"
|
||||
@@ -343,7 +350,7 @@
|
||||
},
|
||||
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
|
||||
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Liburutegia bidali da",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "کپی",
|
||||
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
|
||||
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
|
||||
"copyText": "",
|
||||
"copyText": "کپی در حافطه موقت به صورت متن",
|
||||
"bringForward": "جلو آوردن",
|
||||
"sendToBack": "پس فرستادن",
|
||||
"bringToFront": "جلو آوردن",
|
||||
@@ -36,12 +36,12 @@
|
||||
"arrowhead_arrow": "پیکان",
|
||||
"arrowhead_bar": "میله ای",
|
||||
"arrowhead_dot": "نقطه",
|
||||
"arrowhead_triangle": "",
|
||||
"arrowhead_triangle": "مثلث",
|
||||
"fontSize": "اندازه قلم",
|
||||
"fontFamily": "نوع قلم",
|
||||
"onlySelected": "فقط انتخاب شده ها",
|
||||
"withBackground": "پس زمینه",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene": "تعبیه صحنه",
|
||||
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
|
||||
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
|
||||
"handDrawn": "دست نویس",
|
||||
@@ -65,13 +65,13 @@
|
||||
"cartoonist": "کارتونیست",
|
||||
"fileTitle": "نام فایل",
|
||||
"colorPicker": "انتخابگر رنگ",
|
||||
"canvasColors": "",
|
||||
"canvasColors": "رنگ های بوم",
|
||||
"canvasBackground": "بوم",
|
||||
"drawingCanvas": "بوم نقاشی",
|
||||
"layers": "لایه ها",
|
||||
"actions": "عملیات",
|
||||
"language": "زبان",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "همکاری زنده",
|
||||
"duplicateSelection": "تکرار",
|
||||
"untitled": "بدون عنوان",
|
||||
"name": "نام",
|
||||
@@ -98,29 +98,35 @@
|
||||
"flipHorizontal": "چرخش افقی",
|
||||
"flipVertical": "چرخش عمودی",
|
||||
"viewMode": "حالت نمایش",
|
||||
"toggleExportColorScheme": "",
|
||||
"toggleExportColorScheme": "تغییر طرح خروجی رنگ",
|
||||
"share": "اشتراکگذاری",
|
||||
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
|
||||
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
|
||||
"toggleTheme": "تغییر تم",
|
||||
"personalLib": "",
|
||||
"excalidrawLib": "",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"personalLib": "کتابخانه شخصی",
|
||||
"excalidrawLib": "کتابخانه",
|
||||
"decreaseFontSize": "کاهش اندازه فونت",
|
||||
"increaseFontSize": "افزایش دادن اندازه فونت",
|
||||
"unbindText": "بازکردن نوشته",
|
||||
"bindText": "بستن نوشته",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"edit": "ویرایش لینک",
|
||||
"create": "ایجاد پیوند",
|
||||
"label": "لینک"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "قفل",
|
||||
"unlock": "باز کردن",
|
||||
"lockAll": "قفل همه",
|
||||
"unlockAll": "باز کردن قفل همه"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "منتشر شده",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "پاکسازی بوم نقاشی",
|
||||
@@ -147,19 +153,19 @@
|
||||
"edit": "ویرایش",
|
||||
"undo": "بازگرد",
|
||||
"redo": "از سر",
|
||||
"resetLibrary": "",
|
||||
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||
"createNewRoom": "ایجاد یک اتاق جدید",
|
||||
"fullScreen": "تمامصفحه",
|
||||
"darkMode": "حالت تیره",
|
||||
"lightMode": "حالت روشن",
|
||||
"zenMode": "حالت ذن",
|
||||
"exitZenMode": "خروج از حالت تمرکز",
|
||||
"cancel": "",
|
||||
"clear": "",
|
||||
"remove": "",
|
||||
"publishLibrary": "",
|
||||
"submit": "",
|
||||
"confirm": ""
|
||||
"cancel": "لغو",
|
||||
"clear": "پاک کردن",
|
||||
"remove": "پاک کردن",
|
||||
"publishLibrary": "انتشار",
|
||||
"submit": "ارسال",
|
||||
"confirm": "تایید"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
||||
@@ -168,33 +174,34 @@
|
||||
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
|
||||
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
|
||||
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "به کلیپ بورد کپی نشد.",
|
||||
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
||||
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
||||
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
||||
"collabStopOverridePrompt": "",
|
||||
"collabStopOverridePrompt": "با توقف بوم نقاشی، نقشه قبلی و ذخیره شده محلی شما را بازنویسی می کند. مطمئنی؟\n\n(اگر می خواهید نقاشی محلی خود را حفظ کنید، به سادگی برگه مرورگر را ببندید.)",
|
||||
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
|
||||
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
|
||||
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
||||
"imageDoesNotContainScene": "",
|
||||
"imageDoesNotContainScene": "به نظر نمی رسد این تصویر حاوی داده های بوم نقاشی باشد. آیا جاسازی صحنه را در حین خروجی فعال کرده اید?",
|
||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
|
||||
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
|
||||
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||
"invalidSVGString": "SVG نادرست.",
|
||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||
"importLibraryError": "دادهها بارگذاری نشدند"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "گزینش",
|
||||
"image": "",
|
||||
"image": "وارد کردن تصویر",
|
||||
"rectangle": "مستطیل",
|
||||
"diamond": "لوزی",
|
||||
"ellipse": "بیضی",
|
||||
@@ -204,9 +211,9 @@
|
||||
"text": "متن",
|
||||
"library": "کتابخانه",
|
||||
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": ""
|
||||
"penMode": "از زوم کوچک کردن جلوگیری کنید و ورودی آزاد را فقط از قلم بپذیرید",
|
||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||
"eraser": "پاک کن"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "عملیات روی بوم",
|
||||
@@ -214,25 +221,25 @@
|
||||
"shapes": "شکلها"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
|
||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
||||
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
||||
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
|
||||
"resizeImage": "",
|
||||
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
|
||||
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"publishLibrary": "",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": ""
|
||||
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
|
||||
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
|
||||
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
|
||||
"publishLibrary": "کتابخانه خود را منتشر کنید",
|
||||
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||
@@ -260,27 +267,27 @@
|
||||
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
|
||||
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
|
||||
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
|
||||
"shareTitle": ""
|
||||
"shareTitle": "به یک جلسه همکاری زنده در Excalidraw بپیوندید"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "خطا"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "ذخیره در دیسک",
|
||||
"disk_details": "",
|
||||
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
|
||||
"disk_button": "ذخیره در فایل",
|
||||
"link_title": "لینک قابل اشتراکگذاری",
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"excalidrawplus_description": "",
|
||||
"link_details": "خروجی به عنوان یک پیوند فقط خواندنی.",
|
||||
"link_button": "خروجی در فایل",
|
||||
"excalidrawplus_description": "صحنه را در فضای کاری Excalidraw+ خود ذخیره کنید.",
|
||||
"excalidrawplus_button": "خروجی گرفتن",
|
||||
"excalidrawplus_exportError": ""
|
||||
"excalidrawplus_exportError": "در حال حاضر نمیتوان به Excalidraw+ صادر کرد..."
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "بلاگ ما را بخوانید",
|
||||
"click": "کلیک",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"deepSelect": "انتخاب عمیق",
|
||||
"deepBoxSelect": "انتخاب عمیق در کادر، و جلوگیری از کشیدن",
|
||||
"curvedArrow": "فلش خمیده",
|
||||
"curvedLine": "منحنی",
|
||||
"documentation": "مستندات",
|
||||
@@ -292,7 +299,7 @@
|
||||
"howto": "راهنمای ما را دنبال کنید",
|
||||
"or": "یا",
|
||||
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
||||
"tools": "",
|
||||
"tools": "ابزار",
|
||||
"shortcuts": "میانبرهای صفحه کلید",
|
||||
"textFinish": "پایان ویرایش (ویرایشگر متن)",
|
||||
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
|
||||
@@ -300,63 +307,63 @@
|
||||
"view": "مشاهده",
|
||||
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "قفل/بازکردن انتخاب شده ها"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": ""
|
||||
"title": "پاک کردن بوم"
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "",
|
||||
"itemName": "",
|
||||
"authorName": "",
|
||||
"githubUsername": "",
|
||||
"twitterUsername": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"website": "",
|
||||
"title": "انتشار کتابخانه",
|
||||
"itemName": "نام آیتم",
|
||||
"authorName": "نام نویسنده",
|
||||
"githubUsername": "نام کاربری گیت هاب",
|
||||
"twitterUsername": "نام کاربری توییتر",
|
||||
"libraryName": "نام کتابخانه",
|
||||
"libraryDesc": "توضیحات کتابخانه",
|
||||
"website": "تارنما",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
"authorName": "نام یا نام کاربری شما",
|
||||
"libraryName": "اسم کتابخانه",
|
||||
"libraryDesc": "شرحی از کتابخانه شما برای کمک به مردم برای درک استفاده از آن",
|
||||
"githubHandle": "دسته GitHub (اختیاری)، بنابراین می توانید پس از ارسال برای بررسی، کتابخانه را ویرایش کنید",
|
||||
"twitterHandle": "نام کاربری توییتر (اختیاری)، بنابراین می دانیم هنگام تبلیغ در توییتر به چه کسی اعتبار دهیم",
|
||||
"website": "پیوند به وب سایت شخصی شما یا هر جای دیگر (اختیاری)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "",
|
||||
"website": ""
|
||||
"required": "لازم",
|
||||
"website": "وارد کردن آدرس درست"
|
||||
},
|
||||
"noteDescription": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "کتابخانه خود را ارسال کنید تا در آن گنجانده شود ",
|
||||
"link": "مخزن کتابخانه عمومی",
|
||||
"post": "تا افراد دیگر در نقاشی های خود از آن استفاده کنند."
|
||||
},
|
||||
"noteGuidelines": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "کتابخانه باید ابتدا به صورت دستی تایید شود. لطفاً بخوانید ",
|
||||
"link": "دستورالعملها",
|
||||
"post": " قبل از ارسال برای برقراری ارتباط و ایجاد تغییرات در صورت درخواست، به یک حساب GitHub نیاز دارید، اما به شدت الزامی نیست."
|
||||
},
|
||||
"noteLicense": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "با ارسال، موافقت می کنید که کتابخانه تحت عنوان منتشر شود ",
|
||||
"link": "پروانهٔ MIT ",
|
||||
"post": "که به طور خلاصه به این معنی است که هر کسی می تواند بدون محدودیت از آنها استفاده کند."
|
||||
},
|
||||
"noteItems": "",
|
||||
"atleastOneLibItem": "",
|
||||
"republishWarning": ""
|
||||
"noteItems": "هر مورد کتابخانه باید نام خاص خود را داشته باشد تا قابل فیلتر باشد. اقلام کتابخانه زیر شامل خواهد شد:",
|
||||
"atleastOneLibItem": "لطفاً حداقل یک مورد از کتابخانه را برای شروع انتخاب کنید",
|
||||
"republishWarning": "توجه: برخی از موارد انتخاب شده به عنوان قبلاً منتشر شده/ارسال شده علامت گذاری شده اند. شما فقط باید هنگام بهروزرسانی یک کتابخانه موجود یا ارسال، موارد را دوباره ارسال کنید."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"link": ""
|
||||
"title": "کتابخانه ارسال شد",
|
||||
"content": "تشکر از شما {{authorName}}. کتابخانه شما برای بررسی ارسال شده است. می توانید وضعیت را پیگیری کنید",
|
||||
"link": "اینجا"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
||||
"link": ""
|
||||
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "زاویه",
|
||||
@@ -374,60 +381,60 @@
|
||||
"width": "عرض"
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"addedToLibrary": "به مجموعه اضافه شد",
|
||||
"copyStyles": "کپی سبک.",
|
||||
"copyToClipboard": "در کلیپبورد کپی شد.",
|
||||
"copyToClipboardAsPng": "",
|
||||
"copyToClipboardAsPng": "کپی {{exportSelection}} در کلیپبورد به عنوان PNG\n({{exportColorScheme}})",
|
||||
"fileSaved": "فایل ذخیره شد.",
|
||||
"fileSavedToFilename": "ذخیره در {filename}",
|
||||
"canvas": "بوم",
|
||||
"selection": "انتخاب"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "",
|
||||
"f8f9fa": "",
|
||||
"f1f3f5": "",
|
||||
"fff5f5": "",
|
||||
"fff0f6": "",
|
||||
"f8f0fc": "",
|
||||
"f3f0ff": "",
|
||||
"edf2ff": "",
|
||||
"e7f5ff": "",
|
||||
"e3fafc": "",
|
||||
"e6fcf5": "",
|
||||
"ebfbee": "",
|
||||
"f4fce3": "",
|
||||
"fff9db": "",
|
||||
"fff4e6": "",
|
||||
"transparent": "",
|
||||
"ced4da": "",
|
||||
"868e96": "",
|
||||
"fa5252": "",
|
||||
"e64980": "",
|
||||
"be4bdb": "",
|
||||
"7950f2": "",
|
||||
"4c6ef5": "",
|
||||
"228be6": "",
|
||||
"15aabf": "",
|
||||
"12b886": "",
|
||||
"40c057": "",
|
||||
"82c91e": "",
|
||||
"fab005": "",
|
||||
"fd7e14": "",
|
||||
"000000": "",
|
||||
"343a40": "",
|
||||
"495057": "",
|
||||
"c92a2a": "",
|
||||
"a61e4d": "",
|
||||
"862e9c": "",
|
||||
"5f3dc4": "",
|
||||
"364fc7": "",
|
||||
"1864ab": "",
|
||||
"0b7285": "",
|
||||
"087f5b": "",
|
||||
"2b8a3e": "",
|
||||
"5c940d": "",
|
||||
"e67700": "",
|
||||
"d9480f": ""
|
||||
"ffffff": "سفید",
|
||||
"f8f9fa": "خاکستری 0",
|
||||
"f1f3f5": "خاکستری 1",
|
||||
"fff5f5": "قرمز 0",
|
||||
"fff0f6": "صورتی 0",
|
||||
"f8f0fc": "انگوری 0",
|
||||
"f3f0ff": "بنفش 0",
|
||||
"edf2ff": "نیلی 0",
|
||||
"e7f5ff": "آبی 0",
|
||||
"e3fafc": "آبی نفتی 0",
|
||||
"e6fcf5": "آبی فیروزه ای 0",
|
||||
"ebfbee": "سبز 0",
|
||||
"f4fce3": "لیمویی 0",
|
||||
"fff9db": "زرد 0",
|
||||
"fff4e6": "نارنجی 0",
|
||||
"transparent": "شفاف",
|
||||
"ced4da": "خاکستری 4",
|
||||
"868e96": "خاکستری 6",
|
||||
"fa5252": "قرمز 6",
|
||||
"e64980": "صورتی 6",
|
||||
"be4bdb": "انگوری 6",
|
||||
"7950f2": "بنفش 6",
|
||||
"4c6ef5": "نیلی 6",
|
||||
"228be6": "آبی 6",
|
||||
"15aabf": "آبی نفتی 6",
|
||||
"12b886": "آبی فیروزه ای 6",
|
||||
"40c057": "سبز 6",
|
||||
"82c91e": "لیمویی 6",
|
||||
"fab005": "زرد 6",
|
||||
"fd7e14": "نارنجی 6",
|
||||
"000000": "سیاه",
|
||||
"343a40": "خاکستری 8",
|
||||
"495057": "خاکستری 7",
|
||||
"c92a2a": "قرمز 9",
|
||||
"a61e4d": "صورتی 9",
|
||||
"862e9c": "انگوری 9",
|
||||
"5f3dc4": "بنفش 9",
|
||||
"364fc7": "نیلی 9",
|
||||
"1864ab": "آبی 9",
|
||||
"0b7285": "آبی نفتی 9",
|
||||
"087f5b": "آبی فیروزه ای 9",
|
||||
"2b8a3e": "سبز 9",
|
||||
"5c940d": "لیمویی 9",
|
||||
"e67700": "زرد 9",
|
||||
"d9480f": "نارنجی 9"
|
||||
}
|
||||
}
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "Julkaistu"
|
||||
"statusPublished": "Julkaistu",
|
||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Kirjastossa ei ole vielä yhtään kohdetta...",
|
||||
"hint_emptyLibrary": "Valitse lisättävä kohde piirtoalueelta, tai asenna alta julkinen kirjasto.",
|
||||
"hint_emptyPrivateLibrary": "Valitse lisättävä kohde piirtoalueelta."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Tyhjennä piirtoalue",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
||||
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
||||
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
|
||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
||||
|
@@ -105,22 +105,28 @@
|
||||
"toggleTheme": "Changer le thème",
|
||||
"personalLib": "Bibliothèque personnelle",
|
||||
"excalidrawLib": "Bibliothèque Excalidraw",
|
||||
"decreaseFontSize": "Réduire la taille de police",
|
||||
"decreaseFontSize": "Diminuer la taille de police",
|
||||
"increaseFontSize": "Augmenter la taille de police",
|
||||
"unbindText": "Délier le texte",
|
||||
"bindText": "Lier le texte au conteneur",
|
||||
"unbindText": "Dissocier le texte",
|
||||
"bindText": "Associer le texte au conteneur",
|
||||
"link": {
|
||||
"edit": "Modifier le lien",
|
||||
"create": "Créer un lien",
|
||||
"create": "Ajouter un lien",
|
||||
"label": "Lien"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Verrouiller",
|
||||
"unlock": "Déverrouiller",
|
||||
"lockAll": "Tout verrouiller",
|
||||
"unlockAll": "Tout déverouiller"
|
||||
"unlockAll": "Tout déverrouiller"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publié",
|
||||
"sidebarLock": "Maintenir la barre latérale ouverte"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aucun élément n'a encore été ajouté ...",
|
||||
"hint_emptyLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici ou installez une bibliothèque depuis le dépôt public, ci-dessous.",
|
||||
"hint_emptyPrivateLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Réinitialiser le canevas",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
||||
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
||||
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
|
||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
|
||||
"browserZoom": "Le niveau de zoom de votre navigateur n'est pas défini sur 100 %, ce qui peut entraîner un affichage incorrect du tableau"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Type de fichier non supporté.",
|
||||
@@ -343,7 +350,7 @@
|
||||
},
|
||||
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
|
||||
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Bibliothèque soumise",
|
||||
|
440
src/locales/gl-ES.json
Normal file
440
src/locales/gl-ES.json
Normal file
@@ -0,0 +1,440 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Paste",
|
||||
"pasteCharts": "Pegar gráficos",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"multiSelect": "Engadir elemento á selección",
|
||||
"moveCanvas": "Mover o lenzo",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"copyAsPng": "Copiar no portapapeis como PNG",
|
||||
"copyAsSvg": "Copiar no portapapeis como SVG",
|
||||
"copyText": "Copia no portapapeis como texto",
|
||||
"bringForward": "Traer cara adiante",
|
||||
"sendToBack": "Enviar cara atrás",
|
||||
"bringToFront": "Traer á fronte",
|
||||
"sendBackward": "Enviar ao fondo",
|
||||
"delete": "Borrar",
|
||||
"copyStyles": "Copiar estilo",
|
||||
"pasteStyles": "Pegar estilo",
|
||||
"stroke": "Trazo",
|
||||
"background": "Fondo",
|
||||
"fill": "Recheo",
|
||||
"strokeWidth": "Largo do trazo",
|
||||
"strokeStyle": "Estilo do trazo",
|
||||
"strokeStyle_solid": "Sólido",
|
||||
"strokeStyle_dashed": "Liña de trazos",
|
||||
"strokeStyle_dotted": "Liña de puntos",
|
||||
"sloppiness": "Estilo de trazo",
|
||||
"opacity": "Opacidade",
|
||||
"textAlign": "Aliñar texto",
|
||||
"edges": "Bordos",
|
||||
"sharp": "Agudo",
|
||||
"round": "Redondo",
|
||||
"arrowheads": "Puntas de frecha",
|
||||
"arrowhead_none": "Ningunha",
|
||||
"arrowhead_arrow": "Frecha",
|
||||
"arrowhead_bar": "Barra",
|
||||
"arrowhead_dot": "Punto",
|
||||
"arrowhead_triangle": "Triángulo",
|
||||
"fontSize": "Tamaño da fonte",
|
||||
"fontFamily": "Tipo de fonte",
|
||||
"onlySelected": "Só seleccionados",
|
||||
"withBackground": "Fondo",
|
||||
"exportEmbedScene": "Inserir escena",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "Engadir \"Feito con Excalidraw\"",
|
||||
"handDrawn": "Debuzado á man",
|
||||
"normal": "Normal",
|
||||
"code": "Código",
|
||||
"small": "Pequeno",
|
||||
"medium": "Mediano",
|
||||
"large": "Grande",
|
||||
"veryLarge": "Moi grande",
|
||||
"solid": "Sólido",
|
||||
"hachure": "Folleto",
|
||||
"crossHatch": "Raiado transversal",
|
||||
"thin": "Estreito",
|
||||
"bold": "Groso",
|
||||
"left": "Esquerda",
|
||||
"center": "Centrado",
|
||||
"right": "Dereita",
|
||||
"extraBold": "Moi groso",
|
||||
"architect": "Arquitecto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Caricatura",
|
||||
"fileTitle": "Nome do arquivo",
|
||||
"colorPicker": "Selector de cor",
|
||||
"canvasColors": "Usado en lenzo",
|
||||
"canvasBackground": "Fondo do lenzo",
|
||||
"drawingCanvas": "Lenzo de debuxo",
|
||||
"layers": "Capas",
|
||||
"actions": "Accións",
|
||||
"language": "Idioma",
|
||||
"liveCollaboration": "Colaboración en directo",
|
||||
"duplicateSelection": "Duplicar",
|
||||
"untitled": "Sen título",
|
||||
"name": "Nome",
|
||||
"yourName": "O teu nome",
|
||||
"madeWithExcalidraw": "Feito con Excalidraw",
|
||||
"group": "Agrupar selección",
|
||||
"ungroup": "Desagrupar selección",
|
||||
"collaborators": "Colaboradores",
|
||||
"showGrid": "Mostrar cuadrícula",
|
||||
"addToLibrary": "Engadir á biblioteca",
|
||||
"removeFromLibrary": "Eliminar da biblioteca",
|
||||
"libraryLoadingMessage": "Cargando biblioteca…",
|
||||
"libraries": "Explorar bibliotecas",
|
||||
"loadingScene": "Cargando escena…",
|
||||
"align": "Aliñamento",
|
||||
"alignTop": "Aliñamento superior",
|
||||
"alignBottom": "Aliñamento inferior",
|
||||
"alignLeft": "Aliñar a esquerda",
|
||||
"alignRight": "Aliñar a dereita",
|
||||
"centerVertically": "Centrar verticalmente",
|
||||
"centerHorizontally": "Centrar horizontalmente",
|
||||
"distributeHorizontally": "Distribuír horizontalmente",
|
||||
"distributeVertically": "Distribuír verticalmente",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Modo de visualización",
|
||||
"toggleExportColorScheme": "Alternar esquema de cores de exportación",
|
||||
"share": "Compartir",
|
||||
"showStroke": "Mostrar selector de cores do trazo",
|
||||
"showBackground": "Mostrar selector de cores do fondo",
|
||||
"toggleTheme": "Alternar tema",
|
||||
"personalLib": "Biblioteca Persoal",
|
||||
"excalidrawLib": "Biblioteca Excalidraw",
|
||||
"decreaseFontSize": "Diminuír tamaño da fonte",
|
||||
"increaseFontSize": "Aumentar o tamaño da fonte",
|
||||
"unbindText": "Desvincular texto",
|
||||
"bindText": "Ligar o texto ao contedor",
|
||||
"link": {
|
||||
"edit": "Editar ligazón",
|
||||
"create": "Crear ligazón",
|
||||
"label": "Ligazón"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Bloquear",
|
||||
"unlock": "Desbloquear",
|
||||
"lockAll": "Bloquear todo",
|
||||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Manter a barra lateral aberta"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aínda non hai elementos engadidos...",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Limpar o lenzo",
|
||||
"exportJSON": "Exportar a arquivo",
|
||||
"exportImage": "Gardar como imaxe",
|
||||
"export": "Exportar",
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyToClipboard": "Copiar ao portapapeis",
|
||||
"copyPngToClipboard": "Copiar PNG ao portapapeis",
|
||||
"scale": "Escala",
|
||||
"save": "Gardar no ficheiro actual",
|
||||
"saveAs": "Gardar como",
|
||||
"load": "Cargar",
|
||||
"getShareableLink": "Obter unha ligazón que se poida compartir",
|
||||
"close": "Pechar",
|
||||
"selectLanguage": "Seleccionar idioma",
|
||||
"scrollBackToContent": "Volver ao contido",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reducir",
|
||||
"resetZoom": "Reiniciar zoom",
|
||||
"menu": "Menú",
|
||||
"done": "Feito",
|
||||
"edit": "Editar",
|
||||
"undo": "Desfacer",
|
||||
"redo": "Refacer",
|
||||
"resetLibrary": "Reiniciar biblioteca",
|
||||
"createNewRoom": "Crear nova sala",
|
||||
"fullScreen": "Pantalla completa",
|
||||
"darkMode": "Modo escuro",
|
||||
"lightMode": "Modo claro",
|
||||
"zenMode": "Modo zen",
|
||||
"exitZenMode": "Saír do modo zen",
|
||||
"cancel": "Cancelar",
|
||||
"clear": "Limpar",
|
||||
"remove": "Eliminar",
|
||||
"publishLibrary": "Publicar",
|
||||
"submit": "Enviar",
|
||||
"confirm": "Confirmar"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Isto limpará todo o lenzo. Estás seguro?",
|
||||
"couldNotCreateShareableLink": "Non se puido crear unha ligazón para compartir.",
|
||||
"couldNotCreateShareableLinkTooBig": "Non se puido crear a ligazón para compartir: a escena é demasiado grande",
|
||||
"couldNotLoadInvalidFile": "Non se puido cargar o ficheiro non válido",
|
||||
"importBackendFailed": "",
|
||||
"cannotExportEmptyCanvas": "",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"decryptFailed": "Non se poideron descifrar os datos.",
|
||||
"uploadedSecurly": "A carga foi asegurada con cifrado de extremo a extremo, o que significa que o servidor de Excalidraw e terceiros non poder ler o contenido.",
|
||||
"loadSceneOverridePrompt": "Si carga este debuxo externo, reemprazará o que ten. ¿Desexa continuar?",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorAddingToLibrary": "",
|
||||
"errorRemovingFromLibrary": "",
|
||||
"confirmAddLibrary": "",
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selección",
|
||||
"image": "",
|
||||
"rectangle": "Rectángulo",
|
||||
"diamond": "Diamante",
|
||||
"ellipse": "Elipse",
|
||||
"arrow": "Frecha",
|
||||
"line": "Liña",
|
||||
"freedraw": "Debuxar",
|
||||
"text": "Texto",
|
||||
"library": "Biblioteca",
|
||||
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": ""
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accións do lenzo",
|
||||
"selectedShapeActions": "",
|
||||
"shapes": ""
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"linearElement": "",
|
||||
"freeDraw": "",
|
||||
"text": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "",
|
||||
"lockAngle": "",
|
||||
"resize": "",
|
||||
"resizeImage": "",
|
||||
"rotate": "",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"publishLibrary": "",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "",
|
||||
"canvasTooBig": "",
|
||||
"canvasTooBigTip": ""
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain_pre": "",
|
||||
"headingMain_button": "",
|
||||
"clearCanvasMessage": "",
|
||||
"clearCanvasMessage_button": "",
|
||||
"clearCanvasCaveat": "",
|
||||
"trackedToSentry_pre": "",
|
||||
"trackedToSentry_post": "",
|
||||
"openIssueMessage_pre": "",
|
||||
"openIssueMessage_button": "",
|
||||
"openIssueMessage_post": "",
|
||||
"sceneContent": ""
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "",
|
||||
"desc_privacy": "",
|
||||
"button_startSession": "",
|
||||
"button_stopSession": "",
|
||||
"desc_inProgressIntro": "",
|
||||
"desc_shareLink": "",
|
||||
"desc_exitSession": "",
|
||||
"shareTitle": ""
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Erro"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "",
|
||||
"disk_details": "",
|
||||
"disk_button": "",
|
||||
"link_title": "",
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"excalidrawplus_description": "",
|
||||
"excalidrawplus_button": "",
|
||||
"excalidrawplus_exportError": ""
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"doubleClick": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"editSelectedShape": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"tools": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": "",
|
||||
"toggleElementLock": ""
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": ""
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "",
|
||||
"itemName": "",
|
||||
"authorName": "",
|
||||
"githubUsername": "",
|
||||
"twitterUsername": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"website": "",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
},
|
||||
"errors": {
|
||||
"required": "",
|
||||
"website": ""
|
||||
},
|
||||
"noteDescription": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
},
|
||||
"noteGuidelines": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
},
|
||||
"noteLicense": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
},
|
||||
"noteItems": "",
|
||||
"atleastOneLibItem": "",
|
||||
"republishWarning": ""
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"link": ""
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "",
|
||||
"link": ""
|
||||
},
|
||||
"stats": {
|
||||
"angle": "",
|
||||
"element": "",
|
||||
"elements": "",
|
||||
"height": "",
|
||||
"scene": "",
|
||||
"selected": "",
|
||||
"storage": "",
|
||||
"title": "",
|
||||
"total": "",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": ""
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": ""
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "",
|
||||
"f8f9fa": "",
|
||||
"f1f3f5": "",
|
||||
"fff5f5": "",
|
||||
"fff0f6": "",
|
||||
"f8f0fc": "",
|
||||
"f3f0ff": "",
|
||||
"edf2ff": "",
|
||||
"e7f5ff": "",
|
||||
"e3fafc": "",
|
||||
"e6fcf5": "",
|
||||
"ebfbee": "",
|
||||
"f4fce3": "",
|
||||
"fff9db": "",
|
||||
"fff4e6": "",
|
||||
"transparent": "",
|
||||
"ced4da": "",
|
||||
"868e96": "",
|
||||
"fa5252": "",
|
||||
"e64980": "",
|
||||
"be4bdb": "",
|
||||
"7950f2": "",
|
||||
"4c6ef5": "",
|
||||
"228be6": "",
|
||||
"15aabf": "",
|
||||
"12b886": "",
|
||||
"40c057": "",
|
||||
"82c91e": "",
|
||||
"fab005": "",
|
||||
"fd7e14": "",
|
||||
"000000": "",
|
||||
"343a40": "",
|
||||
"495057": "",
|
||||
"c92a2a": "",
|
||||
"a61e4d": "",
|
||||
"862e9c": "",
|
||||
"5f3dc4": "",
|
||||
"364fc7": "",
|
||||
"1864ab": "",
|
||||
"0b7285": "",
|
||||
"087f5b": "",
|
||||
"2b8a3e": "",
|
||||
"5c940d": "",
|
||||
"e67700": "",
|
||||
"d9480f": ""
|
||||
}
|
||||
}
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "לנעול הכל",
|
||||
"unlockAll": "שחרור הכול"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "אפס את הלוח",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
||||
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
||||
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
|
||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
|
||||
"browserZoom": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "सब ताले के अंदर रखे",
|
||||
"unlockAll": "सब ताले के बाहर निकाले"
|
||||
},
|
||||
"statusPublished": "प्रकाशित"
|
||||
"statusPublished": "प्रकाशित",
|
||||
"sidebarLock": "साइडबार खुला रखे."
|
||||
},
|
||||
"library": {
|
||||
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
||||
"hint_emptyLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.",
|
||||
"hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "कैनवास रीसेट करें",
|
||||
@@ -181,7 +187,8 @@
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidEncryptionKey": "",
|
||||
"browserZoom": "आपके ब्राउज़र का ज़ूम लेवल 100% नहीं हैं इस कारण दृष्य पटल ग़लत दिख सकता हैं"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user