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) {
|
||||
var response = await hostAction(settings, "fetch", {
|
||||
store: login.store,
|
||||
store: login.store.name,
|
||||
file: login.login + ".gpg"
|
||||
});
|
||||
if (response.status != "ok") {
|
||||
|
@@ -18,6 +18,8 @@
|
||||
"chrome-extension-async": "^3.0.0",
|
||||
"fuzzysort": "^1.1.0",
|
||||
"mithril": "^1.1.0",
|
||||
"moment": "^2.22.1",
|
||||
"sha1": "^1.1.1",
|
||||
"tldjs": "^2.3.0"
|
||||
},
|
||||
"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 FuzzySort = require("fuzzysort");
|
||||
var Moment = require("moment");
|
||||
var SearchInterface = require("./searchinterface");
|
||||
|
||||
/**
|
||||
@@ -102,8 +103,21 @@ function view(ctl, params) {
|
||||
}
|
||||
},
|
||||
[
|
||||
badges ? m("div.store.badge", result.store) : null,
|
||||
m("div.name", m.trust(result.display)),
|
||||
badges ? m("div.store.badge", result.store.name) : null,
|
||||
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", {
|
||||
title: "Copy password",
|
||||
onclick: function(e) {
|
||||
@@ -148,7 +162,19 @@ function search(s) {
|
||||
// get candidate list
|
||||
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
|
||||
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) {
|
||||
@@ -156,7 +182,7 @@ function search(s) {
|
||||
// fuzzy-search first word & add highlighting
|
||||
if (fuzzyFirstWord) {
|
||||
candidates = FuzzySort.go(filter[0], candidates, {
|
||||
keys: ["login", "store"],
|
||||
keys: ["login", "store.name"],
|
||||
allowTypo: false
|
||||
}).map(result =>
|
||||
Object.assign(result.obj, {
|
||||
|
@@ -95,9 +95,18 @@ body {
|
||||
}
|
||||
|
||||
.part.login > .name {
|
||||
display: flex;
|
||||
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 {
|
||||
background-position: right 3px top 0;
|
||||
background-repeat: no-repeat;
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
require("chrome-extension-async");
|
||||
var TldJS = require("tldjs");
|
||||
var sha1 = require("sha1");
|
||||
var Interface = require("./interface");
|
||||
|
||||
// wrap with current tab & settings
|
||||
@@ -15,6 +16,20 @@ chrome.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
|
||||
var settings = response.settings;
|
||||
settings.tab = tabs[0];
|
||||
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);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
@@ -79,21 +94,34 @@ async function run(settings) {
|
||||
var response = await chrome.runtime.sendMessage({ action: "listFiles" });
|
||||
var logins = [];
|
||||
var index = 0;
|
||||
var recent = localStorage.getItem("recent:" + settings.host);
|
||||
if (recent) {
|
||||
recent = JSON.parse(recent);
|
||||
}
|
||||
for (var store in response) {
|
||||
for (var key in response[store]) {
|
||||
// set login fields
|
||||
var login = {
|
||||
index: index++,
|
||||
store: store,
|
||||
store: settings.stores[store],
|
||||
login: response[store][key].replace(/\.gpg$/i, ""),
|
||||
allowFill: true
|
||||
};
|
||||
login.domain = pathToDomain(login.store + "/" + login.login);
|
||||
login.domain = pathToDomain(store + "/" + login.login);
|
||||
login.inCurrentDomain =
|
||||
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
|
||||
login.doAction = withLogin.bind(login);
|
||||
login.doAction = withLogin.bind({ settings: settings, login: 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
|
||||
*
|
||||
@@ -142,10 +198,19 @@ async function withLogin(action) {
|
||||
}
|
||||
|
||||
// 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") {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
switch (action) {
|
||||
case "fill":
|
||||
case "copyPassword":
|
||||
case "copyUsername":
|
||||
saveRecent(this.settings, this.login);
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
} catch (e) {
|
||||
|
@@ -224,6 +224,10 @@ cached-path-relative@^1.0.0:
|
||||
version "1.0.1"
|
||||
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:
|
||||
version "3.2.4"
|
||||
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"
|
||||
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:
|
||||
version "3.12.0"
|
||||
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"
|
||||
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:
|
||||
version "1.4.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
|
||||
|
Reference in New Issue
Block a user