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
node_modules/
/config
/dist
.idea
node_modules
config
dist
build
GH_TOKEN
yarn-error.log

View File

@ -8,14 +8,14 @@
},
"homepage": "https://gitlab.com/ArisuOngaku/tabs",
"license": "GPL-3.0-only",
"main": "tabs.js",
"main": "build/main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build-arch": "electron-builder --linux dir",
"release": "GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always",
"test": "echo \"Error: no test specified\" && exit 1"
"clean": "! test -d build || rm -r build",
"compile": "yarn clean && tsc",
"start": "yarn compile && electron .",
"dev": "yarn compile && electron . --dev",
"build": "yarn compile && electron-builder -wl",
"release": "yarn compile && GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always"
},
"dependencies": {
"appdata-path": "^1.0.0",
@ -25,11 +25,17 @@
"single-instance": "^0.0.1"
},
"devDependencies": {
"@types/node": "^12.12.41",
"electron": "^9.0.0",
"electron-builder": "^22.4.0"
"electron-builder": "^22.4.0",
"typescript": "^3.9.3"
},
"build": {
"appId": "tabs-app",
"files": [
"resources/**/*",
"build/**/*"
],
"linux": {
"target": "AppImage",
"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');
export default class Config {
updateCheckSkip = undefined;
securityButton = true;
homeButton = false;
backButton = true;
forwardButton = false;
refreshButton = false;
public services: Service[] = [];
public updateCheckSkip?: string;
public securityButton: boolean = true;
public homeButton: boolean = false;
public backButton: boolean = true;
public forwardButton: boolean = false;
public refreshButton: boolean = false;
properties = [];
private properties: string[] = [];
constructor() {
// Load data from config file
let data = {};
let data: any = {};
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
if (fs.existsSync(configFile) && fs.statSync(configFile).isFile())
data = JSON.parse(fs.readFileSync(configFile, 'utf8'));
@ -29,7 +30,6 @@ export default class Config {
}
// Parse services
this.services = [];
if (typeof data.services === 'object') {
let i = 0;
for (const service of data.services) {
@ -60,18 +60,18 @@ export default class Config {
console.log('> Config saved to', configFile.toString());
}
defineProperty(name, data) {
defineProperty(name: string, data: any) {
if (data[name] !== undefined) {
this[name] = data[name];
(<any>this)[name] = data[name];
}
this.properties.push(name);
}
update(data) {
update(data: any) {
for (const prop of this.properties) {
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 {
#updateInfo;
private updateInfo?: UpdateInfo;
constructor() {
autoUpdater.autoDownload = false;
autoUpdater.on('error', err => {
this.notifyUpdate(false, err);
console.log('Error while checking for updates', err);
});
autoUpdater.on('update-available', v => {
this.notifyUpdate(true, v);
console.log('Update available', v);
});
autoUpdater.on('update-not-available', () => {
this.notifyUpdate(false);
console.log('No update available.');
});
}
/**
* @param {Function} callback
*/
checkForUpdates(callback) {
if (this.#updateInfo) {
callback(this.#updateInfo.version !== this.getCurrentVersion().raw, this.#updateInfo);
checkForUpdates(callback: UpdateCheckCallback) {
if (this.updateInfo) {
callback(this.updateInfo.version !== this.getCurrentVersion().raw, this.updateInfo);
return;
}
autoUpdater.checkForUpdates().then(r => {
this.#updateInfo = r.updateInfo;
this.updateInfo = r.updateInfo;
callback(r.updateInfo.version !== this.getCurrentVersion().raw, r.updateInfo);
}).catch(err => {
callback(false, err);
@ -36,8 +36,6 @@ export default class Updater {
getCurrentVersion() {
return autoUpdater.currentVersion;
}
}
notifyUpdate(available, data) {
console.log('Update:', available, data);
}
}
export type UpdateCheckCallback = (available: boolean, data: UpdateInfo) => void;

View File

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