mirror of https://github.com/keeweb/keeweb.git
open screen progress
This commit is contained in:
parent
caa17a48a8
commit
40dd3637d7
|
@ -28,6 +28,7 @@ import { FileManager } from 'models/file-manager';
|
|||
import { Updater } from './comp/app/updater';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { PluginManager } from 'plugins/plugin-manager';
|
||||
import { Workspace } from 'models/workspace';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -168,6 +169,8 @@ async function bootstrap() {
|
|||
}
|
||||
|
||||
function showView() {
|
||||
Workspace.showOpen();
|
||||
|
||||
const root = document.querySelector('.app');
|
||||
if (root) {
|
||||
const app = h(App, null);
|
||||
|
|
|
@ -106,6 +106,14 @@ class FileManager extends Model<FileManagerEvents> {
|
|||
this.files = this.files.filter((f) => f !== file);
|
||||
}
|
||||
|
||||
getFirstFileInfoToOpen(): FileInfo | undefined {
|
||||
for (const fi of this.fileInfos) {
|
||||
if (!this.getFileById(fi.id)) {
|
||||
return fi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fileClosed(file: File) {
|
||||
if (file.storage === 'file' && file.path) {
|
||||
Storage.file.unwatch(file.path);
|
||||
|
|
|
@ -1,22 +1,229 @@
|
|||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Model } from 'util/model';
|
||||
import { StorageFileOptions } from 'storage/types';
|
||||
import { FileChalRespConfig } from 'models/file-info';
|
||||
import { FileChalRespConfig, FileInfo } from 'models/file-info';
|
||||
import { FileOpener } from 'util/browser/file-opener';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { FileManager } from 'models/file-manager';
|
||||
import { DropboxChooser } from 'storage/dropbox-chooser';
|
||||
|
||||
export class OpenState extends Model {
|
||||
id?: string;
|
||||
name?: string;
|
||||
password?: kdbxweb.ProtectedValue;
|
||||
id?: string;
|
||||
storage?: string;
|
||||
path?: string;
|
||||
fileData?: ArrayBuffer;
|
||||
fileXml?: string;
|
||||
keyFileName?: string;
|
||||
keyFileData?: Uint8Array;
|
||||
keyFileData?: ArrayBuffer;
|
||||
keyFileHash?: string;
|
||||
keyFilePath?: string;
|
||||
fileData?: Uint8Array;
|
||||
rev?: string;
|
||||
opts?: StorageFileOptions;
|
||||
chalResp?: FileChalRespConfig;
|
||||
|
||||
busy = false;
|
||||
secondRowVisible = false;
|
||||
autoFocusPassword = true;
|
||||
capsLockPressed = false;
|
||||
visualFocus = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const fileInfo = FileManager.getFirstFileInfoToOpen();
|
||||
if (fileInfo) {
|
||||
this.selectFileInfo(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
selectFileInfo(fileInfo: FileInfo): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.batchSet(() => {
|
||||
this.resetInternal();
|
||||
this.id = fileInfo.id;
|
||||
this.name = fileInfo.name;
|
||||
this.storage = fileInfo.storage;
|
||||
this.path = fileInfo.path;
|
||||
this.keyFileName = fileInfo.keyFileName;
|
||||
this.keyFileHash = fileInfo.keyFileHash;
|
||||
this.keyFilePath = fileInfo.keyFilePath;
|
||||
this.rev = fileInfo.rev;
|
||||
this.opts = fileInfo.opts;
|
||||
this.chalResp = fileInfo.chalResp;
|
||||
});
|
||||
}
|
||||
|
||||
openFile(): void {
|
||||
if (this.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
FileOpener.openBinary((file, fileData) => {
|
||||
const format = OpenState.getOpenFileFormat(fileData);
|
||||
switch (format) {
|
||||
case 'kdbx':
|
||||
this.batchSet(() => {
|
||||
this.resetInternal();
|
||||
this.name = file.name.replace(/\.kdbx$/i, '');
|
||||
this.fileData = fileData;
|
||||
this.path = file.path || undefined;
|
||||
this.storage = file.path ? 'file' : undefined;
|
||||
});
|
||||
break;
|
||||
case 'xml':
|
||||
this.batchSet(() => {
|
||||
this.resetInternal();
|
||||
this.fileXml = kdbxweb.ByteUtils.bytesToString(fileData);
|
||||
this.name = file.name.replace(/\.\w+$/i, '');
|
||||
});
|
||||
break;
|
||||
case 'kdb':
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openKdbFileBody
|
||||
});
|
||||
break;
|
||||
default:
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openWrongFileBody
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openKeyFile(): void {
|
||||
if (this.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
FileOpener.openBinary((file, keyFileData) => {
|
||||
this.batchSet(() => {
|
||||
this.resetKeyFileInternal();
|
||||
|
||||
this.keyFileName = file.name;
|
||||
if (AppSettings.rememberKeyFiles === 'path' && file.path) {
|
||||
this.keyFilePath = file.path;
|
||||
}
|
||||
this.keyFileData = keyFileData;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
openKeyFileFromDropbox(): void {
|
||||
if (this.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
const dropboxChooser = new DropboxChooser((err, res) => {
|
||||
if (!err && res) {
|
||||
this.batchSet(() => {
|
||||
this.resetKeyFileInternal();
|
||||
|
||||
this.keyFileName = res.name;
|
||||
this.keyFileData = res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
dropboxChooser.choose();
|
||||
}
|
||||
|
||||
clearKeyFile(): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.batchSet(() => {
|
||||
this.resetKeyFileInternal();
|
||||
});
|
||||
}
|
||||
|
||||
selectNextFile(): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
let found = false;
|
||||
for (const fileInfo of FileManager.fileInfos) {
|
||||
if (found) {
|
||||
this.selectFileInfo(fileInfo);
|
||||
return;
|
||||
}
|
||||
if (fileInfo.id === this.id) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectPreviousFile(): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
let prevFileInfo: FileInfo | undefined;
|
||||
for (const fileInfo of FileManager.fileInfos) {
|
||||
if (fileInfo.id === this.id) {
|
||||
if (prevFileInfo) {
|
||||
this.selectFileInfo(prevFileInfo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
prevFileInfo = fileInfo;
|
||||
}
|
||||
}
|
||||
|
||||
open(): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private resetInternal() {
|
||||
this.id = undefined;
|
||||
this.name = undefined;
|
||||
this.password = kdbxweb.ProtectedValue.fromString('');
|
||||
this.storage = undefined;
|
||||
this.path = undefined;
|
||||
this.fileData = undefined;
|
||||
this.fileXml = undefined;
|
||||
this.keyFileName = undefined;
|
||||
this.keyFileData = undefined;
|
||||
this.keyFileHash = undefined;
|
||||
this.keyFilePath = undefined;
|
||||
this.rev = undefined;
|
||||
this.opts = undefined;
|
||||
this.chalResp = undefined;
|
||||
}
|
||||
|
||||
private resetKeyFileInternal() {
|
||||
this.keyFileName = undefined;
|
||||
this.keyFileData = undefined;
|
||||
this.keyFileHash = undefined;
|
||||
this.keyFilePath = undefined;
|
||||
}
|
||||
|
||||
private static getOpenFileFormat(fileData: ArrayBuffer): 'kdbx' | 'kdb' | 'xml' | undefined {
|
||||
if (fileData.byteLength < 8) {
|
||||
return undefined;
|
||||
}
|
||||
const fileSig = new Uint32Array(fileData, 0, 2);
|
||||
if (fileSig[0] === kdbxweb.Consts.Signatures.FileMagic) {
|
||||
if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdb) {
|
||||
return 'kdb';
|
||||
} else if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdbx) {
|
||||
return 'kdbx';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else if (AppSettings.canImportXml) {
|
||||
try {
|
||||
const str = kdbxweb.ByteUtils.bytesToString(fileSig).trim();
|
||||
if (str.startsWith('<?xml')) {
|
||||
return 'xml';
|
||||
}
|
||||
} catch (e) {}
|
||||
return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ class Workspace extends Model {
|
|||
if (!FileManager.getFileByName('Demo')) {
|
||||
const demoFile = await File.openDemo();
|
||||
FileManager.addFile(demoFile);
|
||||
AppSettings.demoOpened = true;
|
||||
}
|
||||
this.showList();
|
||||
}
|
||||
|
@ -112,6 +113,20 @@ class Workspace extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
toggleSettings(page: string): void {
|
||||
// TODO: settings page
|
||||
if (this.mode === 'settings') {
|
||||
if (FileManager.hasOpenFiles) {
|
||||
this.showList();
|
||||
} else {
|
||||
this.showOpen();
|
||||
}
|
||||
} else {
|
||||
this.showSettings();
|
||||
}
|
||||
}
|
||||
|
||||
showList() {
|
||||
if (FileManager.hasOpenFiles) {
|
||||
this.mode = 'list';
|
||||
|
@ -123,6 +138,10 @@ class Workspace extends Model {
|
|||
this.mode = 'open';
|
||||
}
|
||||
|
||||
showSettings(): void {
|
||||
this.mode = 'settings';
|
||||
}
|
||||
|
||||
private filesChanged() {
|
||||
this.updateTags();
|
||||
|
||||
|
@ -265,6 +284,12 @@ class Workspace extends Model {
|
|||
);
|
||||
|
||||
KeyHandler.onKey(Keys.DOM_VK_O, () => this.toggleOpen(), KeyHandler.SHORTCUT_ACTION);
|
||||
KeyHandler.onKey(
|
||||
Keys.DOM_VK_COMMA,
|
||||
() => this.showSettings(),
|
||||
KeyHandler.SHORTCUT_ACTION,
|
||||
'open'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ class FooterView extends View {
|
|||
|
||||
this.onKey(Keys.DOM_VK_G, this.genPass, KeyHandler.SHORTCUT_ACTION);
|
||||
this.onKey(Keys.DOM_VK_S, this.saveAll, KeyHandler.SHORTCUT_ACTION);
|
||||
this.onKey(Keys.DOM_VK_COMMA, this.toggleSettings, KeyHandler.SHORTCUT_ACTION);
|
||||
|
||||
this.listenTo(this, 'hide', this.viewHidden);
|
||||
this.listenTo(this.model.files, 'change', this.render);
|
||||
|
@ -60,10 +59,6 @@ class FooterView extends View {
|
|||
toggleHelp() {
|
||||
Events.emit('toggle-settings', 'help');
|
||||
}
|
||||
|
||||
toggleSettings() {
|
||||
Events.emit('toggle-settings', 'general');
|
||||
}
|
||||
}
|
||||
|
||||
export { FooterView };
|
||||
|
|
|
@ -2,15 +2,9 @@ import * as kdbxweb from 'kdbxweb';
|
|||
import { View } from 'framework/views/view';
|
||||
import { Events } from 'framework/events';
|
||||
import { Storage } from 'storage';
|
||||
import { DropboxChooser } from 'comp/app/dropbox-chooser';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { SecureInput } from 'comp/browser/secure-input';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { YubiKey } from 'comp/app/yubikey';
|
||||
import { Keys } from 'const/keys';
|
||||
import { Comparators } from 'util/data/comparators';
|
||||
import { Features } from 'util/features';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
|
@ -23,16 +17,10 @@ import { OpenChalRespView } from 'views/open-chal-resp-view';
|
|||
import { omit } from 'util/fn';
|
||||
import { GeneratorView } from 'views/generator-view';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import template from 'templates/open.hbs';
|
||||
|
||||
const logger = new Logger('open-view');
|
||||
|
||||
class OpenView extends View {
|
||||
parent = '.app__body';
|
||||
modal = 'open';
|
||||
|
||||
template = template;
|
||||
|
||||
events = {
|
||||
'change .open__file-ctrl': 'fileSelected',
|
||||
'click .open__icon-open': 'openFile',
|
||||
|
@ -58,23 +46,8 @@ class OpenView extends View {
|
|||
drop: 'drop'
|
||||
};
|
||||
|
||||
params = null;
|
||||
passwordInput = null;
|
||||
busy = false;
|
||||
currentSelectedIndex = -1;
|
||||
encryptedPassword = null;
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
window.$ = $;
|
||||
this.resetParams();
|
||||
this.passwordInput = new SecureInput();
|
||||
this.onKey(Keys.DOM_VK_Z, this.undoKeyPress, KeyHandler.SHORTCUT_ACTION, 'open');
|
||||
this.onKey(Keys.DOM_VK_TAB, this.tabKeyPress, null, 'open');
|
||||
this.onKey(Keys.DOM_VK_ENTER, this.enterKeyPress, null, 'open');
|
||||
this.onKey(Keys.DOM_VK_RETURN, this.enterKeyPress, null, 'open');
|
||||
this.onKey(Keys.DOM_VK_DOWN, this.moveOpenFileSelectionDown, null, 'open');
|
||||
this.onKey(Keys.DOM_VK_UP, this.moveOpenFileSelectionUp, null, 'open');
|
||||
constructor() {
|
||||
super();
|
||||
this.listenTo(Events, 'usb-devices-changed', this.usbDevicesChanged.bind(this));
|
||||
}
|
||||
|
||||
|
@ -84,33 +57,11 @@ class OpenView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
resetParams() {
|
||||
this.params = {
|
||||
id: null,
|
||||
name: '',
|
||||
storage: null,
|
||||
path: null,
|
||||
keyFileName: null,
|
||||
keyFileData: null,
|
||||
keyFilePath: null,
|
||||
fileData: null,
|
||||
rev: null,
|
||||
opts: null,
|
||||
chalResp: null
|
||||
};
|
||||
}
|
||||
|
||||
windowFocused() {
|
||||
this.inputEl.focus();
|
||||
this.checkIfEncryptedPasswordDateIsValid();
|
||||
}
|
||||
|
||||
focusInput(focusOnMobile) {
|
||||
if (FocusDetector.hasFocus() && (focusOnMobile || !Features.isMobile)) {
|
||||
this.inputEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
showLocalFileAlert() {
|
||||
if (this.model.settings.skipOpenLocalWarn) {
|
||||
return;
|
||||
|
@ -152,126 +103,6 @@ class OpenView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
processFile(file, complete) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let success = false;
|
||||
switch (this.reading) {
|
||||
case 'fileData': {
|
||||
const format = this.getOpenFileFormat(e.target.result);
|
||||
switch (format) {
|
||||
case 'kdbx':
|
||||
this.params.id = null;
|
||||
this.params.fileData = e.target.result;
|
||||
this.params.name = file.name.replace(/(.+)\.\w+$/i, '$1');
|
||||
this.params.path = file.path || null;
|
||||
this.params.storage = file.path ? 'file' : null;
|
||||
this.params.rev = null;
|
||||
if (!this.params.keyFileData) {
|
||||
this.params.keyFileName = null;
|
||||
}
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenKeyFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
success = true;
|
||||
break;
|
||||
case 'xml':
|
||||
this.params.id = null;
|
||||
this.params.fileXml = kdbxweb.ByteUtils.bytesToString(e.target.result);
|
||||
this.params.name = file.name.replace(/\.\w+$/i, '');
|
||||
this.params.path = null;
|
||||
this.params.storage = null;
|
||||
this.params.rev = null;
|
||||
this.encryptedPassword = null;
|
||||
this.importDbWithXml();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
success = true;
|
||||
break;
|
||||
case 'kdb':
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openKdbFileBody
|
||||
});
|
||||
break;
|
||||
default:
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openWrongFileBody
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'keyFileData':
|
||||
this.params.keyFileData = e.target.result;
|
||||
this.params.keyFileName = file.name;
|
||||
if (this.model.settings.rememberKeyFiles === 'path') {
|
||||
this.params.keyFilePath = file.path;
|
||||
}
|
||||
this.displayOpenKeyFile();
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
if (complete) {
|
||||
complete(success);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
Alerts.error({ header: Locale.openFailedRead });
|
||||
if (complete) {
|
||||
complete(false);
|
||||
}
|
||||
};
|
||||
if (this.reading === 'fileXml') {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
|
||||
getOpenFileFormat(fileData) {
|
||||
if (fileData.byteLength < 8) {
|
||||
return undefined;
|
||||
}
|
||||
const fileSig = new Uint32Array(fileData, 0, 2);
|
||||
if (fileSig[0] === kdbxweb.Consts.Signatures.FileMagic) {
|
||||
if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdb) {
|
||||
return 'kdb';
|
||||
} else if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdbx) {
|
||||
return 'kdbx';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else if (this.model.settings.canImportXml) {
|
||||
try {
|
||||
const str = kdbxweb.ByteUtils.bytesToString(fileSig).trim();
|
||||
if (str.startsWith('<?xml')) {
|
||||
return 'xml';
|
||||
}
|
||||
} catch (e) {}
|
||||
return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
displayOpenFile() {
|
||||
this.$el.addClass('open--file');
|
||||
this.$el.find('.open__settings-key-file,.open__settings-yubikey').removeClass('hide');
|
||||
this.inputEl[0].removeAttribute('readonly');
|
||||
this.inputEl[0].setAttribute('placeholder', Locale.openPassFor + ' ' + this.params.name);
|
||||
this.focusInput();
|
||||
}
|
||||
|
||||
displayOpenKeyFile() {
|
||||
this.$el.toggleClass('open--key-file', !!this.params.keyFileName);
|
||||
this.$el
|
||||
.find('.open__settings-key-file-name')
|
||||
.text(this.params.keyFileName || this.params.keyFilePath || Locale.openKeyFile);
|
||||
this.focusInput();
|
||||
}
|
||||
|
||||
displayOpenChalResp() {
|
||||
this.$el
|
||||
.find('.open__settings-yubikey')
|
||||
|
@ -287,80 +118,6 @@ class OpenView extends View {
|
|||
.classList.toggle('open__pass-enter-btn--touch-id', canUseEncryptedPassword);
|
||||
}
|
||||
|
||||
setFile(file, keyFile, fileReadyCallback) {
|
||||
this.reading = 'fileData';
|
||||
this.processFile(file, (success) => {
|
||||
if (success && keyFile) {
|
||||
this.reading = 'keyFileData';
|
||||
this.processFile(keyFile);
|
||||
}
|
||||
if (success && typeof fileReadyCallback === 'function') {
|
||||
fileReadyCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openFile() {
|
||||
if (this.model.settings.canOpen === false) {
|
||||
return;
|
||||
}
|
||||
if (!this.busy) {
|
||||
this.closeConfig();
|
||||
this.openAny('fileData');
|
||||
}
|
||||
}
|
||||
|
||||
openKeyFile(e) {
|
||||
if ($(e.target).hasClass('open__settings-key-file-dropbox')) {
|
||||
this.openKeyFileFromDropbox();
|
||||
} else if (!this.busy && this.params.name) {
|
||||
if (this.params.keyFileName) {
|
||||
this.params.keyFileData = null;
|
||||
this.params.keyFilePath = null;
|
||||
this.params.keyFileName = '';
|
||||
this.$el.removeClass('open--key-file');
|
||||
this.$el.find('.open__settings-key-file-name').text(Locale.openKeyFile);
|
||||
} else {
|
||||
this.openAny('keyFileData');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openKeyFileFromDropbox() {
|
||||
if (!this.busy) {
|
||||
new DropboxChooser((err, res) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
this.params.keyFileData = res.data;
|
||||
this.params.keyFileName = res.name;
|
||||
this.displayOpenKeyFile();
|
||||
}).choose();
|
||||
}
|
||||
}
|
||||
|
||||
openAny(reading, ext) {
|
||||
this.reading = reading;
|
||||
this.params[reading] = null;
|
||||
|
||||
const fileInput = this.$el
|
||||
.find('.open__file-ctrl')
|
||||
.attr('accept', ext || '')
|
||||
.val(null);
|
||||
|
||||
if (Launcher && Launcher.openFileChooser) {
|
||||
Launcher.openFileChooser((err, file) => {
|
||||
if (err) {
|
||||
logger.error('Error opening file chooser', err);
|
||||
} else {
|
||||
this.processFile(file);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
openLast(e) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
|
@ -399,40 +156,10 @@ class OpenView extends View {
|
|||
this.render();
|
||||
}
|
||||
|
||||
inputKeydown(e) {
|
||||
const code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN) {
|
||||
this.openDb();
|
||||
} else if (code === Keys.DOM_VK_CAPS_LOCK) {
|
||||
this.toggleCapsLockWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
inputKeyup(e) {
|
||||
const code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_CAPS_LOCK) {
|
||||
this.toggleCapsLockWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
inputKeypress(e) {
|
||||
const charCode = e.keyCode || e.which;
|
||||
const ch = String.fromCharCode(charCode);
|
||||
const lower = ch.toLowerCase();
|
||||
const upper = ch.toUpperCase();
|
||||
if (lower !== upper && !e.shiftKey) {
|
||||
this.toggleCapsLockWarning(ch !== lower);
|
||||
}
|
||||
}
|
||||
|
||||
inputInput() {
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
}
|
||||
|
||||
toggleCapsLockWarning(on) {
|
||||
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
||||
}
|
||||
|
||||
dragover(e) {
|
||||
if (this.model.settings.canOpen === false) {
|
||||
return;
|
||||
|
@ -507,88 +234,6 @@ class OpenView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
undoKeyPress(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
tabKeyPress() {
|
||||
this.$el.addClass('open--show-focus');
|
||||
}
|
||||
|
||||
enterKeyPress(e) {
|
||||
const el = this.$el.find('[tabindex]:focus');
|
||||
if (el.length) {
|
||||
el.trigger('click', e);
|
||||
}
|
||||
}
|
||||
|
||||
showOpenFileInfo(fileInfo, fileWasClicked) {
|
||||
if (this.busy || !fileInfo) {
|
||||
return;
|
||||
}
|
||||
this.params.id = fileInfo.id;
|
||||
this.params.storage = fileInfo.storage;
|
||||
this.params.path = fileInfo.path;
|
||||
this.params.name = fileInfo.name;
|
||||
this.params.fileData = null;
|
||||
this.params.rev = null;
|
||||
this.params.keyFileName = fileInfo.keyFileName;
|
||||
this.params.keyFilePath = fileInfo.keyFilePath;
|
||||
this.params.keyFileData = null;
|
||||
this.params.opts = fileInfo.opts;
|
||||
this.params.chalResp = fileInfo.chalResp;
|
||||
this.setEncryptedPassword(fileInfo);
|
||||
|
||||
this.displayOpenFile();
|
||||
this.displayOpenKeyFile();
|
||||
this.displayOpenChalResp();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
|
||||
if (fileWasClicked) {
|
||||
this.focusInput(true);
|
||||
}
|
||||
}
|
||||
|
||||
showOpenLocalFile(path, keyFilePath) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.params.id = null;
|
||||
this.params.storage = 'file';
|
||||
this.params.path = path;
|
||||
this.params.name = path.match(/[^/\\]*$/)[0];
|
||||
this.params.rev = null;
|
||||
this.params.fileData = null;
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
if (keyFilePath) {
|
||||
const parsed = Launcher.parsePath(keyFilePath);
|
||||
this.params.keyFileName = parsed.file;
|
||||
this.params.keyFilePath = keyFilePath;
|
||||
this.params.keyFileData = null;
|
||||
this.displayOpenKeyFile();
|
||||
}
|
||||
}
|
||||
|
||||
createDemo() {
|
||||
if (!this.busy) {
|
||||
this.closeConfig();
|
||||
if (!this.model.createDemoFile()) {
|
||||
this.emit('close');
|
||||
}
|
||||
if (!this.model.settings.demoOpened) {
|
||||
this.model.settings.demoOpened = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createNew() {
|
||||
if (!this.busy) {
|
||||
this.model.createNewFile();
|
||||
}
|
||||
}
|
||||
|
||||
openDb() {
|
||||
if (this.params.id && this.model.files.get(this.params.id)) {
|
||||
this.emit('close');
|
||||
|
@ -686,18 +331,6 @@ class OpenView extends View {
|
|||
);
|
||||
}
|
||||
|
||||
toggleMore() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.closeConfig();
|
||||
this.$el.find('.open__icons--lower').toggleClass('hide');
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
Events.emit('toggle-settings');
|
||||
}
|
||||
|
||||
openStorage(e) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
|
@ -899,31 +532,6 @@ class OpenView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
moveOpenFileSelection(steps) {
|
||||
const lastOpenFiles = this.getLastOpenFiles();
|
||||
if (
|
||||
this.currentSelectedIndex + steps >= 0 &&
|
||||
this.currentSelectedIndex + steps <= lastOpenFiles.length - 1
|
||||
) {
|
||||
this.currentSelectedIndex = this.currentSelectedIndex + steps;
|
||||
}
|
||||
|
||||
const lastOpenFile = lastOpenFiles[this.currentSelectedIndex];
|
||||
if (!lastOpenFile) {
|
||||
return;
|
||||
}
|
||||
const fileInfo = this.model.fileInfos.get(lastOpenFiles[this.currentSelectedIndex].id);
|
||||
this.showOpenFileInfo(fileInfo);
|
||||
}
|
||||
|
||||
moveOpenFileSelectionDown() {
|
||||
this.moveOpenFileSelection(1);
|
||||
}
|
||||
|
||||
moveOpenFileSelectionUp() {
|
||||
this.moveOpenFileSelection(-1);
|
||||
}
|
||||
|
||||
toggleGenerator(e) {
|
||||
e.stopPropagation();
|
||||
if (this.views.gen) {
|
||||
|
|
|
@ -17,6 +17,8 @@ export const Footer: FunctionComponent = () => {
|
|||
|
||||
const openClicked = () => Workspace.toggleOpen();
|
||||
|
||||
const settingsClicked = () => Workspace.toggleSettings('general');
|
||||
|
||||
const lockWorkspaceClicked = () => Workspace.lockWorkspace();
|
||||
|
||||
return h(FooterView, {
|
||||
|
@ -24,6 +26,7 @@ export const Footer: FunctionComponent = () => {
|
|||
updateAvailable,
|
||||
|
||||
openClicked,
|
||||
settingsClicked,
|
||||
lockWorkspaceClicked
|
||||
});
|
||||
};
|
||||
|
|
|
@ -40,6 +40,10 @@ export const OpenButtons: FunctionComponent = () => {
|
|||
AppSettings.yubiKeyShowIcon &&
|
||||
!FileManager.getFileByName('yubikey');
|
||||
|
||||
const openClicked = () => {
|
||||
Workspace.openState.openFile();
|
||||
};
|
||||
|
||||
const newClicked = () => {
|
||||
Workspace.createNewFile().catch((e) => logger.error(e));
|
||||
};
|
||||
|
@ -52,6 +56,10 @@ export const OpenButtons: FunctionComponent = () => {
|
|||
Workspace.createDemoFile().catch((e) => logger.error(e));
|
||||
};
|
||||
|
||||
const settingsClicked = () => {
|
||||
Workspace.showSettings();
|
||||
};
|
||||
|
||||
return h(OpenButtonsView, {
|
||||
secondRowVisible,
|
||||
showOpen: AppSettings.canOpen,
|
||||
|
@ -65,8 +73,10 @@ export const OpenButtons: FunctionComponent = () => {
|
|||
showSettings: AppSettings.canOpenSettings,
|
||||
storageProviders,
|
||||
|
||||
openClicked,
|
||||
newClicked,
|
||||
moreClicked,
|
||||
openDemoClicked
|
||||
openDemoClicked,
|
||||
settingsClicked
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import { OpenLastFilesView } from 'views/open/open-last-files-view';
|
|||
import { FileManager } from 'models/file-manager';
|
||||
import { Storage } from 'storage';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Workspace } from 'models/workspace';
|
||||
|
||||
export const OpenLastFiles: FunctionComponent = () => {
|
||||
const lastOpenFiles = FileManager.fileInfos.map((fi) => {
|
||||
|
@ -17,8 +18,17 @@ export const OpenLastFiles: FunctionComponent = () => {
|
|||
};
|
||||
});
|
||||
|
||||
const lastFileSelected = (id: string) => {
|
||||
const fileInfo = FileManager.getFileInfoById(id);
|
||||
if (fileInfo) {
|
||||
Workspace.openState.selectFileInfo(fileInfo);
|
||||
}
|
||||
};
|
||||
|
||||
return h(OpenLastFilesView, {
|
||||
lastOpenFiles,
|
||||
canRemoveLatest: AppSettings.canRemoveLatest
|
||||
canRemoveLatest: AppSettings.canRemoveLatest,
|
||||
|
||||
lastFileSelected
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import * as kdbxweb from 'kdbxweb';
|
||||
import { h, FunctionComponent } from 'preact';
|
||||
import { OpenPasswordView } from 'views/open/open-password-view';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Workspace } from 'models/workspace';
|
||||
import { useEvent, useModelWatcher } from 'util/ui/hooks';
|
||||
import { Locale } from 'util/locale';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Keys } from 'const/keys';
|
||||
|
||||
export const OpenPassword: FunctionComponent = () => {
|
||||
useModelWatcher(Workspace.openState);
|
||||
|
@ -12,14 +14,59 @@ export const OpenPassword: FunctionComponent = () => {
|
|||
Workspace.openState.password = kdbxweb.ProtectedValue.fromString('');
|
||||
});
|
||||
|
||||
const passwordClicked = () => {
|
||||
Workspace.openState.openFile();
|
||||
};
|
||||
|
||||
const passwordChanged = (password: kdbxweb.ProtectedValue) => {
|
||||
Workspace.openState.password = password;
|
||||
};
|
||||
|
||||
const passwordKeyUp = (e: KeyboardEvent) => {
|
||||
const code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_CAPS_LOCK) {
|
||||
Workspace.openState.capsLockPressed = false;
|
||||
}
|
||||
};
|
||||
|
||||
const passwordKeyDown = (e: KeyboardEvent) => {
|
||||
const code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN) {
|
||||
Workspace.openState.open();
|
||||
} else if (code === Keys.DOM_VK_CAPS_LOCK) {
|
||||
Workspace.openState.capsLockPressed = false;
|
||||
}
|
||||
};
|
||||
|
||||
const passwordKeyPress = (e: KeyboardEvent) => {
|
||||
const charCode = e.keyCode || e.which;
|
||||
const ch = String.fromCharCode(charCode);
|
||||
const lower = ch.toLowerCase();
|
||||
const upper = ch.toUpperCase();
|
||||
if (lower !== upper && !e.shiftKey) {
|
||||
Workspace.openState.capsLockPressed = ch !== lower;
|
||||
}
|
||||
};
|
||||
|
||||
let passwordPlaceholder = '';
|
||||
if (Workspace.openState.name) {
|
||||
passwordPlaceholder = `${Locale.openPassFor} ${Workspace.openState.name}`;
|
||||
} else if (AppSettings.canOpen) {
|
||||
passwordPlaceholder = Locale.openClickToOpen;
|
||||
}
|
||||
|
||||
return h(OpenPasswordView, {
|
||||
password: Workspace.openState.password,
|
||||
canOpen: AppSettings.canOpen,
|
||||
passwordReadOnly: !Workspace.openState.name,
|
||||
passwordPlaceholder,
|
||||
autoFocusPassword: Workspace.openState.autoFocusPassword,
|
||||
buttonFingerprint: false,
|
||||
capsLockPressed: Workspace.openState.capsLockPressed,
|
||||
|
||||
passwordChanged
|
||||
passwordClicked,
|
||||
passwordChanged,
|
||||
passwordKeyUp,
|
||||
passwordKeyDown,
|
||||
passwordKeyPress
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,37 @@
|
|||
import { h, FunctionComponent } from 'preact';
|
||||
import { OpenScreenView } from 'views/open/open-screen-view';
|
||||
import { Workspace } from 'models/workspace';
|
||||
import { useKey, useModal, useModelField } from 'util/ui/hooks';
|
||||
import { Keys } from 'const/keys';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
|
||||
export const OpenScreen: FunctionComponent = () => {
|
||||
return h(OpenScreenView, {});
|
||||
useModal('open');
|
||||
|
||||
const openFile = () => {
|
||||
Workspace.openState.openFile();
|
||||
};
|
||||
|
||||
const setVisualFocus = () => {
|
||||
Workspace.openState.visualFocus = true;
|
||||
};
|
||||
|
||||
const undoKeyPress = (e: KeyboardEvent) => e.preventDefault();
|
||||
|
||||
const name = useModelField(Workspace.openState, 'name');
|
||||
const keyFileName = useModelField(Workspace.openState, 'keyFileName');
|
||||
const visualFocus = useModelField(Workspace.openState, 'visualFocus');
|
||||
|
||||
useKey(Keys.DOM_VK_O, openFile, KeyHandler.SHORTCUT_ACTION, 'open');
|
||||
useKey(Keys.DOM_VK_DOWN, () => Workspace.openState.selectNextFile(), undefined, 'open');
|
||||
useKey(Keys.DOM_VK_UP, () => Workspace.openState.selectPreviousFile(), undefined, 'open');
|
||||
useKey(Keys.DOM_VK_TAB, setVisualFocus, undefined, 'open');
|
||||
useKey(Keys.DOM_VK_TAB, setVisualFocus, KeyHandler.SHORTCUT_SHIFT, 'open');
|
||||
useKey(Keys.DOM_VK_Z, undoKeyPress, undefined, 'open');
|
||||
|
||||
return h(OpenScreenView, {
|
||||
fileSelected: !!name,
|
||||
keyFileSelected: !!keyFileName,
|
||||
visualFocus
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,13 +4,35 @@ import { Launcher } from 'comp/launcher';
|
|||
import { Storage } from 'storage';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { UsbListener } from 'comp/devices/usb-listener';
|
||||
import { useModelField } from 'util/ui/hooks';
|
||||
import { Workspace } from 'models/workspace';
|
||||
|
||||
export const OpenSettings: FunctionComponent = () => {
|
||||
const name = useModelField(Workspace.openState, 'name');
|
||||
const keyFileName = useModelField(Workspace.openState, 'keyFileName');
|
||||
|
||||
const hasYubiKeys = UsbListener.attachedYubiKeys > 0;
|
||||
const canUseChalRespYubiKey = hasYubiKeys && AppSettings.yubiKeyShowChalResp;
|
||||
|
||||
const selectKeyFileClicked = () => {
|
||||
if (keyFileName) {
|
||||
Workspace.openState.clearKeyFile();
|
||||
} else {
|
||||
Workspace.openState.openKeyFile();
|
||||
}
|
||||
};
|
||||
|
||||
const selectKeyFileFromDropboxClicked = () => {
|
||||
Workspace.openState.openKeyFileFromDropbox();
|
||||
};
|
||||
|
||||
return h(OpenSettingsView, {
|
||||
canSelectKeyFile: !!name,
|
||||
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
|
||||
canUseChalRespYubiKey
|
||||
canUseChalRespYubiKey,
|
||||
keyFileName,
|
||||
|
||||
selectKeyFileClicked,
|
||||
selectKeyFileFromDropboxClicked
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { Logger } from 'util/logger';
|
||||
|
||||
let el: HTMLInputElement | undefined;
|
||||
|
||||
const logger = new Logger('file-reader');
|
||||
|
||||
export const FileOpener = {
|
||||
openText(selected: (file: File, data: string) => void): void {
|
||||
this.open((file, data) => {
|
||||
if (typeof data === 'string') {
|
||||
selected(file, data);
|
||||
} else {
|
||||
logger.error(`Error reading file: expected a string, got`, data);
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
openBinary(selected: (file: File, data: ArrayBuffer) => void): void {
|
||||
this.open((file, data) => {
|
||||
if (data instanceof ArrayBuffer) {
|
||||
selected(file, data);
|
||||
} else {
|
||||
logger.error(`Error reading file: expected an ArrayBuffer, got`, data);
|
||||
}
|
||||
}, false);
|
||||
},
|
||||
|
||||
open(selected: (file: File, data: unknown) => void, asText: boolean): void {
|
||||
el?.remove();
|
||||
|
||||
el = document.createElement('input');
|
||||
el.type = 'file';
|
||||
el.classList.add('hide-by-pos');
|
||||
el.click();
|
||||
el.addEventListener('change', () => {
|
||||
const file = el?.files?.[0];
|
||||
if (!file) {
|
||||
logger.error('No file selected');
|
||||
el?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.name) {
|
||||
logger.error('File with empty name selected');
|
||||
el?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('error', () => {
|
||||
el?.remove();
|
||||
logger.error('Error reading file');
|
||||
});
|
||||
reader.addEventListener('load', () => {
|
||||
selected(file, reader.result);
|
||||
el?.remove();
|
||||
});
|
||||
|
||||
if (asText) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -44,5 +44,12 @@ export function unreachable(msg: string, arg: never): never {
|
|||
}
|
||||
|
||||
export function errorToString(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
const str = String(err);
|
||||
if (str === String({})) {
|
||||
return `Error: ${JSON.stringify(str)}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export function withoutPropagation<EventType extends Event, Args extends unknown[]>(
|
||||
listener?: (...args: Args) => void,
|
||||
...args: Args
|
||||
): (e: EventType) => void {
|
||||
return (e: EventType) => {
|
||||
e.stopPropagation();
|
||||
listener?.(...args);
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@ import { ListenerSignature, Model } from 'util/model';
|
|||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { Keys } from 'const/keys';
|
||||
import { Events, GlobalEventSpec } from 'util/events';
|
||||
import { FocusManager } from 'comp/app/focus-manager';
|
||||
|
||||
export function useModelField<
|
||||
ModelEventsType extends ListenerSignature<ModelEventsType>,
|
||||
|
@ -61,3 +62,10 @@ export function useKey(
|
|||
return KeyHandler.onKey(key, handler, shortcut, modal, noPrevent);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useModal(name: string): void {
|
||||
useEffect(() => {
|
||||
FocusManager.pushModal(name);
|
||||
return () => FocusManager.popModal();
|
||||
}, []);
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ import { classes } from 'util/ui/classes';
|
|||
|
||||
const MaxLength = 1024;
|
||||
|
||||
export interface SecureInputEvent {
|
||||
export interface SecureInputEvent extends InputEvent {
|
||||
value: kdbxweb.ProtectedValue;
|
||||
}
|
||||
|
||||
export const SecureInput: FunctionComponent<{
|
||||
name?: string;
|
||||
inputClass?: string;
|
||||
autofocus?: boolean;
|
||||
error?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -23,10 +22,13 @@ export const SecureInput: FunctionComponent<{
|
|||
value?: kdbxweb.ProtectedValue;
|
||||
|
||||
onInput?: (e: SecureInputEvent) => void;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
onKeyUp?: (e: KeyboardEvent) => void;
|
||||
onKeyPress?: (e: KeyboardEvent) => void;
|
||||
}> = ({
|
||||
name,
|
||||
inputClass,
|
||||
autofocus,
|
||||
error,
|
||||
readonly,
|
||||
disabled,
|
||||
|
@ -36,7 +38,11 @@ export const SecureInput: FunctionComponent<{
|
|||
inputRef,
|
||||
value,
|
||||
|
||||
onInput
|
||||
onInput,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
onKeyPress
|
||||
}) => {
|
||||
const minChar = useRef(0x1400 + Math.round(Math.random() * 100));
|
||||
const length = useRef(0);
|
||||
|
@ -100,7 +106,10 @@ export const SecureInput: FunctionComponent<{
|
|||
|
||||
lastValue.current = getValue();
|
||||
|
||||
onInput?.({ value: lastValue.current });
|
||||
const ev = e as SecureInputEvent;
|
||||
ev.value = lastValue.current;
|
||||
|
||||
onInput?.(ev);
|
||||
};
|
||||
|
||||
function getChar(ix: number): string {
|
||||
|
@ -145,15 +154,18 @@ export const SecureInput: FunctionComponent<{
|
|||
type="password"
|
||||
autocomplete="new-password"
|
||||
maxLength={MaxLength}
|
||||
autofocus={autofocus}
|
||||
tabIndex={tabIndex}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
ref={inputRef}
|
||||
onInput={onInternalInput}
|
||||
value={realInputValue}
|
||||
onInput={onInternalInput}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,8 +15,9 @@ export const FooterView: FunctionComponent<{
|
|||
updateAvailable: boolean;
|
||||
|
||||
openClicked: () => void;
|
||||
settingsClicked: () => void;
|
||||
lockWorkspaceClicked: () => void;
|
||||
}> = ({ files, updateAvailable, openClicked, lockWorkspaceClicked }) => {
|
||||
}> = ({ files, updateAvailable, settingsClicked, openClicked, lockWorkspaceClicked }) => {
|
||||
return (
|
||||
<div class="footer">
|
||||
{files.map((file) => (
|
||||
|
@ -57,6 +58,7 @@ export const FooterView: FunctionComponent<{
|
|||
class="footer__btn footer__btn-settings"
|
||||
tip-placement="top"
|
||||
id="footer__btn-settings"
|
||||
onClick={settingsClicked}
|
||||
>
|
||||
<kw-tip text={Locale.settings} />
|
||||
{updateAvailable ? (
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Locale } from 'util/locale';
|
|||
import { useState } from 'preact/hooks';
|
||||
import { MenuItem } from 'models/menu/menu-item';
|
||||
import { AppMenuItem } from 'ui/menu/app-menu-item';
|
||||
import { withoutPropagation } from 'util/ui/events';
|
||||
|
||||
export const AppMenuItemView: FunctionComponent<{
|
||||
title: string;
|
||||
|
@ -49,36 +50,6 @@ export const AppMenuItemView: FunctionComponent<{
|
|||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const mouseOver = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const mouseOut = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
const itemClickedStopPropagation = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
itemClicked?.();
|
||||
};
|
||||
|
||||
const itemDblClickedStopPropagation = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
itemDblClicked?.();
|
||||
};
|
||||
|
||||
const optionClickedStopPropagation = (e: Event, option: MenuOption) => {
|
||||
e.stopPropagation();
|
||||
optionClicked?.(option);
|
||||
};
|
||||
|
||||
const actionClickedStopPropagation = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
actionClicked?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classes({
|
||||
|
@ -90,10 +61,10 @@ export const AppMenuItemView: FunctionComponent<{
|
|||
'menu__item--collapsed': !expanded,
|
||||
...(cls ? { [cls]: true } : null)
|
||||
})}
|
||||
onMouseOver={(e) => mouseOver(e)}
|
||||
onMouseOut={(e) => mouseOut(e)}
|
||||
onClick={itemClickedStopPropagation}
|
||||
onDblClick={itemDblClickedStopPropagation}
|
||||
onMouseOver={withoutPropagation(setHover, true)}
|
||||
onMouseOut={withoutPropagation(setHover, false)}
|
||||
onClick={withoutPropagation(itemClicked)}
|
||||
onDblClick={withoutPropagation(itemDblClicked)}
|
||||
>
|
||||
{collapsible ? (
|
||||
<i class="menu__item-collapse fa fa-ellipsis-v muted-color">
|
||||
|
@ -123,7 +94,7 @@ export const AppMenuItemView: FunctionComponent<{
|
|||
<div
|
||||
class={`menu__item-option ${opt.cls || ''}`}
|
||||
key={opt.value}
|
||||
onClick={(e) => optionClickedStopPropagation(e, opt)}
|
||||
onClick={withoutPropagation(optionClicked, opt)}
|
||||
>
|
||||
{opt.title}
|
||||
</div>
|
||||
|
@ -131,13 +102,16 @@ export const AppMenuItemView: FunctionComponent<{
|
|||
</div>
|
||||
) : null}
|
||||
{editable ? (
|
||||
<i class="menu__item-edit fa fa-cog" onClick={actionClickedStopPropagation} />
|
||||
<i
|
||||
class="menu__item-edit fa fa-cog"
|
||||
onClick={withoutPropagation(actionClicked)}
|
||||
/>
|
||||
) : null}
|
||||
{isTrash ? (
|
||||
<i
|
||||
class="menu__item-empty-trash fa fa-minus-circle"
|
||||
tip-placement="right"
|
||||
onClick={actionClickedStopPropagation}
|
||||
onClick={withoutPropagation(actionClicked)}
|
||||
>
|
||||
<kw-tip text={Locale.menuEmptyTrash} />
|
||||
</i>
|
||||
|
|
|
@ -14,7 +14,7 @@ export const AppMenuSectionView: FunctionComponent<{
|
|||
items: MenuItem[];
|
||||
height?: number;
|
||||
}> = ({ id, scrollable, grow, drag, items, height }) => {
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const menu = useRef<HTMLDivElement>();
|
||||
const menuItems = items.map((item) => <AppMenuItem item={item} key={item.id} />);
|
||||
|
||||
return (
|
||||
|
@ -27,14 +27,14 @@ export const AppMenuSectionView: FunctionComponent<{
|
|||
'menu__section--drag': drag
|
||||
})}
|
||||
style={{ height }}
|
||||
ref={menuRef}
|
||||
ref={menu}
|
||||
>
|
||||
{scrollable ? <Scrollable>{menuItems}</Scrollable> : menuItems}
|
||||
</div>
|
||||
{drag ? (
|
||||
<div class="menu__drag-section">
|
||||
<DragHandle
|
||||
target={menuRef}
|
||||
target={menu}
|
||||
coord="y"
|
||||
name={`menu-section:${id}`}
|
||||
min={55}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Fragment, FunctionComponent } from 'preact';
|
||||
import { classes } from 'util/ui/classes';
|
||||
import { withoutPropagation } from 'util/ui/events';
|
||||
|
||||
export interface ModalViewButton {
|
||||
title: string;
|
||||
|
@ -43,11 +44,6 @@ export const ModalView: FunctionComponent<ModalViewProps> = ({
|
|||
buttonClicked,
|
||||
checkboxChanged
|
||||
}) => {
|
||||
const buttonClickedWithoutPropagation = (e: MouseEvent, result: string) => {
|
||||
e.stopPropagation();
|
||||
buttonClicked(result);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classes({
|
||||
|
@ -90,7 +86,7 @@ export const ModalView: FunctionComponent<ModalViewProps> = ({
|
|||
'btn-error': btn.error || !btn.result,
|
||||
'btn-silent': btn.silent
|
||||
})}
|
||||
onClick={(e) => buttonClickedWithoutPropagation(e, btn.result)}
|
||||
onClick={withoutPropagation(buttonClicked, btn.result)}
|
||||
data-result={btn.result}
|
||||
>
|
||||
{btn.title}
|
||||
|
|
|
@ -19,9 +19,11 @@ export const OpenButtonsView: FunctionComponent<{
|
|||
showSettings: boolean;
|
||||
storageProviders: StorageProvider[];
|
||||
|
||||
openClicked: () => void;
|
||||
moreClicked: () => void;
|
||||
newClicked: () => void;
|
||||
openDemoClicked: () => void;
|
||||
settingsClicked: () => void;
|
||||
}> = ({
|
||||
secondRowVisible,
|
||||
showOpen,
|
||||
|
@ -35,17 +37,23 @@ export const OpenButtonsView: FunctionComponent<{
|
|||
showSettings,
|
||||
storageProviders,
|
||||
|
||||
openClicked,
|
||||
moreClicked,
|
||||
newClicked,
|
||||
openDemoClicked
|
||||
openDemoClicked,
|
||||
settingsClicked
|
||||
}) => {
|
||||
let tabIndex = 0;
|
||||
let tabIndex = 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="open__icons">
|
||||
{showOpen ? (
|
||||
<div class="open__icon open__icon-open" tabIndex={++tabIndex}>
|
||||
<div
|
||||
class="open__icon open__icon-open"
|
||||
tabIndex={++tabIndex}
|
||||
onClick={openClicked}
|
||||
>
|
||||
<i class="fa fa-lock open__icon-i" />
|
||||
<div class="open__icon-text">{Locale.openOpen}</div>
|
||||
</div>
|
||||
|
@ -122,7 +130,11 @@ export const OpenButtonsView: FunctionComponent<{
|
|||
</div>
|
||||
) : null}
|
||||
{showSettings ? (
|
||||
<div class="open__icon open__icon-settings" tabIndex={++tabIndex}>
|
||||
<div
|
||||
class="open__icon open__icon-settings"
|
||||
tabIndex={++tabIndex}
|
||||
onClick={settingsClicked}
|
||||
>
|
||||
<i class="fa fa-cog open__icon-i" />
|
||||
<div class="open__icon-text">{Locale.settings}</div>
|
||||
</div>
|
||||
|
|
|
@ -10,13 +10,20 @@ interface LastOpenFile {
|
|||
export const OpenLastFilesView: FunctionComponent<{
|
||||
lastOpenFiles: LastOpenFile[];
|
||||
canRemoveLatest: boolean;
|
||||
}> = ({ lastOpenFiles, canRemoveLatest }) => {
|
||||
let tabIndex = 0;
|
||||
|
||||
lastFileSelected: (id: string) => void;
|
||||
}> = ({ lastOpenFiles, canRemoveLatest, lastFileSelected }) => {
|
||||
let tabIndex = 400;
|
||||
|
||||
return (
|
||||
<div class="open__last">
|
||||
{lastOpenFiles.map((file) => (
|
||||
<div key={file.id} class="open__last-item" tabIndex={++tabIndex}>
|
||||
<div
|
||||
key={file.id}
|
||||
class="open__last-item"
|
||||
tabIndex={++tabIndex}
|
||||
onClick={() => lastFileSelected(file.id)}
|
||||
>
|
||||
{file.path ? <kw-tip text={file.path} /> : null}
|
||||
{file.icon ? <i class={`fa fa-${file.icon} open__last-item-icon`} /> : null}
|
||||
<span class="open__last-item-text">{file.name}</span>
|
||||
|
|
|
@ -2,21 +2,51 @@ import * as kdbxweb from 'kdbxweb';
|
|||
import { FunctionComponent } from 'preact';
|
||||
import { Locale } from 'util/locale';
|
||||
import { SecureInput, SecureInputEvent } from 'views/components/secure-input';
|
||||
import { useRef } from 'preact/hooks';
|
||||
import { useLayoutEffect, useRef } from 'preact/hooks';
|
||||
import { useEvent } from 'util/ui/hooks';
|
||||
import { classes } from 'util/ui/classes';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
export const OpenPasswordView: FunctionComponent<{
|
||||
password?: kdbxweb.ProtectedValue;
|
||||
canOpen: boolean;
|
||||
passwordReadOnly: boolean;
|
||||
passwordPlaceholder: string;
|
||||
autoFocusPassword: boolean;
|
||||
buttonFingerprint: boolean;
|
||||
capsLockPressed: boolean;
|
||||
|
||||
passwordClicked?: () => void;
|
||||
passwordChanged?: (password: kdbxweb.ProtectedValue) => void;
|
||||
}> = ({ password, canOpen, passwordChanged }) => {
|
||||
let tabIndex = 0;
|
||||
passwordKeyDown?: (e: KeyboardEvent) => void;
|
||||
passwordKeyUp?: (e: KeyboardEvent) => void;
|
||||
passwordKeyPress?: (e: KeyboardEvent) => void;
|
||||
}> = ({
|
||||
password,
|
||||
passwordReadOnly,
|
||||
passwordPlaceholder,
|
||||
passwordChanged,
|
||||
buttonFingerprint,
|
||||
capsLockPressed,
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
autoFocusPassword,
|
||||
passwordClicked,
|
||||
passwordKeyDown,
|
||||
passwordKeyUp,
|
||||
passwordKeyPress
|
||||
}) => {
|
||||
let tabIndex = 200;
|
||||
|
||||
const passwordInput = useRef<HTMLInputElement>();
|
||||
|
||||
useEvent('main-window-focus', () => {
|
||||
passwordInputRef.current?.focus();
|
||||
passwordInput.current?.focus();
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (autoFocusPassword && !Features.isMobile) {
|
||||
passwordInput.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const passwordInputChanged = (e: SecureInputEvent) => {
|
||||
|
@ -25,32 +55,44 @@ export const OpenPasswordView: FunctionComponent<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<input type="file" class="open__file-ctrl hide-by-pos" />
|
||||
<div class="hide">
|
||||
{/* we need these inputs to screw browsers passwords autocompletion */}
|
||||
<input type="text" name="username" />
|
||||
<input type="password" name="password" />
|
||||
</div>
|
||||
<div class="open__pass-warn-wrap">
|
||||
<div class="open__pass-warning muted-color invisible">
|
||||
<div
|
||||
class={classes({
|
||||
'open__pass-warning': true,
|
||||
'muted-color': true,
|
||||
'invisible': !capsLockPressed
|
||||
})}
|
||||
>
|
||||
<i class="fa fa-exclamation-triangle" /> {Locale.openCaps}
|
||||
</div>
|
||||
</div>
|
||||
<div class="open__pass-field-wrap">
|
||||
<SecureInput
|
||||
value={password}
|
||||
onInput={passwordInputChanged}
|
||||
inputClass="open__pass-input"
|
||||
name="password"
|
||||
size={30}
|
||||
placeholder={canOpen ? Locale.openClickToOpen : ''}
|
||||
// readonly={true}
|
||||
placeholder={passwordPlaceholder}
|
||||
readonly={passwordReadOnly}
|
||||
tabIndex={++tabIndex}
|
||||
inputRef={passwordInputRef}
|
||||
inputRef={passwordInput}
|
||||
onInput={passwordInputChanged}
|
||||
onClick={passwordReadOnly ? passwordClicked : undefined}
|
||||
onKeyUp={passwordKeyUp}
|
||||
onKeyDown={passwordKeyDown}
|
||||
onKeyPress={passwordKeyPress}
|
||||
/>
|
||||
<div class="open__pass-enter-btn" tabIndex={++tabIndex}>
|
||||
<i class="fa fa-level-down-alt rotate-90 open__pass-enter-btn-icon-enter" />
|
||||
<i class="fa fa-fingerprint open__pass-enter-btn-icon-touch-id" />
|
||||
{buttonFingerprint ? (
|
||||
<i class="fa fa-fingerprint open__pass-enter-btn-icon-touch-id" />
|
||||
) : (
|
||||
<i class="fa fa-level-down-alt rotate-90 open__pass-enter-btn-icon-enter" />
|
||||
)}
|
||||
</div>
|
||||
<div class="open__pass-opening-icon">
|
||||
<i class="fa fa-spinner spin" />
|
||||
|
|
|
@ -6,10 +6,38 @@ import { OpenStorageConfig } from 'ui/open/open-storage-config';
|
|||
import { OpenPassword } from 'ui/open/open-password';
|
||||
import { OpenLastFiles } from 'ui/open/open-last-files';
|
||||
import { OpenSettings } from 'ui/open/open-settings';
|
||||
import { classes } from 'util/ui/classes';
|
||||
import { useKey } from 'util/ui/hooks';
|
||||
import { Keys } from 'const/keys';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
export const OpenScreenView: FunctionComponent<{
|
||||
fileSelected: boolean;
|
||||
keyFileSelected: boolean;
|
||||
visualFocus: boolean;
|
||||
}> = ({ fileSelected, keyFileSelected, visualFocus }) => {
|
||||
const openEl = useRef<HTMLDivElement>();
|
||||
|
||||
const enterKeyPress = () => {
|
||||
const el = openEl.current.querySelector('[tabindex]:focus');
|
||||
if (el instanceof HTMLDivElement) {
|
||||
el.click();
|
||||
}
|
||||
};
|
||||
|
||||
useKey(Keys.DOM_VK_ENTER, enterKeyPress, undefined, 'open');
|
||||
useKey(Keys.DOM_VK_RETURN, enterKeyPress, undefined, 'open');
|
||||
|
||||
export const OpenScreenView: FunctionComponent = () => {
|
||||
return (
|
||||
<div class="open">
|
||||
<div
|
||||
ref={openEl}
|
||||
class={classes({
|
||||
'open': true,
|
||||
'open--file': fileSelected,
|
||||
'open--key-file': keyFileSelected,
|
||||
'open--show-focus': visualFocus
|
||||
})}
|
||||
>
|
||||
<OpenUnlockMessage />
|
||||
<OpenButtons />
|
||||
|
||||
|
|
|
@ -1,24 +1,50 @@
|
|||
import { FunctionComponent } from 'preact';
|
||||
import { Locale } from 'util/locale';
|
||||
import { classes } from 'util/ui/classes';
|
||||
import { withoutPropagation } from 'util/ui/events';
|
||||
|
||||
export const OpenSettingsView: FunctionComponent<{
|
||||
canSelectKeyFile: boolean;
|
||||
canOpenKeyFromDropbox: boolean;
|
||||
canUseChalRespYubiKey: boolean;
|
||||
}> = ({ canOpenKeyFromDropbox, canUseChalRespYubiKey }) => {
|
||||
let tabIndex = 0;
|
||||
keyFileName?: string;
|
||||
|
||||
selectKeyFileClicked: () => void;
|
||||
selectKeyFileFromDropboxClicked: () => void;
|
||||
}> = ({
|
||||
canSelectKeyFile,
|
||||
canOpenKeyFromDropbox,
|
||||
canUseChalRespYubiKey,
|
||||
keyFileName,
|
||||
|
||||
selectKeyFileClicked,
|
||||
selectKeyFileFromDropboxClicked
|
||||
}) => {
|
||||
let tabIndex = 300;
|
||||
|
||||
return (
|
||||
<div class="open__settings">
|
||||
<div class="open__settings-key-file hide" tabIndex={++tabIndex}>
|
||||
<i class="fa fa-key open__settings-key-file-icon" />
|
||||
<span class="open__settings-key-file-name">{Locale.openKeyFile}</span>
|
||||
{canOpenKeyFromDropbox ? (
|
||||
<span class="open__settings-key-file-dropbox">
|
||||
{' '}
|
||||
{Locale.openKeyFileDropbox}
|
||||
{canSelectKeyFile ? (
|
||||
<div
|
||||
class="open__settings-key-file"
|
||||
tabIndex={++tabIndex}
|
||||
onClick={selectKeyFileClicked}
|
||||
>
|
||||
<i class="fa fa-key open__settings-key-file-icon" />
|
||||
<span class="open__settings-key-file-name">
|
||||
{keyFileName || Locale.openKeyFile}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{canOpenKeyFromDropbox ? (
|
||||
<span
|
||||
class="open__settings-key-file-dropbox"
|
||||
onClick={withoutPropagation(selectKeyFileFromDropboxClicked)}
|
||||
>
|
||||
{' '}
|
||||
{Locale.openKeyFileDropbox}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : undefined}
|
||||
<div
|
||||
class={classes({
|
||||
'open__settings-yubikey': true,
|
||||
|
|
Loading…
Reference in New Issue