import { View } from 'framework/views/view'; import { Events } from 'framework/events'; import { IdleTracker } from 'comp/browser/idle-tracker'; import { KeyHandler } from 'comp/browser/key-handler'; import { Launcher } from 'comp/launcher'; import { SettingsManager } from 'comp/settings/settings-manager'; import { Alerts } from 'comp/ui/alerts'; import { Keys } from 'const/keys'; import { UpdateModel } from 'models/update-model'; import { Features } from 'util/features'; import { Locale } from 'util/locale'; import { Logger } from 'util/logger'; import { CsvParser } from 'util/data/csv-parser'; import { DetailsView } from 'views/details/details-view'; import { DragView } from 'views/drag-view'; import { DropdownView } from 'views/dropdown-view'; import { FooterView } from 'views/footer-view'; import { GeneratorPresetsView } from 'views/generator-presets-view'; import { GrpView } from 'views/grp-view'; import { KeyChangeView } from 'views/key-change-view'; import { ListView } from 'views/list-view'; import { ListWrapView } from 'views/list-wrap-view'; import { MenuView } from 'views/menu/menu-view'; import { OpenView } from 'views/open-view'; import { SettingsView } from 'views/settings/settings-view'; import { TagView } from 'views/tag-view'; import { ImportCsvView } from 'views/import-csv-view'; import template from 'templates/app.hbs'; class AppView extends View { parent = 'body'; template = template; events = { contextmenu: 'contextMenu', drop: 'drop', dragenter: 'dragover', dragover: 'dragover', 'click a[target=_blank]': 'extLinkClick', mousedown: 'bodyClick' }; titlebarStyle = 'default'; constructor(model) { super(model); this.views.menu = new MenuView(this.model.menu, { ownParent: true }); this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' }); this.views.footer = new FooterView(this.model, { ownParent: true }); this.views.listWrap = new ListWrapView(this.model, { ownParent: true }); this.views.list = new ListView(this.model, { ownParent: true }); this.views.listDrag = new DragView('x', { parent: '.app__list-drag' }); this.views.list.dragView = this.views.listDrag; this.views.details = new DetailsView(undefined, { ownParent: true }); this.views.details.appModel = this.model; this.views.menu.listenDrag(this.views.menuDrag); this.views.list.listenDrag(this.views.listDrag); this.titlebarStyle = this.model.settings.titlebarStyle; this.listenTo(this.model.settings, 'change:theme', this.setTheme); this.listenTo(this.model.settings, 'change:locale', this.setLocale); this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize); this.listenTo(this.model.settings, 'change:autoSaveInterval', this.setupAutoSave); this.listenTo(this.model.files, 'change', this.fileListUpdated); this.listenTo(Events, 'select-all', this.selectAll); this.listenTo(Events, 'menu-select', this.menuSelect); this.listenTo(Events, 'lock-workspace', this.lockWorkspace); this.listenTo(Events, 'show-file', this.showFileSettings); this.listenTo(Events, 'open-file', this.toggleOpenFile); this.listenTo(Events, 'save-all', this.saveAll); this.listenTo(Events, 'remote-key-changed', this.remoteKeyChanged); this.listenTo(Events, 'key-change-pending', this.keyChangePending); this.listenTo(Events, 'toggle-settings', this.toggleSettings); this.listenTo(Events, 'toggle-menu', this.toggleMenu); this.listenTo(Events, 'toggle-details', this.toggleDetails); this.listenTo(Events, 'edit-group', this.editGroup); this.listenTo(Events, 'edit-tag', this.editTag); this.listenTo(Events, 'edit-generator-presets', this.editGeneratorPresets); this.listenTo(Events, 'launcher-open-file', this.launcherOpenFile); this.listenTo(Events, 'user-idle', this.userIdle); this.listenTo(Events, 'os-lock', this.osLocked); this.listenTo(Events, 'power-monitor-suspend', this.osLocked); this.listenTo(Events, 'app-minimized', this.appMinimized); this.listenTo(Events, 'show-context-menu', this.showContextMenu); this.listenTo(Events, 'second-instance', this.showSingleInstanceAlert); this.listenTo(Events, 'enter-full-screen', this.enterFullScreen); this.listenTo(Events, 'leave-full-screen', this.leaveFullScreen); this.listenTo(Events, 'import-csv-requested', this.showImportCsv); this.listenTo(Events, 'launcher-before-quit', this.launcherBeforeQuit); this.listenTo(UpdateModel, 'change:updateReady', this.updateApp); window.onbeforeunload = this.beforeUnload.bind(this); window.onresize = this.windowResize.bind(this); window.onblur = this.windowBlur.bind(this); this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed); this.onKey(Keys.DOM_VK_BACK_SPACE, this.backspacePressed); if (Launcher && Launcher.devTools) { this.onKey( Keys.DOM_VK_I, this.openDevTools, KeyHandler.SHORTCUT_ACTION + KeyHandler.SHORTCUT_OPT, '*' ); } this.setWindowClass(); this.setupAutoSave(); } setWindowClass() { const getBrowserCssClass = Features.getBrowserCssClass(); if (getBrowserCssClass) { document.body.classList.add(getBrowserCssClass); } if (this.titlebarStyle !== 'default') { document.body.classList.add('titlebar-' + this.titlebarStyle); } if (Features.isMobile) { document.body.classList.add('mobile'); } } render() { super.render({ beta: this.model.isBeta, titlebarStyle: this.titlebarStyle }); this.panelEl = this.$el.find('.app__panel:first'); this.views.listWrap.render(); this.views.menu.render(); this.views.menuDrag.render(); this.views.footer.render(); this.views.list.render(); this.views.listDrag.render(); this.views.details.render(); this.showLastOpenFile(); } showOpenFile() { this.hideContextMenu(); this.views.menu.hide(); this.views.menuDrag.hide(); this.views.listWrap.hide(); this.views.list.hide(); this.views.listDrag.hide(); this.views.details.hide(); this.views.footer.toggle(this.model.files.hasOpenFiles()); this.hidePanelView(); this.hideSettings(); this.hideOpenFile(); this.hideKeyChange(); this.hideImportCsv(); this.views.open = new OpenView(this.model); this.views.open.render(); this.views.open.on('close', () => { this.showEntries(); }); this.views.open.on('remove', () => { Events.emit('closed-open-view'); }); } showLastOpenFile() { this.showOpenFile(); const lastOpenFile = this.model.fileInfos[0]; if (lastOpenFile) { this.views.open.currentSelectedIndex = 0; this.views.open.showOpenFileInfo(lastOpenFile); } } launcherOpenFile(file) { if (file && file.data && /\.kdbx$/i.test(file.data)) { this.showOpenFile(); this.views.open.showOpenLocalFile(file.data, file.key); } } updateApp() { if (UpdateModel.updateStatus === 'ready' && !Launcher && !this.model.files.hasOpenFiles()) { window.location.reload(); } } showEntries() { this.views.menu.show(); this.views.menuDrag.show(); this.views.listWrap.show(); this.views.list.show(); this.views.listDrag.show(); this.views.details.show(); this.views.footer.show(); this.hidePanelView(); this.hideOpenFile(); this.hideSettings(); this.hideKeyChange(); this.hideImportCsv(); } hideOpenFile() { if (this.views.open) { this.views.open.remove(); this.views.open = null; } } hidePanelView() { if (this.views.panel) { this.views.panel.remove(); this.views.panel = null; this.panelEl.addClass('hide'); } } showPanelView(view) { this.views.listWrap.hide(); this.views.list.hide(); this.views.listDrag.hide(); this.views.details.hide(); this.hidePanelView(); view.render(); this.views.panel = view; this.panelEl.removeClass('hide'); } hideSettings() { if (this.views.settings) { this.model.menu.setMenu('app'); this.views.settings.remove(); this.views.settings = null; } } hideKeyChange() { if (this.views.keyChange) { this.views.keyChange.hide(); this.views.keyChange = null; } } hideImportCsv() { if (this.views.importCsv) { this.views.importCsv.remove(); this.views.importCsv = null; } } showSettings(selectedMenuItem) { this.model.menu.setMenu('settings'); this.views.menu.show(); this.views.menuDrag.show(); this.views.listWrap.hide(); this.views.list.hide(); this.views.listDrag.hide(); this.views.details.hide(); this.hidePanelView(); this.hideOpenFile(); this.hideKeyChange(); this.hideImportCsv(); this.views.settings = new SettingsView(this.model); this.views.settings.render(); if (!selectedMenuItem) { selectedMenuItem = this.model.menu.generalSection.items[0]; } this.model.menu.select({ item: selectedMenuItem }); this.views.menu.switchVisibility(false); } showEditGroup(group) { this.showPanelView(new GrpView(group)); } showEditTag() { this.showPanelView(new TagView(this.model)); } showKeyChange(file, viewConfig) { if (Alerts.alertDisplayed) { return; } if (this.views.keyChange && this.views.keyChange.model.remote) { return; } this.hideSettings(); this.hidePanelView(); this.views.menu.hide(); this.views.listWrap.hide(); this.views.list.hide(); this.views.listDrag.hide(); this.views.details.hide(); this.views.keyChange = new KeyChangeView({ file, expired: viewConfig.expired, remote: viewConfig.remote }); this.views.keyChange.render(); this.views.keyChange.on('accept', this.keyChangeAccept.bind(this)); this.views.keyChange.on('cancel', this.showEntries.bind(this)); } fileListUpdated() { if (this.model.files.hasOpenFiles()) { this.showEntries(); } else { this.showOpenFile(); this.selectLastOpenFile(); } } showFileSettings(e) { const menuItem = this.model.menu.filesSection.items.find( (item) => item.file.id === e.fileId ); if (this.views.settings) { if (this.views.settings.file === menuItem.file) { this.showEntries(); } else { this.model.menu.select({ item: menuItem }); } } else { this.showSettings(menuItem); } } toggleOpenFile() { if (this.views.open) { if (this.model.files.hasOpenFiles()) { this.showEntries(); } } else { this.showOpenFile(); } } launcherBeforeQuit() { // this is currently called only on macos const event = { fromBeforeQuit: true, preventDefault() {} }; const result = this.beforeUnload(event); if (result !== false) { Launcher.exit(); } } beforeUnload(e) { const exitEvent = { preventDefault() { this.prevented = true; } }; Events.emit('main-window-will-close', exitEvent); if (exitEvent.prevented) { return Launcher ? Launcher.preventExit(e) : false; } let minimizeInsteadOfClose = this.model.settings.minimizeOnClose; if (e.fromBeforeQuit) { if (Launcher.quitOnRealQuitEventIfMinimizeOnQuitIsEnabled()) { minimizeInsteadOfClose = false; } } if (this.model.files.hasDirtyFiles()) { const exit = () => { if (minimizeInsteadOfClose) { Launcher.minimizeApp(); } else { Launcher.exit(); } }; if (Launcher && Launcher.exitRequested) { return; } if (Launcher) { if (!this.exitAlertShown) { if (this.model.settings.autoSave) { this.saveAndLock((result) => { if (result) { exit(); } }); return Launcher.preventExit(e); } this.exitAlertShown = true; Alerts.yesno({ header: Locale.appUnsavedWarn, body: Locale.appUnsavedWarnBody, buttons: [ { result: 'save', title: Locale.saveChanges }, { result: 'exit', title: Locale.discardChanges, error: true }, { result: '', title: Locale.appDontExitBtn } ], success: (result) => { if (result === 'save') { this.saveAndLock((result) => { if (result) { exit(); } }); } else { exit(); } }, cancel: () => { Launcher.cancelRestart(false); }, complete: () => { this.exitAlertShown = false; } }); } return Launcher.preventExit(e); } return Locale.appUnsavedWarnBody; } else if ( Launcher && !Launcher.exitRequested && !Launcher.restartPending && minimizeInsteadOfClose ) { Launcher.minimizeApp(); return Launcher.preventExit(e); } } windowResize() { Events.emit('page-geometry', { source: 'window' }); } windowBlur(e) { if (e.target === window) { Events.emit('page-blur'); } } enterFullScreen() { this.$el.addClass('fullscreen'); } leaveFullScreen() { this.$el.removeClass('fullscreen'); } escPressed() { if (this.views.open && this.model.files.hasOpenFiles()) { this.showEntries(); } } backspacePressed(e) { if (e.target === document.body) { e.preventDefault(); } } openDevTools() { if (Launcher && Launcher.devTools) { Launcher.openDevTools(); } } selectAll() { this.menuSelect({ item: this.model.menu.allItemsSection.items[0] }); } menuSelect(opt) { this.model.menu.select(opt); if (this.views.panel && !this.views.panel.isHidden()) { this.showEntries(); } } userIdle() { this.lockWorkspace(true); } osLocked() { if (this.model.settings.lockOnOsLock) { this.lockWorkspace(true); } } appMinimized() { if (this.model.settings.lockOnMinimize) { this.lockWorkspace(true); } } lockWorkspace(autoInit) { if (Alerts.alertDisplayed) { return; } if (this.model.files.hasUnsavedFiles()) { if (this.model.settings.autoSave) { this.saveAndLock(); } else { const message = autoInit ? Locale.appCannotLockAutoInit : Locale.appCannotLock; Alerts.alert({ icon: 'lock', header: 'Lock', body: message, buttons: [ { result: 'save', title: Locale.saveChanges }, { result: 'discard', title: Locale.discardChanges, error: true }, { result: '', title: Locale.alertCancel } ], checkbox: Locale.appAutoSave, success: (result, autoSaveChecked) => { if (result === 'save') { if (autoSaveChecked) { this.model.settings.autoSave = autoSaveChecked; } this.saveAndLock(); } else if (result === 'discard') { this.model.closeAllFiles(); } } }); } } else { this.closeAllFilesAndShowFirst(); } } saveAndLock(complete) { let pendingCallbacks = 0; const errorFiles = []; const that = this; this.model.files.forEach(function (file) { if (!file.dirty) { return; } this.model.syncFile(file, null, fileSaved.bind(this, file)); pendingCallbacks++; }, this); if (!pendingCallbacks) { this.closeAllFilesAndShowFirst(); } function fileSaved(file, err) { if (err) { errorFiles.push(file.name); } if (--pendingCallbacks === 0) { if (errorFiles.length && that.model.files.hasDirtyFiles()) { if (!Alerts.alertDisplayed) { const alertBody = errorFiles.length > 1 ? Locale.appSaveErrorBodyMul : Locale.appSaveErrorBody; Alerts.error({ header: Locale.appSaveError, body: alertBody + ' ' + errorFiles.join(', ') }); } if (complete) { complete(false); } } else { that.closeAllFilesAndShowFirst(); if (complete) { complete(true); } } } } } closeAllFilesAndShowFirst() { let fileToShow = this.model.files.find( (file) => !file.demo && !file.created && !file.external ); this.model.closeAllFiles(); if (!fileToShow) { fileToShow = this.model.fileInfos[0]; } if (fileToShow) { const fileInfo = this.model.fileInfos.getMatch( fileToShow.storage, fileToShow.name, fileToShow.path ); if (fileInfo) { this.views.open.showOpenFileInfo(fileInfo); } } } selectLastOpenFile() { const fileToShow = this.model.fileInfos[0]; if (fileToShow) { this.views.open.showOpenFileInfo(fileToShow); } } saveAll() { this.model.files.forEach(function (file) { this.model.syncFile(file); }, this); } setupAutoSave() { if (this.autoSaveTimer) { clearInterval(this.autoSaveTimer); } if (this.model.settings.autoSaveInterval) { this.autoSaveTimer = setInterval( this.saveAll.bind(this), this.model.settings.autoSaveInterval * 1000 * 60 ); } } remoteKeyChanged(e) { this.showKeyChange(e.file, { remote: true }); } keyChangePending(e) { this.showKeyChange(e.file, { expired: true }); } keyChangeAccept(e) { this.showEntries(); if (e.expired) { e.file.setPassword(e.password); if (e.keyFileData && e.keyFileName) { e.file.setKeyFile(e.keyFileData, e.keyFileName); } else { e.file.removeKeyFile(); } } else { this.model.syncFile(e.file, { remoteKey: { password: e.password, keyFileName: e.keyFileName, keyFileData: e.keyFileData } }); } } toggleSettings(page) { let menuItem = page ? this.model.menu[page + 'Section'] : null; if (menuItem) { menuItem = menuItem.items[0]; } if (this.views.settings) { if (this.views.settings.page === page || !menuItem) { if (this.model.files.hasOpenFiles()) { this.showEntries(); } else { this.showLastOpenFile(); this.views.open.toggleMore(); } } else { if (menuItem) { this.model.menu.select({ item: menuItem }); } } } else { this.showSettings(); if (menuItem) { this.model.menu.select({ item: menuItem }); } } } toggleMenu() { this.views.menu.switchVisibility(); } toggleDetails(visible) { this.$el.toggleClass('app--details-visible', visible); this.views.menu.switchVisibility(false); } editGroup(group) { if (group && !(this.views.panel instanceof GrpView)) { this.showEditGroup(group); } else { this.showEntries(); } } editTag(tag) { if (tag && !(this.views.panel instanceof TagView)) { this.showEditTag(); this.views.panel.showTag(tag); } else { this.showEntries(); } } editGeneratorPresets() { if (!(this.views.panel instanceof GeneratorPresetsView)) { if (this.views.settings) { this.showEntries(); } this.showPanelView(new GeneratorPresetsView(this.model)); } else { this.showEntries(); } } isContextMenuAllowed(e) { return ['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) < 0; } contextMenu(e) { if (this.isContextMenuAllowed(e)) { e.preventDefault(); } } showContextMenu(e) { if (e.options && this.isContextMenuAllowed(e)) { e.stopImmediatePropagation(); e.preventDefault(); if (this.views.contextMenu) { this.views.contextMenu.remove(); } const menu = new DropdownView(e); menu.render({ position: { left: e.pageX, top: e.pageY }, options: e.options }); menu.on('cancel', (e) => this.hideContextMenu()); menu.on('select', (e) => this.contextMenuSelect(e)); this.views.contextMenu = menu; } } hideContextMenu() { if (this.views.contextMenu) { this.views.contextMenu.remove(); delete this.views.contextMenu; } } contextMenuSelect(e) { this.hideContextMenu(); Events.emit('context-menu-select', e); } showSingleInstanceAlert() { this.hideOpenFile(); Alerts.error({ header: Locale.appTabWarn, body: Locale.appTabWarnBody, esc: false, enter: false, click: false, buttons: [] }); } dragover(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'none'; } drop(e) { e.preventDefault(); } setTheme() { SettingsManager.setTheme(this.model.settings.theme); } setFontSize() { SettingsManager.setFontSize(this.model.settings.fontSize); } setLocale() { SettingsManager.setLocale(this.model.settings.locale); if (this.views.settings.isVisible()) { this.hideSettings(); this.showSettings(); } this.$el.find('.app__beta:first').text(Locale.appBeta); } extLinkClick(e) { if (Launcher) { e.preventDefault(); Launcher.openLink(e.target.href); } } bodyClick(e) { IdleTracker.regUserAction(); Events.emit('click', e); } showImportCsv(file) { const reader = new FileReader(); const logger = new Logger('import-csv'); logger.info('Reading CSV...'); reader.onload = (e) => { logger.info('Parsing CSV...'); const ts = logger.ts(); const parser = new CsvParser(); let data; try { data = parser.parse(e.target.result); } catch (e) { logger.error('Error parsing CSV', e); Alerts.error({ header: Locale.openFailedRead, body: e.toString() }); return; } logger.info(`Parsed CSV: ${data.rows.length} records, ${logger.ts(ts)}`); // TODO: refactor this this.hideSettings(); this.hidePanelView(); this.hideOpenFile(); this.hideKeyChange(); this.views.menu.hide(); this.views.listWrap.hide(); this.views.list.hide(); this.views.listDrag.hide(); this.views.details.hide(); this.views.importCsv = new ImportCsvView(data, { appModel: this.model, fileName: file.name }); this.views.importCsv.render(); this.views.importCsv.on('cancel', () => { if (this.model.files.hasOpenFiles()) { this.showEntries(); } else { this.showOpenFile(); } }); this.views.importCsv.on('done', () => { this.model.refresh(); this.showEntries(); }); }; reader.onerror = () => { Alerts.error({ header: Locale.openFailedRead }); }; reader.readAsText(file); } } export { AppView };