Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Telatynski
0acb44d696 Wire up electron download progress to toasts 2021-04-14 10:58:11 +01:00
93 changed files with 4615 additions and 7181 deletions

View File

@@ -1,10 +1,6 @@
module.exports = {
plugins: ["matrix-org"],
extends: [
"plugin:matrix-org/javascript",
],
parserOptions: {
ecmaVersion: 2021,
ecmaVersion: 8,
},
env: {
es6: true,
@@ -12,24 +8,15 @@ module.exports = {
// we also have some browser code (ie. the preload script)
browser: true,
},
extends: ["matrix-org"],
rules: {
// js-sdk uses a babel rule which we can't use because we
// don't use babel, so remove it & put the original back
"babel/no-invalid-this": "off",
"no-invalid-this": "error",
"quotes": "off",
"indent": "off",
"prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off",
},
overrides: [{
files: ["{src,scripts,hak}/**/*.{ts,tsx}"],
extends: [
"plugin:matrix-org/typescript",
],
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
},
}],
};
}
}

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @vector-im/element-web

View File

@@ -1,12 +0,0 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/dist
/lib
/webapp
/webapp.asar
/packages
@@ -11,5 +10,3 @@
/.yarnrc
/docker
/.npmrc
.vscode
.vscode/

View File

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ so the first step is to get a working copy of Element Web. There are a few ways
# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
# We're explicitly asking for no config, so the packaged Element will have no config.json.
yarn run fetch --noverify --cfgdir ""
yarn run fetch --noverify --cfgdir ''
```
...or if you'd like to use GPG to verify the downloaded package:
@@ -30,14 +30,14 @@ yarn run fetch --noverify --cfgdir ""
# once.
yarn run fetch --importkey
# Fetch the package and verify the signature
yarn run fetch --cfgdir ""
yarn run fetch --cfgdir ''
```
...or either of the above, but fetching a specific version of Element:
```
# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
yarn run fetch --noverify --cfgdir "" v1.5.6
yarn run fetch --noverify --cfgdir '' v1.5.6
```
If you only want to run the app locally and don't need to build packages, you can
@@ -49,17 +49,26 @@ ln -s ../element-web/webapp ./
[TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths]
Building
========
Now you have a copy of Element, you're ready to build packages. If you'd just like to
run Element locally, skip to the next section.
## Native Build
If you'd like to build the native modules (for searching in encrypted rooms and
secure storage), do this first. This will take 10 minutes or so, and will
require a number of native tools to be installed, depending on your OS (eg.
rust, tcl, make/nmake). If you don't need these features, you can skip this
step.
```
yarn run build:native
```
TODO: List native pre-requisites
On Windows, this will automatically determine the architecture to build for based
on the environment (ie. set up by vcvarsall.bat).
Optionally, [build the native modules](https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md),
which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features.
Now you can build the package:
Then, run
```
yarn run build
```
@@ -69,9 +78,18 @@ This will do a couple of things:
* Run electron-builder to build a package. The package built will match the operating system
you're running the build process on.
## Docker
If you're on Windows, you can choose to build specifically for 32 or 64 bit:
```
yarn run build32
```
or
```
yarn run build64
```
Alternatively, you can also build using docker, which will always produce the linux package:
This build step will not build any native modules.
You can also build using docker, which will always produce the linux package:
```
# Run this once to make the docker image
yarn run docker:setup
@@ -130,15 +148,6 @@ $PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of
the above created by a pre-1.7 install, in which case it will be `Riot` or
`Riot-$PROFILE`.
Translations
==========================
To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md).
For a developer guide, see the [translating dev doc](https://github.com/vector-im/element-web/blob/develop/docs/translating-dev.md).
[<img src="https://translate.element.io/widgets/element-desktop/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-desktop/?utm_source=widget)
Report bugs & give feedback
==========================

View File

@@ -1,4 +1,4 @@
FROM buildpack-deps:bionic-curl
FROM buildpack-deps:xenial-curl
ENV DEBIAN_FRONTEND noninteractive
@@ -10,7 +10,7 @@ RUN apt-get -qq update && apt-get -qq dist-upgrade && \
# python for node-gyp
# rpm is required for FPM to build rpm package
# libsecret-1-dev and libgnome-keyring-dev are required even for prebuild keytar
apt-get -qq install --no-install-recommends qtbase5-dev bsdtar build-essential autoconf libssl-dev gcc-multilib g++-multilib lzip rpm python libcurl4 git git-lfs ssh unzip \
apt-get -qq install --no-install-recommends qtbase5-dev bsdtar build-essential autoconf libssl-dev gcc-multilib g++-multilib lzip rpm python libcurl3 git git-lfs ssh unzip \
libsecret-1-dev libgnome-keyring-dev \
libopenjp2-tools \
# Used by Seshat
@@ -30,7 +30,7 @@ ENV LC_ALL C.UTF-8
ENV DEBUG_COLORS true
ENV FORCE_COLOR true
ENV NODE_VERSION 14.17.0
ENV NODE_VERSION 12.16.1
# this package is used for snapcraft and we should not clear apt list - to avoid apt-get update during snap build
RUN curl -L https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz | tar xz -C /usr/local --strip-components=1 && \

View File

@@ -10,26 +10,20 @@ modules from source to ensure we can trust the compiled output. In the future,
we may offer a pre-compiled path for those who want to use these features in a
custom build of Element without installing the various build tools required.
Do note that compiling a module for a particular operating system
(Linux/macOS/Windows) will need to be done on that operating system.
Cross-compiling from a host OS for a different target OS may be possible, but
we don't support this flow with Element dependencies at this time.
The process is automated by [vector-im/element-builder](https://github.com/vector-im/element-builder)
when releasing.
when releasing.
The following sections explain the manual steps you can use with a custom build of Element to enable
these features if you'd like to try them out.
It is possible to [build those native modules locally automatically](https://github.com/vector-im/element-desktop#building).
## Building
Install the pre-requisites for your system:
* [Windows pre-requisites](https://github.com/vector-im/element-desktop/blob/develop/docs/windows-requirements.md)
* Linux: TODO
* OS X: TODO
Then optionally, [add seshat and dependencies to support search in E2E rooms](#Adding Seshat for search in E2E encrypted rooms).
Then, to build for an architecture selected automatically based on your system (recommended), run:
```
yarn run build:native
```
If you need to build for a specific architecture, see [here](#Compiling for specific architectures).
## Adding Seshat for search in E2E encrypted rooms
Seshat is a native Node module that adds support for local event indexing and
@@ -65,74 +59,3 @@ After this is done the Electron version of Element can be run from the main fold
as usual using:
yarn start
## Compiling for specific architectures
### macOS
On macOS, you can build universal native modules too:
```
yarn run build:native:universal
```
...or you can build for a specific architecture:
```
yarn run build:native --target x86_64-apple-darwin
```
or
```
yarn run build:native --target aarch64-apple-darwin
```
You'll then need to create a built bundle with the same architecture.
To bundle a universal build for macOS, run:
```
yarn run build:universal
```
### Windows
If you're on Windows, you can choose to build specifically for 32 or 64 bit:
```
yarn run build:32
```
or
```
yarn run build:64
```
### Cross compiling
Compiling a module for a particular operating system (Linux/macOS/Windows) needs
to be done on that operating system. Cross-compiling from a host OS for a different
target OS may be possible, but we don't support this flow with Element dependencies
at this time.
### Switching between architectures
The native module build system keeps the different architectures
separate, so you can keep native modules for several architectures at the same
time and switch which are active using a `yarn run hak copy` command, passing
the appropriate architectures. This will error if you haven't yet built those
architectures. eg:
```
yarn run build:native --target x86_64-apple-darwin
# We've now built & linked into place native modules for Intel
yarn run build:native --target aarch64-apple-darwin
# We've now built Apple Silicon modules too, and linked them into place as the active ones
yarn run hak copy --target x86_64-apple-darwin
# We've now switched back to our Intel modules
yarn run hak copy --target x86_64-apple-darwin --target aarch64-apple-darwin
# Now our native modules are universal x86_64+aarch64 binaries
```
The current set of native modules are stored in `.hak/hakModules`,
so you can use this to check what architecture is currently in place, eg:
```
$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64
```

View File

@@ -1,26 +0,0 @@
# Windows
## Requirements to build native modules
If you want to build native modules, make sure that the following tools are installed on your system.
- [Node 14](https://nodejs.org)
- [Python 3](https://www.python.org/downloads/)
- [Strawberry Perl](https://strawberryperl.com/)
- [Rust](https://rustup.rs/)
- [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) with the following configuration:
- On the Workloads tab:
- Desktop & Mobile -> C++ build tools
- On the Individual components tab:
- MSVC VS 2019 C++ build tools
- Windows 10 SDK (latest version available)
- C++ CMake tools for Windows
Once installed make sure all those utilities are accessible in your `PATH`.
In order to load all the C++ utilities installed by Visual Studio you can run the following in a terminal window.
```
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64
```
You can replace `amd64` with `x86` depending on your CPU architecture.

View File

@@ -1,5 +1,5 @@
{
"update_base_url": "https://packages.element.io/nightly/update/",
"update_base_url": "https://packages.riot.im/nightly/update/",
"default_server_name": "matrix.org",
"brand": "Element Nightly",
"integrations_ui_url": "https://scalar.vector.im/",
@@ -13,7 +13,6 @@
],
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": true,
"piwik": {
"url": "https://piwik.riot.im/",
@@ -23,8 +22,7 @@
"roomDirectory": {
"servers": [
"matrix.org",
"gitter.im",
"libera.chat"
"gitter.im"
]
},
"enable_presence_by_hs_url": {
@@ -40,17 +38,5 @@
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"sentry": {
"dsn": "https://029a0eb289f942508ae0fb17935bd8c5@sentry.matrix.org/6",
"environment": "nightly"
},
"posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"apiHost": "https://posthog.hss.element.io"
},
"features": {
"feature_spotlight": true
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
]
}

View File

@@ -3,8 +3,7 @@ License: Apache-2.0
Vendor: support@element.io
Architecture: amd64
Maintainer: support@element.io
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0, libsqlcipher0
Recommends: libappindicator3-1
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libappindicator3-1, libsecret-1-0, libsqlcipher0
Section: net
Priority: extra
Homepage: https://element.io/

View File

@@ -1,5 +1,5 @@
{
"update_base_url": "https://packages.element.io/desktop/update/",
"update_base_url": "https://packages.riot.im/desktop/update/",
"default_server_name": "matrix.org",
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
@@ -13,12 +13,10 @@
],
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"roomDirectory": {
"servers": [
"matrix.org",
"gitter.im",
"libera.chat"
"gitter.im"
]
},
"showLabsSettings": false,
@@ -40,10 +38,5 @@
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"apiHost": "https://posthog.hss.element.io"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
]
}

View File

@@ -3,8 +3,7 @@ License: Apache-2.0
Vendor: support@element.io
Architecture: amd64
Maintainer: support@element.io
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0, libsqlcipher0
Recommends: libappindicator3-1
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libappindicator3-1, libsecret-1-0, libsqlcipher0
Replaces: riot-desktop (<< 1.7.0), riot-web (<< 1.7.0)
Breaks: riot-desktop (<< 1.7.0), riot-web (<< 1.7.0)
Section: net

View File

@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
module.exports = async function(hakEnv, moduleInfo) {
await buildKeytar(hakEnv, moduleInfo);
};
export default async function buildKeytar(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function buildKeytar(hakEnv, moduleInfo) {
const env = hakEnv.makeGypEnv();
console.log("Running yarn with env", env);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'node-gyp' + (hakEnv.isWin() ? '.cmd' : '')),
['rebuild'],

View File

@@ -14,16 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import childProcess from 'child_process';
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension
for (const tool of tools) {
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'],
});
@@ -36,4 +33,4 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
});
});
}
}
};

View File

@@ -1,7 +1,7 @@
{
"scripts": {
"check": "check.ts",
"build": "build.ts"
"check": "check.js",
"build": "build.js"
},
"copy": "build/Release/keytar.node",
"dependencies": {

View File

@@ -1,5 +1,5 @@
/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
import mkdirp from 'mkdirp';
import fsExtra from 'fs-extra';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
const mkdirp = require('mkdirp');
const fsExtra = require('fs-extra');
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
if (hakEnv.isWin()) {
await buildOpenSslWin(hakEnv, moduleInfo);
await buildSqlCipherWin(hakEnv, moduleInfo);
@@ -30,24 +28,24 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
await buildSqlCipherUnix(hakEnv, moduleInfo);
}
await buildMatrixSeshat(hakEnv, moduleInfo);
}
};
async function buildOpenSslWin(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
const openSslDir = path.join(moduleInfo.moduleDotHakDir, `openssl-${version}`);
const openSslArch = hakEnv.getTargetArch() === 'x64' ? 'VC-WIN64A' : 'VC-WIN32';
const openSslArch = hakEnv.arch === 'x64' ? 'VC-WIN64A' : 'VC-WIN32';
console.log("Building openssl in " + openSslDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'perl',
[
'Configure',
'--prefix=' + moduleInfo.depPrefix,
// sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time.
// sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time.
'no-afalgeng',
'no-capieng',
'no-cms',
@@ -105,7 +103,7 @@ async function buildOpenSslWin(hakEnv, moduleInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['build_libs'],
@@ -119,7 +117,7 @@ async function buildOpenSslWin(hakEnv, moduleInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['install_dev'],
@@ -136,12 +134,12 @@ async function buildOpenSslWin(hakEnv, moduleInfo) {
async function buildSqlCipherWin(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const sqlCipherDir = path.join(moduleInfo.moduleDotHakDir, `sqlcipher-${version}`);
const buildDir = path.join(sqlCipherDir, 'bld');
await mkdirp(buildDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['/f', path.join('..', 'Makefile.msc'), 'libsqlite3.lib', 'TOP=..'],
@@ -173,7 +171,7 @@ async function buildSqlCipherWin(hakEnv, moduleInfo) {
async function buildSqlCipherUnix(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const sqlCipherDir = path.join(moduleInfo.moduleDotHakDir, `sqlcipher-${version}`);
const args = [
'--prefix=' + moduleInfo.depPrefix + '',
@@ -184,39 +182,12 @@ async function buildSqlCipherUnix(hakEnv, moduleInfo) {
if (hakEnv.isMac()) {
args.push('--with-crypto-lib=commoncrypto');
}
if (!hakEnv.isHost()) {
// In the nonsense world of `configure`, it is assumed you are building
// a compiler like `gcc`, so the `host` option actually means the target
// the build output runs on.
args.push(`--host=${hakEnv.getTargetId()}`);
}
const cflags = [
'-DSQLITE_HAS_CODEC',
];
if (!hakEnv.isHost()) {
// `clang` uses more logical option naming.
cflags.push(`--target=${hakEnv.getTargetId()}`);
}
if (cflags.length) {
args.push(`CFLAGS=${cflags.join(' ')}`);
}
const ldflags = [];
args.push('CFLAGS=-DSQLITE_HAS_CODEC');
if (hakEnv.isMac()) {
ldflags.push('-framework Security');
ldflags.push('-framework Foundation');
args.push('LDFLAGS=-framework Security -framework Foundation');
}
if (ldflags.length) {
args.push(`LDFLAGS=${ldflags.join(' ')}`);
}
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(sqlCipherDir, 'configure'),
args,
@@ -230,7 +201,7 @@ async function buildSqlCipherUnix(hakEnv, moduleInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'make',
[],
@@ -244,7 +215,7 @@ async function buildSqlCipherUnix(hakEnv, moduleInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'make',
['install'],
@@ -260,9 +231,6 @@ async function buildSqlCipherUnix(hakEnv, moduleInfo) {
}
async function buildMatrixSeshat(hakEnv, moduleInfo) {
// seshat now uses n-api so we shouldn't need to specify a node version to
// build against, but it does seems to still need something in here, so leaving
// it for now: we should confirm how much of this it still actually needs.
const env = hakEnv.makeGypEnv();
if (!hakEnv.isLinux()) {
@@ -280,15 +248,11 @@ async function buildMatrixSeshat(hakEnv, moduleInfo) {
// the build scripts since they run on the host, but vcvarsall.bat sets the c
// compiler in the path to be the one for the target, so we just use the matching
// toolchain for the target architecture which makes everything happy.
env.RUSTUP_TOOLCHAIN = `stable-${hakEnv.getTargetId()}`;
}
if (!hakEnv.isHost()) {
env.CARGO_BUILD_TARGET = hakEnv.getTargetId();
env.RUSTUP_TOOLCHAIN = hakEnv.arch == 'x64' ? 'stable-x86_64-pc-windows-msvc' : 'stable-i686-pc-windows-msvc';
}
console.log("Running neon with env", env);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'neon' + (hakEnv.isWin() ? '.cmd' : '')),
['build', '--release'],

View File

@@ -1,5 +1,5 @@
/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,16 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import childProcess from 'child_process';
import fsProm from 'fs/promises';
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
// of course tcl doesn't have a --version
if (!hakEnv.isLinux()) {
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn('tclsh', [], {
stdio: ['pipe', 'ignore', 'ignore'],
});
@@ -38,10 +34,7 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
});
}
const tools = [
['rustc', '--version'],
['python', '--version'], // node-gyp uses python for reasons beyond comprehension
];
const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension
if (hakEnv.isWin()) {
tools.push(['perl', '--version']); // for openssl configure
tools.push(['patch', '--version']); // to patch sqlcipher Makefile.msc
@@ -51,7 +44,7 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
}
for (const tool of tools) {
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'],
});
@@ -64,22 +57,4 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
});
});
}
// Ensure Rust target exists (nb. we avoid depending on rustup)
await new Promise((resolve, reject) => {
const rustc = childProcess.execFile('rustc', [
'--target', hakEnv.getTargetId(), '-o', 'tmp', '-',
], (err, out) => {
if (err) {
reject(
"rustc can't build for target " + hakEnv.getTargetId() +
": ensure target is installed via `rustup target add " + hakEnv.getTargetId() + "` " +
"or your package manager if not using `rustup`",
);
}
fsProm.unlink('tmp').then(resolve);
});
rustc.stdin.write('fn main() {}');
rustc.stdin.end();
});
}
};

View File

@@ -14,17 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
import fs from 'fs';
import fsProm from 'fs/promises';
import needle from 'needle';
import tar from 'tar';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
const fs = require('fs');
const fsProm = require('fs').promises;
const needle = require('needle');
const tar = require('tar');
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
if (!hakEnv.isLinux()) {
await getSqlCipher(hakEnv, moduleInfo);
}
@@ -32,11 +30,11 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
if (hakEnv.isWin()) {
await getOpenSsl(hakEnv, moduleInfo);
}
}
};
async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function getSqlCipher(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const sqlCipherDir = path.join(moduleInfo.moduleDotHakDir, `sqlcipher-${version}`);
let haveSqlcipher;
try {
@@ -64,10 +62,9 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
await bob;
}
// Extract the tarball to per-target directories, then we avoid cross-contaiminating archs
await tar.x({
file: sqlCipherTarball,
cwd: moduleInfo.moduleTargetDotHakDir,
cwd: moduleInfo.moduleDotHakDir,
});
if (hakEnv.isWin()) {
@@ -76,8 +73,8 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
// set it to 2 (default to memory).
const patchFile = path.join(moduleInfo.moduleHakDir, `sqlcipher-${version}-win.patch`);
await new Promise<void>((resolve, reject) => {
const readStream = fs.createReadStream(patchFile);
await new Promise((resolve, reject) => {
const readStream = fs.createReadStream(patchFile);
const proc = childProcess.spawn(
'patch',
@@ -95,9 +92,9 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
}
}
async function getOpenSsl(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function getOpenSsl(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
const openSslDir = path.join(moduleInfo.moduleDotHakDir, `openssl-${version}`);
let haveOpenSsl;
try {
@@ -124,9 +121,9 @@ async function getOpenSsl(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<v
});
}
console.log("extracting " + openSslTarball + " in " + moduleInfo.moduleTargetDotHakDir);
console.log("extracting " + openSslTarball + " in " + moduleInfo.moduleDotHakDir);
await tar.x({
file: openSslTarball,
cwd: moduleInfo.moduleTargetDotHakDir,
cwd: moduleInfo.moduleDotHakDir,
});
}

View File

@@ -1,8 +1,8 @@
{
"scripts": {
"check": "check.ts",
"fetchDeps": "fetchDeps.ts",
"build": "build.ts"
"check": "check.js",
"fetchDeps": "fetchDeps.js",
"build": "build.js"
},
"prune": "native",
"copy": "native/index.node",

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2016",
"sourceMap": false,
"lib": [
"es2019",
]
},
"include": [
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -1,8 +1,8 @@
{
"name": "element-desktop",
"productName": "Element",
"main": "lib/electron-main.js",
"version": "1.10.6",
"main": "src/electron-main.js",
"version": "1.7.25",
"description": "A feature-rich client for Matrix.org",
"author": "Element",
"repository": {
@@ -12,36 +12,25 @@
"license": "Apache-2.0",
"files": [],
"scripts": {
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"mkdirs": "mkdirp packages deploys",
"fetch": "yarn run mkdirs && node scripts/fetch-package.js",
"asar-webapp": "asar p webapp webapp.asar",
"start": "yarn run build:ts && yarn run build:res && electron .",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src scripts hak",
"lint:js-fix": "eslint --fix src scripts hak",
"lint:types": "tsc --noEmit && tsc -p scripts/hak/tsconfig.json --noEmit && tsc -p hak/tsconfig.json --noEmit",
"start": "electron .",
"lint": "eslint src/ scripts/ hak/",
"build:native": "yarn run hak",
"build:native:universal": "yarn run hak --target x86_64-apple-darwin fetchandbuild && yarn run hak --target aarch64-apple-darwin fetchandbuild && yarn run hak --target x86_64-apple-darwin --target aarch64-apple-darwin copyandlink",
"build:32": "yarn run build:ts && yarn run build:res && electron-builder --ia32",
"build:64": "yarn run build:ts && yarn run build:res && electron-builder --x64",
"build:universal": "yarn run build:ts && yarn run build:res && electron-builder --universal",
"build": "yarn run build:ts && yarn run build:res && electron-builder",
"build:ts": "tsc",
"build:res": "node scripts/copy-res.js",
"build32": "electron-builder --ia32",
"build64": "electron-builder --x64",
"build": "electron-builder",
"docker:setup": "docker build -t element-desktop-dockerbuild dockerbuild",
"docker:build:native": "scripts/in-docker.sh yarn run hak",
"docker:build": "scripts/in-docker.sh yarn run build",
"docker:install": "scripts/in-docker.sh yarn install",
"debrepo": "scripts/mkrepo.sh",
"clean": "rimraf webapp.asar dist packages deploys lib",
"hak": "ts-node scripts/hak/index.ts"
"clean": "rimraf webapp.asar dist packages deploys",
"hak": "node scripts/hak/index.js"
},
"dependencies": {
"auto-launch": "^5.0.5",
"counterpart": "^0.18.6",
"electron-store": "^6.0.1",
"electron-window-state": "^5.0.3",
"minimist": "^1.2.3",
@@ -49,53 +38,38 @@
"request": "^2.88.2"
},
"devDependencies": {
"@types/auto-launch": "^5.0.1",
"@types/counterpart": "^0.18.1",
"@types/minimist": "^1.2.1",
"@types/mkdirp": "^1.0.2",
"@types/pacote": "^11.1.1",
"@types/rimraf": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"app-builder-lib": "^22.14.10",
"asar": "^2.0.1",
"chokidar": "^3.5.2",
"electron": "^17",
"electron-builder": "22.11.4",
"electron-builder-squirrel-windows": "22.11.4",
"electron-builder": "22.10.5",
"electron-builder-squirrel-windows": "22.10.5",
"electron-devtools-installer": "^3.1.1",
"electron-notarize": "^1.0.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
"eslint": "7.3.1",
"eslint-config-matrix-org": "^0.1.2",
"find-npm-prefix": "^1.0.2",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",
"matrix-web-i18n": "^1.2.0",
"mkdirp": "^1.0.3",
"needle": "^2.5.0",
"node-pre-gyp": "^0.15.0",
"pacote": "^11.3.5",
"npm": "^6.14.11",
"rimraf": "^3.0.2",
"tar": "^6.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.3"
"semver": "^7.3.4",
"tar": "^6.1.0"
},
"hakDependencies": {
"matrix-seshat": "^2.3.0",
"matrix-seshat": "^2.2.3",
"keytar": "^5.6.0"
},
"build": {
"appId": "im.riot.app",
"electronVersion": "12.0.2",
"files": [
"package.json",
{
"from": ".hak/hakModules",
"to": "node_modules"
},
"lib/**"
"src/**"
],
"extraResources": [
{

View File

@@ -1,7 +1 @@
signing_id: releases@riot.im
subprojects:
element-web:
includeByDefault: true
# Because element-web is not in our dependencies, but the versions
# follow those of this project (well, vice-versa really)
mirrorVersion: true

View File

@@ -2,4 +2,4 @@
set -ex
yarn install --pure-lockfile $@
yarn install $@

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env node
// copies resources into the lib directory.
const parseArgs = require('minimist');
const chokidar = require('chokidar');
const path = require('path');
const fs = require('fs');
const argv = parseArgs(process.argv.slice(2), {});
const watch = argv.w;
const verbose = argv.v;
function errCheck(err) {
if (err) {
console.error(err.message);
process.exit(1);
}
}
const I18N_BASE_PATH = "src/i18n/strings/";
const INCLUDE_LANGS = fs.readdirSync(I18N_BASE_PATH).filter(fn => fn.endsWith(".json"));
// Ensure lib, lib/i18n and lib/i18n/strings all exist
fs.mkdirSync('lib/i18n/strings', { recursive: true });
function genLangFile(file, dest) {
let translations = {};
[file].forEach(function(f) {
if (fs.existsSync(f)) {
try {
Object.assign(
translations,
JSON.parse(fs.readFileSync(f).toString()),
);
} catch (e) {
console.error("Failed: " + f, e);
throw e;
}
}
});
translations = weblateToCounterpart(translations);
const json = JSON.stringify(translations, null, 4);
const filename = path.basename(file);
fs.writeFileSync(dest + filename, json);
if (verbose) {
console.log("Generated language file: " + filename);
}
}
/*
* Convert translation key from weblate format
* (which only supports a single level) to counterpart
* which requires object values for 'count' translations.
*
* eg.
* "there are %(count)s badgers|one": "a badger",
* "there are %(count)s badgers|other": "%(count)s badgers"
* becomes
* "there are %(count)s badgers": {
* "one": "a badger",
* "other": "%(count)s badgers"
* }
*/
function weblateToCounterpart(inTrs) {
const outTrs = {};
for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2);
if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]];
if (obj === undefined) {
obj = {};
outTrs[keyParts[0]] = obj;
}
obj[keyParts[1]] = inTrs[key];
} else {
outTrs[key] = inTrs[key];
}
}
return outTrs;
}
/*
watch the input files for a given language,
regenerate the file, and regenerating languages.json with the new filename
*/
function watchLanguage(file, dest) {
// XXX: Use a debounce because for some reason if we read the language
// file immediately after the FS event is received, the file contents
// appears empty. Possibly https://github.com/nodejs/node/issues/6112
let makeLangDebouncer;
const makeLang = () => {
if (makeLangDebouncer) {
clearTimeout(makeLangDebouncer);
}
makeLangDebouncer = setTimeout(() => {
genLangFile(file, dest);
}, 500);
};
chokidar.watch(file)
.on('add', makeLang)
.on('change', makeLang)
.on('error', errCheck);
}
// language resources
const I18N_DEST = "lib/i18n/strings/";
INCLUDE_LANGS.forEach((file) => {
genLangFile(I18N_BASE_PATH + file, I18N_DEST);
}, {});
if (watch) {
INCLUDE_LANGS.forEach(file => watchLanguage(I18N_BASE_PATH + file, I18N_DEST));
}

View File

@@ -10,12 +10,13 @@ const asar = require('asar');
const needle = require('needle');
const riotDesktopPackageJson = require('../package.json');
const { setPackageVersion } = require('./set-version.js');
const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc";
const PACKAGE_URL_PREFIX = "https://github.com/vector-im/element-web/releases/download/";
const ASAR_PATH = 'webapp.asar';
const {setPackageVersion, setDebVersion} = require('./set-version.js');
async function getLatestDevelopUrl(bkToken) {
const buildsResult = await needle('get',
"https://api.buildkite.com/v2/organizations/matrix-dot-org/pipelines/element-web/builds",
@@ -282,6 +283,7 @@ async function main() {
const semVer = targetVersion.slice(1);
console.log("Updating version to " + semVer);
await setPackageVersion(semVer);
await setDebVersion(semVer);
}
console.log("Done!");

View File

@@ -17,8 +17,7 @@ clone() {
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
fi
}

View File

@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DependencyInfo } from "./dep";
import HakEnv from "./hakEnv";
export default async function build(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function build(hakEnv, moduleInfo) {
await moduleInfo.scripts.build(hakEnv, moduleInfo);
}
module.exports = build;

View File

@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DependencyInfo } from "./dep";
import HakEnv from "./hakEnv";
export default async function check(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function check(hakEnv, moduleInfo) {
if (moduleInfo.scripts.check) {
await moduleInfo.scripts.check(hakEnv, moduleInfo);
}
}
module.exports = check;

View File

@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import rimraf from 'rimraf';
const path = require('path');
import { DependencyInfo } from './dep';
import HakEnv from './hakEnv';
const rimraf = require('rimraf');
export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await new Promise<void>((resolve, reject) => {
rimraf(moduleInfo.moduleDotHakDir, (err: Error) => {
async function clean(hakEnv, moduleInfo) {
await new Promise((resolve, reject) => {
rimraf(moduleInfo.moduleDotHakDir, (err) => {
if (err) {
reject(err);
} else {
@@ -31,8 +29,8 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.dotHakDir, 'links', moduleInfo.name), (err: Error) => {
await new Promise((resolve, reject) => {
rimraf(path.join(hakEnv.dotHakDir, 'links', moduleInfo.name), (err) => {
if (err) {
reject(err);
} else {
@@ -41,8 +39,8 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.projectRoot, 'node_modules', moduleInfo.name), (err: Error) => {
await new Promise((resolve, reject) => {
rimraf(path.join(hakEnv.projectRoot, 'node_modules', moduleInfo.name), (err) => {
if (err) {
reject(err);
} else {
@@ -51,3 +49,5 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
}
module.exports = clean;

67
scripts/hak/copy.js Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const path = require('path');
const fsProm = require('fs').promises;
const rimraf = require('rimraf');
const glob = require('glob');
const mkdirp = require('mkdirp');
async function copy(hakEnv, moduleInfo) {
if (moduleInfo.cfg.prune) {
console.log("Removing " + moduleInfo.cfg.prune + " from " + moduleInfo.moduleOutDir);
// rimraf doesn't have a 'cwd' option: it always uses process.cwd()
// (and if you set glob.cwd it just breaks because it can't find the files)
const oldCwd = process.cwd();
try {
process.chdir(moduleInfo.moduleOutDir);
await new Promise((resolve, reject) => {
rimraf(moduleInfo.cfg.prune, {}, err => {
err ? reject(err) : resolve();
});
});
} finally {
process.chdir(oldCwd);
}
}
if (moduleInfo.cfg.copy) {
console.log(
"Copying " + moduleInfo.cfg.prune + " from " +
moduleInfo.moduleOutDir + " to " + moduleInfo.moduleOutDir,
);
const files = await new Promise(async (resolve, reject) => {
glob(moduleInfo.cfg.copy, {
nosort: true,
silent: true,
cwd: moduleInfo.moduleBuildDir,
}, (err, files) => {
err ? reject(err) : resolve(files);
});
});
for (const f of files) {
console.log("\t" + f);
const src = path.join(moduleInfo.moduleBuildDir, f);
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await fsProm.copyFile(src, dst);
}
}
}
module.exports = copy;

View File

@@ -1,100 +0,0 @@
/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import fsProm from 'fs/promises';
import childProcess from 'child_process';
import rimraf from 'rimraf';
import glob from 'glob';
import mkdirp from 'mkdirp';
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (moduleInfo.cfg.prune) {
console.log("Removing " + moduleInfo.cfg.prune + " from " + moduleInfo.moduleOutDir);
// rimraf doesn't have a 'cwd' option: it always uses process.cwd()
// (and if you set glob.cwd it just breaks because it can't find the files)
const oldCwd = process.cwd();
try {
process.chdir(moduleInfo.moduleOutDir);
await new Promise<void>((resolve, reject) => {
rimraf(moduleInfo.cfg.prune, {}, err => {
err ? reject(err) : resolve();
});
});
} finally {
process.chdir(oldCwd);
}
}
if (moduleInfo.cfg.copy) {
// If there are multiple moduleBuildDirs, singular moduleBuildDir
// is the same as moduleBuildDirs[0], so we're just listing the contents
// of the first one.
const files = await new Promise<string[]>((resolve, reject) => {
glob(moduleInfo.cfg.copy, {
nosort: true,
silent: true,
cwd: moduleInfo.moduleBuildDir,
}, (err, files) => {
err ? reject(err) : resolve(files);
});
});
if (moduleInfo.moduleBuildDirs.length > 1) {
if (!hakEnv.isMac()) {
console.error(
"You asked me to copy multiple targets but I've only been taught " +
"how to do that on macOS.",
);
throw new Error("Can't copy multiple targets on this platform");
}
for (const f of files) {
const components = moduleInfo.moduleBuildDirs.map(dir => path.join(dir, f));
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await new Promise<void>((resolve, reject) => {
childProcess.execFile('lipo',
['-create', '-output', dst, ...components], (err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}
} else {
console.log(
"Copying files from " +
moduleInfo.moduleBuildDir + " to " + moduleInfo.moduleOutDir,
);
for (const f of files) {
console.log("\t" + f);
const src = path.join(moduleInfo.moduleBuildDir, f);
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await fsProm.copyFile(src, dst);
}
}
}
}

View File

@@ -1,32 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HakEnv from "./hakEnv";
export interface DependencyInfo {
name: string;
version: string;
cfg: Record<string, any>;
moduleHakDir: string;
moduleDotHakDir: string;
moduleTargetDotHakDir: string;
moduleBuildDir: string;
moduleBuildDirs: string[];
moduleOutDir: string;
nodeModuleBinDir: string;
depPrefix: string;
scripts: Record<string, (hakEnv: HakEnv, moduleInfo: DependencyInfo) => Promise<void> >;
}

125
scripts/hak/fetch.js Normal file
View File

@@ -0,0 +1,125 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const path = require('path');
const url = require('url');
const fsProm = require('fs').promises;
const childProcess = require('child_process');
const npm = require('npm');
const semver = require('semver');
const needle = require('needle');
const mkdirp = require('mkdirp');
const tar = require('tar');
async function fetch(hakEnv, moduleInfo) {
let haveModuleBuildDir;
try {
const stats = await fsProm.stat(moduleInfo.moduleBuildDir);
haveModuleBuildDir = stats.isDirectory();
} catch (e) {
haveModuleBuildDir = false;
}
if (haveModuleBuildDir) return;
await new Promise((resolve) => {
npm.load({'loglevel': 'silent'}, resolve);
});
console.log("Fetching " + moduleInfo.name + " at version " + moduleInfo.version);
const versions = await new Promise((resolve, reject) => {
npm.view([
moduleInfo.name + '@' + moduleInfo.version,
'dist.tarball',
(err, versions) => {
if (err) {
reject(err);
} else {
resolve(versions);
}
},
]);
});
const orderedVersions = Object.keys(versions);
semver.sort(orderedVersions);
console.log("Resolved version " + orderedVersions[0] + " for " + moduleInfo.name);
const tarballUrl = versions[orderedVersions[0]]['dist.tarball'];
await mkdirp(moduleInfo.moduleDotHakDir);
const parsedUrl = url.parse(tarballUrl);
const tarballFile = path.join(moduleInfo.moduleDotHakDir, path.basename(parsedUrl.path));
let haveTarball;
try {
await fsProm.stat(tarballFile);
haveTarball = true;
} catch (e) {
haveTarball = false;
}
if (!haveTarball) {
console.log("Downloading " + tarballUrl);
await needle('get', tarballUrl, { output: tarballFile });
} else {
console.log(tarballFile + " already exists.");
}
await mkdirp(moduleInfo.moduleBuildDir);
await tar.x({
file: tarballFile,
cwd: moduleInfo.moduleBuildDir,
strip: 1,
});
console.log("Running yarn install in " + moduleInfo.moduleBuildDir);
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
hakEnv.isWin() ? 'yarn.cmd' : 'yarn',
['install', '--ignore-scripts'],
{
stdio: 'inherit',
cwd: moduleInfo.moduleBuildDir,
},
);
proc.on('exit', code => {
code ? reject(code) : resolve();
});
});
// also extract another copy to the output directory at this point
// nb. we do not yarn install in the output copy: we could install in
// production mode to get only runtime dependencies and not devDependencies,
// but usually native modules come with dependencies that are needed for
// building/fetching the native modules (eg. node-pre-gyp) rather than
// actually used at runtime: we do not want to bundle these into our app.
// We therefore just install no dependencies at all, and accept that any
// actual runtime dependencies will have to be added to the main app's
// dependencies. We can't tell what dependencies are real runtime deps
// and which are just used for native module building.
await mkdirp(moduleInfo.moduleOutDir);
await tar.x({
file: tarballFile,
cwd: moduleInfo.moduleOutDir,
strip: 1,
});
}
module.exports = fetch;

View File

@@ -1,70 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fsProm from 'fs/promises';
import childProcess from 'child_process';
import pacote from 'pacote';
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
let haveModuleBuildDir;
try {
const stats = await fsProm.stat(moduleInfo.moduleBuildDir);
haveModuleBuildDir = stats.isDirectory();
} catch (e) {
haveModuleBuildDir = false;
}
if (haveModuleBuildDir) return;
console.log("Fetching " + moduleInfo.name + "@" + moduleInfo.version);
const packumentCache = new Map();
await pacote.extract(`${moduleInfo.name}@${moduleInfo.version}`, moduleInfo.moduleBuildDir, {
packumentCache,
});
console.log("Running yarn install in " + moduleInfo.moduleBuildDir);
await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(
hakEnv.isWin() ? 'yarn.cmd' : 'yarn',
['install', '--ignore-scripts'],
{
stdio: 'inherit',
cwd: moduleInfo.moduleBuildDir,
},
);
proc.on('exit', code => {
code ? reject(code) : resolve();
});
});
// also extract another copy to the output directory at this point
// nb. we do not yarn install in the output copy: we could install in
// production mode to get only runtime dependencies and not devDependencies,
// but usually native modules come with dependencies that are needed for
// building/fetching the native modules (eg. node-pre-gyp) rather than
// actually used at runtime: we do not want to bundle these into our app.
// We therefore just install no dependencies at all, and accept that any
// actual runtime dependencies will have to be added to the main app's
// dependencies. We can't tell what dependencies are real runtime deps
// and which are just used for native module building.
await pacote.extract(`${moduleInfo.name}@${moduleInfo.version}`, moduleInfo.moduleOutDir, {
packumentCache,
});
}

View File

@@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import mkdirp from 'mkdirp';
const mkdirp = require('mkdirp');
import { DependencyInfo } from './dep';
import HakEnv from './hakEnv';
export default async function fetchDeps(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function fetchDeps(hakEnv, moduleInfo) {
await mkdirp(moduleInfo.moduleDotHakDir);
if (moduleInfo.scripts.fetchDeps) {
await moduleInfo.scripts.fetchDeps(hakEnv, moduleInfo);
}
}
module.exports = fetchDeps;

115
scripts/hak/hakEnv.js Normal file
View File

@@ -0,0 +1,115 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const path = require('path');
const os = require('os');
const nodePreGypVersioning = require('node-pre-gyp/lib/util/versioning');
function getElectronVersion(packageJson) {
// should we pick the version of an installed electron
// dependency, and if so, before or after electronVersion?
if (packageJson.build && packageJson.build.electronVersion) {
return packageJson.build.electronVersion;
}
return null;
}
function getRuntime(packageJson) {
const electronVersion = getElectronVersion(packageJson);
return electronVersion ? 'electron' : 'node-webkit';
}
function getTarget(packageJson) {
const electronVersion = getElectronVersion(packageJson);
if (electronVersion) {
return electronVersion;
} else {
return process.version.substr(1);
}
}
function detectArch() {
if (process.platform === 'win32') {
// vcvarsall.bat (the script that sets up the environment for
// visual studio build tools) sets an env var to tell us what
// architecture the active build tools target, so we auto-detect
// this.
const targetArch = process.env.VSCMD_ARG_TGT_ARCH;
if (targetArch === 'x86') {
return 'ia32';
} else if (targetArch === 'x64') {
return 'x64';
}
}
return process.arch;
}
module.exports = class HakEnv {
constructor(prefix, packageJson) {
Object.assign(this, {
// what we're targeting
runtime: getRuntime(packageJson),
target: getTarget(packageJson),
platform: process.platform,
arch: detectArch(),
// paths
projectRoot: prefix,
dotHakDir: path.join(prefix, '.hak'),
});
}
getRuntimeAbi() {
return nodePreGypVersioning.get_runtime_abi(
this.runtime,
this.target,
);
}
// {node_abi}-{platform}-{arch}
getNodeTriple() {
return this.getRuntimeAbi() + '-' + this.platform + '-' + this.arch;
}
isWin() {
return this.platform === 'win32';
}
isMac() {
return this.platform === 'darwin';
}
isLinux() {
return this.platform === 'linux';
}
makeGypEnv() {
return Object.assign({}, process.env, {
npm_config_target: this.target,
npm_config_arch: this.arch,
npm_config_target_arch: this.arch,
npm_config_disturl: 'https://atom.io/download/electron',
npm_config_runtime: this.runtime,
npm_config_build_from_source: true,
npm_config_devdir: path.join(os.homedir(), ".electron-gyp"),
});
}
getNodeModuleBin(name) {
return path.join(this.projectRoot, 'node_modules', '.bin', name);
}
};

View File

@@ -1,117 +0,0 @@
/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import os from 'os';
import nodePreGypVersioning from "node-pre-gyp/lib/util/versioning";
import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion";
import { Arch, Target, TARGETS, getHost, isHostId, TargetId } from './target';
async function getRuntime(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersion(projectRoot);
return electronVersion ? 'electron' : 'node-webkit';
}
async function getRuntimeVersion(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersion(projectRoot);
if (electronVersion) {
return electronVersion;
} else {
return process.version.substr(1);
}
}
export default class HakEnv {
public target: Target;
public projectRoot: string;
public runtime: string;
public runtimeVersion: string;
public dotHakDir: string;
constructor(prefix: string, targetId: TargetId) {
let target;
if (targetId) {
target = TARGETS[targetId];
} else {
target = getHost();
}
if (!target) {
throw new Error(`Unknown target ${targetId}!`);
}
this.target = target;
this.projectRoot = prefix;
this.dotHakDir = path.join(this.projectRoot, '.hak');
}
async init() {
this.runtime = await getRuntime(this.projectRoot);
this.runtimeVersion = await getRuntimeVersion(this.projectRoot);
}
getRuntimeAbi(): string {
return nodePreGypVersioning.get_runtime_abi(
this.runtime,
this.runtimeVersion,
);
}
// {node_abi}-{platform}-{arch}
getNodeTriple(): string {
return this.getRuntimeAbi() + '-' + this.target.platform + '-' + this.target.arch;
}
getTargetId(): TargetId {
return this.target.id;
}
isWin(): boolean {
return this.target.platform === 'win32';
}
isMac(): boolean {
return this.target.platform === 'darwin';
}
isLinux(): boolean {
return this.target.platform === 'linux';
}
getTargetArch(): Arch {
return this.target.arch;
}
isHost(): boolean {
return isHostId(this.target.id);
}
makeGypEnv(): Record<string, string> {
return Object.assign({}, process.env, {
npm_config_arch: this.target.arch,
npm_config_target_arch: this.target.arch,
npm_config_disturl: 'https://atom.io/download/electron',
npm_config_runtime: this.runtime,
npm_config_target: this.runtimeVersion,
npm_config_build_from_source: true,
npm_config_devdir: path.join(os.homedir(), ".electron-gyp"),
});
}
getNodeModuleBin(name: string): string {
return path.join(this.projectRoot, 'node_modules', '.bin', name);
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import findNpmPrefix from 'find-npm-prefix';
const path = require('path');
import HakEnv from './hakEnv';
import { TargetId } from './target';
import { DependencyInfo } from './dep';
const findNpmPrefix = require('find-npm-prefix');
const HakEnv = require('./hakEnv');
const GENERALCOMMANDS = [
'target',
@@ -36,13 +35,6 @@ const MODULECOMMANDS = [
'clean',
];
// Shortcuts for multiple commands at once (useful for building universal binaries
// because you can run the fetch/fetchDeps/build for each arch and then copy/link once)
const METACOMMANDS = {
'fetchandbuild': ['check', 'fetch', 'fetchDeps', 'build'],
'copyandlink': ['copy', 'link'],
};
// Scripts valid in a hak.json 'scripts' section
const HAKSCRIPTS = [
'check',
@@ -61,30 +53,9 @@ async function main() {
process.exit(1);
}
const targetIds = [] as TargetId[];
// Apply `--target <target>` option if specified
// Can be specified multiple times for the copy command to bundle
// multiple archs into a single universal output module)
while (true) { // eslint-disable-line no-constant-condition
const targetIndex = process.argv.indexOf('--target');
if (targetIndex === -1) break;
const hakEnv = new HakEnv(prefix, packageJson);
if ((targetIndex + 1) >= process.argv.length) {
console.error("--target option specified without a target");
process.exit(1);
}
// Extract target ID and remove from args
targetIds.push(process.argv.splice(targetIndex, 2)[1] as TargetId);
}
const hakEnvs = targetIds.map(tid => new HakEnv(prefix, tid));
if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, null));
for (const h of hakEnvs) {
await h.init();
}
const hakEnv = hakEnvs[0];
const deps = {} as Record<string, DependencyInfo>;
const deps = {};
const hakDepsCfg = packageJson.hakDependencies || {};
@@ -104,23 +75,16 @@ async function main() {
cfg: hakJson,
moduleHakDir: path.join(prefix, 'hak', dep),
moduleDotHakDir: path.join(hakEnv.dotHakDir, dep),
moduleTargetDotHakDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId()),
moduleBuildDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'build'),
moduleBuildDirs: hakEnvs.map(h => path.join(h.dotHakDir, dep, h.getTargetId(), 'build')),
moduleBuildDir: path.join(hakEnv.dotHakDir, dep, 'build'),
moduleOutDir: path.join(hakEnv.dotHakDir, 'hakModules', dep),
nodeModuleBinDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'build', 'node_modules', '.bin'),
depPrefix: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'opt'),
nodeModuleBinDir: path.join(hakEnv.dotHakDir, dep, 'build', 'node_modules', '.bin'),
depPrefix: path.join(hakEnv.dotHakDir, dep, 'opt'),
scripts: {},
};
for (const s of HAKSCRIPTS) {
if (hakJson.scripts && hakJson.scripts[s]) {
const scriptModule = await import(path.join(prefix, 'hak', dep, hakJson.scripts[s]));
if (scriptModule.__esModule) {
deps[dep].scripts[s] = scriptModule.default;
} else {
deps[dep].scripts[s] = scriptModule;
}
deps[dep].scripts[s] = require(path.join(prefix, 'hak', dep, hakJson.scripts[s]));
}
}
}
@@ -128,18 +92,10 @@ async function main() {
let cmds;
if (process.argv.length < 3) {
cmds = ['check', 'fetch', 'fetchDeps', 'build', 'copy', 'link'];
} else if (METACOMMANDS[process.argv[2]]) {
cmds = METACOMMANDS[process.argv[2]];
} else {
cmds = [process.argv[2]];
}
if (hakEnvs.length > 1 && cmds.some(c => !['copy', 'link'].includes(c))) {
// We allow link here too for convenience because it's completely arch independent
console.error("Multiple targets only supported with the copy command");
return;
}
let modules = process.argv.slice(3);
if (modules.length === 0) modules = Object.keys(deps);
@@ -160,7 +116,7 @@ async function main() {
process.exit(1);
}
const cmdFunc = (await import('./' + cmd)).default;
const cmdFunc = require('./' + cmd);
for (const mod of modules) {
const depInfo = deps[mod];
@@ -177,7 +133,4 @@ async function main() {
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});
main().catch(() => process.exit(1));

View File

@@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import os from 'os';
import fsProm from 'fs/promises';
import childProcess from 'child_process';
const path = require('path');
const os = require('os');
const fsProm = require('fs').promises;
const childProcess = require('child_process');
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function link(hakEnv, moduleInfo) {
const yarnrc = path.join(hakEnv.projectRoot, '.yarnrc');
// this is fairly terrible but it's reasonably clunky to either parse a yarnrc
// properly or get yarn to do it, so this will probably suffice for now.
@@ -49,7 +46,7 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
const yarnCmd = 'yarn' + (hakEnv.isWin() ? '.cmd' : '');
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link'], {
cwd: moduleInfo.moduleOutDir,
stdio: 'inherit',
@@ -59,7 +56,7 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link', moduleInfo.name], {
cwd: hakEnv.projectRoot,
stdio: 'inherit',
@@ -69,3 +66,5 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
}
module.exports = link;

View File

@@ -1,126 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// We borrow Rust's target naming scheme as a way of expressing all target
// details in a single string.
// See https://doc.rust-lang.org/rustc/platform-support.html.
export type TargetId =
'aarch64-apple-darwin' |
'x86_64-apple-darwin' |
'universal-apple-darwin' |
'i686-pc-windows-msvc' |
'x86_64-pc-windows-msvc' |
'x86_64-unknown-linux-gnu';
// Values are expected to match those used in `process.platform`.
export type Platform = 'darwin' | 'linux' | 'win32';
// Values are expected to match those used in `process.arch`.
export type Arch = 'arm64' | 'ia32' | 'x64' | 'universal';
// Values are expected to match those used by Visual Studio's `vcvarsall.bat`.
// See https://docs.microsoft.com/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax
export type VcVarsArch = 'amd64' | 'arm64' | 'x86';
export type Target = {
id: TargetId;
platform: Platform;
arch: Arch;
};
export type WindowsTarget = Target & {
platform: 'win32';
vcVarsArch: VcVarsArch;
};
export type UniversalTarget = Target & {
arch: 'universal';
subtargets: Target[];
};
const aarch64AppleDarwin: Target = {
id: 'aarch64-apple-darwin',
platform: 'darwin',
arch: 'arm64',
};
const x8664AppleDarwin: Target = {
id: 'x86_64-apple-darwin',
platform: 'darwin',
arch: 'x64',
};
const universalAppleDarwin: UniversalTarget = {
id: 'universal-apple-darwin',
platform: 'darwin',
arch: 'universal',
subtargets: [
aarch64AppleDarwin,
x8664AppleDarwin,
],
};
const i686PcWindowsMsvc: WindowsTarget = {
id: 'i686-pc-windows-msvc',
platform: 'win32',
arch: 'ia32',
vcVarsArch: 'x86',
};
const x8664PcWindowsMsvc: WindowsTarget = {
id: 'x86_64-pc-windows-msvc',
platform: 'win32',
arch: 'x64',
vcVarsArch: 'amd64',
};
const x8664UnknownLinuxGnu: Target = {
id: 'x86_64-unknown-linux-gnu',
platform: 'linux',
arch: 'x64',
};
export const TARGETS: Record<TargetId, Target> = {
'aarch64-apple-darwin': aarch64AppleDarwin,
'x86_64-apple-darwin': x8664AppleDarwin,
'universal-apple-darwin': universalAppleDarwin,
'i686-pc-windows-msvc': i686PcWindowsMsvc,
'x86_64-pc-windows-msvc': x8664PcWindowsMsvc,
'x86_64-unknown-linux-gnu': x8664UnknownLinuxGnu,
};
// The set of targets we build by default, sorted by increasing complexity so
// that we fail fast when the native host target fails.
export const ENABLED_TARGETS: Target[] = [
TARGETS['universal-apple-darwin'],
TARGETS['x86_64-unknown-linux-gnu'],
TARGETS['x86_64-pc-windows-msvc'],
];
export function getHost(): Target {
return Object.values(TARGETS).find(target => (
target.platform === process.platform &&
target.arch === process.arch
));
}
export function isHostId(id: TargetId): boolean {
return getHost()?.id === id;
}
export function isHost(target: Target): boolean {
return getHost()?.id === target.id;
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2017",
"module": "commonjs",
"sourceMap": false,
"lib": [
"es2019",
]
},
"include": [
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -7,9 +7,8 @@ if [ $? != 0 ]; then
fi
# Taken from https://www.electron.build/multi-platform-build#docker
# Pass through any vars prefixed with INDOCKER_, removing the prefix
docker run --rm -ti \
--env-file <(env | grep -E '^INDOCKER_' | sed -e 's/^INDOCKER_//') \
--env-file <(env | grep -iE '^BUILDKITE_API_KEY=') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \

View File

@@ -52,4 +52,4 @@ if (require.main === module) {
main(process.argv.slice(2)).then((ret) => process.exit(ret));
}
module.exports = { versionFromAsar, setPackageVersion };
module.exports = {versionFromAsar, setPackageVersion};

View File

@@ -1,26 +0,0 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { BrowserWindow } from "electron";
declare global {
namespace NodeJS {
interface Global {
mainWindow: BrowserWindow;
appQuitting: boolean;
}
}
}

View File

@@ -17,42 +17,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks";
import {
app,
ipcMain,
powerSaveBlocker,
BrowserWindow,
Menu,
autoUpdater,
protocol,
dialog,
desktopCapturer,
} from "electron";
import AutoLaunch from "auto-launch";
import path from "path";
import windowStateKeeper from 'electron-window-state';
import Store from 'electron-store';
import fs, { promises as afs } from "fs";
import crypto from "crypto";
import { URL } from "url";
import minimist from "minimist";
// Squirrel on windows starts the app with various flags
// as hooks to tell us when we've been installed/uninstalled
// etc.
const checkSquirrelHooks = require('./squirrelhooks');
if (checkSquirrelHooks()) return;
import * as tray from "./tray";
import { buildMenuTemplate } from './vectormenu';
import webContentsHandler from './webcontents-handler';
import * as updater from './updater';
import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol';
import { _t, AppLocalization } from './language-helper';
const argv = minimist(process.argv, {
alias: { help: "h" },
const argv = require('minimist')(process.argv, {
alias: {help: "h"},
});
const {
app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol, dialog,
} = require('electron');
const AutoLaunch = require('auto-launch');
const path = require('path');
const tray = require('./tray');
const vectorMenu = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler');
const updater = require('./updater');
const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol');
const windowStateKeeper = require('electron-window-state');
const Store = require('electron-store');
const fs = require('fs');
const afs = fs.promises;
const crypto = require('crypto');
let keytar;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
@@ -67,8 +62,9 @@ let Seshat;
let SeshatRecovery;
let ReindexError;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
@@ -90,7 +86,6 @@ let vectorConfig;
let iconPath;
let trayConfig;
let launcher;
let appLocalization;
if (argv["help"]) {
console.log("Options:");
@@ -185,7 +180,6 @@ async function setupGlobals() {
]);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
vectorConfig = require(asarPath + 'config.json');
} catch (e) {
// it would be nice to check the error code here and bail if the config
@@ -197,7 +191,6 @@ async function setupGlobals() {
try {
// Load local config and use it to override values from the one baked with the build
// eslint-disable-next-line @typescript-eslint/no-var-requires
const localConfig = require(path.join(app.getPath('userData'), 'config.json'));
// If the local config has a homeserver defined, don't use the homeserver from the build
@@ -213,16 +206,6 @@ async function setupGlobals() {
vectorConfig = Object.assign(vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
dialog.showMessageBox({
type: "error",
title: `Your ${vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${vectorConfig.brand || 'Element'}.`,
detail: e.message || "",
});
}
// Could not load local config, this is expected in most cases.
}
@@ -265,13 +248,7 @@ async function moveAutoLauncher() {
}
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
const store = new Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
}>({ name: "electron-config" });
const store = new Store({ name: "electron-config" });
let eventIndex = null;
@@ -279,21 +256,20 @@ let mainWindow = null;
global.appQuitting = false;
const exitShortcuts = [
(input, platform) => platform !== 'darwin' && input.alt && input.key.toUpperCase() === 'F4',
(input, platform) => platform !== 'darwin' && input.control && input.key.toUpperCase() === 'Q',
(input, platform) => platform === 'darwin' && input.meta && input.key.toUpperCase() === 'Q',
(input, platform) => platform !== 'darwin' && input.alt && input.code === 'F4',
(input, platform) => platform !== 'darwin' && input.control && input.code === 'KeyQ',
(input, platform) => platform === 'darwin' && input.meta && input.code === 'KeyQ',
];
const warnBeforeExit = (event, input) => {
const shouldWarnBeforeExit = store.get('warnBeforeExit', true);
const exitShortcutPressed =
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
const exitShortcutPressed = exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, {
type: "question",
buttons: [_t("Cancel"), _t("Close Element")],
message: _t("Are you sure you want to quit?"),
buttons: ["Cancel", "Close Element"],
message: "Are you sure you want to quit?",
defaultId: 1,
cancelId: 0,
}) === 0;
@@ -335,12 +311,7 @@ process.on('uncaughtException', function(error) {
let focusHandlerAttached = false;
ipcMain.on('setBadgeCount', function(ev, count) {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
app.badgeCount = count;
if (count === 0 && mainWindow) {
mainWindow.flashFrame(false);
}
@@ -395,9 +366,6 @@ ipcMain.on('ipcCall', async function(ev, payload) {
launcher.disable();
}
break;
case 'setLanguage':
appLocalization.setAppLocale(args[0]);
break;
case 'shouldWarnBeforeExit':
ret = store.get('warnBeforeExit', true);
break;
@@ -512,13 +480,6 @@ ipcMain.on('ipcCall', async function(ev, payload) {
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
case 'getDesktopCapturerSources':
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
break;
default:
mainWindow.webContents.send('ipcReply', {
@@ -534,26 +495,6 @@ ipcMain.on('ipcCall', async function(ev, payload) {
});
});
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key) {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
} else {
return seshatDefaultPassphrase;
}
}
ipcMain.on('seshat', async function(ev, payload) {
if (!mainWindow) return;
@@ -582,38 +523,82 @@ ipcMain.on('seshat', async function(ev, payload) {
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
let changePassphrase = false;
let passphrase = seshatDefaultPassphrase;
if (keytar) {
try {
// Try to get a passphrase for seshat.
const storedPassphrase = await keytar.getPassword("element.io", passphraseKey);
// If no passphrase was found mark that we should change
// it, if one is found, use that one.
if (storedPassphrase === null) {
changePassphrase = true;
} else {
passphrase = storedPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
}
const openSeshat = async () => {
try {
await afs.mkdir(eventStorePath, {recursive: true});
return new Seshat(eventStorePath, {passphrase});
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
return new Seshat(eventStorePath, {passphrase});
} else {
throw (e);
}
}
};
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
eventIndex = await openSeshat();
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
sendError(payload.id, e);
return;
}
const userVersion = await recoveryIndex.getUserVersion();
if (changePassphrase) {
try {
// Generate a new random passphrase.
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", passphraseKey, newPassphrase);
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
// Set the new passphrase, this will close the event
// index.
await eventIndex.changePassphrase(newPassphrase);
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
// Re-open the event index with the new passphrase.
eventIndex = new Seshat(eventStorePath, {newPassphrase});
} catch (e) {
sendError(payload.id, e);
return;
}
@@ -802,9 +787,6 @@ ipcMain.on('seshat', async function(ev, payload) {
});
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
if (!app.commandLine.hasSwitch('enable-features')) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
}
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
@@ -840,9 +822,6 @@ protocol.registerSchemesAsPrivileged([{
// transition into the user's browser.
app.enableSandbox();
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
app.on('ready', async () => {
try {
await setupGlobals();
@@ -859,7 +838,6 @@ app.on('ready', async () => {
if (argv['devtools']) {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require('electron-devtools-installer');
installExt(REACT_DEVELOPER_TOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
@@ -874,17 +852,17 @@ app.on('ready', async () => {
protocol.registerFileProtocol('vector', (request, callback) => {
if (request.method !== 'GET') {
callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
callback({error: -322}); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
return null;
}
const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== 'vector:') {
callback({ error: -302 }); // UNKNOWN_URL_SCHEME
callback({error: -302}); // UNKNOWN_URL_SCHEME
return;
}
if (parsedUrl.host !== 'vector') {
callback({ error: -105 }); // NAME_NOT_RESOLVED
callback({error: -105}); // NAME_NOT_RESOLVED
return;
}
@@ -892,7 +870,7 @@ app.on('ready', async () => {
// path starts with a '/'
if (target[0] !== '') {
callback({ error: -6 }); // FILE_NOT_FOUND
callback({error: -6}); // FILE_NOT_FOUND
return;
}
@@ -904,7 +882,7 @@ app.on('ready', async () => {
if (target[1] === 'webapp') {
baseDir = asarPath;
} else {
callback({ error: -6 }); // FILE_NOT_FOUND
callback({error: -6}); // FILE_NOT_FOUND
return;
}
@@ -914,7 +892,7 @@ app.on('ready', async () => {
const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith('..')) {
callback({ error: -6 }); // FILE_NOT_FOUND
callback({error: -6}); // FILE_NOT_FOUND
return;
}
const absTarget = path.join(baseDir, relTarget);
@@ -956,11 +934,13 @@ app.on('ready', async () => {
preload: preloadScript,
nodeIntegration: false,
//sandbox: true, // We enable sandboxing from app.enableSandbox() above
enableRemoteModule: false,
contextIsolation: true,
webgl: true,
webgl: false,
},
});
mainWindow.loadURL('vector://vector/webapp/');
Menu.setApplicationMenu(vectorMenu);
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
@@ -992,15 +972,7 @@ app.on('ready', async () => {
// (this is generally how single-window Mac apps
// behave, eg. Mail.app)
e.preventDefault();
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => mainWindow.hide());
mainWindow.setFullScreen(false);
} else {
mainWindow.hide();
}
mainWindow.hide();
return false;
}
});
@@ -1017,14 +989,6 @@ app.on('ready', async () => {
}
webContentsHandler(mainWindow.webContents);
appLocalization = new AppLocalization({
store,
components: [
() => tray.initApplicationMenu(),
() => Menu.setApplicationMenu(buildMenuTemplate()),
],
});
});
app.on('window-all-closed', () => {
@@ -1043,7 +1007,7 @@ function beforeQuit() {
}
app.on('before-quit', beforeQuit);
autoUpdater.on('before-quit-for-update', beforeQuit);
app.on('before-quit-for-update', beforeQuit);
app.on('second-instance', (ev, commandLine, workingDirectory) => {
// If other instance launched with --hidden then skip showing window

View File

@@ -1,46 +0,0 @@
{
"File": "ملف",
"Close": "أغلِق",
"Actual Size": "المقاس الفعلي",
"View": "منظور",
"Select All": "حدّد الكل",
"Delete": "احذف",
"Copy": "انسخ",
"Edit": "تحرير",
"Close Element": "أغلِق Element",
"Cancel": "ألغِ",
"Bring All to Front": "ضَع الكل في المقدّمة",
"Speech": "نطق",
"Add to dictionary": "أضِف إلى القاموس",
"The image failed to save": "فشل حفظ الصورة",
"Failed to save image": "فشل حفظ الصورة",
"Save image as...": "احفظ الصورة كَ‍...",
"Copy link address": "انسخ عنوان الرابط",
"Copy email address": "انسخ عنوان البريد الإلكتروني",
"Copy image": "انسخ الصورة",
"Zoom": "تقريب",
"Stop Speaking": "أوقِف النطق",
"Start Speaking": "ابدأ النطق",
"Unhide": "اعرض",
"Hide Others": "أخفِ البقية",
"Hide": "أخفِ",
"Services": "الخدمات",
"About": "عن",
"Element Help": "مساعدة Element",
"Help": "مساعدة",
"Minimize": "صغّر",
"Window": "نافذة",
"Toggle Developer Tools": "فعّل/عطّل أدوات المطوّرين",
"Toggle Full Screen": "فعّل/عطّل ملء الشاشة",
"Preferences": "التفضيلات",
"Zoom In": "قرّب",
"Zoom Out": "بعّد",
"Paste and Match Style": "ألصِق وطابِق النمط",
"Paste": "ألصِق",
"Cut": "قصّ",
"Redo": "أعِد",
"Undo": "تراجَع",
"Quit": "غادِر",
"Show/Hide": "اعرض/أخفِ",
"Are you sure you want to quit?": "أمتأكّد من الإغلاق؟"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Дадаць у слоўнік",
"The image failed to save": "Не атрымалася захаваць малюнак",
"Failed to save image": "Не атрымалася захаваць малюнак",
"Save image as...": "Захаваць малюнак як...",
"Copy link address": "Скапіраваць спасылку",
"Copy email address": "Скапіраваць адрас пошты",
"Copy image": "Скапіраваць малюнак",
"File": "Файл",
"Bring All to Front": "Вынесці ўсё наперад",
"Zoom": "Маштаб",
"Stop Speaking": "Перастаць гаварыць",
"Start Speaking": "Гаварыць",
"Speech": "Голас",
"Unhide": "Паказаць",
"Hide Others": "Схаваць іншыя",
"Hide": "Схаваць",
"Services": "Сервісы",
"About": "Аб праграме",
"Element Help": "Даведка Element",
"Help": "Даведка",
"Close": "Зачыніць",
"Minimize": "Згарнуць",
"Window": "Акно",
"Toggle Developer Tools": "Пераключэнне інструментаў распрацоўніка",
"Toggle Full Screen": "Пераключэнне на ўвесь экран",
"Preferences": "Параметры",
"Zoom Out": "Паменшыць",
"Zoom In": "Павялічыць",
"Actual Size": "Фактычны Памер",
"View": "Прагляд",
"Select All": "Выбраць усё",
"Delete": "Выдаліць",
"Paste and Match Style": "Уставіць і супаставіць стыль",
"Paste": "Уставіць",
"Copy": "Капіяваць",
"Cut": "Выразаць",
"Redo": "Паўтарыць",
"Undo": "Адмяніць",
"Edit": "Змяніць",
"Quit": "Выйсці",
"Show/Hide": "Паказаць / схаваць",
"Are you sure you want to quit?": "Вы ўпэўненыя, што хочаце выйсці?",
"Close Element": "Зачыніць Element",
"Cancel": "Адмена"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Afegeix al diccionari",
"The image failed to save": "S'ha fallat en desar la imatge",
"Failed to save image": "S'ha fallat en desar la imatge",
"Save image as...": "Anomena i desa la imatge...",
"Copy link address": "Copia l'adreça de l'enllaç",
"Copy email address": "Copia l'adreça de correu electrònic",
"Copy image": "Copia la imatge",
"File": "Fitxer",
"Bring All to Front": "Porta-ho tot al davant",
"Zoom": "Escala",
"Stop Speaking": "Para la veu",
"Start Speaking": "Comença la veu",
"Speech": "Veu",
"Unhide": "Deixa d'amagar",
"Hide Others": "Amaga les altres",
"Hide": "Amaga",
"Services": "Serveis",
"About": "Quant a",
"Element Help": "Ajuda sobre l'Element",
"Help": "Ajuda",
"Close": "Tanca",
"Minimize": "Minimitza",
"Window": "Finestra",
"Toggle Developer Tools": "Commuta les eines per a desenvolupadors",
"Toggle Full Screen": "Commuta la pantalla completa",
"Preferences": "Preferències",
"Zoom Out": "Allunya",
"Zoom In": "Apropia",
"Actual Size": "Mida real",
"View": "Visualitza",
"Select All": "Selecciona-ho tot",
"Delete": "Suprimeix",
"Paste and Match Style": "Enganxa i fes coincidir l'estil",
"Paste": "Enganxa",
"Copy": "Copia",
"Cut": "Retalla",
"Redo": "Refés",
"Undo": "Desfés",
"Edit": "Edita",
"Quit": "Surt",
"Show/Hide": "Mostra/Amaga",
"Are you sure you want to quit?": "Esteu segur que voleu sortir?",
"Close Element": "Tanca l'Element",
"Cancel": "Cancel·la"
}

View File

@@ -1,46 +0,0 @@
{
"Speech": "Sprache",
"Paste and Match Style": "Einfügen und Formatierung beibehalten",
"Stop Speaking": "Aufnahme beenden",
"Start Speaking": "Aufnahme starten",
"Services": "Dienste",
"Are you sure you want to quit?": "Wirklich beenden?",
"Add to dictionary": "Wörterbuch hinzufügen",
"The image failed to save": "Das Bild konnte nicht gespeichert werden",
"Failed to save image": "Bild kann nicht gespeichert werden",
"Save image as...": "Bild speichern unter...",
"Copy link address": "Link-Adresse kopieren",
"Copy email address": "Email-Adresse kopieren",
"Copy image": "Bild kopieren",
"File": "Datei",
"Bring All to Front": "Alles in den Vordergrund",
"Zoom": "Zoom",
"Unhide": "Wieder anzeigen",
"Hide Others": "Andere verstecken",
"Hide": "Verstecken",
"About": "Über",
"Element Help": "Hilfe zu Element",
"Help": "Hilfe",
"Close": "Schließen",
"Minimize": "Minimieren",
"Window": "Fenster",
"Toggle Developer Tools": "Developer-Tools an/aus",
"Toggle Full Screen": "Vollbildschirm an/aus",
"Preferences": "Einstellungen",
"Zoom Out": "Verkleinern",
"Zoom In": "Vergrößern",
"Actual Size": "Tatsächliche Größe",
"View": "Ansicht",
"Select All": "Alles auswählen",
"Delete": "Löschen",
"Paste": "Einfügen",
"Copy": "Kopieren",
"Cut": "Ausschneiden",
"Redo": "Wiederherstellen",
"Undo": "Rückgängig",
"Edit": "Bearbeiten",
"Quit": "Beenden",
"Show/Hide": "Anzeigen/Ausblenden",
"Close Element": "Element schließen",
"Cancel": "Abbrechen"
}

View File

@@ -1,46 +0,0 @@
{
"Cancel": "Cancel",
"Close Element": "Close Element",
"Are you sure you want to quit?": "Are you sure you want to quit?",
"Show/Hide": "Show/Hide",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Delete": "Delete",
"Select All": "Select All",
"View": "View",
"Actual Size": "Actual Size",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Preferences": "Preferences",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Developer Tools": "Toggle Developer Tools",
"Window": "Window",
"Minimize": "Minimize",
"Close": "Close",
"Help": "Help",
"Element Help": "Element Help",
"About": "About",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"Speech": "Speech",
"Start Speaking": "Start Speaking",
"Stop Speaking": "Stop Speaking",
"Zoom": "Zoom",
"Bring All to Front": "Bring All to Front",
"File": "File",
"Copy image": "Copy image",
"Copy email address": "Copy email address",
"Copy link address": "Copy link address",
"Save image as...": "Save image as...",
"Failed to save image": "Failed to save image",
"The image failed to save": "The image failed to save",
"Add to dictionary": "Add to dictionary"
}

View File

@@ -1,46 +0,0 @@
{
"Close": "Close",
"Add to dictionary": "Add to dictionary",
"The image failed to save": "The image failed to save",
"Failed to save image": "Failed to save image",
"Save image as...": "Save image as...",
"Copy link address": "Copy link address",
"Copy email address": "Copy email address",
"Copy image": "Copy image",
"File": "File",
"Bring All to Front": "Bring All to Front",
"Zoom": "Zoom",
"Stop Speaking": "Stop Speaking",
"Start Speaking": "Start Speaking",
"Speech": "Speech",
"Unhide": "Unhide",
"Hide Others": "Hide Others",
"Hide": "Hide",
"Services": "Services",
"About": "About",
"Element Help": "Element Help",
"Help": "Help",
"Minimize": "Minimize",
"Window": "Window",
"Toggle Developer Tools": "Toggle Developer Tools",
"Toggle Full Screen": "Toggle Full Screen",
"Preferences": "Preferences",
"Zoom Out": "Zoom Out",
"Zoom In": "Zoom In",
"Actual Size": "Actual Size",
"View": "View",
"Select All": "Select All",
"Delete": "Delete",
"Paste and Match Style": "Paste and Match Style",
"Paste": "Paste",
"Copy": "Copy",
"Cut": "Cut",
"Redo": "Redo",
"Undo": "Undo",
"Edit": "Edit",
"Quit": "Quit",
"Show/Hide": "Show/Hide",
"Are you sure you want to quit?": "Are you sure you want to quit?",
"Close Element": "Close Element",
"Cancel": "Cancel"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Añadir al diccionario",
"The image failed to save": "La imagen no se ha podido guardar",
"Failed to save image": "No se ha podido guardar la imagen",
"Save image as...": "Guardar imagen como...",
"Copy link address": "Copiar dirección de enlace",
"Copy email address": "Copiar dirección de correo",
"Copy image": "Copiar imagen",
"File": "Archivo",
"Bring All to Front": "Traer todas al primer plano",
"Zoom": "Zoom",
"Start Speaking": "Empezar a hablar",
"Stop Speaking": "Parar de hablar",
"Speech": "Dictado",
"Unhide": "Mostrar",
"Hide Others": "Ocultar otros",
"Hide": "Ocultar",
"Services": "Servicios",
"About": "Acerca de",
"Element Help": "Ayuda de Element",
"Help": "Ayuda",
"Close": "Cerrar",
"Minimize": "Minimizar",
"Window": "Ventana",
"Toggle Developer Tools": "Abrir/cerrar herramientas de desarrollo",
"Toggle Full Screen": "Entrar/salir de pantalla completa",
"Preferences": "Preferencias",
"Zoom Out": "Alejar",
"Zoom In": "Acercar",
"Actual Size": "Tamaño real",
"View": "Ver",
"Select All": "Seleccionar todo",
"Delete": "Eliminar",
"Paste and Match Style": "Pegar manteniendo estilo",
"Paste": "Pegar",
"Copy": "Copiar",
"Cut": "Cortar",
"Redo": "Rehacer",
"Undo": "Deshacer",
"Edit": "Editar",
"Quit": "Salir",
"Show/Hide": "Ver/Ocultar",
"Are you sure you want to quit?": "¿Quieres salir?",
"Close Element": "Cerrar Element",
"Cancel": "Cancelar"
}

View File

@@ -1,46 +0,0 @@
{
"Element Help": "Rakenduse Element abiteave",
"About": "Rakenduse teave",
"The image failed to save": "Seda pilti ei õnnestunud salvestada",
"Add to dictionary": "Lisa sõnastikku",
"Failed to save image": "Pildi salvestamine ei õnnestunud",
"Save image as...": "Salvesta pilt kui...",
"Copy link address": "Kopeeri lingi aadress",
"Copy email address": "Kopeeri e-posti aadress",
"Copy image": "Kopeeri pilt",
"File": "Fail",
"Bring All to Front": "Too kõik esiplaanile",
"Zoom": "Suumi",
"Stop Speaking": "Lõpeta rääkimine",
"Start Speaking": "Alusta rääkimist",
"Speech": "Kõne",
"Unhide": "Näita uuesti",
"Hide Others": "Peida muud",
"Hide": "Peida",
"Services": "Teenused",
"Help": "Abiteave",
"Close": "Sulge",
"Minimize": "Vähenda",
"Window": "Aken",
"Toggle Developer Tools": "Arendaja töövahendid sisse/välja",
"Toggle Full Screen": "Täisekraanivaade sisse/välja",
"Preferences": "Seadistused",
"Zoom Out": "Vähenda",
"Zoom In": "Suurenda",
"Actual Size": "Näita tavasuuruses",
"View": "Vaata",
"Select All": "Vali kõik",
"Delete": "Kustuta",
"Paste and Match Style": "Aseta kasutades sama stiili",
"Paste": "Aseta",
"Copy": "Kopeeri",
"Cut": "Lõika",
"Redo": "Tee uuesti",
"Undo": "Võta tagasi",
"Edit": "Muuda",
"Quit": "Välju",
"Show/Hide": "Näita/peida",
"Are you sure you want to quit?": "Kas sa kindlasti soovid rakendusest väljuda?",
"Close Element": "Sulge Element",
"Cancel": "Tühista"
}

View File

@@ -1,46 +0,0 @@
{
"Paste": "Liitä",
"Paste and Match Style": "Liitä ja sovita tyyli",
"Add to dictionary": "Lisää sanakirjaan",
"The image failed to save": "Kuvan tallennus epäonnistui",
"Failed to save image": "Kuvan tallennus epäonnistui",
"Save image as...": "Tallenna kuva nimellä...",
"Copy link address": "Kopioi linkin osoite",
"Copy email address": "Kopioi sähköpostiosoite",
"Copy image": "Kopioi kuva",
"File": "Tiedosto",
"Bring All to Front": "Tuo kaikki eteen",
"Zoom": "Suurennus",
"Stop Speaking": "Lopeta puhe",
"Start Speaking": "Aloita puhe",
"Speech": "Puhe",
"Unhide": "Palauta näkyviin",
"Hide Others": "Piilota muut",
"Hide": "Piilota",
"Services": "Palvelut",
"About": "Tietoja",
"Element Help": "Elementin ohjeet",
"Help": "Apua",
"Close": "Sulje",
"Minimize": "Pienennä",
"Window": "Ikkuna",
"Toggle Developer Tools": "Näytä tai piilota kehittäjätyökalut",
"Toggle Full Screen": "Vaihda koko näytön tilaa",
"Preferences": "Asetukset",
"Zoom Out": "Pienennä",
"Zoom In": "Suurenna",
"Actual Size": "Alkuperäinen koko",
"View": "Näytä",
"Select All": "Valitse kaikki",
"Delete": "Poista",
"Copy": "Kopioi",
"Cut": "Leikkaa",
"Redo": "Tee uudestaan",
"Undo": "Peru",
"Edit": "Muokkaa",
"Quit": "Lopeta",
"Show/Hide": "Näytä/piilota",
"Are you sure you want to quit?": "Haluatko varmasti poistua?",
"Close Element": "Sulje Element",
"Cancel": "Peruuta"
}

View File

@@ -1,45 +0,0 @@
{
"Undo": "Annuler",
"Edit": "Modifier",
"Quit": "Quitter",
"Show/Hide": "Afficher/Masquer",
"Are you sure you want to quit?": "Êtes-vous sûr de vouloir quitter ?",
"Close Element": "Fermer Element",
"Cancel": "Annuler",
"Unhide": "Dé-masquer",
"Hide Others": "Masquer les autres",
"Hide": "Masquer",
"Services": "Services",
"About": "À propos",
"Element Help": "Aide dElement",
"Help": "Aide",
"Close": "Fermer",
"Minimize": "Minimiser",
"Window": "Fenêtre",
"Toggle Developer Tools": "Basculer les outils de développement",
"Toggle Full Screen": "Basculer le plein écran",
"Preferences": "Préférences",
"Zoom Out": "Dé-zoomer",
"Zoom In": "Zoomer",
"Actual Size": "Taille réelle",
"View": "Afficher",
"Select All": "Tout sélectionner",
"Delete": "Supprimer",
"Paste and Match Style": "Copier avec le style de destination",
"Paste": "Coller",
"Copy": "Copier",
"Cut": "Couper",
"Speech": "Dictée",
"Add to dictionary": "Ajouter au dictionnaire",
"The image failed to save": "Limage na pas pu être sauvegardée",
"Failed to save image": "Échec de la sauvegarde de limage",
"Save image as...": "Enregistrer limage sous…",
"Copy link address": "Copier ladresse du lien",
"Copy email address": "Copier ladresse e-mail",
"Copy image": "Copier limage",
"File": "Fichier",
"Bring All to Front": "Tout amener au premier plan",
"Zoom": "Zoom",
"Stop Speaking": "Arrêter la dictée",
"Start Speaking": "Commencer la dictée"
}

View File

@@ -1,46 +0,0 @@
{
"Copy image": "Ofbylding kopiearje",
"Speech": "Spraak",
"View": "Byld",
"Paste and Match Style": "Plakke en lit stilen oerienkomme",
"Add to dictionary": "Oan wurdlist tafoegje",
"The image failed to save": "It is net slagge de ôfbylding te bewarjen",
"Failed to save image": "Ofbylding bewarjen mislearre",
"Save image as...": "Ofbylding bewarje as…",
"Copy link address": "Keppeling kopiearje",
"Copy email address": "E-mailadres kopiearje",
"File": "Bestân",
"Bring All to Front": "Alles nei foaren bringe",
"Zoom": "Zoom",
"Stop Speaking": "Stopje mei praten",
"Start Speaking": "Begjin mei praten",
"Unhide": "Wer toane",
"Hide Others": "Oare ferbergje",
"Hide": "Ferbergje",
"Services": "Tsjinsten",
"About": "Oer",
"Element Help": "Element help",
"Help": "Help",
"Close": "Slute",
"Minimize": "Minimalisearje",
"Window": "Finster",
"Toggle Developer Tools": "Untwikkelersark yn-/útskeakelje",
"Toggle Full Screen": "Folslein skerm yn-/útskeakelje",
"Preferences": "Foarkarren",
"Zoom Out": "Utzoome",
"Zoom In": "Ynzoome",
"Actual Size": "Werklike grutte",
"Select All": "Alles selektearje",
"Delete": "Fuortsmite",
"Paste": "Plakke",
"Copy": "Kopiearje",
"Cut": "Knippe",
"Redo": "Opnij útfiere",
"Undo": "Ungedien meitsje",
"Edit": "Bewurkje",
"Quit": "Ofslute",
"Show/Hide": "Toane/Ferbergje",
"Are you sure you want to quit?": "Binne jo der wis fan dat jo ôfslute wolle?",
"Close Element": "Element ôfslute",
"Cancel": "Annulearje"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Engadir ao dicionario",
"The image failed to save": "Non se gardou a imaxe",
"Failed to save image": "Fallou o gardado da imaxe",
"Save image as...": "Gardar imaxe como...",
"Copy link address": "Copiar enderezo da ligazón",
"Copy email address": "Copiar enderezo de email",
"Copy image": "Copiar imaxe",
"File": "Ficheiro",
"Bring All to Front": "Traer todo á fronte",
"Zoom": "Aumento",
"Stop Speaking": "Deixa de falar",
"Start Speaking": "Comeza a falar",
"Speech": "Falar",
"Unhide": "Desagochar",
"Hide Others": "Agochar outras",
"Hide": "Agochar",
"Services": "Servizos",
"About": "Acerca de",
"Element Help": "Axuda de Element",
"Help": "Axuda",
"Close": "Pechar",
"Minimize": "Minimizar",
"Window": "Ventá",
"Toggle Developer Tools": "Activar ferramentas de desenvolvemento",
"Toggle Full Screen": "Activar pantalla completa",
"Preferences": "Preferencias",
"Zoom Out": "Diminuir",
"Zoom In": "Aumentar",
"Actual Size": "Tamaño real",
"View": "Ver",
"Select All": "Elexir todo",
"Delete": "Eliminar",
"Paste and Match Style": "Pegar e imitar estilo",
"Paste": "Pegar",
"Copy": "Copiar",
"Cut": "Cortar",
"Redo": "Refacer",
"Undo": "Desfacer",
"Edit": "Editar",
"Quit": "Saír",
"Show/Hide": "Mostrar/Agochar",
"Are you sure you want to quit?": "Tes a certeza de que queres saír?",
"Close Element": "Pechar Element",
"Cancel": "Cancelar"
}

View File

@@ -1,46 +0,0 @@
{
"Actual Size": "גודל ממשי",
"Add to dictionary": "הוסף למילון",
"The image failed to save": "שמירת התמונה נכשלה",
"Failed to save image": "נכשל בשמירת התמונה",
"Save image as...": "שמור תמונה בשם...",
"Copy link address": "העתק קישור",
"Copy email address": "העתק כתובת אימייל",
"Copy image": "העתק תמונה",
"File": "קובץ",
"Bring All to Front": "הבא הכל לחזית",
"Zoom": "גודל תצוגה",
"Stop Speaking": "הפסק לדבר",
"Start Speaking": "התחל לדבר",
"Speech": "דיבור",
"Unhide": "בטל הסתרה",
"Hide Others": "הסתר אחרים",
"Hide": "הסתר",
"Services": "שרותים",
"About": "אודות",
"Element Help": "עזרה של אלמנט",
"Help": "עזרה",
"Close": "סגור",
"Minimize": "מזער",
"Window": "חלון",
"Toggle Developer Tools": "הפעל כלי מפתחים",
"Toggle Full Screen": "הפעל מצב מסך מלא",
"Preferences": "העדפות",
"Zoom Out": "התרחק",
"Zoom In": "התקרב",
"View": "צפה",
"Select All": "בחר הכל",
"Delete": "מחק",
"Paste": "הדבק",
"Copy": "העתק",
"Cut": "גזור",
"Undo": "בטל ביצוע",
"Redo": "בצע שוב",
"Edit": "עריכה",
"Quit": "יציאה",
"Show/Hide": "הצג\\הסתר",
"Are you sure you want to quit?": "האם אתה בטוח שברצונך לצאת?",
"Close Element": "סגור את אלמנט",
"Cancel": "ביטול",
"Paste and Match Style": "הדבק והתאם סגנון"
}

View File

@@ -1,13 +0,0 @@
{
"Paste": "Zalijepiti",
"Copy": "Kopirati",
"Cut": "Izrezati",
"Redo": "Preurediti",
"Undo": "Poništi",
"Edit": "Uredi",
"Quit": "Prestati",
"Show/Hide": "Pokaži/sakrij",
"Are you sure you want to quit?": "Jesi li siguran da želiš odustati?",
"Close Element": "Zatvori Element",
"Cancel": "Otkazati"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Hozzáadás a szótárhoz",
"The image failed to save": "A kép mentése sikertelen",
"Failed to save image": "Kép mentése sikertelen",
"Save image as...": "Kép mentése másként...",
"Copy link address": "Hivatkozás másolása",
"Copy email address": "E-mail cím másolása",
"Copy image": "Kép másolása",
"File": "Fájl",
"Bring All to Front": "Mindent előtérbe hoz",
"Zoom": "Nagyítás",
"Stop Speaking": "Fejezze be a beszédet",
"Start Speaking": "Kezdjen beszélni",
"Speech": "Beszéd",
"Unhide": "Felfed",
"Hide Others": "Minden mást eltakar",
"Hide": "Eltakar",
"Services": "Szolgáltatás",
"About": "Névjegy",
"Element Help": "Element segítség",
"Help": "Segítség",
"Close": "Bezár",
"Minimize": "Lecsukás",
"Window": "Ablak",
"Toggle Developer Tools": "Fejlesztői eszközök",
"Toggle Full Screen": "Teljes képernyő",
"Preferences": "Beállítások",
"Zoom Out": "Kicsinyít",
"Zoom In": "Nagyít",
"Actual Size": "Jelenlegi méret",
"View": "Nézet",
"Select All": "Mind kijelölése",
"Delete": "Töröl",
"Paste and Match Style": "Beillesztés formázással",
"Paste": "Beillesztés",
"Copy": "Másol",
"Cut": "Kivág",
"Redo": "Újra",
"Undo": "Visszavon",
"Edit": "Szerkeszt",
"Quit": "Kilép",
"Show/Hide": "Megmutat/Elrejt",
"Are you sure you want to quit?": "Biztos, hogy kilép?",
"Close Element": "Element bezárása",
"Cancel": "Mégsem"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Tambah ke kamus",
"The image failed to save": "Gambar gagal disimpan",
"Failed to save image": "Gagal menyimpan gambar",
"Save image as...": "Simpan gambar sebagai...",
"Copy link address": "Salin alamat tautan",
"Copy email address": "Salin surel",
"Copy image": "Salin gambar",
"File": "File",
"Hide Others": "Sembunyikan yang Lain",
"Bring All to Front": "Bawa Semua ke Depan",
"Zoom": "Perbesar",
"Stop Speaking": "Berhenti Berbicara",
"Start Speaking": "Mulai Berbicara",
"Speech": "Dikte",
"Unhide": "Tampilkan",
"Hide": "Sembunyikan",
"Services": "Layanan",
"About": "Tentang",
"Element Help": "Bantuan Element",
"Help": "Bantuan",
"Close": "Tutup",
"Minimize": "Minimalkan",
"Window": "Jendela",
"Toggle Developer Tools": "Beralih Alat Pengembang",
"Toggle Full Screen": "Beralih Layar Penuh",
"Preferences": "Pengaturan",
"Zoom Out": "Perkecil",
"Zoom In": "Perbesar",
"Cut": "Potong",
"Redo": "Ulangi",
"Undo": "Urungkan",
"Actual Size": "Ukuran Sebenarnya",
"View": "Tampilan",
"Select All": "Pilih Semua",
"Delete": "Hapus",
"Paste and Match Style": "Tempel dan Cocokkan Gaya",
"Paste": "Tempel",
"Copy": "Salin",
"Edit": "Edit",
"Quit": "Keluar",
"Show/Hide": "Tampilkan/Sembunyikan",
"Are you sure you want to quit?": "Apakah Anda yakin ingin keluar?",
"Close Element": "Tutup Element",
"Cancel": "Batal"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Bæta við orðasafn",
"The image failed to save": "Myndina var ekki hægt að vista",
"Failed to save image": "Mistókst að vista mynd",
"Save image as...": "Vista mynd sem...",
"Copy link address": "Afrita vistfang tengils",
"Copy email address": "Afrita tölvupóstfang",
"Copy image": "Afrita mynd",
"File": "Skrá",
"Bring All to Front": "Setja allt fremst",
"Zoom": "Stærð",
"Stop Speaking": "Hætta tali",
"Start Speaking": "Byrja tal",
"Speech": "Tal",
"Unhide": "Birta",
"Hide Others": "Fela aðra",
"Hide": "Fela",
"Services": "Þjónustur",
"About": "Um hugbúnaðinn",
"Element Help": "Hjálp við Element",
"Help": "Hjálp",
"Close": "Loka",
"Minimize": "Lágmarka",
"Window": "Gluggi",
"Toggle Developer Tools": "Víxla forritunarverkfærum af/á",
"Toggle Full Screen": "Víxla fullum skjá af/á",
"Preferences": "Stillingar",
"Zoom Out": "Minnka",
"Zoom In": "Stækka",
"Actual Size": "Raunstærð",
"View": "Skoða",
"Select All": "Velja allt",
"Delete": "Eyða",
"Paste and Match Style": "Líma og samsvara stíl",
"Paste": "Líma",
"Copy": "Afrita",
"Cut": "Klippa",
"Redo": "Endurgera",
"Undo": "Afturkalla",
"Edit": "Breyta",
"Quit": "Hætta",
"Show/Hide": "Sýna/Fela",
"Are you sure you want to quit?": "Ertu viss um að þú viljir hætta?",
"Close Element": "Loka Element",
"Cancel": "Hætta við"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Aggiungi al dizionario",
"The image failed to save": "Non è stato possibile salvare l'immagine",
"Failed to save image": "Salvataggio immagine fallito",
"Save image as...": "Salva immagine come...",
"Copy link address": "Copia indirizzo collegamento",
"Copy email address": "Copia indirizzo email",
"Copy image": "Copia immagine",
"File": "File",
"Bring All to Front": "Porta tutto in primo piano",
"Zoom": "Zoom",
"Start Speaking": "Inizia a parlare",
"Unhide": "Mostra",
"Hide Others": "Nascondi gli altri",
"Hide": "Nascondi",
"Services": "Servizi",
"About": "Al riguardo",
"Element Help": "Aiuto di Element",
"Help": "Aiuto",
"Close": "Chiudi",
"Minimize": "Riduci",
"Window": "Finestra",
"Toggle Developer Tools": "Attiva strumenti per sviluppatori",
"Toggle Full Screen": "Passa a schermo intero",
"Preferences": "Preferenze",
"Zoom Out": "Rimpicciolisci",
"Zoom In": "Ingrandisci",
"Actual Size": "Dimensione effettiva",
"View": "Vedi",
"Select All": "Seleziona tutto",
"Delete": "Elimina",
"Paste and Match Style": "Incolla e abbina lo stile",
"Paste": "Incolla",
"Copy": "Copia",
"Cut": "Taglia",
"Redo": "Ripeti",
"Undo": "Annulla",
"Edit": "Modifica",
"Quit": "Esci",
"Show/Hide": "Mostra/Nascondi",
"Are you sure you want to quit?": "Vuoi veramente uscire?",
"Close Element": "Chiudi Element",
"Cancel": "Annulla",
"Stop Speaking": "Smetti di parlare",
"Speech": "Dettatura"
}

View File

@@ -1,46 +0,0 @@
{
"Start Speaking": "Runājiet...",
"Add to dictionary": "Pievienot vārdnīcai",
"The image failed to save": "Attēlu saglabāt neizdevās",
"Failed to save image": "Neizdevās saglabāt attēlu",
"Save image as...": "Saglabāt attēlu kā...",
"Copy link address": "Kopēt saiti",
"Copy email address": "Kopēt e-pastu",
"Copy image": "Kopēt attēlu",
"File": "Fails",
"Bring All to Front": "Iznest visu priekšplānā",
"Zoom": "Mērogošana",
"Stop Speaking": "Beidziet runāt",
"Speech": "Runa",
"Unhide": "Rādīt",
"Hide Others": "Slēpt citus",
"Hide": "Slēpt",
"Services": "Servisi/pakalpojumi",
"About": "Par programmu",
"Element Help": "Element palīdzība",
"Help": "Palīdzība",
"Close": "Aizvērt",
"Minimize": "Minimizēt",
"Window": "Logs",
"Toggle Developer Tools": "Pārslēgt uz Izstrādātāja rīkiem",
"Toggle Full Screen": "Pārslēgt uz pilnekrānu",
"Preferences": "Parametri/iestatījumi",
"Zoom Out": "Samazināt",
"Zoom In": "Palielināt",
"Actual Size": "Faktiskais izmērs",
"View": "Skats",
"Select All": "Atzīmēt visus",
"Delete": "Dzēst",
"Paste and Match Style": "Ievietot, saglabājot stilu",
"Paste": "Ievietot",
"Copy": "Kopēt",
"Cut": "Izgriezt",
"Redo": "Atatdarīt/atatgriezt (redo)",
"Undo": "Atgreizt/atdarīt (undo)",
"Edit": "Rediģēt",
"Quit": "Iziet",
"Show/Hide": "Rādīt/nerādīt",
"Are you sure you want to quit?": "Tiešām vēlaties iziet?",
"Close Element": "Aizvērt Elementu",
"Cancel": "Atcelt"
}

View File

@@ -1,37 +0,0 @@
{
"Toggle Developer Tools": "Veksle Utvikleralternativer",
"Add to dictionary": "Legg til i ordbok",
"The image failed to save": "Bildet kunne ikke lagres",
"Failed to save image": "Kunne ikke lagre bildet",
"Save image as...": "Lagre bildet som...",
"Copy email address": "Kopier e-postadressen",
"Copy image": "Kopier bildet",
"File": "Fil",
"Stop Speaking": "Slutt å snakke",
"Start Speaking": "Begynn å snakke",
"Speech": "Tale",
"Hide": "Skjul",
"About": "Om",
"Element Help": "Element Hjelp",
"Help": "Hjelp",
"Close": "Lukk",
"Minimize": "Minimere",
"Window": "Vindu",
"Zoom Out": "Zoom ut",
"Zoom In": "Zoom inn",
"Actual Size": "Faktisk størrelse",
"View": "Se",
"Select All": "Velg alle",
"Delete": "Slett",
"Paste": "Lim inn",
"Copy": "Kopier",
"Undo": "Angre",
"Edit": "Rediger",
"Quit": "Avslutt",
"Show/Hide": "Vis/Skjul",
"Are you sure you want to quit?": "Er du sikker på at du vil slutte?",
"Close Element": "Lukk Element",
"Cancel": "Avbryt",
"Services": "Tjenester",
"Hide Others": "Skjul Andre"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Aan woordenboek toevoegen",
"The image failed to save": "De afbeelding opslaan is mislukt",
"Failed to save image": "Afbeelding opslaan is mislukt",
"Save image as...": "Afbeelding opslaan als...",
"Copy link address": "Link kopiëren",
"Copy email address": "E-mailadres kopiëren",
"Copy image": "Afbeelding kopiëren",
"File": "Bestand",
"Bring All to Front": "Alles naar voren brengen",
"Zoom": "Zoom",
"Stop Speaking": "Stop met praten",
"Start Speaking": "Begin met praten",
"Speech": "Spraak",
"Unhide": "Weer laten zien",
"Hide Others": "Andere verbergen",
"Hide": "Verbergen",
"Services": "Diensten",
"About": "Over",
"Element Help": "Element Help",
"Help": "Help",
"Close": "Sluiten",
"Minimize": "Minimaliseren",
"Window": "Venster",
"Toggle Developer Tools": "Developer Tools wisselen",
"Toggle Full Screen": "Volledig scherm wisselen",
"Preferences": "Voorkeuren",
"Zoom Out": "Uitzoomen",
"Zoom In": "Inzoomen",
"Actual Size": "Werkelijke grootte",
"View": "Bekijken",
"Select All": "Alles selecteren",
"Delete": "Verwijderen",
"Paste and Match Style": "Plakken zonder stijl",
"Paste": "Plakken",
"Copy": "Kopiëren",
"Cut": "Knippen",
"Redo": "Opnieuw doen",
"Undo": "Ongedaan maken",
"Edit": "Bewerken",
"Quit": "Sluiten",
"Show/Hide": "Tonen/Verbergen",
"Are you sure you want to quit?": "Weet u zeker dat u wilt stoppen?",
"Close Element": "Element sluiten",
"Cancel": "Annuleren"
}

View File

@@ -1,46 +0,0 @@
{
"The image failed to save": "Biletet vart ikkje lagra",
"Paste and Match Style": "Lim inn og tilpass stil",
"Redo": "Gjer om",
"Undo": "Angre",
"Are you sure you want to quit?": "Er du sikker på at du vil avslutta?",
"Add to dictionary": "Legg til i ordlista",
"Failed to save image": "Klarte ikkje å lagra biletet",
"Save image as...": "Lagre bilete som…",
"Copy link address": "Kopier lenkjeadresse",
"Copy email address": "Kopier e-postadresse",
"Copy image": "Kopier bilete",
"File": "Fil",
"Bring All to Front": "Plasser lengst fram",
"Zoom": "Zoom",
"Stop Speaking": "Stopp snakka",
"Start Speaking": "Byrja snakka",
"Speech": "Tale",
"Unhide": "Ikkje gøym",
"Hide Others": "Gøym andre",
"Hide": "Gøym",
"Services": "Tenester",
"About": "Om",
"Element Help": "Hjelp med Element",
"Help": "Hjelp",
"Close": "Lat att",
"Minimize": "Minimer",
"Window": "Vindauga",
"Toggle Developer Tools": "Developer Tools av/på",
"Toggle Full Screen": "Fullskjerm av/på",
"Preferences": "Innstillingar",
"Zoom Out": "Zoom ut",
"Zoom In": "Zoom inn",
"Actual Size": "Faktisk storleik",
"View": "Vis",
"Select All": "Marker alt",
"Delete": "Slett",
"Paste": "Lim inn",
"Copy": "Lim inn",
"Cut": "Klipp ut",
"Edit": "Rediger",
"Quit": "Avslutt",
"Show/Hide": "Vis/Gøym",
"Close Element": "Lat att Element",
"Cancel": "Avbryt"
}

View File

@@ -1,46 +0,0 @@
{
"Bring All to Front": "Wyciągnij wszystko do przodu",
"Add to dictionary": "Dodaj do słownika",
"The image failed to save": "Obraz nie został zapisany",
"Failed to save image": "Nie udało się zapisać obrazu",
"Save image as...": "Zapisz obraz jako...",
"Copy link address": "Skopiuj adres łącza",
"Copy email address": "Skopiuj adres email",
"Copy image": "Skopiuj obraz",
"File": "Plik",
"Zoom": "Powiększenie",
"Stop Speaking": "Przestań mówić",
"Start Speaking": "Zacznij mówić",
"Speech": "Mowa",
"Unhide": "Odkryj",
"Hide Others": "Ukryj inne",
"Hide": "Ukryj",
"Services": "Usługi",
"About": "O nas",
"Element Help": "Pomoc Element",
"Help": "Pomoc",
"Close": "Zamknij",
"Minimize": "Zminimalizuj",
"Window": "Okno",
"Toggle Developer Tools": "Przełącz na narzędzia deweloperskie",
"Toggle Full Screen": "Przełącz na pełny ekran",
"Preferences": "Preferencje",
"Zoom Out": "Pomniejsz",
"Zoom In": "Powiększ",
"Actual Size": "Rzeczywisty rozmiar",
"View": "Pokaż",
"Select All": "Zaznacz wszystko",
"Delete": "Usuń",
"Paste and Match Style": "Wklej i dopasuj styl",
"Paste": "Wklej",
"Copy": "Skopiuj",
"Cut": "Wytnij",
"Redo": "Powtórz",
"Undo": "Cofnij",
"Edit": "Edytuj",
"Quit": "Zamknij",
"Show/Hide": "Pokaż/Ukryj",
"Are you sure you want to quit?": "Czy na pewno chcesz zamknąć?",
"Close Element": "Zamknij Elementa",
"Cancel": "Anuluj"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Adicionar a dicionário",
"The image failed to save": "A imagem falhou para salvar",
"Failed to save image": "Falha para salvar imagem",
"Save image as...": "Salvar imagem como...",
"Copy link address": "Copiar endereço de link",
"Copy email address": "Copiar endereço de email",
"Copy image": "Copiar imagem",
"File": "Arquivo",
"Zoom": "Zoom",
"Stop Speaking": "Parar de Falar",
"Start Speaking": "Começar a Falar",
"Speech": "Fala",
"Unhide": "Desesconder",
"Hide": "Esconder",
"Services": "Serviços",
"About": "Sobre",
"Element Help": "Ajuda de Element",
"Help": "Ajuda",
"Close": "Fechar",
"Minimize": "Minimizar",
"Window": "Janela",
"Toggle Developer Tools": "Ativar/Desativar Ferramentas de Desenvolvimento",
"Toggle Full Screen": "Pôr em/Tirar de Tela Cheia",
"Preferences": "Preferências",
"Zoom Out": "Dar Zoom Out",
"Zoom In": "Dar Zoom In",
"Actual Size": "Tamanho de Verdade",
"View": "Visualizar",
"Select All": "Selecionar Todas",
"Delete": "Deletar",
"Paste and Match Style": "Colar e Adequar Estilo",
"Paste": "Colar",
"Copy": "Copiar",
"Cut": "Cortar",
"Redo": "Refazer",
"Undo": "Desfazer",
"Edit": "Editar",
"Quit": "Sair",
"Show/Hide": "Mostrar/Esconder",
"Are you sure you want to quit?": "Você tem certeza que você quer sair?",
"Close Element": "Fechar Element",
"Cancel": "Cancelar",
"Bring All to Front": "Trazer Todas Para Frente",
"Hide Others": "Esconder Outras(os)"
}

View File

@@ -1,43 +0,0 @@
{
"Add to dictionary": "Adăugați la dicționar",
"Failed to save image": "Eroare in salvarea imaginii",
"Save image as...": "Salvează imagine ca ...",
"Copy link address": "Copiază link",
"Copy email address": "Copiază adresă de email",
"Copy image": "Copiază imagine",
"File": "Fișier",
"Bring All to Front": "Aduce-ți totul in față",
"Zoom": "zoom",
"Stop Speaking": "Oprire Voce",
"Start Speaking": "Pornire Voce",
"Speech": "Voce",
"Hide Others": "Ascunde restul",
"Hide": "Ascunde",
"Services": "Servicii",
"About": "Despre",
"Element Help": "Ajutor Element",
"Help": "Ajutor",
"Close": "Inchide",
"Minimize": "Minimizare",
"Window": "Fereastră",
"Toggle Developer Tools": "Comutare unelte dezvoltator",
"Toggle Full Screen": "Comutare pe tot ecranul",
"Preferences": "Preferințe",
"Zoom Out": "Micșorează",
"Zoom In": "Mărește",
"Actual Size": "Mărime reală",
"View": "Vizualizează",
"Select All": "Selectează tot",
"Delete": "Șterge",
"Paste and Match Style": "Lipește si potrivește stilul",
"Paste": "Lipește",
"Copy": "Copiere",
"Redo": "Refă",
"Undo": "Anulare",
"Edit": "Editare",
"Quit": "Închide",
"Show/Hide": "Arată/Ascunde",
"Are you sure you want to quit?": "Sunte-ți sigur ca doriți sa inchideți aplicația ?",
"Close Element": "închide aplicația",
"Cancel": "Anulare"
}

View File

@@ -1,46 +0,0 @@
{
"Edit": "Изменить",
"Quit": "Выйти",
"Close Element": "Закрыть Element",
"Cancel": "Отмена",
"Show/Hide": "Показать/скрыть",
"Are you sure you want to quit?": "Вы уверены, что хотите выйти?",
"Copy email address": "Копировать адрес почты",
"Copy image": "Копировать изображение",
"File": "Файл",
"Zoom": "Масштаб",
"Unhide": "Показать",
"Hide": "Скрыть",
"Services": "Сервисы",
"About": "О программе",
"Element Help": "Справка Element",
"Help": "Справка",
"Close": "Закрыть",
"Minimize": "Свернуть",
"Window": "Окно",
"Toggle Developer Tools": "Переключить инструменты разработчика",
"Toggle Full Screen": "Переключить полноэкранный режим",
"Preferences": "Параметры",
"Zoom Out": "Уменьшить",
"Zoom In": "Увеличить",
"Actual Size": "Фактический размер",
"View": "Просмотр",
"Select All": "Выбрать все",
"Delete": "Удалить",
"Paste": "Вставить",
"Copy": "Копировать",
"Cut": "Вырезать",
"Redo": "Повторить",
"Undo": "Отменить",
"Save image as...": "Сохранить изображение как...",
"Copy link address": "Копировать ссылку",
"Add to dictionary": "Добавить в словарь",
"The image failed to save": "Не удалось сохранить изображение",
"Failed to save image": "Не удалось сохранить изображение",
"Bring All to Front": "Вынести всё вперёд",
"Stop Speaking": "Перестаньте говорить",
"Start Speaking": "Говорите",
"Speech": "Голос",
"Hide Others": "Скрыть прочие",
"Paste and Match Style": "Вставить с тем же стилем"
}

View File

@@ -1,31 +0,0 @@
{
"Show/Hide": "පෙන්වන්න/සඟවන්න",
"Are you sure you want to quit?": "ඔබට ඉවත් වීමට අවශ්‍ය බව විශ්වාස ද?",
"Close Element": "ඉලමෙන්ට් වසන්න",
"Cancel": "අවලංගු කරන්න",
"Add to dictionary": "ශබ්ද කෝෂයට එකතු කරන්න",
"Copy link address": "සබැඳියේ ලිපිනය පිටපත් කරන්න",
"Copy email address": "වි-තැපෑල පිටපත් කරන්න",
"File": "ගොනුව",
"Zoom": "විශාල කරන්න",
"Hide Others": "වෙනත් දෑ සඟවන්න",
"Hide": "සඟවන්න",
"Services": "සේවා",
"About": "පිළිබඳව",
"Element Help": "ඉලමෙන්ට් උපකාර",
"Help": "උපකාර",
"Close": "වසන්න",
"Minimize": "හකුලන්න",
"Window": "කවුළුව",
"Zoom Out": "කුඩාලනය කරන්න",
"Zoom In": "විශාලනය කරන්න",
"Actual Size": "සැබෑ ප්‍රමාණය",
"Select All": "සියල්ල තෝරන්න",
"Paste": "අලවන්න",
"Copy": "පිටපත්",
"Cut": "කපන්න",
"Redo": "පසුසේ",
"Undo": "පෙරසේ",
"Edit": "සංස්කරණය",
"Quit": "ඉවත් වන්න"
}

View File

@@ -1,46 +0,0 @@
{
"Unhide": "Odkryť",
"Stop Speaking": "Zastaviť nahrávanie hlasu",
"Start Speaking": "Spustiť nahrávanie hlasu",
"Speech": "Reč",
"Element Help": "Pomocník pre aplikáciu Element",
"Paste and Match Style": "Vložiť a prispôsobiť štýl",
"Close Element": "Zavrieť aplikáciu Element",
"Add to dictionary": "Pridať do slovníka",
"The image failed to save": "Obrázok sa nepodarilo uložiť",
"Failed to save image": "Chyba pri ukladaní obrázka",
"Save image as...": "Uložiť obrázok ako...",
"Copy link address": "Kopírovať adresu odkazu",
"Copy email address": "Kopírovať e-mailovú adresu",
"Copy image": "Kopírovať obrázok",
"File": "Súbor",
"Bring All to Front": "Preniesť všetky do popredia",
"Zoom": "Lupa",
"Hide Others": "Skryť ostatné",
"Hide": "Skryť",
"Services": "Služby",
"About": "O aplikácii",
"Help": "Pomocník",
"Close": "Zavrieť",
"Minimize": "Minimalizovať",
"Window": "Okno",
"Toggle Developer Tools": "Nástroje pre vývojárov",
"Toggle Full Screen": "Celá obrazovka",
"Preferences": "Vlastnosti",
"Zoom Out": "Oddialiť",
"Zoom In": "Priblížiť",
"Actual Size": "Aktuálna veľkosť",
"View": "Zobraziť",
"Select All": "Vybrať všetko",
"Delete": "Odstrániť",
"Paste": "Vložiť",
"Copy": "Kopírovať",
"Cut": "Vystrihnúť",
"Redo": "Opakovať",
"Undo": "Späť",
"Edit": "Úpravy",
"Quit": "Ukončiť",
"Show/Hide": "Zobraziť/Skryť",
"Are you sure you want to quit?": "Naozaj chcete zavrieť aplikáciu?",
"Cancel": "Zrušiť"
}

View File

@@ -1,46 +0,0 @@
{
"Save image as...": "Spara bild som...",
"Copy link address": "Kopiera länkadress",
"Copy email address": "Kopiera e-postadress",
"Copy image": "Kopiera bild",
"File": "Arkiv",
"Bring All to Front": "Lägg alla överst",
"Stop Speaking": "Sluta tala",
"Start Speaking": "Börja tala",
"Speech": "Tal",
"Hide Others": "Göm övriga",
"Hide": "Göm",
"Services": "Tjänster",
"About": "Om",
"Element Help": "Element Hjälp",
"Help": "Hjälp",
"Close": "Stäng",
"Minimize": "Minimera",
"Window": "Fönster",
"Preferences": "Inställningar",
"Actual Size": "Verklig storlek",
"View": "Visa",
"Select All": "Markera allt",
"Delete": "Radera",
"Paste and Match Style": "Klistra in och matcha stilen",
"Paste": "Klistra in",
"Copy": "Kopiera",
"Cut": "Klipp ut",
"Redo": "Gör om",
"Undo": "Ångra",
"Edit": "Redigera",
"Quit": "Avsluta",
"Cancel": "Avbryt",
"Zoom": "Zooma",
"Toggle Developer Tools": "Växla utvecklarverktyg",
"Toggle Full Screen": "Växla helskärm",
"Unhide": "Göm inte",
"Zoom Out": "Zooma ut",
"Zoom In": "Zooma in",
"Close Element": "Stäng Element",
"Show/Hide": "Visa/dölj",
"Add to dictionary": "Lägg till i ordlistan",
"The image failed to save": "Bilden sparades inte",
"Failed to save image": "Misslyckades med att spara bilden",
"Are you sure you want to quit?": "Är du säker att du vill avsluta?"
}

View File

@@ -1,46 +0,0 @@
{
"Zoom": "பெரிதாக்குதல்",
"Minimize": "சிறிதாக்கு",
"Toggle Developer Tools": "படைப்பாளர் கருவிகளை நிலைமாற்று",
"Toggle Full Screen": "முழு திரையை நிலைமாற்று",
"Paste and Match Style": "ஒட்டு மற்றும் நடையை பொறுத்து",
"Add to dictionary": "அகராதியில் சேர்",
"The image failed to save": "படம் சேமிக்கத் தவறிவிட்டது",
"Failed to save image": "படத்தைச் சேமிப்பதில் தோல்வி",
"Save image as...": "படத்தை இவ்வாறு சேமி...",
"Copy link address": "இணைப்பு முகவரியை நகலெடு",
"Copy email address": "மின்னஞ்சல் முகவரியை நகலெடு",
"Copy image": "படத்தை நகலெடு",
"File": "கோப்பு",
"Bring All to Front": "அனைத்தையும் முன்னால் கொண்டுவா",
"Stop Speaking": "பேசுவதை நிறுத்து",
"Start Speaking": "பேசத் துவங்கு",
"Speech": "பேச்சு",
"Unhide": "காட்டு",
"Hide Others": "மற்றதை மறை",
"Hide": "மறை",
"Services": "சேவைகள்",
"About": "இதனைப் பற்றி",
"Element Help": "எலிமெண்ட் உதவி",
"Help": "உதவி",
"Close": "மூடு",
"Window": "சாளரம்",
"Preferences": "விருப்பத்தேர்வுகள்",
"Zoom Out": "சிறிதாக்கு",
"Zoom In": "பெரிதாக்கு",
"Actual Size": "உண்மையான அளவு",
"View": "காட்சி",
"Select All": "அனைத்தையும் தெரிவுசெய்",
"Delete": "அழி",
"Paste": "ஒட்டு",
"Copy": "நகலெடு",
"Cut": "வெட்டு",
"Redo": "மீண்டும் செய்",
"Undo": "செயல்தவிர்",
"Edit": "திருத்து",
"Quit": "வெளியேறு",
"Show/Hide": "காட்டு/மறை",
"Are you sure you want to quit?": "நீங்கள் நிச்சயம் வெளியேற விரும்புகிறீர்களா?",
"Close Element": "எலிமெண்ட் ஐ மூடு",
"Cancel": "ரத்துசெய்"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Sözlüğe ekle",
"The image failed to save": "Bu resim kaydedilemedi",
"Failed to save image": "Resim kaydedilemedi",
"Save image as...": "Resmi ... olarak farklı kaydet",
"Copy link address": "Bağlantılı adresi kopyala",
"Copy email address": "E-posta adresini kopyala",
"Copy image": "Resmi kopyala",
"File": "Dosya",
"Bring All to Front": "Hepsini öne getir",
"Zoom": "Yaklaştır",
"Stop Speaking": "Konuşmayı durdur",
"Start Speaking": "Konuşmaya başla",
"Speech": "Konuşma",
"Unhide": "Gizlemeyi bırak",
"Hide Others": "Diğerlerini gizle",
"Hide": "Gizle",
"Services": "Hizmetler",
"About": "Hakkında",
"Element Help": "Element yardımı",
"Help": "Yardım",
"Close": "Kapat",
"Minimize": "Küçült",
"Window": "Pencere",
"Toggle Developer Tools": "Geliştirici araçları",
"Toggle Full Screen": "Tam ekran",
"Preferences": "Tercihler",
"Zoom Out": "Uzaklaştır",
"Zoom In": "Yaklaştır",
"Actual Size": "Gerçek boyut",
"View": "Görünüm",
"Select All": "Tümünü seç",
"Delete": "Sil",
"Paste and Match Style": "Biçimiyle bir yapıştır",
"Paste": "Yapıştır",
"Copy": "Kopyala",
"Cut": "Kes",
"Redo": "Yinele",
"Undo": "Geri al",
"Edit": "Düzenle",
"Quit": ık",
"Show/Hide": "Göster/Gizle",
"Are you sure you want to quit?": ıkmak istediğinize emin misiniz?",
"Close Element": "Element'i kapat",
"Cancel": "İptal"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "Thêm vào từ điển",
"The image failed to save": "Ảnh không lưu được",
"Failed to save image": "Lưu ảnh thất bại",
"Save image as...": "Lưu ảnh…",
"Copy link address": "Sao chép địa chỉ liên kết",
"Copy email address": "Sao chép địa chỉ email",
"Copy image": "Sao chép ảnh",
"File": "Tệp",
"Bring All to Front": "Đưa tất cả lên trước",
"Zoom": "Thu phóng",
"Stop Speaking": "Dừng nói",
"Start Speaking": "Bắt đầu nói",
"Speech": "Đọc màn hình",
"Unhide": "Bỏ ẩn",
"Hide Others": "Ẩn cái khác",
"Hide": "Ẩn",
"Services": "Dịch vụ",
"About": "Giới thiệu",
"Element Help": "Trợ giúp Element",
"Help": "Hỗ trợ",
"Close": "Đóng",
"Minimize": "Thu nhỏ",
"Window": "Cửa sổ",
"Toggle Developer Tools": "Công cụ cho Nhà phát triển",
"Toggle Full Screen": "Toàn màn hình",
"Preferences": "Tùy chọn",
"Zoom Out": "Thu nhỏ",
"Zoom In": "Phóng to",
"Actual Size": "Kích thước thực",
"View": "Xem",
"Select All": "Chọn tất cả",
"Delete": "Xóa",
"Paste and Match Style": "Dán và khớp kiểu",
"Paste": "Dán",
"Copy": "Sao chép",
"Cut": "Cắt",
"Redo": "Làm lại",
"Undo": "Hoàn tác",
"Edit": "Chỉnh sửa",
"Quit": "Thoát",
"Show/Hide": "Hiển thị/Ẩn",
"Are you sure you want to quit?": "Bạn có chắc chắn muốn thoát?",
"Close Element": "Đóng Element",
"Cancel": "Hủy bỏ"
}

View File

@@ -1,46 +0,0 @@
{
"Add to dictionary": "添加到字典",
"The image failed to save": "图片保存失败",
"Failed to save image": "图片保存失败",
"Save image as...": "保存图片为……",
"Copy link address": "复制链接地址",
"Copy email address": "复制邮箱地址",
"Copy image": "复制图片",
"File": "文件",
"Bring All to Front": "置前",
"Zoom": "放大",
"Stop Speaking": "停止讲话",
"Start Speaking": "开始讲话",
"Speech": "讲话",
"Unhide": "显示",
"Hide Others": "隐藏其他",
"Hide": "隐藏",
"Services": "服务",
"About": "关于",
"Element Help": "Element 帮助",
"Help": "帮助",
"Close": "关闭",
"Minimize": "最小化",
"Window": "窗口",
"Toggle Developer Tools": "切换开发工具",
"Toggle Full Screen": "切换全屏",
"Preferences": "外观",
"Zoom Out": "缩小",
"Zoom In": "放大",
"Actual Size": "实际大小",
"View": "查看",
"Select All": "选中全部",
"Delete": "删除",
"Paste and Match Style": "粘贴并匹配样式",
"Paste": "粘贴",
"Copy": "复制",
"Cut": "剪贴",
"Redo": "重做",
"Undo": "撤销",
"Edit": "编辑",
"Quit": "退出",
"Show/Hide": "显示/隐藏",
"Are you sure you want to quit?": "你确定要退出吗?",
"Close Element": "关闭 Element",
"Cancel": "取消"
}

View File

@@ -1,46 +0,0 @@
{
"Bring All to Front": "全部移至最前",
"Add to dictionary": "加入至字典",
"The image failed to save": "儲存圖片失敗",
"Failed to save image": "儲存圖片失敗",
"Save image as...": "另存圖片為...",
"Copy link address": "複製連結",
"Copy email address": "複製電子郵件地址",
"Copy image": "複製圖片",
"File": "檔案",
"Zoom": "縮放",
"Stop Speaking": "停止說話",
"Start Speaking": "開始說話",
"Speech": "語音",
"Unhide": "取消隱藏",
"Hide Others": "隱藏其他",
"Hide": "隱藏",
"Services": "服務",
"About": "關於",
"Element Help": "Element 協助",
"Help": "協助",
"Close": "關閉",
"Minimize": "最小化",
"Window": "視窗",
"Toggle Developer Tools": "切換開發工具",
"Toggle Full Screen": "切換全螢幕",
"Preferences": "偏好設定",
"Zoom Out": "縮小",
"Zoom In": "放大",
"Actual Size": "實際大小",
"View": "檢視",
"Select All": "全選",
"Delete": "刪除",
"Paste and Match Style": "貼上並保留格式",
"Paste": "貼上",
"Copy": "複製",
"Cut": "剪下",
"Redo": "取消復原",
"Undo": "復原",
"Edit": "編輯",
"Quit": "退出",
"Show/Hide": "顯示/隱藏",
"Are you sure you want to quit?": "您確定要退出嗎?",
"Close Element": "關閉 Element",
"Cancel": "取消"
}

View File

@@ -1,135 +0,0 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import counterpart from "counterpart";
import type Store from 'electron-store';
const DEFAULT_LOCALE = "en";
export function _td(text: string): string {
return text;
}
type SubstitutionValue = number | string;
interface IVariables {
[key: string]: SubstitutionValue;
count?: number;
}
export function _t(text: string, variables: IVariables = {}): string {
const args = Object.assign({ interpolate: false }, variables);
const { count } = args;
// Horrible hack to avoid https://github.com/vector-im/element-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null
// values and instead will throw an error. This is a problem since everywhere else
// in JS land passing undefined/null will simply stringify instead, and when converting
// valid ES6 template strings to i18n strings it's extremely easy to pass undefined/null
// if there are no existing null guards. To avoid this making the app completely inoperable,
// we'll check all the values for undefined/null and stringify them here.
Object.keys(args).forEach((key) => {
if (args[key] === undefined) {
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key);
args[key] = 'undefined';
}
if (args[key] === null) {
console.warn("safeCounterpartTranslate called with null interpolation name: " + key);
args[key] = 'null';
}
});
let translated = counterpart.translate(text, args);
if (translated === undefined && count !== undefined) {
// counterpart does not do fallback if no pluralisation exists
// in the preferred language, so do it here
translated = counterpart.translate(text, Object.assign({}, args, { locale: DEFAULT_LOCALE }));
}
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
return translated;
}
type Component = () => void;
type TypedStore = Store<{ locale?: string | string[] }>;
export class AppLocalization {
private static readonly STORE_KEY = "locale";
private readonly store: TypedStore;
private readonly localizedComponents: Set<Component>;
constructor({ store, components = [] }: { store: TypedStore, components: Component[] }) {
counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale('en');
counterpart.setSeparator('|');
if (Array.isArray(components)) {
this.localizedComponents = new Set(components);
}
this.store = store;
if (this.store.has(AppLocalization.STORE_KEY)) {
const locales = this.store.get(AppLocalization.STORE_KEY);
this.setAppLocale(locales);
}
this.resetLocalizedUI();
}
public fetchTranslationJson(locale: string): Record<string, string> {
try {
console.log("Fetching translation json for locale: " + locale);
return require(`./i18n/strings/${locale}.json`);
} catch (e) {
console.log(`Could not fetch translation json for locale: '${locale}'`, e);
return null;
}
}
public setAppLocale(locales: string | string[]): void {
console.log(`Changing application language to ${locales}`);
if (!Array.isArray(locales)) {
locales = [locales];
}
locales.forEach(locale => {
const translations = this.fetchTranslationJson(locale);
if (translations !== null) {
counterpart.registerTranslations(locale, translations);
}
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - this looks like a bug but is out of scope for this conversion
counterpart.setLocale(locales);
this.store.set(AppLocalization.STORE_KEY, locales);
this.resetLocalizedUI();
}
public resetLocalizedUI(): void {
console.log("Resetting the UI components after locale change");
this.localizedComponents.forEach(componentSetup => {
if (typeof componentSetup === "function") {
componentSetup();
}
});
}
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron';
const { ipcRenderer, desktopCapturer, contextBridge } = require('electron');
// Expose only expected IPC wrapper APIs to the renderer process to avoid
// handing out generalised messaging access.
@@ -32,26 +32,46 @@ const CHANNELS = [
"seshatReply",
"setBadgeCount",
"update-downloaded",
"userDownloadCompleted",
"userDownloadAction",
"userDownload",
];
contextBridge.exposeInMainWorld(
"electron",
{
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void {
on(channel, listener) {
if (!CHANNELS.includes(channel)) {
console.error(`Unknown IPC channel ${channel} ignored`);
return;
}
ipcRenderer.on(channel, listener);
},
send(channel: string, ...args: any[]): void {
removeListener(channel, listener) {
if (!CHANNELS.includes(channel)) {
console.error(`Unknown IPC channel ${channel} ignored`);
return;
}
ipcRenderer.removeListener(channel, listener);
},
send(channel, ...args) {
if (!CHANNELS.includes(channel)) {
console.error(`Unknown IPC channel ${channel} ignored`);
return;
}
ipcRenderer.send(channel, ...args);
},
async getDesktopCapturerSources(options) {
const sources = await desktopCapturer.getSources(options);
const desktopCapturerSources = [];
for (const source of sources) {
desktopCapturerSources.push({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
});
}
return desktopCapturerSources;
},
},
);

102
src/protocol.js Normal file
View File

@@ -0,0 +1,102 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const {app} = require("electron");
const path = require("path");
const fs = require("fs");
const PROTOCOL = "element://";
const SEARCH_PARAM = "element-desktop-ssoid";
const STORE_FILE_NAME = "sso-sessions.json";
// we getPath userData before electron-main changes it, so this is the default value
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
const processUrl = (url) => {
if (!global.mainWindow) return;
console.log("Handling link: ", url);
global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://"));
};
const readStore = () => {
try {
const s = fs.readFileSync(storePath, { encoding: "utf8" });
const o = JSON.parse(s);
return typeof o === "object" ? o : {};
} catch (e) {
return {};
}
};
const writeStore = (data) => {
fs.writeFileSync(storePath, JSON.stringify(data));
};
module.exports = {
recordSSOSession: (sessionID) => {
const userDataPath = app.getPath('userData');
const store = readStore();
for (const key in store) {
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
if (store[key] === userDataPath) {
delete store[key];
break;
}
}
store[sessionID] = userDataPath;
writeStore(store);
},
getProfileFromDeeplink: (args) => {
// check if we are passed a profile in the SSO callback url
const deeplinkUrl = args.find(arg => arg.startsWith('element://'));
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
const parsedUrl = new URL(deeplinkUrl);
if (parsedUrl.protocol === 'element:') {
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
const store = readStore();
console.log("Forwarding to profile: ", store[ssoID]);
return store[ssoID];
}
}
},
protocolInit: () => {
// get all args except `hidden` as it'd mean the app would not get focused
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden");
if (app.isPackaged) {
app.setAsDefaultProtocolClient('element', process.execPath, args);
} else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
app.setAsDefaultProtocolClient('element', process.execPath, [app.getAppPath(), ...args]);
}
if (process.platform === 'darwin') {
// Protocol handler for macos
app.on('open-url', function(ev, url) {
ev.preventDefault();
processUrl(url);
});
} else {
// Protocol handler for win32/Linux
app.on('second-instance', (ev, commandLine) => {
const url = commandLine[commandLine.length - 1];
if (!url.startsWith(PROTOCOL)) return;
processUrl(url);
});
}
},
};

View File

@@ -1,123 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { app } from "electron";
import { URL } from "url";
import path from "path";
import fs from "fs";
const PROTOCOL = "element:";
const SEARCH_PARAM = "element-desktop-ssoid";
const STORE_FILE_NAME = "sso-sessions.json";
// we getPath userData before electron-main changes it, so this is the default value
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
function processUrl(url: string): void {
if (!global.mainWindow) return;
const parsed = new URL(url);
// sanity check: we only register for the one protocol, so we shouldn't
// be getting anything else unless the user is forcing a URL to open
// with the Element app.
if (parsed.protocol !== PROTOCOL) {
console.log("Ignoring unexpected protocol: ", parsed.protocol);
return;
}
const urlToLoad = new URL("vector://vector/webapp/");
// ignore anything other than the search (used for SSO login redirect)
// and the hash (for general element deep links)
// There's no reason to allow anything else, particularly other paths,
// since this would allow things like the internal jitsi wrapper to
// be loaded, which would get the app stuck on that page and generally
// be a bit strange and confusing.
urlToLoad.search = parsed.search;
urlToLoad.hash = parsed.hash;
console.log("Opening URL: ", urlToLoad.href);
global.mainWindow.loadURL(urlToLoad.href);
}
function readStore(): object {
try {
const s = fs.readFileSync(storePath, { encoding: "utf8" });
const o = JSON.parse(s);
return typeof o === "object" ? o : {};
} catch (e) {
return {};
}
}
function writeStore(data: object): void {
fs.writeFileSync(storePath, JSON.stringify(data));
}
export function recordSSOSession(sessionID: string): void {
const userDataPath = app.getPath('userData');
const store = readStore();
for (const key in store) {
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
if (store[key] === userDataPath) {
delete store[key];
break;
}
}
store[sessionID] = userDataPath;
writeStore(store);
}
export function getProfileFromDeeplink(args): string | undefined {
// check if we are passed a profile in the SSO callback url
const deeplinkUrl = args.find(arg => arg.startsWith(PROTOCOL + '//'));
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
const parsedUrl = new URL(deeplinkUrl);
if (parsedUrl.protocol === PROTOCOL) {
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
const store = readStore();
console.log("Forwarding to profile: ", store[ssoID]);
return store[ssoID];
}
}
}
export function protocolInit(): void {
// get all args except `hidden` as it'd mean the app would not get focused
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden");
if (app.isPackaged) {
app.setAsDefaultProtocolClient('element', process.execPath, args);
} else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
app.setAsDefaultProtocolClient('element', process.execPath, [app.getAppPath(), ...args]);
}
if (process.platform === 'darwin') {
// Protocol handler for macos
app.on('open-url', function(ev, url) {
ev.preventDefault();
processUrl(url);
});
} else {
// Protocol handler for win32/Linux
app.on('second-instance', (ev, commandLine) => {
const url = commandLine[commandLine.length - 1];
if (!url.startsWith(PROTOCOL + '//')) return;
processUrl(url);
});
}
}

View File

@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from "path";
import { spawn } from "child_process";
import { app } from "electron";
import { promises as fsProm } from "fs";
const path = require('path');
const spawn = require('child_process').spawn;
const {app} = require('electron');
const fsProm = require('fs').promises;
function runUpdateExe(args: string[]): Promise<void> {
function runUpdateExe(args) {
// Invokes Squirrel's Update.exe which will do things for us like create shortcuts
// Note that there's an Update.exe in the app-x.x.x directory and one in the parent
// directory: we need to run the one in the parent directory, because it discovers
@@ -28,18 +28,20 @@ function runUpdateExe(args: string[]): Promise<void> {
console.log(`Spawning '${updateExe}' with args '${args}'`);
return new Promise(resolve => {
spawn(updateExe, args, {
detached: true,
detached: true,
}).on('close', resolve);
});
}
function checkSquirrelHooks(): boolean {
function checkSquirrelHooks() {
if (process.platform !== 'win32') return false;
const cmd = process.argv[1];
const target = path.basename(process.execPath);
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
runUpdateExe(['--createShortcut=' + target]).then(() => {
Promise.resolve().then(() => {
return runUpdateExe(['--createShortcut=' + target]);
}).then(() => {
// remove the old 'Riot' shortcuts, if they exist (update.exe --removeShortcut doesn't work
// because it always uses the name of the product as the name of the shortcut: the only variable
// is what executable you're linking to)
@@ -81,6 +83,4 @@ function checkSquirrelHooks(): boolean {
return false;
}
if (checkSquirrelHooks()) {
process.exit(1);
}
module.exports = checkSquirrelHooks;

View File

@@ -15,49 +15,57 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { app, Tray, Menu, nativeImage } from "electron";
import pngToIco from "png-to-ico";
import path from "path";
import fs from "fs";
const {app, Tray, Menu, nativeImage} = require('electron');
const pngToIco = require('png-to-ico');
const path = require('path');
const fs = require('fs');
import { _t } from "./language-helper";
let trayIcon = null;
let trayIcon: Tray = null;
export function hasTray(): boolean {
exports.hasTray = function hasTray() {
return (trayIcon !== null);
}
};
export function destroy(): void {
exports.destroy = function() {
if (trayIcon) {
trayIcon.destroy();
trayIcon = null;
}
}
};
function toggleWin(): void {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized() && global.mainWindow.isFocused()) {
global.mainWindow.hide();
} else {
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
if (!global.mainWindow.isVisible()) global.mainWindow.show();
global.mainWindow.focus();
}
}
interface IConfig {
icon_path: string; // eslint-disable-line camelcase
brand: string;
}
export function create(config: IConfig): void {
exports.create = function(config) {
// no trays on darwin
if (process.platform === 'darwin' || trayIcon) return;
const toggleWin = function() {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
global.mainWindow.hide();
} else {
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
if (!global.mainWindow.isVisible()) global.mainWindow.show();
global.mainWindow.focus();
}
};
const contextMenu = Menu.buildFromTemplate([
{
label: `Show/Hide ${config.brand}`,
click: toggleWin,
},
{ type: 'separator' },
{
label: 'Quit',
click: function() {
app.quit();
},
},
]);
const defaultIcon = nativeImage.createFromPath(config.icon_path);
trayIcon = new Tray(defaultIcon);
trayIcon.setToolTip(config.brand);
initApplicationMenu();
trayIcon.setContextMenu(contextMenu);
trayIcon.on('click', toggleWin);
let lastFavicon = null;
@@ -95,26 +103,4 @@ export function create(config: IConfig): void {
global.mainWindow.webContents.on('page-title-updated', function(ev, title) {
trayIcon.setToolTip(title);
});
}
export function initApplicationMenu(): void {
if (!trayIcon) {
return;
}
const contextMenu = Menu.buildFromTemplate([
{
label: _t('Show/Hide'),
click: toggleWin,
},
{ type: 'separator' },
{
label: _t('Quit'),
click: function() {
app.quit();
},
},
]);
trayIcon.setContextMenu(contextMenu);
}
};

View File

@@ -1,32 +1,16 @@
/*
Copyright 2016-2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { app, autoUpdater, ipcMain } from "electron";
const { app, autoUpdater, ipcMain } = require('electron');
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
function installUpdate(): void {
function installUpdate() {
// for some reason, quitAndInstall does not fire the
// before-quit event, so we need to set the flag here.
global.appQuitting = true;
autoUpdater.quitAndInstall();
}
function pollForUpdates(): void {
function pollForUpdates() {
try {
autoUpdater.checkForUpdates();
} catch (e) {
@@ -34,7 +18,8 @@ function pollForUpdates(): void {
}
}
export function start(updateBaseUrl: string): void {
module.exports = {};
module.exports.start = function startAutoUpdate(updateBaseUrl) {
if (updateBaseUrl.slice(-1) !== '/') {
updateBaseUrl = updateBaseUrl + '/';
}
@@ -63,7 +48,6 @@ export function start(updateBaseUrl: string): void {
}
if (url) {
console.log(`Update URL: ${url}`);
autoUpdater.setFeedURL(url);
// We check for updates ourselves rather than using 'updater' because we need to
// do it in the main process (and we don't really need to check every 10 minutes:
@@ -79,25 +63,18 @@ export function start(updateBaseUrl: string): void {
// will fail if running in debug mode
console.log('Couldn\'t enable update checking', err);
}
}
};
ipcMain.on('install_update', installUpdate);
ipcMain.on('check_updates', pollForUpdates);
function ipcChannelSendUpdateStatus(status: boolean | string): void {
function ipcChannelSendUpdateStatus(status) {
if (!global.mainWindow) return;
global.mainWindow.webContents.send('check_updates', status);
}
interface ICachedUpdate {
releaseNotes: string;
releaseName: string;
releaseDate: Date;
updateURL: string;
}
// cache the latest update which has been downloaded as electron offers no api to read it
let latestUpdateDownloaded: ICachedUpdate;
let latestUpdateDownloaded;
autoUpdater.on('update-available', function() {
ipcChannelSendUpdateStatus(true);
}).on('update-not-available', function() {

143
src/vectormenu.js Normal file
View File

@@ -0,0 +1,143 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const {app, shell, Menu} = require('electron');
// Menu template from http://electron.atom.io/docs/api/menu/, edited
const template = [
{
label: '&Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' },
],
},
{
label: '&View',
submenu: [
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin', accelerator: 'CommandOrControl+=' },
{ role: 'zoomout' },
{ type: 'separator' },
{
label: 'Preferences',
accelerator: 'Command+,', // Mac-only accelerator
click() { global.mainWindow.webContents.send('preferences'); },
},
{ role: 'togglefullscreen' },
{ role: 'toggledevtools' },
],
},
{
label: '&Window',
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'close' },
],
},
{
label: '&Help',
role: 'help',
submenu: [
{
label: 'Element Help',
click() { shell.openExternal('https://element.io/help'); },
},
],
},
];
// macOS has specific menu conventions...
if (process.platform === 'darwin') {
template.unshift({
// first macOS menu is the name of the app
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{
role: 'services',
submenu: [],
},
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
});
// Edit menu.
// This has a 'speech' section on macOS
template[1].submenu.push(
{ type: 'separator' },
{
label: 'Speech',
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' },
],
});
// Window menu.
// This also has specific functionality on macOS
template[3].submenu = [
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close',
},
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize',
},
{
label: 'Zoom',
role: 'zoom',
},
{
type: 'separator',
},
{
label: 'Bring All to Front',
role: 'front',
},
];
} else {
template.unshift({
label: '&File',
submenu: [
// For some reason, 'about' does not seem to work on windows.
/*{
role: 'about'
},*/
{ role: 'quit' },
],
});
}
module.exports = Menu.buildFromTemplate(template);

View File

@@ -1,246 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
import { _t } from './language-helper';
const isMac = process.platform === 'darwin';
export function buildMenuTemplate(): Menu {
// Menu template from http://electron.atom.io/docs/api/menu/, edited
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
{
label: _t('Edit'),
accelerator: 'e',
submenu: [
{
role: 'undo',
label: _t('Undo'),
},
{
role: 'redo',
label: _t('Redo'),
},
{ type: 'separator' },
{
role: 'cut',
label: _t('Cut'),
},
{
role: 'copy',
label: _t('Copy'),
},
{
role: 'paste',
label: _t('Paste'),
},
{
role: 'pasteAndMatchStyle',
label: _t('Paste and Match Style'),
},
{
role: 'delete',
label: _t('Delete'),
},
{
role: 'selectAll',
label: _t('Select All'),
},
],
},
{
label: _t('View'),
accelerator: 'V',
submenu: [
{ type: 'separator' },
{
role: 'resetZoom',
accelerator: 'CmdOrCtrl+Num0',
visible: false,
},
{
role: 'zoomIn',
accelerator: 'CmdOrCtrl+NumAdd',
visible: false,
},
{
role: 'zoomOut',
accelerator: 'CmdOrCtrl+NumSub',
visible: false,
},
{
role: 'resetZoom',
label: _t('Actual Size'),
},
{
role: 'zoomIn',
label: _t('Zoom In'),
},
{
role: 'zoomOut',
label: _t('Zoom Out'),
},
{ type: 'separator' },
// in macOS the Preferences menu item goes in the first menu
...(!isMac ? [{
label: _t('Preferences'),
click() { global.mainWindow.webContents.send('preferences'); },
}] : []),
{
role: 'togglefullscreen',
label: _t('Toggle Full Screen'),
},
{
role: 'toggleDevTools',
label: _t('Toggle Developer Tools'),
},
],
},
{
label: _t('Window'),
accelerator: 'w',
role: 'window',
submenu: [
{
role: 'minimize',
label: _t('Minimize'),
},
{
role: 'close',
label: _t('Close'),
},
],
},
{
label: _t('Help'),
accelerator: 'h',
role: 'help',
submenu: [
{
label: _t('Element Help'),
click() { shell.openExternal('https://element.io/help'); },
},
],
},
];
// macOS has specific menu conventions...
if (isMac) {
template.unshift({
// first macOS menu is the name of the app
role: 'appMenu',
label: app.name,
submenu: [
{
role: 'about',
label: _t('About') + ' ' + app.name,
},
{ type: 'separator' },
{
label: _t('Preferences') + '…',
accelerator: 'Command+,', // Mac-only accelerator
click() { global.mainWindow.webContents.send('preferences'); },
},
{ type: 'separator' },
{
role: 'services',
label: _t('Services'),
submenu: [],
},
{ type: 'separator' },
{
role: 'hide',
label: _t('Hide'),
},
{
role: 'hideOthers',
label: _t('Hide Others'),
},
{
role: 'unhide',
label: _t('Unhide'),
},
{ type: 'separator' },
{
role: 'quit',
label: _t('Quit'),
},
],
});
// Edit menu.
// This has a 'speech' section on macOS
(template[1].submenu as MenuItemConstructorOptions[]).push(
{ type: 'separator' },
{
label: _t('Speech'),
submenu: [
{
role: 'startSpeaking',
label: _t('Start Speaking'),
},
{
role: 'stopSpeaking',
label: _t('Stop Speaking'),
},
],
});
// Window menu.
// This also has specific functionality on macOS
template[3].submenu = [
{
label: _t('Close'),
accelerator: 'CmdOrCtrl+W',
role: 'close',
},
{
label: _t('Minimize'),
accelerator: 'CmdOrCtrl+M',
role: 'minimize',
},
{
label: _t('Zoom'),
role: 'zoom',
},
{
type: 'separator',
},
{
label: _t('Bring All to Front'),
role: 'front',
},
];
} else {
template.unshift({
label: _t('File'),
accelerator: 'f',
submenu: [
// For some reason, 'about' does not seem to work on windows.
/*{
role: 'about',
label: _t('About'),
},*/
{
role: 'quit',
label: _t('Quit'),
},
],
});
}
return Menu.buildFromTemplate(template);
}

View File

@@ -1,50 +1,18 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
clipboard,
nativeImage,
Menu,
MenuItem,
shell,
dialog,
ipcMain,
NativeImage,
WebContents,
ContextMenuParams,
DownloadItem,
MenuItemConstructorOptions,
IpcMainEvent,
} from 'electron';
import url from 'url';
import fs from 'fs';
import request from 'request';
import path from 'path';
import { _t } from './language-helper';
const {clipboard, nativeImage, Menu, MenuItem, shell, dialog, ipcMain} = require('electron');
const url = require('url');
const fs = require('fs');
const request = require('request');
const path = require('path');
const MAILTO_PREFIX = "mailto:";
const PERMITTED_URL_SCHEMES: string[] = [
const PERMITTED_URL_SCHEMES = [
'http:',
'https:',
MAILTO_PREFIX,
];
function safeOpenURL(target: string): void {
function safeOpenURL(target) {
// openExternal passes the target to open/start/xdg-open,
// so put fairly stringent limits on what can be opened
// (for instance, open /bin/sh does indeed open a terminal
@@ -59,7 +27,7 @@ function safeOpenURL(target: string): void {
}
}
function onWindowOrNavigate(ev: Event, target: string): void {
function onWindowOrNavigate(ev, target) {
// always prevent the default: if something goes wrong,
// we don't want to end up opening it in the electron
// app, as we could end up opening any sort of random
@@ -68,7 +36,7 @@ function onWindowOrNavigate(ev: Event, target: string): void {
safeOpenURL(target);
}
function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
function writeNativeImage(filePath, img) {
switch (filePath.split('.').pop().toLowerCase()) {
case "jpg":
case "jpeg":
@@ -81,7 +49,8 @@ function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
}
}
function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void {
function onLinkContextMenu(ev, params) {
let url = params.linkURL || params.srcURL;
if (url.startsWith('vector://vector/webapp')) {
@@ -104,10 +73,9 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
if (params.hasImageContents) {
popupMenu.append(new MenuItem({
label: _t('Copy image'),
accelerator: 'c',
label: '&Copy image',
click() {
webContents.copyImageAt(params.x, params.y);
ev.sender.copyImageAt(params.x, params.y);
},
}));
}
@@ -117,16 +85,14 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do
if (url.startsWith(MAILTO_PREFIX)) {
popupMenu.append(new MenuItem({
label: _t('Copy email address'),
accelerator: 'a',
label: 'Copy email &address',
click() {
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
},
}));
} else {
popupMenu.append(new MenuItem({
label: _t('Copy link address'),
accelerator: 'a',
label: 'Copy link &address',
click() {
clipboard.writeText(url);
},
@@ -138,11 +104,10 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
// only the renderer can resolve them so don't give the user an option to.
if (params.hasImageContents && !url.startsWith('blob:')) {
popupMenu.append(new MenuItem({
label: _t('Save image as...'),
accelerator: 's',
label: 'Sa&ve image as...',
async click() {
const targetFileName = params.titleText || "image.png";
const { filePath } = await dialog.showSaveDialog({
const {filePath} = await dialog.showSaveDialog({
defaultPath: targetFileName,
});
@@ -158,8 +123,8 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
console.error(err);
dialog.showMessageBox({
type: "error",
title: _t("Failed to save image"),
message: _t("The image failed to save"),
title: "Failed to save image",
message: "The image failed to save",
});
}
},
@@ -171,8 +136,8 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
ev.preventDefault();
}
function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemConstructorOptions[] {
const options: MenuItemConstructorOptions[] = [];
function _CutCopyPasteSelectContextMenus(params) {
const options = [];
if (params.misspelledWord) {
params.dictionarySuggestions.forEach(word => {
@@ -186,7 +151,7 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
options.push({
type: 'separator',
}, {
label: _t('Add to dictionary'),
label: 'Add to dictionary',
click: (menuItem, browserWindow) => {
browserWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
},
@@ -197,33 +162,29 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
options.push({
role: 'cut',
label: _t('Cut'),
accelerator: 't',
label: 'Cu&t',
enabled: params.editFlags.canCut,
}, {
role: 'copy',
label: _t('Copy'),
accelerator: 'c',
label: '&Copy',
enabled: params.editFlags.canCopy,
}, {
role: 'paste',
label: _t('Paste'),
accelerator: 'p',
label: '&Paste',
enabled: params.editFlags.canPaste,
}, {
role: 'pasteAndMatchStyle',
role: 'pasteandmatchstyle',
enabled: params.editFlags.canPaste,
}, {
role: 'selectAll',
label: _t("Select All"),
accelerator: 'a',
role: 'selectall',
label: "Select &All",
enabled: params.editFlags.canSelectAll,
});
return options;
}
function onSelectedContextMenu(ev, params) {
const items = cutCopyPasteSelectContextMenus(params);
const items = _CutCopyPasteSelectContextMenus(params);
const popupMenu = Menu.buildFromTemplate(items);
// popup() requires an options object even for no options
@@ -231,13 +192,12 @@ function onSelectedContextMenu(ev, params) {
ev.preventDefault();
}
function onEditableContextMenu(ev: Event, params: ContextMenuParams) {
const items: MenuItemConstructorOptions[] = [
function onEditableContextMenu(ev, params) {
const items = [
{ role: 'undo' },
{ role: 'redo', enabled: params.editFlags.canRedo },
{ type: 'separator' },
...cutCopyPasteSelectContextMenus(params),
];
].concat(_CutCopyPasteSelectContextMenus(params));
const popupMenu = Menu.buildFromTemplate(items);
@@ -246,25 +206,16 @@ function onEditableContextMenu(ev: Event, params: ContextMenuParams) {
ev.preventDefault();
}
let userDownloadIndex = 0;
const userDownloadMap = new Map<number, string>(); // Map from id to path
ipcMain.on('userDownloadAction', function(ev: IpcMainEvent, { id, open = false }) {
if (open) {
shell.openPath(userDownloadMap.get(id));
}
userDownloadMap.delete(id);
});
export default (webContents: WebContents): void => {
module.exports = (webContents) => {
webContents.on('new-window', onWindowOrNavigate);
webContents.on('will-navigate', (ev: Event, target: string): void => {
webContents.on('will-navigate', (ev, target) => {
if (target.startsWith("vector://")) return;
return onWindowOrNavigate(ev, target);
});
webContents.on('context-menu', function(ev: Event, params: ContextMenuParams): void {
webContents.on('context-menu', function(ev, params) {
if (params.linkURL || params.srcURL) {
onLinkContextMenu(ev, params, webContents);
onLinkContextMenu(ev, params);
} else if (params.selectionText) {
onSelectedContextMenu(ev, params);
} else if (params.isEditable) {
@@ -272,16 +223,61 @@ export default (webContents: WebContents): void => {
}
});
webContents.session.on('will-download', (event: Event, item: DownloadItem): void => {
item.once('done', (event, state) => {
if (state === 'completed') {
const savePath = item.getSavePath();
const id = userDownloadIndex++;
userDownloadMap.set(id, savePath);
webContents.send('userDownloadCompleted', {
id,
name: path.basename(savePath),
});
webContents.session.on('will-download', (event, item) => {
let started = false;
const ipcHandler = function(ev, {action, path}) {
if (path !== item.savePath) return;
switch (action) {
case "download":
shell.openPath(path);
ipcMain.off("userDownload", ipcHandler);
break;
case "pause":
item.pause();
break;
case "resume":
item.resume();
break;
case "cancel":
item.cancel();
ipcMain.off("userDownload", ipcHandler);
break;
case "done":
ipcMain.off("userDownload", ipcHandler);
break;
}
};
ipcMain.on("userDownload", ipcHandler);
item.on("updated", (event, state) => {
if (!item.savePath) return;
webContents.send("userDownload", {
state,
path: item.savePath,
name: path.basename(item.savePath),
totalBytes: item.getTotalBytes(),
receivedBytes: item.getReceivedBytes(),
begin: !started,
});
if (!started) started = true;
});
item.once("done", (event, state) => {
if (!item.savePath) return;
webContents.send("userDownload", {
state,
path: item.savePath,
name: path.basename(item.savePath),
totalBytes: item.getTotalBytes(),
receivedBytes: item.getReceivedBytes(),
terminal: true,
});
if (state === "interrupted") {
ipcMain.off("userDownload", ipcHandler);
}
});
});

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"lib": [
"es2019",
"dom"
]
},
"include": [
"./src/**/*.ts"
]
}

6106
yarn.lock
View File

File diff suppressed because it is too large Load Diff