Merge branch 'main' into bundle-ml-demo
This commit is contained in:
parent
df1a63e384
commit
27876e5b6d
49 changed files with 1320 additions and 639 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -2,3 +2,7 @@
|
||||||
path = ui
|
path = ui
|
||||||
url = https://github.com/ente-io/bada-frame
|
url = https://github.com/ente-io/bada-frame
|
||||||
branch = demo
|
branch = demo
|
||||||
|
[submodule "thirdparty/next-electron-server"]
|
||||||
|
path = thirdparty/next-electron-server
|
||||||
|
url = https://github.com/ente-io/next-electron-server.git
|
||||||
|
branch = desktop
|
||||||
|
|
28
README.md
28
README.md
|
@ -25,27 +25,35 @@ If you are running into issues with this app, please drop a mail to [support@ent
|
||||||
- [AUR](https://aur.archlinux.org/packages/ente-desktop-appimage):
|
- [AUR](https://aur.archlinux.org/packages/ente-desktop-appimage):
|
||||||
`yay -S ente-desktop-appimage`
|
`yay -S ente-desktop-appimage`
|
||||||
|
|
||||||
## Develop
|
## Building from source
|
||||||
|
|
||||||
To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
|
You'll need to have node (and yarn) installed on your machine. e.g. on macOS you
|
||||||
|
can do `brew install node`. After that, you can run the following commands to
|
||||||
|
fetch and build from source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone this repository
|
# Clone this repository
|
||||||
git clone https://github.com/ente-io/bhari-frame
|
git clone https://github.com/ente-io/bhari-frame
|
||||||
|
|
||||||
# Go into the repository
|
# Go into the repository
|
||||||
cd bhari-frame
|
cd bhari-frame
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
# Run the app
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt.
|
# Clone submodules (recursively)
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
yarn
|
||||||
|
|
||||||
|
# Run the app
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
### Re-compile automatically
|
### Re-compile automatically
|
||||||
|
|
||||||
To recompile automatically and to allow using [electron-reload](https://github.com/yan-foto/electron-reload), run this in a separate terminal:
|
To recompile automatically and to allow using
|
||||||
|
[electron-reload](https://github.com/yan-foto/electron-reload), run this in a
|
||||||
|
separate terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run watch
|
yarn watch
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { app, BrowserWindow, Menu, Tray, nativeImage } from 'electron';
|
|
||||||
import * as path from 'path';
|
|
||||||
import AppUpdater from './utils/appUpdater';
|
|
||||||
import { createWindow } from './utils/createWindow';
|
|
||||||
import setupIpcComs from './utils/ipcComms';
|
|
||||||
import { buildContextMenu, buildMenuBar } from './utils/menuUtil';
|
|
||||||
import initSentry from './utils/sentry';
|
|
||||||
import electronReload from 'electron-reload';
|
|
||||||
import { PROD_HOST_URL, RENDERER_OUTPUT_DIR } from './config';
|
|
||||||
import { isDev } from './utils/common';
|
|
||||||
import serveNextAt from 'next-electron-server';
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
electronReload(__dirname, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
let tray: Tray;
|
|
||||||
let mainWindow: BrowserWindow;
|
|
||||||
|
|
||||||
let appIsQuitting = false;
|
|
||||||
|
|
||||||
let updateIsAvailable = false;
|
|
||||||
|
|
||||||
export const isAppQuitting = (): boolean => {
|
|
||||||
return appIsQuitting;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setIsAppQuitting = (value: boolean): void => {
|
|
||||||
appIsQuitting = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isUpdateAvailable = (): boolean => {
|
|
||||||
return updateIsAvailable;
|
|
||||||
};
|
|
||||||
export const setIsUpdateAvailable = (value: boolean): void => {
|
|
||||||
updateIsAvailable = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
serveNextAt(PROD_HOST_URL, {
|
|
||||||
outputDir: RENDERER_OUTPUT_DIR,
|
|
||||||
});
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit();
|
|
||||||
} else {
|
|
||||||
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer');
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show();
|
|
||||||
if (mainWindow.isMinimized()) {
|
|
||||||
mainWindow.restore();
|
|
||||||
}
|
|
||||||
mainWindow.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
|
||||||
// initialization and is ready to create browser windows.
|
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.on('ready', () => {
|
|
||||||
initSentry();
|
|
||||||
setIsUpdateAvailable(false);
|
|
||||||
mainWindow = createWindow();
|
|
||||||
Menu.setApplicationMenu(buildMenuBar());
|
|
||||||
|
|
||||||
app.on('activate', function () {
|
|
||||||
// On macOS it's common to re-create a window in the app when the
|
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
const trayImgPath = isDev
|
|
||||||
? 'build/taskbar-icon.png'
|
|
||||||
: path.join(process.resourcesPath, 'taskbar-icon.png');
|
|
||||||
const trayIcon = nativeImage.createFromPath(trayImgPath);
|
|
||||||
tray = new Tray(trayIcon);
|
|
||||||
tray.setToolTip('ente');
|
|
||||||
tray.setContextMenu(buildContextMenu(mainWindow));
|
|
||||||
|
|
||||||
setupIpcComs(tray, mainWindow);
|
|
||||||
if (!isDev) {
|
|
||||||
AppUpdater.checkForUpdate(tray, mainWindow);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
199
main/preload.ts
199
main/preload.ts
|
@ -1,199 +0,0 @@
|
||||||
import { Readable } from 'stream';
|
|
||||||
import * as fs from 'promise-fs';
|
|
||||||
import { webFrame, ipcRenderer } from 'electron';
|
|
||||||
import {
|
|
||||||
getElectronFile,
|
|
||||||
getPendingUploads,
|
|
||||||
setToUploadFiles,
|
|
||||||
getElectronFilesFromGoogleZip,
|
|
||||||
setToUploadCollection,
|
|
||||||
} from './utils/upload';
|
|
||||||
import { logError } from './utils/logging';
|
|
||||||
import { ElectronFile } from './types';
|
|
||||||
import { getEncryptionKey, setEncryptionKey } from './utils/safeStorage';
|
|
||||||
import { clearElectronStore } from './utils/electronStore';
|
|
||||||
import { openDiskCache, deleteDiskCache } from './utils/cache';
|
|
||||||
|
|
||||||
// Patch the global WebSocket constructor to use the correct DevServer url
|
|
||||||
const fixHotReloadNext12 = () => {
|
|
||||||
webFrame.executeJavaScript(`Object.defineProperty(globalThis, 'WebSocket', {
|
|
||||||
value: new Proxy(WebSocket, {
|
|
||||||
construct: (Target, [url, protocols]) => {
|
|
||||||
if (url.endsWith('/_next/webpack-hmr')) {
|
|
||||||
// Fix the Next.js hmr client url
|
|
||||||
return new Target("ws://localhost:3000/_next/webpack-hmr", protocols)
|
|
||||||
} else {
|
|
||||||
return new Target(url, protocols)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});`);
|
|
||||||
};
|
|
||||||
|
|
||||||
fixHotReloadNext12();
|
|
||||||
|
|
||||||
const responseToReadable = (fileStream: any) => {
|
|
||||||
const reader = fileStream.getReader();
|
|
||||||
const rs = new Readable();
|
|
||||||
rs._read = async () => {
|
|
||||||
const result = await reader.read();
|
|
||||||
if (!result.done) {
|
|
||||||
rs.push(Buffer.from(result.value));
|
|
||||||
} else {
|
|
||||||
rs.push(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return rs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const exists = (path: string) => {
|
|
||||||
return fs.existsSync(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkExistsAndCreateCollectionDir = async (dirPath: string) => {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
await fs.mkdir(dirPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkExistsAndRename = async (oldDirPath: string, newDirPath: string) => {
|
|
||||||
if (fs.existsSync(oldDirPath)) {
|
|
||||||
await fs.rename(oldDirPath, newDirPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveStreamToDisk = (path: string, fileStream: ReadableStream<any>) => {
|
|
||||||
const writeable = fs.createWriteStream(path);
|
|
||||||
const readable = responseToReadable(fileStream);
|
|
||||||
readable.pipe(writeable);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveFileToDisk = async (path: string, file: any) => {
|
|
||||||
await fs.writeFile(path, file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectRootDirectory = async () => {
|
|
||||||
try {
|
|
||||||
return await ipcRenderer.invoke('select-dir');
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'error while selecting root directory');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendNotification = (content: string) => {
|
|
||||||
ipcRenderer.send('send-notification', content);
|
|
||||||
};
|
|
||||||
const showOnTray = (content: string) => {
|
|
||||||
ipcRenderer.send('update-tray', content);
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerResumeExportListener = (resumeExport: () => void) => {
|
|
||||||
ipcRenderer.removeAllListeners('resume-export');
|
|
||||||
ipcRenderer.on('resume-export', () => resumeExport());
|
|
||||||
};
|
|
||||||
const registerStopExportListener = (abortExport: () => void) => {
|
|
||||||
ipcRenderer.removeAllListeners('stop-export');
|
|
||||||
ipcRenderer.on('stop-export', () => abortExport());
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerPauseExportListener = (pauseExport: () => void) => {
|
|
||||||
ipcRenderer.removeAllListeners('pause-export');
|
|
||||||
ipcRenderer.on('pause-export', () => pauseExport());
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerRetryFailedExportListener = (retryFailedExport: () => void) => {
|
|
||||||
ipcRenderer.removeAllListeners('retry-export');
|
|
||||||
ipcRenderer.on('retry-export', () => retryFailedExport());
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadWindow = () => {
|
|
||||||
ipcRenderer.send('reload-window');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExportRecord = async (filePath: string) => {
|
|
||||||
try {
|
|
||||||
const filepath = `${filePath}`;
|
|
||||||
const recordFile = await fs.readFile(filepath, 'utf-8');
|
|
||||||
return recordFile;
|
|
||||||
} catch (e) {
|
|
||||||
// ignore exportFile missing
|
|
||||||
logError(e, 'error while selecting files');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setExportRecord = async (filePath: string, data: string) => {
|
|
||||||
const filepath = `${filePath}`;
|
|
||||||
await fs.writeFile(filepath, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showUploadFilesDialog = async () => {
|
|
||||||
try {
|
|
||||||
const filePaths: string[] = await ipcRenderer.invoke(
|
|
||||||
'show-upload-files-dialog'
|
|
||||||
);
|
|
||||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
|
||||||
return files;
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'error while selecting files');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showUploadDirsDialog = async () => {
|
|
||||||
try {
|
|
||||||
const filePaths: string[] = await ipcRenderer.invoke(
|
|
||||||
'show-upload-dirs-dialog'
|
|
||||||
);
|
|
||||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
|
||||||
return files;
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'error while selecting folders');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showUploadZipDialog = async () => {
|
|
||||||
try {
|
|
||||||
const filePaths: string[] = await ipcRenderer.invoke(
|
|
||||||
'show-upload-zip-dialog'
|
|
||||||
);
|
|
||||||
const files: ElectronFile[] = [];
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
files.push(...(await getElectronFilesFromGoogleZip(filePath)));
|
|
||||||
}
|
|
||||||
return { zipPaths: filePaths, files };
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'error while selecting zips');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const windowObject: any = window;
|
|
||||||
windowObject['ElectronAPIs'] = {
|
|
||||||
exists,
|
|
||||||
checkExistsAndCreateCollectionDir,
|
|
||||||
checkExistsAndRename,
|
|
||||||
saveStreamToDisk,
|
|
||||||
saveFileToDisk,
|
|
||||||
selectRootDirectory,
|
|
||||||
sendNotification,
|
|
||||||
showOnTray,
|
|
||||||
reloadWindow,
|
|
||||||
registerResumeExportListener,
|
|
||||||
registerStopExportListener,
|
|
||||||
registerPauseExportListener,
|
|
||||||
registerRetryFailedExportListener,
|
|
||||||
getExportRecord,
|
|
||||||
setExportRecord,
|
|
||||||
getElectronFile,
|
|
||||||
showUploadFilesDialog,
|
|
||||||
showUploadDirsDialog,
|
|
||||||
getPendingUploads,
|
|
||||||
setToUploadFiles,
|
|
||||||
showUploadZipDialog,
|
|
||||||
getElectronFilesFromGoogleZip,
|
|
||||||
setToUploadCollection,
|
|
||||||
getEncryptionKey,
|
|
||||||
setEncryptionKey,
|
|
||||||
clearElectronStore,
|
|
||||||
openDiskCache,
|
|
||||||
deleteDiskCache,
|
|
||||||
};
|
|
|
@ -1,52 +0,0 @@
|
||||||
import Store, { Schema } from 'electron-store';
|
|
||||||
import { KeysStoreType, SafeStorageStoreType, UploadStoreType } from '../types';
|
|
||||||
|
|
||||||
export const uploadStoreSchema: Schema<UploadStoreType> = {
|
|
||||||
filePaths: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
zipPaths: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
collectionName: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const uploadStatusStore = new Store({
|
|
||||||
name: 'upload-status',
|
|
||||||
schema: uploadStoreSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const keysStoreSchema: Schema<KeysStoreType> = {
|
|
||||||
AnonymizeUserID: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const keysStore = new Store({
|
|
||||||
name: 'keys',
|
|
||||||
schema: keysStoreSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const safeStorageSchema: Schema<SafeStorageStoreType> = {
|
|
||||||
encryptionKey: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const safeStorageStore = new Store({
|
|
||||||
name: 'safeStorage',
|
|
||||||
schema: safeStorageSchema,
|
|
||||||
});
|
|
15
package.json
15
package.json
|
@ -4,7 +4,7 @@
|
||||||
"version": "1.7.0-alpha.7",
|
"version": "1.7.0-alpha.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Desktop client for ente.io",
|
"description": "Desktop client for ente.io",
|
||||||
"main": "app/index.js",
|
"main": "app/main.js",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "io.ente.bhari-frame",
|
"appId": "io.ente.bhari-frame",
|
||||||
"artifactName": "${productName}-${version}.${ext}",
|
"artifactName": "${productName}-${version}.${ext}",
|
||||||
|
@ -53,12 +53,12 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"prebuild": "eslint \"main/**/*.{js,jsx,ts,tsx}\"",
|
"prebuild": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"lint": "eslint -c .eslintrc --ext .ts ./main",
|
"lint": "eslint -c .eslintrc --ext .ts src",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"build-main": "yarn install && tsc",
|
"build-main": "yarn install && tsc",
|
||||||
"start-main": "yarn build-main && electron app/index.js",
|
"start-main": "yarn build-main && electron app/main.js",
|
||||||
"start-renderer": "cd ui && yarn install && yarn dev",
|
"start-renderer": "cd ui && yarn install && yarn dev",
|
||||||
"start": "concurrently \"yarn start-main\" \"yarn start-renderer\"",
|
"start": "concurrently \"yarn start-main\" \"yarn start-renderer\"",
|
||||||
"build-renderer": "cd ui && yarn install && yarn build && cd ..",
|
"build-renderer": "cd ui && yarn install && yarn build && cd ..",
|
||||||
|
@ -68,6 +68,7 @@
|
||||||
"author": "ente <code@ente.io>",
|
"author": "ente <code@ente.io>",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/cli": "^1.68.0",
|
"@sentry/cli": "^1.68.0",
|
||||||
|
"@types/auto-launch": "^5.0.2",
|
||||||
"@types/get-folder-size": "^2.0.0",
|
"@types/get-folder-size": "^2.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||||
"@typescript-eslint/parser": "^5.28.0",
|
"@typescript-eslint/parser": "^5.28.0",
|
||||||
|
@ -88,12 +89,14 @@
|
||||||
"@sentry/electron": "^2.5.1",
|
"@sentry/electron": "^2.5.1",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
"@types/promise-fs": "^2.1.1",
|
"@types/promise-fs": "^2.1.1",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"auto-launch": "^5.0.5",
|
||||||
"electron-log": "^4.3.5",
|
"electron-log": "^4.3.5",
|
||||||
"electron-reload": "^2.0.0-alpha.1",
|
"electron-reload": "^2.0.0-alpha.1",
|
||||||
"electron-store": "^8.0.1",
|
"electron-store": "^8.0.1",
|
||||||
"electron-updater": "^4.3.8",
|
"electron-updater": "^4.3.8",
|
||||||
"get-folder-size": "^2.0.1",
|
"get-folder-size": "^2.0.1",
|
||||||
"next-electron-server": "^0.0.8",
|
"next-electron-server": "file:./thirdparty/next-electron-server",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "^1.15.0",
|
||||||
"promise-fs": "^2.1.1"
|
"promise-fs": "^2.1.1"
|
||||||
},
|
},
|
||||||
|
@ -101,7 +104,7 @@
|
||||||
"parser": "babel-eslint"
|
"parser": "babel-eslint"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"main/**/*.{js,jsx,ts,tsx}": [
|
"src/**/*.{js,jsx,ts,tsx}": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"prettier --write --ignore-unknown"
|
"prettier --write --ignore-unknown"
|
||||||
]
|
]
|
||||||
|
|
35
src/api/cache.ts
Normal file
35
src/api/cache.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { ipcRenderer } from 'electron/renderer';
|
||||||
|
import path from 'path';
|
||||||
|
import { existsSync, mkdir, rmSync } from 'promise-fs';
|
||||||
|
import { DiskCache } from '../services/diskCache';
|
||||||
|
|
||||||
|
const CACHE_DIR = 'ente';
|
||||||
|
|
||||||
|
const getCacheDir = async () => {
|
||||||
|
const systemCacheDir = await ipcRenderer.invoke('get-path', 'cache');
|
||||||
|
return path.join(systemCacheDir, CACHE_DIR);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCacheBucketDir = async (cacheName: string) => {
|
||||||
|
const cacheDir = await getCacheDir();
|
||||||
|
const cacheBucketDir = path.join(cacheDir, cacheName);
|
||||||
|
return cacheBucketDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function openDiskCache(cacheName: string) {
|
||||||
|
const cacheBucketDir = await getCacheBucketDir(cacheName);
|
||||||
|
if (!existsSync(cacheBucketDir)) {
|
||||||
|
await mkdir(cacheBucketDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return new DiskCache(cacheBucketDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDiskCache(cacheName: string) {
|
||||||
|
const cacheBucketDir = await getCacheBucketDir(cacheName);
|
||||||
|
if (existsSync(cacheBucketDir)) {
|
||||||
|
rmSync(cacheBucketDir, { recursive: true, force: true });
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
10
src/api/common.ts
Normal file
10
src/api/common.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ipcRenderer } from 'electron/renderer';
|
||||||
|
import { logError } from '../utils/logging';
|
||||||
|
|
||||||
|
export const selectRootDirectory = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await ipcRenderer.invoke('select-dir');
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'error while selecting root directory');
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,10 +1,7 @@
|
||||||
import {
|
import { keysStore } from '../stores/keys.store';
|
||||||
uploadStatusStore,
|
import { safeStorageStore } from '../stores/safeStorage.store';
|
||||||
keysStore,
|
import { uploadStatusStore } from '../stores/upload.store';
|
||||||
safeStorageStore,
|
import { logError } from '../utils/logging';
|
||||||
} from '../services/store';
|
|
||||||
|
|
||||||
import { logError } from './logging';
|
|
||||||
|
|
||||||
export const clearElectronStore = () => {
|
export const clearElectronStore = () => {
|
||||||
try {
|
try {
|
72
src/api/export.ts
Normal file
72
src/api/export.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { readTextFile, writeStream } from './../services/fs';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { logError } from '../utils/logging';
|
||||||
|
import * as fs from 'promise-fs';
|
||||||
|
|
||||||
|
export const exists = (path: string) => {
|
||||||
|
return fs.existsSync(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkExistsAndCreateCollectionDir = async (dirPath: string) => {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
await fs.mkdir(dirPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkExistsAndRename = async (
|
||||||
|
oldDirPath: string,
|
||||||
|
newDirPath: string
|
||||||
|
) => {
|
||||||
|
if (fs.existsSync(oldDirPath)) {
|
||||||
|
await fs.rename(oldDirPath, newDirPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveStreamToDisk = (
|
||||||
|
filePath: string,
|
||||||
|
fileStream: ReadableStream<any>
|
||||||
|
) => {
|
||||||
|
writeStream(filePath, fileStream);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveFileToDisk = async (path: string, fileData: any) => {
|
||||||
|
await fs.writeFile(path, fileData);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExportRecord = async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const recordFile = await readTextFile(filePath);
|
||||||
|
return recordFile;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'error while selecting files');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setExportRecord = async (filePath: string, data: string) => {
|
||||||
|
await fs.writeFile(filePath, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerResumeExportListener = (resumeExport: () => void) => {
|
||||||
|
ipcRenderer.removeAllListeners('resume-export');
|
||||||
|
ipcRenderer.on('resume-export', () => resumeExport());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerStopExportListener = (abortExport: () => void) => {
|
||||||
|
ipcRenderer.removeAllListeners('stop-export');
|
||||||
|
ipcRenderer.on('stop-export', () => abortExport());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerPauseExportListener = (pauseExport: () => void) => {
|
||||||
|
ipcRenderer.removeAllListeners('pause-export');
|
||||||
|
ipcRenderer.on('pause-export', () => pauseExport());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerRetryFailedExportListener = (
|
||||||
|
retryFailedExport: () => void
|
||||||
|
) => {
|
||||||
|
ipcRenderer.removeAllListeners('retry-export');
|
||||||
|
ipcRenderer.on('retry-export', () => retryFailedExport());
|
||||||
|
};
|
8
src/api/fs.ts
Normal file
8
src/api/fs.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { getElectronFile, getDirFilePaths } from '../services/fs';
|
||||||
|
|
||||||
|
export async function getDirFiles(dirPath: string) {
|
||||||
|
const files = await getDirFilePaths(dirPath);
|
||||||
|
const electronFiles = await Promise.all(files.map(getElectronFile));
|
||||||
|
return electronFiles;
|
||||||
|
}
|
||||||
|
export { isFolder } from '../services/fs';
|
|
@ -1,6 +1,6 @@
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import { safeStorageStore } from '../services/store';
|
import { safeStorageStore } from '../stores/safeStorage.store';
|
||||||
import { logError } from './logging';
|
import { logError } from '../utils/logging';
|
||||||
|
|
||||||
export async function setEncryptionKey(encryptionKey: string) {
|
export async function setEncryptionKey(encryptionKey: string) {
|
||||||
try {
|
try {
|
||||||
|
@ -16,7 +16,7 @@ export async function setEncryptionKey(encryptionKey: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEncryptionKey() {
|
export async function getEncryptionKey(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const b64EncryptedKey = safeStorageStore.get('encryptionKey');
|
const b64EncryptedKey = safeStorageStore.get('encryptionKey');
|
||||||
if (b64EncryptedKey) {
|
if (b64EncryptedKey) {
|
11
src/api/system.ts
Normal file
11
src/api/system.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
export const sendNotification = (content: string) => {
|
||||||
|
ipcRenderer.send('send-notification', content);
|
||||||
|
};
|
||||||
|
export const showOnTray = (content: string) => {
|
||||||
|
ipcRenderer.send('update-tray', content);
|
||||||
|
};
|
||||||
|
export const reloadWindow = () => {
|
||||||
|
ipcRenderer.send('reload-window');
|
||||||
|
};
|
84
src/api/upload.ts
Normal file
84
src/api/upload.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { getElectronFile } from './../services/fs';
|
||||||
|
import { uploadStatusStore } from '../stores/upload.store';
|
||||||
|
import { ElectronFile, FILE_PATH_TYPE } from '../types';
|
||||||
|
import { logError } from '../utils/logging';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import {
|
||||||
|
getElectronFilesFromGoogleZip,
|
||||||
|
getSavedFilePaths,
|
||||||
|
} from '../services/upload';
|
||||||
|
|
||||||
|
export const getPendingUploads = async () => {
|
||||||
|
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
|
||||||
|
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
|
||||||
|
const collectionName = uploadStatusStore.get('collectionName');
|
||||||
|
|
||||||
|
let files: ElectronFile[] = [];
|
||||||
|
let type: FILE_PATH_TYPE;
|
||||||
|
if (zipPaths.length) {
|
||||||
|
type = FILE_PATH_TYPE.ZIPS;
|
||||||
|
for (const zipPath of zipPaths) {
|
||||||
|
files.push(...(await getElectronFilesFromGoogleZip(zipPath)));
|
||||||
|
}
|
||||||
|
const pendingFilePaths = new Set(filePaths);
|
||||||
|
files = files.filter((file) => pendingFilePaths.has(file.path));
|
||||||
|
} else if (filePaths.length) {
|
||||||
|
type = FILE_PATH_TYPE.FILES;
|
||||||
|
files = await Promise.all(filePaths.map(getElectronFile));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
collectionName,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showUploadDirsDialog = async () => {
|
||||||
|
try {
|
||||||
|
const filePaths: string[] = await ipcRenderer.invoke(
|
||||||
|
'show-upload-dirs-dialog'
|
||||||
|
);
|
||||||
|
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||||
|
return files;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'error while selecting folders');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showUploadFilesDialog = async () => {
|
||||||
|
try {
|
||||||
|
const filePaths: string[] = await ipcRenderer.invoke(
|
||||||
|
'show-upload-files-dialog'
|
||||||
|
);
|
||||||
|
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||||
|
return files;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'error while selecting files');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showUploadZipDialog = async () => {
|
||||||
|
try {
|
||||||
|
const filePaths: string[] = await ipcRenderer.invoke(
|
||||||
|
'show-upload-zip-dialog'
|
||||||
|
);
|
||||||
|
const files: ElectronFile[] = [];
|
||||||
|
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
files.push(...(await getElectronFilesFromGoogleZip(filePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
zipPaths: filePaths,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'error while selecting zips');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
setToUploadFiles,
|
||||||
|
getElectronFilesFromGoogleZip,
|
||||||
|
setToUploadCollection,
|
||||||
|
} from '../services/upload';
|
117
src/api/watch.ts
Normal file
117
src/api/watch.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import { isMappingPresent } from '../utils/watch';
|
||||||
|
import path from 'path';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { ElectronFile, WatchMapping } from '../types';
|
||||||
|
import { getElectronFile } from '../services/fs';
|
||||||
|
import { getWatchMappings, setWatchMappings } from '../services/watch';
|
||||||
|
|
||||||
|
export async function addWatchMapping(
|
||||||
|
rootFolderName: string,
|
||||||
|
folderPath: string,
|
||||||
|
uploadStrategy: number
|
||||||
|
) {
|
||||||
|
folderPath = path.normalize(folderPath);
|
||||||
|
const watchMappings = getWatchMappings();
|
||||||
|
if (isMappingPresent(watchMappings, folderPath)) {
|
||||||
|
throw new Error(`Watch mapping already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ipcRenderer.invoke('add-watcher', {
|
||||||
|
dir: folderPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
watchMappings.push({
|
||||||
|
rootFolderName,
|
||||||
|
uploadStrategy,
|
||||||
|
folderPath,
|
||||||
|
syncedFiles: [],
|
||||||
|
ignoredFiles: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
setWatchMappings(watchMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeWatchMapping(folderPath: string) {
|
||||||
|
let watchMappings = getWatchMappings();
|
||||||
|
const watchMapping = watchMappings.find(
|
||||||
|
(mapping) => mapping.folderPath === folderPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watchMapping) {
|
||||||
|
throw new Error(`Watch mapping does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ipcRenderer.invoke('remove-watcher', {
|
||||||
|
dir: watchMapping.folderPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
watchMappings = watchMappings.filter(
|
||||||
|
(mapping) => mapping.folderPath !== watchMapping.folderPath
|
||||||
|
);
|
||||||
|
|
||||||
|
setWatchMappings(watchMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWatchMappingSyncedFiles(
|
||||||
|
folderPath: string,
|
||||||
|
files: WatchMapping['syncedFiles']
|
||||||
|
): void {
|
||||||
|
const watchMappings = getWatchMappings();
|
||||||
|
const watchMapping = watchMappings.find(
|
||||||
|
(mapping) => mapping.folderPath === folderPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watchMapping) {
|
||||||
|
throw Error(`Watch mapping not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchMapping.syncedFiles = files;
|
||||||
|
setWatchMappings(watchMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWatchMappingIgnoredFiles(
|
||||||
|
folderPath: string,
|
||||||
|
files: WatchMapping['ignoredFiles']
|
||||||
|
): void {
|
||||||
|
const watchMappings = getWatchMappings();
|
||||||
|
const watchMapping = watchMappings.find(
|
||||||
|
(mapping) => mapping.folderPath === folderPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watchMapping) {
|
||||||
|
throw Error(`Watch mapping not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchMapping.ignoredFiles = files;
|
||||||
|
setWatchMappings(watchMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerWatcherFunctions(
|
||||||
|
addFile: (file: ElectronFile) => Promise<void>,
|
||||||
|
removeFile: (path: string) => Promise<void>,
|
||||||
|
removeFolder: (folderPath: string) => Promise<void>
|
||||||
|
) {
|
||||||
|
ipcRenderer.removeAllListeners('watch-add');
|
||||||
|
ipcRenderer.removeAllListeners('watch-change');
|
||||||
|
ipcRenderer.removeAllListeners('watch-unlink');
|
||||||
|
ipcRenderer.on('watch-add', async (_, filePath: string) => {
|
||||||
|
filePath = path.normalize(
|
||||||
|
filePath.split(path.sep).join(path.posix.sep)
|
||||||
|
);
|
||||||
|
await addFile(await getElectronFile(filePath));
|
||||||
|
});
|
||||||
|
ipcRenderer.on('watch-unlink', async (_, filePath: string) => {
|
||||||
|
filePath = path.normalize(
|
||||||
|
filePath.split(path.sep).join(path.posix.sep)
|
||||||
|
);
|
||||||
|
await removeFile(filePath);
|
||||||
|
});
|
||||||
|
ipcRenderer.on('watch-unlink-dir', async (_, folderPath: string) => {
|
||||||
|
folderPath = path.normalize(
|
||||||
|
folderPath.split(path.sep).join(path.posix.sep)
|
||||||
|
);
|
||||||
|
await removeFolder(folderPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getWatchMappings } from '../services/watch';
|
78
src/main.ts
Normal file
78
src/main.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
import { createWindow } from './utils/createWindow';
|
||||||
|
import setupIpcComs from './utils/ipcComms';
|
||||||
|
import { initWatcher } from './services/chokidar';
|
||||||
|
import { addAllowOriginHeader } from './utils/cors';
|
||||||
|
import {
|
||||||
|
setupTrayItem,
|
||||||
|
handleUpdates,
|
||||||
|
handleDownloads,
|
||||||
|
setupMacWindowOnDockIconClick,
|
||||||
|
setupMainMenu,
|
||||||
|
setupMainHotReload,
|
||||||
|
setupNextElectronServe,
|
||||||
|
enableSharedArrayBufferSupport,
|
||||||
|
handleDockIconHideOnAutoLaunch,
|
||||||
|
} from './utils/main';
|
||||||
|
import { initSentry } from './services/sentry';
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow;
|
||||||
|
|
||||||
|
let appIsQuitting = false;
|
||||||
|
|
||||||
|
let updateIsAvailable = false;
|
||||||
|
|
||||||
|
export const isAppQuitting = (): boolean => {
|
||||||
|
return appIsQuitting;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setIsAppQuitting = (value: boolean): void => {
|
||||||
|
appIsQuitting = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isUpdateAvailable = (): boolean => {
|
||||||
|
return updateIsAvailable;
|
||||||
|
};
|
||||||
|
export const setIsUpdateAvailable = (value: boolean): void => {
|
||||||
|
updateIsAvailable = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
setupMainHotReload();
|
||||||
|
|
||||||
|
setupNextElectronServe();
|
||||||
|
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
handleDockIconHideOnAutoLaunch();
|
||||||
|
enableSharedArrayBufferSupport();
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.on('ready', async () => {
|
||||||
|
mainWindow = await createWindow();
|
||||||
|
const tray = setupTrayItem(mainWindow);
|
||||||
|
const watcher = initWatcher(mainWindow);
|
||||||
|
setupMacWindowOnDockIconClick();
|
||||||
|
initSentry();
|
||||||
|
setupMainMenu();
|
||||||
|
setupIpcComs(tray, mainWindow, watcher);
|
||||||
|
handleUpdates(mainWindow, tray);
|
||||||
|
handleDownloads(mainWindow);
|
||||||
|
addAllowOriginHeader(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => setIsAppQuitting(true));
|
||||||
|
}
|
79
src/preload.ts
Normal file
79
src/preload.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { reloadWindow, sendNotification, showOnTray } from './api/system';
|
||||||
|
import {
|
||||||
|
showUploadDirsDialog,
|
||||||
|
showUploadFilesDialog,
|
||||||
|
showUploadZipDialog,
|
||||||
|
getPendingUploads,
|
||||||
|
setToUploadFiles,
|
||||||
|
getElectronFilesFromGoogleZip,
|
||||||
|
setToUploadCollection,
|
||||||
|
} from './api/upload';
|
||||||
|
import {
|
||||||
|
registerWatcherFunctions,
|
||||||
|
addWatchMapping,
|
||||||
|
removeWatchMapping,
|
||||||
|
updateWatchMappingSyncedFiles,
|
||||||
|
updateWatchMappingIgnoredFiles,
|
||||||
|
getWatchMappings,
|
||||||
|
} from './api/watch';
|
||||||
|
import { getEncryptionKey, setEncryptionKey } from './api/safeStorage';
|
||||||
|
import { clearElectronStore } from './api/electronStore';
|
||||||
|
import { openDiskCache, deleteDiskCache } from './api/cache';
|
||||||
|
import {
|
||||||
|
checkExistsAndCreateCollectionDir,
|
||||||
|
checkExistsAndRename,
|
||||||
|
saveStreamToDisk,
|
||||||
|
saveFileToDisk,
|
||||||
|
registerResumeExportListener,
|
||||||
|
registerStopExportListener,
|
||||||
|
registerPauseExportListener,
|
||||||
|
registerRetryFailedExportListener,
|
||||||
|
getExportRecord,
|
||||||
|
setExportRecord,
|
||||||
|
exists,
|
||||||
|
} from './api/export';
|
||||||
|
import { selectRootDirectory } from './api/common';
|
||||||
|
import { fixHotReloadNext12 } from './utils/preload';
|
||||||
|
import { isFolder, getDirFiles } from './api/fs';
|
||||||
|
|
||||||
|
fixHotReloadNext12();
|
||||||
|
|
||||||
|
const windowObject: any = window;
|
||||||
|
|
||||||
|
windowObject['ElectronAPIs'] = {
|
||||||
|
exists,
|
||||||
|
checkExistsAndCreateCollectionDir,
|
||||||
|
checkExistsAndRename,
|
||||||
|
saveStreamToDisk,
|
||||||
|
saveFileToDisk,
|
||||||
|
selectRootDirectory,
|
||||||
|
clearElectronStore,
|
||||||
|
sendNotification,
|
||||||
|
showOnTray,
|
||||||
|
reloadWindow,
|
||||||
|
registerResumeExportListener,
|
||||||
|
registerStopExportListener,
|
||||||
|
registerPauseExportListener,
|
||||||
|
registerRetryFailedExportListener,
|
||||||
|
getExportRecord,
|
||||||
|
setExportRecord,
|
||||||
|
showUploadFilesDialog,
|
||||||
|
showUploadDirsDialog,
|
||||||
|
getPendingUploads,
|
||||||
|
setToUploadFiles,
|
||||||
|
showUploadZipDialog,
|
||||||
|
getElectronFilesFromGoogleZip,
|
||||||
|
setToUploadCollection,
|
||||||
|
getEncryptionKey,
|
||||||
|
setEncryptionKey,
|
||||||
|
openDiskCache,
|
||||||
|
deleteDiskCache,
|
||||||
|
getDirFiles,
|
||||||
|
getWatchMappings,
|
||||||
|
addWatchMapping,
|
||||||
|
removeWatchMapping,
|
||||||
|
registerWatcherFunctions,
|
||||||
|
isFolder,
|
||||||
|
updateWatchMappingSyncedFiles,
|
||||||
|
updateWatchMappingIgnoredFiles,
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { BrowserWindow, dialog, Tray } from 'electron';
|
import { BrowserWindow, dialog, Tray } from 'electron';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import { setIsAppQuitting, setIsUpdateAvailable } from '..';
|
import { setIsAppQuitting, setIsUpdateAvailable } from '../main';
|
||||||
import { buildContextMenu } from './menuUtil';
|
import { buildContextMenu } from '../utils/menu';
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
36
src/services/autoLauncher.ts
Normal file
36
src/services/autoLauncher.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { AutoLauncherClient } from '../types/autoLauncher';
|
||||||
|
import { isPlatformWindows, isPlatformMac } from '../utils/main';
|
||||||
|
import linuxAutoLauncher from './autoLauncherClients/linuxAutoLauncher';
|
||||||
|
import macAndWindowsAutoLauncher from './autoLauncherClients/macAndWindowsAutoLauncher';
|
||||||
|
|
||||||
|
class AutoLauncher {
|
||||||
|
private client: AutoLauncherClient;
|
||||||
|
init() {
|
||||||
|
if (isPlatformMac() || isPlatformWindows()) {
|
||||||
|
this.client = macAndWindowsAutoLauncher;
|
||||||
|
} else {
|
||||||
|
this.client = linuxAutoLauncher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async isEnabled() {
|
||||||
|
if (!this.client) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
return await this.client.isEnabled();
|
||||||
|
}
|
||||||
|
async toggleAutoLaunch() {
|
||||||
|
if (!this.client) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
await this.client.toggleAutoLaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
wasAutoLaunched() {
|
||||||
|
if (!this.client) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
return this.client.wasAutoLaunched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AutoLauncher();
|
37
src/services/autoLauncherClients/linuxAutoLauncher.ts
Normal file
37
src/services/autoLauncherClients/linuxAutoLauncher.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import AutoLaunch from 'auto-launch';
|
||||||
|
import { AutoLauncherClient } from '../../types/autoLauncher';
|
||||||
|
|
||||||
|
class LinuxAutoLauncher implements AutoLauncherClient {
|
||||||
|
private instance: AutoLaunch;
|
||||||
|
constructor() {
|
||||||
|
const autoLauncher = new AutoLaunch({
|
||||||
|
name: 'ente',
|
||||||
|
isHidden: true,
|
||||||
|
});
|
||||||
|
this.instance = autoLauncher;
|
||||||
|
}
|
||||||
|
async isEnabled() {
|
||||||
|
return await this.instance.isEnabled();
|
||||||
|
}
|
||||||
|
async toggleAutoLaunch() {
|
||||||
|
if (await this.isEnabled()) {
|
||||||
|
await this.disableAutoLaunch();
|
||||||
|
} else {
|
||||||
|
await this.enableAutoLaunch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async wasAutoLaunched() {
|
||||||
|
// can't determine if it was auto launched
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async disableAutoLaunch() {
|
||||||
|
await this.instance.disable();
|
||||||
|
}
|
||||||
|
private async enableAutoLaunch() {
|
||||||
|
await this.instance.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LinuxAutoLauncher();
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { app } from 'electron';
|
||||||
|
import { AutoLauncherClient } from '../../types/autoLauncher';
|
||||||
|
|
||||||
|
class MacAndWindowsAutoLauncher implements AutoLauncherClient {
|
||||||
|
async isEnabled() {
|
||||||
|
return app.getLoginItemSettings().openAtLogin;
|
||||||
|
}
|
||||||
|
async toggleAutoLaunch() {
|
||||||
|
if (await this.isEnabled()) {
|
||||||
|
this.disableAutoLogin();
|
||||||
|
} else {
|
||||||
|
this.enableAutoLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async wasAutoLaunched() {
|
||||||
|
return app.getLoginItemSettings().wasOpenedAtLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableAutoLogin() {
|
||||||
|
app.setLoginItemSettings({ openAsHidden: true, openAtLogin: false });
|
||||||
|
}
|
||||||
|
private enableAutoLogin() {
|
||||||
|
app.setLoginItemSettings({ openAsHidden: true, openAtLogin: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MacAndWindowsAutoLauncher();
|
33
src/services/chokidar.ts
Normal file
33
src/services/chokidar.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
import { logError } from '../utils/logging';
|
||||||
|
import { getWatchMappings } from '../api/watch';
|
||||||
|
|
||||||
|
export function initWatcher(mainWindow: BrowserWindow) {
|
||||||
|
const mappings = getWatchMappings();
|
||||||
|
const folderPaths = mappings.map((mapping) => {
|
||||||
|
return mapping.folderPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const watcher = chokidar.watch(folderPaths, {
|
||||||
|
awaitWriteFinish: true,
|
||||||
|
});
|
||||||
|
watcher
|
||||||
|
.on('add', (path) => {
|
||||||
|
mainWindow.webContents.send('watch-add', path);
|
||||||
|
})
|
||||||
|
.on('change', (path) => {
|
||||||
|
mainWindow.webContents.send('watch-change', path);
|
||||||
|
})
|
||||||
|
.on('unlink', (path) => {
|
||||||
|
mainWindow.webContents.send('watch-unlink', path);
|
||||||
|
})
|
||||||
|
.on('unlinkDir', (path) => {
|
||||||
|
mainWindow.webContents.send('watch-unlink-dir', path);
|
||||||
|
})
|
||||||
|
.on('error', (error) => {
|
||||||
|
logError(error, 'error while watching files');
|
||||||
|
});
|
||||||
|
|
||||||
|
return watcher;
|
||||||
|
}
|
|
@ -1,49 +1,11 @@
|
||||||
import { ipcRenderer } from 'electron/renderer';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import { readFile, writeFile, existsSync, unlink } from 'promise-fs';
|
||||||
readFile,
|
import DiskLRUService from '../services/diskLRU';
|
||||||
writeFile,
|
|
||||||
existsSync,
|
|
||||||
mkdir,
|
|
||||||
rmSync,
|
|
||||||
unlink,
|
|
||||||
} from 'promise-fs';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import DiskLRUService from './diskLRU';
|
|
||||||
|
|
||||||
const CACHE_DIR = 'ente';
|
|
||||||
const MAX_CACHE_SIZE = 1000 * 1000 * 1000; // 1GB
|
const MAX_CACHE_SIZE = 1000 * 1000 * 1000; // 1GB
|
||||||
|
|
||||||
const getCacheDir = async () => {
|
export class DiskCache {
|
||||||
const systemCacheDir = await ipcRenderer.invoke('get-path', 'cache');
|
|
||||||
return path.join(systemCacheDir, CACHE_DIR);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCacheBucketDir = async (cacheName: string) => {
|
|
||||||
const cacheDir = await getCacheDir();
|
|
||||||
const cacheBucketDir = path.join(cacheDir, cacheName);
|
|
||||||
return cacheBucketDir;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function openDiskCache(cacheName: string) {
|
|
||||||
const cacheBucketDir = await getCacheBucketDir(cacheName);
|
|
||||||
if (!existsSync(cacheBucketDir)) {
|
|
||||||
await mkdir(cacheBucketDir, { recursive: true });
|
|
||||||
}
|
|
||||||
return new DiskCache(cacheBucketDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteDiskCache(cacheName: string) {
|
|
||||||
const cacheBucketDir = await getCacheBucketDir(cacheName);
|
|
||||||
if (existsSync(cacheBucketDir)) {
|
|
||||||
rmSync(cacheBucketDir, { recursive: true, force: true });
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DiskCache {
|
|
||||||
constructor(private cacheBucketDir: string) {}
|
constructor(private cacheBucketDir: string) {}
|
||||||
|
|
||||||
async put(cacheKey: string, response: Response): Promise<void> {
|
async put(cacheKey: string, response: Response): Promise<void> {
|
||||||
|
@ -77,7 +39,6 @@ class DiskCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAssetCachePath(cacheDir: string, cacheKey: string) {
|
function getAssetCachePath(cacheDir: string, cacheKey: string) {
|
||||||
// hashing the key to prevent illegal filenames
|
// hashing the key to prevent illegal filenames
|
||||||
const cacheKeyHash = crypto
|
const cacheKeyHash = crypto
|
|
@ -2,7 +2,7 @@ import path from 'path';
|
||||||
import { readdir, stat, unlink } from 'promise-fs';
|
import { readdir, stat, unlink } from 'promise-fs';
|
||||||
import getFolderSize from 'get-folder-size';
|
import getFolderSize from 'get-folder-size';
|
||||||
import { utimes, close, open } from 'promise-fs';
|
import { utimes, close, open } from 'promise-fs';
|
||||||
import { logError } from './logging';
|
import { logError } from '../utils/logging';
|
||||||
|
|
||||||
export interface LeastRecentlyUsedResult {
|
export interface LeastRecentlyUsedResult {
|
||||||
atime: Date;
|
atime: Date;
|
|
@ -1,13 +1,12 @@
|
||||||
import path from 'path';
|
|
||||||
import StreamZip from 'node-stream-zip';
|
|
||||||
import * as fs from 'promise-fs';
|
|
||||||
import { FILE_STREAM_CHUNK_SIZE } from '../config';
|
import { FILE_STREAM_CHUNK_SIZE } from '../config';
|
||||||
import { uploadStatusStore } from '../services/store';
|
import path from 'path';
|
||||||
import { ElectronFile, FILE_PATH_KEYS, FILE_PATH_TYPE } from '../types';
|
import * as fs from 'promise-fs';
|
||||||
import { logError } from './logging';
|
import { ElectronFile } from '../types';
|
||||||
|
import StreamZip from 'node-stream-zip';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
// https://stackoverflow.com/a/63111390
|
// https://stackoverflow.com/a/63111390
|
||||||
export const getFilesFromDir = async (dirPath: string) => {
|
export const getDirFilePaths = async (dirPath: string) => {
|
||||||
if (!(await fs.stat(dirPath)).isDirectory()) {
|
if (!(await fs.stat(dirPath)).isDirectory()) {
|
||||||
return [dirPath];
|
return [dirPath];
|
||||||
}
|
}
|
||||||
|
@ -17,20 +16,19 @@ export const getFilesFromDir = async (dirPath: string) => {
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
const absolute = path.join(dirPath, filePath);
|
const absolute = path.join(dirPath, filePath);
|
||||||
files = files.concat(await getFilesFromDir(absolute));
|
files = files.concat(await getDirFilePaths(absolute));
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFileStream = async (filePath: string) => {
|
export const getFileStream = async (filePath: string) => {
|
||||||
const file = await fs.open(filePath, 'r');
|
const file = await fs.open(filePath, 'r');
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const readableStream = new ReadableStream<Uint8Array>({
|
const readableStream = new ReadableStream<Uint8Array>({
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
try {
|
try {
|
||||||
const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE);
|
const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE);
|
||||||
|
|
||||||
// original types were not working correctly
|
// original types were not working correctly
|
||||||
const bytesRead = (await fs.read(
|
const bytesRead = (await fs.read(
|
||||||
file,
|
file,
|
||||||
|
@ -44,10 +42,9 @@ const getFileStream = async (filePath: string) => {
|
||||||
controller.close();
|
controller.close();
|
||||||
await fs.close(file);
|
await fs.close(file);
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue(buff);
|
controller.enqueue(buff.slice(0, bytesRead));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'stream pull failed');
|
|
||||||
await fs.close(file);
|
await fs.close(file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -55,92 +52,6 @@ const getFileStream = async (filePath: string) => {
|
||||||
return readableStream;
|
return readableStream;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getZipFileStream = async (
|
|
||||||
zip: StreamZip.StreamZipAsync,
|
|
||||||
filePath: string
|
|
||||||
) => {
|
|
||||||
const stream = await zip.stream(filePath);
|
|
||||||
const done = { current: false };
|
|
||||||
|
|
||||||
let resolveObj: (value?: any) => void = null;
|
|
||||||
let rejectObj: (reason?: any) => void = null;
|
|
||||||
|
|
||||||
stream.on('readable', () => {
|
|
||||||
if (resolveObj) {
|
|
||||||
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
|
||||||
if (chunk) {
|
|
||||||
resolveObj(new Uint8Array(chunk));
|
|
||||||
resolveObj = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', () => {
|
|
||||||
done.current = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', (e) => {
|
|
||||||
done.current = true;
|
|
||||||
if (rejectObj) {
|
|
||||||
rejectObj(e);
|
|
||||||
rejectObj = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const readStreamData = () => {
|
|
||||||
return new Promise<Uint8Array>((resolve, reject) => {
|
|
||||||
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
|
||||||
if (chunk || done.current) {
|
|
||||||
resolve(chunk);
|
|
||||||
} else {
|
|
||||||
resolveObj = resolve;
|
|
||||||
rejectObj = reject;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const readableStream = new ReadableStream<Uint8Array>({
|
|
||||||
async pull(controller) {
|
|
||||||
try {
|
|
||||||
const data = await readStreamData();
|
|
||||||
if (data) {
|
|
||||||
controller.enqueue(data);
|
|
||||||
} else {
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'stream reading failed');
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return readableStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getZipEntryAsElectronFile(
|
|
||||||
zip: StreamZip.StreamZipAsync,
|
|
||||||
entry: StreamZip.ZipEntry
|
|
||||||
): Promise<ElectronFile> {
|
|
||||||
return {
|
|
||||||
path: entry.name,
|
|
||||||
name: path.basename(entry.name),
|
|
||||||
size: entry.size,
|
|
||||||
lastModified: entry.time,
|
|
||||||
stream: async () => {
|
|
||||||
return await getZipFileStream(zip, entry.name);
|
|
||||||
},
|
|
||||||
blob: async () => {
|
|
||||||
const buffer = await zip.entryData(entry.name);
|
|
||||||
return new Blob([new Uint8Array(buffer)]);
|
|
||||||
},
|
|
||||||
arrayBuffer: async () => {
|
|
||||||
const buffer = await zip.entryData(entry.name);
|
|
||||||
return new Uint8Array(buffer);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getElectronFile(filePath: string): Promise<ElectronFile> {
|
export async function getElectronFile(filePath: string): Promise<ElectronFile> {
|
||||||
const fileStats = await fs.stat(filePath);
|
const fileStats = await fs.stat(filePath);
|
||||||
return {
|
return {
|
||||||
|
@ -162,77 +73,115 @@ export async function getElectronFile(filePath: string): Promise<ElectronFile> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => {
|
export const getValidPaths = (paths: string[]) => {
|
||||||
const key = FILE_PATH_KEYS[type];
|
if (!paths) {
|
||||||
if (filePaths) {
|
return [] as string[];
|
||||||
uploadStatusStore.set(key, filePaths);
|
|
||||||
} else {
|
|
||||||
uploadStatusStore.delete(key);
|
|
||||||
}
|
}
|
||||||
};
|
return paths.filter(async (path) => {
|
||||||
|
|
||||||
export const setToUploadCollection = (collectionName: string) => {
|
|
||||||
if (collectionName) {
|
|
||||||
uploadStatusStore.set('collectionName', collectionName);
|
|
||||||
} else {
|
|
||||||
uploadStatusStore.delete('collectionName');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSavedPaths = (type: FILE_PATH_TYPE) => {
|
|
||||||
const paths =
|
|
||||||
(uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[]) ?? [];
|
|
||||||
|
|
||||||
const validPaths = paths.filter(async (path) => {
|
|
||||||
try {
|
try {
|
||||||
await fs.stat(path).then((stat) => stat.isFile());
|
await fs.stat(path).then((stat) => stat.isFile());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setToUploadFiles(type, validPaths);
|
|
||||||
return validPaths;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPendingUploads = async () => {
|
export const getZipFileStream = async (
|
||||||
const filePaths = getSavedPaths(FILE_PATH_TYPE.FILES);
|
zip: StreamZip.StreamZipAsync,
|
||||||
const zipPaths = getSavedPaths(FILE_PATH_TYPE.ZIPS);
|
filePath: string
|
||||||
const collectionName = uploadStatusStore.get('collectionName');
|
) => {
|
||||||
|
const stream = await zip.stream(filePath);
|
||||||
let files: ElectronFile[] = [];
|
const done = {
|
||||||
let type: FILE_PATH_TYPE;
|
current: false,
|
||||||
if (zipPaths.length) {
|
|
||||||
type = FILE_PATH_TYPE.ZIPS;
|
|
||||||
for (const zipPath of zipPaths) {
|
|
||||||
files.push(...(await getElectronFilesFromGoogleZip(zipPath)));
|
|
||||||
}
|
|
||||||
const pendingFilePaths = new Set(filePaths);
|
|
||||||
files = files.filter((file) => pendingFilePaths.has(file.path));
|
|
||||||
} else if (filePaths.length) {
|
|
||||||
type = FILE_PATH_TYPE.FILES;
|
|
||||||
files = await Promise.all(filePaths.map(getElectronFile));
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
files,
|
|
||||||
collectionName,
|
|
||||||
type,
|
|
||||||
};
|
};
|
||||||
};
|
let resolveObj: (value?: any) => void = null;
|
||||||
|
let rejectObj: (reason?: any) => void = null;
|
||||||
|
stream.on('readable', () => {
|
||||||
|
if (resolveObj) {
|
||||||
|
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
||||||
|
|
||||||
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
|
if (chunk) {
|
||||||
const zip = new StreamZip.async({
|
resolveObj(new Uint8Array(chunk));
|
||||||
file: filePath,
|
resolveObj = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.on('end', () => {
|
||||||
|
done.current = true;
|
||||||
|
});
|
||||||
|
stream.on('error', (e) => {
|
||||||
|
done.current = true;
|
||||||
|
|
||||||
|
if (rejectObj) {
|
||||||
|
rejectObj(e);
|
||||||
|
rejectObj = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const entries = await zip.entries();
|
const readStreamData = () => {
|
||||||
const files: ElectronFile[] = [];
|
return new Promise<Uint8Array>((resolve, reject) => {
|
||||||
|
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
||||||
|
|
||||||
for (const entry of Object.values(entries)) {
|
if (chunk || done.current) {
|
||||||
const basename = path.basename(entry.name);
|
resolve(chunk);
|
||||||
if (entry.isFile && basename.length > 0 && basename[0] !== '.') {
|
} else {
|
||||||
files.push(await getZipEntryAsElectronFile(zip, entry));
|
resolveObj = resolve;
|
||||||
}
|
rejectObj = reject;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return files;
|
const readableStream = new ReadableStream<Uint8Array>({
|
||||||
|
async pull(controller) {
|
||||||
|
try {
|
||||||
|
const data = await readStreamData();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
controller.enqueue(data);
|
||||||
|
} else {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return readableStream;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function isFolder(dirPath: string) {
|
||||||
|
return await fs
|
||||||
|
.stat(dirPath)
|
||||||
|
.then((stats) => {
|
||||||
|
return stats.isDirectory();
|
||||||
|
})
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertBrowserStreamToNode = (fileStream: any) => {
|
||||||
|
const reader = fileStream.getReader();
|
||||||
|
const rs = new Readable();
|
||||||
|
|
||||||
|
rs._read = async () => {
|
||||||
|
const result = await reader.read();
|
||||||
|
|
||||||
|
if (!result.done) {
|
||||||
|
rs.push(Buffer.from(result.value));
|
||||||
|
} else {
|
||||||
|
rs.push(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return rs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function writeStream(filePath: string, fileStream: any) {
|
||||||
|
const writeable = fs.createWriteStream(filePath);
|
||||||
|
const readable = convertBrowserStreamToNode(fileStream);
|
||||||
|
readable.pipe(writeable);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readTextFile(filePath: string) {
|
||||||
|
return await fs.readFile(filePath, 'utf-8');
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
import * as Sentry from '@sentry/electron/dist/main';
|
import * as Sentry from '@sentry/electron/dist/main';
|
||||||
|
import { keysStore } from '../stores/keys.store';
|
||||||
|
|
||||||
import { keysStore } from '../services/store';
|
import { isDev } from '../utils/common';
|
||||||
import { isDev } from './common';
|
|
||||||
|
|
||||||
const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5';
|
const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const version = require('../../package.json').version;
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
function initSentry(): void {
|
export function initSentry(): void {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_DSN,
|
dsn: SENTRY_DSN,
|
||||||
release: version,
|
release: version,
|
||||||
|
@ -46,6 +46,15 @@ function errorWithContext(originalError: Error, context: string) {
|
||||||
return errorWithContext;
|
return errorWithContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserAnonymizedID() {
|
||||||
|
let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id;
|
||||||
|
if (!anonymizeUserID) {
|
||||||
|
anonymizeUserID = makeID(6);
|
||||||
|
keysStore.set('AnonymizeUserID', { id: anonymizeUserID });
|
||||||
|
}
|
||||||
|
return anonymizeUserID;
|
||||||
|
}
|
||||||
|
|
||||||
function makeID(length: number) {
|
function makeID(length: number) {
|
||||||
let result = '';
|
let result = '';
|
||||||
const characters =
|
const characters =
|
||||||
|
@ -58,14 +67,3 @@ function makeID(length: number) {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserAnonymizedID() {
|
|
||||||
let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id;
|
|
||||||
if (!anonymizeUserID) {
|
|
||||||
anonymizeUserID = makeID(6);
|
|
||||||
keysStore.set('AnonymizeUserID', { id: anonymizeUserID });
|
|
||||||
}
|
|
||||||
return anonymizeUserID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default initSentry;
|
|
73
src/services/upload.ts
Normal file
73
src/services/upload.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import StreamZip from 'node-stream-zip';
|
||||||
|
import path from 'path';
|
||||||
|
import { uploadStatusStore } from '../stores/upload.store';
|
||||||
|
import { FILE_PATH_TYPE, FILE_PATH_KEYS, ElectronFile } from '../types';
|
||||||
|
import { getValidPaths, getZipFileStream } from './fs';
|
||||||
|
|
||||||
|
export const getSavedFilePaths = (type: FILE_PATH_TYPE) => {
|
||||||
|
const paths =
|
||||||
|
getValidPaths(
|
||||||
|
uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[]
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
setToUploadFiles(type, paths);
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getZipEntryAsElectronFile(
|
||||||
|
zip: StreamZip.StreamZipAsync,
|
||||||
|
entry: StreamZip.ZipEntry
|
||||||
|
): Promise<ElectronFile> {
|
||||||
|
return {
|
||||||
|
path: entry.name,
|
||||||
|
name: path.basename(entry.name),
|
||||||
|
size: entry.size,
|
||||||
|
lastModified: entry.time,
|
||||||
|
stream: async () => {
|
||||||
|
return await getZipFileStream(zip, entry.name);
|
||||||
|
},
|
||||||
|
blob: async () => {
|
||||||
|
const buffer = await zip.entryData(entry.name);
|
||||||
|
return new Blob([new Uint8Array(buffer)]);
|
||||||
|
},
|
||||||
|
arrayBuffer: async () => {
|
||||||
|
const buffer = await zip.entryData(entry.name);
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => {
|
||||||
|
const key = FILE_PATH_KEYS[type];
|
||||||
|
if (filePaths) {
|
||||||
|
uploadStatusStore.set(key, filePaths);
|
||||||
|
} else {
|
||||||
|
uploadStatusStore.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setToUploadCollection = (collectionName: string) => {
|
||||||
|
if (collectionName) {
|
||||||
|
uploadStatusStore.set('collectionName', collectionName);
|
||||||
|
} else {
|
||||||
|
uploadStatusStore.delete('collectionName');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
|
||||||
|
const zip = new StreamZip.async({
|
||||||
|
file: filePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = await zip.entries();
|
||||||
|
const files: ElectronFile[] = [];
|
||||||
|
|
||||||
|
for (const entry of Object.values(entries)) {
|
||||||
|
const basename = path.basename(entry.name);
|
||||||
|
if (entry.isFile && basename.length > 0 && basename[0] !== '.') {
|
||||||
|
files.push(await getZipEntryAsElectronFile(zip, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
10
src/services/userPreference.ts
Normal file
10
src/services/userPreference.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { userPreferencesStore } from '../stores/userPreferences.store';
|
||||||
|
|
||||||
|
export function getHideDockIconPreference() {
|
||||||
|
const shouldHideDockIcon = userPreferencesStore.get('hideDockIcon');
|
||||||
|
return shouldHideDockIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setHideDockIconPreference(shouldHideDockIcon: boolean) {
|
||||||
|
userPreferencesStore.set('hideDockIcon', shouldHideDockIcon);
|
||||||
|
}
|
11
src/services/watch.ts
Normal file
11
src/services/watch.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { WatchStoreType } from '../types';
|
||||||
|
import { watchStore } from '../stores/watch.store';
|
||||||
|
|
||||||
|
export function getWatchMappings() {
|
||||||
|
const mappings = watchStore.get('mappings') ?? [];
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWatchMappings(watchMappings: WatchStoreType['mappings']) {
|
||||||
|
watchStore.set('mappings', watchMappings);
|
||||||
|
}
|
18
src/stores/keys.store.ts
Normal file
18
src/stores/keys.store.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import Store, { Schema } from 'electron-store';
|
||||||
|
import { KeysStoreType } from '../types';
|
||||||
|
|
||||||
|
const keysStoreSchema: Schema<KeysStoreType> = {
|
||||||
|
AnonymizeUserID: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keysStore = new Store({
|
||||||
|
name: 'keys',
|
||||||
|
schema: keysStoreSchema,
|
||||||
|
});
|
13
src/stores/safeStorage.store.ts
Normal file
13
src/stores/safeStorage.store.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Store, { Schema } from 'electron-store';
|
||||||
|
import { SafeStorageStoreType } from '../types';
|
||||||
|
|
||||||
|
const safeStorageSchema: Schema<SafeStorageStoreType> = {
|
||||||
|
encryptionKey: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const safeStorageStore = new Store({
|
||||||
|
name: 'safeStorage',
|
||||||
|
schema: safeStorageSchema,
|
||||||
|
});
|
25
src/stores/upload.store.ts
Normal file
25
src/stores/upload.store.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import Store, { Schema } from 'electron-store';
|
||||||
|
import { UploadStoreType } from '../types';
|
||||||
|
|
||||||
|
const uploadStoreSchema: Schema<UploadStoreType> = {
|
||||||
|
filePaths: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zipPaths: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collectionName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadStatusStore = new Store({
|
||||||
|
name: 'upload-status',
|
||||||
|
schema: uploadStoreSchema,
|
||||||
|
});
|
13
src/stores/userPreferences.store.ts
Normal file
13
src/stores/userPreferences.store.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Store, { Schema } from 'electron-store';
|
||||||
|
import { UserPreferencesType } from '../types';
|
||||||
|
|
||||||
|
const userPreferencesSchema: Schema<UserPreferencesType> = {
|
||||||
|
hideDockIcon: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userPreferencesStore = new Store({
|
||||||
|
name: 'userPreferences',
|
||||||
|
schema: userPreferencesSchema,
|
||||||
|
});
|
47
src/stores/watch.store.ts
Normal file
47
src/stores/watch.store.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import Store, { Schema } from 'electron-store';
|
||||||
|
import { WatchStoreType } from '../types';
|
||||||
|
|
||||||
|
const watchStoreSchema: Schema<WatchStoreType> = {
|
||||||
|
mappings: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
rootFolderName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
uploadStrategy: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
folderPath: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
syncedFiles: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignoredFiles: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const watchStore = new Store({
|
||||||
|
name: 'watch-status',
|
||||||
|
schema: watchStoreSchema,
|
||||||
|
});
|
5
src/types/autoLauncher.ts
Normal file
5
src/types/autoLauncher.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface AutoLauncherClient {
|
||||||
|
isEnabled: () => Promise<boolean>;
|
||||||
|
toggleAutoLaunch: () => Promise<void>;
|
||||||
|
wasAutoLaunched: () => Promise<boolean>;
|
||||||
|
}
|
|
@ -20,6 +20,24 @@ export interface KeysStoreType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WatchMappingSyncedFile {
|
||||||
|
path: string;
|
||||||
|
uploadedFileID: number;
|
||||||
|
collectionID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchMapping {
|
||||||
|
rootFolderName: string;
|
||||||
|
uploadStrategy: number;
|
||||||
|
folderPath: string;
|
||||||
|
syncedFiles: WatchMappingSyncedFile[];
|
||||||
|
ignoredFiles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchStoreType {
|
||||||
|
mappings: WatchMapping[];
|
||||||
|
}
|
||||||
|
|
||||||
export enum FILE_PATH_TYPE {
|
export enum FILE_PATH_TYPE {
|
||||||
FILES = 'files',
|
FILES = 'files',
|
||||||
ZIPS = 'zips',
|
ZIPS = 'zips',
|
||||||
|
@ -35,3 +53,7 @@ export const FILE_PATH_KEYS: {
|
||||||
export interface SafeStorageStoreType {
|
export interface SafeStorageStoreType {
|
||||||
encryptionKey: string;
|
encryptionKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserPreferencesType {
|
||||||
|
hideDockIcon: boolean;
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { app, BrowserWindow, nativeImage } from 'electron';
|
import { app, BrowserWindow, nativeImage } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { isDev } from './common';
|
import { isDev } from './common';
|
||||||
import { isAppQuitting } from '..';
|
import { isAppQuitting } from '../main';
|
||||||
import { addAllowOriginHeader } from './cors';
|
|
||||||
import { PROD_HOST_URL } from '../config';
|
import { PROD_HOST_URL } from '../config';
|
||||||
|
import { isPlatformMac } from './main';
|
||||||
|
import { getHideDockIconPreference } from '../services/userPreference';
|
||||||
|
import autoLauncher from '../services/autoLauncher';
|
||||||
|
|
||||||
export function createWindow(): BrowserWindow {
|
export async function createWindow(): Promise<BrowserWindow> {
|
||||||
const appImgPath = isDev
|
const appImgPath = isDev
|
||||||
? 'build/window-icon.png'
|
? 'build/window-icon.png'
|
||||||
: path.join(process.resourcesPath, 'window-icon.png');
|
: path.join(process.resourcesPath, 'window-icon.png');
|
||||||
|
@ -23,15 +25,16 @@ export function createWindow(): BrowserWindow {
|
||||||
show: false, // don't show the main window on load
|
show: false, // don't show the main window on load
|
||||||
});
|
});
|
||||||
mainWindow.maximize();
|
mainWindow.maximize();
|
||||||
|
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
||||||
|
|
||||||
const splash = new BrowserWindow({
|
const splash = new BrowserWindow({
|
||||||
height: 600,
|
height: 600,
|
||||||
width: 800,
|
width: 800,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
show: !wasAutoLaunched,
|
||||||
});
|
});
|
||||||
splash.maximize();
|
splash.maximize();
|
||||||
|
|
||||||
addAllowOriginHeader(mainWindow);
|
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
splash.loadFile(`../build/splash.html`);
|
splash.loadFile(`../build/splash.html`);
|
||||||
mainWindow.loadURL(PROD_HOST_URL);
|
mainWindow.loadURL(PROD_HOST_URL);
|
||||||
|
@ -45,25 +48,35 @@ export function createWindow(): BrowserWindow {
|
||||||
}
|
}
|
||||||
mainWindow.webContents.on('did-fail-load', () => {
|
mainWindow.webContents.on('did-fail-load', () => {
|
||||||
splash.close();
|
splash.close();
|
||||||
mainWindow.show();
|
|
||||||
isDev
|
isDev
|
||||||
? mainWindow.loadFile(`../../build/error.html`)
|
? mainWindow.loadFile(`../../build/error.html`)
|
||||||
: splash.loadURL(
|
: splash.loadURL(
|
||||||
`file://${path.join(process.resourcesPath, 'error.html')}`
|
`file://${path.join(process.resourcesPath, 'error.html')}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', async () => {
|
||||||
mainWindow.show();
|
|
||||||
splash.destroy();
|
splash.destroy();
|
||||||
|
if (!wasAutoLaunched) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mainWindow.on('close', function (event) {
|
mainWindow.on('close', function (event) {
|
||||||
if (!isAppQuitting()) {
|
if (!isAppQuitting()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
const isMac = process.platform === 'darwin';
|
|
||||||
isMac && app.dock.hide();
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
mainWindow.on('hide', () => {
|
||||||
|
const shouldHideDockIcon = getHideDockIconPreference();
|
||||||
|
if (isPlatformMac() && shouldHideDockIcon) {
|
||||||
|
app.dock.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
if (isPlatformMac()) {
|
||||||
|
app.dock.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
|
@ -8,23 +8,24 @@ import {
|
||||||
app,
|
app,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import { createWindow } from './createWindow';
|
import { createWindow } from './createWindow';
|
||||||
import { buildContextMenu } from './menuUtil';
|
import { buildContextMenu } from './menu';
|
||||||
import { logErrorSentry } from './sentry';
|
import { logErrorSentry } from '../services/sentry';
|
||||||
import { getFilesFromDir } from './upload';
|
import chokidar from 'chokidar';
|
||||||
|
import path from 'path';
|
||||||
|
import { getDirFilePaths } from '../services/fs';
|
||||||
|
|
||||||
export default function setupIpcComs(
|
export default function setupIpcComs(
|
||||||
tray: Tray,
|
tray: Tray,
|
||||||
mainWindow: BrowserWindow
|
mainWindow: BrowserWindow,
|
||||||
|
watcher: chokidar.FSWatcher
|
||||||
): void {
|
): void {
|
||||||
ipcMain.handle('select-dir', async () => {
|
ipcMain.handle('select-dir', async () => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory'],
|
properties: ['openDirectory'],
|
||||||
});
|
});
|
||||||
const dir =
|
if (result.filePaths && result.filePaths.length > 0) {
|
||||||
result.filePaths &&
|
return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep);
|
||||||
result.filePaths.length > 0 &&
|
}
|
||||||
result.filePaths[0];
|
|
||||||
return dir;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-tray', (_, args) => {
|
ipcMain.on('update-tray', (_, args) => {
|
||||||
|
@ -38,8 +39,8 @@ export default function setupIpcComs(
|
||||||
};
|
};
|
||||||
new Notification(notification).show();
|
new Notification(notification).show();
|
||||||
});
|
});
|
||||||
ipcMain.on('reload-window', () => {
|
ipcMain.on('reload-window', async () => {
|
||||||
const secondWindow = createWindow();
|
const secondWindow = await createWindow();
|
||||||
mainWindow.destroy();
|
mainWindow.destroy();
|
||||||
mainWindow = secondWindow;
|
mainWindow = secondWindow;
|
||||||
});
|
});
|
||||||
|
@ -66,12 +67,20 @@ export default function setupIpcComs(
|
||||||
|
|
||||||
let files: string[] = [];
|
let files: string[] = [];
|
||||||
for (const dirPath of dir.filePaths) {
|
for (const dirPath of dir.filePaths) {
|
||||||
files = files.concat(await getFilesFromDir(dirPath));
|
files = files.concat(await getDirFilePaths(dirPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('add-watcher', async (_, args: { dir: string }) => {
|
||||||
|
watcher.add(args.dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('remove-watcher', async (_, args: { dir: string }) => {
|
||||||
|
watcher.unwatch(args.dir);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('log-error', (_, err, msg, info?) => {
|
ipcMain.handle('log-error', (_, err, msg, info?) => {
|
||||||
logErrorSentry(err, msg, info);
|
logErrorSentry(err, msg, info);
|
||||||
});
|
});
|
101
src/utils/main.ts
Normal file
101
src/utils/main.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { PROD_HOST_URL, RENDERER_OUTPUT_DIR } from '../config';
|
||||||
|
import { nativeImage, Tray, app, BrowserWindow, Menu } from 'electron';
|
||||||
|
import electronReload from 'electron-reload';
|
||||||
|
import serveNextAt from 'next-electron-server';
|
||||||
|
import path from 'path';
|
||||||
|
import { existsSync } from 'promise-fs';
|
||||||
|
import appUpdater from '../services/appUpdater';
|
||||||
|
import { isDev } from './common';
|
||||||
|
import { buildContextMenu, buildMenuBar } from './menu';
|
||||||
|
import autoLauncher from '../services/autoLauncher';
|
||||||
|
import { getHideDockIconPreference } from '../services/userPreference';
|
||||||
|
|
||||||
|
export function handleUpdates(mainWindow: BrowserWindow, tray: Tray) {
|
||||||
|
if (!isDev) {
|
||||||
|
appUpdater.checkForUpdate(tray, mainWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupTrayItem(mainWindow: BrowserWindow) {
|
||||||
|
const trayImgPath = isDev
|
||||||
|
? 'build/taskbar-icon.png'
|
||||||
|
: path.join(process.resourcesPath, 'taskbar-icon.png');
|
||||||
|
const trayIcon = nativeImage.createFromPath(trayImgPath);
|
||||||
|
const tray = new Tray(trayIcon);
|
||||||
|
tray.setToolTip('ente');
|
||||||
|
tray.setContextMenu(buildContextMenu(mainWindow));
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDownloads(mainWindow: BrowserWindow) {
|
||||||
|
mainWindow.webContents.session.on('will-download', (_, item) => {
|
||||||
|
item.setSavePath(
|
||||||
|
getUniqueSavePath(item.getFilename(), app.getPath('downloads'))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueSavePath(filename: string, directory: string): string {
|
||||||
|
let uniqueFileSavePath = path.join(directory, filename);
|
||||||
|
const { name: filenameWithoutExtension, ext: extension } =
|
||||||
|
path.parse(filename);
|
||||||
|
let n = 0;
|
||||||
|
while (existsSync(uniqueFileSavePath)) {
|
||||||
|
n++;
|
||||||
|
// filter need to remove undefined extension from the array
|
||||||
|
// else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string
|
||||||
|
const fileNameWithNumberedSuffix = [
|
||||||
|
`${filenameWithoutExtension}(${n})`,
|
||||||
|
extension,
|
||||||
|
]
|
||||||
|
.filter((x) => x) // filters out undefined/null values
|
||||||
|
.join('.');
|
||||||
|
uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix);
|
||||||
|
}
|
||||||
|
return uniqueFileSavePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupMacWindowOnDockIconClick() {
|
||||||
|
app.on('activate', function () {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
// we allow only one window
|
||||||
|
windows[0].show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupMainMenu() {
|
||||||
|
Menu.setApplicationMenu(await buildMenuBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupMainHotReload() {
|
||||||
|
if (isDev) {
|
||||||
|
electronReload(__dirname, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupNextElectronServe() {
|
||||||
|
serveNextAt(PROD_HOST_URL, {
|
||||||
|
outputDir: RENDERER_OUTPUT_DIR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlatformMac() {
|
||||||
|
return process.platform === 'darwin';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlatformWindows() {
|
||||||
|
return process.platform === 'win32';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDockIconHideOnAutoLaunch() {
|
||||||
|
const shouldHideDockIcon = getHideDockIconPreference();
|
||||||
|
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
||||||
|
|
||||||
|
if (isPlatformMac() && shouldHideDockIcon && wasAutoLaunched) {
|
||||||
|
app.dock.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableSharedArrayBufferSupport() {
|
||||||
|
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer');
|
||||||
|
}
|
|
@ -5,10 +5,15 @@ import {
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
MenuItemConstructorOptions,
|
MenuItemConstructorOptions,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import { isUpdateAvailable, setIsAppQuitting } from '..';
|
import {
|
||||||
import { showUpdateDialog } from './appUpdater';
|
getHideDockIconPreference,
|
||||||
|
setHideDockIconPreference,
|
||||||
const isMac = process.platform === 'darwin';
|
} from '../services/userPreference';
|
||||||
|
import { isUpdateAvailable, setIsAppQuitting } from '../main';
|
||||||
|
import autoLauncher from '../services/autoLauncher';
|
||||||
|
import { isPlatformMac } from './main';
|
||||||
|
import { showUpdateDialog } from '../services/appUpdater';
|
||||||
|
import { isDev } from './common';
|
||||||
|
|
||||||
export function buildContextMenu(
|
export function buildContextMenu(
|
||||||
mainWindow: BrowserWindow,
|
mainWindow: BrowserWindow,
|
||||||
|
@ -77,8 +82,6 @@ export function buildContextMenu(
|
||||||
label: 'Open ente',
|
label: 'Open ente',
|
||||||
click: function () {
|
click: function () {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
const isMac = process.platform === 'darwin';
|
|
||||||
isMac && app.dock.show();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -92,34 +95,65 @@ export function buildContextMenu(
|
||||||
return contextMenu;
|
return contextMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMenuBar(): Menu {
|
export async function buildMenuBar(): Promise<Menu> {
|
||||||
|
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
|
||||||
|
const isMac = isPlatformMac();
|
||||||
|
let shouldHideDockIcon = getHideDockIconPreference();
|
||||||
const template: MenuItemConstructorOptions[] = [
|
const template: MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: app.name,
|
label: 'ente',
|
||||||
submenu: [
|
submenu: [
|
||||||
...((isMac
|
...((isMac
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: 'About',
|
label: 'About ente',
|
||||||
role: 'about',
|
role: 'about',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []) as MenuItemConstructorOptions[]),
|
: []) as MenuItemConstructorOptions[]),
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'FAQ',
|
label: 'Preferences',
|
||||||
click: () => shell.openExternal('https://ente.io/faq/'),
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Open ente on startup',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: isAutoLaunchEnabled,
|
||||||
|
click: () => {
|
||||||
|
autoLauncher.toggleAutoLaunch();
|
||||||
|
isAutoLaunchEnabled = !isAutoLaunchEnabled;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Support',
|
label: 'Hide dock icon',
|
||||||
click: () => shell.openExternal('mailto:support@ente.io'),
|
type: 'checkbox',
|
||||||
|
checked: shouldHideDockIcon,
|
||||||
|
click: () => {
|
||||||
|
setHideDockIconPreference(!shouldHideDockIcon);
|
||||||
|
shouldHideDockIcon = !shouldHideDockIcon;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{ type: 'separator' },
|
||||||
|
...((isMac
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Hide ente',
|
||||||
|
role: 'hide',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Hide others',
|
||||||
accelerator: 'CommandOrControl+Q',
|
role: 'hideOthers',
|
||||||
click() {
|
|
||||||
setIsAppQuitting(true);
|
|
||||||
app.quit();
|
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []) as MenuItemConstructorOptions[]),
|
||||||
|
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit ente',
|
||||||
|
role: 'quit',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -161,14 +195,17 @@ export function buildMenuBar(): Menu {
|
||||||
]) as MenuItemConstructorOptions[]),
|
]) as MenuItemConstructorOptions[]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// { role: 'viewMenu' }
|
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
|
...((isDev
|
||||||
|
? [
|
||||||
{ role: 'reload', label: 'Reload' },
|
{ role: 'reload', label: 'Reload' },
|
||||||
{ role: 'forceReload', label: 'Force reload' },
|
{ role: 'forceReload', label: 'Force reload' },
|
||||||
{ role: 'toggleDevTools', label: 'Toggle dev tools' },
|
{ role: 'toggleDevTools', label: 'Toggle dev tools' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
]
|
||||||
|
: []) as MenuItemConstructorOptions[]),
|
||||||
{ role: 'resetZoom', label: 'Reset zoom' },
|
{ role: 'resetZoom', label: 'Reset zoom' },
|
||||||
{ role: 'zoomIn', label: 'Zoom in' },
|
{ role: 'zoomIn', label: 'Zoom in' },
|
||||||
{ role: 'zoomOut', label: 'Zoom out' },
|
{ role: 'zoomOut', label: 'Zoom out' },
|
||||||
|
@ -176,7 +213,6 @@ export function buildMenuBar(): Menu {
|
||||||
{ role: 'togglefullscreen', label: 'Toggle fullscreen' },
|
{ role: 'togglefullscreen', label: 'Toggle fullscreen' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// { role: 'windowMenu' }
|
|
||||||
{
|
{
|
||||||
label: 'Window',
|
label: 'Window',
|
||||||
submenu: [
|
submenu: [
|
||||||
|
@ -184,15 +220,33 @@ export function buildMenuBar(): Menu {
|
||||||
...((isMac
|
...((isMac
|
||||||
? [
|
? [
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'front', label: 'Front' },
|
{ role: 'front', label: 'Bring to front' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'window', label: 'Window' },
|
{ role: 'window', label: 'ente' },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ role: 'close', label: 'Close' },
|
{ role: 'close', label: 'Close ente' },
|
||||||
]) as MenuItemConstructorOptions[]),
|
]) as MenuItemConstructorOptions[]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'FAQ',
|
||||||
|
click: () => shell.openExternal('https://ente.io/faq/'),
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Support',
|
||||||
|
click: () => shell.openExternal('mailto:support@ente.io'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Product updates',
|
||||||
|
click: () => shell.openExternal('https://ente.io/blog/'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
return Menu.buildFromTemplate(template);
|
return Menu.buildFromTemplate(template);
|
||||||
}
|
}
|
16
src/utils/preload.ts
Normal file
16
src/utils/preload.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { webFrame } from 'electron';
|
||||||
|
|
||||||
|
export const fixHotReloadNext12 = () => {
|
||||||
|
webFrame.executeJavaScript(`Object.defineProperty(globalThis, 'WebSocket', {
|
||||||
|
value: new Proxy(WebSocket, {
|
||||||
|
construct: (Target, [url, protocols]) => {
|
||||||
|
if (url.endsWith('/_next/webpack-hmr')) {
|
||||||
|
// Fix the Next.js hmr client url
|
||||||
|
return new Target("ws://localhost:3000/_next/webpack-hmr", protocols)
|
||||||
|
} else {
|
||||||
|
return new Target(url, protocols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});`);
|
||||||
|
};
|
11
src/utils/watch.ts
Normal file
11
src/utils/watch.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { WatchMapping } from '../types';
|
||||||
|
|
||||||
|
export function isMappingPresent(
|
||||||
|
watchMappings: WatchMapping[],
|
||||||
|
folderPath: string
|
||||||
|
) {
|
||||||
|
const watchMapping = watchMappings?.find(
|
||||||
|
(mapping) => mapping.folderPath === folderPath
|
||||||
|
);
|
||||||
|
return !!watchMapping;
|
||||||
|
}
|
1
thirdparty/next-electron-server
vendored
Submodule
1
thirdparty/next-electron-server
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a88030295c89dd8f43d9e3a45025678d95c78a45
|
|
@ -6,10 +6,10 @@
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "app",
|
"outDir": "app",
|
||||||
"baseUrl": "./main",
|
"baseUrl": "src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["node_modules/*"]
|
"*": ["node_modules/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["main/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -265,6 +265,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||||
|
|
||||||
|
"@types/auto-launch@^5.0.2":
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.2.tgz#4970f01e5dd27572489b7fe77590204a19f86bd0"
|
||||||
|
integrity sha512-b03X09+GCM9t6AUECpwA2gUPYs8s5tJHFJw92sK8EiJ7G4QNbsHmXV7nfCfP6G6ivtm230vi4oNfe8AzRgzxMQ==
|
||||||
|
|
||||||
"@types/debug@^4.1.6":
|
"@types/debug@^4.1.6":
|
||||||
version "4.1.7"
|
version "4.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||||
|
@ -594,6 +599,11 @@ app-builder-lib@23.1.0:
|
||||||
tar "^6.1.11"
|
tar "^6.1.11"
|
||||||
temp-file "^3.4.0"
|
temp-file "^3.4.0"
|
||||||
|
|
||||||
|
applescript@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/applescript/-/applescript-1.0.0.tgz#bb87af568cad034a4e48c4bdaf6067a3a2701317"
|
||||||
|
integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==
|
||||||
|
|
||||||
aproba@^1.0.3:
|
aproba@^1.0.3:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||||
|
@ -678,6 +688,17 @@ atomically@^1.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
|
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
|
||||||
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
|
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
|
||||||
|
|
||||||
|
auto-launch@^5.0.5:
|
||||||
|
version "5.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.5.tgz#d14bd002b1ef642f85e991a6195ff5300c8ad3c0"
|
||||||
|
integrity sha512-ppdF4mihhYzMYLuCcx9H/c5TUOCev8uM7en53zWVQhyYAJrurd2bFZx3qQVeJKF2jrc7rsPRNN5cD+i23l6PdA==
|
||||||
|
dependencies:
|
||||||
|
applescript "^1.0.0"
|
||||||
|
mkdirp "^0.5.1"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
untildify "^3.0.2"
|
||||||
|
winreg "1.2.4"
|
||||||
|
|
||||||
aws-sign2@~0.7.0:
|
aws-sign2@~0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||||
|
@ -888,7 +909,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
chokidar@^3.5.2:
|
chokidar@^3.5.2, chokidar@^3.5.3:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||||
|
@ -2724,7 +2745,7 @@ minizlib@^2.1.1:
|
||||||
minipass "^3.0.0"
|
minipass "^3.0.0"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
mkdirp@^0.5.4, mkdirp@^0.5.5:
|
mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5:
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||||
|
@ -2756,10 +2777,8 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
next-electron-server@^0.0.8:
|
"next-electron-server@file:./thirdparty/next-electron-server":
|
||||||
version "0.0.8"
|
version "0.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-0.0.8.tgz#bc17772636a099d678a6f0c3f9bc8e952ee75967"
|
|
||||||
integrity sha512-LQhqf1Lrw9QVPntWN8NtqQk4WMufxXinsfg2ODgfqiHpvhcG58MOVIfvtDL37yGRvswORLXUHYRNB+p59LAd/w==
|
|
||||||
|
|
||||||
node-addon-api@^1.6.3:
|
node-addon-api@^1.6.3:
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
|
@ -3878,6 +3897,11 @@ universalify@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||||
|
|
||||||
|
untildify@^3.0.2:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
|
||||||
|
integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
|
||||||
|
|
||||||
update-notifier@^5.1.0:
|
update-notifier@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
|
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
|
||||||
|
@ -3992,6 +4016,11 @@ widest-line@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width "^4.0.0"
|
string-width "^4.0.0"
|
||||||
|
|
||||||
|
winreg@1.2.4:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
|
||||||
|
integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==
|
||||||
|
|
||||||
word-wrap@^1.2.3:
|
word-wrap@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
|
|
Loading…
Add table
Reference in a new issue