<template> <div style="display: flex;flex-direction:column;height:100%" :class="getThemeClass()" @auxclick.prevent> <div v-html="styling"></div> <div style="display:flex;align-items:stretch;border-bottom-width:1px" class="border-bottom" id="window-tabs"> <h4 style="padding:2px 0">F-Chat</h4> <div class="btn" :class="'btn-' + (hasUpdate ? 'warning' : 'light')" @click="openMenu" id="settings"> <i class="fa fa-cog"></i> </div> <ul class="nav nav-tabs" style="border-bottom:0;margin-bottom:-1px;margin-top:1px" ref="tabs"> <li v-for="(tab,index) in tabs" :key="'tab-' + index" class="nav-item" @click.middle="remove(tab)"> <a href="#" @click.prevent="show(tab)" class="nav-link tab" :class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}"> <img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/> <span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span> <a href="#" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit;text-decoration:none" @click.stop="remove(tab)"><i class="fa fa-times"></i> </a> </a> </li> <li v-show="(canOpenTab && hasCompletedUpgrades)" class="addTab nav-item" id="addTab"> <a href="#" @click.prevent="addTab()" class="nav-link"><i class="fa fa-plus"></i></a> </li> </ul> <div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag" class="btn-group" id="windowButtons"> <i class="far fa-window-minimize btn btn-light" @click.stop="minimize()"></i> <i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize()"></i> <span class="btn btn-light" @click.stop="close()"> <i class="fa fa-times fa-lg"></i> </span> </div> </div> </div> </template> <script lang="ts"> import Sortable from 'sortablejs'; import _ from 'lodash'; import {Component, Hook} from '@f-list/vue-ts'; import * as electron from 'electron'; import * as remote from '@electron/remote'; import * as fs from 'fs'; import * as path from 'path'; import * as url from 'url'; import Vue from 'vue'; import l from '../chat/localize'; import {GeneralSettings} from './common'; import { getSafeLanguages, updateSupportedLanguages } from './language'; import log from 'electron-log'; const browserWindow = remote.getCurrentWindow(); // void browserWindow.webContents.setVisualZoomLevelLimits(1, 5); function getWindowBounds(): Electron.Rectangle { const bounds = browserWindow.getContentBounds(); const height = document.body.offsetHeight; return {x: 0, y: height, width: bounds.width, height: bounds.height - height}; } function destroyTab(tab: Tab): void { if(tab.user !== undefined) electron.ipcRenderer.send('disconnect', tab.user); tab.tray.destroy(); tab.view.webContents.stop(); tab.view.webContents.stopPainting(); try { if ((tab.view.webContents as any).destroy) { (tab.view.webContents as any).destroy(); } } catch (err) { console.log(err); } try { if ((tab.view.webContents as any).close) { (tab.view.webContents as any).close(); } } catch (err) { console.log(err); } try { if ((tab.view as any).destroy) { (tab.view as any).destroy(); } } catch (err) { console.log(err); } try { if ((tab.view as any).close) { (tab.view as any).close(); } } catch (err) { console.log(err); } // tab.view.destroy(); electron.ipcRenderer.send('tab-closed'); } interface Tab { user: string | undefined, view: Electron.BrowserView hasNew: boolean tray: Electron.Tray } // console.log(require('./build/tray.png').default); //tslint:disable-next-line:no-require-imports no-unsafe-any const trayIcon = path.join(__dirname, <string>require('./build/tray.png').default); //path.join(__dirname, <string>require('./build/tray.png').default); @Component export default class Window extends Vue { settings!: GeneralSettings; tabs: Tab[] = []; activeTab: Tab | undefined; tabMap: {[key: number]: Tab} = {}; isMaximized = false; canOpenTab = true; l = l; hasUpdate = false; platform = process.platform; lockTab = false; hasCompletedUpgrades = false; @Hook('mounted') async mounted(): Promise<void> { log.debug('init.window.mounting'); // top bar devtools // browserWindow.webContents.openDevTools({ mode: 'detach' }); if (remote.process.argv.includes('--devtools')) { browserWindow.webContents.openDevTools({ mode: 'detach' }); } updateSupportedLanguages(browserWindow.webContents.session.availableSpellCheckerLanguages); log.debug('init.window.languages.supported'); // console.log('MOUNT DICTIONARIES', getSafeLanguages(this.settings.spellcheckLang), this.settings.spellcheckLang); browserWindow.webContents.session.setSpellCheckerLanguages(getSafeLanguages(this.settings.spellcheckLang)); log.debug('init.window.languages'); electron.ipcRenderer.on('settings', (_e: Event, settings: GeneralSettings) => { log.debug('settings.update.window'); this.settings = settings; log.transports.file.level = settings.risingSystemLogLevel; log.transports.console.level = settings.risingSystemLogLevel; }); electron.ipcRenderer.on('rising-upgrade-complete', () => { // console.log('RISING COMPLETE RECV'); this.hasCompletedUpgrades = true; }); electron.ipcRenderer.on('allow-new-tabs', (_e: Event, allow: boolean) => this.canOpenTab = allow); electron.ipcRenderer.on('open-tab', () => this.addTab()); electron.ipcRenderer.on('update-available', (_e: Event, available: boolean) => this.hasUpdate = available); electron.ipcRenderer.on('fix-logs', () => this.activeTab!.view.webContents.send('fix-logs')); electron.ipcRenderer.on('quit', () => this.destroyAllTabs()); electron.ipcRenderer.on('reopen-profile', () => this.activeTab!.view.webContents.send('reopen-profile')); electron.ipcRenderer.on('update-dictionaries', (_e: Event, langs: string[]) => { // console.log('UPDATE DICTIONARIES', langs); browserWindow.webContents.session.setSpellCheckerLanguages(langs); for (const t of this.tabs) { t.view.webContents.session.setSpellCheckerLanguages(langs); } }); // electron.ipcRenderer.on('update-zoom', (_e: Event, zoomLevel: number) => { // // log.info('WINDOWVUE ZOOM UPDATE', zoomLevel); // // browserWindow.webContents.setZoomLevel(zoomLevel); // }); electron.ipcRenderer.on('connect', (_e: Event, id: number, name: string) => { const tab = this.tabMap[id]; tab.user = name; tab.tray.setToolTip(`${l('title')} - ${tab.user}`); const menu = this.createTrayMenu(tab); menu.unshift({label: tab.user, enabled: false}, {type: 'separator'}); tab.tray.setContextMenu(remote.Menu.buildFromTemplate(menu)); }); electron.ipcRenderer.on('disconnect', (_e: Event, id: number) => { const tab = this.tabMap[id]; if(tab.hasNew) { tab.hasNew = false; electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false)); } tab.user = undefined; tab.tray.setToolTip(l('title')); tab.tray.setContextMenu(remote.Menu.buildFromTemplate(this.createTrayMenu(tab))); }); electron.ipcRenderer.on('has-new', (_e: Event, id: number, hasNew: boolean) => { const tab = this.tabMap[id]; tab.hasNew = hasNew; electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false)); }); browserWindow.on('maximize', () => this.isMaximized = true); browserWindow.on('unmaximize', () => this.isMaximized = false); electron.ipcRenderer.on('switch-tab', (_e: Event) => { const index = this.tabs.indexOf(this.activeTab!); this.show(this.tabs[index + 1 === this.tabs.length ? 0 : index + 1]); }); electron.ipcRenderer.on('previous-tab', (_e: Event) => { const index = this.tabs.indexOf(this.activeTab!); this.show(this.tabs[index - 1 < 0 ? this.tabs.length - 1 : index - 1]); }); electron.ipcRenderer.on('show-tab', (_e: Event, id: number) => { this.show(this.tabMap[id]); }); document.addEventListener('click', () => this.activeTab!.view.webContents.focus()); window.addEventListener('focus', () => this.activeTab!.view.webContents.focus()); log.debug('init.window.listeners'); await this.addTab(); log.debug('init.window.tab'); // console.log('SORTABLE', Sortable); Sortable.create(<HTMLElement>this.$refs['tabs'], { animation: 50, onEnd: (e) => { // log.debug('ONEND', e); if(e.oldIndex === e.newIndex) return; // log.debug('PRE', this.tabs); // // const tab = this.tabs.splice(e.oldIndex!, 1)[0]; // this.tabs.splice(e.newIndex!, 0, tab); // // log.debug('POST', this.tabs); }, onMove: (e: {related: HTMLElement}) => e.related.id !== 'addTab', filter: '.addTab' }); window.onbeforeunload = () => { const isConnected = this.tabs.reduce((cur, tab) => cur || tab.user !== undefined, false); if(process.env.NODE_ENV !== 'production' || !isConnected) { this.destroyAllTabs(); return; } if(!this.settings.closeToTray) return setImmediate(() => { if(confirm(l('chat.confirmLeave'))) { this.destroyAllTabs(); browserWindow.close(); } }); browserWindow.hide(); return false; }; this.isMaximized = browserWindow.isMaximized(); log.debug('init.window.mounted'); } destroyAllTabs(): void { browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword this.tabs.forEach(destroyTab); this.tabs = []; } get styling(): string { try { return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.settings.theme}.css`), 'utf8').toString()}</style>`; } catch(e) { if((<Error & {code: string}>e).code === 'ENOENT' && this.settings.theme !== 'default') { this.settings.theme = 'default'; return this.styling; } throw e; } } trayClicked(tab: Tab): void { browserWindow.show(); if(this.isMaximized) browserWindow.maximize(); this.show(tab); } createTrayMenu(tab: Tab): Electron.MenuItemConstructorOptions[] { return [ {label: l('action.open'), click: () => this.trayClicked(tab)}, {label: l('action.quit'), click: () => this.remove(tab, false)} ]; } async addTab(): Promise<void> { if(this.lockTab) return; const tray = new remote.Tray(trayIcon); tray.setToolTip(l('title')); tray.on('click', (_e) => this.trayClicked(tab)); const view = new remote.BrowserView( { webPreferences: { webviewTag: true, nodeIntegration: true, nodeIntegrationInWorker: true, spellcheck: true, contextIsolation: false, partition: 'persist:fchat', } } ); const remoteMain = require("@electron/remote/main"); remoteMain.enable(view.webContents); // tab devtools // view.webContents.openDevTools(); if (remote.process.argv.includes('--devtools')) { view.webContents.openDevTools({ mode: 'detach' }); } // console.log('ADD TAB LANGUAGES', getSafeLanguages(this.settings.spellcheckLang), this.settings.spellcheckLang); view.webContents.session.setSpellCheckerLanguages(getSafeLanguages(this.settings.spellcheckLang)); view.setAutoResize({width: true, height: true}); electron.ipcRenderer.send('tab-added', view.webContents.id); const tab = {active: false, view, user: undefined, hasNew: false, tray}; tray.setContextMenu(remote.Menu.buildFromTemplate(this.createTrayMenu(tab))); this.tabs.push(tab); this.tabMap[view.webContents.id] = tab; this.show(tab); this.lockTab = true; log.debug('init.window.tab.load'); const indexUrl = url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true, query: {settings: JSON.stringify(this.settings), hasCompletedUpgrades: JSON.stringify(this.hasCompletedUpgrades)} }); await view.webContents.loadURL(indexUrl); log.debug('init.window.tab.load.complete', indexUrl); tab.view.setBounds(getWindowBounds()); this.lockTab = false; } show(tab: Tab): void { if(this.lockTab) return; this.activeTab = tab; browserWindow.setBrowserView(tab.view); tab.view.setBounds(getWindowBounds()); tab.view.webContents.focus(); // tab.view.webContents.send('active-tab', { webContentsId: tab.view.webContents.id }); _.each(this.tabs, (t) => t.view.webContents.send(t === tab ? 'active-tab' : 'inactive-tab')); // electron.ipcRenderer.send('active-tab', { webContentsId: tab.view.webContents.id }); } remove(tab: Tab, shouldConfirm: boolean = true): void { if(this.lockTab || shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return; this.tabs.splice(this.tabs.indexOf(tab), 1); electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false)); delete this.tabMap[tab.view.webContents.id]; if(this.tabs.length === 0) { browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword if(process.env.NODE_ENV === 'production') browserWindow.close(); } else if(this.activeTab === tab) this.show(this.tabs[0]); destroyTab(tab); } minimize(): void { browserWindow.minimize(); } maximize(): void { if(browserWindow.isMaximized()) browserWindow.unmaximize(); else browserWindow.maximize(); } close(): void { browserWindow.close(); } openMenu(): void { remote.Menu.getApplicationMenu()!.popup({}); } getThemeClass() { // console.log('getThemeClassWindow', this.settings?.risingDisableWindowsHighContrast); try { // Hack! if (process.platform === 'win32') { if (this.settings?.risingDisableWindowsHighContrast) { document.querySelector('html')?.classList.add('disableWindowsHighContrast'); } else { document.querySelector('html')?.classList.remove('disableWindowsHighContrast'); } } return { ['platform-' + this.platform]: true, disableWindowsHighContrast: this.settings?.risingDisableWindowsHighContrast || false }; } catch (err) { return { ['platform-' + this.platform]: true }; } } } </script> <style lang="scss"> #window-tabs { user-select: none; .btn { border: 0; border-radius: 0; padding: 0 18px; display: flex; align-items: center; line-height: 1; -webkit-app-region: no-drag; flex-grow: 0; } .btn-default { background: transparent; } li { height: 100%; a { display: flex; padding: 2px 10px; height: 100%; align-items: center; &:first-child { border-top-left-radius: 0; } } img { height: 28px; margin: -5px 3px -5px -5px; } } h4 { margin: 0 10px; user-select: none; cursor: default; align-self: center; -webkit-app-region: drag; } .fa { line-height: inherit; } } #windowButtons .btn { border-top: 0; font-size: 14px; } .platform-darwin { #windowButtons .btn, #settings { display: none; } #window-tabs { h4 { margin: 0 15px 0 77px; } .btn, li a { padding-top: 6px; padding-bottom: 6px; } } } .disableWindowsHighContrast, .disableWindowsHighContrast * { forced-color-adjust: none; } </style>