diff --git a/.eslintrc.json b/.eslintrc.json index 96c05aa..c0cb14b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,6 +27,7 @@ "error", { "code": 120, + "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true } @@ -81,7 +82,8 @@ { "accessibility": "explicit" } - ] + ], + "@typescript-eslint/no-floating-promises": "error" }, "ignorePatterns": [ "build/**/*", diff --git a/frontend/ts/files.ts b/frontend/ts/files.ts index 42c532f..36cd3a0 100644 --- a/frontend/ts/files.ts +++ b/frontend/ts/files.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ function requireAll(r: any) { r.keys().forEach(r); } diff --git a/frontend/ts/index.ts b/frontend/ts/index.ts index 8be799d..46c5fce 100644 --- a/frontend/ts/index.ts +++ b/frontend/ts/index.ts @@ -1,13 +1,18 @@ import { clipboard, + ContextMenuParams, DidFailLoadEvent, ipcRenderer, PageFaviconUpdatedEvent, remote, shell, UpdateTargetUrlEvent, - WebContents + WebContents, + WebviewTag, } from "electron"; +import Service from "../../src/Service"; +import {IconProperties, IconSet, SpecialPages} from "../../src/Meta"; +import Config from "../../src/Config"; const { Menu, @@ -16,34 +21,38 @@ const { session, } = remote; -const appInfo: any = {}; -let icons: any[] = []; +const appInfo: { + title?: string; +} = {}; +let icons: IconProperties[] = []; -let services: any[] = []; -let selectedService: any = null; -let securityButton: HTMLElement | null, - homeButton: HTMLElement | null, - forwardButton: HTMLElement | null, - backButton: HTMLElement | null, - refreshButton: HTMLElement | null; +let services: (FrontService | undefined)[] = []; +let selectedServiceId: number | null = null; +let securityButton: HTMLElement | null = null, + homeButton: HTMLElement | null = null, + forwardButton: HTMLElement | null = null, + backButton: HTMLElement | null = null, + refreshButton: HTMLElement | null = null; let addButton, settingsButton; -let pages: any; -let urlPreview: HTMLElement | null; -let serviceSelector: HTMLElement | null; +let specialPages: SpecialPages | null = null; +let urlPreview: HTMLElement | null = null; +let serviceSelector: HTMLElement | null = null; // Service reordering -let lastDragPosition: HTMLElement | null, oldActiveService: number; +let lastDragPosition: HTMLElement | null = null; +let oldActiveService: number | null = null; // Service context menu function openServiceContextMenu(event: Event, serviceId: number) { event.preventDefault(); const service = services[serviceId]; + if (!service) throw new Error('Service doesn\'t exist.'); const menu = new Menu(); const ready = service.view && service.viewReady, notReady = !service.view && !service.viewReady; menu.append(new MenuItem({ label: 'Home', click: () => { - service.view.loadURL(service.url) + service.view?.loadURL(service.url) .catch(console.error); }, enabled: ready, @@ -63,13 +72,13 @@ function openServiceContextMenu(event: Event, serviceId: number) { menu.append(new MenuItem({type: "separator"})); - let permissionsMenu = []; + const permissionsMenu = []; if (ready) { - for (const domain in service.permissions) { - if (service.permissions.hasOwnProperty(domain)) { - const domainPermissionsMenu = []; + for (const domain of Object.keys(service.permissions)) { + const domainPermissionsMenu = []; - const domainPermissions = service.permissions[domain]; + const domainPermissions = service.permissions[domain]; + if (domainPermissions) { for (const permission of domainPermissions) { domainPermissionsMenu.push({ label: (permission.authorized ? '✓' : '❌') + ' ' + permission.name, @@ -82,18 +91,18 @@ function openServiceContextMenu(event: Event, serviceId: number) { }, { label: 'Forget', click: () => { - service.permissions[domain] = domainPermissions.filter((p: any) => p !== permission); + service.permissions[domain] = domainPermissions.filter(p => p !== permission); }, }], }); } + } - if (domainPermissionsMenu.length > 0) { - permissionsMenu.push({ - label: domain, - submenu: domainPermissionsMenu, - }); - } + if (domainPermissionsMenu.length > 0) { + permissionsMenu.push({ + label: domain, + submenu: domainPermissionsMenu, + }); } } } @@ -108,7 +117,7 @@ function openServiceContextMenu(event: Event, serviceId: number) { menu.append(new MenuItem({ label: 'Edit', click: () => { ipcRenderer.send('openServiceSettings', serviceId); - } + }, })); menu.append(new MenuItem({ label: 'Delete', click: () => { @@ -123,22 +132,32 @@ function openServiceContextMenu(event: Event, serviceId: number) { ipcRenderer.send('deleteService', serviceId); } }).catch(console.error); - } + }, })); menu.popup({window: remote.getCurrentWindow()}); } -ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, urls, config) => { +ipcRenderer.on('data', ( + event, + appTitle: string, + iconSets: IconSet[], + activeServiceId: number, + _specialPages: SpecialPages, + config: Config, +) => { // App info - appInfo.title = appData.title; + appInfo.title = appTitle; // Icons icons = []; for (const set of iconSets) { - icons = icons.concat(set); + icons.push(...set); } + // Special pages + specialPages = _specialPages; + console.log('Updating services ...'); services = config.services; @@ -155,7 +174,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, urls, c } for (let i = 0; i < services.length; i++) { - createService(i); + createServiceNavigationElement(i); } // Init drag last position @@ -165,7 +184,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, urls, c const index = services.length; if (draggedId !== index && draggedId !== index - 1) { resetDrag(); - lastDragTarget = dragTargetId = index; + lastDragTarget = index; lastDragPosition?.classList.remove('hidden'); lastDragPosition?.classList.add('drag-target'); } @@ -173,22 +192,20 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, urls, c } // Set active service - if (actualSelectedService < 0 || actualSelectedService >= services.length) { - actualSelectedService = 0; + if (activeServiceId < 0 || activeServiceId >= services.length) { + activeServiceId = 0; } - setActiveService(actualSelectedService); - - // Empty - pages = urls; + setActiveService(activeServiceId); // Url preview element urlPreview = document.getElementById("url-preview"); if (urlPreview) { + const _urlPreview = urlPreview; urlPreview.addEventListener('mouseover', () => { - if (urlPreview!.classList.contains('right')) { - urlPreview!.classList.remove('right'); + if (_urlPreview.classList.contains('right')) { + _urlPreview.classList.remove('right'); } else { - urlPreview!.classList.add('right'); + _urlPreview.classList.add('right'); } }); } @@ -227,32 +244,34 @@ function removeServiceFeatures(id: number): Element | null { } // Remove webview - if (services[id] && services[id].view) { - document.querySelector('#services')?.removeChild(services[id].view); + const service = services[id]; + if (service) { + const view = service.view; + if (view) document.querySelector('#services')?.removeChild(view); } return nextSibling; } -ipcRenderer.on('updateService', (e, id, data) => { +ipcRenderer.on('updateService', (e, id: number | null, data: Service) => { if (id === null) { console.log('Adding new service'); services.push(data); - createService(services.length - 1); + createServiceNavigationElement(services.length - 1); } else { console.log('Updating existing service', id); const nextSibling = removeServiceFeatures(id); // Create new service services[id] = data; - createService(id, nextSibling); - if (parseInt(selectedService) === id) { + createServiceNavigationElement(id, nextSibling); + if (selectedServiceId === id) { setActiveService(id); } } }); -ipcRenderer.on('reorderService', (e, serviceId, targetId) => { +ipcRenderer.on('reorderService', (e, serviceId: number, targetId: number) => { const oldServices = services; services = []; @@ -276,48 +295,56 @@ ipcRenderer.on('reorderService', (e, serviceId, targetId) => { serviceSelector.innerHTML = ''; } for (let i = 0; i < services.length; i++) { - services[i].li = undefined; - createService(i); + const service = services[i]; + if (service) service.li = undefined; + createServiceNavigationElement(i); } setActiveService(newId); }); -ipcRenderer.on('deleteService', (e, id) => { +ipcRenderer.on('deleteService', (e, id: number) => { removeServiceFeatures(id); - if (parseInt(selectedService) === id) { + if (selectedServiceId === id) { setActiveService(0); } - delete services[id]; - services = services.filter(s => s !== null); + services.splice(id, 1); }); -function createService(index: number, nextNavButton?: Element | null) { - let service = services[index]; - let li = document.createElement('li'); +function createServiceNavigationElement(index: number, nextNavButton?: Element | null) { + const service = services[index]; + if (!service) throw new Error('Service doesn\'t exist.'); + + const li = document.createElement('li') as NavigationElement; service.li = li; - let button = document.createElement('button'); + const button = document.createElement('button'); button.dataset.serviceId = '' + index; button.dataset.tooltip = service.name; button.addEventListener('click', () => { - if (button.dataset.serviceId) { - setActiveService(parseInt(button.dataset.serviceId)); + const rawId = button.dataset.serviceId; + if (rawId) { + const id = parseInt(rawId); + setActiveService(id); + ipcRenderer.send('setActiveService', id); } - ipcRenderer.send('setActiveService', button.dataset.serviceId); }); button.addEventListener('contextmenu', e => openServiceContextMenu(e, index)); - let icon: any; + let icon: HTMLImageElement | HTMLElement; if (service.useFavicon && service.favicon != null) { icon = document.createElement('img'); - icon.src = service.favicon; - icon.alt = service.name; - } else if (service.isImage) { + if (icon instanceof HTMLImageElement) { + icon.src = service.favicon; + icon.alt = service.name; + } + } else if (service.isImage && service.icon) { icon = document.createElement('img'); - icon.src = service.icon; - icon.alt = service.name; + if (icon instanceof HTMLImageElement) { + icon.src = service.icon; + icon.alt = service.name; + } } else { icon = document.createElement('i'); let iconProperties = icons.find(i => `${i.set}/${i.name}` === service.icon); @@ -356,10 +383,8 @@ function createService(index: number, nextNavButton?: Element | null) { let draggedId: number; let lastDragTarget = -1; -let dragTargetId = -1; -let dragTargetCount = 0; -function initDrag(index: number, li: any) { +function initDrag(index: number, li: NavigationElement) { li.serviceId = index; li.draggable = true; li.addEventListener('dragstart', (event: DragEvent) => { @@ -371,7 +396,7 @@ function initDrag(index: number, li: any) { }); li.addEventListener('dragover', (e: DragEvent) => { let realIndex = index; - let rect = li.getBoundingClientRect(); + const rect = li.getBoundingClientRect(); if ((e.clientY - rect.y) / rect.height >= 0.5) { realIndex++; @@ -382,12 +407,18 @@ function initDrag(index: number, li: any) { } resetDrag(); - let el = realIndex === services.length ? lastDragPosition : services[realIndex].li; - lastDragTarget = dragTargetId = realIndex; + const el = realIndex === services.length ? + lastDragPosition : + services[realIndex]?.li; + lastDragTarget = realIndex; lastDragPosition?.classList.remove('hidden'); - el.classList.add('drag-target'); - if (draggedId === realIndex || draggedId === realIndex - 1) el.classList.add('drag-target-self'); + if (el) { + el.classList.add('drag-target'); + + if (draggedId === realIndex || draggedId === realIndex - 1) + el.classList.add('drag-target-self'); + } }); li.addEventListener('dragend', () => { reorderService(draggedId, lastDragTarget); @@ -397,8 +428,6 @@ function initDrag(index: number, li: any) { function resetDrag() { lastDragTarget = -1; - dragTargetId = -1; - dragTargetCount = 0; serviceSelector?.querySelectorAll('li').forEach(li => { li.classList.remove('drag-target'); li.classList.remove('drag-target-self'); @@ -411,7 +440,7 @@ function resetDrag() { function reorderService(serviceId: number, targetId: number) { console.log('Reordering service', serviceId, targetId); if (targetId >= 0) { - oldActiveService = selectedService; + oldActiveService = selectedServiceId; setActiveService(-1); ipcRenderer.send('reorderService', serviceId, targetId); } @@ -447,76 +476,78 @@ function setActiveService(serviceId: number) { } // Hide previous service - if (services[selectedService] && services[selectedService].view) { - services[selectedService].view.classList.remove('active'); + if (typeof selectedServiceId === 'number') { + const selectedService = services[selectedServiceId]; + if (selectedService && selectedService.view) { + selectedService.view.classList.remove('active'); + } } // Show service - if (currentService) { - currentService.view.classList.add('active'); - } + currentService?.view?.classList.add('active'); // Save active service ID - selectedService = serviceId; + selectedServiceId = serviceId; // Refresh navigation updateNavigation(); }); } -function loadService(serviceId: number, service: any) { +function loadService(serviceId: number, service: FrontService) { // Load service if not loaded yet if (!service.view && !service.viewReady) { console.log('Loading service', serviceId); document.querySelector('#services > .loader')?.classList.remove('hidden'); - service.view = document.createElement('webview'); + const view = service.view = document.createElement('webview'); updateNavigation(); // Start loading animation - service.view.setAttribute('enableRemoteModule', 'false'); - service.view.setAttribute('partition', 'persist:service_' + service.partition); - service.view.setAttribute('autosize', 'true'); - service.view.setAttribute('src', pages?.empty); + view.setAttribute('enableRemoteModule', 'false'); + view.setAttribute('partition', 'persist:service_' + service.partition); + view.setAttribute('autosize', 'true'); + if (specialPages) view.setAttribute('src', specialPages.empty); // Enable context isolation. This is currently not used as there is no preload script; however it could prevent // eventual future human mistakes. - service.view.setAttribute('webpreferences', 'contextIsolation=yes'); + view.setAttribute('webpreferences', 'contextIsolation=yes'); // Error handling - service.view.addEventListener('did-fail-load', (e: DidFailLoadEvent) => { + view.addEventListener('did-fail-load', (e: DidFailLoadEvent) => { if (e.errorCode <= -100 && e.errorCode > -200) { - service.view.setAttribute('src', pages?.connectionError); + if (specialPages) view.setAttribute('src', specialPages.connectionError); } else if (e.errorCode === -6) { - service.view.setAttribute('src', pages?.fileNotFound); + if (specialPages) view.setAttribute('src', specialPages.fileNotFound); } else if (e.errorCode !== -3) { console.error('Unhandled error:', e); } }); // Append element to DOM - document.querySelector('#services')?.appendChild(service.view); + document.querySelector('#services')?.appendChild(view); // Load chain - let listener: Function; - service.view.addEventListener('dom-ready', listener = () => { - service.view.removeEventListener('dom-ready', listener); + let listener: () => void; + view.addEventListener('dom-ready', listener = () => { + view.removeEventListener('dom-ready', listener); - service.view.addEventListener('dom-ready', listener = () => { + view.addEventListener('dom-ready', listener = () => { if (service.customCSS) { - service.view.insertCSS(service.customCSS); + view.insertCSS(service.customCSS) + .catch(console.error); } document.querySelector('#services > .loader')?.classList.add('hidden'); - service.li.classList.add('loaded'); + service.li?.classList.add('loaded'); service.viewReady = true; updateNavigation(); - if (selectedService === null) { + if (selectedServiceId === null) { setActiveService(serviceId); } }); - const webContents = remote.webContents.fromId(service.view.getWebContentsId()); + const webContents = remote.webContents.fromId(view.getWebContentsId()); // Set custom user agent if (typeof service.customUserAgent === 'string') { @@ -528,7 +559,7 @@ function loadService(serviceId: number, service: any) { // Set permission request handler function getUrlDomain(url: string) { - let matches = url.match(/^https?:\/\/((.+?)\/|(.+))/i); + const matches = url.match(/^https?:\/\/((.+?)\/|(.+))/i); if (matches !== null) { let domain = matches[1]; if (domain.endsWith('/')) domain = domain.substr(0, domain.length - 1); @@ -544,12 +575,12 @@ function loadService(serviceId: number, service: any) { return domainPermissions; } - let serviceSession = session.fromPartition(service.view.partition); - serviceSession.setPermissionRequestHandler(((webContents, permissionName, callback, details) => { - let domain = getUrlDomain(details.requestingUrl); - let domainPermissions = getDomainPermissions(domain); + const serviceSession = session.fromPartition(view.partition); + serviceSession.setPermissionRequestHandler((webContents, permissionName, callback, details) => { + const domain = getUrlDomain(details.requestingUrl); + const domainPermissions = getDomainPermissions(domain); - let existingPermissions = domainPermissions.filter((p: any) => p.name === permissionName); + const existingPermissions = domainPermissions.filter(p => p.name === permissionName); if (existingPermissions.length > 0) { callback(existingPermissions[0].authorized); return; @@ -573,21 +604,21 @@ function loadService(serviceId: number, service: any) { console.log(authorized ? 'Granted' : 'Denied', permissionName, 'for domain', domain); callback(authorized); }).catch(console.error); - })); + }); serviceSession.setPermissionCheckHandler((webContents1, permissionName, requestingOrigin, details) => { console.log('Permission check', permissionName, requestingOrigin, details); - let domain = getUrlDomain(details.requestingUrl); - let domainPermissions = getDomainPermissions(domain); + const domain = getUrlDomain(details.requestingUrl); + const domainPermissions = getDomainPermissions(domain); - let existingPermissions = domainPermissions.filter((p: any) => p.name === permissionName); + const existingPermissions = domainPermissions.filter(p => p.name === permissionName); return existingPermissions.length > 0 && existingPermissions[0].authorized; }); - service.view.setAttribute('src', service.url); + view.setAttribute('src', service.url); }); // Load favicon - service.view.addEventListener('page-favicon-updated', (event: PageFaviconUpdatedEvent) => { + view.addEventListener('page-favicon-updated', (event: PageFaviconUpdatedEvent) => { console.debug('Loaded favicons for', service.name, event.favicons); if (event.favicons.length > 0 && service.favicon !== event.favicons[0]) { ipcRenderer.send('setServiceFavicon', serviceId, event.favicons[0]); @@ -596,15 +627,17 @@ function loadService(serviceId: number, service: any) { img.src = event.favicons[0]; img.alt = service.name; img.onload = () => { - service.li.button.innerHTML = ''; - service.li.button.appendChild(img); + if (service.li) { + service.li.button.innerHTML = ''; + service.li.button.appendChild(img); + } }; } } }); // Display target urls - service.view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => { + view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => { if (event.url.length === 0) { urlPreview?.classList.add('invisible'); } else { @@ -619,21 +652,28 @@ function loadService(serviceId: number, service: any) { function unloadService(serviceId: number) { const service = services[serviceId]; + if (!service) throw new Error('Service doesn\'t exist.'); + if (service.view && service.viewReady) { service.view.remove(); - service.view = null; - service.li.classList.remove('loaded'); + service.view = undefined; + service.li?.classList.remove('loaded'); service.viewReady = false; - if (parseInt(selectedService) === serviceId) { - selectedService = null; + if (selectedServiceId === serviceId) { + selectedServiceId = null; + for (let i = 0; i < services.length; i++) { - if (services[i].view && services[i].viewReady) { + const otherService = services[i]; + if (otherService && otherService.view && otherService.viewReady) { setActiveService(i); break; } } - if (selectedService === null) { + + // false positive: + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (selectedServiceId === null) { updateNavigation(); } } @@ -642,6 +682,8 @@ function unloadService(serviceId: number) { function reloadService(serviceId: number) { const service = services[serviceId]; + if (!service) throw new Error('Service doesn\'t exist.'); + if (service.view && service.viewReady) { console.log('Reloading service', serviceId); document.querySelector('#services > .loader')?.classList.remove('hidden'); @@ -653,6 +695,8 @@ function reloadService(serviceId: number) { function updateServicePermissions(serviceId: number) { const service = services[serviceId]; + if (!service) throw new Error('Service doesn\'t exist.'); + ipcRenderer.send('updateServicePermissions', serviceId, service.permissions); } @@ -661,11 +705,12 @@ function updateNavigation() { // Update active list element for (let i = 0; i < services.length; i++) { const service = services[i]; + if (!service) continue; if (!service.li) continue; // Active? - if (parseInt(selectedService) === i) service.li.classList.add('active'); + if (selectedServiceId === i) service.li.classList.add('active'); else service.li.classList.remove('active'); // Loading? @@ -677,10 +722,10 @@ function updateNavigation() { else service.li.classList.remove('loaded'); } - if (selectedService !== null && services[selectedService].viewReady) { + if (selectedServiceId !== null && services[selectedServiceId]?.viewReady) { console.debug('Updating navigation buttons because view is ready'); // Update history navigation - let view = services[selectedService].view; + const view = services[selectedServiceId]?.view; homeButton?.classList.remove('disabled'); @@ -699,44 +744,59 @@ function updateNavigation() { } function updateStatusButton() { - let protocol = services[selectedService].view.getURL().split('://')[0]; - if (!protocol) protocol = 'unknown'; - for (const c of securityButton?.children) { - if (c.classList.contains(protocol)) c.classList.add('active'); - else c.classList.remove('active'); - } + if (!selectedServiceId) return; + const protocol = services[selectedServiceId]?.view?.getURL().split('://')[0] || 'unknown'; + securityButton?.childNodes.forEach(el => { + if (el instanceof HTMLElement) { + if (el.classList.contains(protocol)) el.classList.add('active'); + else el.classList.remove('active'); + } + }); } function updateWindowTitle() { - if (selectedService === null) { + if (selectedServiceId === null) { ipcRenderer.send('updateWindowTitle', null); - } else if (services[selectedService].viewReady) { - ipcRenderer.send('updateWindowTitle', selectedService, remote.webContents.fromId(services[selectedService].view.getWebContentsId()).getTitle()); + } else { + const service = services[selectedServiceId]; + if (service?.viewReady && service.view) { + ipcRenderer.send('updateWindowTitle', selectedServiceId, remote.webContents.fromId(service.view.getWebContentsId()).getTitle()); + } } } function goHome() { - let service = services[selectedService]; - service.view.loadURL(service.url) + if (selectedServiceId === null) return; + + const service = services[selectedServiceId]; + if (!service) throw new Error('Service doesn\'t exist.'); + + service.view?.loadURL(service.url) .catch(console.error); } function goForward() { - let view = services[selectedService].view; + if (selectedServiceId === null) return; + + const view = services[selectedServiceId]?.view; if (view) remote.webContents.fromId(view.getWebContentsId()).goForward(); } function goBack() { - let view = services[selectedService].view; + if (selectedServiceId === null) return; + + const view = services[selectedServiceId]?.view; if (view) remote.webContents.fromId(view.getWebContentsId()).goBack(); } function reload() { - reloadService(selectedService); + if (selectedServiceId === null) return; + + reloadService(selectedServiceId); } function setContextMenu(webContents: WebContents) { - webContents.on('context-menu', (event, props) => { + webContents.on('context-menu', (event, props: ContextMenuParams) => { const menu = new Menu(); const {editFlags} = props; @@ -785,7 +845,8 @@ function setContextMenu(webContents: WebContents) { } // Text clipboard - if (editFlags.canUndo || editFlags.canRedo || editFlags.canCut || editFlags.canCopy || editFlags.canPaste || editFlags.canDelete) { + if (editFlags.canUndo || editFlags.canRedo || editFlags.canCut || editFlags.canCopy || editFlags.canPaste || + editFlags.canDelete) { if (editFlags.canUndo || editFlags.canRedo) { if (menu.items.length > 0) { menu.append(new MenuItem({type: 'separator'})); @@ -861,7 +922,18 @@ function setContextMenu(webContents: WebContents) { }); } -ipcRenderer.on('fullscreenchange', (e, fullscreen) => { +ipcRenderer.on('fullscreenchange', (e, fullscreen: boolean) => { if (fullscreen) document.body.classList.add('fullscreen'); else document.body.classList.remove('fullscreen'); }); + +type FrontService = Service & { + view?: WebviewTag; + viewReady?: boolean; + li?: NavigationElement; +}; + +type NavigationElement = HTMLLIElement & { + serviceId: number; + button: HTMLButtonElement; +}; diff --git a/frontend/ts/service-settings.ts b/frontend/ts/service-settings.ts index 179f0f8..ec96e6b 100644 --- a/frontend/ts/service-settings.ts +++ b/frontend/ts/service-settings.ts @@ -1,4 +1,6 @@ import {ipcRenderer, remote} from "electron"; +import Service from "../../src/Service"; +import {IconProperties, IconSet} from "../../src/Meta"; let isImageCheckbox: HTMLInputElement | null; let builtInIconSearchField: HTMLInputElement | null; @@ -12,22 +14,22 @@ let autoLoadInput: HTMLInputElement | null; let customCssInput: HTMLInputElement | null; let customUserAgentInput: HTMLInputElement | null; -let serviceId: number; -let service: any; +let serviceId: number | null = null; +let service: Service | null = null; -ipcRenderer.on('syncIcons', (event, iconSets) => { - let icons: any[] = []; +ipcRenderer.on('syncIcons', (event, iconSets: IconSet[]) => { + let icons: IconProperties[] = []; for (const set of iconSets) { icons = icons.concat(set); } loadIcons(icons); }); -ipcRenderer.on('loadService', (e, id, data) => { +ipcRenderer.on('loadService', (e, id: number | null, data?: Service) => { console.log('Load service', id); - if (id === null) { + if (id === null || !data) { document.title = 'Add a new service'; - service = {}; + service = null; if (h1) h1.innerText = 'Add a new service'; } else { @@ -54,9 +56,9 @@ document.addEventListener('DOMContentLoaded', () => { isImageCheckbox?.addEventListener('click', () => { - updateIconChoiceForm(isImageCheckbox?.checked); + updateIconChoiceForm(!!isImageCheckbox?.checked); }); - updateIconChoiceForm(isImageCheckbox?.checked); + updateIconChoiceForm(!!isImageCheckbox?.checked); builtInIconSearchField?.addEventListener('input', updateIconSearchResults); @@ -73,33 +75,37 @@ document.addEventListener('DOMContentLoaded', () => { ipcRenderer.send('sync-settings'); document.getElementById('userAgentAutoFillFirefox')?.addEventListener('click', () => { - let customUserAgent = document.getElementById('custom-user-agent'); + const customUserAgent = document.querySelector('#custom-user-agent'); if (customUserAgent) { - (customUserAgent).value = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'; + customUserAgent.value = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'; } }); document.getElementById('userAgentAutoFillChrome')?.addEventListener('click', () => { - let customUserAgent = document.getElementById('custom-user-agent'); + const customUserAgent = document.querySelector('#custom-user-agent'); if (customUserAgent) { - (customUserAgent).value = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'; + customUserAgent.value = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'; } }); }); function updateIconSearchResults() { - const searchStr: string = builtInIconSearchField!.value; - (iconSelect?.childNodes).forEach((c: HTMLElement) => { - let parts = c.dataset.icon?.split('/') || ''; - let iconName = parts[1] || parts[0]; - if (iconName.match(searchStr) || searchStr.match(iconName)) { - c.classList.remove('hidden'); - } else { - c.classList.add('hidden'); + if (!builtInIconSearchField) throw new Error('builtInIconSearchField no initialized.'); + + const searchStr: string = builtInIconSearchField.value; + iconSelect?.childNodes.forEach((el) => { + if (el instanceof HTMLElement) { + const parts = el.dataset.icon?.split('/') || ''; + const iconName = parts[1] || parts[0]; + if (iconName.match(searchStr) || searchStr.match(iconName)) { + el.classList.remove('hidden'); + } else { + el.classList.add('hidden'); + } } }); } -function loadIcons(icons: any[]) { +function loadIcons(icons: IconProperties[]) { icons.sort((a, b) => a.name > b.name ? 1 : -1); for (const icon of icons) { if (icon.name.length === 0) continue; @@ -132,16 +138,16 @@ function loadIcons(icons: any[]) { function selectIcon(choice: HTMLElement) { if (builtInIconSearchField) builtInIconSearchField.value = choice.dataset.icon || ''; if (iconSelect) { - for (const otherChoice of iconSelect.children) { - otherChoice.classList.remove('selected'); - } + iconSelect.childNodes.forEach(el => { + if (el instanceof Element) el.classList.remove('selected'); + }); } choice.classList.add('selected'); - let radio: HTMLInputElement | null = choice.querySelector('input[type=radio]'); + const radio: HTMLInputElement | null = choice.querySelector('input[type=radio]'); if (radio) radio.checked = true; } -function updateIconChoiceForm(isUrl: any) { +function updateIconChoiceForm(isUrl: boolean) { if (isUrl) { iconSelect?.classList.add('hidden'); builtInIconSearchField?.parentElement?.classList.add('hidden'); @@ -159,21 +165,22 @@ function loadServiceValues() { } if (nameInput) nameInput.value = service.name; - if (urlInput) urlInput.value = service.url; + if (urlInput) urlInput.value = service.url || ''; if (useFaviconInput) useFaviconInput.checked = service.useFavicon; if (autoLoadInput) autoLoadInput.checked = service.autoLoad; - if (customCssInput) customCssInput.value = service.customCSS; - if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent; + if (customCssInput) customCssInput.value = service.customCSS || ''; + if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent || ''; isImageCheckbox.checked = service.isImage; - if (service.isImage) { + if (service.isImage && service.icon) { if (iconUrlField) iconUrlField.value = service.icon; } else { - if (builtInIconSearchField) builtInIconSearchField.value = service.icon; + if (builtInIconSearchField && service.icon) builtInIconSearchField.value = service.icon; updateIconSearchResults(); - let labels = iconSelect?.querySelectorAll('label'); + const labels = iconSelect?.querySelectorAll('label'); if (labels) { - const icon = Array.from(labels).find(i => i.dataset.icon === service.icon); + const _service = service; + const icon = Array.from(labels).find(i => i.dataset.icon === _service.icon); if (icon) { selectIcon(icon); } @@ -182,23 +189,37 @@ function loadServiceValues() { } function save() { - let form = document.querySelector('form'); + if (!service) { + // Don't use new Service() to avoid depending on src/ file. + service = { + name: '', + partition: '', + url: '', + isImage: false, + useFavicon: false, + autoLoad: false, + permissions: {}, + }; + } + + const form = document.querySelector('form'); if (!form) return; const formData = new FormData(form); - service.name = formData.get('name'); - if (typeof service.partition !== 'string' || service.partition.length === 0) { + service.name = String(formData.get('name')); + if (service.partition.length === 0) { service.partition = service.name.replace(/ /g, '-'); service.partition = service.partition.replace(/[^a-zA-Z-_]/g, ''); } - service.url = formData.get('url'); + + service.url = String(formData.get('url')); service.isImage = formData.get('isImage') === 'on'; - service.icon = formData.get('icon'); + service.icon = String(formData.get('icon')); service.useFavicon = formData.get('useFavicon') === 'on'; service.autoLoad = formData.get('autoLoad') === 'on'; - service.customCSS = formData.get('customCSS'); + service.customCSS = String(formData.get('customCSS')); - let customUserAgent = (formData.get('customUserAgent')).trim(); - service.customUserAgent = customUserAgent.length === 0 ? null : customUserAgent; + const customUserAgent = (formData.get('customUserAgent')).trim(); + service.customUserAgent = customUserAgent.length === 0 ? undefined : customUserAgent; if (!isValid()) { return; @@ -209,15 +230,17 @@ function save() { } function isValid() { - if (typeof service.name !== 'string' || service.name.length === 0) { + if (!service) return false; + + if (service.name.length === 0) { console.log('Invalid name'); return false; } - if (typeof service.partition !== 'string' || service.partition.length === 0) { + if (service.partition.length === 0) { console.log('Invalid partition'); return false; } - if (typeof service.url !== 'string' || service.url.length === 0) { + if (service.url.length === 0) { console.log('Invalid url'); return false; } diff --git a/frontend/ts/settings.ts b/frontend/ts/settings.ts index f70cca0..24ec9f9 100644 --- a/frontend/ts/settings.ts +++ b/frontend/ts/settings.ts @@ -1,10 +1,13 @@ import {ipcRenderer, remote, shell} from "electron"; +import Config from "../../src/Config"; +import {SemVer} from "semver"; +import {UpdateInfo} from "electron-updater"; let currentVersion: HTMLElement | null; let updateStatus: HTMLElement | null; -let updateInfo: any; +let updateInfo: UpdateInfo; let updateButton: HTMLElement | null; -let config: any; +let config: Config; let startMinimizedField: HTMLInputElement | null; @@ -16,11 +19,11 @@ let securityButtonField: HTMLInputElement | null, forwardButtonField: HTMLInputElement | null, refreshButtonField: HTMLInputElement | null; -ipcRenderer.on('current-version', (e, version) => { +ipcRenderer.on('current-version', (e, version: SemVer) => { if (currentVersion) currentVersion.innerText = `Version: ${version.version}`; }); -ipcRenderer.on('config', (e, c) => { +ipcRenderer.on('config', (e, c: Config) => { config = c; if (startMinimizedField) startMinimizedField.checked = config.startMinimized; @@ -33,7 +36,7 @@ ipcRenderer.on('config', (e, c) => { if (refreshButtonField) refreshButtonField.checked = config.refreshButton; }); -ipcRenderer.on('updateStatus', (e, available, version) => { +ipcRenderer.on('updateStatus', (e, available: boolean, version: UpdateInfo) => { console.log(available, version); updateInfo = version; if (available) { @@ -45,7 +48,7 @@ ipcRenderer.on('updateStatus', (e, available, version) => { }); function save() { - let form = document.querySelector('form'); + const form = document.querySelector('form'); if (!form) return; const formData = new FormData(form); @@ -94,4 +97,4 @@ document.addEventListener('DOMContentLoaded', () => { ipcRenderer.send('syncSettings'); ipcRenderer.send('checkForUpdates'); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 0bdd009..46baea6 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "scripts": { "clean": "(! test -d build || rm -r build) && (! test -d resources || rm -r resources)", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "compile": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && webpack --mode production", - "compile-dev": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && webpack --mode development", + "compile": "yarn compile-common && webpack --mode production", + "compile-dev": "yarn compile-common && webpack --mode development", + "compile-common": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && rm -r resources/js/src && mv resources/js/frontend/ts/* resources/js/ && rm -r resources/js/frontend", "start": "yarn compile && electron .", "dev": "yarn compile-dev && concurrently -k -n \"Electron,Webpack,TSC-Frontend\" -p \"[{name}]\" -c \"green,yellow\" \"electron . --dev\" \"webpack --mode development --watch\" \"tsc -p tsconfig.frontend.json --watch\"", "build": "yarn compile && electron-builder -wl", diff --git a/src/Application.ts b/src/Application.ts index e7591d1..0104b96 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -12,7 +12,7 @@ export default class Application { private readonly mainWindow: MainWindow; private tray?: Tray; - constructor(devMode: boolean) { + public constructor(devMode: boolean) { this.devMode = devMode; this.config = new Config(); this.updater = new Updater(this.config, this); @@ -68,11 +68,12 @@ export default class Application { // Disable unused features app.on('web-contents-created', (e, contents) => { - contents.on('will-attach-webview', (e, webPreferences, params) => { + contents.on('will-attach-webview', (e, webPreferences) => { delete webPreferences.preload; webPreferences.nodeIntegration = false; - // TODO: Here would be a good place to filter accessed urls (params.src). Also consider 'will-navigate' event on contents. + // TODO: Here would be a good place to filter accessed urls (params.src). + // Also consider 'will-navigate' event on contents. }); }); } @@ -85,8 +86,8 @@ export default class Application { {label: 'Tabs', enabled: false}, {label: 'Open Tabs', click: () => this.mainWindow.getWindow().show()}, {type: 'separator'}, - {label: 'Quit', role: 'quit'} + {label: 'Quit', role: 'quit'}, ])); this.tray.on('click', () => this.mainWindow.toggle()); } -} \ No newline at end of file +} diff --git a/src/Config.ts b/src/Config.ts index b776cc3..0a63570 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -25,9 +25,11 @@ export default class Config { private properties: string[] = []; - constructor() { + [p: string]: unknown; + + public constructor() { // Load data from config file - let data: any = {}; + let data: Record = {}; if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) { if (fs.existsSync(configFile) && fs.statSync(configFile).isFile()) data = JSON.parse(fs.readFileSync(configFile, 'utf8')); @@ -36,7 +38,7 @@ export default class Config { } // Parse services - if (typeof data.services === 'object') { + if (typeof data.services === 'object' && Array.isArray(data.services)) { let i = 0; for (const service of data.services) { this.services[i] = new Service(service); @@ -45,11 +47,18 @@ export default class Config { } if (this.services.length === 0) { - this.services.push(new Service('welcome', 'Welcome', 'rocket', false, 'https://github.com/ArisuOngaku/tabs', false)); + this.services.push(new Service( + 'welcome', + 'Welcome', + 'rocket', + false, + 'https://github.com/ArisuOngaku/tabs', + false, + )); } this.defineProperty('updateCheckSkip', data); - + this.defineProperty('startMinimized', data); this.defineProperty('bigNavBar', data); @@ -63,26 +72,25 @@ export default class Config { this.save(); } - save() { + public save(): void { console.log('Saving config'); - this.services = this.services.filter(s => s !== null); fs.writeFileSync(configFile, JSON.stringify(this, null, 4)); console.log('> Config saved to', configFile.toString()); } - defineProperty(name: string, data: any) { + public defineProperty(name: string, data: Record): void { if (data[name] !== undefined) { - (this)[name] = data[name]; + this[name] = data[name]; } this.properties.push(name); } - update(data: any) { + public update(data: Record): void { for (const prop of this.properties) { if (data[prop] !== undefined) { - (this)[prop] = data[prop]; + this[prop] = data[prop]; } } } -} \ No newline at end of file +} diff --git a/src/Meta.ts b/src/Meta.ts index 2eb8dba..ad34193 100644 --- a/src/Meta.ts +++ b/src/Meta.ts @@ -13,7 +13,7 @@ export default class Meta { public static readonly BRAND_ICONS = Meta.listIcons('brands'); public static readonly SOLID_ICONS = Meta.listIcons('solid'); public static readonly REGULAR_ICONS = Meta.listIcons('regular'); - public static readonly ICON_SETS = [ + public static readonly ICON_SETS: IconSet[] = [ Meta.BRAND_ICONS, Meta.SOLID_ICONS, Meta.REGULAR_ICONS, @@ -21,7 +21,7 @@ export default class Meta { private static devMode?: boolean; - public static isDevMode() { + public static isDevMode(): boolean { if (this.devMode === undefined) { this.devMode = process.argv.length > 2 && process.argv[2] === '--dev'; console.debug('Dev mode:', this.devMode); @@ -30,7 +30,7 @@ export default class Meta { return this.devMode; } - public static getTitleForService(service: Service, viewTitle: string) { + public static getTitleForService(service: Service, viewTitle: string): string { let suffix = ''; if (viewTitle.length > 0) { suffix = ' - ' + viewTitle; @@ -38,7 +38,7 @@ export default class Meta { return this.title + ' - ' + service.name + suffix; } - private static listIcons(set: string) { + private static listIcons(set: string): IconSet { console.log('Loading icon set', set); const directory = path.resolve(Meta.RESOURCES_PATH, `images/icons/${set}`); const icons: { name: string; faIcon: string; set: string; }[] = []; @@ -51,3 +51,17 @@ export default class Meta { return icons; } } + +export type SpecialPages = { + empty: string; + connectionError: string; + fileNotFound: string; +}; + +export type IconProperties = { + name: string; + faIcon: string; + set: string; +}; + +export type IconSet = IconProperties[]; diff --git a/src/Service.ts b/src/Service.ts index 04d6204..1b47742 100644 --- a/src/Service.ts +++ b/src/Service.ts @@ -1,31 +1,77 @@ export default class Service { - public partition?: string; - public name?: string; + public name: string; + public partition: string; + public url: string; + public icon?: string; - public isImage?: boolean = false; - public url?: string; - public useFavicon?: boolean = true; + public isImage: boolean = false; + + public useFavicon: boolean = true; public favicon?: string; - public autoLoad?: boolean = false; + + public autoLoad: boolean = false; public customCSS?: string; public customUserAgent?: string; - public permissions?: {} = {}; + public permissions: ServicePermissions = {}; - 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]; + [p: string]: unknown; + + public constructor( + partition: string | Pick, + name?: string, + icon?: string, + isImage?: boolean, + url?: string, + useFavicon?: boolean, + ) { + const data = arguments.length === 1 ? partition as Record : { + name, + icon, + isImage, + url, + useFavicon, + }; + + if (typeof data.name === 'string') this.name = data.name; + else throw new Error('A service must have a name'); + + if (typeof data.partition === 'string') this.partition = data.partition; + else this.partition = this.name; + + if (typeof data.url === 'string') this.url = data.url; + else throw new Error('A service must have a url.'); + + if (typeof data.icon === 'string') this.icon = data.icon; + if (typeof data.isImage === 'boolean') this.isImage = data.isImage; + + if (typeof data.useFavicon === 'boolean') this.useFavicon = data.useFavicon; + if (typeof data.favicon === 'string') this.favicon = data.favicon; + + if (typeof data.autoLoad === 'boolean') this.autoLoad = data.autoLoad; + if (typeof data.customCSS === 'string') this.customCSS = data.customCSS; + if (typeof data.customUserAgent === 'string') this.customUserAgent = data.customUserAgent; + if (typeof data.permissions === 'object' && data.permissions !== null) { + for (const domain of Object.keys(data.permissions)) { + this.permissions[domain] = []; + + const permissions = (data.permissions as Record)[domain]; + + if (Array.isArray(permissions)) { + for (const permission of permissions) { + if (typeof permission.name === 'string' && + typeof permission.authorized === 'boolean') { + this.permissions[domain]?.push(permission); + } + } } } - } else { - this.partition = partition; - this.name = name; - this.icon = icon; - this.isImage = isImage; - this.url = url; - this.useFavicon = useFavicon; } } } + + +export type ServicePermission = { + name: string; + authorized: boolean; +}; +export type ServicePermissions = Record; diff --git a/src/Updater.ts b/src/Updater.ts index 9491be1..d9230c1 100644 --- a/src/Updater.ts +++ b/src/Updater.ts @@ -1,8 +1,9 @@ import {autoUpdater, UpdateInfo} from "electron-updater"; -import {dialog, shell} from "electron"; +import {dialog} from "electron"; import Config from "./Config"; -import BrowserWindow = Electron.BrowserWindow; import Application from "./Application"; +import {SemVer} from "semver"; +import BrowserWindow = Electron.BrowserWindow; export default class Updater { private readonly config: Config; @@ -36,7 +37,7 @@ export default class Updater { } } - public getCurrentVersion() { + public getCurrentVersion(): SemVer { return autoUpdater.currentVersion; } @@ -54,7 +55,7 @@ export default class Updater { checkboxLabel: `Don't remind me for this version`, cancelId: 0, defaultId: 1, - type: 'question' + type: 'question', }); if (input.checkboxChecked) { diff --git a/src/Window.ts b/src/Window.ts index 44e721c..36603f8 100644 --- a/src/Window.ts +++ b/src/Window.ts @@ -3,7 +3,9 @@ import Application from "./Application"; import Config from "./Config"; export default abstract class Window { - private readonly listeners: { [channel: string]: ((event: IpcMainEvent, ...args: any[]) => void)[] } = {}; + private readonly listeners: { + [channel: string]: ((event: IpcMainEvent, ...args: unknown[]) => void)[] | undefined + } = {}; private readonly onCloseListeners: (() => void)[] = []; protected readonly application: Application; @@ -18,7 +20,7 @@ export default abstract class Window { } - public setup(options: BrowserWindowConstructorOptions) { + public setup(options: BrowserWindowConstructorOptions): void { console.log('Creating window', this.constructor.name); if (this.parent) { @@ -32,7 +34,7 @@ export default abstract class Window { }); } - public teardown() { + public teardown(): void { console.log('Tearing down window', this.constructor.name); for (const listener of this.onCloseListeners) { @@ -40,7 +42,10 @@ export default abstract class Window { } for (const channel in this.listeners) { - for (const listener of this.listeners[channel]) { + const listeners = this.listeners[channel]; + if (!listeners) continue; + + for (const listener of listeners) { ipcMain.removeListener(channel, listener); } } @@ -48,18 +53,20 @@ export default abstract class Window { this.window = undefined; } + // This is the spec of ipcMain.on() + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected onIpc(channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): this { ipcMain.on(channel, listener); if (!this.listeners[channel]) this.listeners[channel] = []; - this.listeners[channel].push(listener); + this.listeners[channel]?.push(listener); return this; } - public onClose(listener: () => void) { + public onClose(listener: () => void): void { this.onCloseListeners.push(listener); } - public toggle() { + public toggle(): void { if (this.window) { if (!this.window.isFocused()) { console.log('Showing window', this.constructor.name); @@ -75,4 +82,4 @@ export default abstract class Window { if (!this.window) throw Error('Window not initialized.'); return this.window; } -} \ No newline at end of file +} diff --git a/src/types/single-instance.d.ts b/src/types/single-instance.d.ts index 646177b..be94c6b 100644 --- a/src/types/single-instance.d.ts +++ b/src/types/single-instance.d.ts @@ -1,7 +1,7 @@ declare module "single-instance" { export default class SingleInstance { - constructor(lockName: string); + public constructor(lockName: string); public lock(): Promise; } -} \ No newline at end of file +} diff --git a/src/windows/MainWindow.ts b/src/windows/MainWindow.ts index 885c144..284ab66 100644 --- a/src/windows/MainWindow.ts +++ b/src/windows/MainWindow.ts @@ -3,19 +3,20 @@ import {ipcMain} from "electron"; import ServiceSettingsWindow from "./ServiceSettingsWindow"; import SettingsWindow from "./SettingsWindow"; import Application from "../Application"; -import Meta from "../Meta"; +import Meta, {SpecialPages} from "../Meta"; import Window from "../Window"; +import {ServicePermissions} from "../Service"; export default class MainWindow extends Window { - private activeService: number = 0; + private activeServiceId: number = 0; private serviceSettingsWindow?: ServiceSettingsWindow; private settingsWindow?: SettingsWindow; - constructor(application: Application) { + public constructor(application: Application) { super(application); } - public setup() { + public setup(): void { super.setup({ webPreferences: { nodeIntegration: true, @@ -36,7 +37,7 @@ export default class MainWindow extends Window { if (this.application.isDevMode()) { window.webContents.openDevTools({ - mode: 'right' + mode: 'right', }); } @@ -46,19 +47,19 @@ export default class MainWindow extends Window { }); // Load active service - this.onIpc('setActiveService', (event, index) => { + this.onIpc('setActiveService', (event, index: number) => { this.setActiveService(index); }); // Set a service's favicon - this.onIpc('setServiceFavicon', (event, index, favicon) => { + this.onIpc('setServiceFavicon', (event, index: number, favicon?: string) => { console.log('Setting service', index, 'favicon', favicon); this.config.services[index].favicon = favicon; this.config.save(); }); // Reorder services - this.onIpc('reorderService', (event, serviceId, targetId) => { + this.onIpc('reorderService', (event, serviceId: number, targetId: number) => { console.log('Reordering services', serviceId, targetId); const oldServices = this.config.services; @@ -81,32 +82,32 @@ export default class MainWindow extends Window { }); // Delete service - this.onIpc('deleteService', (e, id) => { + this.onIpc('deleteService', (e, id: number) => { console.log('Deleting service', id); - delete this.config.services[id]; + this.config.services.splice(id, 1); this.config.save(); window.webContents.send('deleteService', id); }); // Update service permissions - ipcMain.on('updateServicePermissions', (e, serviceId, permissions) => { + ipcMain.on('updateServicePermissions', (e, serviceId: number, permissions: ServicePermissions) => { this.config.services[serviceId].permissions = permissions; this.config.save(); }); // Update window title - ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => { + ipcMain.on('updateWindowTitle', (event, serviceId: number | null, viewTitle?: string) => { if (serviceId === null) { window.setTitle(Meta.title); - } else { + } else if (viewTitle) { const service = this.config.services[serviceId]; window.setTitle(Meta.getTitleForService(service, viewTitle)); } }); // Open service settings window - ipcMain.on('openServiceSettings', (e, serviceId) => { + ipcMain.on('openServiceSettings', (e, serviceId: number | null) => { if (!this.serviceSettingsWindow) { console.log('Opening service settings', serviceId); this.serviceSettingsWindow = new ServiceSettingsWindow(this.application, this, serviceId); @@ -141,22 +142,22 @@ export default class MainWindow extends Window { .catch(console.error); } - public syncData() { + public syncData(): void { this.getWindow().webContents.send('data', Meta.title, Meta.ICON_SETS, - this.activeService, - { + this.activeServiceId, + { empty: path.resolve(Meta.RESOURCES_PATH, 'empty.html'), connectionError: path.resolve(Meta.RESOURCES_PATH, 'connection_error.html'), fileNotFound: path.resolve(Meta.RESOURCES_PATH, 'file_not_found_error.html'), }, - this.config + this.config, ); } private setActiveService(index: number) { console.log('Set active service', index); - this.activeService = index; + this.activeServiceId = index; } -} \ No newline at end of file +} diff --git a/src/windows/ServiceSettingsWindow.ts b/src/windows/ServiceSettingsWindow.ts index ff6679f..8d95dc4 100644 --- a/src/windows/ServiceSettingsWindow.ts +++ b/src/windows/ServiceSettingsWindow.ts @@ -5,14 +5,14 @@ import Meta from "../Meta"; import Service from "../Service"; export default class ServiceSettingsWindow extends Window { - private readonly serviceId: number; + private readonly serviceId: number | null; - constructor(application: Application, parent: Window, serviceId: number) { + public constructor(application: Application, parent: Window, serviceId: number | null) { super(application, parent); this.serviceId = serviceId; } - public setup() { + public setup(): void { super.setup({ webPreferences: { nodeIntegration: true, @@ -28,16 +28,20 @@ export default class ServiceSettingsWindow extends Window { if (this.application.isDevMode()) { window.webContents.openDevTools({ - mode: 'right' + mode: 'right', }); } this.onIpc('sync-settings', () => { window.webContents.send('syncIcons', Meta.ICON_SETS); - window.webContents.send('loadService', this.serviceId, this.config.services[this.serviceId]); + window.webContents.send('loadService', + this.serviceId, typeof this.serviceId === 'number' ? + this.config.services[this.serviceId] : + undefined, + ); }); - this.onIpc('saveService', (e, id, data) => { + this.onIpc('saveService', (e, id: number | null, data: Service) => { console.log('Saving service', id, data); const newService = new Service(data); if (typeof id === 'number') { @@ -45,6 +49,8 @@ export default class ServiceSettingsWindow extends Window { } else { this.config.services.push(newService); id = this.config.services.indexOf(newService); + + if (id < 0) id = null; } this.config.save(); @@ -55,4 +61,4 @@ export default class ServiceSettingsWindow extends Window { .catch(console.error); } -} \ No newline at end of file +} diff --git a/src/windows/SettingsWindow.ts b/src/windows/SettingsWindow.ts index d274003..ffe14b8 100644 --- a/src/windows/SettingsWindow.ts +++ b/src/windows/SettingsWindow.ts @@ -3,9 +3,10 @@ import path from "path"; import Window from "../Window"; import MainWindow from "./MainWindow"; import Meta from "../Meta"; +import Config from "../Config"; export default class SettingsWindow extends Window { - public setup() { + public setup(): void { super.setup({ webPreferences: { nodeIntegration: true, @@ -21,7 +22,7 @@ export default class SettingsWindow extends Window { if (this.application.isDevMode()) { window.webContents.openDevTools({ - mode: 'right' + mode: 'right', }); } @@ -36,7 +37,7 @@ export default class SettingsWindow extends Window { }).catch(console.error); }); - this.onIpc('save-config', (e: Event, data: any) => { + this.onIpc('save-config', (e: Event, data: Config) => { this.config.update(data); this.config.save(); if (this.parent instanceof MainWindow) { @@ -47,4 +48,4 @@ export default class SettingsWindow extends Window { window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'settings.html')) .catch(console.error); } -} \ No newline at end of file +} diff --git a/webpack.config.js b/webpack.config.js index 8c4b7cf..6db54ef 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -97,4 +97,4 @@ const config = { ] }; -module.exports = config; \ No newline at end of file +module.exports = config;