diff --git a/Homely/manifest.chrome.json b/Homely/manifest.chrome.json
index f34a53a..b4b440e 100644
--- a/Homely/manifest.chrome.json
+++ b/Homely/manifest.chrome.json
@@ -19,29 +19,14 @@
"fontSettings",
"storage"
],
+ "optional_host_permissions": [
+ "*://*/*"
+ ],
"optional_permissions": [
"bookmarks",
"history",
"management"
],
- "optional_host_permissions": [
- "https://www.amazon.co.uk/",
- "https://www.amazon.com/",
- "http://cart.payments.ebay.co.uk/",
- "https://www.facebook.com/", "https://m.facebook.com/",
- "https://github.com/",
- "https://accounts.google.com/", "https://mail.google.com/",
- "https://www.linkedin.com/",
- "https://login.live.com/", "https://*.mail.live.com/",
- "https://www.reddit.com/",
- "https://steamcommunity.com/",
- "https://store.steampowered.com/",
- "https://ticktick.com/",
- "https://trello.com/",
- "https://twitter.com/",
- "http://api.openweathermap.org/",
- "http://www.whatismyproxy.com/"
- ],
"icons": {
"16": "res/img/icon-16.png",
"48": "res/img/logo-48.png",
diff --git a/Homely/manifest.firefox.json b/Homely/manifest.firefox.json
index caea7d7..16b8d9f 100644
--- a/Homely/manifest.firefox.json
+++ b/Homely/manifest.firefox.json
@@ -17,26 +17,13 @@
"permissions": [
"storage"
],
+ "optional_host_permissions": [
+ "*://*/*"
+ ],
"optional_permissions": [
"bookmarks",
"history",
- "management",
- "https://www.amazon.co.uk/",
- "https://www.amazon.com/",
- "http://cart.payments.ebay.co.uk/",
- "https://www.facebook.com/", "https://m.facebook.com/",
- "https://github.com/",
- "https://accounts.google.com/", "https://mail.google.com/",
- "https://www.linkedin.com/",
- "https://login.live.com/", "https://*.mail.live.com/",
- "https://www.reddit.com/",
- "https://steamcommunity.com/",
- "https://store.steampowered.com/",
- "https://ticktick.com/",
- "https://trello.com/",
- "https://twitter.com/",
- "http://api.openweathermap.org/",
- "http://www.whatismyproxy.com/"
+ "management"
],
"icons": {
"16": "res/img/icon-16.png",
diff --git a/Homely/res/css/homely.css b/Homely/res/css/homely.css
index 2128246..0bbfebe 100644
--- a/Homely/res/css/homely.css
+++ b/Homely/res/css/homely.css
@@ -68,6 +68,31 @@ body.topbar-fix {
border-color: #222;
color: #fff;
}
+.btn-accent {
+ background-color: #ddd;
+ border-color: #d4d4d4;
+ color: #333;
+}
+.btn-accent:hover,
+.btn-accent:focus,
+.btn-accent:active,
+.btn-accent.active {
+ background-color: #cfcfcf;
+ border-color: #c3c3c3;
+}
+.panel-dark .btn-accent {
+ background-color: #444;
+ border-color: #404040;
+ color: #fff;
+}
+.panel-dark .btn-accent:hover,
+.panel-dark .btn-accent:focus,
+.panel-dark .btn-accent:active,
+.panel-dark .btn-accent.active {
+ background-color: #333;
+ border-color: #222;
+ color: #fff;
+}
.panel.panel-dark {
background-color: #222;
border-color: #080808;
@@ -99,6 +124,17 @@ a {
#links .panel-body .btn {
white-space: normal;
}
+#links .btn-label {
+ position: relative;
+}
+#links .btn-icon {
+ height: 24px;
+ width: 24px;
+ position: absolute;
+ right: calc(100% + 6px);
+ top: 50%;
+ transform: translateY(-50%);
+}
#links-editor .alert {
margin-bottom: 0;
}
diff --git a/Homely/res/js/background.js b/Homely/res/js/background.js
index 20f3b1a..8433aab 100644
--- a/Homely/res/js/background.js
+++ b/Homely/res/js/background.js
@@ -1,3 +1,20 @@
+// Proxy image fetches through the background script to bypass cross-origin canvas tainting
+chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
+ if (msg.type === "fetchImage" && msg.url) {
+ fetch(msg.url).then(function(resp) {
+ return resp.blob();
+ }).then(function(blob) {
+ var reader = new FileReader();
+ reader.onloadend = function() {
+ sendResponse({dataUrl: reader.result});
+ };
+ reader.readAsDataURL(blob);
+ }).catch(function() {
+ sendResponse({dataUrl: null});
+ });
+ return true;
+ }
+});
var links;
chrome.omnibox.onInputStarted.addListener(function() {
chrome.storage.local.get("links", function(store) {
diff --git a/Homely/res/js/homely.js b/Homely/res/js/homely.js
index ec5a100..8d82987 100644
--- a/Homely/res/js/homely.js
+++ b/Homely/res/js/homely.js
@@ -15,6 +15,45 @@ $(document).ready(function() {
var label = function label(text, settings) {
return [" ", $("").addClass("menu-label").html(text)];
}
+ // Average the non-transparent, non-white, non-black pixels of an image to produce a representative color for accent buttons
+ var extractColor = function extractColor(img, callback) {
+ try {
+ var canvas = document.createElement("canvas");
+ canvas.width = 16;
+ canvas.height = 16;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, 16, 16);
+ var data = ctx.getImageData(0, 0, 16, 16).data;
+ var r = 0, g = 0, b = 0, count = 0;
+ for (var i = 0; i < data.length; i += 4) {
+ var pr = data[i], pg = data[i + 1], pb = data[i + 2], pa = data[i + 3];
+ if (pa < 128) continue;
+ if (pr > 240 && pg > 240 && pb > 240) continue;
+ if (pr < 15 && pg < 15 && pb < 15) continue;
+ r += pr;
+ g += pg;
+ b += pb;
+ count++;
+ }
+ if (count > 0) {
+ callback("rgb(" + Math.round(r / count) + "," + Math.round(g / count) + "," + Math.round(b / count) + ")");
+ return true;
+ }
+ } catch(e) {}
+ return false;
+ }
+ // Try to extract the dominant color directly; fall back to re-fetching via the background script if the canvas is tainted by cross-origin data
+ var getDominantColor = function getDominantColor(img, callback) {
+ if (extractColor(img, callback)) return;
+ chrome.runtime.sendMessage({type: "fetchImage", url: img.src}, function(resp) {
+ if (chrome.runtime.lastError) return;
+ if (resp && resp.dataUrl) {
+ var fetchedImg = new Image();
+ fetchedImg.onload = function() { extractColor(fetchedImg, callback); };
+ fetchedImg.src = resp.dataUrl;
+ }
+ });
+ }
var manif = chrome.runtime.getManifest();
// default settings
var settings = {
@@ -744,8 +783,9 @@ $(document).ready(function() {
var btn;
if (linkBtn.menu) {
btn = $("
").addClass("btn-group btn-block");
- btn.append($("").addClass("btn btn-block btn-" + linkBtn.style + " dropdown-toggle").attr("data-toggle", "dropdown")
- .text(linkBtn.title + " ").append($("").addClass("caret")));
+ var dropBtn = $("").addClass("btn btn-block btn-" + linkBtn.style + " dropdown-toggle").attr("data-toggle", "dropdown")
+ .text(linkBtn.title + " ").append($("").addClass("caret"));
+ btn.append(dropBtn);
var menu = $("").addClass("dropdown-menu");
// loop through menu items
var urls = [];
@@ -785,8 +825,38 @@ $(document).ready(function() {
}
btn.append(menu);
} else {
- btn = $("").addClass("btn btn-block btn-" + linkBtn.style).attr("href", linkBtn.url).text(linkBtn.title);
- if (!linkBtn.title) btn.html(" ");
+ btn = $("").addClass("btn btn-block btn-" + linkBtn.style).attr("href", linkBtn.url);
+ if (linkBtn.title) {
+ var titleSpan = $("").addClass("btn-label").text(linkBtn.title);
+ if (linkBtn.icon !== "none") {
+ var iconSrc = linkBtn.icon;
+ if (!iconSrc && linkBtn.url) {
+ try { iconSrc = new URL(linkBtn.url).origin + "/favicon.ico"; } catch(e) {}
+ }
+ if (iconSrc) {
+ var hostname = "";
+ try { hostname = new URL(linkBtn.url).hostname; } catch(e) {}
+ titleSpan.prepend($("
").addClass("btn-icon").attr("src", iconSrc).on("error", function() {
+ var fallback = "https://icons.duckduckgo.com/ip3/" + hostname + ".ico";
+ if (hostname && $(this).attr("src") !== fallback) {
+ $(this).attr("src", fallback);
+ } else {
+ $(this).remove();
+ }
+ }).on("load", function() {
+ if (linkBtn.style === "accent") {
+ getDominantColor(this, function(color) {
+ var m = color.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
+ if (m) btn.css("background-color", "rgba(" + m[1] + "," + m[2] + "," + m[3] + ",0.2)");
+ });
+ }
+ }));
+ }
+ }
+ btn.append(titleSpan);
+ } else {
+ btn.html(" ");
+ }
// workaround for accessing Chrome and file URLs
for (var prefix of ["chrome", "chrome-extension", "file"]) {
if (linkBtn.url.substring(0, prefix.length + 3) === prefix + "://") {
@@ -912,6 +982,13 @@ $(document).ready(function() {
var styles = ["default", "light", "dark", "primary", "info", "success", "warning", "danger"];
var stylePreview = $("").addClass("btn btn-" + linkBtn.style).html(" ");
var styleOpts = [];
+ // Close the style picker and restore the single-button preview
+ var collapseStyleOpts = function() {
+ $(styleOpts).each(function(l, opt) {
+ $(opt).detach();
+ });
+ btnRootRight.append(stylePreview);
+ };
stylePreview.click(function(e) {
stylePreview.detach();
btnRootRight.append(styleOpts);
@@ -919,33 +996,103 @@ $(document).ready(function() {
$(styles).each(function(k, style) {
styleOpts.push($("").addClass("btn btn-" + style).html(" ").click(function(e) {
linkBtn.style = style;
- $(styleOpts).each(function(l, opt) {
- $(opt).detach();
- });
// remove all button style classes
stylePreview.removeClass(function(l, css) {
return (css.match(/\bbtn-\S+/g) || []).join(" ");
}).addClass("btn-" + styles[k]);
- btnRootRight.append(stylePreview);
+ collapseStyleOpts();
}));
});
+ styleOpts.push($("").addClass("btn btn-default").append($("").addClass("fa fa-tint")).click(function(e) {
+ linkBtn.style = "accent";
+ stylePreview.removeClass(function(l, css) {
+ return (css.match(/\bbtn-\S+/g) || []).join(" ");
+ }).addClass("btn-accent");
+ collapseStyleOpts();
+ }));
styleOpts.push($("").addClass("btn btn-default").append($("").addClass("fa fa-magic")).click(function(e) {
var cls = prompt("Enter a class name to apply to the button.\n\nUse the custom CSS box in Settings to add a button style for this name.", "");
if (!cls) return;
linkBtn.style = cls;
if (styles.indexOf(cls) > -1) cls = "btn-" + cls;
- $(styleOpts).each(function(l, opt) {
- $(opt).detach();
- });
// remove all button style classes
stylePreview.removeClass(function(l, css) {
return (css.match(/\bbtn-\S+/g) || []).join(" ");
}).addClass(cls);
- btnRootRight.append(stylePreview);
+ collapseStyleOpts();
}));
btnRootRight.append(stylePreview);
group.append(btnRootRight);
blk.append(group);
+ // icon mode picker (regular buttons only)
+ if (!linkBtn.menu) {
+ var iconGroup = $("").addClass("btn-group btn-group-sm");
+ var iconPreview = $("
").addClass("btn-icon").css({width: "16px", height: "16px", "vertical-align": "middle", "margin-right": "4px"}).hide().on("error", function() {
+ var hostname = "";
+ try { hostname = new URL(linkBtn.url).hostname; } catch(e) {}
+ var fallback = "https://icons.duckduckgo.com/ip3/" + hostname + ".ico";
+ if (hostname && $(this).attr("src") !== fallback) {
+ $(this).attr("src", fallback);
+ } else {
+ $(this).hide();
+ }
+ });
+ var fileInput = $("").attr("type", "file").attr("accept", "image/*").css("display", "none");
+ // Sync the icon picker toggle states and preview with the current linkBtn.icon value
+ var updateIconUI = function() {
+ iconNoImg.removeClass("active");
+ iconFavicon.removeClass("active");
+ iconCustom.removeClass("active");
+ if (linkBtn.icon === "none") {
+ iconNoImg.addClass("active");
+ iconPreview.hide();
+ } else if (linkBtn.icon && linkBtn.icon.indexOf("data:") === 0) {
+ iconCustom.addClass("active");
+ iconPreview.attr("src", linkBtn.icon).show();
+ } else {
+ iconFavicon.addClass("active");
+ if (linkBtn.url) {
+ try { iconPreview.attr("src", new URL(linkBtn.url).origin + "/favicon.ico").show(); } catch(e) { iconPreview.hide(); }
+ } else {
+ iconPreview.hide();
+ }
+ }
+ };
+ var iconNoImg = $("").addClass("btn btn-default").attr("title", "No image").append($("").addClass("fa fa-ban")).click(function(e) {
+ linkBtn.icon = "none";
+ updateIconUI();
+ });
+ var iconFavicon = $("").addClass("btn btn-default").attr("title", "Favicon").append($("").addClass("fa fa-globe")).click(function(e) {
+ delete linkBtn.icon;
+ updateIconUI();
+ });
+ var iconCustom = $("").addClass("btn btn-default").attr("title", "Custom image").append($("").addClass("fa fa-picture-o")).click(function(e) {
+ fileInput.click();
+ });
+ fileInput.change(function(e) {
+ var file = this.files[0];
+ if (!file) return;
+ var reader = new FileReader();
+ reader.onload = function(ev) {
+ var img = new Image();
+ img.onload = function() {
+ var canvas = document.createElement("canvas");
+ canvas.width = 32;
+ canvas.height = 32;
+ canvas.getContext("2d").drawImage(img, 0, 0, 32, 32);
+ linkBtn.icon = canvas.toDataURL("image/png");
+ updateIconUI();
+ };
+ img.src = ev.target.result;
+ };
+ reader.readAsDataURL(file);
+ });
+ iconGroup.append(iconNoImg).append(iconFavicon).append(iconCustom).append(fileInput);
+ updateIconUI();
+ var iconRow = $("").css({"text-align": "right", "padding": "2px 0"});
+ iconRow.append(iconPreview).append(iconGroup);
+ blk.append(iconRow);
+ }
// link/menu options
if (linkBtn.menu) {
var tbody = $("");
@@ -2342,11 +2489,13 @@ $(document).ready(function() {
dragdrop: $("#settings-links-edit-dragdrop").prop("checked")
};
settings.links["behaviour"].dropdownmiddle = $("#settings-links-behaviour-dropdownmiddle").prop("checked");
+ var revokeError = false;
settings.bookmarks["enable"] = $("#settings-bookmarks-enable").prop("checked");
if (!settings.bookmarks["enable"]) {
chrome.permissions.remove({
permissions: ["bookmarks"]
}, function(success) {
+ void chrome.runtime.lastError;
if (!success) revokeError = true;
});
}
@@ -2360,6 +2509,7 @@ $(document).ready(function() {
chrome.permissions.remove({
permissions: ["history"]
}, function(success) {
+ void chrome.runtime.lastError;
if (!success) revokeError = true;
});
}
@@ -2368,10 +2518,10 @@ $(document).ready(function() {
chrome.permissions.remove({
origins: ajaxPerms[key]
}, function(success) {
+ void chrome.runtime.lastError;
if (!success) revokeError = true;
});
}
- var revokeError = false;
settings.notifs["facebook"] = {
enable: {
notifs: $("#settings-notifs-facebook-notifs").prop("checked"),
@@ -2480,6 +2630,7 @@ $(document).ready(function() {
chrome.permissions.remove({
permissions: ["management"]
}, function(success) {
+ void chrome.runtime.lastError;
if (!success) revokeError = true;
});
}
@@ -2518,13 +2669,14 @@ $(document).ready(function() {
});
// write to local storage
chrome.storage.local.set(settings, function() {
- if (chrome.runtime.lastError) {
- $("#settings-alerts").append($("").addClass("alert alert-danger").text("Unable to save: " + chrome.runtime.lastError.message));
+ var err = chrome.runtime.lastError;
+ if (err) {
+ $("#settings-alerts").append($("").addClass("alert alert-danger").text("Unable to save: " + err.message));
$("#settings-save").prop("disabled", false).empty().append(fa("check", false)).append(" Save and reload");
return;
}
if (revokeError) {
- $("#settings-alerts").append($("").addClass("alert alert-warning").text("Failed to revoke permissions: " + chrome.runtime.lastError.message));
+ $("#settings-alerts").append($("").addClass("alert alert-warning").text("Failed to revoke some permissions."));
}
$("#settings-save").empty().append(fa("check", false)).append(" Saved!");
// reload page
@@ -2570,12 +2722,16 @@ $(document).ready(function() {
});
// export settings to file
$("#settings-export").click(function(e) {
+ e.preventDefault();
var toExport = $.extend(true, {}, settings);
// converting image to URI takes too long, hangs browser
delete toExport.style["background"].image;
- // link has a download="homely.json" tag to force download
- $(this).attr("href", "data:application/json;charset=UTF-8," + encodeURIComponent(JSON.stringify(toExport)))
- .click().attr("href", "");
+ var a = document.createElement("a");
+ a.href = "data:application/json;charset=UTF-8," + encodeURIComponent(JSON.stringify(toExport));
+ a.download = "homely.json";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
});
// links selection state
var linksHotkeys = {