Begin migration to Polymer 3.0

This commit is contained in:
Martin Kleinschrodt 2018-05-29 12:23:57 +02:00
parent a464f8d13f
commit ce10f86145
68 changed files with 14760 additions and 5355 deletions

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -10,9 +10,10 @@
margin: 0;
} </style>
<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<link rel="stylesheet" href="./src/styles/fonts.css">
<link rel="import" href="src/ui/app/app.html">
<script type="module" src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script type="module" src="./src/ui/app/app.js"></script>
</head>
<body>

11
app/index.js Normal file
View File

@ -0,0 +1,11 @@
import './src/ui/app/app.js';
import '../@webcomponents/webcomponentsjs/webcomponents-bundle.js';
const $_documentContainer = document.createElement('template');
$_documentContainer.setAttribute('style', 'display: none;');
$_documentContainer.innerHTML = `<title>Padlock</title><style> html, body {
background: #59c6ff;
margin: 0;
} </style><pl-app></pl-app>`;
document.head.appendChild($_documentContainer.content);

22
app/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"homepage": "http://padlock.io",
"name": "padlock",
"version": "3.0.0",
"resolutions": {
"inherits": "2.0.3",
"samsam": "1.1.3",
"supports-color": "3.1.2",
"type-detect": "1.0.0",
"@webcomponents/webcomponentsjs": "2.0.0-beta.2"
},
"main": "index.js",
"author": "Martin Kleinschrodt <martin@maklesoft.com>",
"license": "GPL-3.0",
"dependencies": {
"@polymer/polymer": "^3.0.0",
"@polymer/iron-list": "^3.0.0-pre.18",
"@polymer/paper-spinner": "^3.0.0-pre.18",
"@webcomponents/webcomponentsjs": "^2.0.0"
},
"devDependencies": {}
}

View File

@ -1,18 +1,18 @@
<script src="../padlock-lite.js"></script>
<link rel="import" href="../styles/shared.html">
<link rel="import" href="../ui/animation/animation.html">
<link rel="import" href="../ui/base/base.html">
<link rel="import" href="../ui/dialog/dialog-mixin.html">
<link rel="import" href="../ui/icon/icon.html">
<link rel="import" href="../ui/input/input.html">
<link rel="import" href="../ui/loading-button/loading-button.html">
<link rel="import" href="../ui/locale/locale.html">
<link rel="import" href="../ui/notification/notification.html">
<link rel="import" href="../ui/payment-dialog/payment-dialog.html">
<link rel="import" href="../ui/promo/promo.html">
<link rel="import" href="../ui/promo/promo-dialog.html">
<link rel="import" href="../ui/sync/subinfo.html">
<script type="module" src="../styles/shared.js"></script>
<script type="module" src="../ui/animation/animation.js"></script>
<script type="module" src="../ui/base/base.js"></script>
<script type="module" src="../ui/dialog/dialog-mixin.js"></script>
<script type="module" src="../ui/icon/icon.js"></script>
<script type="module" src="../ui/input/input.js"></script>
<script type="module" src="../ui/loading-button/loading-button.js"></script>
<script type="module" src="../ui/locale/locale.js"></script>
<script type="module" src="../ui/notification/notification.js"></script>
<script type="module" src="../ui/payment-dialog/payment-dialog.js"></script>
<script type="module" src="../ui/promo/promo.js"></script>
<script type="module" src="../ui/promo/promo-dialog.js"></script>
<script type="module" src="../ui/sync/subinfo.js"></script>
<dom-module id="pl-cloud-dashboard">
@ -407,8 +407,20 @@
</template>
<script>
(() => {
<script type="module">
import '../styles/shared.js';
import '../ui/animation/animation.js';
import '../ui/base/base.js';
import '../ui/dialog/dialog-mixin.js';
import '../ui/icon/icon.js';
import '../ui/input/input.js';
import '../ui/loading-button/loading-button.js';
import '../ui/locale/locale.js';
import '../ui/notification/notification.js';
import '../ui/payment-dialog/payment-dialog.js';
import '../ui/promo/promo.js';
import '../ui/promo/promo-dialog.js';
import '../ui/sync/subinfo.js';
const { LocaleMixin, DialogMixin, NotificationMixin, AnimationMixin, SubInfoMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
@ -683,8 +695,6 @@ class Dashboard extends applyMixins(
}
window.customElements.define(Dashboard.is, Dashboard);
})();
</script>
</script>
</dom-module>

View File

@ -1,6 +1,8 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
import '../../../../node_modules/@polymer/polymer/polymer-legacy.js';
const $_documentContainer = document.createElement('template');
$_documentContainer.setAttribute('style', 'display: none;');
<custom-style>
$_documentContainer.innerHTML = `<custom-style>
<style is="custom-style">
body {
@ -132,4 +134,6 @@
}
</style>
</custom-style>
</custom-style>`;
document.head.appendChild($_documentContainer.content);

View File

@ -39,3 +39,11 @@
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'FontAwesome';
src: url('../../assets/fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../../assets/fonts/fontawesome-webfont.eot?#iefix&amp;v=4.7.0') format('embedded-opentype'), url('../../assets/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../../assets/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../../assets/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../../assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -1,7 +1,8 @@
<link rel="stylesheet" href="fonts.css">
<link rel="import" href="config.html">
import './config.js';
const $_documentContainer = document.createElement('template');
$_documentContainer.setAttribute('style', 'display: none;');
<dom-module id="shared">
$_documentContainer.innerHTML = `<dom-module id="shared">
<template>
<style>
:host {
@ -55,7 +56,7 @@
button.arrow::before, a.button.arrow::before {
font-family: "FontAwesome";
content: "\f054";
content: "\\f054";
display: block;
position: absolute;
top: 0;
@ -350,4 +351,6 @@
</style>
</template>
</dom-module>
</dom-module>`;
document.head.appendChild($_documentContainer.content);

View File

@ -1,7 +1,4 @@
<link rel="import" href="../base/base.html">
<script>
(() => {
import '../base/base.js';
const defaults = {
animation: "slideIn",
@ -56,6 +53,3 @@ padlock.AnimationMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,8 +1,5 @@
<link rel="import" href="../base/base.html">
<link rel="import" href="../data/data.html">
<script>
(() => {
import '../base/base.js';
import '../data/data.js';
const startedLoading = new Date().getTime();
const { init, track, setTrackingID } = padlock.tracking;
@ -115,6 +112,3 @@ padlock.AnalyticsMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,29 +1,53 @@
<script src="../../padlock.js"></script>
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';
<link rel="import" href="./analytics.html">
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../clipboard/clipboard.html">
<link rel="import" href="../cloud-view/cloud-view.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../list-view/list-view.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../notification/notification.html">
<link rel="import" href="../record-view/record-view.html">
<link rel="import" href="../settings-view/settings-view.html">
<link rel="import" href="../start-view/start-view.html">
<link rel="import" href="../sync/sync.html">
<link rel="import" href="../title-bar/title-bar.html">
<link rel="import" href="./auto-lock.html">
<link rel="import" href="./auto-sync.html">
<link rel="import" href="./hints.html">
<link rel="import" href="./messages.html">
/* global cordova, StatusBar */
<dom-module id="pl-app">
<template>
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const { getPlatformName, getDeviceInfo, isTouch } = padlock.platform;
const cordovaReady = new Promise((resolve) => {
document.addEventListener("deviceready", resolve);
});
class App extends applyMixins(
BaseElement,
DataMixin,
SyncMixin,
AutoSyncMixin,
AutoLockMixin,
DialogMixin,
MessagesMixin,
NotificationMixin,
HintsMixin,
AnalyticsMixin,
AnimationMixin,
ClipboardMixin,
LocaleMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
@keyframes fadeIn {
@ -236,7 +260,7 @@
.last-sync::before {
font-family: "FontAwesome";
font-size: 90%;
content: "\f017\ ";
content: "\\f017\\ ";
}
.menu-wrapper {
@ -255,7 +279,7 @@
.menu-item-hint.warning::before {
font-family: "FontAwesome";
font-size: 85%;
content: "\f071\ ";
content: "\\f071\\ ";
position: relative;
top: -1px;
}
@ -316,58 +340,58 @@
<pl-start-view id="startView"></pl-start-view>
<div id="menuWrapper" class="menu-wrapper" on-click="_menuWrapperClicked" show-tags$="[[ _showingTags ]]">
<div id="menuWrapper" class="menu-wrapper" on-click="_menuWrapperClicked" show-tags\$="[[ _showingTags ]]">
<div id="menu" class="menu">
<div class="spacer"></div>
<div class="account menu-item tap" on-click="_openCloudView">
<div>
<div hidden$="[[ settings.syncConnected ]]">[[ $l("Log In") ]]</div>
<div hidden$="[[ !settings.syncConnected ]]">[[ $l("My Account") ]]</div>
<div class="menu-item-hint warning" hidden$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Trial Expired") ]]</div>
<div class="menu-item-hint warning" hidden$="[[ !isSubUnpaid(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Payment Failed") ]]</div>
<div class="menu-item-hint warning" hidden$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Subscr. Canceled") ]]</div>
<div hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log In") ]]</div>
<div hidden\$="[[ !settings.syncConnected ]]">[[ \$l("My Account") ]]</div>
<div class="menu-item-hint warning" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Trial Expired") ]]</div>
<div class="menu-item-hint warning" hidden\$="[[ !isSubUnpaid(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Payment Failed") ]]</div>
<div class="menu-item-hint warning" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Subscr. Canceled") ]]</div>
</div>
<pl-icon icon="cloud" class="account-icon"></pl-icon>
</div>
<div class="menu-item tap" on-click="synchronize" disabled$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
<div class="menu-item tap" on-click="synchronize" disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
<div>
<div>[[ $l("Synchronize") ]]</div>
<div class="menu-item-hint" hidden$="[[ settings.syncConnected ]]">[[ $l("Log In To Sync") ]]</div>
<div class="menu-item-hint last-sync" hidden$="[[ !settings.syncConnected ]]">[[ lastSync ]]</div>
<div>[[ \$l("Synchronize") ]]</div>
<div class="menu-item-hint" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log In To Sync") ]]</div>
<div class="menu-item-hint last-sync" hidden\$="[[ !settings.syncConnected ]]">[[ lastSync ]]</div>
</div>
<pl-icon icon="refresh" spin$="[[ isSynching ]]"></pl-icon>
<pl-icon icon="refresh" spin\$="[[ isSynching ]]"></pl-icon>
</div>
<!-- <div class="menu&#45;item tap" on&#45;click="_newRecord"> -->
<!-- <div>[[ $l("Add Record") ]]</div> -->
<!-- <div>[[ \$l("Add Record") ]]</div> -->
<!-- <pl&#45;icon icon="add"></pl&#45;icon> -->
<!-- </div> -->
<div class="menu-item tap" on-click="_openSettings">
<div>[[ $l("Settings") ]]</div>
<div>[[ \$l("Settings") ]]</div>
<pl-icon icon="settings"></pl-icon>
</div>
<div class="menu-item tap" on-click="_showTags">
<div>[[ $l("Tags") ]]</div>
<div>[[ \$l("Tags") ]]</div>
<pl-icon icon="tag"></pl-icon>
</div>
<div class="menu-item tap" on-click="_enableMultiSelect">
<div>[[ $l("Multi-Select") ]]</div>
<div>[[ \$l("Multi-Select") ]]</div>
<pl-icon icon="checked"></pl-icon>
</div>
<div class="menu-item tap" on-click="_lock">
<div>[[ $l("Lock App") ]]</div>
<div>[[ \$l("Lock App") ]]</div>
<pl-icon icon="lock"></pl-icon>
</div>
<div class="spacer"></div>
<div class="menu-info">
<div><strong>Padlock {{ settings.version }}</strong></div>
<div>Made with &#9829; in Germany</div>
<div>Made with in Germany</div>
</div>
</div>
<div class="tags">
<div class="spacer"></div>
<div class="menu-item tap" on-click="_closeTags">
<div>[[ $l("Tags") ]]</div>
<div>[[ \$l("Tags") ]]</div>
<pl-icon icon="close"></pl-icon>
</div>
<template is="dom-repeat" items="[[ collection.tags ]]">
@ -376,8 +400,8 @@
<pl-icon icon="tag"></pl-icon>
</div>
</template>
<div class="no-tags" disabled hidden$="[[ _hasTags(collection.tags) ]]">
[[ $l("You don't have any tags yet!") ]]
<div class="no-tags" disabled="" hidden\$="[[ _hasTags(collection.tags) ]]">
[[ \$l("You don't have any tags yet!") ]]
</div>
<div class="spacer"></div>
</div>
@ -385,12 +409,7 @@
<div id="main">
<pl-list-view id="listView"
selected-record="{{ _selectedRecord }}"
on-open-settings="_openSettings"
on-open-cloud-view="_openCloudView"
on-toggle-menu="_toggleMenu"
></pl-list-view>
<pl-list-view id="listView" selected-record="{{ _selectedRecord }}" on-open-settings="_openSettings" on-open-cloud-view="_openCloudView" on-toggle-menu="_toggleMenu"></pl-list-view>
<div id="pages">
@ -398,362 +417,324 @@
<pl-icon icon="logo" class="placeholder-icon"></pl-icon>
</div>
<pl-record-view id="recordView" class="view"
on-record-close="_closeRecord"></pl-record-view>
<pl-record-view id="recordView" class="view" on-record-close="_closeRecord"></pl-record-view>
<pl-settings-view id="settingsView" class="view"
on-settings-back="_settingsBack"></pl-settings-view>
<pl-settings-view id="settingsView" class="view" on-settings-back="_settingsBack"></pl-settings-view>
<pl-cloud-view id="cloudView" class="view"
on-cloud-back="_cloudViewBack"></pl-cloud-view>
<pl-cloud-view id="cloudView" class="view" on-cloud-back="_cloudViewBack"></pl-cloud-view>
</div>
</div>
<pl-title-bar></pl-title-bar>
`;
}
</template>
static get is() { return "pl-app"; }
<script>
(() => {
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
}
}; }
/* global cordova, StatusBar */
constructor() {
super();
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const { getPlatformName, getDeviceInfo, isTouch } = padlock.platform;
// 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);
const cordovaReady = new Promise((resolve) => {
document.addEventListener("deviceready", resolve);
});
// Listen for android back button
document.addEventListener("backbutton", this._back.bind(this), false);
class App extends applyMixins(
BaseElement,
DataMixin,
SyncMixin,
AutoSyncMixin,
AutoLockMixin,
DialogMixin,
MessagesMixin,
NotificationMixin,
HintsMixin,
AnalyticsMixin,
AnimationMixin,
ClipboardMixin,
LocaleMixin
) {
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
}
static get is() { return "pl-app"; }
get _isNarrow() {
return this.offsetWidth < 600;
}
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
}
}; }
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();
});
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);
// 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"));
}
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();
});
getPlatformName().then((platform) => {
const className = platform.toLowerCase().replace(/ /g, "-");
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);
}
_hasTags() {
return !!this.collection.tags.length;
}
_selectTag(e) {
setTimeout(() => {
this.$.listView.filterString = e.model.item;
}, 350);
}
_hasTags() {
return !!this.collection.tags.length;
}
}
window.customElements.define(App.is, App);
})();
</script>
<script src="../../../bower_components/zxcvbn/lib/zxcvbn.js"></script>
</dom-module>

View File

@ -1,7 +1,4 @@
<link rel="import" href="../base/base.html">
<script>
(() => {
import '../base/base.js';
padlock.AutoLockMixin = (superClass) => {
@ -78,6 +75,3 @@ padlock.AutoLockMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,7 +1,4 @@
<link rel="import" href="../base/base.html">
<script>
(() => {
import '../base/base.js';
padlock.AutoSyncMixin = (superClass) => {
@ -22,6 +19,3 @@ padlock.AutoSyncMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,8 +1,5 @@
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<script>
(() => {
import '../base/base.js';
import '../locale/locale.js';
const { wait } = padlock.util;
const { isChromeApp, isChromeOS, getReviewLink } = padlock.platform;
@ -269,6 +266,3 @@ padlock.HintsMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,8 +1,5 @@
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<script>
(() => {
import '../base/base.js';
import '../locale/locale.js';
const { Messages } = padlock.messages;
const { FileSource } = padlock.source;
@ -45,6 +42,3 @@ padlock.MessagesMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,25 +0,0 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<script>
(() => {
window.padlock = window.padlock || {};
padlock.BaseElement = class Base extends Polymer.Element {
truthy(val) {
return !!val;
}
equals(val, ...vals) {
return vals.some((v) => v === val);
}
identity(val) {
return val;
}
};
})();
</script>

22
app/src/ui/base/base.js Normal file
View File

@ -0,0 +1,22 @@
import '../../../../../node_modules/@polymer/polymer/polymer-legacy.js';
import { PolymerElement, html } from '../../../../../node_modules/@polymer/polymer/polymer-element.js';
import "../../padlock.js";
window.Polymer = { html };
window.padlock = window.padlock || {};
padlock.BaseElement = class Base extends PolymerElement {
truthy(val) {
return !!val;
}
equals(val, ...vals) {
return vals.some((v) => v === val);
}
identity(val) {
return val;
}
};

View File

@ -1,11 +1,13 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
import '../../styles/shared.js';
import '../base/base.js';
import '../locale/locale.js';
<dom-module id="pl-clipboard">
<template>
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;
@ -52,66 +54,59 @@
</style>
<div class="content">
<div class="title">[[ $l("Copied To Clipboard:") ]]</div>
<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><strong>[[ \$l("Clear") ]]</strong></div>
<div class="countdown">[[ _tMinusClear ]]s<div>
</button>
</template>
</div></div></button>
`;
}
<script>
(() => {
static get is() { return "pl-clipboard"; }
const { setClipboard } = padlock.platform;
const { LocaleMixin } = padlock;
static get properties() { return {
record: Object,
field: Object
}; }
class Clipboard extends LocaleMixin(padlock.BaseElement) {
set(record, field, duration = 60) {
clearInterval(this._interval);
static get is() { return "pl-clipboard"; }
this.record = record;
this.field = field;
setClipboard(field.value);
static get properties() { return {
record: Object,
field: Object
}; }
this.classList.add("showing");
set(record, field, duration = 60) {
clearInterval(this._interval);
const tStart = Date.now();
this.record = record;
this.field = field;
setClipboard(field.value);
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);
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;
}
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);
@ -139,9 +134,3 @@ padlock.ClipboardMixin = (baseClass) => {
};
};
})();
</script>
</dom-module>

View File

@ -1,21 +1,31 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../loading-button/loading-button.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../notification/notification.html">
<link rel="import" href="../promo/promo.html">
<link rel="import" href="../sync/sync.html">
<link rel="import" href="../toggle/toggle-button.html">
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';
<dom-module id="pl-cloud-view">
<template>
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
class CloudView extends applyMixins(
BaseElement,
DataMixin,
SyncMixin,
LocaleMixin,
DialogMixin,
NotificationMixin,
AnimationMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: flex;
@ -181,62 +191,62 @@
<header>
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
<div class="title">[[ $l("My Account") ]]</div>
<div class="title">[[ \$l("My Account") ]]</div>
<pl-icon icon="logout" class="tap" on-click="_logout"></pl-icon>
</header>
<main>
<section class="highlight dark" hidden$="[[ !truthy(promo) ]]">
<section class="highlight dark" hidden\$="[[ !truthy(promo) ]]">
<pl-promo promo="[[ promo ]]" data-source="App - Promo" on-promo-redeem="_buySubscription" on-promo-expired="_promoExpired"></pl-promo>
</section>
<section class="highlight tiles warning" hidden$="[[ !isTrialing(subStatus) ]]">
<section class="highlight tiles warning" hidden\$="[[ !isTrialing(subStatus) ]]">
<div class="info">
<pl-icon class="info-icon" icon="time"></pl-icon>
<div class="info-body">
<div class="info-title">[[ $l("Trialing ({0} days left)", remainingTrialDays) ]]</div>
<div class="info-title">[[ \$l("Trialing ({0} days left)", remainingTrialDays) ]]</div>
<div class="info-text">[[ trialingMessage(remainingTrialDays) ]]</div>
</div>
</div>
<button class="tap" on-click="_buySubscription" data-source="App - Trialing">[[ $l("Upgrade Now") ]]</button>
<button class="tap" on-click="_buySubscription" data-source="App - Trialing">[[ \$l("Upgrade Now") ]]</button>
</section>
<section class="highlight tiles warning" hidden$="[[ !isTrialExpired(subStatus) ]]">
<section class="highlight tiles warning" hidden\$="[[ !isTrialExpired(subStatus) ]]">
<div class="info">
<pl-icon class="info-icon" icon="error"></pl-icon>
<div class="info-body">
<div class="info-title">[[ $l("Trial Expired") ]]</div>
<div class="info-title">[[ \$l("Trial Expired") ]]</div>
<div class="info-text">[[ trialExpiredMessage() ]]</div>
</div>
</div>
<button class="tap" on-click="_buySubscription" data-source="App - Trial Expired">[[ $l("Upgrade Now") ]]</button>
<button class="tap" on-click="_buySubscription" data-source="App - Trial Expired">[[ \$l("Upgrade Now") ]]</button>
</section>
<section class="highlight tiles warning" hidden$="[[ !isSubUnpaid(subStatus) ]]">
<section class="highlight tiles warning" hidden\$="[[ !isSubUnpaid(subStatus) ]]">
<div class="info">
<pl-icon class="info-icon" icon="error"></pl-icon>
<div class="info-body">
<div class="info-title">[[ $l("Payment Failed") ]]</div>
<div class="info-title">[[ \$l("Payment Failed") ]]</div>
<div class="info-text">[[ subUnpaidMessage() ]]</div>
</div>
</div>
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Payment Failed">[[ $l("Update Payment Method") ]]</button>
<button class="tap" on-click="_contactSupport">[[ $l("Contact Support") ]]</button>
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Payment Failed">[[ \$l("Update Payment Method") ]]</button>
<button class="tap" on-click="_contactSupport">[[ \$l("Contact Support") ]]</button>
</section>
<section class="highlight tiles warning" hidden$="[[ !isSubCanceled(subStatus) ]]">
<section class="highlight tiles warning" hidden\$="[[ !isSubCanceled(subStatus) ]]">
<div class="info">
<pl-icon class="info-icon" icon="error"></pl-icon>
<div class="info-body">
<div class="info-title">[[ $l("Subscription Canceled") ]]</div>
<div class="info-title">[[ \$l("Subscription Canceled") ]]</div>
<div class="info-text">[[ subCanceledMessage() ]]</div>
</div>
</div>
<button class="tap" on-click="reactivateSubscription">[[ $l("Reactivate Subscription") ]]</button>
<button class="tap" on-click="reactivateSubscription">[[ \$l("Reactivate Subscription") ]]</button>
</section>
<section class="highlight hidden$="[[ !settings.syncConnected ]]">
<section class="highlight hidden\$=" [[="" !settings.syncconnected="" ]]"="">
<div class="account tiles">
<div class="account-info">
<div class="account-email">[[ settings.syncEmail ]]</div>
@ -249,47 +259,44 @@
<span>[[ lastSync ]]</span>
</div>
</div>
<pl-icon class="account-sync tap" icon="refresh" spin$="[[ isSynching ]]" on-click="synchronize"
disabled$="[[ !isSubValid(subStatus) ]]"></pl-icon>
<pl-icon class="account-sync tap" icon="refresh" spin\$="[[ isSynching ]]" on-click="synchronize" disabled\$="[[ !isSubValid(subStatus) ]]"></pl-icon>
</div>
<div class="unlock-feature-hint" hidden$="[[ isSubValid(subStatus) ]]">[[ $l("Upgrade to enable synchronization!") ]]</div>
<div class="unlock-feature-hint" hidden\$="[[ isSubValid(subStatus) ]]">[[ \$l("Upgrade to enable synchronization!") ]]</div>
</section>
<section hidden$="[[ !settings.syncConnected ]]">
<div class="section-header">[[ $l("{0} Devices Connected", account.devices.length) ]]</div>
<section hidden\$="[[ !settings.syncConnected ]]">
<div class="section-header">[[ \$l("{0} Devices Connected", account.devices.length) ]]</div>
<div class="devices">
<template is="dom-repeat" items="[[ account.devices ]]">
<div class="section-row">
<div class="section-row-label">[[ item.description ]]</div>
<pl-icon icon="delete" class="tap" on-click="_revokeDevice" disabled$="[[ _isCurrentDevice(item) ]]"></pl-icon>
<pl-icon icon="delete" class="tap" on-click="_revokeDevice" disabled\$="[[ _isCurrentDevice(item) ]]"></pl-icon>
</div>
</template>
</div>
</section>
<section hidden$="[[ !truthy(account.paymentSource) ]]">
<div class="section-header">[[ $l("Billing") ]]</div>
<section hidden\$="[[ !truthy(account.paymentSource) ]]">
<div class="section-header">[[ \$l("Billing") ]]</div>
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Billing">[[ _paymentSourceLabel(account.paymentSource) ]]</button>
<button class="tap" on-click="cancelSubscription"
hidden$="[[ !isSubActive(subStatus) ]]">[[ $l("Cancel Subscription") ]]</button>
<button class="tap" on-click="cancelSubscription" hidden\$="[[ !isSubActive(subStatus) ]]">[[ \$l("Cancel Subscription") ]]</button>
</section>
</main>
<div class="login" hidden$="[[ settings.syncConnected ]]">
<div class="login" hidden\$="[[ settings.syncConnected ]]">
<pl-icon icon="close" class="back-button tap" on-click="_back"></pl-icon>
<div class="spacer"></div>
<div class="title">[[ $l("Padlock Online") ]]</div>
<div class="title">[[ \$l("Padlock Online") ]]</div>
<div class="text">[[ loginInfoText() ]]</div>
<pl-input id="emailInput" type="email" placeholder="[[ $l('Enter Email Address') ]]" value="[[ settings.syncEmail ]]"
select-on-focus required on-enter="_login" class="tap"></pl-input>
<pl-input id="emailInput" type="email" placeholder="[[ \$l('Enter Email Address') ]]" value="[[ settings.syncEmail ]]" select-on-focus="" required="" on-enter="_login" class="tap"></pl-input>
<pl-loading-button id="loginButton" class="tap" on-click="_login">[[ $l("Log In") ]]</pl-loading-button>
<pl-loading-button id="loginButton" class="tap" on-click="_login">[[ \$l("Log In") ]]</pl-loading-button>
<div class="spacer"></div>
@ -302,158 +309,136 @@
</div>
<div class="rounded-corners"></div>
`;
}
</template>
static get is() { return "pl-cloud-view"; }
<script>
(() => {
static get properties() {
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
}
class CloudView extends applyMixins(
BaseElement,
DataMixin,
SyncMixin,
LocaleMixin,
DialogMixin,
NotificationMixin,
AnimationMixin
) {
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());
}
static get is() { return "pl-cloud-view"; }
animate() {
if (this.settings.syncConnected) {
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
}
}
static get properties() {
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;
}
}
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());
}
_back() {
this.dispatchEvent(new CustomEvent("cloud-back"));
}
animate() {
if (this.settings.syncConnected) {
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
}
}
_logout() {
this.confirm(
$l("Are you sure you want to log out?"),
$l("Log Out")
).then((confirmed) => {
if (confirmed) {
this.disconnectCloud();
}
});
}
focusEmailInput() {
this.$.emailInput.focus();
}
_login() {
if (this._submittingEmail) {
return;
}
_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;
}
}
this.$.loginButton.start();
_back() {
this.dispatchEvent(new CustomEvent("cloud-back"));
}
if (this.$.emailInput.invalid) {
this.alert($l("Please enter a valid email address!"))
.then(() => this.$.emailInput.focus());
this.$.loginButton.fail();
return;
}
_logout() {
this.confirm(
$l("Are you sure you want to log out?"),
$l("Log Out")
).then((confirmed) => {
if (confirmed) {
this.disconnectCloud();
}
});
}
this._submittingEmail = true;
_login() {
if (this._submittingEmail) {
return;
}
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.$.loginButton.start();
}
if (this.$.emailInput.invalid) {
this.alert($l("Please enter a valid email address!"))
.then(() => this.$.emailInput.focus());
this.$.loginButton.fail();
return;
}
_isCurrentDevice(device) {
return this.settings.syncId === device.tokenId;
}
this._submittingEmail = true;
_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" });
});
}
});
}
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();
});
_contactSupport() {
window.open("mailto:support@padlock.io", "_system");
}
}
_paymentSourceLabel() {
const s = this.account && this.account.paymentSource;
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
}
_isCurrentDevice(device) {
return this.settings.syncId === device.tokenId;
}
_buySubscription(e) {
this.buySubscription(e.target.dataset.source);
}
_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" });
});
}
});
}
_contactSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_paymentSourceLabel() {
const s = this.account && this.account.paymentSource;
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
}
_buySubscription(e) {
this.buySubscription(e.target.dataset.source);
}
_updatePaymentMethod(e) {
this.updatePaymentMethod(e.target.dataset.source);
}
_promoExpired() {
this.dispatch("settings-changed");
}
_updatePaymentMethod(e) {
this.updatePaymentMethod(e.target.dataset.source);
}
_promoExpired() {
this.dispatch("settings-changed");
}
}
window.customElements.define(CloudView.is, CloudView);
})();
</script>
</dom-module>

View File

@ -1,14 +1,11 @@
<link rel="import" href="../../../bower_components/polymer/lib/mixins/mutable-data.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<script>
(() => {
import { MutableData } from '../../../../../node_modules/@polymer/polymer/lib/mixins/mutable-data.js';
import '../base/base.js';
import '../locale/locale.js';
import { Polymer } from '../../../../../node_modules/@polymer/polymer/lib/legacy/polymer-fn.js';
const { Collection, Record, Settings } = padlock.data;
const { FileSource, EncryptedSource } = padlock.source;
const { getDesktopSettings } = padlock.platform;
const { MutableData } = Polymer;
const desktopSettings = getDesktopSettings();
const dbPath = desktopSettings ? desktopSettings.get("dbPath") : "data.pls";
@ -293,6 +290,3 @@ Object.assign(padlock.DataMixin, {
collection,
settings
});
})();
</script>

View File

@ -1,134 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="dialog.html">
<dom-module id="pl-dialog-alert">
<template>
<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>
</template>
<script>
(() => {
const defaultButtonLabel = $l("OK");
class DialogAlert extends padlock.BaseElement {
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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,124 @@
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);

View File

@ -1,67 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="dialog.html">
<dom-module id="pl-dialog-confirm">
<template>
<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>
</template>
<script>
(() => {
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 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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,58 @@
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);

View File

@ -1,13 +1,10 @@
<link rel="import" href="../base/base.html">
<link rel="import" href="../generator/generator.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="dialog-alert.html">
<link rel="import" href="dialog-confirm.html">
<link rel="import" href="dialog-prompt.html">
<link rel="import" href="dialog-options.html">
<script>
(() => {
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';
const dialogElements = {};
@ -107,6 +104,3 @@ padlock.DialogMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,72 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="dialog.html">
<dom-module id="pl-dialog-options">
<template>
<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>
</template>
<script>
(() => {
class DialogOptions extends padlock.BaseElement {
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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,62 @@
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);

View File

@ -1,120 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../loading-button/loading-button.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="dialog.html">
<dom-module id="pl-dialog-prompt">
<template>
<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>
</template>
<script>
(() => {
const defaultConfirmLabel = $l("OK");
const defaultCancelLabel = $l("Cancel");
const defaultType = "text";
const defaultPlaceholder = "";
class DialogPrompt extends padlock.BaseElement {
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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,111 @@
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);

View File

@ -1,191 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../input/input.html">
<dom-module id="pl-dialog">
<template>
<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>
</template>
<script>
(() => {
class Dialog extends padlock.AnimationMixin(padlock.BaseElement) {
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);
})();
</script>
</dom-module>

181
app/src/ui/dialog/dialog.js Normal file
View File

@ -0,0 +1,181 @@
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);

View File

@ -1,57 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog.html">
<link rel="import" href="./export.html">
<dom-module id="pl-export-dialog">
<template>
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
};
}
.message {
font-weight: bold;
}
</style>
<pl-dialog id="dialog">
<div class="message">[[ $l("Export {0} Records", records.length) ]]</div>
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
</pl-dialog>
</template>
<script>
(function() {
const { BaseElement, LocaleMixin } = padlock;
class PlExportDialog extends LocaleMixin(BaseElement) {
static get is() { return "pl-export-dialog"; }
static get properties() { return {
records: Array
}; }
_close() {
this.$.dialog.open = false;
}
export(records) {
this.records = records;
this.$.dialog.open = true;
}
}
window.customElements.define(PlExportDialog.is, PlExportDialog);
})();
</script>
</dom-module>

View File

@ -0,0 +1,46 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import './export.js';
const { BaseElement, LocaleMixin } = padlock;
class PlExportDialog extends LocaleMixin(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%);
};
}
.message {
font-weight: bold;
}
</style>
<pl-dialog id="dialog">
<div class="message">[[ \$l("Export {0} Records", records.length) ]]</div>
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
</pl-dialog>
`;
}
static get is() { return "pl-export-dialog"; }
static get properties() { return {
records: Array
}; }
_close() {
this.$.dialog.open = false;
}
export(records) {
this.records = records;
this.$.dialog.open = true;
}
}
window.customElements.define(PlExportDialog.is, PlExportDialog);

View File

@ -1,172 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
<dom-module id="pl-export">
<template>
<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>
</template>
<script>
/* global zxcvbn */
(() => {
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 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);
})();
</script>
</dom-module>

159
app/src/ui/export/export.js Normal file
View File

@ -0,0 +1,159 @@
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);

View File

@ -1,14 +1,16 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../slider/slider.html">
<link rel="import" href="../toggle/toggle-button.html">
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';
<dom-module id="pl-generator">
const { BaseElement, LocaleMixin } = padlock;
<template>
class Generator extends LocaleMixin(BaseElement) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-inner: {
@ -45,7 +47,7 @@
.header::before, .header::after {
font-family: "FontAwesome";
content: "\ \f0e7\ ";
content: "\\ \\f0e7\\ ";
}
.value {
@ -81,96 +83,83 @@
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
<div class="generate-button tap" on-click="_generate">
<div class="header">[[ $l("Generate Random Value") ]]</div>
<div class="header">[[ \$l("Generate Random Value") ]]</div>
<div class="value tiles-1">
{{ value }}
</div>
</div>
<pl-toggle-button label="a-z" active="{{ lower }}" class="tap" reverse></pl-toggle-button>
<pl-toggle-button label="A-Z" active="{{ upper }}" class="tap" reverse></pl-toggle-button>
<pl-toggle-button label="0-9" active="{{ numbers }}" class="tap" reverse></pl-toggle-button>
<pl-toggle-button label="?()/%..." active="{{ other }}" class="tap" reverse></pl-toggle-button>
<pl-slider label="[[ $l('length') ]]" min="5" max="50" value="{{ length }}"></pl-slider>
<button class="confirm-button tap" on-click="_confirm">[[ $l("Apply") ]]</button>
<pl-toggle-button label="a-z" active="{{ lower }}" class="tap" reverse=""></pl-toggle-button>
<pl-toggle-button label="A-Z" active="{{ upper }}" class="tap" reverse=""></pl-toggle-button>
<pl-toggle-button label="0-9" active="{{ numbers }}" class="tap" reverse=""></pl-toggle-button>
<pl-toggle-button label="?()/%..." active="{{ other }}" class="tap" reverse=""></pl-toggle-button>
<pl-slider label="[[ \$l('length') ]]" min="5" max="50" value="{{ length }}"></pl-slider>
<button class="confirm-button tap" on-click="_confirm">[[ \$l("Apply") ]]</button>
<pl-icon icon="cancel" class="close-button tap" on-click="_dismiss"></pl-icon>
</pl-dialog>
`;
}
</template>
static get is() { return "pl-generator"; }
<script>
(function() {
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
}
}; }
const { BaseElement, LocaleMixin } = padlock;
static get observers() { return [
"_generate(length, lower, upper, numbers, other)"
]; }
class Generator extends LocaleMixin(BaseElement) {
generate() {
this._generate();
this.$.dialog.open = true;
return new Promise((resolve) => {
this._resolve = resolve;
});
}
static get is() { return "pl-generator"; }
_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);
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
}
}; }
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
}
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() {
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);
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
}
_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;
}
_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;
}
}
window.customElements.define(Generator.is, Generator);
})();
</script>
</dom-module>

View File

@ -1,19 +1,9 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<style>
@font-face {
font-family: 'FontAwesome';
src: url('../../../assets/fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../../../assets/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../../../assets/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../../../assets/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../../../assets/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../../../assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
</style>
<dom-module id="pl-icon">
<template>
import '../../styles/shared.js';
import '../base/base.js';
class PlIcon extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: inline-block;
@ -38,216 +28,203 @@
}
:host([icon="add"]) > div::before {
content: "\f067";
content: "\\f067";
}
:host([icon="menu"]) > div::before {
content: "\f0c9";
content: "\\f0c9";
}
:host([icon="close"]) > div::before {
content: "\f00d";
content: "\\f00d";
}
:host([icon="more"]) > div::before {
content: "\f142";
content: "\\f142";
}
:host([icon="delete"]) > div::before {
content: "\f014";
content: "\\f014";
}
:host([icon="copy"]) > div::before {
/* content: "\f24d"; */
content: "\f0c5";
/* content: "\\f24d"; */
content: "\\f0c5";
}
:host([icon="edit"]) > div::before {
content: "\f040";
content: "\\f040";
}
:host([icon="forward"]) > div::before {
content: "\f054";
content: "\\f054";
}
:host([icon="backward"]) > div::before {
content: "\f053";
content: "\\f053";
}
:host([icon="check"]) > div::before {
content: "\f00c";
content: "\\f00c";
}
:host([icon="cancel"]) > div::before {
content: "\f00d";
content: "\\f00d";
}
:host([icon="generate"]) > div::before {
/* content: "\f0d0"; */
content: "\f0e7";
/* content: "\\f0d0"; */
content: "\\f0e7";
}
:host([icon="tag"]) > div::before {
content: "\f02b";
content: "\\f02b";
}
:host([icon="dropdown"]) > div::before {
content: "\f0d7";
content: "\\f0d7";
}
:host([icon="dropup"]) > div::before {
content: "\f0d8";
content: "\\f0d8";
}
:host([icon="settings"]) > div::before {
content: "\f013";
content: "\\f013";
font-size: 110%;
}
:host([icon="cloud"]) > div::before {
content: "\f0c2";
content: "\\f0c2";
}
:host([icon="lock"]) > div::before {
content: "\f023";
content: "\\f023";
font-size: 120%;
}
:host([icon="refresh"]) > div::before {
content: "\f021";
content: "\\f021";
}
:host([icon="unlock"]) > div::before {
content: "\f13e";
content: "\\f13e";
}
:host([icon="export"]) > div::before {
content: "\f093";
content: "\\f093";
}
:host([icon="import"]) > div::before {
content: "\f019";
content: "\\f019";
}
:host([icon="search"]) > div::before {
content: "\f002";
content: "\\f002";
}
:host([icon="info"]) > div::before {
content: "\f129";
content: "\\f129";
}
:host([icon="info-round"]) > div::before {
content: "\f05a";
content: "\\f05a";
}
:host([icon="download"]) > div::before {
content: "\f019";
content: "\\f019";
}
:host([icon="upload"]) > div::before {
content: "\f093";
content: "\\f093";
}
:host([icon="show"]) > div::before {
content: "\f06e";
content: "\\f06e";
}
:host([icon="hide"]) > div::before {
content: "\f070";
content: "\\f070";
}
:host([icon="checked"]) > div::before {
content: "\f046";
content: "\\f046";
}
:host([icon="success"]) > div::before {
content: "\f058";
content: "\\f058";
}
:host([icon="unchecked"]) > div::before {
content: "\f096";
content: "\\f096";
}
:host([icon="share"]) > div::before {
content: "\f045";
content: "\\f045";
}
:host([icon="logout"]) > div::before {
content: "\f08b";
content: "\\f08b";
}
:host([icon="mail"]) > div::before {
content: "\f0e0";
content: "\\f0e0";
}
:host([icon="user"]) > div::before {
content: "\f2bd";
content: "\\f2bd";
}
:host([icon="record"]) > div::before {
content: "\f15b";
content: "\\f15b";
font-size: 90%;
}
:host([icon="mobile"]) > div::before {
content: "\f10b";
content: "\\f10b";
font-size: 140%;
}
:host([icon="database"]) > div::before {
content: "\f1c0";
content: "\\f1c0";
}
:host([icon="time"]) > div::before {
content: "\f017";
content: "\\f017";
}
:host([icon="error"]) > div::before {
content: "\f071";
content: "\\f071";
}
:host([icon="question"]) > div::before {
content: "\f059";
content: "\\f059";
}
:host([icon="desktop"]) > div::before {
content: "\f109";
content: "\\f109";
font-size: 140%;
}
:host([icon="logo"]) > div::before {
font-family: "Padlock";
content: "\0041";
content: "\\0041";
font-size: 110%;
}
</style>
<div></div>
`;
}
</template>
<script>
/* global Polymer */
(() => {
class PlIcon extends padlock.BaseElement {
static get is() { return "pl-icon"; }
static get properties() { return {
icon: {
type: String,
reflectToAttribute: true
}
}; }
static get is() { return "pl-icon"; }
static get properties() { return {
icon: {
type: String,
reflectToAttribute: true
}
}; }
}
window.customElements.define(PlIcon.is, PlIcon);
})();
</script>
</dom-module>

View File

@ -1,275 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<dom-module id="pl-input">
<template>
<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>
</template>
<script src="../../../bower_components/autosize/dist/autosize.js"></script>
<script>
/* global autosize */
(() => {
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 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;
})();
</script>
</dom-module>

254
app/src/ui/input/input.js Normal file
View File

@ -0,0 +1,254 @@
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;

View File

@ -1,481 +0,0 @@
<link rel="import" href="../../../bower_components/iron-list/iron-list.html">
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../export/export-dialog.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../record-item/record-item.html">
<link rel="import" href="../sync/sync.html">
<dom-module id="pl-list-view">
<template>
<style include="shared">
:host {
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
background: var(--color-quaternary);
}
.filter-input {
flex: 1;
padding-left: 15px;
}
.filter-input:not([active]) {
text-align: center;
}
header {
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
color: var(--color-foreground);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
border-bottom: none;
background: linear-gradient(90deg, #59c6ff 0%, #077cb9 100%);
}
header pl-icon[icon=logo] {
font-size: 140%;
}
.empty {
@apply --fullbleed;
display: flex;
flex-direction: column;
@apply --fullbleed;
top: var(--row-height);
z-index: 1;
overflow: visible;
}
.empty-message {
padding: 15px 20px;
text-align: center;
position: relative;
background: var(--color-background);
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
}
.empty-message::before {
content: "";
display: block;
width: 15px;
height: 15px;
position: absolute;
top: -7px;
right: 18px;
margin: 0 auto;
transform: rotate(45deg);
background: var(--color-background);
pointer-events: none;
}
.cloud-icon-wrapper {
position: relative;
}
header pl-icon.syncing-icon {
position: absolute;
font-size: 55%;
top: 1px;
left: 0px;
color: var(--color-highlight);
text-shadow: none;
animation: spin 1s infinite;
transform-origin: center 49%;
}
.current-section {
height: 35px;
line-height: 35px;
padding: 0 15px;
width: 100%;
box-sizing: border-box;
font-size: var(--font-size-tiny);
font-weight: bold;
cursor: pointer;
background: var(--color-foreground);
color: var(--color-background);
}
.current-section pl-icon {
float: right;
height: 35px;
width: 10px;
}
.section-separator {
height: 6px;
}
.section-header {
display: flex;
height: 35px;
line-height: 35px;
padding: 0 15px;
font-size: var(--font-size-tiny);
font-weight: bold;
box-sizing: border-box;
}
iron-list {
margin-top: -35px;
}
#sectionSelector {
--row-height: 40px;
--font-size-default: var(--font-size-small);
--pl-dialog-inner: {
background: var(--color-secondary);
};
}
.multi-select {
background: var(--color-background);
height: var(--row-height);
border-top: solid 1px rgba(0, 0, 0, 0.2);
display: flex;
}
.multi-select > pl-icon {
width: var(--row-height);
height: var(--row-height);
}
.multi-select-count {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-small);
font-weight: bold;
overflow: hidden;
text-align: center;
}
</style>
<header hidden$="[[ multiSelect ]]">
<pl-icon icon="menu" class="tap" on-click="_toggleMenu" hidden$="[[ filterActive ]]"></pl-icon>
<pl-input id="filterInput" class="filter-input tap" placeholder="[[ $l('Type To Search') ]]" active$="[[ filterActive ]]"
value="{{ filterString }}" on-escape="clearFilter" no-tab></pl-input>
<pl-icon icon="add" class="tap" on-click="_newRecord" hidden$="[[ filterActive ]]"></pl-icon>
<pl-icon icon="cancel" class="tap" on-click="clearFilter" hidden$="[[ !filterActive ]]"></pl-icon>
</header>
<header hidden$="[[ !multiSelect ]]">
<pl-icon icon="cancel" class="tap" on-click="_clearMultiSelection"></pl-icon>
<pl-icon icon="checked" class="tap" on-click="_selectAll"></pl-icon>
<div class="multi-select-count"><div>[[ _multiSelectLabel(selectedRecords.length) ]]</div></div>
<pl-icon icon="delete" class="tap" on-click="_deleteSelected"></pl-icon>
<pl-icon icon="share" class="tap" on-click="_shareSelected"></pl-icon>
</header>
<div class="current-section tap" on-click="_selectSection" hidden$="[[ _isEmpty(records.length) ]]">
<pl-icon icon="dropdown" class="float-right"></pl-icon>
<div>[[ _currentSection ]]</div>
</div>
<main id="main">
<iron-list id="list"
mutable-data
scroll-target="main"
multi-selection="[[ multiSelect ]]"
hidden$="[[ _isEmpty(records.length) ]]"
items="[[ records ]]"
selection-enabled
selected-item="{{ selectedRecord }}"
selected-items="{{ selectedRecords }}">
<template>
<div>
<div class="section-header" hidden$="[[ !_firstInSection(records, index) ]]">
<div>[[ _sectionHeader(index, records) ]]</div>
<div class="spacer"></div>
<div>[[ _sectionHeader(index, records) ]]</div>
</div>
<pl-record-item record="[[ item ]]" selected$="[[ selected ]]" tabindex$="[[ tabIndex ]]"
multi-select="[[ multiSelect ]]" on-multi-select="_recordMultiSelect"></pl-record-item>
<div class="section-separator" hidden$="[[ !_lastInSection(records, index) ]]"></div>
</div>
</template>
</iron-list>
</main>
<div hidden$="[[ !_isEmpty(records.length) ]]" class="empty">
<div class="empty-message">
[[ $l("You don't have any data yet! Start by creating your first record!") ]]
</div>
<div class="spacer tiles-2"></div>
</div>
<pl-dialog-options id="sectionSelector" on-dialog-open="_stopPropagation"
on-dialog-close="_stopPropagation"></pl-dialog-options>
<div class="rounded-corners"></div>
</template>
<script>
(() => {
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
const { applyMixins } = padlock.util;
class ListView extends applyMixins(
BaseElement,
LocaleMixin,
SyncMixin,
DataMixin,
DialogMixin,
AnimationMixin
) {
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 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());
}
dataUnloaded() {
this._clearFilter();
}
select(record) {
this.$.list.selectItem(record);
}
deselect() {
this.$.list.clearSelection();
}
recordCreated(record) {
this.select(record);
}
_isEmpty() {
return !this.collection.records.filter((r) => !r.removed).length;
}
_openMenu() {
this.dispatchEvent(new CustomEvent("open-menu"));
}
_newRecord() {
this.createRecord();
}
_filterActive() {
return this.filterString !== "";
}
_clearFilter() {
this.set("filterString", "");
}
_toggleMenu() {
this.dispatchEvent(new CustomEvent("toggle-menu"));
}
_openSettings() {
this.dispatchEvent(new CustomEvent("open-settings"));
}
_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));
}
}
_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);
}
_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");
}
_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);
});
}
}
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.animateCascade(animated);
this.$.list.style.opacity = 1;
}, delay);
}
_stopPropagation(e) {
e.stopPropagation();
}
_selectedCountChanged() {
const count = this.selectedRecords && this.selectedRecords.length;
if (this._lastSelectCount && !count) {
this.multiSelect = false;
}
this._lastSelectCount = count;
}
_recordMultiSelect() {
this.multiSelect = true;
}
_clearMultiSelection() {
this.$.list.clearSelection();
this.multiSelect = false;
}
_selectAll() {
this.records.forEach((r) => this.$.list.selectItem(r));
}
_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;
}
});
}
_multiSelectLabel(count) {
return count ? $l("{0} records selected", count) : $l("tap to select");
}
search() {
this.$.filterInput.focus();
}
clearFilter() {
this.$.filterInput.value = "";
}
}
window.customElements.define(ListView.is, ListView);
})();
</script>
</dom-module>

View File

@ -0,0 +1,459 @@
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';
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
const { applyMixins } = padlock.util;
class ListView extends applyMixins(
BaseElement,
LocaleMixin,
SyncMixin,
DataMixin,
DialogMixin,
AnimationMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
background: var(--color-quaternary);
}
.filter-input {
flex: 1;
padding-left: 15px;
}
.filter-input:not([active]) {
text-align: center;
}
header {
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
color: var(--color-foreground);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
border-bottom: none;
background: linear-gradient(90deg, #59c6ff 0%, #077cb9 100%);
}
header pl-icon[icon=logo] {
font-size: 140%;
}
.empty {
@apply --fullbleed;
display: flex;
flex-direction: column;
@apply --fullbleed;
top: var(--row-height);
z-index: 1;
overflow: visible;
}
.empty-message {
padding: 15px 20px;
text-align: center;
position: relative;
background: var(--color-background);
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
}
.empty-message::before {
content: "";
display: block;
width: 15px;
height: 15px;
position: absolute;
top: -7px;
right: 18px;
margin: 0 auto;
transform: rotate(45deg);
background: var(--color-background);
pointer-events: none;
}
.cloud-icon-wrapper {
position: relative;
}
header pl-icon.syncing-icon {
position: absolute;
font-size: 55%;
top: 1px;
left: 0px;
color: var(--color-highlight);
text-shadow: none;
animation: spin 1s infinite;
transform-origin: center 49%;
}
.current-section {
height: 35px;
line-height: 35px;
padding: 0 15px;
width: 100%;
box-sizing: border-box;
font-size: var(--font-size-tiny);
font-weight: bold;
cursor: pointer;
background: var(--color-foreground);
color: var(--color-background);
}
.current-section pl-icon {
float: right;
height: 35px;
width: 10px;
}
.section-separator {
height: 6px;
}
.section-header {
display: flex;
height: 35px;
line-height: 35px;
padding: 0 15px;
font-size: var(--font-size-tiny);
font-weight: bold;
box-sizing: border-box;
}
iron-list {
margin-top: -35px;
}
#sectionSelector {
--row-height: 40px;
--font-size-default: var(--font-size-small);
--pl-dialog-inner: {
background: var(--color-secondary);
};
}
.multi-select {
background: var(--color-background);
height: var(--row-height);
border-top: solid 1px rgba(0, 0, 0, 0.2);
display: flex;
}
.multi-select > pl-icon {
width: var(--row-height);
height: var(--row-height);
}
.multi-select-count {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-small);
font-weight: bold;
overflow: hidden;
text-align: center;
}
</style>
<header hidden\$="[[ multiSelect ]]">
<pl-icon icon="menu" class="tap" on-click="_toggleMenu" hidden\$="[[ filterActive ]]"></pl-icon>
<pl-input id="filterInput" class="filter-input tap" placeholder="[[ \$l('Type To Search') ]]" active\$="[[ filterActive ]]" value="{{ filterString }}" on-escape="clearFilter" no-tab=""></pl-input>
<pl-icon icon="add" class="tap" on-click="_newRecord" hidden\$="[[ filterActive ]]"></pl-icon>
<pl-icon icon="cancel" class="tap" on-click="clearFilter" hidden\$="[[ !filterActive ]]"></pl-icon>
</header>
<header hidden\$="[[ !multiSelect ]]">
<pl-icon icon="cancel" class="tap" on-click="_clearMultiSelection"></pl-icon>
<pl-icon icon="checked" class="tap" on-click="_selectAll"></pl-icon>
<div class="multi-select-count"><div>[[ _multiSelectLabel(selectedRecords.length) ]]</div></div>
<pl-icon icon="delete" class="tap" on-click="_deleteSelected"></pl-icon>
<pl-icon icon="share" class="tap" on-click="_shareSelected"></pl-icon>
</header>
<div class="current-section tap" on-click="_selectSection" hidden\$="[[ _isEmpty(records.length) ]]">
<pl-icon icon="dropdown" class="float-right"></pl-icon>
<div>[[ _currentSection ]]</div>
</div>
<main id="main">
<iron-list id="list" mutable-data="" scroll-target="main" multi-selection="[[ multiSelect ]]" hidden\$="[[ _isEmpty(records.length) ]]" items="[[ records ]]" selection-enabled="" selected-item="{{ selectedRecord }}" selected-items="{{ selectedRecords }}">
<template>
<div>
<div class="section-header" hidden\$="[[ !_firstInSection(records, index) ]]">
<div>[[ _sectionHeader(index, records) ]]</div>
<div class="spacer"></div>
<div>[[ _sectionHeader(index, records) ]]</div>
</div>
<pl-record-item record="[[ item ]]" selected\$="[[ selected ]]" tabindex\$="[[ tabIndex ]]" multi-select="[[ multiSelect ]]" on-multi-select="_recordMultiSelect"></pl-record-item>
<div class="section-separator" hidden\$="[[ !_lastInSection(records, index) ]]"></div>
</div>
</template>
</iron-list>
</main>
<div hidden\$="[[ !_isEmpty(records.length) ]]" class="empty">
<div class="empty-message">
[[ \$l("You don't have any data yet! Start by creating your first record!") ]]
</div>
<div class="spacer tiles-2"></div>
</div>
<pl-dialog-options id="sectionSelector" on-dialog-open="_stopPropagation" on-dialog-close="_stopPropagation"></pl-dialog-options>
<div class="rounded-corners"></div>
`;
}
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 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());
}
dataUnloaded() {
this._clearFilter();
}
select(record) {
this.$.list.selectItem(record);
}
deselect() {
this.$.list.clearSelection();
}
recordCreated(record) {
this.select(record);
}
_isEmpty() {
return !this.collection.records.filter((r) => !r.removed).length;
}
_openMenu() {
this.dispatchEvent(new CustomEvent("open-menu"));
}
_newRecord() {
this.createRecord();
}
_filterActive() {
return this.filterString !== "";
}
_clearFilter() {
this.set("filterString", "");
}
_toggleMenu() {
this.dispatchEvent(new CustomEvent("toggle-menu"));
}
_openSettings() {
this.dispatchEvent(new CustomEvent("open-settings"));
}
_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));
}
}
_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);
}
_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");
}
_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);
});
}
}
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.animateCascade(animated);
this.$.list.style.opacity = 1;
}, delay);
}
_stopPropagation(e) {
e.stopPropagation();
}
_selectedCountChanged() {
const count = this.selectedRecords && this.selectedRecords.length;
if (this._lastSelectCount && !count) {
this.multiSelect = false;
}
this._lastSelectCount = count;
}
_recordMultiSelect() {
this.multiSelect = true;
}
_clearMultiSelection() {
this.$.list.clearSelection();
this.multiSelect = false;
}
_selectAll() {
this.records.forEach((r) => this.$.list.selectItem(r));
}
_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;
}
});
}
_multiSelectLabel(count) {
return count ? $l("{0} records selected", count) : $l("tap to select");
}
search() {
this.$.filterInput.focus();
}
clearFilter() {
this.$.filterInput.value = "";
}
}
window.customElements.define(ListView.is, ListView);

View File

@ -1,126 +0,0 @@
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../icon/icon.html">
<dom-module id="pl-loading-button">
<template>
<style include="shared">
:host {
display: flex;
}
button {
position: relative;
flex: 1;
}
button > * {
@apply --absolute-center;
transition: transform 0.2s cubic-bezier(1, -0.3, 0, 1.3), opacity 0.2s;
}
button > .label {
@appy --fullbleed;
display: flex;
align-items: center;
justify-content: center;
}
button.loading .label, button.success .label, button.fail .label,
button:not(.loading) .spinner,
button:not(.success) .icon-success,
button:not(.fail) .icon-fail {
opacity: 0.5;
transform: scale(0);
}
button pl-icon {
font-size: 120%;
}
paper-spinner-lite {
line-height: normal;
--paper-spinner-color: currentColor;
--paper-spinner-stroke-width: 2px;
}
</style>
<button type="button" class$="[[ _buttonClass(_loading, _success, _fail) ]]"
tabindex$="[[ _tabIndex(noTab) ]]">
<div class="label"><slot></slot></div>
<paper-spinner-lite active="[[ _loading ]]" class="spinner"></paper-spinner-lite>
<pl-icon icon="check" class="icon-success"></pl-icon>
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
</button>
</template>
<script>
(() => {
class LoadingButton extends padlock.BaseElement {
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
}
}; }
start() {
clearTimeout(this._stopTimeout);
this._success = this._fail = false;
this._loading = true;
}
stop() {
this._success = this._fail = this._loading = false;
}
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);
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_buttonClass() {
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
}
}
window.customElements.define(LoadingButton.is, LoadingButton);
})();
</script>
</dom-module>

View File

@ -0,0 +1,114 @@
import '../../../../../node_modules/@polymer/paper-spinner/paper-spinner-lite.js';
import '../base/base.js';
import '../icon/icon.js';
class LoadingButton extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: flex;
}
button {
position: relative;
flex: 1;
}
button > * {
@apply --absolute-center;
transition: transform 0.2s cubic-bezier(1, -0.3, 0, 1.3), opacity 0.2s;
}
button > .label {
@appy --fullbleed;
display: flex;
align-items: center;
justify-content: center;
}
button.loading .label, button.success .label, button.fail .label,
button:not(.loading) .spinner,
button:not(.success) .icon-success,
button:not(.fail) .icon-fail {
opacity: 0.5;
transform: scale(0);
}
button pl-icon {
font-size: 120%;
}
paper-spinner-lite {
line-height: normal;
--paper-spinner-color: currentColor;
--paper-spinner-stroke-width: 2px;
}
</style>
<button type="button" class\$="[[ _buttonClass(_loading, _success, _fail) ]]" tabindex\$="[[ _tabIndex(noTab) ]]">
<div class="label"><slot></slot></div>
<paper-spinner-lite active="[[ _loading ]]" class="spinner"></paper-spinner-lite>
<pl-icon icon="check" class="icon-success"></pl-icon>
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
</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
}
}; }
start() {
clearTimeout(this._stopTimeout);
this._success = this._fail = false;
this._loading = true;
}
stop() {
this._success = this._fail = this._loading = false;
}
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);
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_buttonClass() {
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
}
}
window.customElements.define(LoadingButton.is, LoadingButton);

View File

@ -1,46 +0,0 @@
<link rel="import" href="../base/base.html">
<script>
(() => {
const { resolveLanguage } = padlock.util;
const { getLocale } = padlock.platform;
let translations, language;
function localize(msg, ...fmtArgs) {
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;
}
localize.loadTranslations = (t) => {
translations = t;
language = resolveLanguage(getLocale(), translations);
};
padlock.LocaleMixin = (superClass) => {
return class LocaleMixin extends superClass {
$l() {
return localize.apply(null, arguments);
}
};
};
window.$l = localize;
})();
</script>
<script src="translations.js" type="text/javascript" charset="utf-8"></script>

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,9 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<dom-module id="pl-notification">
<template>
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;
@ -46,59 +45,54 @@
<div class="background"></div>
<div class="text" on-click="_click">{{ message }}</div>
`;
}
</template>
static get is() { return "pl-notification"; }
<script>
(() => {
static get properties() { return {
message: String,
type: {
type: String,
value: "info",
observer: "_typeChanged"
}
}; }
class Notification extends padlock.BaseElement {
show(message, type, duration) {
if (message) {
this.message = message;
}
static get is() { return "pl-notification"; }
if (type) {
this.type = type;
}
static get properties() { return {
message: String,
type: {
type: String,
value: "info",
observer: "_typeChanged"
}
}; }
this.classList.add("showing");
show(message, type, duration) {
if (message) {
this.message = message;
}
if (duration) {
setTimeout(() => this.hide(false), duration);
}
if (type) {
this.type = type;
}
return new Promise((resolve) => {
this._resolve = resolve;
});
}
this.classList.add("showing");
hide(clicked) {
this.classList.remove("showing");
typeof this._resolve === "function" && this._resolve(clicked);
this._resolve = null;
}
if (duration) {
setTimeout(() => this.hide(false), duration);
}
_typeChanged(newType, oldType) {
this.classList.remove(oldType);
this.classList.add(newType);
}
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);
}
_click() {
this.hide(true);
}
}
window.customElements.define(Notification.is, Notification);
@ -120,9 +114,3 @@ padlock.NotificationMixin = (baseClass) => {
};
};
})();
</script>
</dom-module>

View File

@ -1,15 +1,31 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../loading-button/loading-button.html">
<link rel="import" href="../locale/locale.html">
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';
<dom-module id="pl-payment-dialog">
const { LocaleMixin, BaseElement } = padlock;
const { applyMixins, formatDateUntil } = padlock.util;
const { track } = padlock.tracking;
<template>
let stripe;
const stripeLoaded = new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://js.stripe.com/v3/";
script.async = true;
script.onload = resolve;
document.body.appendChild(script);
});
class PaymentDialog extends applyMixins(
BaseElement,
LocaleMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-max-width: 500px;
@ -177,7 +193,7 @@
.secure-payment::before {
font-family: "FontAwesome";
content: "\f023\ ";
content: "\\f023\\ ";
vertical-align: middle;
position: relative;
top: 1px;
@ -238,44 +254,43 @@
<pl-dialog id="cardDialog" open="{{ open }}" on-dialog-dismiss="_dismiss">
<form id="paymentForm" method="post" on-submit="_submitCard">
<div class="update-title" hidden$="[[ _hasPlan(plan) ]]">[[ $l("Update Payment Method") ]]</div>
<div class="plan" hidden$="[[ !_hasPlan(plan) ]]">
<div class="plan-name">[[ $l("Upgrade to {0} now!", plan.name) ]]</div>
<div class="update-title" hidden\$="[[ _hasPlan(plan) ]]">[[ \$l("Update Payment Method") ]]</div>
<div class="plan" hidden\$="[[ !_hasPlan(plan) ]]">
<div class="plan-name">[[ \$l("Upgrade to {0} now!", plan.name) ]]</div>
<div class="features">
<div class="feature sync">
<pl-icon icon="mobile"></pl-icon>
<pl-icon icon="desktop"></pl-icon>
<div class="feature-label">[[ $l("Seamless Synchronization") ]]</div>
<div class="feature-label">[[ \$l("Seamless Synchronization") ]]</div>
</div>
<div class="feature backup">
<pl-icon icon="database"></pl-icon>
<div>[[ $l("Automatic Backups") ]]</div>
<div>[[ \$l("Automatic Backups") ]]</div>
</div>
<div class="feature encryption">
<pl-icon icon="lock"></pl-icon>
<div>[[ $l("End-To-End Encryption") ]]</div>
<div>[[ \$l("End-To-End Encryption") ]]</div>
</div>
</div>
<div class="price">
<div class="price-original" hidden$="[[ !truthy(promo) ]]">$[[ _originalPrice.0 ]][[ _originalPrice.1 ]]</div>
<div class="price-original" hidden\$="[[ !truthy(promo) ]]">\$[[ _originalPrice.0 ]][[ _originalPrice.1 ]]</div>
<div class="price-amount">
<span class="price-currency">$</span>[[ _price.0 ]]<span class="price-decimal">[[ _price.1 ]]</span>
<span class="price-currency">\$</span>[[ _price.0 ]]<span class="price-decimal">[[ _price.1 ]]</span>
</div>
<div class="price-period">[[ $l("per month*") ]]</div>
<div class="price-period">[[ \$l("per month*") ]]</div>
</div>
<div class="price-hint">[[ $l("*USD\, billed annually") ]]</div>
<div class="price-hint">[[ \$l("*USD\\, billed annually") ]]</div>
</div>
<div class="promo-desc" hidden$="[[ !truthy(promo) ]]">
<div class="promo-desc" hidden\$="[[ !truthy(promo) ]]">
<div>[[ promo.title ]]</div>
<div class="redeem-within" hidden$="[[ !truthy(promo.redeemWithin) ]]">[[ $l("Expires In") ]] [[ _redeemCountDown ]]</div>
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
</div>
<div class="message card-hint tap" hidden$="[[ _hasError(_cardError) ]]" on-click="_toggleCoupon">
<div>[[ $l("Please enter your credit or debit card information:") ]]</div>
<div class="message card-hint tap" hidden\$="[[ _hasError(_cardError) ]]" on-click="_toggleCoupon">
<div>[[ \$l("Please enter your credit or debit card information:") ]]</div>
</div>
<div class="message card-hint tap" hidden$="[[ !_hasError(_cardError) ]]" on-click="_openSupport">
<div class="message card-hint tap" hidden\$="[[ !_hasError(_cardError) ]]" on-click="_openSupport">
<div class="error">[[ _cardError ]]</div>
<div class="support-link"
hidden$="[[ !_needsSupport ]]">[[ $l("Contact Support") ]]</div>
<div class="support-link" hidden\$="[[ !_needsSupport ]]">[[ \$l("Contact Support") ]]</div>
</div>
<div class="stripe-element-wrapper">
<slot>
@ -284,217 +299,188 @@
<pl-loading-button class="tap" id="submitButton" on-click="_submitCard">[[ _submitLabel(plan) ]]</pl-loading-button>
</form>
<div slot="after" class="secure-payment">
<span>[[ $l("Secure Payment - ") ]]</span>
<span>[[ \$l("Secure Payment - ") ]]</span>
<img src="assets/img/powered_by_stripe.svg">
</div>
<button class="trial-button" slot="after" hidden$="[[ _trialExpired(remainingTrialDays) ]]">
[[ $l("Continue Trial ({0} Days Left)", remainingTrialDays) ]]
<button class="trial-button" slot="after" hidden\$="[[ _trialExpired(remainingTrialDays) ]]">
[[ \$l("Continue Trial ({0} Days Left)", remainingTrialDays) ]]
</button>
</pl-dialog>
`;
}
</template>
static get is() { return "pl-payment-dialog"; }
<script>
/* global Stripe */
(() => {
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)"
}
}; }
const { LocaleMixin, BaseElement } = padlock;
const { applyMixins, formatDateUntil } = padlock.util;
const { track } = padlock.tracking;
static get observers() { return [
"_setupCountdown(promo.redeemWithin)"
]; }
let stripe;
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
}, 1000);
}
}
const stripeLoaded = new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://js.stripe.com/v3/";
script.async = true;
script.onload = resolve;
document.body.appendChild(script);
});
show(source) {
this._needsSupport = false;
this._cardError = "";
this.open = true;
this._source = source;
class PaymentDialog extends applyMixins(
BaseElement,
LocaleMixin
) {
if (!this._cardElement) {
this._setupPayment();
}
static get is() { return "pl-payment-dialog"; }
track("Payment Dialog: Open", {
"Plan": this.plan && this.plan.id,
"Source": this._source
});
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)"
}
}; }
return new Promise((resolve) => {
this._resolve = resolve;
});
}
static get observers() { return [
"_setupCountdown(promo.redeemWithin)"
]; }
_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"
}
}
});
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
}, 1000);
}
}
const cardElement = document.createElement("div");
this.appendChild(cardElement);
card.mount(cardElement);
show(source) {
this._needsSupport = false;
this._cardError = "";
this.open = true;
this._source = source;
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
});
}
if (!this._cardElement) {
this._setupPayment();
}
_submitCard() {
if (this._submittingCard) {
return;
}
track("Payment Dialog: Open", {
"Plan": this.plan && this.plan.id,
"Source": this._source
});
this.$.submitButton.start();
this._submittingCard = true;
return new Promise((resolve) => {
this._resolve = resolve;
});
}
const coupon = this.promo && this.promo.coupon.id || "";
_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"
}
}
});
stripe.createToken(this._cardElement).then((result) => {
const edata = {
"Plan": this.plan && this.plan.id,
"Source": this._source,
"Success": true,
"Token Created": true,
"Coupon": coupon
};
const cardElement = document.createElement("div");
this.appendChild(cardElement);
card.mount(cardElement);
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);
});
}
});
}
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
});
}
_hasError() {
return !!this._cardError;
}
_submitCard() {
if (this._submittingCard) {
return;
}
_calcPrice(amount, percentOff = 0, amountOff = 0) {
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
const d = Math.round((amount / 12) % 100);
this.$.submitButton.start();
this._submittingCard = true;
return [
`${Math.floor(amount / 1200)}`,
d < 10 ? `.0${d}` : `.${d}`
];
}
const coupon = this.promo && this.promo.coupon.id || "";
_dismiss() {
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
stripe.createToken(this._cardElement).then((result) => {
const edata = {
"Plan": this.plan && this.plan.id,
"Source": this._source,
"Success": true,
"Token Created": true,
"Coupon": coupon
};
_openSupport() {
window.open("mailto:support@padlock.io", "_system");
}
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);
});
}
});
}
_trialExpired() {
return !this.remainingTrialDays;
}
_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);
return [
`${Math.floor(amount / 1200)}`,
d < 10 ? `.0${d}` : `.${d}`
];
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
_openSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_trialExpired() {
return !this.remainingTrialDays;
}
_hasPlan() {
return !!this.plan;
}
_submitLabel() {
return this.plan ? $l("Upgrade Now") : $l("Submit");
}
_hasPlan() {
return !!this.plan;
}
_submitLabel() {
return this.plan ? $l("Upgrade Now") : $l("Submit");
}
}
window.customElements.define(PaymentDialog.is, PaymentDialog);
})();
</script>
</dom-module>

View File

@ -1,63 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog.html">
<link rel="import" href="./promo.html">
<dom-module id="pl-promo-dialog">
<template>
<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>
</template>
<script>
(function() {
const { BaseElement } = padlock;
class PlExportDialog extends BaseElement {
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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,52 @@
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);

View File

@ -1,12 +1,14 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
import '../../styles/shared.js';
import '../base/base.js';
import '../icon/icon.js';
import '../locale/locale.js';
<dom-module id="pl-promo">
<template>
const { LocaleMixin } = padlock;
const { formatDateUntil, isFuture } = padlock.util;
class Promo extends LocaleMixin(padlock.BaseElement) {
static get template() {
return Polymer.html`
<style include="shared">
button {
@ -49,53 +51,38 @@
</div>
<button class="tap" on-click="_redeem">
<div>[[ $l("Redeem Now") ]]</div>
<div class="redeem-within" hidden$="[[ !truthy(promo.redeemWithin) ]]">[[ $l("Expires In") ]] [[ _redeemCountDown ]]</div>
<div>[[ \$l("Redeem Now") ]]</div>
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
</button>
`;
}
</template>
static get is() { return "pl-promo"; }
<script>
(() => {
static get properties() { return {
promo: Object
}; }
const { LocaleMixin } = padlock;
const { formatDateUntil, isFuture } = padlock.util;
static get observers() { return [
"_setupCountdown(promo.redeemWithin)"
]; }
class Promo extends LocaleMixin(padlock.BaseElement) {
static get is() { return "pl-promo"; }
static get properties() { return {
promo: Object
}; }
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);
}
}
_redeem() {
this.dispatchEvent(new CustomEvent("promo-redeem"));
}
_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"));
}
}
window.customElements.define(Promo.is, Promo);
})();
</script>
</dom-module>

View File

@ -1,13 +1,22 @@
<link rel="import" href="../../../bower_components/polymer/lib/mixins/mutable-data.html">
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../clipboard/clipboard.html">
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';
<dom-module id="pl-record-item">
<template>
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`
<style include="shared">
:host {
display: block;
@ -87,7 +96,7 @@
.copied-message::before {
font-family: "FontAwesome";
content: "\f00c\ ";
content: "\\f00c\\ ";
}
:host(:not(.touch):not([multi-select])) .field:hover {
@ -152,11 +161,11 @@
<div class="highlight"></div>
<div class="header">
<div class="name" hidden$="[[ !truthy(record.name) ]]">[[ record.name ]]</div>
<div class="name" disabled hidden$="[[ truthy(record.name) ]]">[[ $l("No Name") ]]</div>
<div class="name" hidden\$="[[ !truthy(record.name) ]]">[[ record.name ]]</div>
<div class="name" disabled="" hidden\$="[[ truthy(record.name) ]]">[[ \$l("No Name") ]]</div>
<div class="spacer"></div>
<div class="tags">
<template is="dom-repeat" items="[[ _limitTags(record.tags) ]]" mutable-data>
<template is="dom-repeat" items="[[ _limitTags(record.tags) ]]" mutable-data="">
<div class="ellipsis tag">[[ item ]]</div>
</template>
</div>
@ -164,102 +173,81 @@
<div class="fields">
<template is="dom-repeat" items="[[ record.fields ]]" mutable-data>
<template is="dom-repeat" items="[[ record.fields ]]" mutable-data="">
<div class="field" on-click="_copyField">
<div class="field-label">[[ item.name ]]</div>
<div class="copied-message">[[ $l("copied") ]]</div>
<div class="copied-message">[[ \$l("copied") ]]</div>
</div>
</template>
<div class="field" disabled hidden$="[[ _hasFields(record.fields.length) ]]">[[ $l("No Fields") ]]</div>
<div class="field" disabled="" hidden\$="[[ _hasFields(record.fields.length) ]]">[[ \$l("No Fields") ]]</div>
</div>
`;
}
</template>
static get is() { return "pl-record-item"; }
<script>
(() => {
static get properties() { return {
multiSelect: {
type: Boolean,
value: false,
reflectToAttribute: true
},
record: Object
}; }
const { ClipboardMixin, LocaleMixin, DataMixin, BaseElement } = padlock;
const { MutableData } = Polymer;
const { applyMixins } = padlock.util;
const { isTouch } = padlock.platform;
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();
}
});
class RecordItem extends applyMixins(
BaseElement,
DataMixin,
MutableData,
ClipboardMixin,
LocaleMixin
) {
this.classList.toggle("touch", isTouch());
}
static get is() { return "pl-record-item"; }
_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);
}
}
static get properties() { return {
multiSelect: {
type: Boolean,
value: false,
reflectToAttribute: true
},
record: Object
}; }
_fieldLabel(value) {
return value ? value + ":" : "";
}
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();
}
});
_hasFields() {
return !!this.record.fields.length;
}
this.classList.toggle("touch", isTouch());
}
_selectIconClicked() {
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
}
_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);
}
}
_limitTags() {
const tags = this.record.tags.slice(0, 2);
const more = this.record.tags.length - tags.length;
_fieldLabel(value) {
return value ? value + ":" : "";
}
if (more) {
tags.push("+" + more);
}
_hasFields() {
return !!this.record.fields.length;
}
_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;
if (more) {
tags.push("+" + more);
}
return tags;
}
return tags;
}
}
window.customElements.define(RecordItem.is, RecordItem);
})();
</script>
</dom-module>

View File

@ -1,12 +1,14 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import '../icon/icon.js';
import '../locale/locale.js';
<dom-module id="pl-record-field-dialog">
const { BaseElement, LocaleMixin } = padlock;
<template>
class RecordFieldDialog extends LocaleMixin(BaseElement) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-inner: {
@ -97,137 +99,122 @@
<pl-dialog open="{{ open }}" prevent-dismiss="[[ editing ]]" on-dialog-dismiss="_close">
<div class="header">
<pl-input id="nameInput" placeholder="[[ $l('Enter Field Name') ]]" class="name"
readonly="[[ !editing ]]" on-click="_inputClicked" on-enter="_nameInputEnter"></pl-input>
<pl-icon icon="cancel" class="tap" on-click="_close" hidden$="[[ editing ]]"></pl-icon>
<pl-input id="nameInput" placeholder="[[ \$l('Enter Field Name') ]]" class="name" readonly="[[ !editing ]]" on-click="_inputClicked" on-enter="_nameInputEnter"></pl-input>
<pl-icon icon="cancel" class="tap" on-click="_close" hidden\$="[[ editing ]]"></pl-icon>
</div>
<div class="value-wrapper">
<pl-input id="valueInput" multiline class="value" autosize on-click="_inputClicked"
placeholder="[[ $l('Enter Content') ]]" readonly="[[ !editing ]]"></pl-input>
<button class="generate-button tap" on-click="_generate" hidden$="[[ !editing ]]">
<div>[[ $l("Generate") ]]</div>
<pl-input id="valueInput" multiline="" class="value" autosize="" on-click="_inputClicked" placeholder="[[ \$l('Enter Content') ]]" readonly="[[ !editing ]]"></pl-input>
<button class="generate-button tap" on-click="_generate" hidden\$="[[ !editing ]]">
<div>[[ \$l("Generate") ]]</div>
<pl-icon icon="generate"></pl-icon>
</button>
</div>
<div class="actions">
<div class="action-items tiles-3 tiles" hidden$="[[ editing ]]">
<div class="action-items tiles-3 tiles" hidden\$="[[ editing ]]">
<pl-icon icon="copy" class="tap" on-click="_copy"></pl-icon>
<pl-icon icon="edit" class="tap" on-click="_edit"></pl-icon>
<pl-icon icon="generate" class="tap" on-click="_generate"></pl-icon>
<pl-icon icon="delete" class="tap" on-click="_delete"></pl-icon>
</div>
<div class="action-items" hidden$="[[ !editing ]]">
<button class="tap" on-click="_discardChanges">[[ $l("Discard") ]]</button>
<button class="tap" on-click="_saveChanges">[[ $l("Save") ]]</button>
<div class="action-items" hidden\$="[[ !editing ]]">
<button class="tap" on-click="_discardChanges">[[ \$l("Discard") ]]</button>
<button class="tap" on-click="_saveChanges">[[ \$l("Save") ]]</button>
</div>
</div>
</pl-dialog>
`;
}
</template>
static get is() { return "pl-record-field-dialog"; }
<script>
(function() {
static get properties() { return {
editing: {
type: Boolean,
value: false,
reflectToAttribute: true
},
field: {
type: Object,
value: () => { return { name: "", value: "" }; }
},
open: {
type: Boolean,
value: false
}
}; }
const { BaseElement, LocaleMixin } = padlock;
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;
});
}
class RecordFieldDialog extends LocaleMixin(BaseElement) {
_closeWithAction(action) {
this.open = false;
this._resolve && this._resolve({
action: action,
name: this.$.nameInput.value,
value: this.$.valueInput.value
});
this._resolve = null;
}
static get is() { return "pl-record-field-dialog"; }
_close() {
this._closeWithAction();
}
static get properties() { return {
editing: {
type: Boolean,
value: false,
reflectToAttribute: true
},
field: {
type: Object,
value: () => { return { name: "", value: "" }; }
},
open: {
type: Boolean,
value: false
}
}; }
_delete() {
this._closeWithAction("delete");
}
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;
});
}
_copy() {
this._closeWithAction("copy");
}
_closeWithAction(action) {
this.open = false;
this._resolve && this._resolve({
action: action,
name: this.$.nameInput.value,
value: this.$.valueInput.value
});
this._resolve = null;
}
_generate() {
this._closeWithAction("generate");
}
_close() {
this._closeWithAction();
}
_edit() {
this.editing = true;
setTimeout(() => {
if (!this.$.nameInput.value) {
this.$.nameInput.focus();
} else {
this.$.valueInput.focus();
}
}, 100);
}
_delete() {
this._closeWithAction("delete");
}
_discardChanges() {
this.$.nameInput.value = this.field.name;
this.$.valueInput.value = this.field.value;
this._close();
}
_copy() {
this._closeWithAction("copy");
}
_saveChanges() {
this.field.name = this.$.nameInput.value;
this.field.value = this.$.valueInput.value;
this._closeWithAction("edited");
}
_generate() {
this._closeWithAction("generate");
}
_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();
}
_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;
}
}
_nameInputEnter() {
this.$.valueInput.focus();
}
_inputClicked(e) {
if (!e.target.value) {
this.editing = true;
}
}
_nameInputEnter() {
this.$.valueInput.focus();
}
}
window.customElements.define(RecordFieldDialog.is, RecordFieldDialog);
})();
</script>
</dom-module>

View File

@ -1,200 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../clipboard/clipboard.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../notification/notification.html">
<link rel="import" href="record-field-dialog.html">
<dom-module id="pl-record-field">
<template>
<style include="shared">
:host {
display: block;
color: inherit;
font-size: var(--font-size-small);
overflow: hidden;
position: relative;
}
.container {
display: flex;
height: 100px;
position: relative;
flex-direction: column;
padding-right: 50px;
}
.name {
font-size: var(--font-size-tiny);
font-weight: bold;
color: var(--color-highlight);
margin: 12px 12px 6px 12px;
}
#valueInput {
font-family: var(--font-family-mono);
font-size: 130%;
line-height: 1.2;
flex: 1;
margin: 0 12px;
opacity: 1;
}
#valueInput::after {
content: "";
display: block;
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 20px;
background: linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
}
.field-buttons {
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
.field-button {
display: block;
cursor: pointer;
font-size: 110%;
width: 50px;
height: 50px;
}
:host(:not(.touch):not(:hover)) .field-buttons {
visibility: hidden;
}
.field-button:hover {
background: rgba(0, 0, 0, 0.05);
}
</style>
<div class="container tap" on-click="_fieldClicked">
<div class="name">[[ field.name ]]</div>
<pl-input multiline id="valueInput" value="[[ field.value ]]" disabled
placeholder="[[ $l('No Content') ]]" masked="[[ field.masked ]]"></pl-input>
</div>
<div class="field-buttons">
<pl-icon icon="[[ _toggleMaskIcon(field.masked) ]]" class="field-button tap"
on-click="_toggleMask" hidden$="[[ !_hasValue(field.value) ]]"></pl-icon>
<pl-icon icon="copy" class="field-button tap" on-click="_copy" hidden$="[[ !_hasValue(field.value) ]]"></pl-icon>
<pl-icon icon="edit" class="field-button tap" on-click="_edit" hidden$="[[ _hasValue(field.value) ]]"></pl-icon>
<pl-icon icon="generate" class="field-button tap" on-click="_showGenerator" hidden$="[[ _hasValue(field.value) ]]"></pl-icon>
</div>
</template>
<script>
(() => {
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 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 }; }
}
}; }
connectedCallback() {
super.connectedCallback();
this.classList.toggle("touch", isTouch());
}
_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"));
});
}
_copy() {
this.setClipboard(this.record, this.field);
}
_toggleMaskIcon() {
return this.field.masked ? "show" : "hide";
}
_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;
}
});
}
_hasValue() {
return !!this.field.value;
}
_fieldClicked() {
this._openFieldDialog();
}
_edit() {
this._openFieldDialog(true);
}
}
window.customElements.define(RecordField.is, RecordField);
})();
</script>
</dom-module>

View File

@ -0,0 +1,188 @@
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';
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`
<style include="shared">
:host {
display: block;
color: inherit;
font-size: var(--font-size-small);
overflow: hidden;
position: relative;
}
.container {
display: flex;
height: 100px;
position: relative;
flex-direction: column;
padding-right: 50px;
}
.name {
font-size: var(--font-size-tiny);
font-weight: bold;
color: var(--color-highlight);
margin: 12px 12px 6px 12px;
}
#valueInput {
font-family: var(--font-family-mono);
font-size: 130%;
line-height: 1.2;
flex: 1;
margin: 0 12px;
opacity: 1;
}
#valueInput::after {
content: "";
display: block;
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 20px;
background: linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
}
.field-buttons {
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
.field-button {
display: block;
cursor: pointer;
font-size: 110%;
width: 50px;
height: 50px;
}
:host(:not(.touch):not(:hover)) .field-buttons {
visibility: hidden;
}
.field-button:hover {
background: rgba(0, 0, 0, 0.05);
}
</style>
<div class="container tap" on-click="_fieldClicked">
<div class="name">[[ field.name ]]</div>
<pl-input multiline="" id="valueInput" value="[[ field.value ]]" disabled="" placeholder="[[ \$l('No Content') ]]" masked="[[ field.masked ]]"></pl-input>
</div>
<div class="field-buttons">
<pl-icon icon="[[ _toggleMaskIcon(field.masked) ]]" class="field-button tap" on-click="_toggleMask" hidden\$="[[ !_hasValue(field.value) ]]"></pl-icon>
<pl-icon icon="copy" class="field-button tap" on-click="_copy" hidden\$="[[ !_hasValue(field.value) ]]"></pl-icon>
<pl-icon icon="edit" class="field-button tap" on-click="_edit" hidden\$="[[ _hasValue(field.value) ]]"></pl-icon>
<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 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());
}
_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"));
});
}
_copy() {
this.setClipboard(this.record, this.field);
}
_toggleMaskIcon() {
return this.field.masked ? "show" : "hide";
}
_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;
}
});
}
_hasValue() {
return !!this.field.value;
}
_fieldClicked() {
this._openFieldDialog();
}
_edit() {
this._openFieldDialog(true);
}
}
window.customElements.define(RecordField.is, RecordField);

View File

@ -1,303 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="record-field.html">
<link rel="import" href="record-field-dialog.html">
<dom-module id="pl-record-view">
<template>
<style include="shared">
:host {
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
@apply --fullbleed;
transition: background 0.5s;
background: var(--color-quaternary);
}
#background {
@apply --fullbleed;
}
header > pl-input {
flex: 1;
width: 0;
}
.name {
font-weight: bold;
text-align: center;
}
pl-record-field, .add-button-wrapper {
transform: translate3d(0, 0, 0);
margin: 6px;
@apply --card;
}
.add-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.add-button pl-icon {
width: 30px;
position: relative;
top: 1px;
}
.tags {
display: flex;
overflow-x: auto;
margin: 8px 0;
padding: 0 8px;
/* align-items: center; */
-webkit-overflow-scrolling: touch;
}
.tags::after {
content: "";
display: block;
width: 1px;
flex: none;
}
.tag {
background: var(--color-foreground);
color: var(--color-background);
font-weight: bold;
border-radius: var(--border-radius);
margin-right: 6px;
display: flex;
align-items: center;
font-size: var(--font-size-tiny);
white-space: nowrap;
line-height: 0;
padding-left: 12px;
}
.tags pl-icon {
width: 30px;
height: 30px;
}
.tag.add {
padding-left: 0;
padding-right: 12px;
border: dashed 1px;
background: transparent;
color: var(--color-foreground);
}
</style>
<header>
<pl-icon icon="close" class="tap" on-click="close"></pl-icon>
<pl-input id="nameInput" class="name tap" value="{{ record.name }}"
placeholder="[[ $l('Enter Record Name') ]]" select-on-focus autocapitalize
on-change="_notifyChange" on-enter="_nameEnter"
></pl-input>
<pl-icon icon="delete" class="tap" on-click="_deleteRecord"></pl-icon>
</header>
<main id="main">
<div class="tags animate">
<template is="dom-repeat" items="[[ record.tags ]]">
<div class="tag tap" on-click="_removeTag">
<div class="tag-name">[[ item ]]</div>
<pl-icon icon="cancel"></pl-icon>
</div>
</template>
<div class="tag add tap" on-click="_addTag">
<pl-icon icon="tag"></pl-icon>
<div>[[ $l("Add Tag") ]]</div>
</div>
</div>
<div class="fields">
<template is="dom-repeat" items="[[ record.fields ]]" id="fieldList">
<div class="animate">
<pl-record-field field="[[ item ]]" record="[[ record ]]"
on-field-change="_notifyChange"
on-field-delete="_deleteField"></pl-record-field>
</div>
</template>
</div>
<div class="add-button-wrapper animate">
<button class="add-button tap" on-click="_addField">
<pl-icon icon="add"></pl-icon>
<div>[[ $l("Add Field") ]]</div>
</button>
</div>
</main>
<div class="rounded-corners"></div>
</template>
<script>
(() => {
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
class RecordView extends applyMixins(
BaseElement,
DataMixin,
LocaleMixin,
DialogMixin,
AnimationMixin
) {
static get is() { return "pl-record-view"; }
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;
}
_recordChanged() {
setTimeout(() => {
if (this.record && !this.record.name) {
this.$.nameInput.focus();
}
}, 500);
}
_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();
}
});
}
_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;
}
});
}
_fieldButtonClicked() {
this._addField();
}
_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();
}
});
}
_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();
}
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();
}
});
}
_nameEnter() {
this.$.nameInput.blur();
}
animate() {
setTimeout(() => {
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
}, 100);
}
close() {
this.dispatchEvent(new CustomEvent("record-close"));
}
edit() {
this.$.nameInput.focus();
}
}
window.customElements.define(RecordView.is, RecordView);
})();
</script>
</dom-module>

View File

@ -0,0 +1,288 @@
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';
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`
<style include="shared">
:host {
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
@apply --fullbleed;
transition: background 0.5s;
background: var(--color-quaternary);
}
#background {
@apply --fullbleed;
}
header > pl-input {
flex: 1;
width: 0;
}
.name {
font-weight: bold;
text-align: center;
}
pl-record-field, .add-button-wrapper {
transform: translate3d(0, 0, 0);
margin: 6px;
@apply --card;
}
.add-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.add-button pl-icon {
width: 30px;
position: relative;
top: 1px;
}
.tags {
display: flex;
overflow-x: auto;
margin: 8px 0;
padding: 0 8px;
/* align-items: center; */
-webkit-overflow-scrolling: touch;
}
.tags::after {
content: "";
display: block;
width: 1px;
flex: none;
}
.tag {
background: var(--color-foreground);
color: var(--color-background);
font-weight: bold;
border-radius: var(--border-radius);
margin-right: 6px;
display: flex;
align-items: center;
font-size: var(--font-size-tiny);
white-space: nowrap;
line-height: 0;
padding-left: 12px;
}
.tags pl-icon {
width: 30px;
height: 30px;
}
.tag.add {
padding-left: 0;
padding-right: 12px;
border: dashed 1px;
background: transparent;
color: var(--color-foreground);
}
</style>
<header>
<pl-icon icon="close" class="tap" on-click="close"></pl-icon>
<pl-input id="nameInput" class="name tap" value="{{ record.name }}" placeholder="[[ \$l('Enter Record Name') ]]" select-on-focus="" autocapitalize="" on-change="_notifyChange" on-enter="_nameEnter"></pl-input>
<pl-icon icon="delete" class="tap" on-click="_deleteRecord"></pl-icon>
</header>
<main id="main">
<div class="tags animate">
<template is="dom-repeat" items="[[ record.tags ]]">
<div class="tag tap" on-click="_removeTag">
<div class="tag-name">[[ item ]]</div>
<pl-icon icon="cancel"></pl-icon>
</div>
</template>
<div class="tag add tap" on-click="_addTag">
<pl-icon icon="tag"></pl-icon>
<div>[[ \$l("Add Tag") ]]</div>
</div>
</div>
<div class="fields">
<template is="dom-repeat" items="[[ record.fields ]]" id="fieldList">
<div class="animate">
<pl-record-field field="[[ item ]]" record="[[ record ]]" on-field-change="_notifyChange" on-field-delete="_deleteField"></pl-record-field>
</div>
</template>
</div>
<div class="add-button-wrapper animate">
<button class="add-button tap" on-click="_addField">
<pl-icon icon="add"></pl-icon>
<div>[[ \$l("Add Field") ]]</div>
</button>
</div>
</main>
<div class="rounded-corners"></div>
`;
}
static get is() { return "pl-record-view"; }
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;
}
_recordChanged() {
setTimeout(() => {
if (this.record && !this.record.name) {
this.$.nameInput.focus();
}
}, 500);
}
_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();
}
});
}
_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;
}
});
}
_fieldButtonClicked() {
this._addField();
}
_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();
}
});
}
_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();
}
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();
}
});
}
_nameEnter() {
this.$.nameInput.blur();
}
animate() {
setTimeout(() => {
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
}, 100);
}
close() {
this.dispatchEvent(new CustomEvent("record-close"));
}
edit() {
this.$.nameInput.focus();
}
}
window.customElements.define(RecordView.is, RecordView);

View File

@ -1,543 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../export/export.html">
<link rel="import" href="../icon/icon.html">
<link rel="import" href="../locale/locale.html">
<link rel="import" href="../slider/slider.html">
<link rel="import" href="../sync/sync.html">
<link rel="import" href="../toggle/toggle-button.html">
<dom-module id="pl-settings-view">
<template>
<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...") ]]</div>
</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 &#9829; 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>
</template>
<script>
(() => {
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 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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,523 @@
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);

View File

@ -1,9 +1,9 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<dom-module id="pl-slider">
<template>
import '../../styles/shared.js';
import '../base/base.js';
class Slider extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: flex;
@ -78,68 +78,59 @@
<label>{{ label }}</label>
<input type="range" value="{{ _strValue::input }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" on-change="_inputChange">
<span class="value-display">{{ value }}{{ unit }}</span>
`;
}
</template>
<script>
(() => {
static get is() { return "pl-slider"; }
class Slider extends padlock.BaseElement {
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 is() { return "pl-slider"; }
_strValueChanged() {
this.value = parseFloat(this._strValue, 10);
}
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);
}
_valueChanged() {
this._strValue = this.value.toString();
}
_inputChange() {
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
_valueChanged() {
this._strValue = this.value.toString();
}
_inputChange() {
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
}
window.customElements.define(Slider.is, Slider);
})();
</script>
</dom-module>

View File

@ -1,911 +0,0 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../animation/animation.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="../dialog/dialog-mixin.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../input/input.html">
<link rel="import" href="../loading-button/loading-button.html">
<link rel="import" href="../locale/locale.html">
<dom-module id="pl-start-view">
<template>
<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>
</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>
</template>
<script>
/* global zxcvbn */
(() => {
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 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);
})();
</script>
</dom-module>

View File

@ -0,0 +1,891 @@
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);

View File

@ -1,7 +1,4 @@
<link rel="import" href="../base/base.html">
<script>
(() => {
import '../base/base.js';
const { isFuture } = padlock.util;
@ -98,6 +95,3 @@ padlock.SubInfoMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,11 +1,8 @@
<link rel="import" href="../base/base.html">
<link rel="import" href="../data/data.html">
<link rel="import" href="../payment-dialog/payment-dialog.html">
<link rel="import" href="../promo/promo-dialog.html">
<link rel="import" href="./subinfo.html">
<script>
(() => {
import '../base/base.js';
import '../data/data.js';
import '../payment-dialog/payment-dialog.js';
import '../promo/promo-dialog.js';
import './subinfo.js';
const { EncryptedSource, CloudSource } = padlock.source;
const { DataMixin, SubInfoMixin } = padlock;
@ -571,6 +568,3 @@ padlock.SyncMixin = (superClass) => {
};
};
})();
</script>

View File

@ -1,9 +1,9 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<dom-module id="pl-title-bar">
<template>
import '../../styles/shared.js';
import '../base/base.js';
class TitleBar extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
height: var(--title-bar-height);
@ -73,7 +73,7 @@
}
.buttons.macos-linux button.close::after {
content: "\f00d";
content: "\\f00d";
font-size: 8px;
}
@ -82,7 +82,7 @@
}
.buttons.macos-linux button.minimize::after {
content: "\f2d1";
content: "\\f2d1";
font-size: 6px;
}
@ -92,7 +92,7 @@
}
.buttons.macos-linux button.maximize::after {
content: "\f07d";
content: "\\f07d";
font-size: 9px;
}
@ -115,7 +115,7 @@
}
.buttons.windows button.close::after {
content: "\f00d";
content: "\\f00d";
font-size: 16px;
}
@ -124,12 +124,12 @@
}
.buttons.windows button.minimize::after {
content: "\f2d1";
content: "\\f2d1";
font-size: 12px;
}
.buttons.windows button.maximize::after {
content: "\f2d0";
content: "\\f2d0";
font-size: 14px;
}
</style>
@ -147,36 +147,23 @@
<button class="maximize" on-click="_maximize"></button>
<button class="close" on-click="_close"></button>
</div>
`;
}
</template>
static get is() { return "pl-title-bar"; }
<script>
/* global Polymer */
_close() {
require("electron").remote.getCurrentWindow().close();
}
(() => {
class TitleBar extends padlock.BaseElement {
static get is() { return "pl-title-bar"; }
_close() {
require("electron").remote.getCurrentWindow().close();
}
_minimize() {
require("electron").remote.getCurrentWindow().minimize();
}
_maximize() {
var win = require("electron").remote.getCurrentWindow();
win.setFullScreen(!win.isFullScreen());
}
_minimize() {
require("electron").remote.getCurrentWindow().minimize();
}
_maximize() {
var win = require("electron").remote.getCurrentWindow();
win.setFullScreen(!win.isFullScreen());
}
}
window.customElements.define(TitleBar.is, TitleBar);
})();
</script>
</dom-module>

View File

@ -1,10 +1,10 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<link rel="import" href="toggle.html">
<dom-module id="pl-toggle-button">
<template>
import '../../styles/shared.js';
import '../base/base.js';
import './toggle.js';
class ToggleButton extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: inline-block;
@ -45,41 +45,32 @@
<pl-toggle id="toggle" active="{{ active }}"></pl-toggle>
<div>[[ label ]]</div>
</button>
`;
}
</template>
static get is() { return "pl-toggle-button"; }
<script>
(() => {
static get properties() { return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
label: {
type: String,
value: ""
},
reverse: {
type: Boolean,
value: false,
reflectToAttribute: true
}
}; }
class ToggleButton extends padlock.BaseElement {
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
}
}; }
toggle() {
this.$.toggle.toggle();
}
toggle() {
this.$.toggle.toggle();
}
}
window.customElements.define(ToggleButton.is, ToggleButton);
})();
</script>
</dom-module>

View File

@ -1,10 +1,9 @@
<link rel="import" href="../../styles/shared.html">
<link rel="import" href="../base/base.html">
<dom-module id="pl-toggle">
<template>
import '../../styles/shared.js';
import '../base/base.js';
class Toggle extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared"></style>
<style>
:host {
@ -47,47 +46,37 @@
</style>
<div class="knob"></div>
`;
}
</template>
static get is() { return "pl-toggle"; }
<script>
(() => {
static get properties() { return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
notap: Boolean
}; }
class Toggle extends padlock.BaseElement {
static get is() { return "pl-toggle"; }
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this._tap.bind(this));
}
static get properties() { return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
notap: Boolean
}; }
_tap() {
if (!this.notap) {
this.toggle();
}
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this._tap.bind(this));
}
_tap() {
if (!this.notap) {
this.toggle();
}
}
toggle() {
this.active = !this.active;
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
toggle() {
this.active = !this.active;
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
}
window.customElements.define(Toggle.is, Toggle);
})();
</script>
</dom-module>

9798
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -50,12 +50,18 @@
"watchify": "^3.7.0"
},
"dependencies": {
"@polymer/iron-list": "^3.0.0-pre.18",
"@polymer/paper-spinner": "^3.0.0-pre.18",
"@polymer/polymer": "^3.0.0",
"@webcomponents/webcomponentsjs": "^2.0.0",
"autosize": "^4.0.2",
"electron-store": "^1.1.0",
"electron-updater": "^2.21.0",
"fs-extra": "^5.0.0",
"moment": "^2.21.0",
"uuid": "^3.1.0",
"yargs": "^4.8.1"
"yargs": "^4.8.1",
"zxcvbn": "^4.4.2"
},
"scripts": {
"bower-install": "pushd app && bower install && popd",