Implement search (#3)
* Handle settings response properly * Change location of default store settings * Generate chrome extension * Move popup into its own folder * Move distribution javascript into new folder * Remove checkpoint * Ignore dist JS files * Rename background source file * Update clean rule * Fix make rule for generated files * Also tidy CSS files * Implement searching * Add icon to search bar * Add copy-password action * Add copy-user action * Add launch action * Button styling * Send action to backend * Set targets to .PHONY * Highlight the first entry * Allow disabling fuzzy search by starting search with a space
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
src/node_modules
|
||||
src/*.js
|
||||
src/*.log
|
||||
!src/*.src.js
|
||||
/chrome
|
||||
/src/node_modules
|
||||
/src/js
|
||||
/src/*.js
|
||||
/src/*.log
|
||||
!/src/*.src.js
|
||||
|
31
Makefile
Normal file
31
Makefile
Normal file
@@ -0,0 +1,31 @@
|
||||
CLEAN_FILES := chrome
|
||||
|
||||
.PHONY: all
|
||||
all: extension chrome
|
||||
|
||||
.PHONY: extension
|
||||
extension:
|
||||
$(MAKE) -C src
|
||||
|
||||
CHROME_FILES := manifest.json \
|
||||
*.css \
|
||||
*.png \
|
||||
popup/*.html \
|
||||
popup/*.css \
|
||||
popup/*.svg
|
||||
CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \
|
||||
src/js/background.dist.js \
|
||||
src/js/popup.dist.js
|
||||
CHROME_FILES := $(patsubst src/%,chrome/%,$(CHROME_FILES))
|
||||
|
||||
.PHONY: chrome
|
||||
chrome: extension $(CHROME_FILES)
|
||||
|
||||
$(CHROME_FILES) : chrome/% : src/%
|
||||
[ -d $(dir $@) ] || mkdir -p $(dir $@)
|
||||
cp $< $@
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(CLEAN_FILES)
|
||||
$(MAKE) -C src clean
|
24
src/Makefile
24
src/Makefile
@@ -1,18 +1,28 @@
|
||||
BROWSERIFY := node_modules/.bin/browserify
|
||||
PRETTIER := node_modules/.bin/prettier
|
||||
|
||||
CLEAN_FILES := js
|
||||
PRETTIER_FILES := $(wildcard *.js popup/*.js *.css popup/*.css)
|
||||
|
||||
.PHONY: all
|
||||
all: deps prettier background.js popup.js
|
||||
all: deps prettier js/background.dist.js js/popup.dist.js
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
yarn install
|
||||
|
||||
prettier: $(PRETTIER) *.src.js
|
||||
$(PRETTIER) --write *.src.js
|
||||
.PHONY: prettier
|
||||
prettier: $(PRETTIER) $(PRETTIER_FILES)
|
||||
$(PRETTIER) --write $(PRETTIER_FILES)
|
||||
|
||||
background.js: $(BROWSERIFY) background.src.js
|
||||
$(BROWSERIFY) -o background.js background.src.js
|
||||
js/background.dist.js: $(BROWSERIFY) background.js
|
||||
[ -d js ] || mkdir -p js
|
||||
$(BROWSERIFY) -o js/background.dist.js background.js
|
||||
|
||||
popup.js: $(BROWSERIFY) popup.src.js
|
||||
$(BROWSERIFY) -o popup.js popup.src.js
|
||||
js/popup.dist.js: $(BROWSERIFY) popup/*.js
|
||||
[ -d js ] || mkdir -p js
|
||||
$(BROWSERIFY) -o js/popup.dist.js popup/popup.js
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(CLEAN_FILES)
|
||||
|
@@ -155,8 +155,8 @@ async function receiveMessage(message, sender, sendResponse) {
|
||||
// no user-configured stores, so use the default store
|
||||
settings.stores.default = {
|
||||
name: "default",
|
||||
path: response.data.defaultPath,
|
||||
settings: response.data.defaultSettings
|
||||
path: response.data.defaultStore.path,
|
||||
settings: response.data.defaultStore.settings
|
||||
};
|
||||
}
|
||||
handleMessage(settings, message, sendResponse);
|
@@ -1,6 +1,16 @@
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
font-family: sans;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: #0a0;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
margin: 0 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
@@ -12,12 +12,12 @@
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": [
|
||||
"background.js"
|
||||
"js/background.dist.js"
|
||||
]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "icon-lock.png",
|
||||
"default_popup": "popup.html"
|
||||
"default_popup": "popup/popup.html"
|
||||
},
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
@@ -1,28 +0,0 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.part {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 4px 0 4px;
|
||||
}
|
||||
|
||||
.part:last-child {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.part.error {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
.part.notice {
|
||||
color: #090;
|
||||
}
|
||||
|
209
src/popup.src.js
209
src/popup.src.js
@@ -1,209 +0,0 @@
|
||||
//------------------------------------- Initialisation --------------------------------------//
|
||||
"use strict";
|
||||
|
||||
require("chrome-extension-async");
|
||||
var Mithril = require("mithril");
|
||||
var TldJS = require("tldjs");
|
||||
var FuzzySort = require("fuzzysort");
|
||||
|
||||
var startTime = Date.now();
|
||||
var settings = null;
|
||||
var error = null;
|
||||
var notice = null;
|
||||
var logins = [];
|
||||
var domainLogins = [];
|
||||
|
||||
if (typeof browser === "undefined") {
|
||||
var browser = chrome;
|
||||
}
|
||||
|
||||
// performance debugging function - TODO remove once extension is ready for release
|
||||
function checkpoint(activity) {
|
||||
console.log("Elapsed: " + (Date.now() - startTime) + "ms (" + activity + ")");
|
||||
}
|
||||
|
||||
// wrap with current tab & settings
|
||||
checkpoint("start");
|
||||
browser.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
|
||||
checkpoint("after tab");
|
||||
try {
|
||||
var response = browser.runtime.sendMessage({ action: "getSettings" });
|
||||
checkpoint("after getSettings");
|
||||
settings = response;
|
||||
settings.tab = tabs[0];
|
||||
settings.host = new URL(settings.tab.url).hostname;
|
||||
run();
|
||||
} catch (e) {
|
||||
console.log(e.toString()); // TODO
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------- Function definitions ----------------------------------//
|
||||
|
||||
/**
|
||||
* Get the logins which match the provided domain
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string domain Domain to filter against
|
||||
* @return array
|
||||
*/
|
||||
function getDomainLogins(domain) {
|
||||
var domainLogins = [];
|
||||
var t = TldJS.parse(domain);
|
||||
|
||||
// ignore invalid domains
|
||||
if (!t.isValid || t.domain === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// filter against the domain
|
||||
for (var key in logins) {
|
||||
if (logins[key].domain === t.hostname) {
|
||||
domainLogins.push(logins[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// recurse and add matching domains to the list
|
||||
domainLogins = domainLogins.concat(getDomainLogins(t.hostname.replace(/^.+?\./, "")));
|
||||
|
||||
return domainLogins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deepest available domain component of a path
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string path Path to parse
|
||||
* @return string|null Extracted domain
|
||||
*/
|
||||
function pathToDomain(path) {
|
||||
var parts = path.split(/\//).reverse();
|
||||
for (var key in parts) {
|
||||
if (parts[key].indexOf("@") >= 0) {
|
||||
continue;
|
||||
}
|
||||
var t = TldJS.parse(parts[key]);
|
||||
if (t.isValid && t.domain !== null) {
|
||||
return t.hostname;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the popup contents
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function render() {
|
||||
var body = document.getElementsByTagName("body")[0];
|
||||
Mithril.mount(body, {
|
||||
view: function() {
|
||||
return [renderError(), renderNotice(), renderList()];
|
||||
}
|
||||
});
|
||||
checkpoint("after render");
|
||||
}
|
||||
|
||||
/**
|
||||
* Render any error messages
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @return Vnode
|
||||
*/
|
||||
function renderError() {
|
||||
return error === null ? null : Mithril("div.part.error", error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render any notices
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @return Vnode
|
||||
*/
|
||||
function renderNotice() {
|
||||
return notice === null ? null : Mithril("div.part.notice", notice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of available logins
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @return []Vnode
|
||||
*/
|
||||
function renderList() {
|
||||
if (!logins.length) {
|
||||
showError("There are no matching logins available");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = [];
|
||||
domainLogins.forEach(function(login) {
|
||||
list.push(
|
||||
Mithril("div.part.login", { title: login.domain }, login.store + ":" + login.login)
|
||||
);
|
||||
});
|
||||
if (!list.length) {
|
||||
showNotice("There are no logins matching " + settings.host + ".");
|
||||
}
|
||||
|
||||
checkpoint("after renderList");
|
||||
return Mithril("div.logins", list);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// get list of logins
|
||||
var response = await browser.runtime.sendMessage({ action: "listFiles" });
|
||||
checkpoint("after listFiles");
|
||||
for (var store in response) {
|
||||
for (var key in response[store]) {
|
||||
var login = {
|
||||
store: store,
|
||||
login: response[store][key].replace(/\.gpg$/i, "")
|
||||
};
|
||||
login.domain = pathToDomain(login.store + "/" + login.login);
|
||||
logins.push(login);
|
||||
}
|
||||
}
|
||||
checkpoint("after listFiles post-processing");
|
||||
|
||||
domainLogins = getDomainLogins(settings.host);
|
||||
|
||||
render();
|
||||
} catch (e) {
|
||||
showError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error message
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string message Message text
|
||||
*/
|
||||
function showError(message) {
|
||||
error = message;
|
||||
Mithril.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an informational message
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string message Message text
|
||||
*/
|
||||
function showNotice(message) {
|
||||
notice = message;
|
||||
Mithril.redraw();
|
||||
}
|
1
src/popup/icon-key.svg
Normal file
1
src/popup/icon-key.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 100 100" height="100px" id="Layer_1" version="1.1" viewBox="0 0 100 100" width="100px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Captions"/><path d="M70,0C53.432,0,40,13.432,40,30v10L30,50L20,60L10,70L0,80v10v10h10h10V90h10V80h10V70h10V60h10h10 c16.568,0,30-13.432,30-30S86.568,0,70,0z M70,40c-5.522,0-10-4.478-10-10s4.478-10,10-10s10,4.478,10,10S75.522,40,70,40z"/></svg>
|
After Width: | Height: | Size: 591 B |
11
src/popup/icon-launch.svg
Normal file
11
src/popup/icon-launch.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="459px" height="459px" viewBox="0 0 459 459" style="enable-background:new 0 0 459 459;" xml:space="preserve">
|
||||
<g>
|
||||
<g id="share">
|
||||
<path d="M459,216.75L280.5,38.25v102c-178.5,25.5-255,153-280.5,280.5C63.75,331.5,153,290.7,280.5,290.7v104.55L459,216.75z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 657 B |
1
src/popup/icon-search.svg
Normal file
1
src/popup/icon-search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 100 100" height="100px" id="Layer_1" version="1.1" viewBox="0 0 100 100" width="100px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Captions"/><path d="M64.238,54.239C67.879,48.719,70,42.107,70,35C70,15.67,54.33,0,35,0S0,15.67,0,35s15.67,35,35,35 c7.107,0,13.718-2.121,19.238-5.761L90,100l10-10L64.238,54.239z M10,35c0-13.785,11.215-25,25-25c13.785,0,25,11.215,25,25 S48.785,60,35,60C21.215,60,10,48.785,10,35z"/></svg>
|
After Width: | Height: | Size: 636 B |
15
src/popup/icon-user.svg
Normal file
15
src/popup/icon-user.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 350 350" style="enable-background:new 0 0 350 350;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M175,171.173c38.914,0,70.463-38.318,70.463-85.586C245.463,38.318,235.105,0,175,0s-70.465,38.318-70.465,85.587
|
||||
C104.535,132.855,136.084,171.173,175,171.173z"/>
|
||||
<path d="M41.909,301.853C41.897,298.971,41.885,301.041,41.909,301.853L41.909,301.853z"/>
|
||||
<path d="M308.085,304.104C308.123,303.315,308.098,298.63,308.085,304.104L308.085,304.104z"/>
|
||||
<path d="M307.935,298.397c-1.305-82.342-12.059-105.805-94.352-120.657c0,0-11.584,14.761-38.584,14.761
|
||||
s-38.586-14.761-38.586-14.761c-81.395,14.69-92.803,37.805-94.303,117.982c-0.123,6.547-0.18,6.891-0.202,6.131
|
||||
c0.005,1.424,0.011,4.058,0.011,8.651c0,0,19.592,39.496,133.08,39.496c113.486,0,133.08-39.496,133.08-39.496
|
||||
c0-2.951,0.002-5.003,0.005-6.399C308.062,304.575,308.018,303.664,307.935,298.397z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
166
src/popup/interface.js
Normal file
166
src/popup/interface.js
Normal file
@@ -0,0 +1,166 @@
|
||||
module.exports = Interface;
|
||||
|
||||
var m = require("mithril");
|
||||
var FuzzySort = require("fuzzysort");
|
||||
var SearchInterface = require("./searchinterface");
|
||||
|
||||
/**
|
||||
* Popup main interface
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param object settings Settings object
|
||||
* @param array logins Array of available logins
|
||||
* @return void
|
||||
*/
|
||||
function Interface(settings, logins) {
|
||||
// public methods
|
||||
this.attach = attach;
|
||||
this.view = view;
|
||||
this.search = search;
|
||||
|
||||
// fields
|
||||
this.settings = settings;
|
||||
this.logins = logins;
|
||||
this.results = [];
|
||||
this.active = true;
|
||||
this.searchPart = new SearchInterface(this);
|
||||
|
||||
// initialise with empty search
|
||||
this.search("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the interface on the given element
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param DOMElement element Target element
|
||||
* @return void
|
||||
*/
|
||||
function attach(element) {
|
||||
m.mount(element, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates vnodes for render
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param function ctl Controller
|
||||
* @param object params Runtime params
|
||||
* @return []Vnode
|
||||
*/
|
||||
function view(ctl, params) {
|
||||
var badges = Object.keys(this.settings.stores).length > 1;
|
||||
var nodes = [];
|
||||
nodes.push(m(this.searchPart));
|
||||
|
||||
nodes.push(
|
||||
m(
|
||||
"div.logins",
|
||||
this.results.map(function(result) {
|
||||
return m(
|
||||
"div.part.login",
|
||||
{
|
||||
key: result.index,
|
||||
tabindex: 0,
|
||||
onclick: function(e) {
|
||||
result.doAction("fill");
|
||||
},
|
||||
onkeydown: function(e) {
|
||||
switch (e.code) {
|
||||
case "ArrowDown":
|
||||
if (e.target.nextSibling) {
|
||||
e.target.nextSibling.focus();
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (e.target.previousSibling) {
|
||||
e.target.previousSibling.focus();
|
||||
} else {
|
||||
document
|
||||
.querySelector(".part.search input[type=text]")
|
||||
.focus();
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
result.doAction("fill");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
badges ? m("div.store.badge", result.store) : null,
|
||||
m("div.name", m.trust(result.display)),
|
||||
m("div.action.copy-password", {
|
||||
title: "Copy password",
|
||||
onclick: function(e) {
|
||||
e.stopPropagation();
|
||||
result.doAction("copyPassword");
|
||||
}
|
||||
}),
|
||||
m("div.action.copy-user", {
|
||||
title: "Copy username",
|
||||
onclick: function(e) {
|
||||
e.stopPropagation();
|
||||
result.doAction("copyUser");
|
||||
}
|
||||
}),
|
||||
m("div.action.launch", {
|
||||
title: "Open URL",
|
||||
onclick: function(e) {
|
||||
e.stopPropagation();
|
||||
result.doAction("launch");
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a search
|
||||
*
|
||||
* @param string s Search string
|
||||
* @param bool fuzzyFirstWord Whether to use fuzzy search on the first word
|
||||
* @return void
|
||||
*/
|
||||
function search(s, fuzzyFirstWord = true) {
|
||||
var self = this;
|
||||
|
||||
// get candidate list
|
||||
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
|
||||
if (this.active) {
|
||||
candidates = candidates.filter(login => login.active);
|
||||
}
|
||||
|
||||
if (s.length) {
|
||||
var filter = s.split(/\s+/);
|
||||
// fuzzy-search first word & add highlighting
|
||||
if (fuzzyFirstWord) {
|
||||
candidates = FuzzySort.go(filter[0], candidates, {
|
||||
keys: ["login", "store"],
|
||||
allowTypo: false
|
||||
}).map(result =>
|
||||
Object.assign(result.obj, {
|
||||
display: result[0]
|
||||
? FuzzySort.highlight(result[0], "<em>", "</em>")
|
||||
: result.obj.login
|
||||
})
|
||||
);
|
||||
}
|
||||
// substring-search to refine against each remaining word
|
||||
filter.slice(fuzzyFirstWord ? 1 : 0).forEach(function(word) {
|
||||
candidates = candidates.filter(
|
||||
login => login.login.toLowerCase().indexOf(word.toLowerCase()) >= 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.results = candidates;
|
||||
}
|
117
src/popup/popup.css
Normal file
117
src/popup/popup.css
Normal file
@@ -0,0 +1,117 @@
|
||||
html,
|
||||
body {
|
||||
min-width: 260px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logins {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
height: inherit;
|
||||
max-height: 203px; /* 7 x 29px login part*/
|
||||
}
|
||||
|
||||
.part {
|
||||
border-bottom: 1px solid #eee;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
min-height: 29px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.part:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.part > .badge:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.part.error {
|
||||
color: #f00;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.part.notice {
|
||||
color: #090;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.part.search {
|
||||
padding-right: 28px;
|
||||
background-image: url("icon-search.svg");
|
||||
background-position: top 6px right 6px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
}
|
||||
|
||||
.part.search:focus-within {
|
||||
background-color: #eef;
|
||||
}
|
||||
|
||||
.part.search > .hint {
|
||||
background-color: #66c;
|
||||
}
|
||||
|
||||
.part.search > input[type="text"] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.part.login {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.part.login:first-child {
|
||||
background-color: #efe;
|
||||
}
|
||||
|
||||
.part.login:hover,
|
||||
.part.login:focus {
|
||||
background-color: #eee;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.part.login > .name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.part.login > .action {
|
||||
background-position: right 3px top 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px;
|
||||
filter: drop-shadow(2px 2px 1px #666) invert(18%);
|
||||
margin: -2px 0;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.past.login > .action:hover {
|
||||
background-color: #f00;
|
||||
}
|
||||
|
||||
.part.login > .action.copy-password {
|
||||
background-image: url("icon-key.svg");
|
||||
}
|
||||
|
||||
.part.login > .action.copy-user {
|
||||
background-image: url("icon-user.svg");
|
||||
}
|
||||
|
||||
.part.login > .action.launch {
|
||||
background-image: url("icon-launch.svg");
|
||||
}
|
||||
|
||||
.part.login em {
|
||||
color: #00c;
|
||||
font-style: normal;
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="global.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../global.css" />
|
||||
<link rel="stylesheet" type="text/css" href="popup.css" />
|
||||
<script src="popup.js"></script>
|
||||
<script src="../js/popup.dist.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="part notice">Loading available logins...</div>
|
125
src/popup/popup.js
Normal file
125
src/popup/popup.js
Normal file
@@ -0,0 +1,125 @@
|
||||
//------------------------------------- Initialisation --------------------------------------//
|
||||
"use strict";
|
||||
|
||||
require("chrome-extension-async");
|
||||
var TldJS = require("tldjs");
|
||||
var Interface = require("./interface");
|
||||
|
||||
if (typeof browser === "undefined") {
|
||||
var browser = chrome;
|
||||
}
|
||||
|
||||
// wrap with current tab & settings
|
||||
browser.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
|
||||
try {
|
||||
var response = await browser.runtime.sendMessage({ action: "getSettings" });
|
||||
if (response.status != "ok") {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
var settings = response.settings;
|
||||
settings.tab = tabs[0];
|
||||
settings.host = new URL(settings.tab.url).hostname;
|
||||
run(settings);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------- Function definitions ----------------------------------//
|
||||
|
||||
/**
|
||||
* Handle an error
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param Error error Error object
|
||||
*/
|
||||
function handleError(error) {
|
||||
console.log(error);
|
||||
var errorNode = document.createElement("div");
|
||||
errorNode.setAttribute("class", "part error");
|
||||
errorNode.textContent = error.toString();
|
||||
document.body.innerHTML = "";
|
||||
document.body.appendChild(errorNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deepest available domain component of a path
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string path Path to parse
|
||||
* @return string|null Extracted domain
|
||||
*/
|
||||
function pathToDomain(path) {
|
||||
var parts = path.split(/\//).reverse();
|
||||
for (var key in parts) {
|
||||
if (parts[key].indexOf("@") >= 0) {
|
||||
continue;
|
||||
}
|
||||
var t = TldJS.parse(parts[key]);
|
||||
if (t.isValid && t.domain !== null) {
|
||||
return t.hostname;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the main popup logic
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param object settings Settings object
|
||||
* @return void
|
||||
*/
|
||||
async function run(settings) {
|
||||
try {
|
||||
// get list of logins
|
||||
var response = await browser.runtime.sendMessage({ action: "listFiles" });
|
||||
var logins = [];
|
||||
var index = 0;
|
||||
for (var store in response) {
|
||||
for (var key in response[store]) {
|
||||
// set login fields
|
||||
var login = {
|
||||
index: index++,
|
||||
store: store,
|
||||
login: response[store][key].replace(/\.gpg$/i, "")
|
||||
};
|
||||
login.domain = pathToDomain(login.store + "/" + login.login);
|
||||
login.active =
|
||||
settings.host == login.domain || settings.host.endsWith("." + login.domain);
|
||||
|
||||
// bind handlers
|
||||
login.doAction = withLogin.bind(login);
|
||||
|
||||
logins.push(login);
|
||||
}
|
||||
}
|
||||
var popup = new Interface(settings, logins);
|
||||
popup.attach(document.body);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a login action
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param string action Action to take
|
||||
* @return void
|
||||
*/
|
||||
async function withLogin(action) {
|
||||
try {
|
||||
var response = await browser.runtime.sendMessage({ action: action, login: this });
|
||||
if (response.status != "ok") {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
77
src/popup/searchinterface.js
Normal file
77
src/popup/searchinterface.js
Normal file
@@ -0,0 +1,77 @@
|
||||
module.exports = SearchInterface;
|
||||
|
||||
var m = require("mithril");
|
||||
|
||||
/**
|
||||
* Search interface
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param object interface Popup main interface
|
||||
* @return void
|
||||
*/
|
||||
function SearchInterface(popup) {
|
||||
// public methods
|
||||
this.view = view;
|
||||
|
||||
// fields
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates vnodes for render
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param function ctl Controller
|
||||
* @param object params Runtime params
|
||||
* @return []Vnode
|
||||
*/
|
||||
function view(ctl, params) {
|
||||
var self = this;
|
||||
return m(
|
||||
"form.part.search",
|
||||
{
|
||||
onsubmit: function(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
onkeydown: function(e) {
|
||||
switch (e.code) {
|
||||
case "ArrowDown":
|
||||
if (self.popup.results.length) {
|
||||
document.querySelector("*[tabindex]").focus();
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
if (self.popup.results.length) {
|
||||
self.popup.results[0].doAction("fill");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
this.popup.active ? m("div.hint.badge", this.popup.settings.host) : null,
|
||||
m("input[type=text]", {
|
||||
focused: true,
|
||||
placeholder: "Search logins...",
|
||||
oncreate: function(e) {
|
||||
e.dom.focus();
|
||||
},
|
||||
oninput: function(e) {
|
||||
self.popup.search(e.target.value.trim(), e.target.value.substr(0, 1) !== " ");
|
||||
},
|
||||
onkeydown: function(e) {
|
||||
switch (e.code) {
|
||||
case "Backspace":
|
||||
if (self.popup.active && e.target.value.length == 0) {
|
||||
self.popup.active = false;
|
||||
self.popup.search("");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user