diff --git a/.gitignore b/.gitignore index 6130491..d59a0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -/.idea -node_modules/ -/config -/dist +.idea +node_modules +config +dist +build GH_TOKEN yarn-error.log diff --git a/package.json b/package.json index da24adb..4c7514a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Config.js b/src/Config.ts similarity index 79% rename from src/Config.js rename to src/Config.ts index f8c0132..9d06768 100644 --- a/src/Config.js +++ b/src/Config.ts @@ -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]; + (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]; + (this)[prop] = data[prop]; } } } diff --git a/src/Meta.js b/src/Meta.js deleted file mode 100644 index 65dec44..0000000 --- a/src/Meta.js +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/Meta.ts b/src/Meta.ts new file mode 100644 index 0000000..3f0a83a --- /dev/null +++ b/src/Meta.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/Service.js b/src/Service.js deleted file mode 100644 index 5d3a9d2..0000000 --- a/src/Service.js +++ /dev/null @@ -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; diff --git a/src/Service.ts b/src/Service.ts new file mode 100644 index 0000000..04d6204 --- /dev/null +++ b/src/Service.ts @@ -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)) { + (this)[k] = data[k]; + } + } + } else { + this.partition = partition; + this.name = name; + this.icon = icon; + this.isImage = isImage; + this.url = url; + this.useFavicon = useFavicon; + } + } +} diff --git a/src/Updater.js b/src/Updater.ts similarity index 53% rename from src/Updater.js rename to src/Updater.ts index 372dbcc..5cece7e 100644 --- a/src/Updater.js +++ b/src/Updater.ts @@ -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); - } -} \ No newline at end of file +export type UpdateCheckCallback = (available: boolean, data: UpdateInfo) => void; diff --git a/src/main.js b/src/main.ts similarity index 81% rename from src/main.js rename to src/main.ts index 7b5572a..f8950fa 100644 --- a/src/main.js +++ b/src/main.ts @@ -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); }); diff --git a/src/types/single-instance.d.ts b/src/types/single-instance.d.ts new file mode 100644 index 0000000..646177b --- /dev/null +++ b/src/types/single-instance.d.ts @@ -0,0 +1,7 @@ +declare module "single-instance" { + export default class SingleInstance { + constructor(lockName: string); + + public lock(): Promise; + } +} \ No newline at end of file diff --git a/tabs.js b/tabs.js deleted file mode 100644 index 2bc68be..0000000 --- a/tabs.js +++ /dev/null @@ -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); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f133197 --- /dev/null +++ b/tsconfig.json @@ -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/**/*" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7611e64..21dcbcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"