Convert project backend to typescript

This commit is contained in:
Alice Gaudon 2020-05-21 07:38:44 +02:00
parent 31f2b3a133
commit 8683140003
13 changed files with 179 additions and 148 deletions

9
.gitignore vendored
View File

@ -1,7 +1,8 @@
/.idea .idea
node_modules/ node_modules
/config config
/dist dist
build
GH_TOKEN GH_TOKEN
yarn-error.log yarn-error.log

View File

@ -8,14 +8,14 @@
}, },
"homepage": "https://gitlab.com/ArisuOngaku/tabs", "homepage": "https://gitlab.com/ArisuOngaku/tabs",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"main": "tabs.js", "main": "build/main.js",
"scripts": { "scripts": {
"start": "electron .", "clean": "! test -d build || rm -r build",
"dev": "electron . --dev", "compile": "yarn clean && tsc",
"build": "electron-builder", "start": "yarn compile && electron .",
"build-arch": "electron-builder --linux dir", "dev": "yarn compile && electron . --dev",
"release": "GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always", "build": "yarn compile && electron-builder -wl",
"test": "echo \"Error: no test specified\" && exit 1" "release": "yarn compile && GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always"
}, },
"dependencies": { "dependencies": {
"appdata-path": "^1.0.0", "appdata-path": "^1.0.0",
@ -25,11 +25,17 @@
"single-instance": "^0.0.1" "single-instance": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.12.41",
"electron": "^9.0.0", "electron": "^9.0.0",
"electron-builder": "^22.4.0" "electron-builder": "^22.4.0",
"typescript": "^3.9.3"
}, },
"build": { "build": {
"appId": "tabs-app", "appId": "tabs-app",
"files": [
"resources/**/*",
"build/**/*"
],
"linux": { "linux": {
"target": "AppImage", "target": "AppImage",
"icon": "resources/logo.png", "icon": "resources/logo.png",

View File

@ -9,18 +9,19 @@ const configDir = Meta.isDevMode() ? getAppDataPath('tabs-app-dev') : getAppData
const configFile = path.resolve(configDir, 'config.json'); const configFile = path.resolve(configDir, 'config.json');
export default class Config { export default class Config {
updateCheckSkip = undefined; public services: Service[] = [];
securityButton = true; public updateCheckSkip?: string;
homeButton = false; public securityButton: boolean = true;
backButton = true; public homeButton: boolean = false;
forwardButton = false; public backButton: boolean = true;
refreshButton = false; public forwardButton: boolean = false;
public refreshButton: boolean = false;
properties = []; private properties: string[] = [];
constructor() { constructor() {
// Load data from config file // Load data from config file
let data = {}; let data: any = {};
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) { if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
if (fs.existsSync(configFile) && fs.statSync(configFile).isFile()) if (fs.existsSync(configFile) && fs.statSync(configFile).isFile())
data = JSON.parse(fs.readFileSync(configFile, 'utf8')); data = JSON.parse(fs.readFileSync(configFile, 'utf8'));
@ -29,7 +30,6 @@ export default class Config {
} }
// Parse services // Parse services
this.services = [];
if (typeof data.services === 'object') { if (typeof data.services === 'object') {
let i = 0; let i = 0;
for (const service of data.services) { for (const service of data.services) {
@ -60,18 +60,18 @@ export default class Config {
console.log('> Config saved to', configFile.toString()); console.log('> Config saved to', configFile.toString());
} }
defineProperty(name, data) { defineProperty(name: string, data: any) {
if (data[name] !== undefined) { if (data[name] !== undefined) {
this[name] = data[name]; (<any>this)[name] = data[name];
} }
this.properties.push(name); this.properties.push(name);
} }
update(data) { update(data: any) {
for (const prop of this.properties) { for (const prop of this.properties) {
if (data[prop] !== undefined) { if (data[prop] !== undefined) {
this[prop] = data[prop]; (<any>this)[prop] = data[prop];
} }
} }
} }

View File

@ -1,29 +0,0 @@
export default class Meta {
static #title = 'Tabs';
static #devMode = null;
static get title() {
return this.#title;
}
static isDevMode() {
if (this.#devMode === null) {
this.#devMode = process.argv.length > 2 && process.argv[2] === '--dev';
console.debug('Dev mode:', this.#devMode);
}
return this.#devMode;
}
/**
* @param service {Service}
* @param viewTitle {string}
*/
static getTitleForService(service, viewTitle) {
let suffix = '';
if (typeof viewTitle === 'string' && viewTitle.length > 0) {
suffix = ' - ' + viewTitle;
}
return this.title + ' - ' + service.name + suffix;
}
}

23
src/Meta.ts Normal file
View File

@ -0,0 +1,23 @@
import Service from "./Service";
export default class Meta {
public static readonly title = 'Tabs';
private static devMode?: boolean;
public static isDevMode() {
if (this.devMode === undefined) {
this.devMode = process.argv.length > 2 && process.argv[2] === '--dev';
console.debug('Dev mode:', this.devMode);
}
return this.devMode;
}
public static getTitleForService(service: Service, viewTitle: string) {
let suffix = '';
if (viewTitle.length > 0) {
suffix = ' - ' + viewTitle;
}
return this.title + ' - ' + service.name + suffix;
}
}

View File

@ -1,42 +0,0 @@
class Service {
constructor(partition, name, icon, isImage, url, useFavicon) {
if (arguments.length === 1) {
let data = arguments[0];
for (let k in data) {
if (data.hasOwnProperty(k)) {
this[k] = data[k];
}
}
} else {
this.partition = partition;
this.name = name;
this.icon = icon;
this.isImage = isImage;
this.url = url;
this.useFavicon = useFavicon;
}
for (let k in Service.requiredProperties) {
if (Service.requiredProperties.hasOwnProperty(k)) {
if (!this.hasOwnProperty(k) || this[k] === undefined) {
this[k] = Service.requiredProperties[k];
}
}
}
}
}
Service.requiredProperties = {
'partition': null,
'name': null,
'icon': null,
'isImage': null,
'url': null,
'useFavicon': true,
'autoLoad': false,
'customCSS': null,
'customUserAgent': null,
'permissions': {},
};
export default Service;

31
src/Service.ts Normal file
View File

@ -0,0 +1,31 @@
export default class Service {
public partition?: string;
public name?: string;
public icon?: string;
public isImage?: boolean = false;
public url?: string;
public useFavicon?: boolean = true;
public favicon?: string;
public autoLoad?: boolean = false;
public customCSS?: string;
public customUserAgent?: string;
public permissions?: {} = {};
constructor(partition: string, name?: string, icon?: string, isImage?: boolean, url?: string, useFavicon?: boolean) {
if (arguments.length === 1) {
const data = arguments[0];
for (const k in data) {
if (data.hasOwnProperty(k)) {
(<any>this)[k] = data[k];
}
}
} else {
this.partition = partition;
this.name = name;
this.icon = icon;
this.isImage = isImage;
this.url = url;
this.useFavicon = useFavicon;
}
}
}

View File

@ -1,32 +1,32 @@
import {autoUpdater} from "electron-updater"; import {autoUpdater, UpdateInfo} from "electron-updater";
export default class Updater { export default class Updater {
#updateInfo; private updateInfo?: UpdateInfo;
constructor() { constructor() {
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
autoUpdater.on('error', err => { autoUpdater.on('error', err => {
this.notifyUpdate(false, err); console.log('Error while checking for updates', err);
}); });
autoUpdater.on('update-available', v => { autoUpdater.on('update-available', v => {
this.notifyUpdate(true, v); console.log('Update available', v);
}); });
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
this.notifyUpdate(false); console.log('No update available.');
}); });
} }
/** /**
* @param {Function} callback * @param {Function} callback
*/ */
checkForUpdates(callback) { checkForUpdates(callback: UpdateCheckCallback) {
if (this.#updateInfo) { if (this.updateInfo) {
callback(this.#updateInfo.version !== this.getCurrentVersion().raw, this.#updateInfo); callback(this.updateInfo.version !== this.getCurrentVersion().raw, this.updateInfo);
return; return;
} }
autoUpdater.checkForUpdates().then(r => { autoUpdater.checkForUpdates().then(r => {
this.#updateInfo = r.updateInfo; this.updateInfo = r.updateInfo;
callback(r.updateInfo.version !== this.getCurrentVersion().raw, r.updateInfo); callback(r.updateInfo.version !== this.getCurrentVersion().raw, r.updateInfo);
}).catch(err => { }).catch(err => {
callback(false, err); callback(false, err);
@ -36,8 +36,6 @@ export default class Updater {
getCurrentVersion() { getCurrentVersion() {
return autoUpdater.currentVersion; return autoUpdater.currentVersion;
} }
}
notifyUpdate(available, data) { export type UpdateCheckCallback = (available: boolean, data: UpdateInfo) => void;
console.log('Update:', available, data);
}
}

View File

@ -1,11 +1,13 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import SingleInstance from "single-instance";
import {app, BrowserWindow, dialog, ipcMain, Menu, shell, Tray} from "electron"; import {app, BrowserWindow, dialog, ipcMain, Menu, shell, Tray} from "electron";
import Meta from "./Meta"; import Meta from "./Meta";
import Config from "./Config"; import Config from "./Config";
import Service from "./Service"; import Service from "./Service";
import Updater from "./Updater"; import Updater from "./Updater";
import Event = Electron.Event;
const resourcesDir = path.resolve(__dirname, '../resources'); const resourcesDir = path.resolve(__dirname, '../resources');
const iconPath = path.resolve(resourcesDir, 'logo.png'); const iconPath = path.resolve(resourcesDir, 'logo.png');
@ -21,9 +23,9 @@ const solidIcons = listIcons('solid');
let selectedService = 0; let selectedService = 0;
let tray; let tray: Tray;
let window; let window: BrowserWindow | null;
let serviceSettingsWindow, settingsWindow; let serviceSettingsWindow: BrowserWindow | null, settingsWindow: BrowserWindow | null;
function toggleMainWindow() { function toggleMainWindow() {
if (window != null) { if (window != null) {
@ -41,7 +43,7 @@ async function createWindow() {
// Check for updates // Check for updates
updater.checkForUpdates((available, updateInfo) => { updater.checkForUpdates((available, updateInfo) => {
if (available && updateInfo.version !== config.updateCheckSkip) { if (available && updateInfo.version !== config.updateCheckSkip) {
dialog.showMessageBox(window, { dialog.showMessageBox(window!, {
message: `Version ${updateInfo.version} of tabs is available. Do you wish to download this update?`, message: `Version ${updateInfo.version} of tabs is available. Do you wish to download this update?`,
buttons: [ buttons: [
'Cancel', 'Cancel',
@ -71,7 +73,7 @@ async function createWindow() {
tray.setToolTip('Tabs'); tray.setToolTip('Tabs');
tray.setContextMenu(Menu.buildFromTemplate([ tray.setContextMenu(Menu.buildFromTemplate([
{label: 'Tabs', enabled: false}, {label: 'Tabs', enabled: false},
{label: 'Open Tabs', click: () => window.show()}, {label: 'Open Tabs', click: () => window!.show()},
{type: 'separator'}, {type: 'separator'},
{label: 'Quit', role: 'quit'} {label: 'Quit', role: 'quit'}
])); ]));
@ -142,7 +144,7 @@ async function createWindow() {
enableRemoteModule: true, enableRemoteModule: true,
webviewTag: true, webviewTag: true,
}, },
parent: window, parent: window!,
modal: true, modal: true,
autoHideMenuBar: true, autoHideMenuBar: true,
height: 850, height: 850,
@ -155,10 +157,10 @@ async function createWindow() {
mode: 'right' mode: 'right'
}); });
} }
let syncListener; let syncListener: () => void;
ipcMain.on('sync-settings', syncListener = () => { ipcMain.on('sync-settings', syncListener = () => {
serviceSettingsWindow.webContents.send('syncIcons', brandIcons, solidIcons); serviceSettingsWindow!.webContents.send('syncIcons', brandIcons, solidIcons);
serviceSettingsWindow.webContents.send('loadService', serviceId, config.services[serviceId]); serviceSettingsWindow!.webContents.send('loadService', serviceId, config.services[serviceId]);
}); });
serviceSettingsWindow.on('close', () => { serviceSettingsWindow.on('close', () => {
ipcMain.removeListener('sync-settings', syncListener); ipcMain.removeListener('sync-settings', syncListener);
@ -179,7 +181,7 @@ async function createWindow() {
} }
config.save(); config.save();
window.webContents.send('updateService', id, newService); window!.webContents.send('updateService', id, newService);
}); });
ipcMain.on('deleteService', (e, id) => { ipcMain.on('deleteService', (e, id) => {
@ -187,7 +189,7 @@ async function createWindow() {
delete config.services[id]; delete config.services[id];
config.save(); config.save();
window.webContents.send('deleteService', id); window!.webContents.send('deleteService', id);
}); });
ipcMain.on('reorderService', (e, serviceId, targetId) => { ipcMain.on('reorderService', (e, serviceId, targetId) => {
@ -219,10 +221,10 @@ async function createWindow() {
ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => { ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => {
if (serviceId === null) { if (serviceId === null) {
window.setTitle(Meta.title); window!.setTitle(Meta.title);
} else { } else {
const service = config.services[serviceId]; const service = config.services[serviceId];
window.setTitle(Meta.getTitleForService(service, viewTitle)); window!.setTitle(Meta.getTitleForService(service, viewTitle));
} }
}); });
@ -236,7 +238,7 @@ async function createWindow() {
enableRemoteModule: true, enableRemoteModule: true,
webviewTag: true, webviewTag: true,
}, },
parent: window, parent: window!,
modal: true, modal: true,
autoHideMenuBar: true, autoHideMenuBar: true,
height: 850, height: 850,
@ -249,21 +251,21 @@ async function createWindow() {
mode: 'right' mode: 'right'
}); });
} }
let syncListener; let syncListener: () => void;
ipcMain.on('syncSettings', syncListener = () => { ipcMain.on('syncSettings', syncListener = () => {
settingsWindow.webContents.send('current-version', updater.getCurrentVersion()); settingsWindow!.webContents.send('current-version', updater.getCurrentVersion());
settingsWindow.webContents.send('config', config); settingsWindow!.webContents.send('config', config);
}); });
let checkForUpdatesListener; let checkForUpdatesListener: () => void;
ipcMain.on('checkForUpdates', checkForUpdatesListener = (e) => { ipcMain.on('checkForUpdates', checkForUpdatesListener = () => {
updater.checkForUpdates((available, version) => { updater.checkForUpdates((available, version) => {
settingsWindow.webContents.send('updateStatus', available, version); settingsWindow!.webContents.send('updateStatus', available, version);
}); });
}); });
let saveConfigListener; let saveConfigListener: (e: Event, data: any) => void;
ipcMain.on('save-config', saveConfigListener = (e, data) => { ipcMain.on('save-config', saveConfigListener = (e: Event, data: any) => {
config.update(data); config.update(data);
config.save(); config.save();
sendData(); sendData();
@ -284,18 +286,18 @@ async function createWindow() {
function sendData() { function sendData() {
console.log('Syncing data'); console.log('Syncing data');
window.webContents.send('data', Meta.title, brandIcons, solidIcons, selectedService, path.resolve(resourcesDir, 'empty.html'), config); window!.webContents.send('data', Meta.title, brandIcons, solidIcons, selectedService, path.resolve(resourcesDir, 'empty.html'), config);
} }
function setActiveService(index) { function setActiveService(index: number) {
console.log('Selected service is now', index); console.log('Selected service is now', index);
selectedService = index; selectedService = index;
} }
function listIcons(set) { function listIcons(set: string) {
console.log('Loading icon set', set); console.log('Loading icon set', set);
const directory = path.resolve(resourcesDir, 'icons/' + set); const directory = path.resolve(resourcesDir, 'icons/' + set);
const icons = []; const icons: { name: string; faIcon: string }[] = [];
const dir = set === 'brands' ? 'fab' : 'fas'; const dir = set === 'brands' ? 'fab' : 'fas';
fs.readdirSync(directory).forEach(i => icons.push({ fs.readdirSync(directory).forEach(i => icons.push({
name: i.split('.svg')[0], name: i.split('.svg')[0],
@ -304,7 +306,15 @@ function listIcons(set) {
return icons; return icons;
} }
console.log('Starting app');
app.on('ready', () => { // Check if application is already running
createWindow().catch(console.error); const lock = new SingleInstance('tabs-app');
lock.lock().then(() => {
console.log('Starting app');
app.on('ready', () => {
createWindow().catch(console.error);
});
}).catch(err => {
console.error(err);
process.exit(0);
}); });

7
src/types/single-instance.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare module "single-instance" {
export default class SingleInstance {
constructor(lockName: string);
public lock(): Promise<void>;
}
}

10
tabs.js
View File

@ -1,10 +0,0 @@
#!/bin/electron
const SingleInstance = require('single-instance');
const lock = new SingleInstance('tabs-app');
lock.lock().then(() => {
require = require("esm")(module);
module.exports = require("./src/main.js");
}).catch(error => {
console.error(error);
process.exit(0);
});

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"module": "CommonJS",
"esModuleInterop": true,
"outDir": "build",
"target": "ES6",
"strict": true,
"lib": [
"es2020",
"dom"
],
"typeRoots": [
"./node_modules/@types",
"src/types"
],
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
}
},
"include": [
"src/**/*"
]
}

View File

@ -70,6 +70,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.39.tgz#532d25c1e639d89dd6f3aa1d7b3962e3e7fa943d" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.39.tgz#532d25c1e639d89dd6f3aa1d7b3962e3e7fa943d"
integrity sha512-pADGfwnDkr6zagDwEiCVE4yQrv7XDkoeVa4OfA9Ju/zRTk6YNDLGtQbkdL4/56mCQQCs4AhNrBIag6jrp7ZuOg== integrity sha512-pADGfwnDkr6zagDwEiCVE4yQrv7XDkoeVa4OfA9Ju/zRTk6YNDLGtQbkdL4/56mCQQCs4AhNrBIag6jrp7ZuOg==
"@types/node@^12.12.41":
version "12.12.41"
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.41.tgz#cf48562b53ab6cf85d28dde95f1d06815af275c8"
integrity sha512-Q+eSkdYQJ2XK1AJnr4Ji8Gvk3sRDybEwfTvtL9CA25FFUSD2EgZQewN6VCyWYZCXg5MWZdwogdTNBhlWRcWS1w==
"@types/semver@^7.1.0": "@types/semver@^7.1.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" resolved "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
@ -1505,6 +1510,11 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.9.3:
version "3.9.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
unique-string@^2.0.0: unique-string@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"