Compare commits

..

5 Commits

Author SHA1 Message Date
pomdtr 8984d8f19a don't clean export options on load 2022-06-29 10:23:48 +00:00
Achille Lacoin b80706cd4a Update appState.ts 2022-06-28 17:53:01 +02:00
pomdtr cf34cbdd30 fix export.test.tsx 2022-06-28 08:16:14 +00:00
pomdtr 6ead3ff839 fix export snapshot 2022-06-28 08:12:38 +00:00
pomdtr d7f0d4ee21 [feat] serialize export options when embedding scene in an image 2022-06-27 20:31:05 +00:00
157 changed files with 5318 additions and 13326 deletions
-9
View File
@@ -11,12 +11,3 @@ 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
View File
@@ -0,0 +1,37 @@
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
+1 -1
View File
@@ -1,4 +1,4 @@
name: Auto release excalidraw next
name: Auto release @excalidraw/excalidraw-next
on:
push:
branches:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Auto release excalidraw preview
name: Auto release preview @excalidraw/excalidraw-preview
on:
issue_comment:
types: [created, edited]
+5 -10
View File
@@ -10,16 +10,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v2
- uses: actions/checkout@v2
- uses: docker/build-push-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: excalidraw/excalidraw:latest
repository: excalidraw/excalidraw
tag_with_ref: true
tag_with_sha: true
+1
View File
@@ -19,6 +19,7 @@ logs
node_modules
npm-debug.log*
package-lock.json
static
yarn-debug.log*
yarn-error.log*
src/packages/excalidraw/types
-20
View File
@@ -1,20 +0,0 @@
# 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
View File
@@ -1,41 +0,0 @@
# 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
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};
-6
View File
@@ -1,6 +0,0 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).
-8
View File
@@ -1,8 +0,0 @@
---
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
View File
@@ -1,6 +0,0 @@
---
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
View File
@@ -1,121 +0,0 @@
// @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
View File
@@ -1,46 +0,0 @@
{
"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
View File
@@ -1,31 +0,0 @@
/**
* 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
View File
@@ -1,62 +0,0 @@
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>
);
}
@@ -1,70 +0,0 @@
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&apos;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>
);
}
@@ -1,11 +0,0 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
-43
View File
@@ -1,43 +0,0 @@
/**
* 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
View File
@@ -1,42 +0,0 @@
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
View File
@@ -1,27 +0,0 @@
/**
* 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
View File
@@ -1,42 +0,0 @@
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
View File
@@ -1,7 +0,0 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

-4
View File
@@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

-7
View File
@@ -1,7 +0,0 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
}
}
-7489
View File
File diff suppressed because it is too large Load Diff
+12 -15
View File
@@ -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.7.1",
"@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/react": "17.0.39",
"@types/react-dom": "17.0.11",
"@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.4",
"i18next-browser-languagedetector": "6.1.2",
"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": "18.2.0",
"react-dom": "18.2.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"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.7",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.7",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.6",
"dotenv": "16.0.1",
"dotenv": "10.0.0",
"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": "6.0.0"
"rewire": "5.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.10.2"
@@ -94,8 +94,7 @@
"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:prebuild": "node ./scripts/prebuild.js",
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
"build": "yarn build:app && yarn build:version",
"eject": "react-scripts eject",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
@@ -113,8 +112,6 @@
"test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"release": "node scripts/release.js"
"autorelease": "node scripts/autorelease.js"
}
}
-16
View File
@@ -98,22 +98,6 @@
/>
<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.
+11 -6
View File
@@ -5,25 +5,22 @@ 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 --tag ${tag}`);
console.info(`Published ${pkg.name}@${tag}🎉`);
execSync(`yarn --cwd ${excalidrawDir} publish`);
console.info("Published 🎉");
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
@@ -54,19 +51,27 @@ 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
View File
@@ -1,21 +0,0 @@
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);
});
-10
View File
@@ -36,7 +36,6 @@ 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",
@@ -48,8 +47,6 @@ const crowdinMap = {
"lv-LV": "en-lv",
"cs-CZ": "en-cs",
"kk-KZ": "en-kk",
"vi-vn": "en-vi",
"mr-in": "en-mr",
};
const flags = {
@@ -89,7 +86,6 @@ const flags = {
"ru-RU": "🇷🇺",
"si-LK": "🇱🇰",
"sk-SK": "🇸🇰",
"sl-SI": "🇸🇮",
"sv-SE": "🇸🇪",
"ta-IN": "🇮🇳",
"tr-TR": "🇹🇷",
@@ -97,9 +93,6 @@ const flags = {
"zh-CN": "🇨🇳",
"zh-HK": "🇭🇰",
"zh-TW": "🇹🇼",
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
};
const languages = {
@@ -140,7 +133,6 @@ 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",
@@ -148,8 +140,6 @@ const languages = {
"zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
};
const percentages = fs.readFileSync(
-20
View File
@@ -1,20 +0,0 @@
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
View File
@@ -1,37 +0,0 @@
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);
+25 -30
View File
@@ -1,44 +1,39 @@
const fs = require("fs");
const { execSync } = require("child_process");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateReadme = require("./updateReadme");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
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 updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const publish = () => {
const release = async (nextVersion) => {
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
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!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
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();
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
release(nextVersion);
+27
View File
@@ -0,0 +1,27 @@
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;
+1 -1
View File
@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
commitToHistory: false,
appState: {
...appState,
toast: { message: t("toast.addedToLibrary") },
toastMessage: t("toast.addedToLibrary"),
},
};
})
+8 -10
View File
@@ -107,16 +107,14 @@ export const actionCopyAsPng = register({
return {
appState: {
...appState,
toast: {
message: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
toastMessage: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
commitToHistory: false,
};
+6 -9
View File
@@ -128,15 +128,12 @@ const duplicateElements = (
{
...appState,
selectedGroupIds: {},
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
selectedElementIds: newElements.reduce((acc, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
}, {} as any),
},
getNonDeletedElements(finalElements),
),
+7 -9
View File
@@ -144,15 +144,13 @@ export const actionSaveToActiveFile = register({
appState: {
...appState,
fileHandle,
toast: fileHandleExists
? {
message: fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved"),
}
toastMessage: fileHandleExists
? fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved")
: null,
},
};
+10 -14
View File
@@ -2,7 +2,6 @@ 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",
@@ -16,19 +15,16 @@ export const actionSelectAll = register({
{
...appState,
editingGroupId: null,
selectedElementIds: elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
),
selectedElementIds: elements.reduce((map, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
element.locked === false
) {
map[element.id] = true;
}
return map;
}, {} as any),
},
getNonDeletedElements(elements),
),
+1 -1
View File
@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
return {
appState: {
...appState,
toast: { message: t("toast.copyStyles") },
toastMessage: t("toast.copyStyles"),
},
commitToHistory: false,
};
+217 -75
View File
@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
showStats: false,
startBoundElement: null,
suggestedBindings: [],
toast: null,
toastMessage: null,
viewBackgroundColor: oc.white,
zenModeEnabled: false,
zoom: {
@@ -101,90 +101,228 @@ 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 file/database */
export: boolean;
/** whether to keep when exporting to a text file */
text: boolean;
/** whether to keep when exporting to an image file */
image: 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, 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: {
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,
export: false,
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,
},
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,
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 "export" | "browser" | "server",
ExportType extends "image" | "text" | "browser" | "server",
>(
appState: Partial<AppState>,
exportType: ExportType,
@@ -211,8 +349,12 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser");
};
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export");
export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "text");
};
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "image");
};
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
+73 -128
View File
@@ -166,7 +166,7 @@ import {
isAndroid,
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene";
import { renderScene } from "../renderer";
import { invalidateShapeForElement } from "../renderer/renderElement";
import {
calculateScrollCenter,
@@ -286,10 +286,6 @@ 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(),
@@ -384,7 +380,7 @@ class App extends React.Component<AppProps, AppState> {
getAppState: () => this.state,
getFiles: () => this.files,
refresh: this.refresh,
setToast: this.setToast,
setToastMessage: this.setToastMessage,
id: this.id,
setActiveTool: this.setActiveTool,
setCursor: this.setCursor,
@@ -473,6 +469,7 @@ class App extends React.Component<AppProps, AppState> {
}
public render() {
const { zenModeEnabled, viewModeEnabled } = this.state;
const selectedElement = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
@@ -487,7 +484,7 @@ class App extends React.Component<AppProps, AppState> {
return (
<div
className={clsx("excalidraw excalidraw-container", {
"excalidraw--view-mode": this.state.viewModeEnabled,
"excalidraw--view-mode": viewModeEnabled,
"excalidraw--mobile": this.device.isMobile,
})}
ref={this.excalidrawContainerRef}
@@ -518,14 +515,17 @@ 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" &&
this.state.zenModeEnabled
zenModeEnabled
}
showThemeBtn={
typeof this.props?.theme === "undefined" &&
@@ -549,12 +549,10 @@ class App extends React.Component<AppProps, AppState> {
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.toast !== null && (
{this.state.toastMessage !== null && (
<Toast
message={this.state.toast.message}
onClose={() => this.setToast(null)}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
message={this.state.toastMessage}
clearToast={this.clearToast}
/>
)}
<main>{this.renderCanvas()}</main>
@@ -768,7 +766,6 @@ 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 = {
@@ -862,7 +859,6 @@ 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!);
@@ -908,7 +904,6 @@ class App extends React.Component<AppProps, AppState> {
} else {
this.updateDOMRect(this.initializeScene);
}
this.checkIfBrowserZoomed();
}
public componentWillUnmount() {
@@ -921,25 +916,8 @@ 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));
@@ -951,10 +929,6 @@ 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,
@@ -1003,12 +977,6 @@ 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);
}
@@ -1225,8 +1193,7 @@ class App extends React.Component<AppProps, AppState> {
element.id !== this.state.editingElement.id
);
});
renderScene(
const { atLeastOneVisibleElement, scrollBars } = renderScene(
renderingElements,
this.state,
this.state.selectionElement,
@@ -1249,30 +1216,24 @@ 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 (!THROTTLE_NEXT_RENDER) {
THROTTLE_NEXT_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 });
}
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
@@ -1540,15 +1501,12 @@ class App extends React.Component<AppProps, AppState> {
this.state.isLibraryOpen && this.device.canDeviceFitSidebar
? this.state.isLibraryMenuDocked
: false,
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
selectedElementIds: newElements.reduce((map, element) => {
if (!isBoundToContainer(element)) {
map[element.id] = true;
}
return map;
}, {} as any),
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
@@ -1641,6 +1599,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
toggleZenMode = () => {
this.actionManager.executeAction(actionToggleZenMode);
};
scrollToContent = (
target:
| ExcalidrawElement
@@ -1655,14 +1617,12 @@ class App extends React.Component<AppProps, AppState> {
});
};
setToast = (
toast: {
message: string;
closable?: boolean;
duration?: number;
} | null,
) => {
this.setState({ toast });
clearToast = () => {
this.setState({ toastMessage: null });
};
setToastMessage = (toastMessage: string) => {
this.setState({ toastMessage });
};
restoreFileFromShare = async () => {
@@ -1748,7 +1708,6 @@ 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)) ||
@@ -1772,14 +1731,6 @@ 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
@@ -1872,32 +1823,35 @@ class App extends React.Component<AppProps, AppState> {
this.state,
);
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,
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,
),
});
event.preventDefault();
return;
}
} else if (
selectedElements.length === 1 &&
!isLinearElement(selectedElements[0])
) {
const selectedElement = selectedElements[0];
this.startTextEditing({
sceneX: selectedElement.x + selectedElement.width / 2,
sceneY: selectedElement.y + selectedElement.height / 2,
shouldBind: true,
});
event.preventDefault();
return;
}
} else if (
!event.ctrlKey &&
@@ -1959,13 +1913,6 @@ 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) {
@@ -4307,13 +4254,10 @@ class App extends React.Component<AppProps, AppState> {
...prevState,
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
acc[element.id] = true;
return acc;
},
{},
),
...elementsWithinSelection.reduce((map, element) => {
map[element.id] = true;
return map;
}, {} as any),
...(pointerDownState.hit.element
? {
// if using ctrl/cmd, select the hitElement only if we
@@ -5780,6 +5724,7 @@ class App extends React.Component<AppProps, AppState> {
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
event.preventDefault();
if (isPanning) {
return;
}
-1
View File
@@ -4,7 +4,6 @@ import "./Card.scss";
export const Card: React.FC<{
color: keyof OpenColor | "primary";
children?: React.ReactNode;
}> = ({ children, color }) => {
return (
<div
-1
View File
@@ -8,7 +8,6 @@ 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
+2 -4
View File
@@ -18,15 +18,13 @@
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.6em;
font-family: "Cascadia";
font-size: 0.7em;
font-family: var(--ui-font);
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ const CollabButton = ({
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDevice().isMobile}
>
{isCollaborating && (
{collaboratorCount > 0 && (
<div className="CollabButton-collaborators">{collaboratorCount}</div>
)}
</ToolButton>
+1 -4
View File
@@ -58,7 +58,6 @@ const ExportButton: React.FC<{
onClick: () => void;
title: string;
shade?: number;
children?: React.ReactNode;
}> = ({ children, title, onClick, color, shade = 6 }) => {
return (
<button
@@ -171,9 +170,7 @@ const ImageExportModal = ({
<Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")}
</Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>
{t("buttons.scale")}
</p>
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
</div>
<div
style={{
+28 -25
View File
@@ -39,7 +39,6 @@ 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;
@@ -52,13 +51,16 @@ 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;
@@ -77,12 +79,15 @@ const LayerUI = ({
onLockToggle,
onPenModeToggle,
onInsertElements,
zenModeEnabled,
showExitZenModeBtn,
showThemeBtn,
toggleZenMode,
isCollaborating,
renderTopRightUI,
renderCustomFooter,
renderCustomStats,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
focusContainer,
@@ -166,7 +171,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -187,7 +192,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -227,7 +232,7 @@ const LayerUI = ({
<Section
heading="selectedShapeActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
<Island
@@ -297,34 +302,32 @@ const LayerUI = ({
<div className="App-menu App-menu_top">
<Stack.Col
gap={4}
className={clsx({
"disable-pointerEvents": appState.zenModeEnabled,
})}
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
>
{appState.viewModeEnabled
{viewModeEnabled
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col>
{!appState.viewModeEnabled && (
{!viewModeEnabled && (
<Section heading="shapes">
{(heading: React.ReactNode) => (
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": appState.zenModeEnabled,
"zen-mode": zenModeEnabled,
})}
>
<PenModeButton
zenModeEnabled={appState.zenModeEnabled}
zenModeEnabled={zenModeEnabled}
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
<LockButton
zenModeEnabled={appState.zenModeEnabled}
zenModeEnabled={zenModeEnabled}
checked={appState.activeTool.locked}
onChange={() => onLockToggle()}
title={t("toolBar.lock")}
@@ -332,7 +335,7 @@ const LayerUI = ({
<Island
padding={1}
className={clsx("App-toolbar", {
"zen-mode": appState.zenModeEnabled,
"zen-mode": zenModeEnabled,
})}
>
<HintViewer
@@ -368,7 +371,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__top-right zen-mode-transition",
{
"transition-right": appState.zenModeEnabled,
"transition-right": zenModeEnabled,
},
)}
>
@@ -393,8 +396,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
},
)}
>
@@ -406,12 +408,12 @@ const LayerUI = ({
zoom={appState.zoom}
/>
</Island>
{!appState.viewModeEnabled && (
{!viewModeEnabled && (
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
@@ -421,20 +423,20 @@ const LayerUI = ({
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{!appState.viewModeEnabled &&
{!viewModeEnabled &&
appState.multiElement &&
device.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
@@ -448,7 +450,7 @@ const LayerUI = ({
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
zenModeEnabled,
},
)}
>
@@ -458,7 +460,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": appState.zenModeEnabled,
"transition-right disable-pointerEvents": zenModeEnabled,
},
)}
>
@@ -468,7 +470,7 @@ const LayerUI = ({
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={() => actionManager.executeAction(actionToggleZenMode)}
onClick={toggleZenMode}
>
{t("buttons.exitZenMode")}
</button>
@@ -541,6 +543,7 @@ const LayerUI = ({
canvas={canvas}
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
+1 -1
View File
@@ -224,7 +224,7 @@ export const LibraryMenu = ({
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback(
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
(data, libraryItems: LibraryItems) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();
+19 -19
View File
@@ -36,6 +36,7 @@ type MobileMenuProps = {
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
viewModeEnabled: boolean;
showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
@@ -59,6 +60,7 @@ export const MobileMenu = ({
canvas,
isCollaborating,
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
onImageAction,
renderTopRightUI,
@@ -68,7 +70,7 @@ export const MobileMenu = ({
return (
<FixedSideContainer side="top" className="App-top-bar">
<Section heading="shapes">
{(heading: React.ReactNode) => (
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1} className="App-toolbar-container">
<Island padding={1} className="App-toolbar">
@@ -123,7 +125,7 @@ export const MobileMenu = ({
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (appState.viewModeEnabled) {
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
@@ -149,7 +151,7 @@ export const MobileMenu = ({
};
const renderCanvasActions = () => {
if (appState.viewModeEnabled) {
if (viewModeEnabled) {
return (
<>
{renderJSONExportDialog()}
@@ -183,7 +185,7 @@ export const MobileMenu = ({
};
return (
<>
{!appState.viewModeEnabled && renderToolbar()}
{!viewModeEnabled && renderToolbar()}
{renderStats()}
<div
className="App-bottom-bar"
@@ -214,7 +216,7 @@ export const MobileMenu = ({
</div>
</Section>
) : appState.openMenu === "shape" &&
!appState.viewModeEnabled &&
!viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
@@ -227,20 +229,18 @@ export const MobileMenu = ({
) : null}
<footer className="App-toolbar">
{renderAppToolbar()}
{appState.scrolledOutside &&
!appState.openMenu &&
!appState.isLibraryOpen && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
{appState.scrolledOutside && !appState.openMenu && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>
+2 -2
View File
@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
import { AppState } from "../types";
import { THEME } from "../constants";
export const Modal: React.FC<{
export const Modal = (props: {
className?: string;
children: React.ReactNode;
maxWidth?: number;
@@ -16,7 +16,7 @@ export const Modal: React.FC<{
labelledBy: string;
theme?: AppState["theme"];
closeOnClickOutside?: boolean;
}> = (props) => {
}) => {
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
const modalRoot = useBodyRoot(theme);
-1
View File
@@ -2,6 +2,5 @@
.popover {
position: absolute;
z-index: 10;
padding: 5px 0 5px;
}
}
+1 -16
View File
@@ -69,27 +69,12 @@ 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 - 10}px`;
element.style.left = `${viewportWidth - width}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]);
+5 -4
View File
@@ -2,11 +2,12 @@ import React from "react";
import { t } from "../i18n";
import { useExcalidrawContainer } from "./App";
export const Section: React.FC<{
interface SectionProps extends React.HTMLProps<HTMLElement> {
heading: string;
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
className?: string;
}> = ({ heading, children, ...props }) => {
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
}
export const Section = ({ heading, children, ...props }: SectionProps) => {
const { id } = useExcalidrawContainer();
const header = (
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
+4 -20
View File
@@ -2,9 +2,6 @@
.excalidraw {
.Toast {
$closeButtonSize: 1.2rem;
$closeButtonPadding: 0.4rem;
animation: fade-in 0.5s;
background-color: var(--button-gray-1);
border-radius: 4px;
@@ -18,24 +15,11 @@
text-align: center;
width: 300px;
z-index: 999999;
}
.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;
}
}
.Toast__message {
color: var(--popup-text-color);
white-space: pre-wrap;
}
@keyframes fade-in {
+12 -37
View File
@@ -1,59 +1,34 @@
import { useCallback, useEffect, useRef } from "react";
import { close } from "./icons";
import { TOAST_TIMEOUT } from "../constants";
import "./Toast.scss";
import { ToolButton } from "./ToolButton";
const DEFAULT_TOAST_TIMEOUT = 5000;
export const Toast = ({
message,
onClose,
closable = false,
// To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT,
clearToast,
}: {
message: string;
onClose: () => void;
closable?: boolean;
duration?: number;
clearToast: () => void;
}) => {
const timerRef = useRef<number>(0);
const shouldAutoClose = duration !== Infinity;
const scheduleTimeout = useCallback(() => {
if (!shouldAutoClose) {
return;
}
timerRef.current = window.setTimeout(() => onClose(), duration);
}, [onClose, duration, shouldAutoClose]);
const scheduleTimeout = useCallback(
() =>
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
[clearToast],
);
useEffect(() => {
if (!shouldAutoClose) {
return;
}
scheduleTimeout();
return () => clearTimeout(timerRef.current);
}, [scheduleTimeout, message, duration, shouldAutoClose]);
}, [scheduleTimeout, message]);
const onMouseEnter = shouldAutoClose
? () => clearTimeout(timerRef?.current)
: undefined;
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
return (
<div
className="Toast"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseEnter={() => clearTimeout(timerRef?.current)}
onMouseLeave={scheduleTimeout}
>
<p className="Toast__message">{message}</p>
{closable && (
<ToolButton
icon={close}
aria-label="close"
type="icon"
onClick={onClose}
className="close"
/>
)}
</div>
);
};
+5 -3
View File
@@ -212,14 +212,16 @@
}
}
.ToolIcon.ToolIcon__library {
top: calc(var(--sat) + 100px);
top: 100px;
}
.ToolIcon.ToolIcon__lock {
top: calc(var(--sat) + 60px);
margin-inline-end: 0;
top: 60px;
}
.ToolIcon.ToolIcon__penMode {
top: calc(var(--sat) + 140px);
margin-inline-end: 0;
top: 140px;
}
}
+1
View File
@@ -32,6 +32,7 @@
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
+1
View File
@@ -116,6 +116,7 @@ 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;
+42
View File
@@ -0,0 +1,42 @@
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,
};
};
+2 -2
View File
@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState";
import { cleanAppStateForImageExport } 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,
...cleanAppStateForExport(data.appState || {}),
...cleanAppStateForImageExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(
data.elements || [],
+7 -2
View File
@@ -1,4 +1,5 @@
import {
FileWithHandle,
fileOpen as _fileOpen,
fileSave as _fileSave,
FileSystemHandle,
@@ -25,9 +26,13 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description: string;
multiple?: M;
}): Promise<M extends false | undefined ? File : File[]> => {
}): Promise<
M extends false | undefined ? FileWithHandle : FileWithHandle[]
> => {
// an unsafe TS hack, alas not much we can do AFAIK
type RetType = M extends false | undefined ? File : File[];
type RetType = M extends false | undefined
? FileWithHandle
: FileWithHandle[];
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
mimeTypes.push(MIME_TYPES[type]);
+1 -1
View File
@@ -82,7 +82,7 @@ export const exportCanvas = async (
await import(/* webpackChunkName: "image" */ "./image")
).encodePngMetadata({
blob,
metadata: serializeAsJSON(elements, appState, files, "local"),
metadata: serializeAsJSON(elements, appState, files, "image"),
});
}
+26 -20
View File
@@ -1,5 +1,9 @@
import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import {
cleanAppStateForImageExport,
cleanAppStateForTextExport,
clearAppStateForDatabase,
} from "../appState";
import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
@@ -43,25 +47,32 @@ export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: Partial<AppState>,
files: BinaryFiles,
type: "local" | "database",
destination: "text" | "image" | "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:
type === "local"
? clearElementsForExport(elements)
: clearElementsForDatabase(elements),
appState:
type === "local"
? cleanAppStateForExport(appState)
: clearAppStateForDatabase(appState),
destination === "database"
? clearElementsForDatabase(elements)
: clearElementsForExport(elements),
appState: cleanAppState(),
files:
type === "local"
? filterOutDeletedFiles(elements, files)
: // will be stripped from JSON
undefined,
destination === "database"
? // will be stripped from JSON
undefined
: filterOutDeletedFiles(elements, files),
};
return JSON.stringify(data, null, 2);
@@ -72,7 +83,7 @@ export const saveAsJSON = async (
appState: AppState,
files: BinaryFiles,
) => {
const serialized = serializeAsJSON(elements, appState, files, "local");
const serialized = serializeAsJSON(elements, appState, files, "text");
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw,
});
@@ -98,12 +109,7 @@ 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,
file.handle,
);
return loadFromBlob(await normalizeFile(file), localAppState, localElements);
};
export const isValidExcalidrawData = (data?: {
+2 -2
View File
@@ -5,7 +5,7 @@ import {
LibraryItems,
LibraryItems_anyVersion,
} from "../types";
import type { cleanAppStateForExport } from "../appState";
import type { cleanAppStateForTextExport } 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 cleanAppStateForExport>;
appState: ReturnType<typeof cleanAppStateForTextExport>;
files: BinaryFiles | undefined;
}
+2 -2
View File
@@ -19,7 +19,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
}, STORAGE_SIZE_TIMEOUT);
type Props = {
setToast: (message: string) => void;
setToastMessage: (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.setToast(t("toast.copyToClipboard"));
props.setToastMessage(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}
@@ -8,12 +8,10 @@ import {
ExcalidrawElement,
InitializedExcalidrawImageElement,
} from "../../element/types";
import {
getSceneVersion,
restoreElements,
} from "../../packages/excalidraw/index";
import { getSceneVersion } from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../types";
import {
getFrame,
preventUnload,
resolvablePromise,
withBatchedUpdates,
@@ -49,9 +47,11 @@ 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,45 +70,52 @@ 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 Collab>;
type CollabInstance = InstanceType<typeof CollabWrapper>;
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"];
startCollaboration: CollabInstance["startCollaboration"];
stopCollaboration: CollabInstance["stopCollaboration"];
initializeSocketClient: CollabInstance["initializeSocketClient"];
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
syncElements: CollabInstance["syncElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void;
}
interface PublicProps {
interface Props {
excalidrawAPI: ExcalidrawImperativeAPI;
onRoomClose?: () => void;
}
type Props = PublicProps & { modalIsShown: boolean };
const {
Context: CollabContext,
Consumer: CollabContextConsumer,
Provider: CollabContextProvider,
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
class Collab extends PureComponent<Props, CollabState> {
export { CollabContext, CollabContextConsumer };
class CollabWrapper 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>();
@@ -116,8 +123,10 @@ class Collab 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);
@@ -155,18 +164,6 @@ class Collab 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
@@ -199,11 +196,7 @@ class Collab extends PureComponent<Props, CollabState> {
}
}
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
private setIsCollaborating = (isCollaborating: boolean) => {
jotaiStore.set(isCollaboratingAtom, isCollaborating);
};
isCollaborating = () => this._isCollaborating;
private onUnload = () => {
this.destroySocketClient({ isUnload: true });
@@ -215,7 +208,7 @@ class Collab extends PureComponent<Props, CollabState> {
);
if (
this.isCollaborating() &&
this._isCollaborating &&
(this.fileManager.shouldPreventUnload(syncableElements) ||
!isSavedToFirebase(this.portal, syncableElements))
) {
@@ -259,7 +252,12 @@ class Collab extends PureComponent<Props, CollabState> {
}
};
stopCollaboration = (keepRemoteState = true) => {
openPortal = async () => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
return this.initializeSocketClient(null);
};
closePortal = () => {
this.queueBroadcastAllElements.cancel();
this.queueSaveToFirebase.cancel();
this.loadImageFiles.cancel();
@@ -269,26 +267,16 @@ class Collab extends PureComponent<Props, CollabState> {
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
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"))) {
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");
LocalData.fileStorage.reset();
this.props.onRoomClose?.();
const elements = this.excalidrawAPI
.getSceneElementsIncludingDeleted()
@@ -307,20 +295,20 @@ class Collab 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: {
@@ -361,9 +349,7 @@ class Collab extends PureComponent<Props, CollabState> {
}
};
private fallbackInitializationHandler: null | (() => any) = null;
startCollaboration = async (
private initializeSocketClient = async (
existingRoomLinkData: null | { roomId: string; roomKey: string },
): Promise<ImportedDataState | null> => {
if (this.portal.socket) {
@@ -386,23 +372,13 @@ class Collab extends PureComponent<Props, CollabState> {
const scenePromise = resolvablePromise<ImportedDataState | null>();
this.setIsCollaborating(true);
this._isCollaborating = 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();
@@ -415,8 +391,6 @@ class Collab extends PureComponent<Props, CollabState> {
roomId,
roomKey,
);
this.portal.socket.once("connect_error", fallbackInitializationHandler);
} catch (error: any) {
console.error(error);
this.setState({ errorMessage: error.message });
@@ -445,10 +419,13 @@ class Collab 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(
fallbackInitializationHandler,
INITIAL_SCENE_UPDATE_TIMEOUT,
);
this.socketInitializationTimer = window.setTimeout(() => {
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
});
scenePromise.resolve(null);
}, INITIAL_SCENE_UPDATE_TIMEOUT);
// All socket listeners are moving to Portal
this.portal.socket.on(
@@ -553,12 +530,6 @@ class Collab 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();
@@ -596,8 +567,6 @@ class Collab extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null);
const reconciledElements = _reconcileElements(
localElements,
remoteElements,
@@ -703,17 +672,19 @@ class Collab extends PureComponent<Props, CollabState> {
};
setCollaborators(sockets: string[]) {
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.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, {});
}
}
}
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
});
}
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
@@ -742,6 +713,7 @@ class Collab extends PureComponent<Props, CollabState> {
);
onIdleStateChange = (userState: UserIdleState) => {
this.setState({ userState });
this.portal.broadcastIdleChange(userState);
};
@@ -775,22 +747,18 @@ class Collab 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,
{ leading: false },
);
queueSaveToFirebase = throttle(() => {
if (this.portal.socketInitialized) {
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
}
}, SYNC_FULL_SCENE_INTERVAL_MS);
handleClose = () => {
jotaiStore.set(collabDialogShownAtom, false);
this.setState({ modalIsShown: false });
};
setUsername = (username: string) => {
@@ -802,10 +770,35 @@ class Collab extends PureComponent<Props, CollabState> {
saveUsernameToLocalStorage(username);
};
render() {
const { username, errorMessage, activeRoomLink } = this.state;
onCollabButtonClick = () => {
this.setState({
modalIsShown: true,
});
};
const { modalIsShown } = this.props;
/** 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;
return (
<>
@@ -815,8 +808,8 @@ class Collab extends PureComponent<Props, CollabState> {
activeRoomLink={activeRoomLink}
username={username}
onUsernameChange={this.onUsernameChange}
onRoomCreate={() => this.startCollaboration(null)}
onRoomDestroy={this.stopCollaboration}
onRoomCreate={this.openPortal}
onRoomDestroy={this.closePortal}
setErrorMessage={(errorMessage) => {
this.setState({ errorMessage });
}}
@@ -829,6 +822,11 @@ class Collab extends PureComponent<Props, CollabState> {
onClose={() => this.setState({ errorMessage: "" })}
/>
)}
<CollabContextProvider
value={{
api: this.getContextValue(),
}}
/>
</>
);
}
@@ -836,7 +834,7 @@ class Collab extends PureComponent<Props, CollabState> {
declare global {
interface Window {
collab: InstanceType<typeof Collab>;
collab: InstanceType<typeof CollabWrapper>;
}
}
@@ -847,11 +845,4 @@ if (
window.collab = window.collab || ({} as Window["collab"]);
}
const _Collab: React.FC<PublicProps> = (props) => {
const [collabDialogShown] = useAtom(collabDialogShownAtom);
return <Collab {...props} modalIsShown={collabDialogShown} />;
};
export default _Collab;
export type TCollabClass = Collab;
export default CollabWrapper;
+3 -3
View File
@@ -4,7 +4,7 @@ import {
SocketUpdateDataSource,
} from "../data";
import { TCollabClass } from "./Collab";
import CollabWrapper from "./CollabWrapper";
import { ExcalidrawElement } from "../../element/types";
import {
@@ -20,14 +20,14 @@ import { BroadcastedExcalidrawElement } from "./reconciliation";
import { encryptData } from "../../data/encryption";
class Portal {
collab: TCollabClass;
collab: CollabWrapper;
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: TCollabClass) {
constructor(collab: CollabWrapper) {
this.collab = collab;
}
+2 -10
View File
@@ -14,8 +14,6 @@ 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;
@@ -97,10 +95,7 @@ const RoomDialog = ({
title={t("roomDialog.button_startSession")}
aria-label={t("roomDialog.button_startSession")}
showAriaLabel={true}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();
}}
onClick={onRoomCreate}
/>
</div>
</>
@@ -165,10 +160,7 @@ const RoomDialog = ({
title={t("roomDialog.button_stopSession")}
aria-label={t("roomDialog.button_stopSession")}
showAriaLabel={true}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
}}
onClick={onRoomDestroy}
/>
</div>
</>
+1 -8
View File
@@ -134,16 +134,9 @@ 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(RE_COLLAB_LINK);
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
if (match && match[2].length !== 22) {
window.alert(t("alerts.invalidEncryptionKey"));
return null;
+28 -57
View File
@@ -1,5 +1,5 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { ErrorDialog } from "../components/ErrorDialog";
@@ -45,26 +45,20 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import Collab, {
import CollabWrapper, {
CollabAPI,
collabAPIAtom,
collabDialogShownAtom,
isCollaboratingAtom,
} from "./collab/Collab";
CollabContext,
CollabContextConsumer,
} from "./collab/CollabWrapper";
import { LanguageList } from "./components/LanguageList";
import {
exportToBackend,
getCollaborationLinkData,
isCollaborationLink,
loadScene,
} from "./data";
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import {
getLibraryItemsFromStorage,
importFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import { restoreAppState, RestoredDataState } from "../data/restore";
import { Tooltip } from "../components/Tooltip";
import { shield } from "../components/icons";
@@ -78,13 +72,8 @@ 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,
);
@@ -181,7 +170,7 @@ const initializeScene = async (opts: {
if (roomLinkData) {
return {
scene: await opts.collabAPI.startCollaboration(roomLinkData),
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
isExternalScene: true,
id: roomLinkData.roomId,
key: roomLinkData.roomKey,
@@ -253,11 +242,7 @@ const ExcalidrawWrapper = () => {
const [excalidrawAPI, excalidrawRefCallback] =
useCallbackRefState<ExcalidrawImperativeAPI>();
const [collabAPI] = useAtom(collabAPIAtom);
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
return isCollaborationLink(window.location.href);
});
const collabAPI = useContext(CollabContext)?.api;
useHandleLibrary({
excalidrawAPI,
@@ -335,44 +320,21 @@ const ExcalidrawWrapper = () => {
}
};
initializeScene({ collabAPI }).then(async (data) => {
initializeScene({ collabAPI }).then((data) => {
loadImages(data, /* isInitialLoad */ true);
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(),
),
});
initialStatePromiseRef.current.promise.resolve(data.scene);
});
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,
...restore(data.scene, null, null),
commitToHistory: true,
appState: restoreAppState(data.scene.appState, null),
});
}
});
@@ -660,7 +622,7 @@ const ExcalidrawWrapper = () => {
const renderCustomStats = () => {
return (
<CustomStats
setToast={(message) => excalidrawAPI!.setToast({ message })}
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
/>
);
};
@@ -674,19 +636,23 @@ 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": isCollaborating,
"is-collaborating": collabAPI?.isCollaborating(),
})}
>
<Excalidraw
ref={excalidrawRefCallback}
onChange={onChange}
initialData={initialStatePromiseRef.current.promise}
onCollabButtonClick={() => setCollabDialogShown(true)}
isCollaborating={isCollaborating}
onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()}
onPointerUpdate={collabAPI?.onPointerUpdate}
UIOptions={{
canvasActions: {
@@ -720,7 +686,12 @@ const ExcalidrawWrapper = () => {
onLibraryChange={onLibraryChange}
autoFocus={true}
/>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{excalidrawAPI && (
<CollabWrapper
excalidrawAPI={excalidrawAPI}
onRoomClose={onRoomClose}
/>
)}
{errorMessage && (
<ErrorDialog
message={errorMessage}
@@ -734,9 +705,9 @@ const ExcalidrawWrapper = () => {
const ExcalidrawApp = () => {
return (
<TopErrorBoundary>
<Provider unstable_createStore={() => jotaiStore}>
<CollabContextConsumer>
<ExcalidrawWrapper />
</Provider>
</CollabContextConsumer>
</TopErrorBoundary>
);
};
-1
View File
@@ -14,7 +14,6 @@ interface Window {
__EXCALIDRAW_SHA__: string | undefined;
EXCALIDRAW_ASSET_PATH: string | undefined;
EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function;
}
+1 -4
View File
@@ -48,13 +48,10 @@ 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
@@ -89,7 +86,7 @@ export const setLanguage = async (lang: Language) => {
currentLangData = {};
} else {
currentLangData = await import(
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
);
}
};
+3 -9
View File
@@ -1,14 +1,8 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import ReactDOM from "react-dom";
import ExcalidrawApp from "./excalidraw-app";
import "./excalidraw-app/pwa";
import "./excalidraw-app/sentry";
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
root.render(
<StrictMode>
<ExcalidrawApp />
</StrictMode>,
);
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
+1 -24
View File
@@ -1,27 +1,4 @@
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
import { useLayoutEffect } from "react";
import { unstable_createStore } from "jotai";
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;
};
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
"browserZoom": ""
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
},
"errors": {
"unsupportedFileType": "نوع الملف غير مدعوم.",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Нулиране на платно",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "Този файлов формат не се поддържа.",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
+13 -20
View File
@@ -9,7 +9,7 @@
"copy": "Copia",
"copyAsPng": "Copia al porta-retalls com a PNG",
"copyAsSvg": "Copia al porta-retalls com a SVG",
"copyText": "Copia al porta-retalls com a text",
"copyText": "",
"bringForward": "Porta endavant",
"sendToBack": "Envia enrere",
"bringToFront": "Porta al davant",
@@ -115,18 +115,12 @@
"label": "Enllaç"
},
"elementLock": {
"lock": "Bloca",
"unlock": "Desbloca",
"lockAll": "Bloca-ho tot",
"unlockAll": "Desbloca-ho tot"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Publicat",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Neteja el llenç",
@@ -174,7 +168,7 @@
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
"importBackendFailed": "Importació fallida.",
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
"couldNotCopyToClipboard": "No s'ha pogut copiar al porta-retalls.",
"couldNotCopyToClipboard": "",
"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?",
@@ -187,8 +181,7 @@
"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.",
"browserZoom": ""
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
},
"errors": {
"unsupportedFileType": "Tipus de fitxer no suportat.",
@@ -197,7 +190,7 @@
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": "",
"importLibraryError": "No s'ha pogut carregar la biblioteca"
"importLibraryError": ""
},
"toolBar": {
"selection": "Selecció",
@@ -213,7 +206,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": "Esborrador"
"eraser": ""
},
"headings": {
"canvasActions": "Accions del llenç",
@@ -239,7 +232,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": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",
@@ -299,7 +292,7 @@
"howto": "Seguiu les nostres guies",
"or": "o",
"preventBinding": "Prevenir vinculació de la fletxa",
"tools": "Eines",
"tools": "",
"shortcuts": "Dreceres de teclat",
"textFinish": "Finalitza l'edició (editor de text)",
"textNewLine": "Afegeix una línia nova (editor de text)",
@@ -350,7 +343,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": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",
+8 -15
View File
@@ -9,7 +9,7 @@
"copy": "Kopírovat",
"copyAsPng": "Zkopírovat do schránky jako PNG",
"copyAsSvg": "Zkopírovat do schránky jako SVG",
"copyText": "Zkopírovat do schránky jako text",
"copyText": "",
"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": "Trojúhelník",
"arrowhead_triangle": "",
"fontSize": "Velikost písma",
"fontFamily": "Písmo",
"onlySelected": "Pouze vybrané",
"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\"",
"withBackground": "",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "Od ruky",
"normal": "Normální",
"code": "Kód",
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
+3 -10
View File
@@ -120,13 +120,7 @@
"lockAll": "Alle sperren",
"unlockAll": "Alle entsperren"
},
"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."
"statusPublished": ""
},
"buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -187,8 +181,7 @@
"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.",
"browserZoom": "Die Zoomstufe Deines Browsers ist nicht auf 100% gesetzt, was dazu führen kann, dass der Zeichenbereich falsch angezeigt wird"
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
},
"errors": {
"unsupportedFileType": "Nicht unterstützter Dateityp.",
@@ -350,7 +343,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": "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."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Bibliothek übermittelt",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Επαναφορά του καμβά",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
"browserZoom": ""
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
},
"errors": {
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
+1 -2
View File
@@ -187,8 +187,7 @@
"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.",
"browserZoom": "Your browser's zoom level is not set to 100% which may cause the board to display incorrectly"
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
},
"errors": {
"unsupportedFileType": "Unsupported file type.",
+3 -10
View File
@@ -120,13 +120,7 @@
"lockAll": "Bloquear todo",
"unlockAll": "Desbloquear todo"
},
"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í."
"statusPublished": ""
},
"buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -187,8 +181,7 @@
"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.",
"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"
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
},
"errors": {
"unsupportedFileType": "Tipo de archivo no admitido.",
@@ -350,7 +343,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": "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."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",
+9 -16
View File
@@ -115,18 +115,12 @@
"label": "Esteka"
},
"elementLock": {
"lock": "Blokeatu",
"unlock": "Desblokeatu",
"lockAll": "Blokeatu guztiak",
"unlockAll": "Desblokeatu guztiak"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Argitaratua",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Garbitu oihala",
@@ -187,8 +181,7 @@
"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.",
"browserZoom": ""
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
},
"errors": {
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
@@ -197,7 +190,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": "Ezin izan da liburutegia kargatu"
"importLibraryError": ""
},
"toolBar": {
"selection": "Hautapena",
@@ -307,7 +300,7 @@
"view": "Bistaratu",
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
"zoomToSelection": "Zooma hautapenera",
"toggleElementLock": "Blokeatu/desbloketatu hautapena"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Garbitu oihala"
@@ -350,7 +343,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": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Liburutegia bidali da",
+148 -155
View File
@@ -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,35 +98,29 @@
"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": "منتشر شده",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "پاکسازی بوم نقاشی",
@@ -153,19 +147,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": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
@@ -174,34 +168,33 @@
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
"couldNotCopyToClipboard": "به کلیپ بورد کپی نشد.",
"couldNotCopyToClipboard": "",
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
"collabStopOverridePrompt": "با توقف بوم نقاشی، نقشه قبلی و ذخیره شده محلی شما را بازنویسی می کند. مطمئنی؟\n\n(اگر می خواهید نقاشی محلی خود را حفظ کنید، به سادگی برگه مرورگر را ببندید.)",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
"imageDoesNotContainScene": "به نظر نمی رسد این تصویر حاوی داده های بوم نقاشی باشد. آیا جاسازی صحنه را در حین خروجی فعال کرده اید?",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
"browserZoom": ""
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
"invalidSVGString": "SVG نادرست.",
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
"importLibraryError": "داده‌ها بارگذاری نشدند"
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "گزینش",
"image": "وارد کردن تصویر",
"image": "",
"rectangle": "مستطیل",
"diamond": "لوزی",
"ellipse": "بیضی",
@@ -211,9 +204,9 @@
"text": "متن",
"library": "کتابخانه",
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "از زوم کوچک کردن جلوگیری کنید و ورودی آزاد را فقط از قلم بپذیرید",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن"
"penMode": "",
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "عملیات روی بوم",
@@ -221,25 +214,25 @@
"shapes": "شکل‌ها"
},
"hints": {
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
"canvasPanning": "",
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
"resizeImage": "",
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
"publishLibrary": "کتابخانه خود را منتشر کنید",
"bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -267,27 +260,27 @@
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
"shareTitle": "به یک جلسه همکاری زنده در Excalidraw بپیوندید"
"shareTitle": ""
},
"errorDialog": {
"title": "خطا"
},
"exportDialog": {
"disk_title": "ذخیره در دیسک",
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
"disk_details": "",
"disk_button": "ذخیره در فایل",
"link_title": "لینک قابل اشتراک‌گذاری",
"link_details": "خروجی به عنوان یک پیوند فقط خواندنی.",
"link_button": "خروجی در فایل",
"excalidrawplus_description": "صحنه را در فضای کاری Excalidraw+ خود ذخیره کنید.",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "خروجی گرفتن",
"excalidrawplus_exportError": "در حال حاضر نمی‌توان به Excalidraw+ صادر کرد..."
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "بلاگ ما را بخوانید",
"click": "کلیک",
"deepSelect": "انتخاب عمیق",
"deepBoxSelect": "انتخاب عمیق در کادر، و جلوگیری از کشیدن",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "فلش خمیده",
"curvedLine": "منحنی",
"documentation": "مستندات",
@@ -299,7 +292,7 @@
"howto": "راهنمای ما را دنبال کنید",
"or": "یا",
"preventBinding": "مانع شدن از چسبیدن فلش ها",
"tools": "ابزار",
"tools": "",
"shortcuts": "میانبرهای صفحه کلید",
"textFinish": "پایان ویرایش (ویرایشگر متن)",
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
@@ -307,63 +300,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": "دسته GitHub (اختیاری)، بنابراین می توانید پس از ارسال برای بررسی، کتابخانه را ویرایش کنید",
"twitterHandle": "نام کاربری توییتر (اختیاری)، بنابراین می دانیم هنگام تبلیغ در توییتر به چه کسی اعتبار دهیم",
"website": "پیوند به وب سایت شخصی شما یا هر جای دیگر (اختیاری)"
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "لازم",
"website": "وارد کردن آدرس درست"
"required": "",
"website": ""
},
"noteDescription": {
"pre": "کتابخانه خود را ارسال کنید تا در آن گنجانده شود ",
"link": "مخزن کتابخانه عمومی",
"post": "تا افراد دیگر در نقاشی های خود از آن استفاده کنند."
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "کتابخانه باید ابتدا به صورت دستی تایید شود. لطفاً بخوانید ",
"link": "دستورالعمل‌ها",
"post": " قبل از ارسال برای برقراری ارتباط و ایجاد تغییرات در صورت درخواست، به یک حساب GitHub نیاز دارید، اما به شدت الزامی نیست."
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "با ارسال، موافقت می کنید که کتابخانه تحت عنوان منتشر شود ",
"link": "پروانهٔ MIT ",
"post": "که به طور خلاصه به این معنی است که هر کسی می تواند بدون محدودیت از آنها استفاده کند."
"pre": "",
"link": "",
"post": ""
},
"noteItems": "هر مورد کتابخانه باید نام خاص خود را داشته باشد تا قابل فیلتر باشد. اقلام کتابخانه زیر شامل خواهد شد:",
"atleastOneLibItem": "لطفاً حداقل یک مورد از کتابخانه را برای شروع انتخاب کنید",
"republishWarning": "توجه: برخی از موارد انتخاب شده به عنوان قبلاً منتشر شده/ارسال شده علامت گذاری شده اند. شما فقط باید هنگام به‌روزرسانی یک کتابخانه موجود یا ارسال، موارد را دوباره ارسال کنید."
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "کتابخانه ارسال شد",
"content": "تشکر از شما {{authorName}}. کتابخانه شما برای بررسی ارسال شده است. می توانید وضعیت را پیگیری کنید",
"link": "اینجا"
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "تنظیم مجدد کتابخانه",
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
"link": ""
},
"stats": {
"angle": "زاویه",
@@ -381,60 +374,60 @@
"width": "عرض"
},
"toast": {
"addedToLibrary": "به مجموعه اضافه شد",
"addedToLibrary": "",
"copyStyles": "کپی سبک.",
"copyToClipboard": "در کلیپ‌بورد کپی شد.",
"copyToClipboardAsPng": "کپی {{exportSelection}} در کلیپبورد به عنوان PNG\n({{exportColorScheme}})",
"copyToClipboardAsPng": "",
"fileSaved": "فایل ذخیره شد.",
"fileSavedToFilename": "ذخیره در {filename}",
"canvas": "بوم",
"selection": "انتخاب"
},
"colors": {
"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"
"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": ""
}
}
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"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."
"statusPublished": "Julkaistu"
},
"buttons": {
"clearReset": "Tyhjennä piirtoalue",
@@ -187,8 +181,7 @@
"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ä.",
"browserZoom": ""
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
},
"errors": {
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
+8 -15
View File
@@ -105,28 +105,22 @@
"toggleTheme": "Changer le thème",
"personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw",
"decreaseFontSize": "Diminuer la taille de police",
"decreaseFontSize": "Réduire la taille de police",
"increaseFontSize": "Augmenter la taille de police",
"unbindText": "Dissocier le texte",
"bindText": "Associer le texte au conteneur",
"unbindText": "Délier le texte",
"bindText": "Lier le texte au conteneur",
"link": {
"edit": "Modifier le lien",
"create": "Ajouter un lien",
"create": "Créer un lien",
"label": "Lien"
},
"elementLock": {
"lock": "Verrouiller",
"unlock": "Déverrouiller",
"lockAll": "Tout verrouiller",
"unlockAll": "Tout déverrouiller"
"unlockAll": "Tout déverouiller"
},
"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."
"statusPublished": ""
},
"buttons": {
"clearReset": "Réinitialiser le canevas",
@@ -187,8 +181,7 @@
"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.",
"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"
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
},
"errors": {
"unsupportedFileType": "Type de fichier non supporté.",
@@ -350,7 +343,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": "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."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Bibliothèque soumise",
-440
View File
@@ -1,440 +0,0 @@
{
"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": ""
}
}
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "לנעול הכל",
"unlockAll": "שחרור הכול"
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "אפס את הלוח",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
"browserZoom": ""
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
},
"errors": {
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "सब ताले के अंदर रखे",
"unlockAll": "सब ताले के बाहर निकाले"
},
"statusPublished": "प्रकाशित",
"sidebarLock": "साइडबार खुला रखे."
},
"library": {
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
"hint_emptyLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.",
"hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने."
"statusPublished": "प्रकाशित"
},
"buttons": {
"clearReset": "कैनवास रीसेट करें",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": "आपके ब्राउज़र का ज़ूम लेवल 100% नहीं हैं इस कारण दृष्य पटल ग़लत दिख सकता हैं"
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
+2 -9
View File
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Vászon törlése",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
"browserZoom": ""
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
},
"errors": {
"unsupportedFileType": "Nem támogatott fájltípus.",

Some files were not shown because too many files have changed in this diff Show More