context menu options for cipher listing

This commit is contained in:
Kyle Spearrin 2018-02-16 13:59:46 -05:00
parent 6c0148bb96
commit 34b3890647
8 changed files with 146 additions and 12 deletions

View File

@ -9,7 +9,8 @@
<ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers"> <ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers">
<div class="list" *ngIf="searchedCiphers.length > 0"> <div class="list" *ngIf="searchedCiphers.length > 0">
<a *ngFor="let c of searchedCiphers" appStopClick (click)="cipherClicked(c)" <a *ngFor="let c of searchedCiphers" appStopClick (click)="cipherClicked(c)"
href="#" title="{{'viewItem' | i18n}}" [ngClass]="{'active': c.id === activeCipherId}"> (contextmenu)="cipherRightClicked(c)" href="#" title="{{'viewItem' | i18n}}"
[ngClass]="{'active': c.id === activeCipherId}">
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="c"></app-vault-icon>
<span class="text"> <span class="text">
{{c.name}} {{c.name}}
@ -32,7 +33,8 @@
</ng-container> </ng-container>
</div> </div>
<div class="footer"> <div class="footer">
<button appBlurClick (click)="addCipher()" class="block primary" title="{{'addItem' | i18n}}"> <button appBlurClick (click)="addCipher()" (contextmenu)="addCipherOptions()"
class="block primary" title="{{'addItem' | i18n}}">
<i class="fa fa-plus fa-lg"></i> <i class="fa fa-plus fa-lg"></i>
</button> </button>
</div> </div>

View File

@ -18,7 +18,9 @@ import { CipherView } from 'jslib/models/view/cipherView';
export class CiphersComponent { export class CiphersComponent {
@Input() activeCipherId: string = null; @Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>(); @Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
@Output() onAddCipher = new EventEmitter(); @Output() onAddCipher = new EventEmitter();
@Output() onAddCipherOptions = new EventEmitter();
loaded: boolean = false; loaded: boolean = false;
ciphers: CipherView[] = []; ciphers: CipherView[] = [];
@ -51,7 +53,15 @@ export class CiphersComponent {
this.onCipherClicked.emit(cipher); this.onCipherClicked.emit(cipher);
} }
cipherRightClicked(cipher: CipherView) {
this.onCipherRightClicked.emit(cipher);
}
addCipher() { addCipher() {
this.onAddCipher.emit(); this.onAddCipher.emit();
} }
addCipherOptions() {
this.onAddCipherOptions.emit();
}
} }

View File

@ -11,7 +11,9 @@
<app-vault-ciphers id="items" <app-vault-ciphers id="items"
[activeCipherId]="cipherId" [activeCipherId]="cipherId"
(onCipherClicked)="viewCipher($event)" (onCipherClicked)="viewCipher($event)"
(onAddCipher)="addCipher($event)"> (onCipherRightClicked)="viewCipherMenu($event)"
(onAddCipher)="addCipher($event)"
(onAddCipherOptions)="addCipherOptions($event)">
</app-vault-ciphers> </app-vault-ciphers>
<app-vault-view id="details" <app-vault-view id="details"
*ngIf="cipherId && action === 'view'" *ngIf="cipherId && action === 'view'"

View File

@ -1,5 +1,7 @@
import * as template from './vault.component.html'; import * as template from './vault.component.html';
import { remote } from 'electron';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
@ -38,6 +40,7 @@ import { FolderView } from 'jslib/models/view/folderView';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service'; import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { SyncService } from 'jslib/abstractions/sync.service';
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
@ -69,8 +72,8 @@ export class VaultComponent implements OnInit, OnDestroy {
private componentFactoryResolver: ComponentFactoryResolver, private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, private i18nService: I18nService,
private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private syncService: SyncService, private analytics: Angulartics2, private ngZone: NgZone, private syncService: SyncService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService) { private toasterService: ToasterService, private messagingService: MessagingService,
} private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() { async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
@ -205,6 +208,74 @@ export class VaultComponent implements OnInit, OnDestroy {
this.go(); this.go();
} }
viewCipherMenu(cipher: CipherView) {
const menu = new remote.Menu();
menu.append(new remote.MenuItem({
label: this.i18nService.t('view'),
click: () => {
this.ngZone.run(async () => {
this.viewCipher(cipher);
this.changeDetectorRef.detectChanges();
});
},
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('edit'),
click: () => {
this.ngZone.run(async () => {
this.editCipher(cipher);
this.changeDetectorRef.detectChanges();
});
},
}));
switch (cipher.type) {
case CipherType.Login:
if (cipher.login.canLaunch || cipher.login.username != null || cipher.login.password != null) {
menu.append(new remote.MenuItem({ type: 'separator' }));
}
if (cipher.login.canLaunch) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('launch'),
click: () => this.platformUtilsService.launchUri(cipher.login.uri),
}));
}
if (cipher.login.username != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyUsername'),
click: () => this.platformUtilsService.copyToClipboard(cipher.login.username),
}));
}
if (cipher.login.password != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyPassword'),
click: () => this.platformUtilsService.copyToClipboard(cipher.login.password),
}));
}
break;
case CipherType.Card:
if (cipher.card.number != null || cipher.card.code != null) {
menu.append(new remote.MenuItem({ type: 'separator' }));
}
if (cipher.card.number != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyNumber'),
click: () => this.platformUtilsService.copyToClipboard(cipher.card.number),
}));
}
if (cipher.card.code != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copySecurityCode'),
click: () => this.platformUtilsService.copyToClipboard(cipher.card.code),
}));
}
break;
default:
break;
}
menu.popup(remote.getCurrentWindow());
}
editCipher(cipher: CipherView) { editCipher(cipher: CipherView) {
if (this.action === 'edit' && this.cipherId === cipher.id) { if (this.action === 'edit' && this.cipherId === cipher.id) {
return; return;
@ -226,6 +297,27 @@ export class VaultComponent implements OnInit, OnDestroy {
this.go(); this.go();
} }
addCipherOptions() {
const menu = new remote.Menu();
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeLogin'),
click: () => this.addCipherWithChangeDetection(CipherType.Login),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeCard'),
click: () => this.addCipherWithChangeDetection(CipherType.Card),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeIdentity'),
click: () => this.addCipherWithChangeDetection(CipherType.Identity),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeSecureNote'),
click: () => this.addCipherWithChangeDetection(CipherType.SecureNote),
}));
menu.popup(remote.getCurrentWindow());
}
async savedCipher(cipher: CipherView) { async savedCipher(cipher: CipherView) {
this.cipherId = cipher.id; this.cipherId = cipher.id;
this.action = 'view'; this.action = 'view';
@ -394,4 +486,11 @@ export class VaultComponent implements OnInit, OnDestroy {
const url = this.router.createUrlTree(['vault'], { queryParams: queryParams }).toString(); const url = this.router.createUrlTree(['vault'], { queryParams: queryParams }).toString();
this.location.go(url); this.location.go(url);
} }
private addCipherWithChangeDetection(type: CipherType = null) {
this.ngZone.run(async () => {
this.addCipher(type);
this.changeDetectorRef.detectChanges();
});
}
} }

View File

@ -34,7 +34,7 @@
{{cipher.login.username}} {{cipher.login.username}}
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}" <a class="row-btn" href="#" appStopClick title="{{'copyUsername' | i18n}}"
(click)="copy(cipher.login.username, 'Username')"> (click)="copy(cipher.login.username, 'Username')">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
@ -52,7 +52,7 @@
<i class="fa fa-lg" <i class="fa fa-lg"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i> [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a> </a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}" <a class="row-btn" href="#" appStopClick title="{{'copyPassword' | i18n}}"
(click)="copy(cipher.login.password, 'Password')"> (click)="copy(cipher.login.password, 'Password')">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
@ -94,7 +94,7 @@
{{cipher.card.number}} {{cipher.card.number}}
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}" <a class="row-btn" href="#" appStopClick title="{{'copyNumber' | i18n}}"
(click)="copy(cipher.card.number, 'Number')"> (click)="copy(cipher.card.number, 'Number')">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
@ -114,7 +114,7 @@
{{cipher.card.code}} {{cipher.card.code}}
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}" <a class="row-btn" href="#" appStopClick title="{{'copySecurityCode' | i18n}}"
(click)="copy(cipher.card.code, 'Security Code')"> (click)="copy(cipher.card.code, 'Security Code')">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>

View File

@ -78,7 +78,8 @@
"message": "Launch" "message": "Launch"
}, },
"copyValue": { "copyValue": {
"message": "Copy Value" "message": "Copy Value",
"description": "Copy value to clipboard"
}, },
"toggleVisibility": { "toggleVisibility": {
"message": "Toggle Visibility" "message": "Toggle Visibility"
@ -802,5 +803,16 @@
}, },
"unknown": { "unknown": {
"message": "Unknown" "message": "Unknown"
},
"copyUsername": {
"message": "Copy Username"
},
"copyNumber": {
"message": "Copy Number",
"description": "Copy credit card number"
},
"copySecurityCode": {
"message": "Copy Security Code",
"description": "Copy credit card security code (CVV)"
} }
} }

View File

@ -8,8 +8,8 @@ import { autoUpdater } from 'electron-updater';
import { Main } from '../main'; import { Main } from '../main';
import { import {
isDev,
isAppImage, isAppImage,
isDev,
} from '../scripts/utils'; } from '../scripts/utils';
const UpdaterCheckInitalDelay = 5 * 1000; // 5 seconds const UpdaterCheckInitalDelay = 5 * 1000; // 5 seconds

View File

@ -1,4 +1,8 @@
import { remote, shell } from 'electron'; import {
clipboard,
remote,
shell,
} from 'electron';
import { isDev } from '../scripts/utils'; import { isDev } from '../scripts/utils';
@ -149,4 +153,9 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService {
isDev(): boolean { isDev(): boolean {
return isDev(); return isDev();
} }
copyToClipboard(text: string, options?: any): void {
const type = options ? options.type : null;
clipboard.writeText(text, type);
}
} }