Restructure project: group elements and mixing in directories, flatten directory structure
Directly import from core modules, mixins
This commit is contained in:
parent
d1f276f46d
commit
bffb2648b8
|
@ -10,11 +10,6 @@
|
|||
},
|
||||
"extends": "eslint:recommended",
|
||||
"plugins": ["html"],
|
||||
"globals": {
|
||||
"Polymer": true,
|
||||
"padlock": true,
|
||||
"$l": true
|
||||
},
|
||||
"rules": {
|
||||
"brace-style": "off",
|
||||
"camelcase": "error",
|
||||
|
@ -38,7 +33,6 @@
|
|||
"no-unused-expressions": "off",
|
||||
"no-use-before-define": "warn",
|
||||
"no-whitespace-before-property": "error",
|
||||
"operator-linebreak": ["error", "after"],
|
||||
"quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"semi": "error",
|
||||
"semi-spacing": "error",
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "padlock",
|
||||
"version": "2.0.0",
|
||||
"homepage": "http://padlock.io",
|
||||
"authors": [
|
||||
"Martin Kleinschrodt <martin@maklesoft.com>"
|
||||
],
|
||||
"main": "index.html",
|
||||
"license": "GPL-3.0",
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
],
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"polymer": "~2.1.0",
|
||||
"zxcvbn": "~3.1.0",
|
||||
"autosize": "~3.0.13",
|
||||
"iron-list": "https://github.com/MaKleSoft/iron-list.git#custom-fixes",
|
||||
"paper-spinner": "^2.0.0",
|
||||
"webcomponentsjs": "1.0.13"
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
<link rel="stylesheet" href="./src/styles/fonts.css">
|
||||
|
||||
<script type="module" src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
|
||||
<script type="module" src="./src/ui/app/app.js"></script>
|
||||
<script type="module" src="./src/elements/app.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { unparse } from "papaparse";
|
||||
import * as papaparse from "papaparse";
|
||||
import { Collection, Record } from "./data";
|
||||
import { MemorySource, EncryptedSource } from "./source";
|
||||
|
||||
|
@ -10,7 +10,9 @@ function recordsToTable(records: Record[]) {
|
|||
// Two dimensional array, starting with column names
|
||||
let table = [cols];
|
||||
// Filter out removed items
|
||||
records = records.filter(function(rec) { return !rec.removed; });
|
||||
records = records.filter(function(rec) {
|
||||
return !rec.removed;
|
||||
});
|
||||
|
||||
// Fill up columns array with distinct field names
|
||||
for (let rec of records) {
|
||||
|
@ -54,7 +56,7 @@ function recordsToTable(records: Record[]) {
|
|||
}
|
||||
|
||||
export function toCSV(records: Record[]): string {
|
||||
return unparse(recordsToTable(records));
|
||||
return papaparse.unparse(recordsToTable(records));
|
||||
}
|
||||
|
||||
export async function toPadlock(records: Record[], password: string): Promise<string> {
|
||||
|
|
|
@ -3,17 +3,13 @@ import { Record, Field } from "./data";
|
|||
import { Container } from "./crypto";
|
||||
|
||||
export class ImportError {
|
||||
constructor(
|
||||
public code: "invalid_csv"
|
||||
) {};
|
||||
constructor(public code: "invalid_csv") {}
|
||||
}
|
||||
|
||||
//* Detects if a string contains a SecuStore backup
|
||||
export function isFromSecuStore(data: string): boolean {
|
||||
return data.indexOf("SecuStore") != -1 &&
|
||||
data.indexOf("#begin") != -1 &&
|
||||
data.indexOf("#end") != -1;
|
||||
};
|
||||
return data.indexOf("SecuStore") != -1 && data.indexOf("#begin") != -1 && data.indexOf("#end") != -1;
|
||||
}
|
||||
|
||||
export async function fromSecuStore(rawData: string, password: string): Promise<Record[]> {
|
||||
const begin = "#begin";
|
||||
|
@ -45,10 +41,11 @@ export async function fromSecuStore(rawData: string, password: string): Promise<
|
|||
|
||||
// Convert the _items_ array of the SecuStore Set object into an array of Padlock records
|
||||
let records = data.items.map((item: any) => {
|
||||
let fields = item.template.containsPassword ?
|
||||
// Passwords are a separate property in SecuStore but will be treated as
|
||||
// regular fields in Padlock
|
||||
item.fields.concat([{name: "password", value: item.password}]) : item.fields;
|
||||
let fields = item.template.containsPassword
|
||||
? // Passwords are a separate property in SecuStore but will be treated as
|
||||
// regular fields in Padlock
|
||||
item.fields.concat([{ name: "password", value: item.password }])
|
||||
: item.fields;
|
||||
|
||||
return new Record(item.title, fields, [data.name]);
|
||||
});
|
||||
|
@ -82,12 +79,11 @@ export function fromTable(data: string[][], nameColIndex?: number, tagsColIndex?
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// All subsequent rows should contain values
|
||||
let records = data.slice(1).map(function(row) {
|
||||
// Construct an array of field object from column names and values
|
||||
let fields = [];
|
||||
for (let i=0; i<row.length; i++) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
// Skip name column, category column (if any) and empty fields
|
||||
if (i != nameColIndex && i != tagsColIndex && row[i]) {
|
||||
fields.push({
|
||||
|
@ -98,7 +94,7 @@ export function fromTable(data: string[][], nameColIndex?: number, tagsColIndex?
|
|||
}
|
||||
|
||||
const tags = row[tagsColIndex!];
|
||||
return new Record(row[nameColIndex || 0], fields, tags && tags.split(",") || []);
|
||||
return new Record(row[nameColIndex || 0], fields, (tags && tags.split(",")) || []);
|
||||
});
|
||||
|
||||
return records;
|
||||
|
@ -123,7 +119,7 @@ export function isFromPadlock(data: string): boolean {
|
|||
try {
|
||||
Container.fromJSON(data);
|
||||
return true;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +167,7 @@ function lpParseRow(row: string[]): Record {
|
|||
{ name: "url", value: row[urlIndex] },
|
||||
{ name: "username", value: row[usernameIndex] },
|
||||
{ name: "password", value: row[passwordIndex], masked: true }
|
||||
]
|
||||
];
|
||||
let notes = row[notesIndex];
|
||||
|
||||
if (row[urlIndex] === "http://sn") {
|
||||
|
@ -191,8 +187,8 @@ function lpParseRow(row: string[]): Record {
|
|||
}
|
||||
|
||||
export function fromLastPass(data: string): Record[] {
|
||||
let records = parse(data).data
|
||||
// Remove first row as it only contains field names
|
||||
let records = parse(data)
|
||||
.data// Remove first row as it only contains field names
|
||||
.slice(1)
|
||||
// Filter out empty rows
|
||||
.filter(row => row.length > 1)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { resolveLanguage } from "./util";
|
||||
import { getLocale } from "./platform";
|
||||
|
||||
interface Translations {
|
||||
[lang: string]: { [msg: string]: string };
|
||||
}
|
||||
|
||||
let translations: Translations = {};
|
||||
let language: string;
|
||||
|
||||
export function localize(msg: string, ...fmtArgs: string[]) {
|
||||
const lang = translations[language];
|
||||
let res = (lang && lang[msg]) || msg;
|
||||
|
||||
for (let i = 0; i < fmtArgs.length; i++) {
|
||||
res = res.replace(new RegExp(`\\{${i}\\}`, "g"), fmtArgs[i]);
|
||||
}
|
||||
|
||||
res = res.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function loadTranslations(t: Translations) {
|
||||
translations = t;
|
||||
language = resolveLanguage(getLocale(), translations);
|
||||
}
|
|
@ -3,21 +3,24 @@ import { Source } from "./source";
|
|||
import { resolveLanguage } from "./util";
|
||||
import { getLocale, getPlatformName } from "./platform";
|
||||
import { Settings } from "./data";
|
||||
import { satisfies } from "semver";
|
||||
// import { satisfies } from "semver";
|
||||
|
||||
function satisfies(v1: string, v2: string): boolean {
|
||||
return v1 === v2;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
from: Date
|
||||
until: Date
|
||||
link: string
|
||||
text: string
|
||||
platform?: string[]
|
||||
subStatus?: string[]
|
||||
version?: string
|
||||
id: string;
|
||||
from: Date;
|
||||
until: Date;
|
||||
link: string;
|
||||
text: string;
|
||||
platform?: string[];
|
||||
subStatus?: string[];
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export class Messages {
|
||||
|
||||
constructor(public url: string, public source: Source, public settings: Settings) {}
|
||||
|
||||
private async fetchRead(): Promise<any> {
|
||||
|
@ -58,7 +61,8 @@ export class Messages {
|
|||
})
|
||||
.filter((a: Message) => {
|
||||
return (
|
||||
!read[a.id] && a.from <= now &&
|
||||
!read[a.id] &&
|
||||
a.from <= now &&
|
||||
a.until >= now &&
|
||||
(!a.platform || a.platform.includes(platform)) &&
|
||||
(!a.subStatus || a.subStatus.includes(this.settings.syncSubStatus)) &&
|
||||
|
@ -68,8 +72,12 @@ export class Messages {
|
|||
}
|
||||
|
||||
async fetch(): Promise<Message[]> {
|
||||
const req = await request("GET", this.url, undefined,
|
||||
new Map<string, string>([["Accept", "application/json"]]));
|
||||
const req = await request(
|
||||
"GET",
|
||||
this.url,
|
||||
undefined,
|
||||
new Map<string, string>([["Accept", "application/json"]])
|
||||
);
|
||||
return this.parseAndFilter(req.responseText);
|
||||
}
|
||||
|
||||
|
@ -78,6 +86,4 @@ export class Messages {
|
|||
read[a.id] = true;
|
||||
await this.saveRead(read);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import * as moment from "moment";
|
||||
import "moment-duration-format";
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
// RFC4122-compliant uuid generator
|
||||
export function uuid(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
||||
var r = Math.random()*16|0, v = c == "x" ? r : (r&0x3|0x8);
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
@ -31,7 +33,7 @@ export function randomString(length = 32, charSet = charSets.full) {
|
|||
while (str.length < length) {
|
||||
window.crypto.getRandomValues(rnd);
|
||||
// Prevent modulo bias by rejecting values larger than the highest muliple of `charSet.length`
|
||||
if (rnd[0] > 255 - 256 % charSet.length) {
|
||||
if (rnd[0] > 255 - (256 % charSet.length)) {
|
||||
continue;
|
||||
}
|
||||
str += charSet[rnd[0] % charSet.length];
|
||||
|
@ -49,7 +51,7 @@ export function debounce(fn: (...args: any[]) => any, delay: number) {
|
|||
}
|
||||
|
||||
export function wait(dt: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, dt));
|
||||
return new Promise<void>(resolve => setTimeout(resolve, dt));
|
||||
}
|
||||
|
||||
export function resolveLanguage(locale: string, supportedLanguages: { [lang: string]: any }): string {
|
||||
|
@ -75,11 +77,21 @@ export function formatDateFromNow(date: Date) {
|
|||
return moment(date).fromNow();
|
||||
}
|
||||
|
||||
export function formatDateUntil(startDate: Date|string|number, duration: number) {
|
||||
const d = moment.duration(moment(startDate).add(duration, "hours").diff(moment()));
|
||||
export function formatDateUntil(startDate: Date | string | number, duration: number) {
|
||||
const d = moment.duration(
|
||||
moment(startDate)
|
||||
.add(duration, "hours")
|
||||
.diff(moment())
|
||||
);
|
||||
return d.format("hh:mm:ss");
|
||||
}
|
||||
|
||||
export function isFuture(date: Date|string|number, duration: number) {
|
||||
return moment(date).add(duration, "hours").isAfter();
|
||||
export function isFuture(date: Date | string | number, duration: number) {
|
||||
return moment(date)
|
||||
.add(duration, "hours")
|
||||
.isAfter();
|
||||
}
|
||||
|
||||
export function passwordStrength(password: string): zxcvbn.ZXCVBNResult {
|
||||
return zxcvbn(password);
|
||||
}
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
import './analytics.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
import '../cloud-view/cloud-view.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../list-view/list-view.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import '../record-view/record-view.js';
|
||||
import '../settings-view/settings-view.js';
|
||||
import '../start-view/start-view.js';
|
||||
import '../sync/sync.js';
|
||||
import '../title-bar/title-bar.js';
|
||||
import './auto-lock.js';
|
||||
import './auto-sync.js';
|
||||
import './hints.js';
|
||||
import './messages.js';
|
||||
import { getPlatformName, getDeviceInfo, isTouch } from "../../core/platform.js"
|
||||
import "../styles/shared.js";
|
||||
import "./cloud-view.js";
|
||||
import "./icon.js";
|
||||
import "./list-view.js";
|
||||
import "./record-view.js";
|
||||
import "./settings-view.js";
|
||||
import "./start-view.js";
|
||||
import "./title-bar.js";
|
||||
import { getPlatformName, getDeviceInfo, isTouch } from "../core/platform.js";
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import {
|
||||
NotificationMixin,
|
||||
DialogMixin,
|
||||
MessagesMixin,
|
||||
DataMixin,
|
||||
AnimationMixin,
|
||||
ClipboardMixin,
|
||||
SyncMixin,
|
||||
AutoSyncMixin,
|
||||
AutoLockMixin,
|
||||
HintsMixin,
|
||||
AnalyticsMixin,
|
||||
LocaleMixin
|
||||
} from "../mixins";
|
||||
|
||||
/* global cordova, StatusBar */
|
||||
|
||||
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
|
||||
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
const cordovaReady = new Promise((resolve) => {
|
||||
const cordovaReady = new Promise(resolve => {
|
||||
document.addEventListener("deviceready", resolve);
|
||||
});
|
||||
|
||||
|
@ -46,8 +45,8 @@ class App extends applyMixins(
|
|||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
@ -429,312 +428,318 @@ class App extends applyMixins(
|
|||
|
||||
<pl-title-bar></pl-title-bar>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-app"; }
|
||||
static get is() {
|
||||
return "pl-app";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
locked: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: "_lockedChanged"
|
||||
},
|
||||
_currentView: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "_currentViewChanged"
|
||||
},
|
||||
_selectedRecord: {
|
||||
type: Object,
|
||||
observer: "_selectedRecordChanged"
|
||||
},
|
||||
_menuOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_menuOpenChanged"
|
||||
},
|
||||
_showingTags: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
locked: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: "_lockedChanged"
|
||||
},
|
||||
_currentView: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "_currentViewChanged"
|
||||
},
|
||||
_selectedRecord: {
|
||||
type: Object,
|
||||
observer: "_selectedRecordChanged"
|
||||
},
|
||||
_menuOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_menuOpenChanged"
|
||||
},
|
||||
_showingTags: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// If we want to capture all keydown events, we have to add the listener
|
||||
// directly to the document
|
||||
document.addEventListener("keydown", this._keydown.bind(this), false);
|
||||
// If we want to capture all keydown events, we have to add the listener
|
||||
// directly to the document
|
||||
document.addEventListener("keydown", this._keydown.bind(this), false);
|
||||
|
||||
// Listen for android back button
|
||||
document.addEventListener("backbutton", this._back.bind(this), false);
|
||||
// Listen for android back button
|
||||
document.addEventListener("backbutton", this._back.bind(this), false);
|
||||
|
||||
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
|
||||
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
|
||||
}
|
||||
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
|
||||
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
|
||||
}
|
||||
|
||||
get _isNarrow() {
|
||||
return this.offsetWidth < 600;
|
||||
}
|
||||
get _isNarrow() {
|
||||
return this.offsetWidth < 600;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
let isIPhoneX;
|
||||
getDeviceInfo()
|
||||
.then((device) => {
|
||||
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
|
||||
if (isIPhoneX) {
|
||||
Object.assign(document.body.style, {
|
||||
margin: 0,
|
||||
height: "812px",
|
||||
position: "relative"
|
||||
});
|
||||
}
|
||||
return cordovaReady;
|
||||
})
|
||||
.then(() => {
|
||||
// Replace window.open method with the inappbrowser equivalent
|
||||
window.open = cordova.InAppBrowser.open;
|
||||
if (isIPhoneX) {
|
||||
StatusBar && StatusBar.show();
|
||||
}
|
||||
navigator.splashscreen.hide();
|
||||
});
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
let isIPhoneX;
|
||||
getDeviceInfo()
|
||||
.then(device => {
|
||||
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
|
||||
if (isIPhoneX) {
|
||||
Object.assign(document.body.style, {
|
||||
margin: 0,
|
||||
height: "812px",
|
||||
position: "relative"
|
||||
});
|
||||
}
|
||||
return cordovaReady;
|
||||
})
|
||||
.then(() => {
|
||||
// Replace window.open method with the inappbrowser equivalent
|
||||
window.open = cordova.InAppBrowser.open;
|
||||
if (isIPhoneX) {
|
||||
StatusBar && StatusBar.show();
|
||||
}
|
||||
navigator.splashscreen.hide();
|
||||
});
|
||||
|
||||
getPlatformName().then((platform) => {
|
||||
const className = platform.toLowerCase().replace(/ /g, "-");
|
||||
if (className) {
|
||||
this.classList.add(className);
|
||||
this.root.querySelector("pl-title-bar").classList.add(className);
|
||||
}
|
||||
});
|
||||
getPlatformName().then(platform => {
|
||||
const className = platform.toLowerCase().replace(/ /g, "-");
|
||||
if (className) {
|
||||
this.classList.add(className);
|
||||
this.root.querySelector("pl-title-bar").classList.add(className);
|
||||
}
|
||||
});
|
||||
|
||||
if (!isTouch()) {
|
||||
window.addEventListener("focus", () => setTimeout(() => {
|
||||
if (this.locked) {
|
||||
this.$.startView.focus();
|
||||
}
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
if (!isTouch()) {
|
||||
window.addEventListener("focus", () =>
|
||||
setTimeout(() => {
|
||||
if (this.locked) {
|
||||
this.$.startView.focus();
|
||||
}
|
||||
}, 100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
recordDeleted(record) {
|
||||
if (record === this._selectedRecord) {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
}
|
||||
recordDeleted(record) {
|
||||
if (record === this._selectedRecord) {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
}
|
||||
|
||||
dataLoaded() {
|
||||
this.locked = false;
|
||||
this.$.startView.open = true;
|
||||
}
|
||||
dataLoaded() {
|
||||
this.locked = false;
|
||||
this.$.startView.open = true;
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this.clearDialogs();
|
||||
this.$.startView.reset();
|
||||
this.locked = true;
|
||||
this.$.startView.open = false;
|
||||
this.clearClipboard();
|
||||
}
|
||||
dataUnloaded() {
|
||||
this.clearDialogs();
|
||||
this.$.startView.reset();
|
||||
this.locked = true;
|
||||
this.$.startView.open = false;
|
||||
this.clearClipboard();
|
||||
}
|
||||
|
||||
dataReset() {
|
||||
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
|
||||
}
|
||||
dataReset() {
|
||||
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
|
||||
}
|
||||
|
||||
_closeRecord() {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
_closeRecord() {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
|
||||
_selectedRecordChanged() {
|
||||
clearTimeout(this._selectedRecordChangedTimeout);
|
||||
this._selectedRecordChangedTimeout = setTimeout(() => {
|
||||
if (this._selectedRecord) {
|
||||
this.$.recordView.record = this._selectedRecord;
|
||||
this._currentView = "recordView";
|
||||
this._selectedRecord.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
} else if (this._currentView == "recordView") {
|
||||
this._currentView = "";
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
_selectedRecordChanged() {
|
||||
clearTimeout(this._selectedRecordChangedTimeout);
|
||||
this._selectedRecordChangedTimeout = setTimeout(() => {
|
||||
if (this._selectedRecord) {
|
||||
this.$.recordView.record = this._selectedRecord;
|
||||
this._currentView = "recordView";
|
||||
this._selectedRecord.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
} else if (this._currentView == "recordView") {
|
||||
this._currentView = "";
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
this._currentView = "settingsView";
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
_openSettings() {
|
||||
this._currentView = "settingsView";
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
|
||||
_settingsBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
_settingsBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
|
||||
_openCloudView() {
|
||||
this._currentView = "cloudView";
|
||||
this.refreshAccount();
|
||||
this.$.listView.deselect();
|
||||
if (!this.settings.syncConnected && !isTouch()) {
|
||||
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
|
||||
}
|
||||
}
|
||||
_openCloudView() {
|
||||
this._currentView = "cloudView";
|
||||
this.refreshAccount();
|
||||
this.$.listView.deselect();
|
||||
if (!this.settings.syncConnected && !isTouch()) {
|
||||
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
_cloudViewBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
_cloudViewBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
|
||||
_currentViewChanged(curr, prev) {
|
||||
this.$.main.classList.toggle("showing-pages", !!curr);
|
||||
_currentViewChanged(curr, prev) {
|
||||
this.$.main.classList.toggle("showing-pages", !!curr);
|
||||
|
||||
const currView = this.$[curr];
|
||||
const prevView = this.$[prev];
|
||||
if (currView) {
|
||||
this.animateElement(currView, {
|
||||
animation: "viewIn",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "backwards"
|
||||
});
|
||||
currView.classList.add("showing");
|
||||
currView.animate();
|
||||
}
|
||||
if (prevView) {
|
||||
this.animateElement(prevView, {
|
||||
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "forwards"
|
||||
});
|
||||
setTimeout(() => prevView.classList.remove("showing"), 350);
|
||||
}
|
||||
}
|
||||
const currView = this.$[curr];
|
||||
const prevView = this.$[prev];
|
||||
if (currView) {
|
||||
this.animateElement(currView, {
|
||||
animation: "viewIn",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "backwards"
|
||||
});
|
||||
currView.classList.add("showing");
|
||||
currView.animate();
|
||||
}
|
||||
if (prevView) {
|
||||
this.animateElement(prevView, {
|
||||
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "forwards"
|
||||
});
|
||||
setTimeout(() => prevView.classList.remove("showing"), 350);
|
||||
}
|
||||
}
|
||||
|
||||
//* Keyboard shortcuts
|
||||
_keydown(event) {
|
||||
if (this.locked || padlock.Input.activeInput) {
|
||||
return;
|
||||
}
|
||||
//* Keyboard shortcuts
|
||||
_keydown(event) {
|
||||
if (this.locked || padlock.Input.activeInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shortcut;
|
||||
const control = event.ctrlKey || event.metaKey;
|
||||
let shortcut;
|
||||
const control = event.ctrlKey || event.metaKey;
|
||||
|
||||
// ESCAPE -> Back
|
||||
if (event.key === "Escape") {
|
||||
shortcut = () => this._back();
|
||||
}
|
||||
// CTRL/CMD + F -> Filter
|
||||
else if (control && event.key === "f") {
|
||||
shortcut = () => this.$.listView.search();
|
||||
}
|
||||
// CTRL/CMD + N -> New Record
|
||||
else if (control && event.key === "n") {
|
||||
shortcut = () => this.createRecord();
|
||||
}
|
||||
// ESCAPE -> Back
|
||||
if (event.key === "Escape") {
|
||||
shortcut = () => this._back();
|
||||
}
|
||||
// CTRL/CMD + F -> Filter
|
||||
else if (control && event.key === "f") {
|
||||
shortcut = () => this.$.listView.search();
|
||||
}
|
||||
// CTRL/CMD + N -> New Record
|
||||
else if (control && event.key === "n") {
|
||||
shortcut = () => this.createRecord();
|
||||
}
|
||||
|
||||
// If one of the shortcuts matches, execute it and prevent the default behaviour
|
||||
if (shortcut) {
|
||||
shortcut();
|
||||
event.preventDefault();
|
||||
} else if (event.key.length === 1) {
|
||||
this.$.listView.search();
|
||||
}
|
||||
}
|
||||
// If one of the shortcuts matches, execute it and prevent the default behaviour
|
||||
if (shortcut) {
|
||||
shortcut();
|
||||
event.preventDefault();
|
||||
} else if (event.key.length === 1) {
|
||||
this.$.listView.search();
|
||||
}
|
||||
}
|
||||
|
||||
_back() {
|
||||
switch (this._currentView) {
|
||||
case "recordView":
|
||||
this._closeRecord();
|
||||
break;
|
||||
case "settingsView":
|
||||
this._settingsBack();
|
||||
break;
|
||||
case "cloudView":
|
||||
this._cloudViewBack();
|
||||
break;
|
||||
default:
|
||||
if (this.$.listView.filterActive) {
|
||||
this.$.listView.clearFilter();
|
||||
} else {
|
||||
navigator.Backbutton && navigator.Backbutton.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
_back() {
|
||||
switch (this._currentView) {
|
||||
case "recordView":
|
||||
this._closeRecord();
|
||||
break;
|
||||
case "settingsView":
|
||||
this._settingsBack();
|
||||
break;
|
||||
case "cloudView":
|
||||
this._cloudViewBack();
|
||||
break;
|
||||
default:
|
||||
if (this.$.listView.filterActive) {
|
||||
this.$.listView.clearFilter();
|
||||
} else {
|
||||
navigator.Backbutton && navigator.Backbutton.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lockedChanged() {
|
||||
if (this.locked) {
|
||||
this._currentView = "";
|
||||
this.$.main.classList.remove("active");
|
||||
this._menuOpen = false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.$.main.classList.add("active");
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
_lockedChanged() {
|
||||
if (this.locked) {
|
||||
this._currentView = "";
|
||||
this.$.main.classList.remove("active");
|
||||
this._menuOpen = false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.$.main.classList.add("active");
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
_menuOpenChanged() {
|
||||
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
|
||||
this.$.main.classList.toggle("show-menu", this._menuOpen);
|
||||
if (!this._menuOpen) {
|
||||
setTimeout(() => this._showingTags = false, 300);
|
||||
}
|
||||
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
|
||||
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 50,
|
||||
fill: "both"
|
||||
});
|
||||
}
|
||||
_menuOpenChanged() {
|
||||
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
|
||||
this.$.main.classList.toggle("show-menu", this._menuOpen);
|
||||
if (!this._menuOpen) {
|
||||
setTimeout(() => (this._showingTags = false), 300);
|
||||
}
|
||||
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
|
||||
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 50,
|
||||
fill: "both"
|
||||
});
|
||||
}
|
||||
|
||||
_toggleMenu() {
|
||||
this._menuOpen = !this._menuOpen;
|
||||
}
|
||||
_toggleMenu() {
|
||||
this._menuOpen = !this._menuOpen;
|
||||
}
|
||||
|
||||
_lock() {
|
||||
if (this.isSynching) {
|
||||
this.alert($l("Cannot lock app while sync is in progress!"));
|
||||
} else {
|
||||
this.unloadData();
|
||||
}
|
||||
}
|
||||
_lock() {
|
||||
if (this.isSynching) {
|
||||
this.alert($l("Cannot lock app while sync is in progress!"));
|
||||
} else {
|
||||
this.unloadData();
|
||||
}
|
||||
}
|
||||
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
|
||||
_enableMultiSelect() {
|
||||
this.$.listView.multiSelect = true;
|
||||
}
|
||||
_enableMultiSelect() {
|
||||
this.$.listView.multiSelect = true;
|
||||
}
|
||||
|
||||
_menuWrapperClicked() {
|
||||
setTimeout(() => this._menuOpen = false, 50);
|
||||
}
|
||||
_menuWrapperClicked() {
|
||||
setTimeout(() => (this._menuOpen = false), 50);
|
||||
}
|
||||
|
||||
_showTags(e) {
|
||||
this._menuOpen = true;
|
||||
this._showingTags = true;
|
||||
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
|
||||
animation: "tagIn",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
fill: "both"
|
||||
});
|
||||
e.stopPropagation();
|
||||
}
|
||||
_showTags(e) {
|
||||
this._menuOpen = true;
|
||||
this._showingTags = true;
|
||||
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
|
||||
animation: "tagIn",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
fill: "both"
|
||||
});
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_closeTags(e) {
|
||||
this._showingTags = false;
|
||||
e.stopPropagation();
|
||||
}
|
||||
_closeTags(e) {
|
||||
this._showingTags = false;
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_selectTag(e) {
|
||||
setTimeout(() => {
|
||||
this.$.listView.filterString = e.model.item;
|
||||
}, 350);
|
||||
}
|
||||
_selectTag(e) {
|
||||
setTimeout(() => {
|
||||
this.$.listView.filterString = e.model.item;
|
||||
}, 350);
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.collection.tags.length;
|
||||
}
|
||||
_hasTags() {
|
||||
return !!this.collection.tags.length;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(App.is, App);
|
|
@ -1,11 +1,7 @@
|
|||
import "@polymer/polymer/polymer-legacy";
|
||||
import { PolymerElement, html } from "@polymer/polymer/polymer-element";
|
||||
import "../../padlock.js";
|
||||
|
||||
window.Polymer = { html };
|
||||
import "@polymer/polymer/polymer-legacy.js";
|
||||
import { PolymerElement, html } from "@polymer/polymer/polymer-element.js";
|
||||
|
||||
export class BaseElement extends PolymerElement {
|
||||
|
||||
truthy(val) {
|
||||
return !!val;
|
||||
}
|
||||
|
@ -17,7 +13,6 @@ export class BaseElement extends PolymerElement {
|
|||
identity(val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
padlock.BaseElement = BaseElement;
|
||||
export { html };
|
|
@ -0,0 +1,112 @@
|
|||
import "../styles/shared.js";
|
||||
import { setClipboard } from "../core/platform.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
class Clipboard extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
color: var(--color-background);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
}
|
||||
|
||||
:host(:not(.showing)) {
|
||||
transform: translateY(130%);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="content">
|
||||
<div class="title">[[ \$l("Copied To Clipboard:") ]]</div>
|
||||
<div class="name">[[ record.name ]] / [[ field.name ]]</div>
|
||||
</div>
|
||||
|
||||
<button class="tiles-2 tap" on-click="clear">
|
||||
<div><strong>[[ \$l("Clear") ]]</strong></div>
|
||||
<div class="countdown">[[ _tMinusClear ]]s</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-clipboard";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
record: Object,
|
||||
field: Object
|
||||
};
|
||||
}
|
||||
|
||||
set(record, field, duration = 60) {
|
||||
clearInterval(this._interval);
|
||||
|
||||
this.record = record;
|
||||
this.field = field;
|
||||
setClipboard(field.value);
|
||||
|
||||
this.classList.add("showing");
|
||||
|
||||
const tStart = Date.now();
|
||||
|
||||
this._tMinusClear = duration;
|
||||
this._interval = setInterval(() => {
|
||||
const dt = tStart + duration * 1000 - Date.now();
|
||||
if (dt <= 0) {
|
||||
this.clear();
|
||||
} else {
|
||||
this._tMinusClear = Math.floor(dt / 1000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearInterval(this._interval);
|
||||
setClipboard(" ");
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Clipboard.is, Clipboard);
|
|
@ -1,19 +1,15 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import '../promo/promo.js';
|
||||
import '../sync/sync.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./icon.js";
|
||||
import "./input.js";
|
||||
import "./loading-button.js";
|
||||
import "./promo.js";
|
||||
import "./toggle-button.js";
|
||||
|
||||
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
import { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin } from "../mixins";
|
||||
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
|
||||
class CloudView extends applyMixins(
|
||||
BaseElement,
|
||||
|
@ -24,8 +20,8 @@ class CloudView extends applyMixins(
|
|||
NotificationMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -310,135 +306,129 @@ class CloudView extends applyMixins(
|
|||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-cloud-view"; }
|
||||
static get is() {
|
||||
return "pl-cloud-view";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
static get properties() {}
|
||||
|
||||
}
|
||||
ready() {
|
||||
super.ready();
|
||||
this.listen("data-loaded", () => this.animate());
|
||||
this.listen("sync-connect-start", () => this.animate());
|
||||
this.listen("sync-connect-cancel", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.animate());
|
||||
this.listen("sync-disconnect", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.refreshAccount());
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.listen("data-loaded", () => this.animate());
|
||||
this.listen("sync-connect-start", () => this.animate());
|
||||
this.listen("sync-connect-cancel", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.animate());
|
||||
this.listen("sync-disconnect", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.refreshAccount());
|
||||
}
|
||||
animate() {
|
||||
if (this.settings.syncConnected) {
|
||||
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (this.settings.syncConnected) {
|
||||
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
|
||||
}
|
||||
}
|
||||
focusEmailInput() {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
focusEmailInput() {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
_credentialsChanged() {
|
||||
if (this.isActivationPending() && !this._testCredsTimeout) {
|
||||
// Wait 1 minute, then poll every 10 seconds
|
||||
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
|
||||
// Also test on first focus event since there is a chance the user is just returning
|
||||
// from his email client / web browsers
|
||||
window.addEventListener("focus", () => this.testCredentials(), { once: true });
|
||||
} else if (!this.isActivationPending()) {
|
||||
clearTimeout(this._testCredsTimeout);
|
||||
this._testCredsTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
_credentialsChanged() {
|
||||
if (this.isActivationPending() && !this._testCredsTimeout) {
|
||||
// Wait 1 minute, then poll every 10 seconds
|
||||
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
|
||||
// Also test on first focus event since there is a chance the user is just returning
|
||||
// from his email client / web browsers
|
||||
window.addEventListener("focus", () => this.testCredentials(), { once: true });
|
||||
} else if (!this.isActivationPending()) {
|
||||
clearTimeout(this._testCredsTimeout);
|
||||
this._testCredsTimeout = null;
|
||||
}
|
||||
}
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("cloud-back"));
|
||||
}
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("cloud-back"));
|
||||
}
|
||||
_logout() {
|
||||
this.confirm($l("Are you sure you want to log out?"), $l("Log Out")).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.disconnectCloud();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_logout() {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to log out?"),
|
||||
$l("Log Out")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.disconnectCloud();
|
||||
}
|
||||
});
|
||||
}
|
||||
_login() {
|
||||
if (this._submittingEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
_login() {
|
||||
if (this._submittingEmail) {
|
||||
return;
|
||||
}
|
||||
this.$.loginButton.start();
|
||||
|
||||
this.$.loginButton.start();
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert($l("Please enter a valid email address!")).then(() => this.$.emailInput.focus());
|
||||
this.$.loginButton.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert($l("Please enter a valid email address!"))
|
||||
.then(() => this.$.emailInput.focus());
|
||||
this.$.loginButton.fail();
|
||||
return;
|
||||
}
|
||||
this._submittingEmail = true;
|
||||
|
||||
this._submittingEmail = true;
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.success();
|
||||
return this.promptLoginCode();
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
this.synchronize();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.fail();
|
||||
});
|
||||
}
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.success();
|
||||
return this.promptLoginCode();
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
this.synchronize();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.fail();
|
||||
});
|
||||
_isCurrentDevice(device) {
|
||||
return this.settings.syncId === device.tokenId;
|
||||
}
|
||||
|
||||
}
|
||||
_revokeDevice(e) {
|
||||
const device = e.model.item;
|
||||
this.confirm($l('Do you want to revoke access to for the device "{0}"?', device.description)).then(
|
||||
confirmed => {
|
||||
if (confirmed) {
|
||||
this.cloudSource.source.revokeAuthToken(device.tokenId).then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Access for {0} revoked successfully!", device.description), { type: "success" });
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_isCurrentDevice(device) {
|
||||
return this.settings.syncId === device.tokenId;
|
||||
}
|
||||
_contactSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_revokeDevice(e) {
|
||||
const device = e.model.item;
|
||||
this.confirm($l("Do you want to revoke access to for the device \"{0}\"?", device.description))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.cloudSource.source.revokeAuthToken(device.tokenId)
|
||||
.then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Access for {0} revoked successfully!", device.description),
|
||||
{ type: "success" });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
_paymentSourceLabel() {
|
||||
const s = this.account && this.account.paymentSource;
|
||||
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
|
||||
}
|
||||
|
||||
_contactSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
_buySubscription(e) {
|
||||
this.buySubscription(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_paymentSourceLabel() {
|
||||
const s = this.account && this.account.paymentSource;
|
||||
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
|
||||
}
|
||||
_updatePaymentMethod(e) {
|
||||
this.updatePaymentMethod(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_buySubscription(e) {
|
||||
this.buySubscription(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_updatePaymentMethod(e) {
|
||||
this.updatePaymentMethod(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_promoExpired() {
|
||||
this.dispatch("settings-changed");
|
||||
}
|
||||
_promoExpired() {
|
||||
this.dispatch("settings-changed");
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(CloudView.is, CloudView);
|
|
@ -0,0 +1,128 @@
|
|||
import "../styles/shared.js";
|
||||
import { localize } from "../core/locale.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./dialog.js";
|
||||
|
||||
const defaultButtonLabel = localize("OK");
|
||||
|
||||
class DialogAlert extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="warning"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="plain"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-background);
|
||||
};
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-text,
|
||||
:host([hide-icon]) .info-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-text:not(.small) {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
|
||||
<div class="info" hidden\$="[[ _hideInfo(title, message) ]]">
|
||||
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ title ]]</div>
|
||||
<div class\$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button on-click="_selectOption" class\$="[[ _buttonClass(index) ]]">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-dialog-alert";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
buttonLabel: { type: String, value: defaultButtonLabel },
|
||||
title: { type: String, value: "" },
|
||||
message: { type: String, value: "" },
|
||||
options: { type: Array, value: ["OK"] },
|
||||
preventDismiss: { type: Boolean, value: false },
|
||||
type: { type: String, value: "info", reflectToAttribute: true },
|
||||
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
|
||||
open: { type: Boolean, value: false }
|
||||
};
|
||||
}
|
||||
|
||||
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
|
||||
this.message = message;
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.options = options;
|
||||
this.hideIcon = hideIcon;
|
||||
|
||||
setTimeout(() => (this.open = true), 10);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_icon() {
|
||||
switch (this.type) {
|
||||
case "info":
|
||||
return "info-round";
|
||||
case "warning":
|
||||
return "error";
|
||||
case "success":
|
||||
return "success";
|
||||
case "question":
|
||||
return "question";
|
||||
}
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_dialogDismiss() {
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_textClass() {
|
||||
return this.title ? "small" : "";
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hideInfo() {
|
||||
return !this.title && !this.message;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogAlert.is, DialogAlert);
|
|
@ -0,0 +1,62 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { localize } from "../core/locale.js";
|
||||
import "./dialog.js";
|
||||
|
||||
const defaultMessage = localize("Are you sure you want to do this?");
|
||||
const defaultConfirmLabel = localize("Confirm");
|
||||
const defaultCancelLabel = localize("Cancel");
|
||||
|
||||
class DialogConfirm extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="">
|
||||
<div class="message tiles-1">{{ message }}</div>
|
||||
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
|
||||
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-dialog-confirm";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: defaultMessage },
|
||||
open: { type: Boolean, value: false }
|
||||
};
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this._resolve === "function") && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this.resolve === "function") && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
confirm(message, confirmLabel, cancelLabel) {
|
||||
this.message = message || defaultMessage;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this.open = true;
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogConfirm.is, DialogConfirm);
|
|
@ -1,13 +1,12 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import './export.js';
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./dialog.js";
|
||||
import "./export.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
class PlExportDialog extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
|
@ -25,22 +24,26 @@ class PlExportDialog extends LocaleMixin(BaseElement) {
|
|||
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-export-dialog"; }
|
||||
static get is() {
|
||||
return "pl-dialog-export";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
records: Array
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
records: Array
|
||||
};
|
||||
}
|
||||
|
||||
_close() {
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
_close() {
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
export(records) {
|
||||
this.records = records;
|
||||
this.$.dialog.open = true;
|
||||
}
|
||||
export(records) {
|
||||
this.records = records;
|
||||
this.$.dialog.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
|
@ -1,14 +1,12 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./dialog.js";
|
||||
import "./icon.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
class RecordFieldDialog extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
|
@ -123,98 +121,105 @@ class RecordFieldDialog extends LocaleMixin(BaseElement) {
|
|||
</div>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-field-dialog"; }
|
||||
static get is() {
|
||||
return "pl-dialog-field";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
editing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "" }; }
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
editing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => {
|
||||
return { name: "", value: "" };
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
openField(field, edit = false, presets = {}) {
|
||||
this.open = true;
|
||||
this.editing = false;
|
||||
this.field = field;
|
||||
this.$.nameInput.value = presets.name || this.field.name;
|
||||
this.$.valueInput.value = presets.value || this.field.value;
|
||||
if (edit) {
|
||||
this._edit();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
openField(field, edit = false, presets = {}) {
|
||||
this.open = true;
|
||||
this.editing = false;
|
||||
this.field = field;
|
||||
this.$.nameInput.value = presets.name || this.field.name;
|
||||
this.$.valueInput.value = presets.value || this.field.value;
|
||||
if (edit) {
|
||||
this._edit();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_closeWithAction(action) {
|
||||
this.open = false;
|
||||
this._resolve && this._resolve({
|
||||
action: action,
|
||||
name: this.$.nameInput.value,
|
||||
value: this.$.valueInput.value
|
||||
});
|
||||
this._resolve = null;
|
||||
}
|
||||
_closeWithAction(action) {
|
||||
this.open = false;
|
||||
this._resolve &&
|
||||
this._resolve({
|
||||
action: action,
|
||||
name: this.$.nameInput.value,
|
||||
value: this.$.valueInput.value
|
||||
});
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_close() {
|
||||
this._closeWithAction();
|
||||
}
|
||||
_close() {
|
||||
this._closeWithAction();
|
||||
}
|
||||
|
||||
_delete() {
|
||||
this._closeWithAction("delete");
|
||||
}
|
||||
_delete() {
|
||||
this._closeWithAction("delete");
|
||||
}
|
||||
|
||||
_copy() {
|
||||
this._closeWithAction("copy");
|
||||
}
|
||||
_copy() {
|
||||
this._closeWithAction("copy");
|
||||
}
|
||||
|
||||
_generate() {
|
||||
this._closeWithAction("generate");
|
||||
}
|
||||
_generate() {
|
||||
this._closeWithAction("generate");
|
||||
}
|
||||
|
||||
_edit() {
|
||||
this.editing = true;
|
||||
setTimeout(() => {
|
||||
if (!this.$.nameInput.value) {
|
||||
this.$.nameInput.focus();
|
||||
} else {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
_edit() {
|
||||
this.editing = true;
|
||||
setTimeout(() => {
|
||||
if (!this.$.nameInput.value) {
|
||||
this.$.nameInput.focus();
|
||||
} else {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_discardChanges() {
|
||||
this.$.nameInput.value = this.field.name;
|
||||
this.$.valueInput.value = this.field.value;
|
||||
this._close();
|
||||
}
|
||||
_discardChanges() {
|
||||
this.$.nameInput.value = this.field.name;
|
||||
this.$.valueInput.value = this.field.value;
|
||||
this._close();
|
||||
}
|
||||
|
||||
_saveChanges() {
|
||||
this.field.name = this.$.nameInput.value;
|
||||
this.field.value = this.$.valueInput.value;
|
||||
this._closeWithAction("edited");
|
||||
}
|
||||
_saveChanges() {
|
||||
this.field.name = this.$.nameInput.value;
|
||||
this.field.value = this.$.valueInput.value;
|
||||
this._closeWithAction("edited");
|
||||
}
|
||||
|
||||
_inputClicked(e) {
|
||||
if (!e.target.value) {
|
||||
this.editing = true;
|
||||
}
|
||||
}
|
||||
_inputClicked(e) {
|
||||
if (!e.target.value) {
|
||||
this.editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
_nameInputEnter() {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
_nameInputEnter() {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordFieldDialog.is, RecordFieldDialog);
|
|
@ -0,0 +1,66 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { localize } from "../core/locale.js";
|
||||
import "./dialog.js";
|
||||
|
||||
class DialogOptions extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp="">
|
||||
<div class="message tiles-1">[[ message ]]</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button class\$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-dialog-options";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
options: { type: Array, value: [localize("Dismiss")] },
|
||||
preventDismiss: { type: Boolean, value: false }
|
||||
};
|
||||
}
|
||||
|
||||
choose(message, options) {
|
||||
this.message = message || "";
|
||||
this.options = options || this.options;
|
||||
|
||||
setTimeout(() => (this.open = true), 50);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hasMessage(message) {
|
||||
return !!message;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(-1);
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogOptions.is, DialogOptions);
|
|
@ -1,18 +1,19 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins, formatDateUntil } from "../core/util.js";
|
||||
import { track } from "../core/tracking.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import "./dialog.js";
|
||||
import "./icon.js";
|
||||
import "./input.js";
|
||||
import "./loading-button.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
const { LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins, formatDateUntil } = padlock.util;
|
||||
const { track } = padlock.tracking;
|
||||
/* global Stripe */
|
||||
|
||||
let stripe;
|
||||
|
||||
const stripeLoaded = new Promise((resolve) => {
|
||||
const stripeLoaded = new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://js.stripe.com/v3/";
|
||||
script.async = true;
|
||||
|
@ -20,12 +21,9 @@ const stripeLoaded = new Promise((resolve) => {
|
|||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
class PaymentDialog extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class PaymentDialog extends applyMixins(BaseElement, LocaleMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-max-width: 500px;
|
||||
|
@ -307,180 +305,182 @@ class PaymentDialog extends applyMixins(
|
|||
</button>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-payment-dialog"; }
|
||||
static get is() {
|
||||
return "pl-dialog-payment";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
open: { type: Boolean },
|
||||
promo: { type: Object, value: null },
|
||||
plan: { type: Object, value: null },
|
||||
source: Object,
|
||||
stripePubKey: { type: String, value: "" },
|
||||
csrfToken: { stype: String, value: ""},
|
||||
remainingTrialDays: { type: Number, value: 0 },
|
||||
_cardError: { type: String, value: "" },
|
||||
_needsSupport: { type: Boolean, value: false },
|
||||
_price: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
|
||||
},
|
||||
_originalPrice: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount)"
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
open: { type: Boolean },
|
||||
promo: { type: Object, value: null },
|
||||
plan: { type: Object, value: null },
|
||||
source: Object,
|
||||
stripePubKey: { type: String, value: "" },
|
||||
csrfToken: { stype: String, value: "" },
|
||||
remainingTrialDays: { type: Number, value: 0 },
|
||||
_cardError: { type: String, value: "" },
|
||||
_needsSupport: { type: Boolean, value: false },
|
||||
_price: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
|
||||
},
|
||||
_originalPrice: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount)"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
static get observers() {
|
||||
return ["_setupCountdown(promo.redeemWithin)"];
|
||||
}
|
||||
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
show(source) {
|
||||
this._needsSupport = false;
|
||||
this._cardError = "";
|
||||
this.open = true;
|
||||
this._source = source;
|
||||
show(source) {
|
||||
this._needsSupport = false;
|
||||
this._cardError = "";
|
||||
this.open = true;
|
||||
this._source = source;
|
||||
|
||||
if (!this._cardElement) {
|
||||
this._setupPayment();
|
||||
}
|
||||
if (!this._cardElement) {
|
||||
this._setupPayment();
|
||||
}
|
||||
|
||||
track("Payment Dialog: Open", {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source
|
||||
});
|
||||
track("Payment Dialog: Open", {
|
||||
Plan: this.plan && this.plan.id,
|
||||
Source: this._source
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_setupPayment() {
|
||||
stripeLoaded.then(() => {
|
||||
stripe = Stripe(this.stripePubKey);
|
||||
const elements = stripe.elements();
|
||||
const card = this._cardElement = elements.create("card", {
|
||||
iconStyle: "solid",
|
||||
style: {
|
||||
base: {
|
||||
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "18px"
|
||||
},
|
||||
invalid: {
|
||||
textShadow: "none"
|
||||
}
|
||||
}
|
||||
});
|
||||
_setupPayment() {
|
||||
stripeLoaded.then(() => {
|
||||
stripe = Stripe(this.stripePubKey);
|
||||
const elements = stripe.elements();
|
||||
const card = (this._cardElement = elements.create("card", {
|
||||
iconStyle: "solid",
|
||||
style: {
|
||||
base: {
|
||||
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "18px"
|
||||
},
|
||||
invalid: {
|
||||
textShadow: "none"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const cardElement = document.createElement("div");
|
||||
this.appendChild(cardElement);
|
||||
card.mount(cardElement);
|
||||
const cardElement = document.createElement("div");
|
||||
this.appendChild(cardElement);
|
||||
card.mount(cardElement);
|
||||
|
||||
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
|
||||
});
|
||||
}
|
||||
card.addEventListener("change", e => (this._cardError = (e.error && e.error.message) || ""));
|
||||
});
|
||||
}
|
||||
|
||||
_submitCard() {
|
||||
if (this._submittingCard) {
|
||||
return;
|
||||
}
|
||||
_submitCard() {
|
||||
if (this._submittingCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.submitButton.start();
|
||||
this._submittingCard = true;
|
||||
this.$.submitButton.start();
|
||||
this._submittingCard = true;
|
||||
|
||||
const coupon = this.promo && this.promo.coupon.id || "";
|
||||
const coupon = (this.promo && this.promo.coupon.id) || "";
|
||||
|
||||
stripe.createToken(this._cardElement).then((result) => {
|
||||
const edata = {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source,
|
||||
"Success": true,
|
||||
"Token Created": true,
|
||||
"Coupon": coupon
|
||||
};
|
||||
stripe.createToken(this._cardElement).then(result => {
|
||||
const edata = {
|
||||
Plan: this.plan && this.plan.id,
|
||||
Source: this._source,
|
||||
Success: true,
|
||||
"Token Created": true,
|
||||
Coupon: coupon
|
||||
};
|
||||
|
||||
if (result.error) {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
Object.assign(edata, {
|
||||
"Error Code": result.error.code,
|
||||
"Error Type": result.error.type,
|
||||
"Error Message": result.error.message,
|
||||
"Success": false,
|
||||
"Token Created": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
} else {
|
||||
this.source.subscribe(result.token.id, coupon, this._source)
|
||||
.then(() => {
|
||||
this.$.submitButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._submittingCard = false;
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
track("Payment Dialog: Submit", edata);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
this._cardError = e.message;
|
||||
this._needsSupport = true;
|
||||
Object.assign(edata, {
|
||||
"Error Code": e.code,
|
||||
"Error Message": e.message,
|
||||
"Success": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (result.error) {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
Object.assign(edata, {
|
||||
"Error Code": result.error.code,
|
||||
"Error Type": result.error.type,
|
||||
"Error Message": result.error.message,
|
||||
Success: false,
|
||||
"Token Created": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
} else {
|
||||
this.source
|
||||
.subscribe(result.token.id, coupon, this._source)
|
||||
.then(() => {
|
||||
this.$.submitButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._submittingCard = false;
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
track("Payment Dialog: Submit", edata);
|
||||
})
|
||||
.catch(e => {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
this._cardError = e.message;
|
||||
this._needsSupport = true;
|
||||
Object.assign(edata, {
|
||||
"Error Code": e.code,
|
||||
"Error Message": e.message,
|
||||
Success: false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasError() {
|
||||
return !!this._cardError;
|
||||
}
|
||||
_hasError() {
|
||||
return !!this._cardError;
|
||||
}
|
||||
|
||||
_calcPrice(amount, percentOff = 0, amountOff = 0) {
|
||||
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
|
||||
const d = Math.round((amount / 12) % 100);
|
||||
_calcPrice(amount, percentOff = 0, amountOff = 0) {
|
||||
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
|
||||
const d = Math.round((amount / 12) % 100);
|
||||
|
||||
return [
|
||||
`${Math.floor(amount / 1200)}`,
|
||||
d < 10 ? `.0${d}` : `.${d}`
|
||||
];
|
||||
}
|
||||
return [`${Math.floor(amount / 1200)}`, d < 10 ? `.0${d}` : `.${d}`];
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_openSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
_openSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_trialExpired() {
|
||||
return !this.remainingTrialDays;
|
||||
}
|
||||
_trialExpired() {
|
||||
return !this.remainingTrialDays;
|
||||
}
|
||||
|
||||
_hasPlan() {
|
||||
return !!this.plan;
|
||||
}
|
||||
_hasPlan() {
|
||||
return !!this.plan;
|
||||
}
|
||||
|
||||
_submitLabel() {
|
||||
return this.plan ? $l("Upgrade Now") : $l("Submit");
|
||||
}
|
||||
_submitLabel() {
|
||||
return this.plan ? $l("Upgrade Now") : $l("Submit");
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PaymentDialog.is, PaymentDialog);
|
|
@ -0,0 +1,54 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./dialog.js";
|
||||
import "./promo.js";
|
||||
|
||||
class PlPromoDialog extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #555 0%, #222 100%);
|
||||
};
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
|
||||
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-promo-dialog";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
promo: Object
|
||||
};
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
setTimeout(() => (this.$.dialog.open = true), 10);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlPromoDialog.is, PlPromoDialog);
|
|
@ -0,0 +1,115 @@
|
|||
import "../styles/shared.js";
|
||||
import { localize } from "../core/locale.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./input.js";
|
||||
import "./loading-button.js";
|
||||
import "./dialog.js";
|
||||
|
||||
const defaultConfirmLabel = localize("OK");
|
||||
const defaultCancelLabel = localize("Cancel");
|
||||
const defaultType = "text";
|
||||
const defaultPlaceholder = "";
|
||||
|
||||
class DialogPrompt extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
pl-input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-error);
|
||||
text-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<div class="message tiles-1" hidden\$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
|
||||
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
|
||||
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
|
||||
<button class="tap tiles-4" on-click="_dismiss" hidden\$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
|
||||
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-dialog-prompt";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
placeholder: { type: String, value: "" },
|
||||
preventDismiss: { type: Boolean, value: true },
|
||||
type: { type: String, value: defaultType },
|
||||
validationFn: Function,
|
||||
_validationMessage: { type: String, value: "" }
|
||||
};
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
this.$.confirmButton.start();
|
||||
const val = this.$.input.value;
|
||||
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
|
||||
p.then(v => {
|
||||
this._validationMessage = "";
|
||||
this.$.confirmButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(v);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}).catch(e => {
|
||||
this.$.dialog.rumble();
|
||||
this._validationMessage = e;
|
||||
this.$.confirmButton.fail();
|
||||
});
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(null);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_hasMessage() {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
|
||||
this.$.confirmButton.stop();
|
||||
this.message = message || "";
|
||||
this.type = type || defaultType;
|
||||
this.placeholder = placeholder || defaultPlaceholder;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this._hideCancelButton = cancelLabel === false;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.validationFn = validation;
|
||||
this._validationMessage = "";
|
||||
this.$.input.value = "";
|
||||
this.open = true;
|
||||
|
||||
setTimeout(() => this.$.input.focus(), 100);
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogPrompt.is, DialogPrompt);
|
|
@ -0,0 +1,184 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { Input } from "./input.js";
|
||||
import { AnimationMixin } from "../mixins";
|
||||
|
||||
class Dialog extends AnimationMixin(BaseElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@apply --scroll;
|
||||
}
|
||||
|
||||
:host(:not(.open)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outer {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
display: block;
|
||||
background: var(--color-background);
|
||||
opacity: 0;
|
||||
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
transform: translate3d(0, 0, 0);
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
:host(.open) .scrim {
|
||||
opacity: 0.90;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: var(--pl-dialog-max-width, 400px);
|
||||
z-index: 1;
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
border-radius: var(--border-radius);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
@apply --pl-dialog-inner;
|
||||
}
|
||||
|
||||
.outer {
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
|
||||
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host(:not(.open)) .outer {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="scrim"></div>
|
||||
|
||||
<div class="outer" on-click="dismiss">
|
||||
<slot name="before"></slot>
|
||||
<div id="inner" class="inner" on-click="_preventDismiss">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-dialog";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: {
|
||||
duration: 500,
|
||||
fullDuration: 700
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
isShowing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
preventDismiss: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// window.addEventListener("keydown", (e) => {
|
||||
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
|
||||
// this.dismiss();
|
||||
// // e.preventDefault();
|
||||
// // e.stopPropagation();
|
||||
// }
|
||||
// });
|
||||
window.addEventListener("backbutton", e => {
|
||||
if (this.open) {
|
||||
this.dismiss();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rumble() {
|
||||
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
//* Changed handler for the _open_ property. Shows/hides the dialog
|
||||
_openChanged() {
|
||||
clearTimeout(this._hideTimeout);
|
||||
|
||||
// Set _display: block_ if we're showing. If we're hiding
|
||||
// we need to wait until the transitions have finished before we
|
||||
// set _display: none_.
|
||||
if (this.open) {
|
||||
if (Input.activeInput) {
|
||||
Input.activeInput.blur();
|
||||
}
|
||||
this.style.display = "";
|
||||
this.isShowing = true;
|
||||
} else {
|
||||
this._hideTimeout = window.setTimeout(() => {
|
||||
this.style.display = "none";
|
||||
this.isShowing = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
this.offsetLeft;
|
||||
|
||||
this.classList.toggle("open", this.open);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(this.open ? "dialog-open" : "dialog-close", { bubbles: true, composed: true })
|
||||
);
|
||||
}
|
||||
|
||||
_preventDismiss(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (!this.preventDismiss) {
|
||||
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Dialog.is, Dialog);
|
|
@ -0,0 +1,165 @@
|
|||
import "../styles/shared.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./icon.js";
|
||||
import { applyMixins, passwordStrength } from "../core/util.js";
|
||||
import { isCordova, setClipboard } from "../core/platform.js";
|
||||
import { toPadlock, toCSV } from "../core/export.js";
|
||||
import { LocaleMixin, DialogMixin, DataMixin } from "../mixins";
|
||||
|
||||
const exportCSVWarning = $l(
|
||||
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
|
||||
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
|
||||
"a secure, encrypted file, instead! Are you sure you want to proceed?"
|
||||
);
|
||||
|
||||
class PlExport extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pl-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tiles tiles-1 row">
|
||||
<div class="label">[[ \$l("As CSV") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="tiles tiles-2 row">
|
||||
<div class="label">[[ \$l("As Encrypted File") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-export";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
exportRecords: Array
|
||||
};
|
||||
}
|
||||
|
||||
_downloadCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" }).then(confirm => {
|
||||
if (confirm) {
|
||||
setTimeout(() => {
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.csv`;
|
||||
const csv = toCSV(this.exportRecords);
|
||||
const a = document.createElement("a");
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_copyCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" }).then(confirm => {
|
||||
if (confirm) {
|
||||
setClipboard(toCSV(this.exportRecords)).then(() =>
|
||||
this.alert(
|
||||
$l(
|
||||
"Your data has successfully been copied to the system " +
|
||||
"clipboard. You can now paste it into the spreadsheet program of your choice."
|
||||
)
|
||||
)
|
||||
);
|
||||
this.dispatch("data-exported");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getEncryptedData() {
|
||||
return this.prompt(
|
||||
$l(
|
||||
"Please choose a password to protect your data. This may be the same as " +
|
||||
"your master password or something else, but make sure it is sufficiently strong!"
|
||||
),
|
||||
$l("Enter Password"),
|
||||
"password",
|
||||
$l("Confirm"),
|
||||
$l("Cancel")
|
||||
).then(pwd => {
|
||||
if (!pwd) {
|
||||
if (pwd === "") {
|
||||
this.alert($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
if (passwordStrength(pwd).score < 2) {
|
||||
return this.confirm(
|
||||
$l(
|
||||
"WARNING: The password you entered is weak which makes it easier for " +
|
||||
"attackers to break the encryption used to protect your data. Try to use a longer " +
|
||||
"password or include a variation of uppercase, lowercase and special characters as " +
|
||||
"well as numbers."
|
||||
),
|
||||
$l("Use Anyway"),
|
||||
$l("Choose Different Password"),
|
||||
{ type: "warning" }
|
||||
).then(confirm => {
|
||||
if (!confirm) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
});
|
||||
} else {
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_downloadEncrypted() {
|
||||
this._getEncryptedData().then(data => {
|
||||
const a = document.createElement("a");
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.pls`;
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
|
||||
a.download = fileName;
|
||||
setTimeout(() => {
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
_copyEncrypted() {
|
||||
this._getEncryptedData().then(data => {
|
||||
setClipboard(data).then(() => {
|
||||
this.alert($l("Your data has successfully been copied to the system clipboard."), { type: "success" });
|
||||
});
|
||||
this.dispatch("data-exported");
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExport.is, PlExport);
|
|
@ -1,16 +1,15 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../slider/slider.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
import "../styles/shared.js";
|
||||
import { randomString, chars } from "../core/util.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./dialog.js";
|
||||
import "./icon.js";
|
||||
import "./slider.js";
|
||||
import "./toggle-button.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
class Generator extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
|
@ -97,69 +96,73 @@ class Generator extends LocaleMixin(BaseElement) {
|
|||
<pl-icon icon="cancel" class="close-button tap" on-click="_dismiss"></pl-icon>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-generator"; }
|
||||
static get is() {
|
||||
return "pl-generator";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
lower: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
upper: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
numbers: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
other: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
lower: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
upper: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
numbers: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
other: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_generate(length, lower, upper, numbers, other)"
|
||||
]; }
|
||||
static get observers() {
|
||||
return ["_generate(length, lower, upper, numbers, other)"];
|
||||
}
|
||||
|
||||
generate() {
|
||||
this._generate();
|
||||
this.$.dialog.open = true;
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
generate() {
|
||||
this._generate();
|
||||
this.$.dialog.open = true;
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_generate() {
|
||||
var charSet = "";
|
||||
this.lower && (charSet += padlock.util.chars.lower);
|
||||
this.upper && (charSet += padlock.util.chars.upper);
|
||||
this.numbers && (charSet += padlock.util.chars.numbers);
|
||||
this.other && (charSet += padlock.util.chars.other);
|
||||
_generate() {
|
||||
var charSet = "";
|
||||
this.lower && (charSet += chars.lower);
|
||||
this.upper && (charSet += chars.upper);
|
||||
this.numbers && (charSet += chars.numbers);
|
||||
this.other && (charSet += chars.other);
|
||||
|
||||
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
|
||||
}
|
||||
this.value = charSet ? randomString(this.length, charSet) : "";
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
typeof this._resolve === "function" && this._resolve(this.value);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
_confirm() {
|
||||
typeof this._resolve === "function" && this._resolve(this.value);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(undefined);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(undefined);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Generator.is, Generator);
|
|
@ -1,10 +1,9 @@
|
|||
import "../../styles/shared";
|
||||
import { BaseElement } from "../base/base";
|
||||
import { html } from "@polymer/polymer/polymer-element";
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
|
||||
export class PlIcon extends BaseElement {
|
||||
static get template() {
|
||||
return html `
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
@ -218,7 +217,9 @@ export class PlIcon extends BaseElement {
|
|||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-icon"; }
|
||||
static get is() {
|
||||
return "pl-icon";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
|
@ -0,0 +1,258 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import autosize from "../../../../../node_modules/autosize/src/autosize.js";
|
||||
|
||||
let activeInput = null;
|
||||
|
||||
// On touch devices, blur active input when tapping on a non-input
|
||||
document.addEventListener("touchend", () => {
|
||||
if (activeInput) {
|
||||
activeInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
export class Input extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(:not([multiline])) {
|
||||
padding: 0 10px;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
text-align: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
text-shadow: inherit;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
@apply --pl-input-placeholder;
|
||||
}
|
||||
|
||||
.mask {
|
||||
@apply --fullbleed;
|
||||
pointer-events: none;
|
||||
font-size: 150%;
|
||||
line-height: 22px;
|
||||
letter-spacing: -4.5px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
input[disabled], textarea[disabled] {
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
|
||||
input[invisible], textarea[invisible] {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
|
||||
<textarea id="input" value="{{ value::input }}" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" rows="1" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" tabindex\$="[[ _tabIndex(noTab) ]]" invisible\$="[[ _showMask(masked, value, focused) ]]" disabled\$="[[ disabled ]]"></textarea>
|
||||
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled=""></textarea>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
|
||||
<input id="input" value="{{ value::input }}" tabindex\$="[[ _tabIndex(noTab) ]]" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" type\$="[[ type ]]" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" required\$="[[ required ]]" pattern\$="[[ pattern ]]" disabled\$="[[ disabled ]]" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" invisible\$="[[ _showMask(masked, value, focused) ]]">
|
||||
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled="">
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-input";
|
||||
}
|
||||
|
||||
static get activeInput() {
|
||||
return activeInput;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
autosize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
autocapitalize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notifiy: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
masked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
value: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
value: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
value: "text"
|
||||
},
|
||||
selectOnFocus: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true,
|
||||
observer: "_valueChanged"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get inputElement() {
|
||||
return this.root.querySelector(this.multiline ? "textarea" : "input");
|
||||
}
|
||||
|
||||
_domChange() {
|
||||
if (this.autosize && this.multiline && this.inputElement) {
|
||||
autosize(this.inputElement);
|
||||
}
|
||||
setTimeout(() => this._valueChanged(), 50);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_focused(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = true;
|
||||
activeInput = this;
|
||||
this.dispatchEvent(new CustomEvent("focus"));
|
||||
|
||||
if (this.selectOnFocus) {
|
||||
setTimeout(() => this.selectAll(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
_blurred(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = false;
|
||||
if (activeInput === this) {
|
||||
activeInput = null;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("blur"));
|
||||
}
|
||||
|
||||
_changeHandler(e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
_keydown(e) {
|
||||
if (e.key === "Enter" && !this.multiline) {
|
||||
this.dispatchEvent(new CustomEvent("enter"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.key === "Escape") {
|
||||
this.dispatchEvent(new CustomEvent("escape"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this.invalid = this.inputElement && !this.inputElement.checkValidity();
|
||||
if (this.autosize && this.multiline) {
|
||||
autosize.update(this.inputElement);
|
||||
}
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_showMask() {
|
||||
return this.masked && !!this.value && !this.focused;
|
||||
}
|
||||
|
||||
_mask(value) {
|
||||
return value && value.replace(/[^\n]/g, "\u2022");
|
||||
}
|
||||
|
||||
_computeAutoCapitalize() {
|
||||
return this.autocapitalize ? "" : "off";
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.inputElement.blur();
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
try {
|
||||
this.inputElement.setSelectionRange(0, this.value.length);
|
||||
} catch (e) {
|
||||
this.inputElement.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Input.is, Input);
|
|
@ -1,29 +1,19 @@
|
|||
import '../../../../../node_modules/@polymer/iron-list/iron-list.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../export/export-dialog.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import '../record-item/record-item.js';
|
||||
import '../sync/sync.js';
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import { isIOS } from "../core/platform.js";
|
||||
import "@polymer/iron-list/iron-list.js";
|
||||
import "../styles/shared.js";
|
||||
import "./dialog-export.js";
|
||||
import "./icon.js";
|
||||
import "./input.js";
|
||||
import "./record-item.js";
|
||||
|
||||
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
import { LocaleMixin, DataMixin, SyncMixin, DialogMixin, AnimationMixin } from "../mixins";
|
||||
|
||||
class ListView extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin,
|
||||
SyncMixin,
|
||||
DataMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class ListView extends applyMixins(BaseElement, LocaleMixin, SyncMixin, DataMixin, DialogMixin, AnimationMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
|
@ -222,238 +212,246 @@ class ListView extends applyMixins(
|
|||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-list-view"; }
|
||||
static get is() {
|
||||
return "pl-list-view";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
_currentSection: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: { clear: true }
|
||||
},
|
||||
filterActive: {
|
||||
type: Boolean,
|
||||
computed: "_filterActive(filterString)"
|
||||
},
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
selectedRecord: {
|
||||
type: Object,
|
||||
notify: true
|
||||
},
|
||||
selectedRecords: {
|
||||
type: Array,
|
||||
value: () => [],
|
||||
notify: true
|
||||
}
|
||||
static get properties() {
|
||||
return {
|
||||
_currentSection: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: { clear: true }
|
||||
},
|
||||
filterActive: {
|
||||
type: Boolean,
|
||||
computed: "_filterActive(filterString)"
|
||||
},
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
selectedRecord: {
|
||||
type: Object,
|
||||
notify: true
|
||||
},
|
||||
selectedRecords: {
|
||||
type: Array,
|
||||
value: () => [],
|
||||
notify: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}; }
|
||||
static get observers() {
|
||||
return [
|
||||
"_fixScroll(records)",
|
||||
"_scrollToSelected(records, selectedRecord)",
|
||||
"_updateCurrentSection(records)",
|
||||
"_selectedCountChanged(selectedRecords.length)"
|
||||
];
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_fixScroll(records)",
|
||||
"_scrollToSelected(records, selectedRecord)",
|
||||
"_updateCurrentSection(records)",
|
||||
"_selectedCountChanged(selectedRecords.length)"
|
||||
]; }
|
||||
ready() {
|
||||
super.ready();
|
||||
window.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.$.list.focusItem(this.$.list.firstVisibleIndex);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.$.list.focusItem(this.$.list.lastVisibleIndex);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.$.list.addEventListener("keydown", e => e.stopPropagation());
|
||||
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
|
||||
this.listen("data-loaded", () => {
|
||||
this.animateRecords(600);
|
||||
});
|
||||
this.listen("sync-success", e => {
|
||||
if (!e.detail || !e.detail.auto) {
|
||||
this.animateRecords();
|
||||
}
|
||||
});
|
||||
this.listen("data-imported", () => this.animateRecords());
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
window.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.$.list.focusItem(this.$.list.firstVisibleIndex);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.$.list.focusItem(this.$.list.lastVisibleIndex);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.$.list.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
|
||||
this.listen("data-loaded", () => { this.animateRecords(600); });
|
||||
this.listen("sync-success", (e) => {
|
||||
if (!e.detail || !e.detail.auto) {
|
||||
this.animateRecords();
|
||||
}
|
||||
});
|
||||
this.listen("data-imported", () => this.animateRecords());
|
||||
}
|
||||
dataUnloaded() {
|
||||
this._clearFilter();
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this._clearFilter();
|
||||
}
|
||||
select(record) {
|
||||
this.$.list.selectItem(record);
|
||||
}
|
||||
|
||||
select(record) {
|
||||
this.$.list.selectItem(record);
|
||||
}
|
||||
deselect() {
|
||||
this.$.list.clearSelection();
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.$.list.clearSelection();
|
||||
}
|
||||
recordCreated(record) {
|
||||
this.select(record);
|
||||
}
|
||||
|
||||
recordCreated(record) {
|
||||
this.select(record);
|
||||
}
|
||||
_isEmpty() {
|
||||
return !this.collection.records.filter(r => !r.removed).length;
|
||||
}
|
||||
|
||||
_isEmpty() {
|
||||
return !this.collection.records.filter((r) => !r.removed).length;
|
||||
}
|
||||
_openMenu() {
|
||||
this.dispatchEvent(new CustomEvent("open-menu"));
|
||||
}
|
||||
|
||||
_openMenu() {
|
||||
this.dispatchEvent(new CustomEvent("open-menu"));
|
||||
}
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
_filterActive() {
|
||||
return this.filterString !== "";
|
||||
}
|
||||
|
||||
_filterActive() {
|
||||
return this.filterString !== "";
|
||||
}
|
||||
_clearFilter() {
|
||||
this.set("filterString", "");
|
||||
}
|
||||
|
||||
_clearFilter() {
|
||||
this.set("filterString", "");
|
||||
}
|
||||
_toggleMenu() {
|
||||
this.dispatchEvent(new CustomEvent("toggle-menu"));
|
||||
}
|
||||
|
||||
_toggleMenu() {
|
||||
this.dispatchEvent(new CustomEvent("toggle-menu"));
|
||||
}
|
||||
_openSettings() {
|
||||
this.dispatchEvent(new CustomEvent("open-settings"));
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
this.dispatchEvent(new CustomEvent("open-settings"));
|
||||
}
|
||||
_openCloudView() {
|
||||
this.dispatchEvent(new CustomEvent("open-cloud-view"));
|
||||
}
|
||||
|
||||
_openCloudView() {
|
||||
this.dispatchEvent(new CustomEvent("open-cloud-view"));
|
||||
}
|
||||
_scrollToSelected() {
|
||||
const l = this.$.list;
|
||||
const i = l.items.indexOf(this.selectedRecord);
|
||||
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
|
||||
// Scroll to item before the selected one so that selected
|
||||
// item is more towards the middle of the list
|
||||
l.scrollToIndex(Math.max(i - 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
_scrollToSelected() {
|
||||
const l = this.$.list;
|
||||
const i = l.items.indexOf(this.selectedRecord);
|
||||
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
|
||||
// Scroll to item before the selected one so that selected
|
||||
// item is more towards the middle of the list
|
||||
l.scrollToIndex(Math.max(i - 1, 0));
|
||||
}
|
||||
}
|
||||
_fixScroll() {
|
||||
// Workaround for list losing scrollability on iOS after resetting filter
|
||||
isIOS().then(yes => {
|
||||
if (yes) {
|
||||
this.$.main.style.overflow = "hidden";
|
||||
setTimeout(() => (this.$.main.style.overflow = "auto"), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fixScroll() {
|
||||
// Workaround for list losing scrollability on iOS after resetting filter
|
||||
padlock.platform.isIOS().then((yes) => {
|
||||
if (yes) {
|
||||
this.$.main.style.overflow = "hidden";
|
||||
setTimeout(() => this.$.main.style.overflow = "auto", 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
_firstInSection(records, index) {
|
||||
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_firstInSection(records, index) {
|
||||
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
|
||||
}
|
||||
_lastInSection(records, index) {
|
||||
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_lastInSection(records, index) {
|
||||
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
|
||||
}
|
||||
_sectionHeader(index) {
|
||||
const record = this.records[index];
|
||||
return index < this._recentCount
|
||||
? $l("Recently Used")
|
||||
: (record && record.name[0] && record.name[0].toUpperCase()) || $l("No Name");
|
||||
}
|
||||
|
||||
_sectionHeader(index) {
|
||||
const record = this.records[index];
|
||||
return index < this._recentCount ? $l("Recently Used") :
|
||||
record && record.name[0] && record.name[0].toUpperCase() || $l("No Name");
|
||||
}
|
||||
_updateCurrentSection() {
|
||||
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
|
||||
}
|
||||
|
||||
_updateCurrentSection() {
|
||||
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
|
||||
}
|
||||
_selectSection() {
|
||||
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
|
||||
if (sections.length > 1) {
|
||||
this.$.sectionSelector.choose("", sections).then(i => {
|
||||
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
|
||||
this.$.list.scrollToItem(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_selectSection() {
|
||||
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
|
||||
if (sections.length > 1) {
|
||||
this.$.sectionSelector.choose("", sections)
|
||||
.then((i) => {
|
||||
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
|
||||
this.$.list.scrollToItem(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
animateRecords(delay = 100) {
|
||||
const m4e = e => this.$.list.modelForElement(e);
|
||||
|
||||
animateRecords(delay = 100) {
|
||||
const m4e = (e) => this.$.list.modelForElement(e);
|
||||
this.$.list.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
const first = this.$.list.firstVisibleIndex;
|
||||
const last = this.$.list.lastVisibleIndex + 1;
|
||||
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
|
||||
const animated = elements
|
||||
.filter(el => m4e(el).index >= first && m4e(el).index <= last)
|
||||
.sort((a, b) => m4e(a).index - m4e(b).index);
|
||||
|
||||
this.$.list.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
const first = this.$.list.firstVisibleIndex;
|
||||
const last = this.$.list.lastVisibleIndex + 1;
|
||||
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
|
||||
const animated = elements
|
||||
.filter((el) => m4e(el).index >= first && m4e(el).index <= last)
|
||||
.sort((a, b) => m4e(a).index - m4e(b).index);
|
||||
this.animateCascade(animated);
|
||||
this.$.list.style.opacity = 1;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
this.animateCascade(animated);
|
||||
this.$.list.style.opacity = 1;
|
||||
}, delay);
|
||||
}
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
_selectedCountChanged() {
|
||||
const count = this.selectedRecords && this.selectedRecords.length;
|
||||
if (this._lastSelectCount && !count) {
|
||||
this.multiSelect = false;
|
||||
}
|
||||
this._lastSelectCount = count;
|
||||
}
|
||||
|
||||
_selectedCountChanged() {
|
||||
const count = this.selectedRecords && this.selectedRecords.length;
|
||||
if (this._lastSelectCount && !count) {
|
||||
this.multiSelect = false;
|
||||
}
|
||||
this._lastSelectCount = count;
|
||||
}
|
||||
_recordMultiSelect() {
|
||||
this.multiSelect = true;
|
||||
}
|
||||
|
||||
_recordMultiSelect() {
|
||||
this.multiSelect = true;
|
||||
}
|
||||
_clearMultiSelection() {
|
||||
this.$.list.clearSelection();
|
||||
this.multiSelect = false;
|
||||
}
|
||||
|
||||
_clearMultiSelection() {
|
||||
this.$.list.clearSelection();
|
||||
this.multiSelect = false;
|
||||
}
|
||||
_selectAll() {
|
||||
this.records.forEach(r => this.$.list.selectItem(r));
|
||||
}
|
||||
|
||||
_selectAll() {
|
||||
this.records.forEach((r) => this.$.list.selectItem(r));
|
||||
}
|
||||
_shareSelected() {
|
||||
const exportDialog = this.getSingleton("pl-dialog-export");
|
||||
exportDialog.export(this.selectedRecords);
|
||||
}
|
||||
|
||||
_shareSelected() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.selectedRecords);
|
||||
}
|
||||
_deleteSelected() {
|
||||
this.confirm(
|
||||
$l(
|
||||
"Are you sure you want to delete these records? " + "This action can not be undone!",
|
||||
this.selectedRecords.length
|
||||
),
|
||||
$l("Delete {0} Records", this.selectedRecords.length)
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.deleteRecords(this.selectedRecords);
|
||||
this.multiSelect = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_deleteSelected() {
|
||||
this.confirm($l(
|
||||
"Are you sure you want to delete these records? " +
|
||||
"This action can not be undone!",
|
||||
this.selectedRecords.length
|
||||
), $l("Delete {0} Records", this.selectedRecords.length))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecords(this.selectedRecords);
|
||||
this.multiSelect = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
_multiSelectLabel(count) {
|
||||
return count ? $l("{0} records selected", count) : $l("tap to select");
|
||||
}
|
||||
|
||||
_multiSelectLabel(count) {
|
||||
return count ? $l("{0} records selected", count) : $l("tap to select");
|
||||
}
|
||||
search() {
|
||||
this.$.filterInput.focus();
|
||||
}
|
||||
|
||||
search() {
|
||||
this.$.filterInput.focus();
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.$.filterInput.value = "";
|
||||
}
|
||||
clearFilter() {
|
||||
this.$.filterInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(ListView.is, ListView);
|
|
@ -1,10 +1,10 @@
|
|||
import '../../../../../node_modules/@polymer/paper-spinner/paper-spinner-lite.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import "@polymer/paper-spinner/paper-spinner-lite.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./icon.js";
|
||||
|
||||
class LoadingButton extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class LoadingButton extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -53,62 +53,66 @@ class LoadingButton extends padlock.BaseElement {
|
|||
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-loading-button"; }
|
||||
static get is() {
|
||||
return "pl-loading-button";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
_fail: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_success: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
_fail: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_success: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
start() {
|
||||
clearTimeout(this._stopTimeout);
|
||||
this._success = this._fail = false;
|
||||
this._loading = true;
|
||||
}
|
||||
start() {
|
||||
clearTimeout(this._stopTimeout);
|
||||
this._success = this._fail = false;
|
||||
this._loading = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._success = this._fail = this._loading = false;
|
||||
}
|
||||
stop() {
|
||||
this._success = this._fail = this._loading = false;
|
||||
}
|
||||
|
||||
success() {
|
||||
this._loading = this._fail = false;
|
||||
this._success = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
success() {
|
||||
this._loading = this._fail = false;
|
||||
this._success = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
fail() {
|
||||
this._loading = this._success = false;
|
||||
this._fail = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
fail() {
|
||||
this._loading = this._success = false;
|
||||
this._fail = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_buttonClass() {
|
||||
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
|
||||
}
|
||||
_buttonClass() {
|
||||
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(LoadingButton.is, LoadingButton);
|
|
@ -0,0 +1,102 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "../elements/base.js";
|
||||
|
||||
class Notification extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
color: var(--color-background);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
}
|
||||
|
||||
:host(:not(.showing)) {
|
||||
transform: translateY(130%);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.background {
|
||||
opacity: 0.95;
|
||||
border-radius: var(--border-radius);
|
||||
@apply --fullbleed;
|
||||
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
}
|
||||
|
||||
:host(.error) .background, :host(.warning) .background {
|
||||
background: linear-gradient(90deg, #f49300 0%, #f25b00 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="background"></div>
|
||||
|
||||
<div class="text" on-click="_click">{{ message }}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-notification";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
message: String,
|
||||
type: {
|
||||
type: String,
|
||||
value: "info",
|
||||
observer: "_typeChanged"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
show(message, type, duration) {
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
this.classList.add("showing");
|
||||
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(false), duration);
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
hide(clicked) {
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve(clicked);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_typeChanged(newType, oldType) {
|
||||
this.classList.remove(oldType);
|
||||
this.classList.add(newType);
|
||||
}
|
||||
|
||||
_click() {
|
||||
this.hide(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Notification.is, Notification);
|
|
@ -1,14 +1,12 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import "../styles/shared.js";
|
||||
import { formatDateUntil, isFuture } from "../core/util.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./icon.js";
|
||||
import { LocaleMixin } from "../mixins";
|
||||
|
||||
const { LocaleMixin } = padlock;
|
||||
const { formatDateUntil, isFuture } = padlock.util;
|
||||
|
||||
class Promo extends LocaleMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class Promo extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
|
||||
button {
|
||||
|
@ -55,34 +53,38 @@ class Promo extends LocaleMixin(padlock.BaseElement) {
|
|||
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-promo"; }
|
||||
static get is() {
|
||||
return "pl-promo";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
promo: Object
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
static get observers() {
|
||||
return ["_setupCountdown(promo.redeemWithin)"];
|
||||
}
|
||||
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
if (!isFuture(p.created, p.redeemWithin)) {
|
||||
this.dispatchEvent(new CustomEvent("promo-expired"));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
if (!isFuture(p.created, p.redeemWithin)) {
|
||||
this.dispatchEvent(new CustomEvent("promo-expired"));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.dispatchEvent(new CustomEvent("promo-redeem"));
|
||||
}
|
||||
_redeem() {
|
||||
this.dispatchEvent(new CustomEvent("promo-redeem"));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Promo.is, Promo);
|
|
@ -1,26 +1,15 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import './record-field-dialog.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins } from "../core/util";
|
||||
import { isTouch } from "../core/platform";
|
||||
import "./icon.js";
|
||||
import "./input.js";
|
||||
import "./dialog-field.js";
|
||||
import { NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } from "../mixins";
|
||||
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
const { BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } = padlock;
|
||||
|
||||
class RecordField extends applyMixins(
|
||||
BaseElement,
|
||||
NotificationMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
ClipboardMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class RecordField extends applyMixins(BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
|
@ -101,88 +90,91 @@ class RecordField extends applyMixins(
|
|||
<pl-icon icon="generate" class="field-button tap" on-click="_showGenerator" hidden\$="[[ _hasValue(field.value) ]]"></pl-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-field"; }
|
||||
static get is() {
|
||||
return "pl-record-field";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
_masked: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
record: Object,
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "", masked: false }; }
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
_masked: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
record: Object,
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => {
|
||||
return { name: "", value: "", masked: false };
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
|
||||
_delete() {
|
||||
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
|
||||
}
|
||||
_delete() {
|
||||
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
_showGenerator() {
|
||||
return this.generate()
|
||||
.then((value) => {
|
||||
this.set("field.value", value);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
});
|
||||
}
|
||||
_showGenerator() {
|
||||
return this.generate().then(value => {
|
||||
this.set("field.value", value);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
});
|
||||
}
|
||||
|
||||
_copy() {
|
||||
this.setClipboard(this.record, this.field);
|
||||
}
|
||||
_copy() {
|
||||
this.setClipboard(this.record, this.field);
|
||||
}
|
||||
|
||||
_toggleMaskIcon() {
|
||||
return this.field.masked ? "show" : "hide";
|
||||
}
|
||||
_toggleMaskIcon() {
|
||||
return this.field.masked ? "show" : "hide";
|
||||
}
|
||||
|
||||
_toggleMask() {
|
||||
this.set("field.masked", !this.field.masked);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
}
|
||||
_toggleMask() {
|
||||
this.set("field.masked", !this.field.masked);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
}
|
||||
|
||||
_openFieldDialog(edit, presets) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(this.field, edit, presets))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "copy":
|
||||
this._copy();
|
||||
break;
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
this._openFieldDialog(true, { name: result.name, value: value || result.value });
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
this._delete();
|
||||
break;
|
||||
case "edited":
|
||||
this.notifyPath("field.name");
|
||||
this.notifyPath("field.value");
|
||||
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
_openFieldDialog(edit, presets) {
|
||||
this.lineUpDialog("pl-dialog-field", d => d.openField(this.field, edit, presets)).then(result => {
|
||||
switch (result.action) {
|
||||
case "copy":
|
||||
this._copy();
|
||||
break;
|
||||
case "generate":
|
||||
this.generate().then(value => {
|
||||
this._openFieldDialog(true, { name: result.name, value: value || result.value });
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
this._delete();
|
||||
break;
|
||||
case "edited":
|
||||
this.notifyPath("field.name");
|
||||
this.notifyPath("field.value");
|
||||
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasValue() {
|
||||
return !!this.field.value;
|
||||
}
|
||||
_hasValue() {
|
||||
return !!this.field.value;
|
||||
}
|
||||
|
||||
_fieldClicked() {
|
||||
this._openFieldDialog();
|
||||
}
|
||||
_fieldClicked() {
|
||||
this._openFieldDialog();
|
||||
}
|
||||
|
||||
_edit() {
|
||||
this._openFieldDialog(true);
|
||||
}
|
||||
_edit() {
|
||||
this._openFieldDialog(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordField.is, RecordField);
|
|
@ -1,22 +1,14 @@
|
|||
import '../../../../../node_modules/@polymer/polymer/lib/mixins/mutable-data.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
import "@polymer/polymer/lib/mixins/mutable-data.js";
|
||||
import "../styles/shared.js";
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { isTouch } from "../core/platform.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./icon.js";
|
||||
import { ClipboardMixin, LocaleMixin, DataMixin } from "../mixins";
|
||||
|
||||
const { ClipboardMixin, LocaleMixin, DataMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
|
||||
class RecordItem extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class RecordItem extends applyMixins(BaseElement, DataMixin, ClipboardMixin, LocaleMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
|
@ -186,68 +178,72 @@ class RecordItem extends applyMixins(
|
|||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-item"; }
|
||||
static get is() {
|
||||
return "pl-record-item";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
record: Object
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
record: Object
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
|
||||
// re-dispatch it
|
||||
this.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
ready() {
|
||||
super.ready();
|
||||
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
|
||||
// re-dispatch it
|
||||
this.addEventListener("keydown", e => {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
|
||||
_copyField(e) {
|
||||
if (!this.multiSelect) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setClipboard(this.record, e.model.item);
|
||||
const field = this.root.querySelectorAll(".field")[e.model.index];
|
||||
field.classList.add("copied");
|
||||
this.record.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
setTimeout(() => field.classList.remove("copied"), 1000);
|
||||
}
|
||||
}
|
||||
_copyField(e) {
|
||||
if (!this.multiSelect) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setClipboard(this.record, e.model.item);
|
||||
const field = this.root.querySelectorAll(".field")[e.model.index];
|
||||
field.classList.add("copied");
|
||||
this.record.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
setTimeout(() => field.classList.remove("copied"), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_fieldLabel(value) {
|
||||
return value ? value + ":" : "";
|
||||
}
|
||||
_fieldLabel(value) {
|
||||
return value ? value + ":" : "";
|
||||
}
|
||||
|
||||
_hasFields() {
|
||||
return !!this.record.fields.length;
|
||||
}
|
||||
_hasFields() {
|
||||
return !!this.record.fields.length;
|
||||
}
|
||||
|
||||
_selectIconClicked() {
|
||||
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
|
||||
}
|
||||
_selectIconClicked() {
|
||||
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
|
||||
}
|
||||
|
||||
_limitTags() {
|
||||
const tags = this.record.tags.slice(0, 2);
|
||||
const more = this.record.tags.length - tags.length;
|
||||
_limitTags() {
|
||||
const tags = this.record.tags.slice(0, 2);
|
||||
const more = this.record.tags.length - tags.length;
|
||||
|
||||
if (more) {
|
||||
tags.push("+" + more);
|
||||
}
|
||||
if (more) {
|
||||
tags.push("+" + more);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordItem.is, RecordItem);
|
|
@ -1,26 +1,16 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../animation/animation.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import './record-field.js';
|
||||
import './record-field-dialog.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import "./icon.js";
|
||||
import "./input.js";
|
||||
import "./record-field.js";
|
||||
import "./dialog-field.js";
|
||||
import { LocaleMixin, DialogMixin, DataMixin, AnimationMixin } from "../mixins";
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class RecordView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class RecordView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
|
@ -145,144 +135,145 @@ class RecordView extends applyMixins(
|
|||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-view"; }
|
||||
static get is() {
|
||||
return "pl-record-view";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
record: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
observer: "_recordChanged"
|
||||
},
|
||||
_edited: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
record: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
observer: "_recordChanged"
|
||||
},
|
||||
_edited: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this.record = null;
|
||||
const fieldDialog = this.getSingleton("pl-record-field-dialog");
|
||||
fieldDialog.open = false;
|
||||
fieldDialog.field = null;
|
||||
}
|
||||
dataUnloaded() {
|
||||
this.record = null;
|
||||
const fieldDialog = this.getSingleton("pl-dialog-field");
|
||||
fieldDialog.open = false;
|
||||
fieldDialog.field = null;
|
||||
}
|
||||
|
||||
_recordChanged() {
|
||||
setTimeout(() => {
|
||||
if (this.record && !this.record.name) {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
_recordChanged() {
|
||||
setTimeout(() => {
|
||||
if (this.record && !this.record.name) {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_notifyChange() {
|
||||
this.dispatch("record-changed", this.record);
|
||||
this.notifyPath("record");
|
||||
}
|
||||
_notifyChange() {
|
||||
this.dispatch("record-changed", this.record);
|
||||
this.notifyPath("record");
|
||||
}
|
||||
|
||||
_deleteField(e) {
|
||||
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.splice("record.fields", e.model.index, 1);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
_deleteField(e) {
|
||||
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.splice("record.fields", e.model.index, 1);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_deleteRecord() {
|
||||
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecord(this.record);
|
||||
}
|
||||
});
|
||||
}
|
||||
_deleteRecord() {
|
||||
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.deleteRecord(this.record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addField(field = { name: "", value: "", masked: false }) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(field, true))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
field.value = value;
|
||||
field.name = result.name;
|
||||
this._addField(field);
|
||||
});
|
||||
break;
|
||||
case "edited":
|
||||
this.push("record.fields", field);
|
||||
this._notifyChange();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
_addField(field = { name: "", value: "", masked: false }) {
|
||||
this.lineUpDialog("pl-dialog-field", d => d.openField(field, true)).then(result => {
|
||||
switch (result.action) {
|
||||
case "generate":
|
||||
this.generate().then(value => {
|
||||
field.value = value;
|
||||
field.name = result.name;
|
||||
this._addField(field);
|
||||
});
|
||||
break;
|
||||
case "edited":
|
||||
this.push("record.fields", field);
|
||||
this._notifyChange();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fieldButtonClicked() {
|
||||
this._addField();
|
||||
}
|
||||
_fieldButtonClicked() {
|
||||
this._addField();
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.record.tags.length;
|
||||
}
|
||||
_hasTags() {
|
||||
return !!this.record.tags.length;
|
||||
}
|
||||
|
||||
_removeTag(e) {
|
||||
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), { title: $l("Remove Tag") })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.record.removeTag(e.model.item);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
_removeTag(e) {
|
||||
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), {
|
||||
title: $l("Remove Tag")
|
||||
}).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.record.removeTag(e.model.item);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createTag() {
|
||||
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false)
|
||||
.then((tag) => {
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
_createTag() {
|
||||
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false).then(tag => {
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addTag() {
|
||||
const tags = this.collection.tags.filter((tag) => !this.record.hasTag(tag));
|
||||
if (!tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
_addTag() {
|
||||
const tags = this.collection.tags.filter(tag => !this.record.hasTag(tag));
|
||||
if (!tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false })
|
||||
.then((choice) => {
|
||||
if (choice == tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false }).then(choice => {
|
||||
if (choice == tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
const tag = tags[choice];
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
const tag = tags[choice];
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_nameEnter() {
|
||||
this.$.nameInput.blur();
|
||||
}
|
||||
_nameEnter() {
|
||||
this.$.nameInput.blur();
|
||||
}
|
||||
|
||||
animate() {
|
||||
setTimeout(() => {
|
||||
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
|
||||
}, 100);
|
||||
}
|
||||
animate() {
|
||||
setTimeout(() => {
|
||||
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dispatchEvent(new CustomEvent("record-close"));
|
||||
}
|
||||
close() {
|
||||
this.dispatchEvent(new CustomEvent("record-close"));
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
edit() {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordView.is, RecordView);
|
|
@ -0,0 +1,551 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins } from "../core/util.js";
|
||||
import { getClipboard } from "../core/platform.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import * as imp from "../core/import.js";
|
||||
import "./dialog-export.js";
|
||||
import "./icon.js";
|
||||
import "./slider.js";
|
||||
import "./toggle-button.js";
|
||||
import { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin } from "../mixins";
|
||||
import {
|
||||
isCordova,
|
||||
getReviewLink,
|
||||
isTouch,
|
||||
getDesktopSettings,
|
||||
checkForUpdates,
|
||||
saveDBAs,
|
||||
loadDB,
|
||||
isElectron
|
||||
} from "../core/platform.js";
|
||||
|
||||
class SettingsView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
@keyframes beat {
|
||||
0% { transform: scale(1); }
|
||||
5% { transform: scale(1.4); }
|
||||
15% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:host {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
section {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
section .info {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button > pl-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
pl-toggle-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.padlock-heart {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
animation: beat 5s infinite;
|
||||
}
|
||||
|
||||
.padlock-heart::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\\f004";
|
||||
}
|
||||
|
||||
.made-in {
|
||||
font-size: var(--font-size-tiny);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.db-path {
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
word-wrap: break-word;
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
|
||||
#customUrlInput:not([invalid]) + .url-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-warning {
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-top: solid 1px var(--border-color);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.feature-locked {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-error);
|
||||
margin: -14px 15px 12px 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
|
||||
<div class="title">[[ \$l("Settings") ]]</div>
|
||||
<pl-icon></pl-icon>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Auto Lock") ]]</div>
|
||||
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ \$l('Lock Automatically') ]]" class="tap" reverse="" on-change="settingChanged"></pl-toggle-button>
|
||||
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ \$l(' min') ]]" label="[[ \$l('After') ]]" hidden\$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Synchronization") ]]</div>
|
||||
<div disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ \$l('Sync Automatically') ]]" reverse="" class="tap" on-change="settingChanged"></pl-toggle-button>
|
||||
<div class="feature-locked" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log in to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
</div>
|
||||
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ \$l('Use Custom Server') ]]" reverse="" on-change="_customHostChanged" class="tap" disabled\$="[[ settings.syncConnected ]]"></pl-toggle-button>
|
||||
<div class="tap" hidden\$="[[ !settings.syncCustomHost ]]" disabled\$="[[ settings.syncConnected ]]">
|
||||
<pl-input id="customUrlInput" placeholder="[[ \$l('Enter Custom URL') ]]" value="{{ settings.syncHostUrl }}" pattern="^https://[^\\s/\$.?#].[^\\s]*\$" required="" on-change="settingChanged"></pl-input>
|
||||
<div class="url-warning">
|
||||
<strong>[[ \$l("Invalid URL") ]]</strong> -
|
||||
[[ \$l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button on-click="_changePassword" class="tap">[[ \$l("Change Master Password") ]]</button>
|
||||
<button on-click="_resetData" class="tap">[[ \$l("Reset App") ]]</button>
|
||||
<button class="tap" on-click="_import">[[ \$l("Import...") ]]</button>
|
||||
<button class="tap" on-click="_export">[[ \$l("Export...") ]]
|
||||
</button></section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Updates") ]]</div>
|
||||
<pl-toggle-button id="autoUpdatesButton" label="[[ \$l('Automatically Install Updates') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<pl-toggle-button id="betaReleasesButton" label="[[ \$l('Install Beta Releases') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<button on-click="_checkForUpdates" class="tap">[[ \$l("Check For Updates...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Database") ]]</div>
|
||||
<div class="info">
|
||||
<div>[[ \$l("Current Location:") ]]</div>
|
||||
<div class="db-path">[[ _dbPath ]]</div>
|
||||
</div>
|
||||
<button on-click="_saveDBAs" class="tap">[[ \$l("Change Location...") ]]</button>
|
||||
<button on-click="_loadDB" class="tap">[[ \$l("Load Different Database...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button class="info tap" on-click="_openSource">
|
||||
<div><strong>Padlock {{ settings.version }}</strong></div>
|
||||
<div class="made-in">Made with ♥ in Germany</div>
|
||||
</button>
|
||||
<button on-click="_openWebsite" class="tap">[[ \$l("Website") ]]</button>
|
||||
<button on-click="_sendMail" class="tap">[[ \$l("Support") ]]</button>
|
||||
<button on-click="_promptReview" class="tap">
|
||||
<span>[[ \$l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden="">
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-settings-view";
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (isElectron()) {
|
||||
const desktopSettings = getDesktopSettings().get();
|
||||
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
|
||||
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
|
||||
this._dbPath = desktopSettings.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
|
||||
}
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("settings-back"));
|
||||
}
|
||||
|
||||
//* Opens the change password dialog and resets the corresponding input elements
|
||||
_changePassword() {
|
||||
let newPwd;
|
||||
return this.promptPassword(
|
||||
this.password,
|
||||
$l("Are you sure you want to change your master password? Enter your " + "current password to continue!"),
|
||||
$l("Confirm"),
|
||||
$l("Cancel")
|
||||
)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
return this.prompt(
|
||||
$l("Now choose a new master password!"),
|
||||
$l("Enter New Password"),
|
||||
"password",
|
||||
$l("Confirm"),
|
||||
$l("Cancel"),
|
||||
false,
|
||||
val => {
|
||||
if (val === "") {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.resolve(val);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(pwd => {
|
||||
if (pwd === null) {
|
||||
return Promise.reject();
|
||||
}
|
||||
newPwd = pwd;
|
||||
return this.promptPassword(pwd, $l("Confirm your new master password!"), $l("Confirm"), $l("Cancel"));
|
||||
})
|
||||
.then(success => {
|
||||
if (success) {
|
||||
return this.setPassword(newPwd);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
return this.confirm(
|
||||
$l(
|
||||
"Do you want to update the password for you online account {0} as well?",
|
||||
this.settings.syncEmail
|
||||
),
|
||||
$l("Yes"),
|
||||
$l("No")
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
return this.setRemotePassword(this.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.alert($l("Master password changed successfully."), { type: "success" });
|
||||
});
|
||||
}
|
||||
|
||||
_openWebsite() {
|
||||
window.open("https://padlock.io", "_system");
|
||||
}
|
||||
|
||||
_sendMail() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_openGithub() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_resetData() {
|
||||
this.promptPassword(
|
||||
this.password,
|
||||
$l(
|
||||
"Are you sure you want to delete all your data and reset the app? Enter your " +
|
||||
"master password to continue!"
|
||||
),
|
||||
$l("Reset App")
|
||||
).then(success => {
|
||||
if (success) {
|
||||
return this.resetData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_import() {
|
||||
const options = [$l("From Clipboard")];
|
||||
if (!isCordova()) {
|
||||
options.push($l("From File"));
|
||||
}
|
||||
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" }).then(
|
||||
choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._importFromClipboard();
|
||||
break;
|
||||
case 1:
|
||||
this.$.importFile.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_importFile() {
|
||||
const file = this.$.importFile.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this._importString(reader.result).catch(e => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to open file. Did you enter the correct password?"), { type: "warning" });
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm(
|
||||
$l(
|
||||
"It seems the data you are trying to import was exported from a " +
|
||||
"newer version of Padlock and can not be opened with the version you are " +
|
||||
"currently running."
|
||||
),
|
||||
$l("Check For Updates"),
|
||||
$l("Cancel"),
|
||||
{ type: "info" }
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
checkForUpdates();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "invalid_csv":
|
||||
this.alert($l("Failed to recognize file format."), { type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("Failed to open file."), { type: "warning" });
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
this.$.importFile.value = "";
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
_importFromClipboard() {
|
||||
getClipboard()
|
||||
.then(str => this._importString(str))
|
||||
.catch(e => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to decrypt data. Did you enter the correct password?"), {
|
||||
type: "warning"
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.alert(
|
||||
$l(
|
||||
"No supported data found in clipboard. Please make sure to copy " +
|
||||
"you data to the clipboard first (e.g. via ctrl + C)."
|
||||
),
|
||||
{ type: "warning" }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importString(rawStr) {
|
||||
const isPadlock = imp.isFromPadlock(rawStr);
|
||||
const isSecuStore = imp.isFromSecuStore(rawStr);
|
||||
const isLastPass = imp.isFromLastPass(rawStr);
|
||||
const isCSV = imp.isCSV(rawStr);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (isPadlock || isSecuStore) {
|
||||
return this.prompt(
|
||||
$l("This file is protected by a password."),
|
||||
$l("Enter Password"),
|
||||
"password",
|
||||
$l("Confirm"),
|
||||
$l("Cancel")
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(pwd => {
|
||||
if (pwd === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPadlock) {
|
||||
return imp.fromPadlock(rawStr, pwd);
|
||||
} else if (isSecuStore) {
|
||||
return imp.fromSecuStore(rawStr, pwd);
|
||||
} else if (isLastPass) {
|
||||
return imp.fromLastPass(rawStr);
|
||||
} else if (isCSV) {
|
||||
return this.choose(
|
||||
$l(
|
||||
"The data you want to import seems to be in CSV format. Before you continue, " +
|
||||
"please make sure that the data is structured according to Padlocks specific " +
|
||||
"requirements!"
|
||||
),
|
||||
[$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")],
|
||||
{ type: "info" }
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
|
||||
// Reopen dialog for when the user comes back from the web page
|
||||
return this._importString(rawStr);
|
||||
case 1:
|
||||
return imp.fromCSV(rawStr);
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new imp.ImportError("invalid_csv");
|
||||
}
|
||||
})
|
||||
.then(records => {
|
||||
if (records) {
|
||||
this.addRecords(records);
|
||||
this.dispatch("data-imported", { records: records });
|
||||
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
|
||||
_isTouch() {
|
||||
return isTouch();
|
||||
}
|
||||
|
||||
_openSource() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_autoLockInfo() {
|
||||
return this.$l(
|
||||
"Tell Padlock to automatically lock the app after a certain period of " +
|
||||
"inactivity in case you leave your device unattended for a while."
|
||||
);
|
||||
}
|
||||
|
||||
_peekValuesInfo() {
|
||||
return this.$l(
|
||||
"If enabled allows peeking at field values in the record list " +
|
||||
"by moving the mouse cursor over the corresponding field."
|
||||
);
|
||||
}
|
||||
|
||||
_resetDataInfo() {
|
||||
return this.$l(
|
||||
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
|
||||
"and will restore the app to the state it was when you first launched it."
|
||||
);
|
||||
}
|
||||
|
||||
_promptReview() {
|
||||
this.choose(
|
||||
$l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
),
|
||||
[$l("Rate Padlock"), $l("No Thanks")]
|
||||
).then(choice => {
|
||||
if (choice === 0) {
|
||||
getReviewLink(0).then(link => window.open(link, "_system"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isDesktop() {
|
||||
return isElectron();
|
||||
}
|
||||
|
||||
_desktopSettingsChanged() {
|
||||
getDesktopSettings().set({
|
||||
autoDownloadUpdates: this.$.autoUpdatesButton.active,
|
||||
allowPrerelease: this.$.betaReleasesButton.active
|
||||
});
|
||||
}
|
||||
|
||||
_checkForUpdates() {
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
_saveDBAs() {
|
||||
saveDBAs();
|
||||
}
|
||||
|
||||
_loadDB() {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
_export() {
|
||||
const exportDialog = this.getSingleton("pl-dialog-export");
|
||||
exportDialog.export(this.records);
|
||||
}
|
||||
|
||||
_autoSyncInfoText() {
|
||||
return $l(
|
||||
"Enable Auto Sync to automatically synchronize your data with " +
|
||||
"your Padlock online account every time you make a change!"
|
||||
);
|
||||
}
|
||||
|
||||
_customHostChanged() {
|
||||
if (this.settings.syncCustomHost) {
|
||||
this.confirm(
|
||||
$l(
|
||||
"Are you sure you want to use a custom server for synchronization? " +
|
||||
"This option is only recommended for advanced users!"
|
||||
),
|
||||
$l("Continue")
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.settingChanged();
|
||||
} else {
|
||||
this.set("settings.syncCustomHost", false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.settingChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(SettingsView.is, SettingsView);
|
|
@ -1,9 +1,9 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
|
||||
class Slider extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class Slider extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -79,58 +79,62 @@ class Slider extends padlock.BaseElement {
|
|||
<input type="range" value="{{ _strValue::input }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" on-change="_inputChange">
|
||||
<span class="value-display">{{ value }}{{ unit }}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-slider"; }
|
||||
static get is() {
|
||||
return "pl-slider";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
min: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "_valueChanged",
|
||||
notify: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
hideValue: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
_strValue: {
|
||||
type: String,
|
||||
observer: "_strValueChanged"
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
min: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "_valueChanged",
|
||||
notify: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
hideValue: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
_strValue: {
|
||||
type: String,
|
||||
observer: "_strValueChanged"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_strValueChanged() {
|
||||
this.value = parseFloat(this._strValue, 10);
|
||||
}
|
||||
_strValueChanged() {
|
||||
this.value = parseFloat(this._strValue, 10);
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this._strValue = this.value.toString();
|
||||
}
|
||||
_valueChanged() {
|
||||
this._strValue = this.value.toString();
|
||||
}
|
||||
|
||||
_inputChange() {
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
_inputChange() {
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Slider.is, Slider);
|
|
@ -0,0 +1,882 @@
|
|||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import { applyMixins, wait, passwordStrength } from "../core/util";
|
||||
import { isTouch, getAppStoreLink, checkForUpdates } from "../core/platform";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import { track } from "../core/tracking.js";
|
||||
import * as stats from "../core/stats.js";
|
||||
import "./input.js";
|
||||
import "./loading-button.js";
|
||||
import { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin } from "../mixins";
|
||||
|
||||
class StartView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
|
||||
@keyframes reveal {
|
||||
from { transform: translate(0, 30px); opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
to { transform: translate(0, -200px); opacity: 0; }
|
||||
}
|
||||
|
||||
:host {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
@apply --fullbleed;
|
||||
@apply --scroll;
|
||||
color: var(--color-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
text-align: center;
|
||||
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
|
||||
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
main {
|
||||
@apply --fullbleed;
|
||||
background: transparent;
|
||||
min-height: 510px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-box {
|
||||
width: 300px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: block;
|
||||
font-size: 110px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin-bottom: 30px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.start-button pl-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.form-box pl-input, .form-box .input-wrapper {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-box pl-loading-button {
|
||||
width: var(--row-height);
|
||||
}
|
||||
|
||||
.strength-meter {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: -15px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: var(--font-size-tiny);
|
||||
width: 305px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.hint pl-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host(:not([_mode="get-started"])) .get-started,
|
||||
:host(:not([_mode="unlock"])) .unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.get-started-steps {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.get-started-step {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.get-started-step > * {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(3) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.get-started-step.left > * {
|
||||
transform: translate3d(-200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-step.right > * {
|
||||
transform: translate3d(200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-thumbs {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.get-started-thumbs > * {
|
||||
background: var(--color-foreground);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.get-started-thumbs > .right {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
button.skip {
|
||||
background: none;
|
||||
border: none;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 20px;
|
||||
margin: auto;
|
||||
font-size: var(--font-size-small);
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
text-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint.choose-password {
|
||||
margin-top: 30px;
|
||||
width: 160px;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
transition-delay: 0.4s;
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="unlock">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="form-box tiles-2 animate-in animate-out">
|
||||
<pl-input id="passwordInput" type="password" class="tap" select-on-focus="" on-enter="_unlock" no-tab="[[ open ]]" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap" label="[[ \$l('Unlock') ]]" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<main class="get-started">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="get-started-steps">
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
|
||||
|
||||
<div class="welcome-title animate-in">[[ \$l("Welcome to Padlock!") ]]</div>
|
||||
<div class="welcome-subtitle animate-in">[[ \$l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
|
||||
|
||||
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
|
||||
<div>[[ \$l("Get Started") ]]</div>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="emailInput" type="email" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterEmail" placeholder="[[ \$l('Enter Email Address') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="cloud"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_skipEmail">[[ \$l("Use Offline") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="codeInput" required="" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterCode" placeholder="[[ \$l('Enter Login Code') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="mail"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_cancelActivation">[[ \$l("Cancel") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ !_hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterCloudPassword" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_forgotCloudPassword">[[ \$l("I Forgot My Password") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ _hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="strength-meter">[[ _pwdStrength ]]</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
|
||||
</div>
|
||||
|
||||
<div on-click="_openPwdHowto" class="hint choose-password">[[ \$l("How do I choose a good master password?") ]]</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_confirmNewPassword" placeholder="[[ \$l('Confirm Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]" <="" span="">
|
||||
</span></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
|
||||
|
||||
<div class="welcome-title animate-out">[[ \$l("All done!") ]]</div>
|
||||
<div class="welcome-subtitle animate-out">[[ \$l("You're all set! Enjoy using Padlock!") ]]</div>
|
||||
|
||||
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
|
||||
<span>[[ \$l("Finish Setup") ]]</span>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="get-started-thumbs animate-in animate-out">
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return "pl-start-view";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
_getStartedStep: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
_hasData: {
|
||||
type: Boolean
|
||||
},
|
||||
_hasCloudData: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_mode: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeMode(_hasData)"
|
||||
},
|
||||
_pwdStrength: {
|
||||
type: String,
|
||||
computed: "_passwordStrength(newPwd)"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._failCount = 0;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.dataReady()
|
||||
.then(() => this.reset())
|
||||
.then(() => {
|
||||
if (!isTouch() && this._hasData) {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
this._openChanged();
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$.passwordInput.value = "";
|
||||
this.$.emailInput.value = "";
|
||||
this.$.newPasswordInput.value = "";
|
||||
this.$.confirmPasswordInput.value = "";
|
||||
this.$.cloudPwdInput.value = "";
|
||||
this.$.unlockButton.stop();
|
||||
this.$.getStartedButton.stop();
|
||||
this._failCount = 0;
|
||||
this._getStartedStep = 0;
|
||||
this._hasCloudData = false;
|
||||
return this._checkHasData();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
|
||||
_openChanged() {
|
||||
if (this.open) {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
|
||||
animation: "fade",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 0,
|
||||
fill: "forwards",
|
||||
easing: "cubic-bezier(1, 0, 0.2, 1)",
|
||||
clear: 3000
|
||||
});
|
||||
} else {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
|
||||
animation: "reveal",
|
||||
duration: 1000,
|
||||
fullDuration: 1500,
|
||||
initialDelay: 300,
|
||||
fill: "backwards",
|
||||
clear: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_startSetup() {
|
||||
this._getStartedStep = 1;
|
||||
if (!isTouch()) {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Start");
|
||||
}
|
||||
|
||||
_enterEmail() {
|
||||
this.$.emailInput.blur();
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert(this.$.emailInput.validationMessage || $l("Please enter a valid email address!"), {
|
||||
type: "warning"
|
||||
}).then(() => this.$.emailInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.emailButton.start();
|
||||
stats.set({ pairingSource: "Setup" });
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this.$.emailButton.success();
|
||||
this._getStartedStep = 2;
|
||||
this.$.codeInput.value = "";
|
||||
if (!isTouch()) {
|
||||
this.$.codeInput.focus();
|
||||
}
|
||||
})
|
||||
.catch(() => this.$.emailButton.fail());
|
||||
|
||||
track("Setup: Email", { Skipped: false, Email: this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_enterCode() {
|
||||
if (this._checkingCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$.codeInput.invalid) {
|
||||
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkingCode = true;
|
||||
|
||||
this.$.codeButton.start();
|
||||
this.activateToken(this.$.codeInput.value)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
return this.hasCloudData().then(hasData => {
|
||||
this._checkingCode = false;
|
||||
this.$.codeButton.success();
|
||||
this._hasCloudData = hasData;
|
||||
return this._connected();
|
||||
});
|
||||
} else {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
|
||||
track("Setup: Code", { Email: this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_connected() {
|
||||
setTimeout(() => {
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
_cancelActivation() {
|
||||
this.cancelConnect();
|
||||
this.$.codeInput.value = "";
|
||||
this._getStartedStep = 1;
|
||||
}
|
||||
|
||||
_skipEmail() {
|
||||
this.$.emailInput.value = "";
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Email", { Skipped: true });
|
||||
}
|
||||
|
||||
_enterNewPassword() {
|
||||
this.$.newPasswordInput.blur();
|
||||
const pwd = this.$.newPasswordInput.value;
|
||||
|
||||
if (!pwd) {
|
||||
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
this._getStartedStep = 4;
|
||||
if (!isTouch()) {
|
||||
this.$.confirmPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Choose Password");
|
||||
};
|
||||
|
||||
if (passwordStrength(pwd).score < 2) {
|
||||
this.choose(
|
||||
$l(
|
||||
"The password you entered is weak which makes it easier for attackers to break " +
|
||||
"the encryption used to protect your data. Try to use a longer password or include a " +
|
||||
"variation of uppercase, lowercase and special characters as well as numbers!"
|
||||
),
|
||||
[$l("Learn More"), $l("Choose Different Password"), $l("Use Anyway")],
|
||||
{
|
||||
type: "warning",
|
||||
title: $l("WARNING: Weak Password"),
|
||||
hideIcon: true
|
||||
}
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._openPwdHowto();
|
||||
break;
|
||||
case 1:
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
case 2:
|
||||
next();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_confirmNewPassword() {
|
||||
this.$.confirmPasswordInput.blur();
|
||||
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
|
||||
this.choose(
|
||||
$l("The password you entered does not match the original one!"),
|
||||
[$l("Try Again"), $l("Change Password")],
|
||||
{ type: "warning" }
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.$.confirmPasswordInput.focus();
|
||||
break;
|
||||
case 1:
|
||||
this._getStartedStep = 3;
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._getStartedStep = 5;
|
||||
|
||||
track("Setup: Confirm Password");
|
||||
}
|
||||
|
||||
_computeMode() {
|
||||
return this._hasData ? "unlock" : "get-started";
|
||||
}
|
||||
|
||||
_checkHasData() {
|
||||
return this.hasData().then(has => (this._hasData = has));
|
||||
}
|
||||
|
||||
_finishSetup() {
|
||||
this._initializeData();
|
||||
|
||||
track("Setup: Finish");
|
||||
}
|
||||
|
||||
_initializeData() {
|
||||
this.$.getStartedButton.start();
|
||||
if (this._initializing) {
|
||||
return;
|
||||
}
|
||||
this._initializing = true;
|
||||
|
||||
const password = this.cloudSource.password || this.$.newPasswordInput.value;
|
||||
this.cloudSource.password = password;
|
||||
|
||||
const promises = [this.initData(password), wait(1000)];
|
||||
|
||||
if (this.settings.syncConnected && !this._hasCloudData) {
|
||||
promises.push(this.collection.save(this.cloudSource));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
this.$.getStartedButton.success();
|
||||
this.$.newPasswordInput.blur();
|
||||
this._initializing = false;
|
||||
});
|
||||
}
|
||||
|
||||
_promptResetData(message) {
|
||||
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App")).then(value => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value.toUpperCase() === "RESET") {
|
||||
this.resetData();
|
||||
} else {
|
||||
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_unlock() {
|
||||
const password = this.$.passwordInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.passwordInput.blur();
|
||||
this.$.unlockButton.start();
|
||||
|
||||
if (this._unlocking) {
|
||||
return;
|
||||
}
|
||||
this._unlocking = true;
|
||||
|
||||
Promise.all([this.loadData(password), wait(1000)])
|
||||
.then(() => {
|
||||
this.$.unlockButton.success();
|
||||
this._unlocking = false;
|
||||
})
|
||||
.catch(e => {
|
||||
this.$.unlockButton.fail();
|
||||
this._unlocking = false;
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this._rumble();
|
||||
this._failCount++;
|
||||
if (this._failCount > 2) {
|
||||
this.promptForgotPassword().then(doReset => {
|
||||
if (doReset) {
|
||||
this.resetData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm(
|
||||
$l(
|
||||
"It seems the data stored on this device was saved with a newer version " +
|
||||
"of Padlock and can not be opened with the version you are currently running. " +
|
||||
"Please install the latest version of Padlock or reset the data to start over!"
|
||||
),
|
||||
$l("Check For Updates"),
|
||||
$l("Reset Data")
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
checkForUpdates();
|
||||
} else {
|
||||
this._promptResetData(
|
||||
$l(
|
||||
"Are you sure you want to reset the app? " +
|
||||
"WARNING: This will delete all your data!"
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this._promptResetData(
|
||||
$l(
|
||||
"An error occured while loading your data! If the problem persists, please try " +
|
||||
"resetting or reinstalling the app!"
|
||||
)
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_passwordStrength(pwd) {
|
||||
const score = pwd ? passwordStrength(pwd).score : -1;
|
||||
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
|
||||
return strength && $l("strength: {0}", strength);
|
||||
}
|
||||
|
||||
_getStartedHint(step) {
|
||||
return [
|
||||
$l(
|
||||
"Logging in will unlock advanced features like automatic backups and seamless " +
|
||||
"synchronization between all your devices!"
|
||||
),
|
||||
$l(
|
||||
"Your **master password** is a single passphrase used to protect your data. " +
|
||||
"Without it, nobody will be able to access your data - not even us!"
|
||||
),
|
||||
$l(
|
||||
"**Don't forget your master password!** For privacy and security reasons we never store your " +
|
||||
"password anywhere which means we won't be able to help you recover your data in case you forget " +
|
||||
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
|
||||
)
|
||||
][step];
|
||||
}
|
||||
|
||||
_getStartedClass(currStep, step) {
|
||||
return currStep > step ? "left" : currStep < step ? "right" : "center";
|
||||
}
|
||||
|
||||
_goToStep(e) {
|
||||
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
|
||||
if (s < this._getStartedStep) {
|
||||
this._getStartedStep = s;
|
||||
}
|
||||
}
|
||||
|
||||
_openProductPage() {
|
||||
getAppStoreLink().then(link => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_openPwdHowto() {
|
||||
window.open("https://padlock.io/howto/choose-master-password/", "_system");
|
||||
}
|
||||
|
||||
_rumble() {
|
||||
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`), {
|
||||
animation: "rumble",
|
||||
duration: 200,
|
||||
clear: true
|
||||
});
|
||||
}
|
||||
|
||||
_enterCloudPassword() {
|
||||
if (this._restoringCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.$.cloudPwdInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.cloudPwdInput.blur();
|
||||
this.$.cloudPwdButton.start();
|
||||
this._restoringCloud = true;
|
||||
|
||||
this.cloudSource.password = password;
|
||||
this.collection
|
||||
.fetch(this.cloudSource)
|
||||
.then(() => {
|
||||
this.$.cloudPwdButton.success();
|
||||
this._restoringCloud = false;
|
||||
this._getStartedStep = 5;
|
||||
})
|
||||
.catch(e => {
|
||||
this.$.cloudPwdButton.fail();
|
||||
this._restoringCloud = false;
|
||||
|
||||
if (e.code === "decryption_failed") {
|
||||
this._rumble();
|
||||
} else {
|
||||
this._handleCloudError(e);
|
||||
}
|
||||
});
|
||||
|
||||
track("Setup: Remote Password", { Email: this.settings.syncEmail });
|
||||
}
|
||||
|
||||
_forgotCloudPassword() {
|
||||
this.forgotCloudPassword().then(() => {
|
||||
this._hasCloudData = false;
|
||||
this._connected();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(StartView.is, StartView);
|
|
@ -1,9 +1,9 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
|
||||
class TitleBar extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class TitleBar extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
height: var(--title-bar-height);
|
||||
|
@ -148,22 +148,28 @@ class TitleBar extends padlock.BaseElement {
|
|||
<button class="close" on-click="_close"></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-title-bar"; }
|
||||
static get is() {
|
||||
return "pl-title-bar";
|
||||
}
|
||||
|
||||
_close() {
|
||||
require("electron").remote.getCurrentWindow().close();
|
||||
}
|
||||
_close() {
|
||||
require("electron")
|
||||
.remote.getCurrentWindow()
|
||||
.close();
|
||||
}
|
||||
|
||||
_minimize() {
|
||||
require("electron").remote.getCurrentWindow().minimize();
|
||||
}
|
||||
_minimize() {
|
||||
require("electron")
|
||||
.remote.getCurrentWindow()
|
||||
.minimize();
|
||||
}
|
||||
|
||||
_maximize() {
|
||||
var win = require("electron").remote.getCurrentWindow();
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
}
|
||||
_maximize() {
|
||||
var win = require("electron").remote.getCurrentWindow();
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(TitleBar.is, TitleBar);
|
|
@ -1,10 +1,10 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import './toggle.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
import "./toggle.js";
|
||||
|
||||
class ToggleButton extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class ToggleButton extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
@ -46,31 +46,35 @@ class ToggleButton extends padlock.BaseElement {
|
|||
<div>[[ label ]]</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-toggle-button"; }
|
||||
static get is() {
|
||||
return "pl-toggle-button";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.$.toggle.toggle();
|
||||
}
|
||||
toggle() {
|
||||
this.$.toggle.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(ToggleButton.is, ToggleButton);
|
|
@ -1,9 +1,9 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import "../styles/shared.js";
|
||||
import { BaseElement, html } from "./base.js";
|
||||
|
||||
class Toggle extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
class Toggle extends BaseElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="shared"></style>
|
||||
<style>
|
||||
:host {
|
||||
|
@ -47,36 +47,40 @@ class Toggle extends padlock.BaseElement {
|
|||
|
||||
<div class="knob"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get is() { return "pl-toggle"; }
|
||||
static get is() {
|
||||
return "pl-toggle";
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
notap: Boolean
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
notap: Boolean
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._tap.bind(this));
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._tap.bind(this));
|
||||
}
|
||||
|
||||
_tap() {
|
||||
if (!this.notap) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
_tap() {
|
||||
if (!this.notap) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Toggle.is, Toggle);
|
|
@ -0,0 +1,111 @@
|
|||
import { init, track, setTrackingID } from "../core/tracking.js";
|
||||
import * as statsApi from "../core/stats.js";
|
||||
import { CloudSource } from "../core/source.js";
|
||||
import { DataMixin } from ".";
|
||||
|
||||
const startedLoading = new Date().getTime();
|
||||
|
||||
init(new CloudSource(DataMixin.settings), statsApi);
|
||||
|
||||
export function AnalyticsMixin(superClass) {
|
||||
return class AnalyticsMixin extends superClass {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.listen("data-exported", () => statsApi.set({ lastExport: new Date().getTime() }));
|
||||
|
||||
this.listen("settings-changed", () => {
|
||||
statsApi.set({
|
||||
syncCustomHost: this.settings.syncCustomHost
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("data-initialized", () => {
|
||||
track("Setup", {
|
||||
"With Email": !!this.settings.syncEmail
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("data-loaded", () => track("Unlock"));
|
||||
|
||||
this.listen("data-unloaded", () => track("Lock"));
|
||||
|
||||
this.listen("data-reset", () => {
|
||||
track("Reset Local Data").then(() => setTrackingID(""));
|
||||
});
|
||||
|
||||
this.listen("sync-connect-start", e => {
|
||||
statsApi.get().then(stats => {
|
||||
track("Start Pairing", {
|
||||
Source: stats.pairingSource,
|
||||
Email: e.detail.email
|
||||
});
|
||||
statsApi.set({ startedPairing: new Date().getTime() });
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-connect-success", () => {
|
||||
return statsApi.get().then(stats => {
|
||||
track("Finish Pairing", {
|
||||
Success: true,
|
||||
Source: stats.pairingSource,
|
||||
$duration: (new Date().getTime() - stats.startedPairing) / 1000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-connect-cancel", () => {
|
||||
return statsApi.get().then(stats => {
|
||||
track("Finish Pairing", {
|
||||
Success: false,
|
||||
Canceled: true,
|
||||
Source: stats.pairingSource,
|
||||
$duration: (new Date().getTime() - stats.startedPairing) / 1000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-disconnect", () => track("Unpair").then(() => setTrackingID("")));
|
||||
|
||||
let startedSync;
|
||||
this.listen("sync-start", () => (startedSync = new Date().getTime()));
|
||||
|
||||
this.listen("sync-success", e => {
|
||||
statsApi.set({ lastSync: new Date().getTime() }).then(() =>
|
||||
track("Synchronize", {
|
||||
Success: true,
|
||||
"Auto Sync": e.detail ? e.detail.auto : false,
|
||||
$duration: (new Date().getTime() - startedSync) / 1000
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.listen("sync-fail", e =>
|
||||
track("Synchronize", {
|
||||
Success: false,
|
||||
"Auto Sync": e.detail.auto,
|
||||
"Error Code": e.detail.error.code,
|
||||
$duration: (new Date().getTime() - startedSync) / 1000
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.hasData().then(hasData =>
|
||||
track("Launch", {
|
||||
"Clean Launch": !hasData,
|
||||
$duration: (new Date().getTime() - startedLoading) / 1000
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
track(event, props) {
|
||||
// Don't track events if user is using a custom padlock cloud instance
|
||||
if (DataMixin.settings.syncCustomHost) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
track(event, props);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import '../base/base.js';
|
||||
|
||||
const defaults = {
|
||||
animation: "slideIn",
|
||||
duration: 500,
|
||||
|
@ -12,35 +10,40 @@ const defaults = {
|
|||
direction: "normal"
|
||||
};
|
||||
|
||||
padlock.AnimationMixin = (superClass) => {
|
||||
|
||||
export function AnimationMixin(superClass) {
|
||||
return class AnimationMixin extends superClass {
|
||||
|
||||
static get properties() { return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: () => { return {}; }
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
animateElement(el, opts = {}) {
|
||||
const { animation, duration, direction, easing, delay, fill, clear } =
|
||||
Object.assign({}, defaults, this.animationOptions, opts);
|
||||
const { animation, duration, direction, easing, delay, fill, clear } = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
this.animationOptions,
|
||||
opts
|
||||
);
|
||||
clearTimeout(el.clearAnimation);
|
||||
el.style.animation = "";
|
||||
el.offsetLeft;
|
||||
el.style.animation = `${animation} ${direction} ${duration}ms ${easing} ${delay}ms ${fill}`;
|
||||
if (clear) {
|
||||
const clearDelay = typeof clear === "number" ? clear : 0;
|
||||
el.clearAnimation = setTimeout(() => el.style.animation = "", delay + duration + clearDelay);
|
||||
el.clearAnimation = setTimeout(() => (el.style.animation = ""), delay + duration + clearDelay);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => setTimeout(resolve, delay + duration));
|
||||
return new Promise(resolve => setTimeout(resolve, delay + duration));
|
||||
}
|
||||
|
||||
animateCascade(els, opts = {}) {
|
||||
const { fullDuration, duration, initialDelay } =
|
||||
Object.assign({}, defaults, this.animationOptions, opts);
|
||||
const { fullDuration, duration, initialDelay } = Object.assign({}, defaults, this.animationOptions, opts);
|
||||
const dt = Math.max(30, Math.floor((fullDuration - duration) / els.length));
|
||||
|
||||
const promises = [];
|
||||
|
@ -50,6 +53,5 @@ padlock.AnimationMixin = (superClass) => {
|
|||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import '../base/base.js';
|
||||
|
||||
padlock.AutoLockMixin = (superClass) => {
|
||||
import { debounce } from "../core/util.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
|
||||
export function AutoLockMixin(superClass) {
|
||||
return class AutoLockMixin extends superClass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const moved = padlock.util.debounce(() => this._autoLockChanged(), 300);
|
||||
const moved = debounce(() => this._autoLockChanged(), 300);
|
||||
document.addEventListener("touchstart", moved, { passive: true });
|
||||
document.addEventListener("keydown", moved);
|
||||
document.addEventListener("mousemove", moved);
|
||||
|
@ -16,9 +15,9 @@ padlock.AutoLockMixin = (superClass) => {
|
|||
document.addEventListener("resume", () => this._resume());
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_autoLockChanged(settings.autoLock, settings.autoLockDelay, locked, isSynching)"
|
||||
]; }
|
||||
static get observers() {
|
||||
return ["_autoLockChanged(settings.autoLock, settings.autoLockDelay, locked, isSynching)"];
|
||||
}
|
||||
|
||||
get lockDelay() {
|
||||
return this.settings.autoLockDelay * 60 * 1000;
|
||||
|
@ -71,7 +70,5 @@ padlock.AutoLockMixin = (superClass) => {
|
|||
}, this.lockDelay - 10000);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
import '../base/base.js';
|
||||
|
||||
padlock.AutoSyncMixin = (superClass) => {
|
||||
import { debounce } from "../core/util.js";
|
||||
|
||||
export function AutoSyncMixin(superClass) {
|
||||
return class AutoSyncMixin extends superClass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const debouncedSynchronize = padlock.util.debounce(() => this.synchronize(true), 1000);
|
||||
const debouncedSynchronize = debounce(() => this.synchronize(true), 1000);
|
||||
const autoSync = () => {
|
||||
if (this.settings.syncAuto && this.settings.syncConnected) {
|
||||
debouncedSynchronize();
|
||||
|
@ -17,5 +15,4 @@ padlock.AutoSyncMixin = (superClass) => {
|
|||
this.listen("data-loaded", autoSync);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import "../elements/clipboard";
|
||||
|
||||
let clipboardSingleton;
|
||||
|
||||
export function ClipboardMixin(baseClass) {
|
||||
return class ClipboardMixin extends baseClass {
|
||||
setClipboard(record, field, duration) {
|
||||
if (!clipboardSingleton) {
|
||||
clipboardSingleton = document.createElement("pl-clipboard");
|
||||
document.body.appendChild(clipboardSingleton);
|
||||
clipboardSingleton.offsetLeft;
|
||||
}
|
||||
|
||||
return clipboardSingleton.set(record, field, duration);
|
||||
}
|
||||
|
||||
clearClipboard() {
|
||||
if (clipboardSingleton) {
|
||||
clipboardSingleton.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import "../base/base";
|
||||
import { MutableData } from "@polymer/polymer/lib/mixins/mutable-data";
|
||||
import { localize as $l } from "../locale/locale";
|
||||
import { Collection, Record, Settings } from "../../core/data";
|
||||
import { FileSource, EncryptedSource, LocalStorageSource } from "../../core/source";
|
||||
import { getDesktopSettings } from "../../core/platform";
|
||||
import { debounce } from "../../core/util";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import { Collection, Record, Settings } from "../core/data.js";
|
||||
import { FileSource, EncryptedSource, LocalStorageSource } from "../core/source.js";
|
||||
import { getDesktopSettings } from "../core/platform.js";
|
||||
import { debounce } from "../core/util.js";
|
||||
|
||||
export const collection = new Collection();
|
||||
export const settings = new Settings();
|
||||
|
||||
const desktopSettings = getDesktopSettings();
|
||||
const dbPath = desktopSettings ? desktopSettings.get("dbPath") : "data.pls";
|
||||
const collection = new Collection();
|
||||
const localSource = new EncryptedSource(new FileSource(dbPath));
|
||||
const settings = new Settings();
|
||||
const settingsSource = new EncryptedSource(new FileSource("settings.pls"));
|
||||
const dispatcher = document.createElement("div");
|
||||
|
||||
|
@ -280,5 +280,3 @@ Object.assign(DataMixin, {
|
|||
collection,
|
||||
settings
|
||||
});
|
||||
|
||||
padlock.DataMixin = DataMixin;
|
|
@ -1,20 +1,17 @@
|
|||
import '../base/base.js';
|
||||
import '../generator/generator.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog-alert.js';
|
||||
import './dialog-confirm.js';
|
||||
import './dialog-prompt.js';
|
||||
import './dialog-options.js';
|
||||
import { localize as $l } from "../core/locale";
|
||||
import "../elements/generator.js";
|
||||
import "../elements/dialog-alert.js";
|
||||
import "../elements/dialog-confirm.js";
|
||||
import "../elements/dialog-prompt.js";
|
||||
import "../elements/dialog-options.js";
|
||||
|
||||
const dialogElements = {};
|
||||
|
||||
let lastDialogPromise = Promise.resolve();
|
||||
let currentDialog;
|
||||
|
||||
padlock.DialogMixin = (superClass) => {
|
||||
|
||||
export function DialogMixin(superClass) {
|
||||
return class DialogMixin extends superClass {
|
||||
|
||||
getDialog(elName) {
|
||||
let el = dialogElements[elName];
|
||||
|
||||
|
@ -28,11 +25,10 @@ padlock.DialogMixin = (superClass) => {
|
|||
|
||||
lineUpDialog(dialog, fn) {
|
||||
dialog = typeof dialog === "string" ? this.getDialog(dialog) : dialog;
|
||||
const promise = lastDialogPromise
|
||||
.then(() => {
|
||||
currentDialog = dialog;
|
||||
return fn(dialog);
|
||||
});
|
||||
const promise = lastDialogPromise.then(() => {
|
||||
currentDialog = dialog;
|
||||
return fn(dialog);
|
||||
});
|
||||
|
||||
lastDialogPromise = promise;
|
||||
|
||||
|
@ -40,19 +36,16 @@ padlock.DialogMixin = (superClass) => {
|
|||
}
|
||||
|
||||
alert(message, options) {
|
||||
return this.lineUpDialog("pl-dialog-alert", (dialog) => dialog.show(message, options));
|
||||
return this.lineUpDialog("pl-dialog-alert", dialog => dialog.show(message, options));
|
||||
}
|
||||
|
||||
confirm(message, confirmLabel = $l("Confirm"), cancelLabel = $l("Cancel"), options = { type: "question" }) {
|
||||
options.options = [
|
||||
confirmLabel,
|
||||
cancelLabel
|
||||
];
|
||||
return this.alert(message, options).then((choice) => choice === 0);
|
||||
options.options = [confirmLabel, cancelLabel];
|
||||
return this.alert(message, options).then(choice => choice === 0);
|
||||
}
|
||||
|
||||
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss, verify) {
|
||||
return this.lineUpDialog("pl-dialog-prompt", (dialog) => {
|
||||
return this.lineUpDialog("pl-dialog-prompt", dialog => {
|
||||
return dialog.prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss, verify);
|
||||
});
|
||||
}
|
||||
|
@ -63,7 +56,7 @@ padlock.DialogMixin = (superClass) => {
|
|||
}
|
||||
|
||||
generate() {
|
||||
return this.lineUpDialog("pl-generator", (dialog) => dialog.generate());
|
||||
return this.lineUpDialog("pl-generator", dialog => dialog.generate());
|
||||
}
|
||||
|
||||
getSingleton(elName) {
|
||||
|
@ -78,7 +71,7 @@ padlock.DialogMixin = (superClass) => {
|
|||
}
|
||||
|
||||
promptPassword(password, msg, confirmLabel, cancelLabel) {
|
||||
return this.prompt(msg, $l("Enter Password"), "password", confirmLabel, cancelLabel, true, (pwd) => {
|
||||
return this.prompt(msg, $l("Enter Password"), "password", confirmLabel, cancelLabel, true, pwd => {
|
||||
if (!pwd) {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
} else if (pwd !== password) {
|
||||
|
@ -90,17 +83,25 @@ padlock.DialogMixin = (superClass) => {
|
|||
}
|
||||
|
||||
promptForgotPassword() {
|
||||
return this.confirm($l(
|
||||
"For security reasons don't keep a record of your master password so unfortunately we cannot " +
|
||||
"help you recover it. You can reset your password, but your data will be lost in the process!"
|
||||
), $l("Reset Password"), $l("Keep Trying"), { hideIcon: true, title: $l("Forgot Your Password?") })
|
||||
.then((confirmed) => {
|
||||
return confirmed && this.confirm($l(
|
||||
"Are you sure you want to reset your password? " +
|
||||
"WARNING: All your data will be lost!"
|
||||
), $l("Reset Password"), $l("Cancel"), { type: "warning" });
|
||||
});
|
||||
return this.confirm(
|
||||
$l(
|
||||
"For security reasons don't keep a record of your master password so unfortunately we cannot " +
|
||||
"help you recover it. You can reset your password, but your data will be lost in the process!"
|
||||
),
|
||||
$l("Reset Password"),
|
||||
$l("Keep Trying"),
|
||||
{ hideIcon: true, title: $l("Forgot Your Password?") }
|
||||
).then(confirmed => {
|
||||
return (
|
||||
confirmed &&
|
||||
this.confirm(
|
||||
$l("Are you sure you want to reset your password? " + "WARNING: All your data will be lost!"),
|
||||
$l("Reset Password"),
|
||||
$l("Cancel"),
|
||||
{ type: "warning" }
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
import { localize as $l } from "../core/locale";
|
||||
import { wait } from "../core/util";
|
||||
import { isChromeApp, isChromeOS, getReviewLink } from "../core/platform";
|
||||
import * as statsApi from "../core/stats";
|
||||
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
|
||||
let stats;
|
||||
const statsLoaded = statsApi.get().then(s => (stats = s));
|
||||
|
||||
function daysPassed(date) {
|
||||
return (new Date().getTime() - date) / day;
|
||||
}
|
||||
|
||||
export function HintsMixin(superClass) {
|
||||
return class HintsMixin extends superClass {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindBackup()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindSync()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._askFeedback()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._crossPlatformHint()));
|
||||
this.listen("auto-lock", () => statsLoaded.then(() => this._showAutoLockNotice()));
|
||||
this.listen("settings-changed", () => this._notifySubStatus());
|
||||
this.listen("sync-connect-success", () => this._notifySubStatus());
|
||||
this.listen("data-loaded", () => wait(1000).then(() => this._notifySubStatus()));
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
statsLoaded.then(() => wait(1000)).then(() => this._notifyChromeAppDeprecation());
|
||||
}
|
||||
|
||||
_remindBackup() {
|
||||
if (
|
||||
daysPassed(stats.lastExport || stats.firstLaunch) > 7 &&
|
||||
daysPassed(stats.lastBackupReminder || stats.firstLaunch) > 7 &&
|
||||
!this.settings.syncConnected
|
||||
) {
|
||||
this.choose(
|
||||
$l(
|
||||
"Have you backed up your data yet? Remember that by default your data is only stored " +
|
||||
"locally and can not be recovered in case your device gets damaged or lost. " +
|
||||
"Log in now to enable automatic online backups!"
|
||||
),
|
||||
[$l("Log In"), $l("Do Nothing")],
|
||||
{ type: "warning" }
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
statsApi.set({ pairingSource: "Backup Reminder" });
|
||||
this._openCloudView();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
statsApi.set({ lastBackupReminder: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
|
||||
_remindSync() {
|
||||
const daysSinceLastSync = daysPassed(stats.lastSync || stats.firstLaunch);
|
||||
if (
|
||||
this.settings.syncConnected &&
|
||||
daysSinceLastSync > 7 &&
|
||||
daysPassed(stats.lastSyncReminder || stats.firstLaunch) > 7
|
||||
) {
|
||||
this.choose(
|
||||
$l(
|
||||
"The last time you synced your data with your account was {0} days ago! You should " +
|
||||
"synchronize regularly to keep all your devices up-to-date and to make sure you always " +
|
||||
"have a recent backup in the cloud.",
|
||||
Math.floor(daysSinceLastSync)
|
||||
),
|
||||
[$l("Synchronize Now"), $l("Turn On Auto Sync"), $l("Do Nothing")],
|
||||
{ type: "warning" }
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.synchronize();
|
||||
break;
|
||||
case 1:
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
this.synchronize();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
statsApi.set({ lastSyncReminder: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
|
||||
_notifyChromeAppDeprecation() {
|
||||
isChromeOS().then(yes => {
|
||||
if (isChromeApp() && !yes) {
|
||||
this.confirm(
|
||||
$l(
|
||||
"In August 2016, Google announced that they will be discontinuing Chrome Apps for all " +
|
||||
"operating systems other than ChromeOS. This means that by early 2018, you will no " +
|
||||
"longer be able to load this app! Don't worry though, because Padlock is now " +
|
||||
"available as a native app for Windows, MacOS and Linux! Head over to our website " +
|
||||
"to find out how to switch now!"
|
||||
),
|
||||
$l("Learn More"),
|
||||
$l("Dismiss"),
|
||||
{ type: "info", hideIcon: true }
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
window.open("https://padlock.io/news/discontinuing-chrome/", "_system");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_askFeedback() {
|
||||
if (stats.launchCount > 20 && !stats.dontAskFeeback && !stats.lastAskedFeedback) {
|
||||
this.choose(
|
||||
$l("Hey there! Sorry to bother you, but we'd love to know how you are liking Padlock so far!"),
|
||||
[$l("I Love it!") + " \u2665", $l("It's not bad, but..."), $l("Hate it.")]
|
||||
).then(rating => {
|
||||
this.track("Rate App", { Rating: rating });
|
||||
|
||||
statsApi.set({
|
||||
lastAskedFeedback: new Date().getTime(),
|
||||
lastRating: rating,
|
||||
lastRatedVersion: this.settings.version
|
||||
});
|
||||
|
||||
switch (rating) {
|
||||
case 0:
|
||||
this.choose(
|
||||
$l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
),
|
||||
[$l("Rate Padlock"), $l("No Thanks")]
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.track("Review App", { Rating: rating });
|
||||
this._sendFeedback(rating);
|
||||
statsApi.set({ lastReviewed: new Date().getTime() });
|
||||
break;
|
||||
case 1:
|
||||
statsApi.set({ dontAskFeedback: true });
|
||||
break;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
this.choose(
|
||||
$l(
|
||||
"Your opinion as a user is very important to us and we're always " +
|
||||
"looking for feedback and suggestions on how to improve Padlock. " +
|
||||
"Would you mind taking a second to let us know what you think about " +
|
||||
"the app?"
|
||||
),
|
||||
[$l("Send Feedback"), $l("No Thanks")]
|
||||
).then(choice => {
|
||||
if (choice === 0) {
|
||||
this.track("Provide Feedback", { Rating: rating });
|
||||
this._sendFeedback(rating);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_sendFeedback(rating) {
|
||||
getReviewLink(rating).then(link => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_showAutoLockNotice() {
|
||||
if (!stats.hasShownAutoLockNotice) {
|
||||
const minutes = this.settings.autoLockDelay;
|
||||
setTimeout(() => {
|
||||
this.alert(
|
||||
$l(
|
||||
"Padlock was automatically locked after {0} {1} " +
|
||||
"of inactivity. You can change this behavior from the settings page.",
|
||||
minutes,
|
||||
minutes > 1 ? $l("minutes") : $l("minute")
|
||||
)
|
||||
);
|
||||
}, 1000);
|
||||
statsApi.set({ hasShownAutoLockNotice: true });
|
||||
}
|
||||
}
|
||||
|
||||
_notifySubStatus() {
|
||||
if (this.promo && !this._hasShownPromo) {
|
||||
this.alertPromo();
|
||||
this._hasShownPromo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.settings.syncSubStatus === "trialing" &&
|
||||
(!stats.lastTrialEndsReminder || daysPassed(stats.lastTrialEndsReminder) > 7)
|
||||
) {
|
||||
this.buySubscription("App - Trialing (Alert)");
|
||||
statsApi.set({ lastTrialEndsReminder: new Date().getTime() });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.syncConnected && this.settings.syncSubStatus !== this._lastSubStatus) {
|
||||
this._lastSubStatus = this.settings.syncSubStatus;
|
||||
|
||||
if (this.isTrialExpired()) {
|
||||
this.alert(this.trialExpiredMessage(), {
|
||||
type: "warning",
|
||||
title: $l("Trial Expired"),
|
||||
options: [$l("Upgrade Now")]
|
||||
}).then(choice => {
|
||||
if (choice === 0) {
|
||||
this.buySubscription("App - Trial Expired (Alert)");
|
||||
}
|
||||
});
|
||||
} else if (this.isSubUnpaid()) {
|
||||
this.alert(this.subUnpaidMessage(), {
|
||||
type: "warning",
|
||||
title: $l("Payment Failed"),
|
||||
options: [$l("Update Payment Method"), $l("Contact Support")]
|
||||
}).then(choice => {
|
||||
if (choice === 0) {
|
||||
this.updatePaymentMethod("App - Payment Failed (Alert)");
|
||||
} else if (choice === 1) {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_crossPlatformHint() {
|
||||
if (!this.settings.syncConnected && !stats.hasShownCrossPlatformHint) {
|
||||
this.confirm(
|
||||
$l(
|
||||
"Padlock lets you share your data seamlessly across all your devices like phones, " +
|
||||
"tablets and computers! Log in now and we'll get you set up in just few steps!"
|
||||
),
|
||||
$l("Log In"),
|
||||
$l("No Thanks"),
|
||||
{ title: $l("Did You Know?"), type: "question" }
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
statsApi.set({ pairingSource: "Cross-Platform Hint" });
|
||||
this._openCloudView();
|
||||
}
|
||||
});
|
||||
|
||||
statsApi.set({ hasShownCrossPlatformHint: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export { AnalyticsMixin } from "./analytics.js";
|
||||
export { AnimationMixin } from "./animation.js";
|
||||
export { AutoLockMixin } from "./auto-lock.js";
|
||||
export { AutoSyncMixin } from "./auto-sync.js";
|
||||
export { DataMixin } from "./data.js";
|
||||
export { DialogMixin } from "./dialog.js";
|
||||
export { HintsMixin } from "./hints.js";
|
||||
export { LocaleMixin } from "./locale.js";
|
||||
export { MessagesMixin } from "./messages.js";
|
||||
export { SubInfoMixin } from "./subinfo.js";
|
||||
export { SyncMixin } from "./sync.js";
|
||||
export { NotificationMixin } from "./notification.js";
|
||||
export { ClipboardMixin } from "./clipboard.js";
|
|
@ -0,0 +1,9 @@
|
|||
import { localize } from "../core/locale";
|
||||
|
||||
export function LocaleMixin(superClass) {
|
||||
return class LocaleMixin extends superClass {
|
||||
$l() {
|
||||
return localize.apply(null, arguments);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { localize as $l } from "../core/locale.js";
|
||||
import { wait } from "../core/util.js";
|
||||
import { Messages } from "../core/messages.js";
|
||||
import { FileSource } from "../core/source.js";
|
||||
|
||||
export function MessagesMixin(superClass) {
|
||||
return class MessagesMixin extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this._messages = new Messages(
|
||||
"https://padlock.io/messages.json",
|
||||
new FileSource("read-messages.json"),
|
||||
this.settings
|
||||
);
|
||||
this.listen("data-loaded", () => this.checkMessages());
|
||||
}
|
||||
|
||||
checkMessages() {
|
||||
wait(1000)
|
||||
.then(() => this._messages.fetch())
|
||||
.then(aa => aa.forEach(a => this._displayMessage(a)));
|
||||
}
|
||||
|
||||
_displayMessage(a) {
|
||||
if (a.link) {
|
||||
this.confirm(a.text, $l("Learn More"), $l("Dismiss"), { type: "info" }).then(confirmed => {
|
||||
if (confirmed) {
|
||||
window.open(a.link, "_system");
|
||||
}
|
||||
this._messages.markRead(a);
|
||||
});
|
||||
} else {
|
||||
this.alert(a.text).then(() => this._messages.markRead(a));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import "../elements/notification.js";
|
||||
|
||||
let notificationSingleton;
|
||||
|
||||
export function NotificationMixin(baseClass) {
|
||||
return class NotificationMixin extends baseClass {
|
||||
notify(message, type, duration) {
|
||||
if (!notificationSingleton) {
|
||||
notificationSingleton = document.createElement("pl-notification");
|
||||
document.body.appendChild(notificationSingleton);
|
||||
notificationSingleton.offsetLeft;
|
||||
}
|
||||
|
||||
return notificationSingleton.show(message, type, duration);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,26 +1,25 @@
|
|||
import '../base/base.js';
|
||||
|
||||
const { isFuture } = padlock.util;
|
||||
|
||||
padlock.SubInfoMixin = (superClass) => {
|
||||
import { isFuture } from "../core/util";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
|
||||
export function SubInfoMixin(superClass) {
|
||||
return class SyncMixin extends superClass {
|
||||
|
||||
static get properties() { return {
|
||||
remainingTrialDays: {
|
||||
type: Number,
|
||||
computed: "_computeRemainingTrialDays(account.subscription.trialEnd)"
|
||||
},
|
||||
promo: {
|
||||
type: Object,
|
||||
computed: "_getPromo(account.promo, subStatus)"
|
||||
},
|
||||
subStatus: {
|
||||
type: String,
|
||||
computed: "identity(account.subscription.status)",
|
||||
value: ""
|
||||
}
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
remainingTrialDays: {
|
||||
type: Number,
|
||||
computed: "_computeRemainingTrialDays(account.subscription.trialEnd)"
|
||||
},
|
||||
promo: {
|
||||
type: Object,
|
||||
computed: "_getPromo(account.promo, subStatus)"
|
||||
},
|
||||
subStatus: {
|
||||
type: String,
|
||||
computed: "identity(account.subscription.status)",
|
||||
value: ""
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
isTrialing() {
|
||||
return this.subStatus === "trialing";
|
||||
|
@ -51,7 +50,7 @@ padlock.SubInfoMixin = (superClass) => {
|
|||
trialingMessage() {
|
||||
return $l(
|
||||
"Your trial period ends in {0} days. Upgrade now to continue using online features like " +
|
||||
"synchronization and automatic backups!",
|
||||
"synchronization and automatic backups!",
|
||||
this.remainingTrialDays
|
||||
);
|
||||
}
|
||||
|
@ -59,21 +58,20 @@ padlock.SubInfoMixin = (superClass) => {
|
|||
trialExpiredMessage() {
|
||||
return $l(
|
||||
"Your free trial has expired. Upgrade now to continue using advanced features like " +
|
||||
"automatic online backups and seamless synchronization between devices!"
|
||||
"automatic online backups and seamless synchronization between devices!"
|
||||
);
|
||||
}
|
||||
|
||||
subUnpaidMessage() {
|
||||
return $l(
|
||||
"Your last payment has failed. Please contact your card provider " +
|
||||
"or update your payment method!"
|
||||
"Your last payment has failed. Please contact your card provider " + "or update your payment method!"
|
||||
);
|
||||
}
|
||||
|
||||
subCanceledMessage() {
|
||||
return $l(
|
||||
"Your subscription has been canceled. Reactivate it now to continue using advanced " +
|
||||
"features like automatic online backups and seamless synchronization between devices!"
|
||||
"features like automatic online backups and seamless synchronization between devices!"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,13 +83,9 @@ padlock.SubInfoMixin = (superClass) => {
|
|||
|
||||
_getPromo() {
|
||||
const promo = this.account && this.account.promo;
|
||||
const promoActive = !this.isSubActive() && promo && (
|
||||
!promo.redeemWithin ||
|
||||
isFuture(promo.created, promo.redeemWithin)
|
||||
);
|
||||
const promoActive =
|
||||
!this.isSubActive() && promo && (!promo.redeemWithin || isFuture(promo.created, promo.redeemWithin));
|
||||
return promoActive ? promo : null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
}
|
|
@ -1,31 +1,31 @@
|
|||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../payment-dialog/payment-dialog.js';
|
||||
import '../promo/promo-dialog.js';
|
||||
import './subinfo.js';
|
||||
import { EncryptedSource, CloudSource } from "../core/source.js";
|
||||
import { formatDateFromNow } from "../core/util.js";
|
||||
import { settings } from "./data.js";
|
||||
import { localize as $l } from "../core/locale.js";
|
||||
import * as statsApi from "../core/stats.js";
|
||||
import { checkForUpdates } from "../core/platform.js";
|
||||
import { SubInfoMixin } from ".";
|
||||
import "../elements/dialog-payment.js";
|
||||
import "../elements/dialog-promo.js";
|
||||
|
||||
const { EncryptedSource, CloudSource } = padlock.source;
|
||||
const { DataMixin, SubInfoMixin } = padlock;
|
||||
const { formatDateFromNow } = padlock.util;
|
||||
|
||||
const cloudSource = new EncryptedSource(new CloudSource(DataMixin.settings));
|
||||
|
||||
padlock.SyncMixin = (superClass) => {
|
||||
const cloudSource = new EncryptedSource(new CloudSource(settings));
|
||||
|
||||
export function SyncMixin(superClass) {
|
||||
return class SyncMixin extends SubInfoMixin(superClass) {
|
||||
|
||||
static get properties() { return {
|
||||
account: {
|
||||
type: Object,
|
||||
computed: "identity(settings.account)"
|
||||
},
|
||||
isSynching: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
lastSync: String
|
||||
}; }
|
||||
static get properties() {
|
||||
return {
|
||||
account: {
|
||||
type: Object,
|
||||
computed: "identity(settings.account)"
|
||||
},
|
||||
isSynching: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
lastSync: String
|
||||
};
|
||||
}
|
||||
|
||||
get cloudSource() {
|
||||
return cloudSource;
|
||||
|
@ -33,8 +33,8 @@ padlock.SyncMixin = (superClass) => {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.listen("data-unloaded", () => cloudSource.password = "");
|
||||
this.listen("data-reset", () => cloudSource.password = "");
|
||||
this.listen("data-unloaded", () => (cloudSource.password = ""));
|
||||
this.listen("data-reset", () => (cloudSource.password = ""));
|
||||
this.listen("sync-start", () => this._syncStart());
|
||||
this.listen("sync-success", () => this._syncSuccess());
|
||||
this.listen("sync-fail", () => this._syncFail());
|
||||
|
@ -43,22 +43,22 @@ padlock.SyncMixin = (superClass) => {
|
|||
}
|
||||
|
||||
connectCloud(email) {
|
||||
return this._requestAuthToken(email.toLowerCase(), false, "code")
|
||||
.then(() => this.dispatch("sync-connect-start", { email: email }));
|
||||
return this._requestAuthToken(email.toLowerCase(), false, "code").then(() =>
|
||||
this.dispatch("sync-connect-start", { email: email })
|
||||
);
|
||||
}
|
||||
|
||||
activateToken(code) {
|
||||
return cloudSource.source.activateToken(code)
|
||||
.then((success) => {
|
||||
this.settings.syncConnected = success;
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
return cloudSource.source.activateToken(code).then(success => {
|
||||
this.settings.syncConnected = success;
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
|
||||
if (success) {
|
||||
this.dispatch("sync-connect-success");
|
||||
}
|
||||
return success;
|
||||
});
|
||||
if (success) {
|
||||
this.dispatch("sync-connect-success");
|
||||
}
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
cancelConnect() {
|
||||
|
@ -79,13 +79,14 @@ padlock.SyncMixin = (superClass) => {
|
|||
this.dispatch("sync-disconnect");
|
||||
};
|
||||
|
||||
this.cloudSource.source.logout()
|
||||
this.cloudSource.source
|
||||
.logout()
|
||||
.then(done)
|
||||
.catch(done);
|
||||
}
|
||||
|
||||
synchronize(auto) {
|
||||
if (!this.settings.syncConnected || auto === true && !this.isSubValid()) {
|
||||
if (!this.settings.syncConnected || (auto === true && !this.isSubValid())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -97,27 +98,30 @@ padlock.SyncMixin = (superClass) => {
|
|||
if (this._currentSync) {
|
||||
// There is already a synchronization in process. wait for the current sync to finish
|
||||
// before starting a new one.
|
||||
const chained = this._chainedSync = this._currentSync
|
||||
.then(() => {
|
||||
this._chainedSync = null;
|
||||
return this.synchronize();
|
||||
});
|
||||
const chained = (this._chainedSync = this._currentSync.then(() => {
|
||||
this._chainedSync = null;
|
||||
return this.synchronize();
|
||||
}));
|
||||
return chained;
|
||||
}
|
||||
|
||||
const sync = this._currentSync = this._synchronize(auto)
|
||||
.then(() => this._currentSync = null, () => this._currentSync = null);
|
||||
const sync = (this._currentSync = this._synchronize(auto).then(
|
||||
() => (this._currentSync = null),
|
||||
() => (this._currentSync = null)
|
||||
));
|
||||
return sync;
|
||||
}
|
||||
|
||||
setRemotePassword(password) {
|
||||
this.dispatch("sync-start");
|
||||
cloudSource.password = password;
|
||||
return this.collection.save(cloudSource)
|
||||
return this.collection
|
||||
.save(cloudSource)
|
||||
.then(() => {
|
||||
this.dispatch("sync-success");
|
||||
this.alert($l("Successfully updated the password for your account {0}.", this.settings.syncEmail),
|
||||
{ type: "success" });
|
||||
this.alert($l("Successfully updated the password for your account {0}.", this.settings.syncEmail), {
|
||||
type: "success"
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.dispatch("sync-fail");
|
||||
|
@ -138,35 +142,39 @@ padlock.SyncMixin = (superClass) => {
|
|||
return Promise.reject();
|
||||
}
|
||||
|
||||
this._testCredsPromise = this._testCredsPromise || cloudSource.source.testCredentials()
|
||||
.then((connected) => {
|
||||
this._testCredsPromise = null;
|
||||
this.settings.syncConnected = connected;
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
this._testCredsPromise =
|
||||
this._testCredsPromise ||
|
||||
cloudSource.source
|
||||
.testCredentials()
|
||||
.then(connected => {
|
||||
this._testCredsPromise = null;
|
||||
this.settings.syncConnected = connected;
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
|
||||
if (connected) {
|
||||
this.dispatch("sync-connect-success");
|
||||
if (connected) {
|
||||
this.dispatch("sync-connect-success");
|
||||
|
||||
this.confirm(
|
||||
$l("You successfully paired this device with Padlock Cloud!"),
|
||||
$l("Synchonize Now"), $l("Dismiss")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.synchronize();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.confirm(
|
||||
$l("You successfully paired this device with Padlock Cloud!"),
|
||||
$l("Synchonize Now"),
|
||||
$l("Dismiss")
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.synchronize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!connected && pollInterval) {
|
||||
setTimeout(() => this.testCredentials(pollInterval), pollInterval);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this._testCredsPromise = null;
|
||||
this.cancelConnect();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
if (!connected && pollInterval) {
|
||||
setTimeout(() => this.testCredentials(pollInterval), pollInterval);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this._testCredsPromise = null;
|
||||
this.cancelConnect();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
|
||||
return this._testCredsPromise;
|
||||
}
|
||||
|
@ -174,8 +182,9 @@ padlock.SyncMixin = (superClass) => {
|
|||
openDashboard(action = "", referer = "app") {
|
||||
action = encodeURIComponent(action);
|
||||
referer = encodeURIComponent(referer);
|
||||
cloudSource.source.getLoginUrl(`/dashboard/?action=${action}&ref=${referer}`)
|
||||
.then((url) => window.open(url, "_system"))
|
||||
cloudSource.source
|
||||
.getLoginUrl(`/dashboard/?action=${action}&ref=${referer}`)
|
||||
.then(url => window.open(url, "_system"))
|
||||
.catch(() => window.open(`${this.settings.syncHostUrl}/dashboard/`, "_system"));
|
||||
}
|
||||
|
||||
|
@ -187,7 +196,7 @@ padlock.SyncMixin = (superClass) => {
|
|||
$l("Confirm"),
|
||||
$l("Cancel"),
|
||||
true,
|
||||
(code) => {
|
||||
code => {
|
||||
if (code === null) {
|
||||
// Dialog canceled
|
||||
this.cancelConnect();
|
||||
|
@ -196,12 +205,12 @@ padlock.SyncMixin = (superClass) => {
|
|||
return Promise.reject("Please enter a valid login code!");
|
||||
} else {
|
||||
return this.activateToken(code)
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
this._handleCloudError(e);
|
||||
this.cancelConnect();
|
||||
return e;
|
||||
})
|
||||
.then((success) => {
|
||||
.then(success => {
|
||||
// Error from previous catch clause; Cancel dialog
|
||||
if (success.code) {
|
||||
return null;
|
||||
|
@ -218,8 +227,14 @@ padlock.SyncMixin = (superClass) => {
|
|||
}
|
||||
|
||||
loginDialog() {
|
||||
return this.prompt(this.loginInfoText(), $l("Enter Email Address"), "email", $l("Login"), false, false,
|
||||
(val) => {
|
||||
return this.prompt(
|
||||
this.loginInfoText(),
|
||||
$l("Enter Email Address"),
|
||||
"email",
|
||||
$l("Login"),
|
||||
false,
|
||||
false,
|
||||
val => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "email";
|
||||
input.value = val;
|
||||
|
@ -232,17 +247,17 @@ padlock.SyncMixin = (superClass) => {
|
|||
}
|
||||
}
|
||||
)
|
||||
.then((val) => val === null ? Promise.reject() : val)
|
||||
.then(val => (val === null ? Promise.reject() : val))
|
||||
.then(() => this.promptLoginCode())
|
||||
.then((val) => val === null ? Promise.reject() : val)
|
||||
.then(val => (val === null ? Promise.reject() : val))
|
||||
.then(() => this.synchronize())
|
||||
.catch((e) => e && this._handleCloudError(e));
|
||||
.catch(e => e && this._handleCloudError(e));
|
||||
}
|
||||
|
||||
loginInfoText() {
|
||||
return $l(
|
||||
"Log in now to unlock advanced features like automatic online backups and " +
|
||||
"seamless synchronization between devices!"
|
||||
"seamless synchronization between devices!"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -254,7 +269,8 @@ padlock.SyncMixin = (superClass) => {
|
|||
cloudSource.password = this.password;
|
||||
}
|
||||
|
||||
return this.collection.fetch(cloudSource)
|
||||
return this.collection
|
||||
.fetch(cloudSource)
|
||||
.then(() => this.saveCollection())
|
||||
.then(() => this.collection.save(cloudSource))
|
||||
.then(() => {
|
||||
|
@ -264,21 +280,17 @@ padlock.SyncMixin = (superClass) => {
|
|||
|
||||
if (cloudSource.password !== this.password) {
|
||||
return this.choose(
|
||||
$l("The master password you use locally does not match the one of your " +
|
||||
"online account {0}. What do you want to do?", this.settings.syncEmail),
|
||||
[
|
||||
$l("Update Local Password"),
|
||||
$l("Update Online Password"),
|
||||
$l("Keep Both Passwords")
|
||||
]
|
||||
).then((choice) => {
|
||||
$l(
|
||||
"The master password you use locally does not match the one of your " +
|
||||
"online account {0}. What do you want to do?",
|
||||
this.settings.syncEmail
|
||||
),
|
||||
[$l("Update Local Password"), $l("Update Online Password"), $l("Keep Both Passwords")]
|
||||
).then(choice => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.setPassword(cloudSource.password).then(() => {
|
||||
this.alert(
|
||||
$l("Local password updated successfully."),
|
||||
{ type: "success" }
|
||||
);
|
||||
this.alert($l("Local password updated successfully."), { type: "success" });
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
|
@ -288,7 +300,7 @@ padlock.SyncMixin = (superClass) => {
|
|||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
this.dispatch("settings-changed");
|
||||
if (this._handleCloudError(e)) {
|
||||
this.dispatch("records-changed");
|
||||
|
@ -321,12 +333,13 @@ padlock.SyncMixin = (superClass) => {
|
|||
this.settings.syncToken = "";
|
||||
this.dispatch("settings-changed");
|
||||
|
||||
return padlock.stats.get()
|
||||
.then((stats) => {
|
||||
return statsApi
|
||||
.get()
|
||||
.then(stats => {
|
||||
const redirect = `/dashboard/?tid=${encodeURIComponent(stats.trackingID)}`;
|
||||
return cloudSource.source.requestAuthToken(email, create, redirect, actType);
|
||||
})
|
||||
.then((authToken) => {
|
||||
.then(authToken => {
|
||||
// We're getting back the api key directly, but it will valid only
|
||||
// after the user has visited the activation link in the email he was sent
|
||||
this.settings.syncConnected = false;
|
||||
|
@ -335,14 +348,18 @@ padlock.SyncMixin = (superClass) => {
|
|||
this.settings.syncId = authToken.id;
|
||||
this.dispatch("settings-changed");
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
this.dispatch("settings-changed");
|
||||
switch (typeof e === "string" ? e : e.code) {
|
||||
case "account_not_found":
|
||||
return this._requestAuthToken(email, true, actType);
|
||||
case "rate_limit_exceeded":
|
||||
this.alert($l("For security reasons only a limited amount of connection request " +
|
||||
"are allowed at a time. Please wait a little before trying again!"));
|
||||
this.alert(
|
||||
$l(
|
||||
"For security reasons only a limited amount of connection request " +
|
||||
"are allowed at a time. Please wait a little before trying again!"
|
||||
)
|
||||
);
|
||||
throw e;
|
||||
default:
|
||||
this._handleCloudError(e);
|
||||
|
@ -372,14 +389,16 @@ padlock.SyncMixin = (superClass) => {
|
|||
return false;
|
||||
case "deprecated_api_version":
|
||||
this.confirm(
|
||||
$l("A newer version of Padlock is available now! Update now to keep using " +
|
||||
"online features (you won't be able to sync with your account until then)!"),
|
||||
$l(
|
||||
"A newer version of Padlock is available now! Update now to keep using " +
|
||||
"online features (you won't be able to sync with your account until then)!"
|
||||
),
|
||||
$l("Update Now"),
|
||||
$l("Cancel"),
|
||||
{ type: "info" }
|
||||
).then((confirm) => {
|
||||
).then(confirm => {
|
||||
if (confirm) {
|
||||
padlock.platform.checkForUpdates();
|
||||
checkForUpdates();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
|
@ -388,12 +407,15 @@ padlock.SyncMixin = (superClass) => {
|
|||
return false;
|
||||
case "invalid_container_data":
|
||||
case "invalid_key_params":
|
||||
this.alert($l(
|
||||
"The data received from your online account seems to be corrupt and " +
|
||||
"cannot be decrypted. This might be due to a network error but could " +
|
||||
"also be the result of someone trying to compromise your connection to " +
|
||||
"our servers. If the problem persists, please notify Padlock support!"
|
||||
), { type: "warning" });
|
||||
this.alert(
|
||||
$l(
|
||||
"The data received from your online account seems to be corrupt and " +
|
||||
"cannot be decrypted. This might be due to a network error but could " +
|
||||
"also be the result of someone trying to compromise your connection to " +
|
||||
"our servers. If the problem persists, please notify Padlock support!"
|
||||
),
|
||||
{ type: "warning" }
|
||||
);
|
||||
return false;
|
||||
case "decryption_failed":
|
||||
case "encryption_failed":
|
||||
|
@ -402,14 +424,20 @@ padlock.SyncMixin = (superClass) => {
|
|||
// we need to prompt the user for the correct password.
|
||||
this.prompt(
|
||||
$l("Please enter the master password for the online account {0}.", this.settings.syncEmail),
|
||||
$l("Enter Master Password"), "password", $l("Submit"), $l("Forgot Password"), true, (pwd) => {
|
||||
$l("Enter Master Password"),
|
||||
"password",
|
||||
$l("Submit"),
|
||||
$l("Forgot Password"),
|
||||
true,
|
||||
pwd => {
|
||||
if (!pwd) {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
}
|
||||
cloudSource.password = pwd;
|
||||
return this.collection.fetch(cloudSource)
|
||||
return this.collection
|
||||
.fetch(cloudSource)
|
||||
.then(() => true)
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
if (e.code == "decryption_failed") {
|
||||
throw $l("Incorrect password. Please try again!");
|
||||
} else {
|
||||
|
@ -417,42 +445,49 @@ padlock.SyncMixin = (superClass) => {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((success) => {
|
||||
if (success === null) {
|
||||
this.forgotCloudPassword()
|
||||
.then(() => this.synchronize());
|
||||
}
|
||||
if (success) {
|
||||
this.synchronize();
|
||||
}
|
||||
});
|
||||
}
|
||||
).then(success => {
|
||||
if (success === null) {
|
||||
this.forgotCloudPassword().then(() => this.synchronize());
|
||||
}
|
||||
if (success) {
|
||||
this.synchronize();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data stored on Padlock Cloud was saved with a newer version of Padlock " +
|
||||
"and can not be opened with the version you are currently running. Please install the " +
|
||||
"latest version of Padlock on this device!"
|
||||
), $l("Check For Updates"), $l("Cancel")).
|
||||
then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
}
|
||||
});
|
||||
this.confirm(
|
||||
$l(
|
||||
"It seems the data stored on Padlock Cloud was saved with a newer version of Padlock " +
|
||||
"and can not be opened with the version you are currently running. Please " +
|
||||
"install the latest version of Padlock on this device!"
|
||||
),
|
||||
$l("Check For Updates"),
|
||||
$l("Cancel")
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
checkForUpdates();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
case "subscription_required":
|
||||
return true;
|
||||
case "failed_connection":
|
||||
this.alert($l("Looks like we can't connect to our servers right now. Please check your internet " +
|
||||
"connection and try again!"), { type: "warning", title: $l("Failed Connection") });
|
||||
this.alert(
|
||||
$l(
|
||||
"Looks like we can't connect to our servers right now. Please check your internet " +
|
||||
"connection and try again!"
|
||||
),
|
||||
{ type: "warning", title: $l("Failed Connection") }
|
||||
);
|
||||
return false;
|
||||
default:
|
||||
this.confirm(
|
||||
e && e.message || $l("Something went wrong. Please try again later!"),
|
||||
(e && e.message) || $l("Something went wrong. Please try again later!"),
|
||||
$l("Contact Support"),
|
||||
$l("Dismiss"),
|
||||
{ type: "warning" }
|
||||
).then((confirmed) => {
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
window.open(`mailto:support@padlock.io?subject=Server+Error+(${e.code})`);
|
||||
}
|
||||
|
@ -466,46 +501,42 @@ padlock.SyncMixin = (superClass) => {
|
|||
}
|
||||
|
||||
forgotCloudPassword() {
|
||||
return this.promptForgotPassword()
|
||||
.then((doReset) => {
|
||||
if (doReset) {
|
||||
return this.cloudSource.clear();
|
||||
}
|
||||
});
|
||||
return this.promptForgotPassword().then(doReset => {
|
||||
if (doReset) {
|
||||
return this.cloudSource.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateLastSync() {
|
||||
return padlock.stats.get().then((s) => this.lastSync = formatDateFromNow(s.lastSync));
|
||||
return statsApi.get().then(s => (this.lastSync = formatDateFromNow(s.lastSync)));
|
||||
}
|
||||
|
||||
buySubscription(source) {
|
||||
if (!this._plansPromise) {
|
||||
this._plansPromise = this.cloudSource.source.getPlans();
|
||||
}
|
||||
this._plansPromise.then((plans) => this.openPaymentDialog(plans[0], source))
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.refreshAccount();
|
||||
this.alert(
|
||||
$l("Congratulations, your upgrade was successful! Enjoy using Padlock!"),
|
||||
{ type: "success" }
|
||||
);
|
||||
}
|
||||
});
|
||||
this._plansPromise.then(plans => this.openPaymentDialog(plans[0], source)).then(success => {
|
||||
if (success) {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Congratulations, your upgrade was successful! Enjoy using Padlock!"), {
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updatePaymentMethod(source) {
|
||||
this.openPaymentDialog(null, source)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Payment method updated successfully!"), { type: "success" });
|
||||
}
|
||||
});
|
||||
this.openPaymentDialog(null, source).then(success => {
|
||||
if (success) {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Payment method updated successfully!"), { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openPaymentDialog(plan, source) {
|
||||
return this.lineUpDialog("pl-payment-dialog", (dialog) => {
|
||||
return this.lineUpDialog("pl-dialog-payment", dialog => {
|
||||
dialog.plan = plan;
|
||||
dialog.stripePubKey = this.settings.stripePubKey;
|
||||
dialog.source = this.cloudSource.source;
|
||||
|
@ -516,18 +547,20 @@ padlock.SyncMixin = (superClass) => {
|
|||
}
|
||||
|
||||
reactivateSubscription() {
|
||||
return this.cloudSource.source.subscribe()
|
||||
return this.cloudSource.source
|
||||
.subscribe()
|
||||
.then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Subscription reactivated successfully!"), { type: "success" });
|
||||
})
|
||||
.catch((e) => this._handleCloudError(e));
|
||||
.catch(e => this._handleCloudError(e));
|
||||
}
|
||||
|
||||
refreshAccount() {
|
||||
if (this.settings.syncConnected) {
|
||||
return this.cloudSource.source.getAccountInfo()
|
||||
.catch((e) => this._handleCloudError(e))
|
||||
return this.cloudSource.source
|
||||
.getAccountInfo()
|
||||
.catch(e => this._handleCloudError(e))
|
||||
.then(() => this.dispatch("settings-changed"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
|
@ -536,35 +569,33 @@ padlock.SyncMixin = (superClass) => {
|
|||
|
||||
cancelSubscription() {
|
||||
return this.confirm(
|
||||
$l("Are you sure you want to cancel your subscription? You won't be able " +
|
||||
"to continue using advanced features like automatic online backups and seamless " +
|
||||
"synchronization between devices!"),
|
||||
$l(
|
||||
"Are you sure you want to cancel your subscription? You won't be able " +
|
||||
"to continue using advanced features like automatic online backups and seamless " +
|
||||
"synchronization between devices!"
|
||||
),
|
||||
$l("Cancel Subscription"),
|
||||
$l("Don't Cancel"),
|
||||
{ type: "warning" }
|
||||
).then((confirmed) => {
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.cloudSource.source.cancelSubscription()
|
||||
.then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Subscription canceled successfully."), { type: "success" });
|
||||
});
|
||||
this.cloudSource.source.cancelSubscription().then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Subscription canceled successfully."), { type: "success" });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
alertPromo() {
|
||||
return this.lineUpDialog("pl-promo-dialog", (dialog) => {
|
||||
return this.lineUpDialog("pl-dialog-promo", dialog => {
|
||||
dialog.promo = this.promo;
|
||||
return dialog.show();
|
||||
})
|
||||
.then((redeem) => {
|
||||
if (redeem) {
|
||||
this.buySubscription("App - Promo (Alert)");
|
||||
}
|
||||
});
|
||||
}).then(redeem => {
|
||||
if (redeem) {
|
||||
this.buySubscription("App - Promo (Alert)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
}
|
File diff suppressed because one or more lines are too long
33372
app/src/padlock.js
33372
app/src/padlock.js
File diff suppressed because one or more lines are too long
|
@ -1,114 +0,0 @@
|
|||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
|
||||
const startedLoading = new Date().getTime();
|
||||
const { init, track, setTrackingID } = padlock.tracking;
|
||||
const { DataMixin } = padlock;
|
||||
const { CloudSource } = padlock.source;
|
||||
|
||||
init(new CloudSource(DataMixin.settings), padlock.stats);
|
||||
|
||||
padlock.AnalyticsMixin = (superClass) => {
|
||||
|
||||
return class AnalyticsMixin extends superClass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.listen("data-exported", () => padlock.stats.set({ lastExport: new Date().getTime() }));
|
||||
|
||||
this.listen("settings-changed", () => {
|
||||
padlock.stats.set({
|
||||
syncCustomHost: this.settings.syncCustomHost
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("data-initialized", () => {
|
||||
track("Setup", {
|
||||
"With Email": !!this.settings.syncEmail
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("data-loaded", () => track("Unlock"));
|
||||
|
||||
this.listen("data-unloaded", () => track("Lock"));
|
||||
|
||||
this.listen("data-reset", () => {
|
||||
track("Reset Local Data")
|
||||
.then(() => setTrackingID(""));
|
||||
});
|
||||
|
||||
this.listen("sync-connect-start", (e) => {
|
||||
padlock.stats.get().then((stats) => {
|
||||
track("Start Pairing", {
|
||||
"Source": stats.pairingSource,
|
||||
"Email": e.detail.email
|
||||
});
|
||||
padlock.stats.set({ startedPairing: new Date().getTime() });
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-connect-success", () => {
|
||||
return padlock.stats.get()
|
||||
.then((stats) => {
|
||||
track("Finish Pairing", {
|
||||
"Success": true,
|
||||
"Source": stats.pairingSource,
|
||||
"$duration": (new Date().getTime() - stats.startedPairing) / 1000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-connect-cancel", () => {
|
||||
return padlock.stats.get()
|
||||
.then((stats) => {
|
||||
track("Finish Pairing", {
|
||||
"Success": false,
|
||||
"Canceled": true,
|
||||
"Source": stats.pairingSource,
|
||||
"$duration": (new Date().getTime() - stats.startedPairing) / 1000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.listen("sync-disconnect", () => track("Unpair").then(() => setTrackingID("")));
|
||||
|
||||
let startedSync;
|
||||
this.listen("sync-start", () => startedSync = new Date().getTime());
|
||||
|
||||
this.listen("sync-success", (e) => {
|
||||
padlock.stats.set({ lastSync: new Date().getTime() })
|
||||
.then(() => track("Synchronize", {
|
||||
"Success": true,
|
||||
"Auto Sync": e.detail ? e.detail.auto : false,
|
||||
"$duration": (new Date().getTime() - startedSync) / 1000
|
||||
}));
|
||||
});
|
||||
|
||||
this.listen("sync-fail", (e) => track("Synchronize", {
|
||||
"Success": false,
|
||||
"Auto Sync": e.detail.auto,
|
||||
"Error Code": e.detail.error.code,
|
||||
"$duration": (new Date().getTime() - startedSync) / 1000
|
||||
}));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.hasData()
|
||||
.then((hasData) => track("Launch", {
|
||||
"Clean Launch": !hasData,
|
||||
"$duration": (new Date().getTime() - startedLoading) / 1000
|
||||
}));
|
||||
}
|
||||
|
||||
track(event, props) {
|
||||
// Don't track events if user is using a custom padlock cloud instance
|
||||
if (DataMixin.settings.syncCustomHost) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
track(event, props);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
|
@ -1,268 +0,0 @@
|
|||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { wait } = padlock.util;
|
||||
const { isChromeApp, isChromeOS, getReviewLink } = padlock.platform;
|
||||
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
|
||||
let stats;
|
||||
const statsLoaded = padlock.stats.get().then((s) => stats = s);
|
||||
|
||||
function daysPassed(date) {
|
||||
return (new Date().getTime() - date) / day;
|
||||
}
|
||||
|
||||
padlock.HintsMixin = (superClass) => {
|
||||
|
||||
return class HintsMixin extends superClass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindBackup()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindSync()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._askFeedback()));
|
||||
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._crossPlatformHint()));
|
||||
this.listen("auto-lock", () => statsLoaded.then(() => this._showAutoLockNotice()));
|
||||
this.listen("settings-changed", () => this._notifySubStatus());
|
||||
this.listen("sync-connect-success", () => this._notifySubStatus());
|
||||
this.listen("data-loaded", () => wait(1000).then(() => this._notifySubStatus()));
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
statsLoaded
|
||||
.then(() => wait(1000))
|
||||
.then(() => this._notifyChromeAppDeprecation());
|
||||
}
|
||||
|
||||
_remindBackup() {
|
||||
if (
|
||||
daysPassed(stats.lastExport || stats.firstLaunch) > 7 &&
|
||||
daysPassed(stats.lastBackupReminder || stats.firstLaunch) > 7 &&
|
||||
!this.settings.syncConnected
|
||||
) {
|
||||
this.choose($l(
|
||||
"Have you backed up your data yet? Remember that by default your data is only stored " +
|
||||
"locally and can not be recovered in case your device gets damaged or lost. " +
|
||||
"Log in now to enable automatic online backups!"
|
||||
), [$l("Log In"), $l("Do Nothing")], { type: "warning" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
padlock.stats.set({ pairingSource: "Backup Reminder" });
|
||||
this._openCloudView();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
padlock.stats.set({ lastBackupReminder: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
|
||||
_remindSync() {
|
||||
const daysSinceLastSync = daysPassed(stats.lastSync || stats.firstLaunch);
|
||||
if (
|
||||
this.settings.syncConnected &&
|
||||
daysSinceLastSync > 7 &&
|
||||
daysPassed(stats.lastSyncReminder || stats.firstLaunch) > 7
|
||||
) {
|
||||
this.choose($l(
|
||||
"The last time you synced your data with your account was {0} days ago! You should " +
|
||||
"synchronize regularly to keep all your devices up-to-date and to make sure you always " +
|
||||
"have a recent backup in the cloud.",
|
||||
Math.floor(daysSinceLastSync)
|
||||
), [$l("Synchronize Now"), $l("Turn On Auto Sync"), $l("Do Nothing")], { type: "warning" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.synchronize();
|
||||
break;
|
||||
case 1:
|
||||
this.settings.syncAuto = true;
|
||||
this.dispatch("settings-changed");
|
||||
this.synchronize();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
padlock.stats.set({ lastSyncReminder: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
|
||||
_notifyChromeAppDeprecation() {
|
||||
isChromeOS().then((yes) => {
|
||||
if (isChromeApp() && !yes) {
|
||||
this.confirm($l(
|
||||
"In August 2016, Google announced that they will be discontinuing Chrome Apps for all " +
|
||||
"operating systems other than ChromeOS. This means that by early 2018, you will no " +
|
||||
"longer be able to load this app! Don't worry though, because Padlock is now available " +
|
||||
"as a native app for Windows, MacOS and Linux! Head over to our website to find out " +
|
||||
"how to switch now!"
|
||||
), $l("Learn More"), $l("Dismiss"), { type: "info", hideIcon: true })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
window.open("https://padlock.io/news/discontinuing-chrome/", "_system");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_askFeedback() {
|
||||
if (
|
||||
stats.launchCount > 20 &&
|
||||
!stats.dontAskFeeback &&
|
||||
!stats.lastAskedFeedback
|
||||
) {
|
||||
this.choose($l(
|
||||
"Hey there! Sorry to bother you, but we'd love to know how you are liking Padlock so far!"
|
||||
), [
|
||||
$l("I Love it!") + " \u2665",
|
||||
$l("It's not bad, but..."),
|
||||
$l("Hate it.")
|
||||
])
|
||||
.then((rating) => {
|
||||
this.track("Rate App", { "Rating": rating });
|
||||
|
||||
padlock.stats.set({
|
||||
lastAskedFeedback: new Date().getTime(),
|
||||
lastRating: rating,
|
||||
lastRatedVersion: this.settings.version
|
||||
});
|
||||
|
||||
switch (rating) {
|
||||
case 0:
|
||||
this.choose($l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
), [
|
||||
$l("Rate Padlock"),
|
||||
$l("No Thanks")
|
||||
])
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.track("Review App", { "Rating": rating });
|
||||
this._sendFeedback(rating);
|
||||
padlock.stats.set({ lastReviewed: new Date().getTime() });
|
||||
break;
|
||||
case 1:
|
||||
padlock.stats.set({ dontAskFeedback: true });
|
||||
break;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
this.choose($l(
|
||||
"Your opinion as a user is very important to us and we're always " +
|
||||
"looking for feedback and suggestions on how to improve Padlock. " +
|
||||
"Would you mind taking a second to let us know what you think about " +
|
||||
"the app?"
|
||||
), [
|
||||
$l("Send Feedback"),
|
||||
$l("No Thanks")
|
||||
])
|
||||
.then((choice) => {
|
||||
if (choice === 0) {
|
||||
this.track("Provide Feedback", { "Rating": rating });
|
||||
this._sendFeedback(rating);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_sendFeedback(rating) {
|
||||
getReviewLink(rating).then((link) => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_showAutoLockNotice() {
|
||||
if (!stats.hasShownAutoLockNotice) {
|
||||
const minutes = this.settings.autoLockDelay;
|
||||
setTimeout(() => {
|
||||
this.alert($l("Padlock was automatically locked after {0} {1} " +
|
||||
"of inactivity. You can change this behavior from the settings page.",
|
||||
minutes, minutes > 1 ? $l("minutes") : $l("minute")));
|
||||
}, 1000);
|
||||
padlock.stats.set({ hasShownAutoLockNotice: true });
|
||||
}
|
||||
}
|
||||
|
||||
_notifySubStatus() {
|
||||
if (this.promo && !this._hasShownPromo) {
|
||||
this.alertPromo();
|
||||
this._hasShownPromo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.settings.syncSubStatus === "trialing" &&
|
||||
(!stats.lastTrialEndsReminder || daysPassed(stats.lastTrialEndsReminder) > 7)
|
||||
) {
|
||||
this.buySubscription("App - Trialing (Alert)");
|
||||
padlock.stats.set({ lastTrialEndsReminder: new Date().getTime() });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.syncConnected && this.settings.syncSubStatus !== this._lastSubStatus) {
|
||||
this._lastSubStatus = this.settings.syncSubStatus;
|
||||
|
||||
if (this.isTrialExpired()) {
|
||||
this.alert(this.trialExpiredMessage(), {
|
||||
type: "warning",
|
||||
title: $l("Trial Expired"),
|
||||
options: [$l("Upgrade Now")]
|
||||
}).then((choice) => {
|
||||
if (choice === 0) {
|
||||
this.buySubscription("App - Trial Expired (Alert)");
|
||||
}
|
||||
});
|
||||
} else if (this.isSubUnpaid()) {
|
||||
this.alert(this.subUnpaidMessage(), {
|
||||
type: "warning",
|
||||
title: $l("Payment Failed"),
|
||||
options: [$l("Update Payment Method"), $l("Contact Support")]
|
||||
}).then((choice) => {
|
||||
if (choice === 0) {
|
||||
this.updatePaymentMethod("App - Payment Failed (Alert)");
|
||||
} else if (choice === 1) {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_crossPlatformHint() {
|
||||
if (
|
||||
!this.settings.syncConnected &&
|
||||
!stats.hasShownCrossPlatformHint
|
||||
) {
|
||||
this.confirm(
|
||||
$l(
|
||||
"Padlock lets you share your data seamlessly across all your devices like phones, " +
|
||||
"tablets and computers! Log in now and we'll get you set up in just few steps!"
|
||||
),
|
||||
$l("Log In"),
|
||||
$l("No Thanks"),
|
||||
{ title: $l("Did You Know?"), type: "question" }
|
||||
)
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.stats.set({ pairingSource: "Cross-Platform Hint" });
|
||||
this._openCloudView();
|
||||
}
|
||||
});
|
||||
|
||||
padlock.stats.set({ hasShownCrossPlatformHint: new Date().getTime() });
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
|
@ -1,44 +0,0 @@
|
|||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { Messages } = padlock.messages;
|
||||
const { FileSource } = padlock.source;
|
||||
|
||||
padlock.MessagesMixin = (superClass) => {
|
||||
|
||||
return class MessagesMixin extends superClass {
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this._messages = new Messages(
|
||||
"https://padlock.io/messages.json",
|
||||
new FileSource("read-messages.json"),
|
||||
this.settings
|
||||
);
|
||||
this.listen("data-loaded", () => this.checkMessages());
|
||||
}
|
||||
|
||||
checkMessages() {
|
||||
padlock.util.wait(1000)
|
||||
.then(() => this._messages.fetch())
|
||||
.then((aa) => aa.forEach((a) => this._displayMessage(a)));
|
||||
}
|
||||
|
||||
_displayMessage(a) {
|
||||
if (a.link) {
|
||||
this.confirm(a.text, $l("Learn More"), $l("Dismiss"), { type: "info" })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
window.open(a.link, "_system");
|
||||
}
|
||||
this._messages.markRead(a);
|
||||
});
|
||||
} else {
|
||||
this.alert(a.text)
|
||||
.then(() => this._messages.markRead(a));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
|
@ -1,136 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { setClipboard } = padlock.platform;
|
||||
const { LocaleMixin } = padlock;
|
||||
|
||||
class Clipboard extends LocaleMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
color: var(--color-background);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
}
|
||||
|
||||
:host(:not(.showing)) {
|
||||
transform: translateY(130%);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="content">
|
||||
<div class="title">[[ \$l("Copied To Clipboard:") ]]</div>
|
||||
<div class="name">[[ record.name ]] / [[ field.name ]]</div>
|
||||
</div>
|
||||
|
||||
<button class="tiles-2 tap" on-click="clear">
|
||||
<div><strong>[[ \$l("Clear") ]]</strong></div>
|
||||
<div class="countdown">[[ _tMinusClear ]]s<div>
|
||||
|
||||
|
||||
</div></div></button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-clipboard"; }
|
||||
|
||||
static get properties() { return {
|
||||
record: Object,
|
||||
field: Object
|
||||
}; }
|
||||
|
||||
set(record, field, duration = 60) {
|
||||
clearInterval(this._interval);
|
||||
|
||||
this.record = record;
|
||||
this.field = field;
|
||||
setClipboard(field.value);
|
||||
|
||||
this.classList.add("showing");
|
||||
|
||||
const tStart = Date.now();
|
||||
|
||||
this._tMinusClear = duration;
|
||||
this._interval = setInterval(() => {
|
||||
const dt = tStart + duration * 1000 - Date.now();
|
||||
if (dt <= 0) {
|
||||
this.clear();
|
||||
} else {
|
||||
this._tMinusClear = Math.floor(dt/1000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearInterval(this._interval);
|
||||
setClipboard(" ");
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Clipboard.is, Clipboard);
|
||||
|
||||
let clipboardSingleton;
|
||||
|
||||
padlock.ClipboardMixin = (baseClass) => {
|
||||
return class ClipboardMixin extends baseClass {
|
||||
|
||||
setClipboard(record, field, duration) {
|
||||
if (!clipboardSingleton) {
|
||||
clipboardSingleton = document.createElement("pl-clipboard");
|
||||
document.body.appendChild(clipboardSingleton);
|
||||
clipboardSingleton.offsetLeft;
|
||||
}
|
||||
|
||||
return clipboardSingleton.set(record, field, duration);
|
||||
}
|
||||
|
||||
clearClipboard() {
|
||||
if (clipboardSingleton) {
|
||||
clipboardSingleton.clear();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
|
@ -1,124 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultButtonLabel = $l("OK");
|
||||
|
||||
class DialogAlert extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="warning"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="plain"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-background);
|
||||
};
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-text,
|
||||
:host([hide-icon]) .info-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-text:not(.small) {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
|
||||
<div class="info" hidden\$="[[ _hideInfo(title, message) ]]">
|
||||
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ title ]]</div>
|
||||
<div class\$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button on-click="_selectOption" class\$="[[ _buttonClass(index) ]]">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-alert"; }
|
||||
|
||||
static get properties() { return {
|
||||
buttonLabel: { type: String, value: defaultButtonLabel },
|
||||
title: { type: String, value: ""},
|
||||
message: { type: String, value: "" },
|
||||
options: { type: Array, value: ["OK"] },
|
||||
preventDismiss: { type: Boolean, value: false },
|
||||
type: { type: String, value: "info", reflectToAttribute: true },
|
||||
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
|
||||
this.message = message;
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.options = options;
|
||||
this.hideIcon = hideIcon;
|
||||
|
||||
setTimeout(() => this.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_icon() {
|
||||
switch (this.type) {
|
||||
case "info":
|
||||
return "info-round";
|
||||
case "warning":
|
||||
return "error";
|
||||
case "success":
|
||||
return "success";
|
||||
case "question":
|
||||
return "question";
|
||||
}
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_dialogDismiss() {
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_textClass() {
|
||||
return this.title ? "small" : "";
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hideInfo() {
|
||||
return !this.title && !this.message;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogAlert.is, DialogAlert);
|
|
@ -1,58 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultMessage = $l("Are you sure you want to do this?");
|
||||
const defaultConfirmLabel = $l("Confirm");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
|
||||
class DialogConfirm extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="">
|
||||
<div class="message tiles-1">{{ message }}</div>
|
||||
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
|
||||
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-confirm"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: defaultMessage },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this._resolve === "function") && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this.resolve === "function") && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
confirm(message, confirmLabel, cancelLabel) {
|
||||
this.message = message || defaultMessage;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this.open = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogConfirm.is, DialogConfirm);
|
|
@ -1,62 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
class DialogOptions extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp="">
|
||||
<div class="message tiles-1">[[ message ]]</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button class\$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-options"; }
|
||||
|
||||
static get properties() { return {
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
options: { type: Array, value: [$l("Dismiss")] },
|
||||
preventDismiss: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
choose(message, options) {
|
||||
this.message = message || "";
|
||||
this.options = options || this.options;
|
||||
|
||||
setTimeout(() => this.open = true, 50);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hasMessage(message) {
|
||||
return !!message;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(-1);
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogOptions.is, DialogOptions);
|
|
@ -1,111 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultConfirmLabel = $l("OK");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
const defaultType = "text";
|
||||
const defaultPlaceholder = "";
|
||||
|
||||
class DialogPrompt extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
pl-input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-error);
|
||||
text-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<div class="message tiles-1" hidden\$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
|
||||
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
|
||||
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
|
||||
<button class="tap tiles-4" on-click="_dismiss" hidden\$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
|
||||
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-prompt"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
placeholder: { type: String, value: ""},
|
||||
preventDismiss: { type: Boolean, value: true},
|
||||
type: { type: String, value: defaultType },
|
||||
validationFn: Function,
|
||||
_validationMessage: { type: String, value: "" }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.$.confirmButton.start();
|
||||
const val = this.$.input.value;
|
||||
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
|
||||
p.then((v) => {
|
||||
this._validationMessage = "";
|
||||
this.$.confirmButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(v);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}).catch((e) => {
|
||||
this.$.dialog.rumble();
|
||||
this._validationMessage = e;
|
||||
this.$.confirmButton.fail();
|
||||
});
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(null);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_hasMessage() {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
|
||||
this.$.confirmButton.stop();
|
||||
this.message = message || "";
|
||||
this.type = type || defaultType;
|
||||
this.placeholder = placeholder || defaultPlaceholder;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this._hideCancelButton = cancelLabel === false;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.validationFn = validation;
|
||||
this._validationMessage = "";
|
||||
this.$.input.value = "";
|
||||
this.open = true;
|
||||
|
||||
setTimeout(() => this.$.input.focus(), 100);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogPrompt.is, DialogPrompt);
|
|
@ -1,181 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../input/input.js';
|
||||
|
||||
class Dialog extends padlock.AnimationMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@apply --scroll;
|
||||
}
|
||||
|
||||
:host(:not(.open)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outer {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
display: block;
|
||||
background: var(--color-background);
|
||||
opacity: 0;
|
||||
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
transform: translate3d(0, 0, 0);
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
:host(.open) .scrim {
|
||||
opacity: 0.90;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: var(--pl-dialog-max-width, 400px);
|
||||
z-index: 1;
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
border-radius: var(--border-radius);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
@apply --pl-dialog-inner;
|
||||
}
|
||||
|
||||
.outer {
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
|
||||
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host(:not(.open)) .outer {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="scrim"></div>
|
||||
|
||||
<div class="outer" on-click="dismiss">
|
||||
<slot name="before"></slot>
|
||||
<div id="inner" class="inner" on-click="_preventDismiss">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: {
|
||||
duration: 500,
|
||||
fullDuration: 700
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
isShowing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
preventDismiss: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// window.addEventListener("keydown", (e) => {
|
||||
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
|
||||
// this.dismiss();
|
||||
// // e.preventDefault();
|
||||
// // e.stopPropagation();
|
||||
// }
|
||||
// });
|
||||
window.addEventListener("backbutton", (e) => {
|
||||
if (this.open) {
|
||||
this.dismiss();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rumble() {
|
||||
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
//* Changed handler for the _open_ property. Shows/hides the dialog
|
||||
_openChanged() {
|
||||
clearTimeout(this._hideTimeout);
|
||||
|
||||
// Set _display: block_ if we're showing. If we're hiding
|
||||
// we need to wait until the transitions have finished before we
|
||||
// set _display: none_.
|
||||
if (this.open) {
|
||||
if (padlock.Input.activeInput) {
|
||||
padlock.Input.activeInput.blur();
|
||||
}
|
||||
this.style.display = "";
|
||||
this.isShowing = true;
|
||||
} else {
|
||||
this._hideTimeout = window.setTimeout(() => {
|
||||
this.style.display = "none";
|
||||
this.isShowing = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
this.offsetLeft;
|
||||
|
||||
this.classList.toggle("open", this.open);
|
||||
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
this.open ? "dialog-open" : "dialog-close",
|
||||
{ bubbles: true, composed: true }
|
||||
));
|
||||
}
|
||||
|
||||
_preventDismiss(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (!this.preventDismiss) {
|
||||
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Dialog.is, Dialog);
|
|
@ -1,159 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const exportCSVWarning = $l(
|
||||
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
|
||||
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
|
||||
"a secure, encrypted file, instead! Are you sure you want to proceed?"
|
||||
);
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, setClipboard } = padlock.platform;
|
||||
const { toPadlock, toCSV } = padlock.exp;
|
||||
|
||||
class PlExport extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pl-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tiles tiles-1 row">
|
||||
<div class="label">[[ \$l("As CSV") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="tiles tiles-2 row">
|
||||
<div class="label">[[ \$l("As Encrypted File") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-export"; }
|
||||
|
||||
static get properties() { return {
|
||||
exportRecords: Array
|
||||
}; }
|
||||
|
||||
_downloadCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setTimeout(() => {
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.csv`;
|
||||
const csv = toCSV(this.exportRecords);
|
||||
const a = document.createElement("a");
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_copyCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setClipboard(toCSV(this.exportRecords))
|
||||
.then(() => this.alert($l("Your data has successfully been copied to the system " +
|
||||
"clipboard. You can now paste it into the spreadsheet program of your choice.")));
|
||||
this.dispatch("data-exported");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getEncryptedData() {
|
||||
return this.prompt($l("Please choose a password to protect your data. This may be the same as " +
|
||||
"your master password or something else, but make sure it is sufficiently strong!"),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"))
|
||||
.then((pwd) => {
|
||||
if (!pwd) {
|
||||
if (pwd === "") {
|
||||
this.alert($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
return this.confirm($l(
|
||||
"WARNING: The password you entered is weak which makes it easier for " +
|
||||
"attackers to break the encryption used to protect your data. Try to use a longer " +
|
||||
"password or include a variation of uppercase, lowercase and special characters as " +
|
||||
"well as numbers."
|
||||
), $l("Use Anyway"), $l("Choose Different Password"), { type: "warning" }).then((confirm) => {
|
||||
if (!confirm) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
});
|
||||
} else {
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_downloadEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
const a = document.createElement("a");
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.pls`;
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
|
||||
a.download = fileName;
|
||||
setTimeout(() => {
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
_copyEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
setClipboard(data)
|
||||
.then(() => {
|
||||
this.alert($l("Your data has successfully been copied to the system clipboard."),
|
||||
{ type: "success" });
|
||||
});
|
||||
this.dispatch("data-exported");
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExport.is, PlExport);
|
|
@ -1,254 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import autosize from '../../../../../node_modules/autosize/src/autosize.js';
|
||||
|
||||
let activeInput = null;
|
||||
|
||||
// On touch devices, blur active input when tapping on a non-input
|
||||
document.addEventListener("touchend", () => {
|
||||
if (activeInput) {
|
||||
activeInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
class PlInput extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(:not([multiline])) {
|
||||
padding: 0 10px;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
text-align: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
text-shadow: inherit;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
@apply --pl-input-placeholder;
|
||||
}
|
||||
|
||||
.mask {
|
||||
@apply --fullbleed;
|
||||
pointer-events: none;
|
||||
font-size: 150%;
|
||||
line-height: 22px;
|
||||
letter-spacing: -4.5px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
input[disabled], textarea[disabled] {
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
|
||||
input[invisible], textarea[invisible] {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
|
||||
<textarea id="input" value="{{ value::input }}" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" rows="1" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" tabindex\$="[[ _tabIndex(noTab) ]]" invisible\$="[[ _showMask(masked, value, focused) ]]" disabled\$="[[ disabled ]]"></textarea>
|
||||
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled=""></textarea>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
|
||||
<input id="input" value="{{ value::input }}" tabindex\$="[[ _tabIndex(noTab) ]]" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" type\$="[[ type ]]" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" required\$="[[ required ]]" pattern\$="[[ pattern ]]" disabled\$="[[ disabled ]]" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" invisible\$="[[ _showMask(masked, value, focused) ]]">
|
||||
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled="">
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-input"; }
|
||||
|
||||
static get activeInput() { return activeInput; }
|
||||
|
||||
static get properties() { return {
|
||||
autosize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
autocapitalize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notifiy: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
masked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
value: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
value: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
value: "text"
|
||||
},
|
||||
selectOnFocus: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true,
|
||||
observer: "_valueChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
get inputElement() {
|
||||
return this.root.querySelector(this.multiline ? "textarea" : "input");
|
||||
}
|
||||
|
||||
_domChange() {
|
||||
if (this.autosize && this.multiline && this.inputElement) {
|
||||
autosize(this.inputElement);
|
||||
}
|
||||
setTimeout(() => this._valueChanged(), 50);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_focused(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = true;
|
||||
activeInput = this;
|
||||
this.dispatchEvent(new CustomEvent("focus"));
|
||||
|
||||
if (this.selectOnFocus) {
|
||||
setTimeout(() => this.selectAll(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
_blurred(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = false;
|
||||
if (activeInput === this) {
|
||||
activeInput = null;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("blur"));
|
||||
}
|
||||
|
||||
_changeHandler(e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
_keydown(e) {
|
||||
if (e.key === "Enter" && !this.multiline) {
|
||||
this.dispatchEvent(new CustomEvent("enter"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.key === "Escape") {
|
||||
this.dispatchEvent(new CustomEvent("escape"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this.invalid = this.inputElement && !this.inputElement.checkValidity();
|
||||
if (this.autosize && this.multiline) {
|
||||
autosize.update(this.inputElement);
|
||||
}
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_showMask() {
|
||||
return this.masked && !!this.value && !this.focused;
|
||||
}
|
||||
|
||||
_mask(value) {
|
||||
return value && value.replace(/[^\n]/g, "\u2022");
|
||||
}
|
||||
|
||||
_computeAutoCapitalize() {
|
||||
return this.autocapitalize ? "" : "off";
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.inputElement.blur();
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
try {
|
||||
this.inputElement.setSelectionRange(0, this.value.length);
|
||||
} catch (e) {
|
||||
this.inputElement.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlInput.is, PlInput);
|
||||
|
||||
padlock.Input = PlInput;
|
File diff suppressed because one or more lines are too long
|
@ -1,116 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class Notification extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
color: var(--color-background);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
}
|
||||
|
||||
:host(:not(.showing)) {
|
||||
transform: translateY(130%);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.background {
|
||||
opacity: 0.95;
|
||||
border-radius: var(--border-radius);
|
||||
@apply --fullbleed;
|
||||
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
}
|
||||
|
||||
:host(.error) .background, :host(.warning) .background {
|
||||
background: linear-gradient(90deg, #f49300 0%, #f25b00 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="background"></div>
|
||||
|
||||
<div class="text" on-click="_click">{{ message }}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-notification"; }
|
||||
|
||||
static get properties() { return {
|
||||
message: String,
|
||||
type: {
|
||||
type: String,
|
||||
value: "info",
|
||||
observer: "_typeChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
show(message, type, duration) {
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
this.classList.add("showing");
|
||||
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(false), duration);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
hide(clicked) {
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve(clicked);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_typeChanged(newType, oldType) {
|
||||
this.classList.remove(oldType);
|
||||
this.classList.add(newType);
|
||||
}
|
||||
|
||||
_click() {
|
||||
this.hide(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Notification.is, Notification);
|
||||
|
||||
let notificationSingleton;
|
||||
|
||||
padlock.NotificationMixin = (baseClass) => {
|
||||
return class NotificationMixin extends baseClass {
|
||||
|
||||
notify(message, type, duration) {
|
||||
if (!notificationSingleton) {
|
||||
notificationSingleton = document.createElement("pl-notification");
|
||||
document.body.appendChild(notificationSingleton);
|
||||
notificationSingleton.offsetLeft;
|
||||
}
|
||||
|
||||
return notificationSingleton.show(message, type, duration);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import './promo.js';
|
||||
|
||||
const { BaseElement } = padlock;
|
||||
|
||||
class PlExportDialog extends BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #555 0%, #222 100%);
|
||||
};
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
|
||||
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-promo-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
|
||||
_dismiss() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
setTimeout(() => this.$.dialog.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
|
@ -1,523 +0,0 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../export/export.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../slider/slider.js';
|
||||
import '../sync/sync.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, getReviewLink, isTouch, getDesktopSettings, checkForUpdates,
|
||||
saveDBAs, loadDB, isElectron } = padlock.platform;
|
||||
|
||||
class SettingsView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
@keyframes beat {
|
||||
0% { transform: scale(1); }
|
||||
5% { transform: scale(1.4); }
|
||||
15% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:host {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
section {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
section .info {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button > pl-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
pl-toggle-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.padlock-heart {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
animation: beat 5s infinite;
|
||||
}
|
||||
|
||||
.padlock-heart::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\\f004";
|
||||
}
|
||||
|
||||
.made-in {
|
||||
font-size: var(--font-size-tiny);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.db-path {
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
word-wrap: break-word;
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
|
||||
#customUrlInput:not([invalid]) + .url-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-warning {
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-top: solid 1px var(--border-color);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.feature-locked {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-error);
|
||||
margin: -14px 15px 12px 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
|
||||
<div class="title">[[ \$l("Settings") ]]</div>
|
||||
<pl-icon></pl-icon>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Auto Lock") ]]</div>
|
||||
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ \$l('Lock Automatically') ]]" class="tap" reverse="" on-change="settingChanged"></pl-toggle-button>
|
||||
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ \$l(' min') ]]" label="[[ \$l('After') ]]" hidden\$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Synchronization") ]]</div>
|
||||
<div disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ \$l('Sync Automatically') ]]" reverse="" class="tap" on-change="settingChanged"></pl-toggle-button>
|
||||
<div class="feature-locked" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log in to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
</div>
|
||||
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ \$l('Use Custom Server') ]]" reverse="" on-change="_customHostChanged" class="tap" disabled\$="[[ settings.syncConnected ]]"></pl-toggle-button>
|
||||
<div class="tap" hidden\$="[[ !settings.syncCustomHost ]]" disabled\$="[[ settings.syncConnected ]]">
|
||||
<pl-input id="customUrlInput" placeholder="[[ \$l('Enter Custom URL') ]]" value="{{ settings.syncHostUrl }}" pattern="^https://[^\\s/\$.?#].[^\\s]*\$" required="" on-change="settingChanged"></pl-input>
|
||||
<div class="url-warning">
|
||||
<strong>[[ \$l("Invalid URL") ]]</strong> -
|
||||
[[ \$l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button on-click="_changePassword" class="tap">[[ \$l("Change Master Password") ]]</button>
|
||||
<button on-click="_resetData" class="tap">[[ \$l("Reset App") ]]</button>
|
||||
<button class="tap" on-click="_import">[[ \$l("Import...") ]]</button>
|
||||
<button class="tap" on-click="_export">[[ \$l("Export...") ]]
|
||||
</button></section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Updates") ]]</div>
|
||||
<pl-toggle-button id="autoUpdatesButton" label="[[ \$l('Automatically Install Updates') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<pl-toggle-button id="betaReleasesButton" label="[[ \$l('Install Beta Releases') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<button on-click="_checkForUpdates" class="tap">[[ \$l("Check For Updates...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Database") ]]</div>
|
||||
<div class="info">
|
||||
<div>[[ \$l("Current Location:") ]]</div>
|
||||
<div class="db-path">[[ _dbPath ]]</div>
|
||||
</div>
|
||||
<button on-click="_saveDBAs" class="tap">[[ \$l("Change Location...") ]]</button>
|
||||
<button on-click="_loadDB" class="tap">[[ \$l("Load Different Database...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button class="info tap" on-click="_openSource">
|
||||
<div><strong>Padlock {{ settings.version }}</strong></div>
|
||||
<div class="made-in">Made with ♥ in Germany</div>
|
||||
</button>
|
||||
<button on-click="_openWebsite" class="tap">[[ \$l("Website") ]]</button>
|
||||
<button on-click="_sendMail" class="tap">[[ \$l("Support") ]]</button>
|
||||
<button on-click="_promptReview" class="tap">
|
||||
<span>[[ \$l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden="">
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-settings-view"; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (isElectron()) {
|
||||
const desktopSettings = getDesktopSettings().get();
|
||||
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
|
||||
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
|
||||
this._dbPath = desktopSettings.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
|
||||
}
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("settings-back"));
|
||||
}
|
||||
|
||||
//* Opens the change password dialog and resets the corresponding input elements
|
||||
_changePassword() {
|
||||
let newPwd;
|
||||
return this.promptPassword(this.password, $l(
|
||||
"Are you sure you want to change your master password? Enter your " +
|
||||
"current password to continue!"
|
||||
), $l("Confirm"), $l("Cancel"))
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.prompt($l("Now choose a new master password!"),
|
||||
$l("Enter New Password"), "password", $l("Confirm"), $l("Cancel"), false, (val) => {
|
||||
if (val === "") {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.resolve(val);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return Promise.reject();
|
||||
}
|
||||
newPwd = pwd;
|
||||
return this.promptPassword(pwd, $l("Confirm your new master password!"),
|
||||
$l("Confirm"), $l("Cancel"));
|
||||
})
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.setPassword(newPwd);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
return this.confirm(
|
||||
$l("Do you want to update the password for you online account {0} as well?",
|
||||
this.settings.syncEmail),
|
||||
$l("Yes"), $l("No")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.setRemotePassword(this.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.alert($l("Master password changed successfully."), { type: "success" });
|
||||
});
|
||||
}
|
||||
|
||||
_openWebsite() {
|
||||
window.open("https://padlock.io", "_system");
|
||||
}
|
||||
|
||||
_sendMail() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_openGithub() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_resetData() {
|
||||
this.promptPassword(this.password,
|
||||
$l(
|
||||
"Are you sure you want to delete all your data and reset the app? Enter your " +
|
||||
"master password to continue!"
|
||||
),
|
||||
$l("Reset App")
|
||||
)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.resetData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_import() {
|
||||
const options = [
|
||||
$l("From Clipboard")
|
||||
];
|
||||
if (!isCordova()) {
|
||||
options.push($l("From File"));
|
||||
}
|
||||
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._importFromClipboard();
|
||||
break;
|
||||
case 1:
|
||||
this.$.importFile.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importFile() {
|
||||
const file = this.$.importFile.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this._importString(reader.result)
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to open file. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data you are trying to import was exported from a " +
|
||||
"newer version of Padlock and can not be opened with the version you are " +
|
||||
"currently running."
|
||||
), $l("Check For Updates"), $l("Cancel"), { type: "info" })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "invalid_csv":
|
||||
this.alert($l("Failed to recognize file format."), { type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("Failed to open file."), { type: "warning" });
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
this.$.importFile.value = "";
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
_importFromClipboard() {
|
||||
padlock.platform.getClipboard()
|
||||
.then((str) => this._importString(str))
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to decrypt data. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("No supported data found in clipboard. Please make sure to copy " +
|
||||
"you data to the clipboard first (e.g. via ctrl + C)."), { type: "warning" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importString(rawStr) {
|
||||
const imp = padlock.imp;
|
||||
const isPadlock = imp.isFromPadlock(rawStr);
|
||||
const isSecuStore = imp.isFromSecuStore(rawStr);
|
||||
const isLastPass = imp.isFromLastPass(rawStr);
|
||||
const isCSV = imp.isCSV(rawStr);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (isPadlock || isSecuStore) {
|
||||
return this.prompt($l("This file is protected by a password."),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"));
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPadlock) {
|
||||
return imp.fromPadlock(rawStr, pwd);
|
||||
} else if (isSecuStore) {
|
||||
return imp.fromSecuStore(rawStr, pwd);
|
||||
} else if (isLastPass) {
|
||||
return imp.fromLastPass(rawStr);
|
||||
} else if (isCSV) {
|
||||
return this.choose($l(
|
||||
"The data you want to import seems to be in CSV format. Before you continue, " +
|
||||
"please make sure that the data is structured according to Padlocks specific " +
|
||||
"requirements!"
|
||||
), [$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")], { type: "info" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
|
||||
// Reopen dialog for when the user comes back from the web page
|
||||
return this._importString(rawStr);
|
||||
case 1:
|
||||
return imp.fromCSV(rawStr);
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
throw new imp.ImportError("invalid_csv");
|
||||
}
|
||||
})
|
||||
.then((records) => {
|
||||
if (records) {
|
||||
this.addRecords(records);
|
||||
this.dispatch("data-imported", { records: records });
|
||||
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
|
||||
_isTouch() {
|
||||
return isTouch();
|
||||
}
|
||||
|
||||
_openSource() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_autoLockInfo() {
|
||||
return this.$l(
|
||||
"Tell Padlock to automatically lock the app after a certain period of " +
|
||||
"inactivity in case you leave your device unattended for a while."
|
||||
);
|
||||
}
|
||||
|
||||
_peekValuesInfo() {
|
||||
return this.$l(
|
||||
"If enabled allows peeking at field values in the record list " +
|
||||
"by moving the mouse cursor over the corresponding field."
|
||||
);
|
||||
}
|
||||
|
||||
_resetDataInfo() {
|
||||
return this.$l(
|
||||
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
|
||||
"and will restore the app to the state it was when you first launched it."
|
||||
);
|
||||
}
|
||||
|
||||
_promptReview() {
|
||||
this.choose($l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
), [$l("Rate Padlock"), $l("No Thanks")])
|
||||
.then((choice) => {
|
||||
if (choice === 0) {
|
||||
getReviewLink(0).then((link) => window.open(link, "_system"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isDesktop() {
|
||||
return isElectron();
|
||||
}
|
||||
|
||||
_desktopSettingsChanged() {
|
||||
getDesktopSettings().set({
|
||||
autoDownloadUpdates: this.$.autoUpdatesButton.active,
|
||||
allowPrerelease: this.$.betaReleasesButton.active
|
||||
});
|
||||
}
|
||||
|
||||
_checkForUpdates() {
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
_saveDBAs() {
|
||||
saveDBAs();
|
||||
}
|
||||
|
||||
_loadDB() {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
_export() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.records);
|
||||
}
|
||||
|
||||
_autoSyncInfoText() {
|
||||
return $l(
|
||||
"Enable Auto Sync to automatically synchronize your data with " +
|
||||
"your Padlock online account every time you make a change!"
|
||||
);
|
||||
}
|
||||
|
||||
_customHostChanged() {
|
||||
if (this.settings.syncCustomHost) {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to use a custom server for synchronization? " +
|
||||
"This option is only recommended for advanced users!"),
|
||||
$l("Continue"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.settingChanged();
|
||||
} else {
|
||||
this.set("settings.syncCustomHost", false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.settingChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(SettingsView.is, SettingsView);
|
|
@ -1,891 +0,0 @@
|
|||
import '../../../../../node_modules/zxcvbn/dist/zxcvbn.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../data/data.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, BaseElement, SyncMixin } = padlock;
|
||||
const { applyMixins, wait } = padlock.util;
|
||||
const { isTouch, getAppStoreLink } = padlock.platform;
|
||||
const { track } = padlock.tracking;
|
||||
|
||||
class StartView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
@keyframes reveal {
|
||||
from { transform: translate(0, 30px); opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
to { transform: translate(0, -200px); opacity: 0; }
|
||||
}
|
||||
|
||||
:host {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
@apply --fullbleed;
|
||||
@apply --scroll;
|
||||
color: var(--color-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
text-align: center;
|
||||
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
|
||||
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
main {
|
||||
@apply --fullbleed;
|
||||
background: transparent;
|
||||
min-height: 510px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-box {
|
||||
width: 300px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: block;
|
||||
font-size: 110px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin-bottom: 30px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.start-button pl-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.form-box pl-input, .form-box .input-wrapper {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-box pl-loading-button {
|
||||
width: var(--row-height);
|
||||
}
|
||||
|
||||
.strength-meter {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: -15px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: var(--font-size-tiny);
|
||||
width: 305px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.hint pl-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host(:not([_mode="get-started"])) .get-started,
|
||||
:host(:not([_mode="unlock"])) .unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.get-started-steps {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.get-started-step {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.get-started-step > * {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(3) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.get-started-step.left > * {
|
||||
transform: translate3d(-200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-step.right > * {
|
||||
transform: translate3d(200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-thumbs {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.get-started-thumbs > * {
|
||||
background: var(--color-foreground);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.get-started-thumbs > .right {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
button.skip {
|
||||
background: none;
|
||||
border: none;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 20px;
|
||||
margin: auto;
|
||||
font-size: var(--font-size-small);
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
text-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint.choose-password {
|
||||
margin-top: 30px;
|
||||
width: 160px;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
transition-delay: 0.4s;
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="unlock">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="form-box tiles-2 animate-in animate-out">
|
||||
<pl-input id="passwordInput" type="password" class="tap" select-on-focus="" on-enter="_unlock" no-tab="[[ open ]]" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap" label="[[ \$l('Unlock') ]]" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<main class="get-started">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="get-started-steps">
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
|
||||
|
||||
<div class="welcome-title animate-in">[[ \$l("Welcome to Padlock!") ]]</div>
|
||||
<div class="welcome-subtitle animate-in">[[ \$l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
|
||||
|
||||
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
|
||||
<div>[[ \$l("Get Started") ]]</div>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="emailInput" type="email" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterEmail" placeholder="[[ \$l('Enter Email Address') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="cloud"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_skipEmail">[[ \$l("Use Offline") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="codeInput" required="" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterCode" placeholder="[[ \$l('Enter Login Code') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="mail"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_cancelActivation">[[ \$l("Cancel") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ !_hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterCloudPassword" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_forgotCloudPassword">[[ \$l("I Forgot My Password") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ _hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="strength-meter">[[ _pwdStrength ]]</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
|
||||
</div>
|
||||
|
||||
<div on-click="_openPwdHowto" class="hint choose-password">[[ \$l("How do I choose a good master password?") ]]</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_confirmNewPassword" placeholder="[[ \$l('Confirm Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]" <="" span="">
|
||||
</span></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
|
||||
|
||||
<div class="welcome-title animate-out">[[ \$l("All done!") ]]</div>
|
||||
<div class="welcome-subtitle animate-out">[[ \$l("You're all set! Enjoy using Padlock!") ]]</div>
|
||||
|
||||
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
|
||||
<span>[[ \$l("Finish Setup") ]]</span>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="get-started-thumbs animate-in animate-out">
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-start-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
_getStartedStep: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
_hasData: {
|
||||
type: Boolean
|
||||
},
|
||||
_hasCloudData: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_mode: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeMode(_hasData)"
|
||||
},
|
||||
_pwdStrength: {
|
||||
type: String,
|
||||
computed: "_passwordStrength(newPwd)"
|
||||
}
|
||||
}; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._failCount = 0;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.dataReady()
|
||||
.then(() => this.reset())
|
||||
.then(() => {
|
||||
if (!isTouch() && this._hasData) {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
this._openChanged();
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$.passwordInput.value = "";
|
||||
this.$.emailInput.value = "";
|
||||
this.$.newPasswordInput.value = "";
|
||||
this.$.confirmPasswordInput.value = "";
|
||||
this.$.cloudPwdInput.value = "";
|
||||
this.$.unlockButton.stop();
|
||||
this.$.getStartedButton.stop();
|
||||
this._failCount = 0;
|
||||
this._getStartedStep = 0;
|
||||
this._hasCloudData = false;
|
||||
return this._checkHasData();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
|
||||
_openChanged() {
|
||||
if (this.open) {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
|
||||
animation: "fade",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 0,
|
||||
fill: "forwards",
|
||||
easing: "cubic-bezier(1, 0, 0.2, 1)",
|
||||
clear: 3000
|
||||
});
|
||||
} else {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
|
||||
animation: "reveal",
|
||||
duration: 1000,
|
||||
fullDuration: 1500,
|
||||
initialDelay: 300,
|
||||
fill: "backwards",
|
||||
clear: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_startSetup() {
|
||||
this._getStartedStep = 1;
|
||||
if (!isTouch()) {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Start");
|
||||
}
|
||||
|
||||
_enterEmail() {
|
||||
this.$.emailInput.blur();
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert(
|
||||
this.$.emailInput.validationMessage || $l("Please enter a valid email address!"),
|
||||
{ type: "warning" }
|
||||
).then(() => this.$.emailInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.emailButton.start();
|
||||
padlock.stats.set({ pairingSource: "Setup" });
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this.$.emailButton.success();
|
||||
this._getStartedStep = 2;
|
||||
this.$.codeInput.value = "";
|
||||
if (!isTouch()) {
|
||||
this.$.codeInput.focus();
|
||||
}
|
||||
})
|
||||
.catch(() => this.$.emailButton.fail());
|
||||
|
||||
track("Setup: Email", { "Skipped": false, "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_enterCode() {
|
||||
if (this._checkingCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$.codeInput.invalid) {
|
||||
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkingCode = true;
|
||||
|
||||
this.$.codeButton.start();
|
||||
this.activateToken(this.$.codeInput.value)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.hasCloudData().then((hasData) => {
|
||||
this._checkingCode = false;
|
||||
this.$.codeButton.success();
|
||||
this._hasCloudData = hasData;
|
||||
return this._connected();
|
||||
});
|
||||
} else {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
|
||||
track("Setup: Code", { "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_connected() {
|
||||
setTimeout(() => {
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
_cancelActivation() {
|
||||
this.cancelConnect();
|
||||
this.$.codeInput.value = "";
|
||||
this._getStartedStep = 1;
|
||||
}
|
||||
|
||||
_skipEmail() {
|
||||
this.$.emailInput.value = "";
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Email", { "Skipped": true });
|
||||
}
|
||||
|
||||
_enterNewPassword() {
|
||||
this.$.newPasswordInput.blur();
|
||||
const pwd = this.$.newPasswordInput.value;
|
||||
|
||||
if (!pwd) {
|
||||
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
this._getStartedStep = 4;
|
||||
if (!isTouch()) {
|
||||
this.$.confirmPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Choose Password");
|
||||
};
|
||||
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
this.choose($l(
|
||||
"The password you entered is weak which makes it easier for attackers to break " +
|
||||
"the encryption used to protect your data. Try to use a longer password or include a " +
|
||||
"variation of uppercase, lowercase and special characters as well as numbers!"
|
||||
), [
|
||||
$l("Learn More"),
|
||||
$l("Choose Different Password"),
|
||||
$l("Use Anyway")
|
||||
], {
|
||||
type: "warning",
|
||||
title: $l("WARNING: Weak Password"),
|
||||
hideIcon: true
|
||||
}).then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._openPwdHowto();
|
||||
break;
|
||||
case 1:
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
case 2:
|
||||
next();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_confirmNewPassword() {
|
||||
this.$.confirmPasswordInput.blur();
|
||||
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
|
||||
this.choose($l(
|
||||
"The password you entered does not match the original one!"
|
||||
), [
|
||||
$l("Try Again"),
|
||||
$l("Change Password")
|
||||
], { type: "warning" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.$.confirmPasswordInput.focus();
|
||||
break;
|
||||
case 1:
|
||||
this._getStartedStep = 3;
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._getStartedStep = 5;
|
||||
|
||||
track("Setup: Confirm Password");
|
||||
}
|
||||
|
||||
_computeMode() {
|
||||
return this._hasData ? "unlock" : "get-started";
|
||||
}
|
||||
|
||||
_checkHasData() {
|
||||
return this.hasData()
|
||||
.then((has) => this._hasData = has);
|
||||
}
|
||||
|
||||
_finishSetup() {
|
||||
this._initializeData();
|
||||
|
||||
track("Setup: Finish");
|
||||
}
|
||||
|
||||
_initializeData() {
|
||||
this.$.getStartedButton.start();
|
||||
if (this._initializing) {
|
||||
return;
|
||||
}
|
||||
this._initializing = true;
|
||||
|
||||
const password = this.cloudSource.password || this.$.newPasswordInput.value;
|
||||
this.cloudSource.password = password;
|
||||
|
||||
const promises = [
|
||||
this.initData(password),
|
||||
wait(1000)
|
||||
];
|
||||
|
||||
if (this.settings.syncConnected && !this._hasCloudData) {
|
||||
promises.push(this.collection.save(this.cloudSource));
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$.getStartedButton.success();
|
||||
this.$.newPasswordInput.blur();
|
||||
this._initializing = false;
|
||||
});
|
||||
}
|
||||
|
||||
_promptResetData(message) {
|
||||
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App"))
|
||||
.then((value) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value.toUpperCase() === "RESET") {
|
||||
this.resetData();
|
||||
} else {
|
||||
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_unlock() {
|
||||
const password = this.$.passwordInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.passwordInput.blur();
|
||||
this.$.unlockButton.start();
|
||||
|
||||
if (this._unlocking) {
|
||||
return;
|
||||
}
|
||||
this._unlocking = true;
|
||||
|
||||
Promise.all([
|
||||
this.loadData(password),
|
||||
wait(1000)
|
||||
])
|
||||
.then(() => {
|
||||
this.$.unlockButton.success();
|
||||
this._unlocking = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.unlockButton.fail();
|
||||
this._unlocking = false;
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this._rumble();
|
||||
this._failCount++;
|
||||
if (this._failCount > 2) {
|
||||
this.promptForgotPassword()
|
||||
.then((doReset) => {
|
||||
if (doReset) {
|
||||
this.resetData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data stored on this device was saved with a newer version " +
|
||||
"of Padlock and can not be opened with the version you are currently running. " +
|
||||
"Please install the latest version of Padlock or reset the data to start over!"
|
||||
), $l("Check For Updates"), $l("Reset Data"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
} else {
|
||||
this._promptResetData($l(
|
||||
"Are you sure you want to reset the app? " +
|
||||
"WARNING: This will delete all your data!"
|
||||
));
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this._promptResetData($l(
|
||||
"An error occured while loading your data! If the problem persists, please try " +
|
||||
"resetting or reinstalling the app!"
|
||||
));
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_passwordStrength(pwd) {
|
||||
const score = pwd ? zxcvbn(pwd).score : -1;
|
||||
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
|
||||
return strength && $l("strength: {0}", strength);
|
||||
}
|
||||
|
||||
_getStartedHint(step) {
|
||||
return [
|
||||
$l(
|
||||
"Logging in will unlock advanced features like automatic backups and seamless " +
|
||||
"synchronization between all your devices!"
|
||||
),
|
||||
$l(
|
||||
"Your **master password** is a single passphrase used to protect your data. " +
|
||||
"Without it, nobody will be able to access your data - not even us!"
|
||||
),
|
||||
$l(
|
||||
"**Don't forget your master password!** For privacy and security reasons we never store your " +
|
||||
"password anywhere which means we won't be able to help you recover your data in case you forget " +
|
||||
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
|
||||
)
|
||||
][step];
|
||||
}
|
||||
|
||||
_getStartedClass(currStep, step) {
|
||||
return currStep > step ? "left" : currStep < step ? "right" : "center";
|
||||
}
|
||||
|
||||
_goToStep(e) {
|
||||
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
|
||||
if (s < this._getStartedStep) {
|
||||
this._getStartedStep = s;
|
||||
}
|
||||
}
|
||||
|
||||
_openProductPage() {
|
||||
getAppStoreLink().then((link) => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_openPwdHowto() {
|
||||
window.open("https://padlock.io/howto/choose-master-password/", "_system");
|
||||
}
|
||||
|
||||
_rumble() {
|
||||
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`),
|
||||
{ animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
_enterCloudPassword() {
|
||||
if (this._restoringCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.$.cloudPwdInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.cloudPwdInput.blur();
|
||||
this.$.cloudPwdButton.start();
|
||||
this._restoringCloud = true;
|
||||
|
||||
this.cloudSource.password = password;
|
||||
this.collection.fetch(this.cloudSource)
|
||||
.then(() => {
|
||||
this.$.cloudPwdButton.success();
|
||||
this._restoringCloud = false;
|
||||
this._getStartedStep = 5;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.cloudPwdButton.fail();
|
||||
this._restoringCloud = false;
|
||||
|
||||
if (e.code === "decryption_failed") {
|
||||
this._rumble();
|
||||
} else {
|
||||
this._handleCloudError(e);
|
||||
}
|
||||
});
|
||||
|
||||
track("Setup: Remote Password", { Email: this.settings.syncEmail });
|
||||
}
|
||||
|
||||
_forgotCloudPassword() {
|
||||
this.forgotCloudPassword().then(() => {
|
||||
this._hasCloudData = false;
|
||||
this._connected();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(StartView.is, StartView);
|
|
@ -218,6 +218,12 @@
|
|||
"integrity": "sha512-PBHCvO98hNec9A491vBbh0ZNDOVxccwKL1u2pm6fs9oDgm7SEnw0lEHqHfjsYryDxnE3zaf7LvERWEXjOp1hig==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/zxcvbn": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz",
|
||||
"integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==",
|
||||
"dev": true
|
||||
},
|
||||
"@webcomponents/shadycss": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.2.1.tgz",
|
||||
|
@ -8132,9 +8138,9 @@
|
|||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
|
||||
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"semver-diff": {
|
||||
"version": "2.1.0",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@types/moment-duration-format": "^2.2.0",
|
||||
"@types/papaparse": "^4.1.28",
|
||||
"@types/semver": "^5.3.33",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"archiver": "^1.2.0",
|
||||
"bower": "^1.8.0",
|
||||
"browserify": "^13.1.1",
|
||||
|
@ -39,7 +40,6 @@
|
|||
"polymer-analyzer": "^2.2.0",
|
||||
"polymer-bundler": "^2.3.1",
|
||||
"rimraf": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"st": "^1.2.0",
|
||||
"tsify": "^2.0.3",
|
||||
"typescript": "^2.8.3",
|
||||
|
@ -58,12 +58,12 @@
|
|||
"moment": "^2.21.0",
|
||||
"moment-duration-format": "^2.2.2",
|
||||
"papaparse": "^4.1.2",
|
||||
"semver": "^5.5.0",
|
||||
"uuid": "^3.1.0",
|
||||
"yargs": "^4.8.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"bower-install": "pushd app && bower install && popd",
|
||||
"compile": "gulp compile --silent",
|
||||
"lint": "eslint app/**/*.{js,html}",
|
||||
"test": "karma start --single-run --browsers ChromeHeadless karma.conf.js",
|
||||
|
@ -71,6 +71,6 @@
|
|||
"build:mac": "gulp build --mac --silent",
|
||||
"build:win": "gulp build --win --silent",
|
||||
"build:linux": "gulp build --linux --silent",
|
||||
"postinstall": "npm run bower-install && npm run compile && mkdir -p cordova/www"
|
||||
"postinstall": "npm run compile && mkdir -p cordova/www"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,10 @@
|
|||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["typings/*"]
|
||||
"*": ["typings/*"],
|
||||
"zxcvbn/dist/zxcvbn": ["node_modules/@types/zxcvbn"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"app/src/**/*.ts",
|
||||
"typings/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules/**/*.ts"
|
||||
]
|
||||
"include": ["app/src/**/*.ts", "typings/*"],
|
||||
"exclude": ["node_modules/**/*.ts"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue