mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 03:03:43 +00:00
move new app into popup folder
This commit is contained in:
304
src/popup/vault/add-edit.component.html
Normal file
304
src/popup/vault/add-edit.component.html
Normal file
@@ -0,0 +1,304 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" appBlurClick (click)="cancel()">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
<div class="center">
|
||||
<span class="title">{{title}}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="submit" appBlurClick [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{'save' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-spin" [hidden]="!form.loading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<content *ngIf="cipher">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
{{'itemInformation' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
|
||||
<label for="type">{{'type' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type">
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" type="text" name="Name" [(ngModel)]="cipher.name">
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="loginUsername">{{'username' | i18n}}</label>
|
||||
<input id="loginUsername" type="text" name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username">
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginPassword">{{'password' | i18n}}</label>
|
||||
<input id="loginPassword" class="monospaced"
|
||||
type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password">
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button type="button" #checkPasswordBtn class="row-btn btn" appBlurClick
|
||||
title="{{'checkPassword' | i18n}}" (click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise" [disabled]="checkPasswordBtn.loading">
|
||||
<i class="fa fa-lg fa-check-circle" [hidden]="checkPasswordBtn.loading"></i>
|
||||
<i class="fa fa-lg fa-spinner fa-spin" [hidden]="!checkPasswordBtn.loading"></i>
|
||||
</button>
|
||||
<a class="row-btn" href="#" appStopClick appBlurClick
|
||||
title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</a>
|
||||
<a class="row-btn" href="#" appStopClick appBlurClick
|
||||
title="{{'generatePassword' | i18n}}" (click)="generatePassword()">
|
||||
<i class="fa fa-lg fa-refresh"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
|
||||
<input id="loginTotp" type="text" name="Login.Totp" class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardCardholderName">{{'cardholderName' | i18n}}</label>
|
||||
<input id="cardCardholderName" type="text" name="Card.CardCardholderName"
|
||||
[(ngModel)]="cipher.card.cardholderName">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardNumber">{{'number' | i18n}}</label>
|
||||
<input id="cardNumber" type="text" name="Card.Number" [(ngModel)]="cipher.card.number">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardBrand">{{'brand' | i18n}}</label>
|
||||
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpMonth">{{'expirationMonth' | i18n}}</label>
|
||||
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpYear">{{'expirationYear' | i18n}}</label>
|
||||
<input id="cardExpYear" type="text" name="Card.ExpYear" [(ngModel)]="cipher.card.expYear"
|
||||
placeholder="{{'ex' | i18n}} 2019">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardCode">{{'securityCode' | i18n}}</label>
|
||||
<input id="cardCode" type="text" name="Card.Code" [(ngModel)]="cipher.card.code">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.type === cipherType.Identity">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idTitle">{{'title' | i18n}}</label>
|
||||
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idFirstName">{{'firstName' | i18n}}</label>
|
||||
<input id="idFirstName" type="text" name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idMiddleName">{{'middleName' | i18n}}</label>
|
||||
<input id="idMiddleName" type="text" name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLastName">{{'lastName' | i18n}}</label>
|
||||
<input id="idLastName" type="text" name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idUsername">{{'username' | i18n}}</label>
|
||||
<input id="idUsername" type="text" name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCompany">{{'company' | i18n}}</label>
|
||||
<input id="idCompany" type="text" name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idSsn">{{'ssn' | i18n}}</label>
|
||||
<input id="idSsn" type="text" name="Identity.SSN" [(ngModel)]="cipher.identity.ssn">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPassportNumber">{{'passportNumber' | i18n}}</label>
|
||||
<input id="idPassportNumber" type="text" name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLicenseNumber">{{'licenseNumber' | i18n}}</label>
|
||||
<input id="idLicenseNumber" type="text" name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idEmail">{{'email' | i18n}}</label>
|
||||
<input id="idEmail" type="text" name="Identity.Email" [(ngModel)]="cipher.identity.email">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPhone">{{'phone' | i18n}}</label>
|
||||
<input id="idPhone" type="text" name="Identity.Phone" [(ngModel)]="cipher.identity.phone">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress1">{{'address1' | i18n}}</label>
|
||||
<input id="idAddress1" type="text" name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress2">{{'address2' | i18n}}</label>
|
||||
<input id="idAddress2" type="text" name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress3">{{'address3' | i18n}}</label>
|
||||
<input id="idAddress3" type="text" name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCity">{{'cityTown' | i18n}}</label>
|
||||
<input id="idCity" type="text" name="Identity.City" [(ngModel)]="cipher.identity.city">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idState">{{'stateProvince' | i18n}}</label>
|
||||
<input id="idState" type="text" name="Identity.State" [(ngModel)]="cipher.identity.state">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPostalCode">{{'zipPostalCode' | i18n}}</label>
|
||||
<input id="idPostalCode" type="text" name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode">
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCountry">{{'country' | i18n}}</label>
|
||||
<input id="idCountry" type="text" name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content">
|
||||
<ng-container *ngIf="cipher.login.hasUris">
|
||||
<div class="box-content-row box-content-row-multi" appBoxRow
|
||||
*ngFor="let u of cipher.login.uris; let i = index">
|
||||
<a href="#" appStopClick (click)="removeUri(u)" title="{{'remove' | i18n}}">
|
||||
<i class="fa fa-minus-circle fa-lg"></i>
|
||||
</a>
|
||||
<div class="row-main">
|
||||
<label for="loginUri{{i}}">{{'uriPosition' | i18n : (i + 1)}}</label>
|
||||
<input id="loginUri{{i}}" type="text" name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri"
|
||||
placeholder="{{'ex' | i18n}} https://google.com">
|
||||
<label for="loginUriMatch{{i}}" class="sr-only">
|
||||
{{'matchDetection' | i18n}} {{(i + 1)}}
|
||||
</label>
|
||||
<select id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match" [(ngModel)]="u.match"
|
||||
[hidden]="u.showOptions === false || (u.showOptions == null && u.match == null)"
|
||||
(change)="loginUriMatchChanged(u)">
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick appBlurClick
|
||||
title="{{'toggleOptions' | i18n}}" (click)="toggleUriOptions(u)">
|
||||
<i class="fa fa-lg fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<a href="#" appStopClick appBlurClick (click)="addUri()"
|
||||
class="box-content-row box-content-row-newmulti">
|
||||
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newUri' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="folder">{{'folder' | i18n}}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
|
||||
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favorite">{{'favorite' | i18n}}</label>
|
||||
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite">
|
||||
</div>
|
||||
<a class="box-content-row box-content-row-flex text-default" href="#" appStopClick appBlurClick
|
||||
(click)="attachments()" *ngIf="editMode">
|
||||
<div class="row-main">{{'attachments' | i18n}}</div>
|
||||
<i class="fa fa-chevron-right row-sub-icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<label for="notes">{{'notes' | i18n}}</label>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
{{'customFields' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<ng-container *ngIf="cipher.hasFields">
|
||||
<div class="box-content-row box-content-row-multi" appBoxRow
|
||||
*ngFor="let f of cipher.fields; let i = index"
|
||||
[ngClass]="{'box-content-row-checkbox': f.type === fieldType.Boolean}">
|
||||
<a href="#" appStopClick (click)="removeField(f)" title="{{'remove' | i18n}}">
|
||||
<i class="fa fa-minus-circle fa-lg"></i>
|
||||
</a>
|
||||
<label for="fieldName{{i}}" class="sr-only">{{'name' | i18n}}</label>
|
||||
<label for="fieldValue{{i}}" class="sr-only">{{'value' | i18n}}</label>
|
||||
<div class="row-main">
|
||||
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
|
||||
class="row-label" placeholder="{{'name' | i18n}}">
|
||||
<input id="fieldValue{{i}}" type="text" name="Field.Value{{i}}" [(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Text" placeholder="{{'value' | i18n}}">
|
||||
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}"
|
||||
name="Field.Value{{i}}" [(ngModel)]="f.value" class="monospaced"
|
||||
*ngIf="f.type === fieldType.Hidden" placeholder="{{'value' | i18n}}">
|
||||
</div>
|
||||
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
|
||||
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean">
|
||||
<div class="action-buttons" *ngIf="f.type === fieldType.Hidden">
|
||||
<a class="row-btn" href="#" appStopClick appBlurClick
|
||||
title="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="box-content-row box-content-row-newmulti" appBoxRow>
|
||||
<a href="#" appStopClick (click)="addField()">
|
||||
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}}
|
||||
</a>
|
||||
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
|
||||
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
|
||||
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</content>
|
||||
</form>
|
||||
98
src/popup/vault/add-edit.component.ts
Normal file
98
src/popup/vault/add-edit.component.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/add-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-add-edit',
|
||||
templateUrl: 'add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
constructor(cipherService: CipherService, folderService: FolderService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
auditService: AuditService, stateService: StateService,
|
||||
private route: ActivatedRoute, private router: Router,
|
||||
private location: Location) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
|
||||
toasterService, auditService, stateService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
if (params.folderId) {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
if (params.type) {
|
||||
const type = parseInt(params.type, null);
|
||||
this.type = type;
|
||||
}
|
||||
this.editMode = !params.cipherId;
|
||||
await this.load();
|
||||
|
||||
if (!this.editMode) {
|
||||
if (params.name) {
|
||||
this.cipher.name = params.name;
|
||||
}
|
||||
if (params.uri) {
|
||||
this.cipher.login.uris[0].uri = params.uri;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.editMode) {
|
||||
if (this.cipher.name != null && this.cipher.name !== '') {
|
||||
document.getElementById('loginUsername').focus();
|
||||
} else {
|
||||
document.getElementById('name').focus();
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (await super.submit()) {
|
||||
this.location.back();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
super.cancel();
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
async generatePassword(): Promise<boolean> {
|
||||
const confirmed = await super.generatePassword();
|
||||
if (confirmed) {
|
||||
this.stateService.save('addEditCipher', this.cipher);
|
||||
this.router.navigate(['generator']);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
41
src/popup/vault/ciphers.component.html
Normal file
41
src/popup/vault/ciphers.component.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" appBlurClick (click)="back()">
|
||||
<span class="header-icon"><i class="fa fa-chevron-left"></i></span>
|
||||
<span>{{'back' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<input type="search" placeholder="{{searchPlaceholder || ('searchVault' | i18n)}}" id="search"
|
||||
[(ngModel)]="searchText" appAutofocus>
|
||||
<i class="fa fa-search"></i>
|
||||
</div>
|
||||
<div class="right" *ngIf="showAdd">
|
||||
<button type="button" appBlurClick (click)="addCipher()" title="{{'addItem' | i18n}}">
|
||||
<i class="fa fa-plus fa-lg fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<content>
|
||||
<ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers">
|
||||
<div class="no-items" *ngIf="!searchedCiphers.length">
|
||||
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p>{{'noItemsInList' | i18n}}</p>
|
||||
<button (click)="addCipher()" class="btn block primary link" *ngIf="showAdd">
|
||||
{{'addItem' | i18n}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="box list only-list" *ngIf="searchedCiphers.length > 0">
|
||||
<div class="box-header">
|
||||
{{groupingTitle}}
|
||||
<span class="flex-right">{{searchedCiphers.length}}</span>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="searchedCiphers" title="{{'viewItem' | i18n}}"
|
||||
(onSelected)="selectCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</content>
|
||||
150
src/popup/vault/ciphers.component.ts
Normal file
150
src/popup/vault/ciphers.component.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { CiphersComponent as BaseCiphersComponent } from 'jslib/angular/components/ciphers.component';
|
||||
|
||||
import { PopupUtilsService } from '../services/popup-utils.service';
|
||||
|
||||
const ComponentId = 'CiphersComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-ciphers',
|
||||
templateUrl: 'ciphers.component.html',
|
||||
})
|
||||
export class CiphersComponent extends BaseCiphersComponent implements OnInit, OnDestroy {
|
||||
groupingTitle: string;
|
||||
searchText: string;
|
||||
state: any;
|
||||
showAdd = true;
|
||||
folderId: string = null;
|
||||
type: CipherType = null;
|
||||
|
||||
constructor(cipherService: CipherService, private route: ActivatedRoute,
|
||||
private router: Router, private location: Location,
|
||||
private ngZone: NgZone, private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef, private stateService: StateService,
|
||||
private popupUtils: PopupUtilsService, private i18nService: I18nService,
|
||||
private folderService: FolderService, private collectionService: CollectionService) {
|
||||
super(cipherService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.type) {
|
||||
this.searchPlaceholder = this.i18nService.t('searchType');
|
||||
this.type = parseInt(params.type, null);
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.groupingTitle = this.i18nService.t('logins');
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.groupingTitle = this.i18nService.t('cards');
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.groupingTitle = this.i18nService.t('identities');
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.groupingTitle = this.i18nService.t('secureNotes');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
await super.load((c) => c.type === this.type);
|
||||
} else if (params.folderId) {
|
||||
this.folderId = params.folderId === 'none' ? null : params.folderId;
|
||||
this.searchPlaceholder = this.i18nService.t('searchFolder');
|
||||
if (this.folderId != null) {
|
||||
const folder = await this.folderService.get(this.folderId);
|
||||
if (folder != null) {
|
||||
this.groupingTitle = (await folder.decrypt()).name;
|
||||
}
|
||||
} else {
|
||||
this.groupingTitle = this.i18nService.t('noneFolder');
|
||||
}
|
||||
await super.load((c) => c.folderId === this.folderId);
|
||||
} else if (params.collectionId) {
|
||||
this.showAdd = false;
|
||||
this.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
const collection = await this.collectionService.get(params.collectionId);
|
||||
if (collection != null) {
|
||||
this.groupingTitle = (await collection.decrypt()).name;
|
||||
}
|
||||
await super.load((c) => c.collectionIds != null && c.collectionIds.indexOf(params.collectionId) > -1);
|
||||
} else {
|
||||
this.groupingTitle = this.i18nService.t('allItems');
|
||||
await super.load();
|
||||
}
|
||||
|
||||
this.state = (await this.stateService.get<any>(ComponentId)) || {};
|
||||
if (this.state.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
}
|
||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0);
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'syncCompleted':
|
||||
window.setTimeout(() => {
|
||||
this.load();
|
||||
}, 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
super.selectCipher(cipher);
|
||||
this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } });
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
super.addCipher();
|
||||
this.router.navigate(['/add-cipher'], { queryParams: { folderId: this.folderId, type: this.type } });
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: this.popupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.save(ComponentId, this.state);
|
||||
}
|
||||
}
|
||||
62
src/popup/vault/current-tab.component.html
Normal file
62
src/popup/vault/current-tab.component.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<app-pop-out *ngIf="showPopout"></app-pop-out>
|
||||
<button type="button" appBlurClick (click)="refresh()" title="{{'refresh' | i18n}}" *ngIf="inSidebar">
|
||||
<i class="fa fa-refresh fa-lg fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<input type="search" placeholder="{{'searchVault' | i18n}}" id="search"
|
||||
[(ngModel)]="searchText" (input)="searchVault()" appAutofocus>
|
||||
<i class="fa fa-search"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" appBlurClick (click)="addCipher()" title="{{'addItem' | i18n}}">
|
||||
<i class="fa fa-plus fa-lg fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<content>
|
||||
<div class="no-items" *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="loaded">
|
||||
<div class="box list" *ngIf="loginCiphers">
|
||||
<div class="box-header">
|
||||
{{'typeLogins' | i18n}}
|
||||
<span class="flex-right">{{loginCiphers.length}}</span>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="loginCiphers" title="{{'autoFill' | i18n}}" [showView]="true"
|
||||
(onSelected)="fillCipher($event)" (onView)="viewCipher($event)"
|
||||
*ngIf="loginCiphers.length"></app-ciphers-list>
|
||||
<div class="box-content-row text-center padded no-hover" *ngIf="!loginCiphers.length">
|
||||
<p>{{'autoFillInfo' | i18n}}</p>
|
||||
<button type="button" class="btn primary link block" (click)="addCipher()">
|
||||
{{'addLogin' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="cardCiphers && cardCiphers.length">
|
||||
<div class="box-header">
|
||||
{{'cards' | i18n}}
|
||||
<span class="flex-right">{{cardCiphers.length}}</span>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="cardCiphers" title="{{'autoFill' | i18n}}" [showView]="true"
|
||||
(onSelected)="fillCipher($event)" (onView)="viewCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="identityCiphers && identityCiphers.length">
|
||||
<div class="box-header">
|
||||
{{'identities' | i18n}}
|
||||
<span class="flex-right">{{identityCiphers.length}}</span>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="identityCiphers" title="{{'autoFill' | i18n}}" [showView]="true"
|
||||
(onSelected)="fillCipher($event)" (onView)="viewCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</content>
|
||||
188
src/popup/vault/current-tab.component.ts
Normal file
188
src/popup/vault/current-tab.component.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { BrowserApi } from '../../browser/browserApi';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { AutofillService } from '../../services/abstractions/autofill.service';
|
||||
|
||||
import { PopupUtilsService } from '../services/popup-utils.service';
|
||||
import { setTimeout } from 'timers';
|
||||
|
||||
const BroadcasterSubscriptionId = 'CurrentTabComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-current-tab',
|
||||
templateUrl: 'current-tab.component.html',
|
||||
})
|
||||
export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
pageDetails: any[] = [];
|
||||
cardCiphers: CipherView[];
|
||||
identityCiphers: CipherView[];
|
||||
loginCiphers: CipherView[];
|
||||
url: string;
|
||||
domain: string;
|
||||
searchText: string;
|
||||
canAutofill = false;
|
||||
inSidebar = false;
|
||||
showPopout = true;
|
||||
disableSearch = false;
|
||||
loaded = false;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService, private cipherService: CipherService,
|
||||
private popupUtilsService: PopupUtilsService, private autofillService: AutofillService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private router: Router,
|
||||
private ngZone: NgZone, private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef) {
|
||||
this.inSidebar = popupUtilsService.inSidebar(window);
|
||||
this.showPopout = !this.inSidebar && !platformUtilsService.isSafari();
|
||||
this.disableSearch = platformUtilsService.isEdge();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'syncCompleted':
|
||||
if (this.loaded) {
|
||||
setTimeout(() => {
|
||||
this.load();
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
case 'collectPageDetailsResponse':
|
||||
if (message.sender === BroadcasterSubscriptionId) {
|
||||
this.pageDetails.push({
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
});
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
this.router.navigate(['/add-cipher'], { queryParams: { name: this.domain, uri: this.url } });
|
||||
}
|
||||
|
||||
viewCipher(cipher: CipherView) {
|
||||
this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } });
|
||||
}
|
||||
|
||||
async fillCipher(cipher: CipherView) {
|
||||
if (!this.canAutofill) {
|
||||
this.analytics.eventTrack.next({ action: 'Autofilled Error' });
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('autofillError'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const totpCode = await this.autofillService.doAutoFill({
|
||||
cipher: cipher,
|
||||
pageDetails: this.pageDetails,
|
||||
fromBackground: false,
|
||||
doc: window.document,
|
||||
});
|
||||
|
||||
this.analytics.eventTrack.next({ action: 'Autofilled' });
|
||||
if (totpCode != null && this.platformUtilsService.isFirefox()) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { doc: window.document });
|
||||
}
|
||||
|
||||
if (this.popupUtilsService.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
} catch {
|
||||
this.analytics.eventTrack.next({ action: 'Autofilled Error' });
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('autofillError'));
|
||||
}
|
||||
}
|
||||
|
||||
searchVault() {
|
||||
this.router.navigate(['/tabs/vault'], { queryParams: { searchText: this.searchText } });
|
||||
}
|
||||
|
||||
private async load() {
|
||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (tab) {
|
||||
this.url = tab.url;
|
||||
} else {
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.domain = this.platformUtilsService.getDomain(this.url);
|
||||
|
||||
BrowserApi.tabSendMessage(tab, {
|
||||
command: 'collectPageDetails',
|
||||
tab: tab,
|
||||
sender: BroadcasterSubscriptionId,
|
||||
}).then(() => {
|
||||
this.canAutofill = true;
|
||||
});
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(this.url, [
|
||||
CipherType.Card,
|
||||
CipherType.Identity,
|
||||
]);
|
||||
|
||||
this.loginCiphers = [];
|
||||
this.cardCiphers = [];
|
||||
this.identityCiphers = [];
|
||||
|
||||
ciphers.forEach((c) => {
|
||||
switch (c.type) {
|
||||
case CipherType.Login:
|
||||
this.loginCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.cardCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identityCiphers.push(c);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
}
|
||||
140
src/popup/vault/groupings.component.html
Normal file
140
src/popup/vault/groupings.component.html
Normal file
@@ -0,0 +1,140 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
<div class="search">
|
||||
<input type="search" placeholder="{{'searchVault' | i18n}}" id="search"
|
||||
[(ngModel)]="searchText" appAutofocus>
|
||||
<i class="fa fa-search"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button appBlurClick (click)="addCipher()" title="{{'addItem' | i18n}}">
|
||||
<i class="fa fa-plus fa-lg fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<content>
|
||||
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
|
||||
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<i class="fa fa-frown-o fa-4x"></i>
|
||||
<p>{{'noItemsInList' | i18n}}</p>
|
||||
<button (click)="addCipher()" class="btn block primary link">{{'addItem' | i18n}}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="ciphers && ciphers.length && (!searchText || searchText.length < 2)">
|
||||
<div class="box list" *ngIf="favoriteCiphers">
|
||||
<div class="box-header">
|
||||
{{'favorites' | i18n}}
|
||||
<span class="flex-right">{{favoriteCiphers.length}}</span>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="favoriteCiphers" title="{{'viewItem' | i18n}}"
|
||||
(onSelected)="selectCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<div class="box-header">
|
||||
{{'types' | i18n}}
|
||||
<span class="flex-right">4</span>
|
||||
</div>
|
||||
<div class="box-content single-line">
|
||||
<a href="#" class="box-content-row" appStopClick appBlurClick
|
||||
(click)="selectType(cipherType.Login)">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="fa fa-fw fa-lg fa-globe"></i></div>
|
||||
<span class="text">{{'typeLogin' | i18n}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{typeCounts.get(cipherType.Login) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
<a href="#" class="box-content-row" appStopClick appBlurClick
|
||||
(click)="selectType(cipherType.Card)">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="fa fa-fw fa-lg fa-credit-card"></i></div>
|
||||
<span class="text">{{'typeCard' | i18n}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{typeCounts.get(cipherType.Card) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
<a href="#" class="box-content-row" appStopClick appBlurClick
|
||||
(click)="selectType(cipherType.Identity)">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="fa fa-fw fa-lg fa-id-card-o"></i></div>
|
||||
<span class="text">{{'typeIdentity' | i18n}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{typeCounts.get(cipherType.Identity) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
<a href="#" class="box-content-row" appStopClick appBlurClick
|
||||
(click)="selectType(cipherType.SecureNote)">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="fa fa-fw fa-lg fa-sticky-note-o"></i></div>
|
||||
<span class="text">{{'typeSecureNote' | i18n}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{typeCounts.get(cipherType.SecureNote) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<div class="box-header">
|
||||
{{'folders' | i18n}}
|
||||
<span class="flex-right">{{folderCount}}</span>
|
||||
</div>
|
||||
<div class="box-content single-line">
|
||||
<a *ngFor="let f of folders" href="#" class="box-content-row"
|
||||
appStopClick appBlurClick (click)="selectFolder(f)">
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="fa fa-fw fa-lg"
|
||||
[ngClass]="{'fa-folder-open': f.id, 'fa-folder-open-o': !f.id}"></i>
|
||||
</div>
|
||||
<span class="text">{{f.name}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{folderCounts.get(f.id) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<div class="box-header">
|
||||
{{'collections' | i18n}}
|
||||
<span class="flex-right">{{collections.length}}</span>
|
||||
</div>
|
||||
<div class="box-content single-line">
|
||||
<a *ngFor="let c of collections" href="#" class="box-content-row"
|
||||
appStopClick appBlurClick (click)="selectCollection(c)">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="fa fa-fw fa-lg fa-cube"></i></div>
|
||||
<span class="text">{{c.name}}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{collectionCounts.get(c.id) || 0}}</span>
|
||||
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="showNoFolderCiphers">
|
||||
<div class="box-header">
|
||||
{{'noneFolder' | i18n}}
|
||||
<div class="flex-right">{{noFolderCiphers.length}}</div>
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="noFolderCiphers" title="{{'viewItem' | i18n}}"
|
||||
(onSelected)="selectCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="ciphers && ciphers.length && searchText && searchText.length > 1 &&
|
||||
(ciphers | searchCiphers: searchText) as searchedCiphers">
|
||||
<div class="no-items" *ngIf="!searchedCiphers.length">
|
||||
<p>{{'noItemsInList' | i18n}}</p>
|
||||
</div>
|
||||
<div class="box list full-list" *ngIf="searchedCiphers.length > 0">
|
||||
<div class="box-content">
|
||||
<app-ciphers-list [ciphers]="searchedCiphers" title="{{'viewItem' | i18n}}"
|
||||
(onSelected)="selectCipher($event)"></app-ciphers-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</content>
|
||||
182
src/popup/vault/groupings.component.ts
Normal file
182
src/popup/vault/groupings.component.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
import { FolderView } from 'jslib/models/view/folderView';
|
||||
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { GroupingsComponent as BaseGroupingsComponent } from 'jslib/angular/components/groupings.component';
|
||||
|
||||
import { PopupUtilsService } from '../services/popup-utils.service';
|
||||
|
||||
const ComponentId = 'GroupingsComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-groupings',
|
||||
templateUrl: 'groupings.component.html',
|
||||
})
|
||||
export class GroupingsComponent extends BaseGroupingsComponent implements OnInit, OnDestroy {
|
||||
ciphers: CipherView[];
|
||||
favoriteCiphers: CipherView[];
|
||||
noFolderCiphers: CipherView[];
|
||||
folderCount: number;
|
||||
folderCounts = new Map<string, number>();
|
||||
collectionCounts = new Map<string, number>();
|
||||
typeCounts = new Map<CipherType, number>();
|
||||
showNoFolderCiphers = false;
|
||||
searchText: string;
|
||||
state: any;
|
||||
|
||||
constructor(collectionService: CollectionService, folderService: FolderService,
|
||||
private cipherService: CipherService, private router: Router,
|
||||
private ngZone: NgZone, private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef, private route: ActivatedRoute,
|
||||
private stateService: StateService, private popupUtils: PopupUtilsService) {
|
||||
super(collectionService, folderService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.remove('CiphersComponent');
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'syncCompleted':
|
||||
window.setTimeout(() => {
|
||||
this.load();
|
||||
}, 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
});
|
||||
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
this.state = (await this.stateService.get<any>(ComponentId)) || {};
|
||||
if (this.state.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
} else if (params.searchText) {
|
||||
this.searchText = params.searchText;
|
||||
}
|
||||
|
||||
this.load();
|
||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
await super.load();
|
||||
super.loaded = false;
|
||||
|
||||
await this.loadCiphers();
|
||||
this.showNoFolderCiphers = this.noFolderCiphers != null && this.noFolderCiphers.length < 100 &&
|
||||
this.collections.length === 0;
|
||||
if (this.showNoFolderCiphers) {
|
||||
// Remove "No Folder" from folder listing
|
||||
this.folders.pop();
|
||||
this.folderCount = this.folders.length;
|
||||
} else {
|
||||
this.folderCount = this.folders.length - 1;
|
||||
}
|
||||
|
||||
super.loaded = true;
|
||||
}
|
||||
|
||||
async loadCiphers() {
|
||||
this.ciphers = await this.cipherService.getAllDecrypted();
|
||||
this.ciphers.forEach((c) => {
|
||||
if (c.favorite) {
|
||||
if (this.favoriteCiphers == null) {
|
||||
this.favoriteCiphers = [];
|
||||
}
|
||||
this.favoriteCiphers.push(c);
|
||||
}
|
||||
|
||||
if (c.folderId == null) {
|
||||
if (this.noFolderCiphers == null) {
|
||||
this.noFolderCiphers = [];
|
||||
}
|
||||
this.noFolderCiphers.push(c);
|
||||
}
|
||||
|
||||
if (this.typeCounts.has(c.type)) {
|
||||
this.typeCounts.set(c.type, this.typeCounts.get(c.type) + 1);
|
||||
} else {
|
||||
this.typeCounts.set(c.type, 1);
|
||||
}
|
||||
|
||||
if (this.folderCounts.has(c.folderId)) {
|
||||
this.folderCounts.set(c.folderId, this.folderCounts.get(c.folderId) + 1);
|
||||
} else {
|
||||
this.folderCounts.set(c.folderId, 1);
|
||||
}
|
||||
|
||||
if (c.collectionIds != null) {
|
||||
c.collectionIds.forEach((colId) => {
|
||||
if (this.collectionCounts.has(colId)) {
|
||||
this.collectionCounts.set(colId, this.collectionCounts.get(colId) + 1);
|
||||
} else {
|
||||
this.collectionCounts.set(colId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async selectType(type: CipherType) {
|
||||
super.selectType(type);
|
||||
this.router.navigate(['/ciphers'], { queryParams: { type: type } });
|
||||
}
|
||||
|
||||
async selectFolder(folder: FolderView) {
|
||||
super.selectFolder(folder);
|
||||
this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id || 'none' } });
|
||||
}
|
||||
|
||||
async selectCollection(collection: CollectionView) {
|
||||
super.selectCollection(collection);
|
||||
this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } });
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } });
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
this.router.navigate(['/add-cipher']);
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: this.popupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.save(ComponentId, this.state);
|
||||
}
|
||||
}
|
||||
252
src/popup/vault/view.component.html
Normal file
252
src/popup/vault/view.component.html
Normal file
@@ -0,0 +1,252 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" appBlurClick (click)="close()">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
<div class="center">
|
||||
<span class="title">{{'viewItem' | i18n}}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" appBlurClick (click)="edit()">{{'edit' | i18n}}</button>
|
||||
</div>
|
||||
</header>
|
||||
<content *ngIf="cipher">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
{{'itemInformation' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<span class="row-label">{{'name' | i18n}}</span>
|
||||
{{cipher.name}}
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.login">
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{'username' | i18n}}</span>
|
||||
{{cipher.login.username}}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyUsername' | i18n}}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{'password' | i18n}}</span>
|
||||
<span [hidden]="showPassword" class="monospaced">{{cipher.login.maskedPassword}}</span>
|
||||
<span [hidden]="!showPassword" class="monospaced">{{cipher.login.password}}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button type="button" #checkPasswordBtn class="row-btn btn" appBlurClick
|
||||
title="{{'checkPassword' | i18n}}" (click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise" [disabled]="checkPasswordBtn.loading">
|
||||
<i class="fa fa-lg fa-check-circle" [hidden]="checkPasswordBtn.loading"></i>
|
||||
<i class="fa fa-lg fa-spinner fa-spin" [hidden]="!checkPasswordBtn.loading"></i>
|
||||
</button>
|
||||
<a class="row-btn" href="#" appStopClick title="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword()">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</a>
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyPassword' | i18n}}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex totp" [ngClass]="{'low': totpLow}"
|
||||
*ngIf="cipher.login.totp && totpCode">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{'verificationCodeTotp' | i18n}}</span>
|
||||
<span class="totp-code">{{totpCodeFormatted}}</span>
|
||||
</div>
|
||||
<span class="totp-countdown">
|
||||
<span class="totp-sec">{{totpSec}}</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle class="totp-circle inner" r="12.6" cy="16" cx="16"
|
||||
[ngStyle]="{'stroke-dashoffset.px': totpDash}"></circle>
|
||||
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.card">
|
||||
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
|
||||
<span class="row-label">{{'cardholderName' | i18n}}</span>
|
||||
{{cipher.card.cardholderName}}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{'number' | i18n}}</span>
|
||||
{{cipher.card.number}}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyNumber' | i18n}}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Number')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.brand">
|
||||
<span class="row-label">{{'brand' | i18n}}</span>
|
||||
{{cipher.card.brand}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.expiration">
|
||||
<span class="row-label">{{'expiration' | i18n}}</span>
|
||||
{{cipher.card.expiration}}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{'securityCode' | i18n}}</span>
|
||||
{{cipher.card.code}}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copySecurityCode' | i18n}}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.identity">
|
||||
<div class="box-content-row" *ngIf="cipher.identity.fullName">
|
||||
<span class="row-label">{{'identityName' | i18n}}</span>
|
||||
{{cipher.identity.fullName}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.username">
|
||||
<span class="row-label">{{'username' | i18n}}</span>
|
||||
{{cipher.identity.username}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.company">
|
||||
<span class="row-label">{{'company' | i18n}}</span>
|
||||
{{cipher.identity.company}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.ssn">
|
||||
<span class="row-label">{{'ssn' | i18n}}</span>
|
||||
{{cipher.identity.ssn}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
|
||||
<span class="row-label">{{'passportNumber' | i18n}}</span>
|
||||
{{cipher.identity.passportNumber}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
|
||||
<span class="row-label">{{'licenseNumber' | i18n}}</span>
|
||||
{{cipher.identity.licenseNumber}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.email">
|
||||
<span class="row-label">{{'email' | i18n}}</span>
|
||||
{{cipher.identity.email}}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.phone">
|
||||
<span class="row-label">{{'phone' | i18n}}</span>
|
||||
{{cipher.identity.phone}}
|
||||
</div>
|
||||
<div class="box-content-row"
|
||||
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country">
|
||||
<span class="row-label">{{'address' | i18n}}</span>
|
||||
<div *ngIf="cipher.identity.address1">{{cipher.identity.address1}}</div>
|
||||
<div *ngIf="cipher.identity.address2">{{cipher.identity.address2}}</div>
|
||||
<div *ngIf="cipher.identity.address3">{{cipher.identity.address3}}</div>
|
||||
<div *ngIf="cipher.identity.city || cipher.identity.state || cipher.identity.postalCode">
|
||||
{{cipher.identity.city || '-'}},
|
||||
{{cipher.identity.state || '-'}},
|
||||
{{cipher.identity.postalCode || '-'}}
|
||||
</div>
|
||||
<div *ngIf="cipher.identity.country">{{cipher.identity.country}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let u of cipher.login.uris; let i = index">
|
||||
<div class="row-main">
|
||||
<span class="row-label" *ngIf="!u.isWebsite">{{'uri' | i18n}}</span>
|
||||
<span class="row-label" *ngIf="u.isWebsite">{{'website' | i18n}}</span>
|
||||
<span title="{{u.uri}}">{{u.domainOrUri}}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'launch' | i18n}}"
|
||||
*ngIf="u.canLaunch" (click)="launch(u)">
|
||||
<i class="fa fa-lg fa-share-square-o"></i>
|
||||
</a>
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyUri' | i18n}}"
|
||||
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.notes">
|
||||
<div class="box-header">
|
||||
{{'notes' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row pre">{{cipher.notes}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.hasFields">
|
||||
<div class="box-header">
|
||||
{{'customFields' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{field.name}}</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{field.value || ' '}}
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Hidden">
|
||||
<span [hidden]="!field.showValue" class="monospaced">{{field.value}}</span>
|
||||
<span [hidden]="field.showValue" class="monospaced">{{field.maskedValue}}</span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<i class="fa fa-check-square-o" *ngIf="field.value === 'true'"></i>
|
||||
<i class="fa fa-square-o" *ngIf="field.value !== 'true'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a class="row-btn" href="#" appStopClick title="{{'toggleVisibility' | i18n}}"
|
||||
*ngIf="field.type === fieldType.Hidden" (click)="toggleFieldValue(field)">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !field.showValue, 'fa-eye-slash': field.showValue}"></i>
|
||||
</a>
|
||||
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
|
||||
*ngIf="field.value && field.type !== fieldType.Boolean"
|
||||
(click)="copy(field.value, 'value', 'Field')">
|
||||
<i class="fa fa-lg fa-clipboard"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.hasAttachments && isPremium">
|
||||
<div class="box-header">
|
||||
{{'attachments' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<a class="box-content-row box-content-row-flex text-default"
|
||||
*ngFor="let attachment of cipher.attachments"
|
||||
href="#" appStopClick appBlurCLick (click)="downloadAttachment(attachment)">
|
||||
<span class="row-main">{{attachment.fileName}}</span>
|
||||
<small class="row-sub-label">{{attachment.sizeName}}</small>
|
||||
<i class="fa fa-download fa-fw row-sub-icon" *ngIf="!attachment.downloading"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin row-sub-icon" *ngIf="attachment.downloading"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</content>
|
||||
61
src/popup/vault/view.component.ts
Normal file
61
src/popup/vault/view.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { BrowserApi } from '../../browser/browserApi';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { TotpService } from 'jslib/abstractions/totp.service';
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from 'jslib/angular/components/view.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-view',
|
||||
templateUrl: 'view.component.html',
|
||||
})
|
||||
export class ViewComponent extends BaseViewComponent implements OnInit {
|
||||
constructor(cipherService: CipherService, totpService: TotpService,
|
||||
tokenService: TokenService, toasterService: ToasterService,
|
||||
cryptoService: CryptoService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, analytics: Angulartics2,
|
||||
auditService: AuditService, private route: ActivatedRoute,
|
||||
private router: Router, private location: Location) {
|
||||
super(cipherService, totpService, tokenService, toasterService, cryptoService, platformUtilsService,
|
||||
i18nService, analytics, auditService, window);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
edit() {
|
||||
super.edit();
|
||||
this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
|
||||
close() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user