Track recently used logins & display first in popup (#9)
* Track recently used logins & display first in popup Results are sorted by: - Most recent store first - Within each store, login with highest use count first - Within same usage count, most recent first There is a 60-sec debounce on logins, to avoid excessive incrementing of the counter when a login is used multiple times in rapid succession (e.g. for copying user / pass etc.).
This commit is contained in:
@@ -301,7 +301,7 @@ function hostAction(settings, action, params = {}) {
|
|||||||
*/
|
*/
|
||||||
async function parseFields(settings, login) {
|
async function parseFields(settings, login) {
|
||||||
var response = await hostAction(settings, "fetch", {
|
var response = await hostAction(settings, "fetch", {
|
||||||
store: login.store,
|
store: login.store.name,
|
||||||
file: login.login + ".gpg"
|
file: login.login + ".gpg"
|
||||||
});
|
});
|
||||||
if (response.status != "ok") {
|
if (response.status != "ok") {
|
||||||
|
@@ -18,6 +18,8 @@
|
|||||||
"chrome-extension-async": "^3.0.0",
|
"chrome-extension-async": "^3.0.0",
|
||||||
"fuzzysort": "^1.1.0",
|
"fuzzysort": "^1.1.0",
|
||||||
"mithril": "^1.1.0",
|
"mithril": "^1.1.0",
|
||||||
|
"moment": "^2.22.1",
|
||||||
|
"sha1": "^1.1.1",
|
||||||
"tldjs": "^2.3.0"
|
"tldjs": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
1
src/popup/icon-history.svg
Normal file
1
src/popup/icon-history.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg height="21px" version="1.1" viewBox="0 0 20 21" width="20px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#000000" id="Core" opacity="0.9" transform="translate(-464.000000, -254.000000)"><g id="history" transform="translate(464.000000, 254.500000)"><path d="M10.5,0 C7,0 3.9,1.9 2.3,4.8 L0,2.5 L0,9 L6.5,9 L3.7,6.2 C5,3.7 7.5,2 10.5,2 C14.6,2 18,5.4 18,9.5 C18,13.6 14.6,17 10.5,17 C7.2,17 4.5,14.9 3.4,12 L1.3,12 C2.4,16 6.1,19 10.5,19 C15.8,19 20,14.7 20,9.5 C20,4.3 15.7,0 10.5,0 L10.5,0 Z M9,5 L9,10.1 L13.7,12.9 L14.5,11.6 L10.5,9.2 L10.5,5 L9,5 L9,5 Z" id="Shape"/></g></g></g></svg>
|
After Width: | Height: | Size: 813 B |
@@ -2,6 +2,7 @@ module.exports = Interface;
|
|||||||
|
|
||||||
var m = require("mithril");
|
var m = require("mithril");
|
||||||
var FuzzySort = require("fuzzysort");
|
var FuzzySort = require("fuzzysort");
|
||||||
|
var Moment = require("moment");
|
||||||
var SearchInterface = require("./searchinterface");
|
var SearchInterface = require("./searchinterface");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,8 +103,21 @@ function view(ctl, params) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
badges ? m("div.store.badge", result.store) : null,
|
badges ? m("div.store.badge", result.store.name) : null,
|
||||||
m("div.name", m.trust(result.display)),
|
m("div.name", [
|
||||||
|
m.trust(result.display),
|
||||||
|
result.recent.when > 0
|
||||||
|
? m("div.recent", {
|
||||||
|
title:
|
||||||
|
"Used here " +
|
||||||
|
result.recent.count +
|
||||||
|
" time" +
|
||||||
|
(result.recent.count > 1 ? "s" : "") +
|
||||||
|
", last " +
|
||||||
|
Moment(new Date(result.recent.when)).fromNow()
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
]),
|
||||||
m("div.action.copy-password", {
|
m("div.action.copy-password", {
|
||||||
title: "Copy password",
|
title: "Copy password",
|
||||||
onclick: function(e) {
|
onclick: function(e) {
|
||||||
@@ -148,7 +162,19 @@ function search(s) {
|
|||||||
// get candidate list
|
// get candidate list
|
||||||
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
|
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
|
||||||
if (this.currentDomainOnly) {
|
if (this.currentDomainOnly) {
|
||||||
candidates = candidates.filter(login => login.inCurrentDomain);
|
var recent = candidates.filter(login => login.recent.count > 0);
|
||||||
|
recent.sort(function(a, b) {
|
||||||
|
if (a.store.when != b.store.when) {
|
||||||
|
return b.store.when - a.store.when;
|
||||||
|
}
|
||||||
|
if (a.recent.count != b.recent.count) {
|
||||||
|
return b.recent.count - a.recent.count;
|
||||||
|
}
|
||||||
|
return b.recent.when - a.recent.when;
|
||||||
|
});
|
||||||
|
candidates = recent.concat(
|
||||||
|
candidates.filter(login => login.inCurrentDomain && !login.recent.count)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.length) {
|
if (s.length) {
|
||||||
@@ -156,7 +182,7 @@ function search(s) {
|
|||||||
// fuzzy-search first word & add highlighting
|
// fuzzy-search first word & add highlighting
|
||||||
if (fuzzyFirstWord) {
|
if (fuzzyFirstWord) {
|
||||||
candidates = FuzzySort.go(filter[0], candidates, {
|
candidates = FuzzySort.go(filter[0], candidates, {
|
||||||
keys: ["login", "store"],
|
keys: ["login", "store.name"],
|
||||||
allowTypo: false
|
allowTypo: false
|
||||||
}).map(result =>
|
}).map(result =>
|
||||||
Object.assign(result.obj, {
|
Object.assign(result.obj, {
|
||||||
|
@@ -95,9 +95,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.part.login > .name {
|
.part.login > .name {
|
||||||
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.part.login > .name > .recent {
|
||||||
|
background-image: url("icon-history.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.part.login > .action {
|
.part.login > .action {
|
||||||
background-position: right 3px top 0;
|
background-position: right 3px top 0;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
require("chrome-extension-async");
|
require("chrome-extension-async");
|
||||||
var TldJS = require("tldjs");
|
var TldJS = require("tldjs");
|
||||||
|
var sha1 = require("sha1");
|
||||||
var Interface = require("./interface");
|
var Interface = require("./interface");
|
||||||
|
|
||||||
// wrap with current tab & settings
|
// wrap with current tab & settings
|
||||||
@@ -15,6 +16,20 @@ chrome.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
|
|||||||
var settings = response.settings;
|
var settings = response.settings;
|
||||||
settings.tab = tabs[0];
|
settings.tab = tabs[0];
|
||||||
settings.host = new URL(settings.tab.url).hostname;
|
settings.host = new URL(settings.tab.url).hostname;
|
||||||
|
for (var store in settings.stores) {
|
||||||
|
var when = localStorage.getItem("recent:" + settings.stores[store].path);
|
||||||
|
if (when) {
|
||||||
|
settings.stores[store].when = JSON.parse(when);
|
||||||
|
} else {
|
||||||
|
settings.stores[store].when = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.recent = localStorage.getItem("recent");
|
||||||
|
if (settings.recent) {
|
||||||
|
settings.recent = JSON.parse(settings.recent);
|
||||||
|
} else {
|
||||||
|
settings.recent = {};
|
||||||
|
}
|
||||||
run(settings);
|
run(settings);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e);
|
handleError(e);
|
||||||
@@ -79,21 +94,34 @@ async function run(settings) {
|
|||||||
var response = await chrome.runtime.sendMessage({ action: "listFiles" });
|
var response = await chrome.runtime.sendMessage({ action: "listFiles" });
|
||||||
var logins = [];
|
var logins = [];
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
var recent = localStorage.getItem("recent:" + settings.host);
|
||||||
|
if (recent) {
|
||||||
|
recent = JSON.parse(recent);
|
||||||
|
}
|
||||||
for (var store in response) {
|
for (var store in response) {
|
||||||
for (var key in response[store]) {
|
for (var key in response[store]) {
|
||||||
// set login fields
|
// set login fields
|
||||||
var login = {
|
var login = {
|
||||||
index: index++,
|
index: index++,
|
||||||
store: store,
|
store: settings.stores[store],
|
||||||
login: response[store][key].replace(/\.gpg$/i, ""),
|
login: response[store][key].replace(/\.gpg$/i, ""),
|
||||||
allowFill: true
|
allowFill: true
|
||||||
};
|
};
|
||||||
login.domain = pathToDomain(login.store + "/" + login.login);
|
login.domain = pathToDomain(store + "/" + login.login);
|
||||||
login.inCurrentDomain =
|
login.inCurrentDomain =
|
||||||
settings.host == login.domain || settings.host.endsWith("." + login.domain);
|
settings.host == login.domain || settings.host.endsWith("." + login.domain);
|
||||||
|
login.recent =
|
||||||
|
settings.recent[
|
||||||
|
sha1(settings.host + sha1(login.store.path + sha1(login.login)))
|
||||||
|
];
|
||||||
|
if (!login.recent) {
|
||||||
|
login.recent = {
|
||||||
|
when: 0,
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
// bind handlers
|
// bind handlers
|
||||||
login.doAction = withLogin.bind(login);
|
login.doAction = withLogin.bind({ settings: settings, login: login });
|
||||||
|
|
||||||
logins.push(login);
|
logins.push(login);
|
||||||
}
|
}
|
||||||
@@ -105,6 +133,34 @@ async function run(settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save login to recent list for current domain
|
||||||
|
*
|
||||||
|
* @since 3.0.0
|
||||||
|
*
|
||||||
|
* @param object settings Settings object
|
||||||
|
* @param object login Login object
|
||||||
|
* @param bool remove Remove this item from recent history
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function saveRecent(settings, login, remove = false) {
|
||||||
|
var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window
|
||||||
|
|
||||||
|
// save store timestamp
|
||||||
|
localStorage.setItem("recent:" + login.store.path, JSON.stringify(Date.now()));
|
||||||
|
|
||||||
|
// update login usage count & timestamp
|
||||||
|
if (Date.now() > login.recent.when + ignoreInterval) {
|
||||||
|
login.recent.count++;
|
||||||
|
}
|
||||||
|
login.recent.when = Date.now();
|
||||||
|
settings.recent[sha1(settings.host + sha1(login.store.path + sha1(login.login)))] =
|
||||||
|
login.recent;
|
||||||
|
|
||||||
|
// save to local storage
|
||||||
|
localStorage.setItem("recent", JSON.stringify(settings.recent));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a login action
|
* Do a login action
|
||||||
*
|
*
|
||||||
@@ -142,10 +198,19 @@ async function withLogin(action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hand off action to background script
|
// hand off action to background script
|
||||||
var response = await chrome.runtime.sendMessage({ action: action, login: this });
|
var response = await chrome.runtime.sendMessage({
|
||||||
|
action: action,
|
||||||
|
login: this.login
|
||||||
|
});
|
||||||
if (response.status != "ok") {
|
if (response.status != "ok") {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
|
switch (action) {
|
||||||
|
case "fill":
|
||||||
|
case "copyPassword":
|
||||||
|
case "copyUsername":
|
||||||
|
saveRecent(this.settings, this.login);
|
||||||
|
}
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -224,6 +224,10 @@ cached-path-relative@^1.0.0:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
|
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
|
||||||
|
|
||||||
|
"charenc@>= 0.0.1":
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||||
|
|
||||||
chrome-extension-async@^3.0.0:
|
chrome-extension-async@^3.0.0:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/chrome-extension-async/-/chrome-extension-async-3.2.4.tgz#c5b3e206688ca81c903b5e239ff3f9a73a29c47e"
|
resolved "https://registry.yarnpkg.com/chrome-extension-async/-/chrome-extension-async-3.2.4.tgz#c5b3e206688ca81c903b5e239ff3f9a73a29c47e"
|
||||||
@@ -303,6 +307,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
"crypt@>= 0.0.1":
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||||
|
|
||||||
crypto-browserify@^3.0.0:
|
crypto-browserify@^3.0.0:
|
||||||
version "3.12.0"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||||
@@ -606,6 +614,10 @@ module-deps@^6.0.0:
|
|||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
|
moment@^2.22.1:
|
||||||
|
version "2.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
|
||||||
|
|
||||||
once@^1.3.0:
|
once@^1.3.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
@@ -759,6 +771,13 @@ sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
sha1@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848"
|
||||||
|
dependencies:
|
||||||
|
charenc ">= 0.0.1"
|
||||||
|
crypt ">= 0.0.1"
|
||||||
|
|
||||||
shasum@^1.0.0:
|
shasum@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
|
resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
|
||||||
|
Reference in New Issue
Block a user