diff --git a/resources/index.html b/resources/index.html index c173ca5..4e74d31 100644 --- a/resources/index.html +++ b/resources/index.html @@ -7,8 +7,10 @@ - - + + + + @@ -28,343 +30,6 @@
- - \ No newline at end of file diff --git a/resources/js/index.js b/resources/js/index.js new file mode 100644 index 0000000..a05384e --- /dev/null +++ b/resources/js/index.js @@ -0,0 +1,334 @@ +const { + remote, + ipcRenderer, +} = require('electron'); +const { + Menu, + MenuItem, + dialog, +} = remote; + +const icons = []; + +let services = []; +let selectedService = null; +let forwardButton; +let backButton; +let addButton; + + +// Service context menu +const serviceContextMenu = new Menu(); +serviceContextMenu.append(new MenuItem({ + label: 'Reload', click: () => { + reloadService(serviceContextMenu.serviceId); + } +})); +serviceContextMenu.append(new MenuItem({ + label: 'Close', click: () => { + unloadService(serviceContextMenu.serviceId); + } +})); +serviceContextMenu.append(new MenuItem({type: "separator"})); +serviceContextMenu.append(new MenuItem({ + label: 'Edit', click: () => { + ipcRenderer.send('openServiceSettings', serviceContextMenu.serviceId); + } +})); +serviceContextMenu.append(new MenuItem({ + label: 'Delete', click: () => { + dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'question', + title: 'Confirm', + message: 'Are you sure you want to delete this service?', + buttons: ['Cancel', 'Confirm'], + cancelId: 0, + }).then(result => { + if (result.response === 1) { + ipcRenderer.send('deleteService', serviceContextMenu.serviceId); + } + }).catch(console.error); + } +})); + +function openServiceContextMenu(event, index) { + event.preventDefault(); + serviceContextMenu.serviceId = index; + serviceContextMenu.popup({window: remote.getCurrentWindow()}); +} + + +ipcRenderer.on('data', (event, brandIcons, solidIcons, actualServices, actualSelectedService) => { + for (const icon of brandIcons) { + icons.push(icon); + } + for (const icon of solidIcons) { + icons.push(icon); + } + + console.log('Updating services ...'); + services = actualServices; + + const nav = document.querySelector('#service-selector'); + while (nav.children.length > 0) { + nav.removeChild(nav.children[0]); + } + + const serviceContainer = document.querySelector('#services'); + serviceContainer.querySelectorAll(":scope > webview").forEach(w => serviceContainer.removeChild(w)); + + for (let i = 0; i < services.length; i++) { + createService(i); + } + + if (actualSelectedService < 0 || actualSelectedService >= services.length) { + actualSelectedService = 0; + } + setActiveService(actualSelectedService); +}); + +ipcRenderer.on('updateService', (e, id, data) => { + if (id === null) { + services.push(data); + createService(services.length - 1); + } else { + const nav = document.querySelector('#service-selector'); + + // Remove nav + const oldNavButton = nav.querySelector('li:nth-of-type(' + (id + 1) + ')'); + const nextNavButton = oldNavButton.nextSibling; + nav.removeChild(oldNavButton); + + // Remove webview + if (services[id].view) { + const serviceContainer = document.querySelector('#services'); + serviceContainer.removeChild(services[id].view); + } + + // Create new service + services[id] = data; + createService(id, nextNavButton); + if (parseInt(selectedService) === id) { + setActiveService(id); + } + } +}); + +ipcRenderer.on('deleteService', (e, id) => { + const nav = document.querySelector('#service-selector'); + + // Remove nav + const navButton = nav.querySelector('li:nth-of-type(' + (id + 1) + ')'); + if (navButton) { + nav.removeChild(navButton); + } + + // Remove webview + if (services[id].view) { + const serviceContainer = document.querySelector('#services'); + serviceContainer.removeChild(services[id].view); + } + + if (parseInt(selectedService) === id) { + setActiveService(0); + } + + delete services[id]; + services = services.filter(s => s !== null); +}); + +function createService(index, nextNavButton) { + let service = services[index]; + let li = document.createElement('li'); + service.li = li; + + let button = document.createElement('button'); + button.dataset.serviceId = '' + index; + button.dataset.tooltip = service.name; + button.addEventListener('click', () => { + setActiveService(button.dataset.serviceId); + ipcRenderer.send('setActiveService', button.dataset.serviceId); + }); + button.addEventListener('contextmenu', e => openServiceContextMenu(e, index)); + + let icon; + if (service.useFavicon && service.favicon != null) { + icon = document.createElement('img'); + icon.src = service.favicon; + icon.alt = service.name; + } else if (service.isImage) { + icon = document.createElement('img'); + icon.src = service.icon; + icon.alt = service.name; + } else { + icon = document.createElement('i'); + const iconProperties = icons.find(i => i.name === service.icon); + if (iconProperties) { + iconProperties.faIcon.split(' ').forEach(cl => { + icon.classList.add(cl); + }); + } + } + + button.appendChild(icon); + li.appendChild(button); + li.button = button; + + const nav = document.querySelector('#service-selector'); + if (nextNavButton === nav || nextNavButton === undefined) { + nav.appendChild(li); + } else { + nav.insertBefore(li, nextNavButton); + } + + if (service.autoLoad) { + loadService(index, service); + } +} + +document.addEventListener('DOMContentLoaded', () => { + forwardButton = document.querySelector('#forward'); + forwardButton.addEventListener('click', () => goForward()); + + backButton = document.querySelector('#back'); + backButton.addEventListener('click', () => goBack()); + + addButton = document.querySelector('#add-button'); + addButton.addEventListener('click', () => ipcRenderer.send('openServiceSettings', null)); +}); + +function setActiveService(serviceId) { + const currentService = services[serviceId]; + process.nextTick(() => { + loadService(serviceId, currentService); + + // Hide previous service + if (services[selectedService] && services[selectedService].view) { + services[selectedService].view.classList.remove('active'); + } + + // Show service + currentService.view.classList.add('active'); + + // Save active service ID + selectedService = serviceId; + + // Refresh navigation + updateNavigation(); + }); +} + +function loadService(serviceId, service) { + // Load service if not loaded yet + if (!service.view && !service.viewReady) { + document.querySelector('#services > .loader').classList.remove('hidden'); + service.view = document.createElement('webview'); + service.view.setAttribute('src', service.url); + service.view.setAttribute('partition', 'persist:service_' + service.partition); + service.view.setAttribute('autosize', "true"); + + // Append element to DOM + document.querySelector('#services').appendChild(service.view); + + // On load event + service.view.addEventListener('dom-ready', () => { + if (service.customCSS) { + service.view.insertCSS(service.customCSS); + } + + document.querySelector('#services > .loader').classList.add('hidden'); + updateNavigation(); + service.li.classList.add('loaded'); + service.viewReady = true; + + if (selectedService === null) { + setActiveService(serviceId); + } + }); + + // Load favicon + service.view.addEventListener('page-favicon-updated', event => { + console.debug('Loaded favicons for', service.name, event.favicons); + if (event.favicons.length > 0) { + ipcRenderer.send('setServiceFavicon', serviceId, event.favicons[0]); + if (service.useFavicon) { + const img = document.createElement('img'); + img.src = event.favicons[0]; + img.alt = service.name; + img.onload = () => { + service.li.button.innerHTML = ''; + service.li.button.appendChild(img); + }; + } + } + }); + } +} + +function unloadService(serviceId) { + const service = services[serviceId]; + if (service.view && service.viewReady) { + service.view.remove(); + service.view = null; + service.li.classList.remove('loaded'); + service.viewReady = false; + + if (parseInt(selectedService) === serviceId) { + selectedService = null; + for (let i = 0; i < services.length; i++) { + if (services[i].view && services[i].viewReady) { + setActiveService(i); + break; + } + } + if (selectedService === null) { + updateNavigation(); + } + } + } +} + +function reloadService(serviceId) { + const service = services[serviceId]; + if (service.view && service.viewReady) { + document.querySelector('#services > .loader').classList.remove('hidden'); + service.view.reload(); + } else if (!service.view && !service.viewReady) { + loadService(serviceId, service); + } +} + +function updateNavigation() { + console.debug('Updating navigation'); + // Update active list element + for (let i = 0; i < services.length; i++) { + const service = services[i]; + if (parseInt(selectedService) === i) { + service.li.classList.add('active'); + } else { + service.li.classList.remove('active'); + } + } + + if (selectedService !== null && services[selectedService].viewReady) { + console.debug('Updating navigation buttons because view is ready'); + // Update history navigation + let view = services[selectedService].view; + + if (view && view.canGoForward()) forwardButton.classList.remove('disabled'); + else forwardButton.classList.add('disabled'); + + if (view && view.canGoBack()) backButton.classList.remove('disabled'); + else backButton.classList.add('disabled'); + } +} + +function goForward() { + let view = services[selectedService].view; + if (view) view.getWebContents().goForward(); +} + +function goBack() { + let view = services[selectedService].view; + if (view) view.getWebContents().goBack(); +} \ No newline at end of file diff --git a/resources/js/service-settings.js b/resources/js/service-settings.js new file mode 100644 index 0000000..bbccc27 --- /dev/null +++ b/resources/js/service-settings.js @@ -0,0 +1,178 @@ +const {ipcRenderer, remote} = require('electron'); +let isImageCheckbox; +let builtInIconSearchField; +let iconSelect; +let iconUrlField; + +let serviceId; +let service; + +ipcRenderer.on('syncIcons', (event, brands, solid) => { + loadIcons(brands, 'brands'); + loadIcons(solid, 'solid'); +}); + +ipcRenderer.on('loadService', (e, id, data) => { + console.log('Load service', id); + if (id === null) { + document.title = 'Add a new service'; + service = {}; + + document.querySelector('h1').innerText = 'Add a new service'; + } else { + serviceId = id; + service = data; + document.querySelector('h1').innerText = 'Service settings'; + loadServiceValues(); + } +}); + +document.addEventListener('DOMContentLoaded', () => { + isImageCheckbox = document.querySelector('#is-image'); + builtInIconSearchField = document.querySelector('#built-in-icon-search'); + iconSelect = document.querySelector('#icon-select'); + iconUrlField = document.querySelector('#icon-url'); + + + isImageCheckbox.addEventListener('click', () => { + updateIconChoiceForm(isImageCheckbox.checked); + }); + updateIconChoiceForm(isImageCheckbox.checked); + + builtInIconSearchField.addEventListener('input', updateIconSearchResults); + + document.getElementById('cancel-button').addEventListener('click', e => { + e.preventDefault(); + remote.getCurrentWindow().close(); + }); + + ipcRenderer.send('sync-settings'); +}); + +function updateIconSearchResults() { + const searchStr = builtInIconSearchField.value; + iconSelect.childNodes.forEach(c => { + if (c.dataset.icon.match(searchStr) || searchStr.match(c.dataset.icon)) { + c.classList.remove('hidden'); + } else { + c.classList.add('hidden'); + } + }); +} + +function loadIcons(icons, set) { + for (const icon of icons) { + if (icon.name.length === 0) continue; + const choice = document.createElement('label'); + choice.dataset.icon = icon.name; + choice.classList.add('choice'); + + const display = document.createElement('img'); + display.src = 'icons/' + set + '/' + icon.name + '.svg'; + choice.appendChild(display); + + const label = document.createElement('span'); + label.innerText = icon.name; + choice.appendChild(label); + + const radio = document.createElement('input'); + radio.setAttribute('type', 'radio'); + radio.setAttribute('name', 'icon'); + radio.setAttribute('value', icon.name); + choice.appendChild(radio); + + iconSelect.appendChild(choice); + + choice.addEventListener('click', () => { + selectIcon(choice); + }); + } +} + +function selectIcon(choice) { + builtInIconSearchField.value = choice.dataset.icon; + for (const otherChoice of iconSelect.children) { + otherChoice.classList.remove('selected'); + } + choice.classList.add('selected'); + choice.querySelector('input[type=radio]').checked = true; +} + +function updateIconChoiceForm(isUrl) { + if (isUrl) { + iconSelect.classList.add('hidden'); + builtInIconSearchField.parentElement.classList.add('hidden'); + iconUrlField.parentElement.classList.remove('hidden'); + } else { + iconSelect.classList.remove('hidden'); + builtInIconSearchField.parentElement.classList.remove('hidden'); + iconUrlField.parentElement.classList.add('hidden'); + } +} + +function loadServiceValues() { + if (!service || !isImageCheckbox) { + return; + } + + document.getElementById('name').value = service.name; + document.getElementById('url').value = service.url; + document.getElementById('use-favicon').checked = service.useFavicon; + document.getElementById('auto-load').checked = service.autoLoad; + document.getElementById('custom-css').value = service.customCSS; + + isImageCheckbox.checked = service.isImage; + if (service.isImage) { + iconUrlField.value = service.icon; + } else { + builtInIconSearchField.value = service.icon; + updateIconSearchResults(); + const icon = Array.from(iconSelect.querySelectorAll('label')).find(i => i.dataset.icon === service.icon); + if (icon) { + selectIcon(icon); + } + } +} + +function save() { + const formData = new FormData(document.querySelector('form')); + service.name = formData.get('name'); + if (typeof service.partition !== 'string' || 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.isImage = formData.get('isImage') === 'on'; + service.icon = formData.get('icon'); + service.useFavicon = formData.get('useFavicon') === 'on'; + service.autoLoad = formData.get('autoLoad') === 'on'; + service.customCSS = formData.get('customCSS'); + + + if (!isValid()) { + return; + } + + ipcRenderer.send('saveService', serviceId, service); + remote.getCurrentWindow().close(); +} + +function isValid() { + if (typeof service.name !== 'string' || service.name.length === 0) { + console.log('Invalid name'); + return false; + } + if (typeof service.partition !== 'string' || service.partition.length === 0) { + console.log('Invalid partition'); + return false; + } + if (typeof service.url !== 'string' || service.url.length === 0) { + console.log('Invalid url'); + return false; + } + if (!(service.useFavicon || typeof service.icon === 'string' && service.icon.length > 0)) { + console.log('Invalid icon'); + return false; + } + return true; +} \ No newline at end of file diff --git a/resources/service-settings.html b/resources/service-settings.html index b0cee2a..7bfee0a 100644 --- a/resources/service-settings.html +++ b/resources/service-settings.html @@ -7,8 +7,10 @@ - - + + + + @@ -73,186 +75,5 @@ - - \ No newline at end of file diff --git a/resources/index.css b/resources/style/index.css similarity index 100% rename from resources/index.css rename to resources/style/index.css diff --git a/resources/layout.css b/resources/style/layout.css similarity index 100% rename from resources/layout.css rename to resources/style/layout.css diff --git a/resources/service-settings.css b/resources/style/service-settings.css similarity index 100% rename from resources/service-settings.css rename to resources/style/service-settings.css