├── icons ├── bad-api.png ├── good-api.png ├── pin-19.png ├── pin-32.png ├── pin-38.png ├── bookmark-16.png ├── bookmark-32.png ├── pin-ticked-19.png ├── pin-ticked-38.png ├── pinboard-32.png ├── pinboard-48.png ├── pinboard-96.png ├── readlater-16.png └── readlater-32.png ├── options ├── options.css ├── options.js └── options.html ├── pinboard_menu.css ├── README.md ├── manifest.json ├── pinboard_menu.html ├── pinboard_menu.js └── background.js /icons/bad-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/bad-api.png -------------------------------------------------------------------------------- /icons/good-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/good-api.png -------------------------------------------------------------------------------- /icons/pin-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pin-19.png -------------------------------------------------------------------------------- /icons/pin-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pin-32.png -------------------------------------------------------------------------------- /icons/pin-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pin-38.png -------------------------------------------------------------------------------- /icons/bookmark-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/bookmark-16.png -------------------------------------------------------------------------------- /icons/bookmark-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/bookmark-32.png -------------------------------------------------------------------------------- /icons/pin-ticked-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pin-ticked-19.png -------------------------------------------------------------------------------- /icons/pin-ticked-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pin-ticked-38.png -------------------------------------------------------------------------------- /icons/pinboard-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pinboard-32.png -------------------------------------------------------------------------------- /icons/pinboard-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pinboard-48.png -------------------------------------------------------------------------------- /icons/pinboard-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/pinboard-96.png -------------------------------------------------------------------------------- /icons/readlater-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/readlater-16.png -------------------------------------------------------------------------------- /icons/readlater-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rami114/pinboard/HEAD/icons/readlater-32.png -------------------------------------------------------------------------------- /options/options.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | width: 25em; 4 | font-family: "Open Sans Light", sans-serif; 5 | font-size: 0.9em; 6 | font-weight: 300; 7 | } 8 | 9 | 10 | .title { 11 | font-size: 1.2em; 12 | margin-bottom: 0.5em; 13 | } 14 | 15 | label { 16 | float: right; 17 | } 18 | 19 | input { 20 | margin: 0.5em; 21 | width: 200px; 22 | height: 2.5em; 23 | } 24 | -------------------------------------------------------------------------------- /pinboard_menu.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0 0.2em 0.2em 0.2em; 3 | width: auto; 4 | min-width: 200px; 5 | max-width: 400px; 6 | } 7 | 8 | .panel-section-separator { 9 | margin: 0.2em 0; 10 | } 11 | 12 | .good-api { 13 | background-image: url('icons/good-api.png'); 14 | background-repeat: no-repeat; 15 | padding-left: 20px; 16 | display: block; 17 | } 18 | 19 | .bad-api { 20 | background-image: url('icons/bad-api.png'); 21 | background-repeat: no-repeat; 22 | padding-left: 20px; 23 | display: block; 24 | } 25 | 26 | .hidden { 27 | display: none; 28 | } -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | const apiKeyInput = document.querySelector("#api-key"); 2 | 3 | // Helper functions 4 | function storeApiKey() { 5 | browser.storage.local.set({ 6 | apiKey: apiKeyInput.value 7 | }); 8 | // Will trigger an immediate poll 9 | browser.runtime.sendMessage({'type': 'api-key-saved'}); 10 | } 11 | 12 | function updateUI(restoredSettings) { 13 | apiKeyInput.value = restoredSettings.apiKey || ""; 14 | } 15 | 16 | function onError(e) { 17 | console.error(e); 18 | } 19 | 20 | // Add hooks to read/write api key as needed 21 | browser.storage.local.get().then(updateUI, onError); 22 | apiKeyInput.addEventListener("blur", storeApiKey); 23 | 24 | -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

API key

13 |

Adding your API key will allow the add-on to display the status of the current URL. I.e. has it previously been saved or not.

14 |

You can find your API key on your settings page

15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinboard 2 | 3 | Firefox Quantum addon for [pinboard.in](https://pinboard.in). 4 | 5 | ## Features 6 | 7 | This plugin replicates the functionality of the legacy plugin as closely as possible: 8 | 9 | * Address bar button with menu to allow 10 | * Add to pinboard 11 | * Read later 12 | * Access to various direct links on pinboard.in 13 | * Right-click context menu on links to allow 14 | * Add to pinboard 15 | * Read later 16 | 17 | In addition, an optional API key for Pinboard.in can be supplied via about:addons. 18 | Once an API key has been added the API status will show in the address bar popup. 19 | A URL which is already bookmarked will show the Pinboard icon with a green tick in the address bar. 20 | 21 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "gecko": { 4 | "id": "{31547e30-cfb0-43bd-9bf7-7f2d7a96fcd6}", 5 | "strict_min_version": "48.0a1" 6 | } 7 | }, 8 | 9 | "manifest_version": 2, 10 | "name": "Pinboard", 11 | "version": "1.2.0", 12 | "description": "Pinboard.in extension to allow for easy bookmarking", 13 | "homepage_url": "https://pinboard.in/", 14 | "background": { 15 | "scripts": ["background.js"] 16 | }, 17 | 18 | "commands": { 19 | "command-all-bookmarks": { 20 | "suggested_key": { 21 | "default": "Alt+Shift+A" 22 | }, 23 | "description": "View all your Pinboard bookmarks" 24 | }, 25 | "command-save-to-pinboard": { 26 | "suggested_key": { 27 | "default": "Alt+Shift+P" 28 | }, 29 | "description": "Save current URL to pinboard" 30 | } 31 | }, 32 | 33 | "page_action": { 34 | "browser_style": true, 35 | "default_icon": { 36 | "19" : "icons/pin-19.png", 37 | "38" : "icons/pin-38.png" 38 | }, 39 | "default_title": "Pinboard", 40 | "default_popup" : "pinboard_menu.html" 41 | }, 42 | "options_ui": { 43 | "page": "options/options.html" 44 | }, 45 | "icons": { 46 | "48": "icons/pinboard-48.png", 47 | "96": "icons/pinboard-96.png" 48 | }, 49 | 50 | "permissions": [ 51 | "*://api.pinboard.in/*", 52 | "activeTab", 53 | "tabs", 54 | "menus", 55 | "notifications", 56 | "storage", 57 | "alarms" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /pinboard_menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |
Save to Pinboard
15 |
16 |
17 |
Read later
18 |
19 |
20 |
Save tab set
21 |
22 |
23 | 24 |
25 | 26 |
27 |
Your unread bookmarks
28 |
29 |
30 |
All your bookmarks
31 |
32 |
33 | 34 |
35 |
36 |
Your saved tab sets
37 |
38 | 39 |
40 |
41 |
Add API key to see status
42 |
43 | 46 | 49 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /pinboard_menu.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("click", (e) => { 2 | let querying = browser.tabs.query({currentWindow: true, active: true}); 3 | switch(e.target.id) { 4 | case "save-to-pinboard": 5 | querying.then((tabs) => { 6 | for (let tab of tabs) { 7 | let title = tab.title; 8 | let desc = ''; 9 | let uri = tab.url; 10 | browser.runtime.sendMessage({ 11 | 'type' : 'save-to-pinboard', 12 | 'title' : title, 13 | 'desc' : desc, 14 | 'uri' : uri 15 | }); 16 | break; 17 | } 18 | }); 19 | break; 20 | case "read-later": 21 | querying.then((tabs) => { 22 | for (let tab of tabs) { 23 | let title = tab.title; 24 | let uri = tab.url; 25 | browser.runtime.sendMessage({ 26 | 'type' : 'read-later', 27 | 'title' : title, 28 | 'uri' : uri 29 | }); 30 | break; 31 | } 32 | }); 33 | break; 34 | case "save-tab-set": 35 | // TODO 36 | break; 37 | case "goto-unread-bookmarks": 38 | browser.tabs.create({active: true, url: "https://pinboard.in/toread"}); 39 | break 40 | case "goto-all-bookmarks": 41 | browser.tabs.create({active: true, url: "https://pinboard.in/"}); 42 | break 43 | case "goto-pinboard-popular": 44 | browser.tabs.create({active: true, url: "https://pinboard.in/network"}); 45 | break 46 | case "goto-saved-tab-sets": 47 | browser.tabs.create({active: true, url: "https://pinboard.in/tabs"}); 48 | break 49 | } 50 | }); 51 | 52 | // Api functions 53 | function onError(e) { 54 | console.error(e); 55 | } 56 | 57 | // We rely on the status from storage, 58 | function checkApi(settings) { 59 | let apiStatus = (settings.apiStatus && typeof settings.apiStatus !== undefined) ? settings.apiStatus : "no-api-key"; 60 | console.log(apiStatus); 61 | let noApiKey = document.getElementById("no-api-key"); 62 | let goodApiKey = document.getElementById("good-api-key"); 63 | let apiError = document.getElementById("api-error"); 64 | let badApiKey = document.getElementById("bad-api-key"); 65 | switch(apiStatus) { 66 | case "no-api-key": 67 | noApiKey.classList.remove("hidden"); 68 | goodApiKey.classList.add("hidden"); 69 | apiError.classList.add("hidden"); 70 | badApiKey.classList.add("hidden"); 71 | break; 72 | case "bad-api-key": 73 | noApiKey.classList.add("hidden"); 74 | goodApiKey.classList.add("hidden"); 75 | apiError.classList.add("hidden"); 76 | badApiKey.classList.remove("hidden"); 77 | break; 78 | case "network-issue": 79 | noApiKey.classList.add("hidden"); 80 | goodApiKey.classList.add("hidden"); 81 | apiError.classList.remove("hidden"); 82 | badApiKey.classList.add("hidden"); 83 | break 84 | case "good-api-key": 85 | noApiKey.classList.add("hidden"); 86 | goodApiKey.classList.remove("hidden"); 87 | apiError.classList.add("hidden"); 88 | badApiKey.classList.add("hidden"); 89 | break; 90 | } 91 | } 92 | 93 | 94 | browser.storage.local.get().then(checkApi, onError); 95 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // Globals 2 | var gShowNotifications = true; 3 | // Add-on functions 4 | function onCreated() { 5 | if (browser.runtime.lastError) { 6 | console.log(`Error: ${browser.runtime.lastError}`); 7 | } else { 8 | console.log("Item created successfully"); 9 | } 10 | } 11 | 12 | function onError(e) { 13 | console.error(e); 14 | } 15 | 16 | function readSettings(settings) { 17 | // Api-key checks 18 | let apiKey = settings.apiKey; 19 | if (settings.apiKey && settings.apiKey !== undefined) { 20 | apiCheck(apiKey); 21 | } 22 | // We will poll in the background every 5 minutes 23 | browser.alarms.create("poll-api", { 24 | delayInMinutes: 5, 25 | periodInMinutes: 5 26 | }); 27 | // Determines if we should show notifications 28 | gShowNotifications = (settings.showNotifications && typeof settings.showNotifications !== undefined) ? settings.showNotifications : false; 29 | browser.menus.update("pinboard-menu-notifications", { 30 | checked: settings.showNotifications 31 | }); 32 | } 33 | 34 | function performApiPoll() { 35 | browser.storage.local.get().then((settings) => { 36 | let apiKey = settings.apiKey; 37 | if (settings.apiKey && settings.apiKey !== undefined) { 38 | apiCheck(apiKey); 39 | } else { 40 | browser.storage.local.set({ 41 | apiStatus: "no-api-key" 42 | }); 43 | } 44 | }, onError); 45 | } 46 | 47 | function apiCheck(apiKey) { 48 | let apiUrl = "https://api.pinboard.in/v1/user/api_token/?auth_token=" + encodeURIComponent(apiKey); 49 | fetch(apiUrl).then((response) => { 50 | if (response.ok) { 51 | browser.storage.local.set({ 52 | apiStatus: "good-api-key" 53 | }); 54 | } else { 55 | browser.storage.local.set({ 56 | apiStatus: "bad-api-key" 57 | }); 58 | } 59 | }).catch((error) => { 60 | browser.storage.local.set({ 61 | apiStatus: "network-issue" 62 | }); 63 | }); 64 | } 65 | 66 | function handleAlarm(alarmInfo) { 67 | switch (alarmInfo.name) { 68 | case "poll-api": 69 | performApiPoll(); 70 | break; 71 | } 72 | } 73 | 74 | // These two globals are to cache the last tab lookup - otherwise we ping the API consecutively 75 | var cacheTabUrl = ''; 76 | var cacheTabXml = null; 77 | 78 | function clearTabCache() { 79 | cacheTabUrl = ''; 80 | cacheTabXml = null; 81 | } 82 | 83 | // Our icons for the address bad 84 | const defaultIcons = {"19" : "icons/pin-19.png", 85 | "38" : "icons/pin-38.png"}; 86 | const tickIcons = {"19" : "icons/pin-ticked-19.png", 87 | "38" : "icons/pin-ticked-38.png"}; 88 | 89 | function checkTabUrl(tab, settings) { 90 | let apiKey = settings.apiKey; 91 | let apiStatus = settings.apiStatus; 92 | if (apiStatus == "good-api-key") { 93 | let checkUrl = 'https://api.pinboard.in/v1/posts/get?auth_token=' + encodeURIComponent(apiKey) + '&url=' + encodeURIComponent(tab.url); 94 | if(checkUrl !== cacheTabUrl) { 95 | fetch(checkUrl) 96 | .then(response => response.text()) 97 | .then(str => (new window.DOMParser()).parseFromString(str, "text/xml")) 98 | .then((xml) => { 99 | cacheTabXml = xml; 100 | cacheTabUrl = checkUrl; 101 | let posts = cacheTabXml.getElementsByTagName("post"); 102 | if (posts.length > 0) { 103 | // We've seen this, change the icon 104 | browser.pageAction.setIcon({tabId: tab.id, path: tickIcons}); 105 | } else { 106 | browser.pageAction.setIcon({tabId: tab.id, path: defaultIcons}); 107 | } 108 | }); 109 | } 110 | } else { 111 | browser.pageAction.setIcon({tabId: tab.id, path: defaultIcons}); 112 | } 113 | } 114 | 115 | // Pinboard functions 116 | 117 | function bookmark(uri, desc, title) { 118 | const dest = 'https://pinboard.in/add?showtags=yes&url=' + encodeURIComponent(uri) + '&description=' + encodeURIComponent(desc) + '&title=' + encodeURIComponent(title); 119 | browser.windows.create({ 120 | type: "popup", 121 | height: 350, 122 | width: 725, 123 | url: dest 124 | }); 125 | clearTabCache(); 126 | } 127 | 128 | function readLater(uri, title) { 129 | const dest = 'https://pinboard.in/add?later=yes&noui=yes&jump=close&url=' + encodeURIComponent(uri) + '&title=' + encodeURIComponent(title); 130 | // We want a window far, far away 131 | // Ugly method to refocus our current window 132 | // Does not gracefully handle when you're not already logged in :( 133 | let getting = browser.windows.getCurrent(); 134 | getting.then((windowInfo) => { 135 | // Popup 136 | browser.windows.create({ 137 | allowScriptsToClose: true, 138 | //focused: false, // Unsupported by FF?! Sigh. 139 | height: 100, 140 | width: 100, 141 | type: "popup", 142 | url: dest 143 | }); 144 | // Refocus 145 | browser.windows.update(windowInfo.id, { 146 | focused: true 147 | }); 148 | }); 149 | if (gShowNotifications) { 150 | browser.notifications.create({ 151 | type: "basic", 152 | message: "Saved to read later", 153 | title: "Pinboard", 154 | iconUrl: browser.extension.getURL("icons/pinboard-32.png") 155 | }); 156 | } 157 | clearTabCache(); 158 | } 159 | // Creatte the contextual menus (tools and right-click) 160 | browser.menus.create({ 161 | id: "pinboard-menu-notifications", 162 | title: "Show notifications", 163 | type: "checkbox", 164 | checked: gShowNotifications, 165 | contexts: ["tools_menu"] 166 | }, onCreated); 167 | // This is primarily to force a sub-menu, but why not link to the source v0v 168 | browser.menus.create({ 169 | id: "pinboard-menu-github", 170 | title: "View on Github", 171 | type: "normal", 172 | contexts: ["tools_menu"] 173 | }, onCreated); 174 | browser.menus.create({ 175 | id: "link-save-to-pinboard", 176 | title: "Save to Pinboard", 177 | type: "normal", 178 | icons: { 179 | "16": "icons/bookmark-16.png", 180 | "32": "icons/bookmark-32.png" 181 | }, 182 | contexts: ["link"] 183 | }, onCreated); 184 | browser.menus.create({ 185 | id: "link-read-later", 186 | title: "Read later", 187 | type: "normal", 188 | icons: { 189 | "16": "icons/readlater-16.png", 190 | "32": "icons/readlater-32.png" 191 | }, 192 | contexts: ["link"] 193 | }, onCreated); 194 | 195 | // Event listeners 196 | 197 | browser.menus.onClicked.addListener((info, tab) => { 198 | switch (info.menuItemId) { 199 | case "pinboard-menu-notifications": 200 | gShowNotifications = info.checked; 201 | browser.storage.local.set({ 202 | showNotifications: info.checked 203 | }); 204 | break; 205 | case "pinboard-menu-github": 206 | browser.tabs.create({ 207 | active: true, 208 | url: "https://github.com/Rami114/pinboard" 209 | }); 210 | break; 211 | case "link-save-to-pinboard": 212 | let desc = (info.selectionText && typeof info.selectionText !== undefined) ? info.selectionText : ''; 213 | bookmark(info.linkUrl, desc, info.linkText); 214 | break; 215 | case "link-read-later": 216 | readLater(info.linkUrl, info.linkText); 217 | break; 218 | } 219 | }); 220 | browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 221 | if (!tab.url.match(/^about:/)) { 222 | browser.pageAction.show(tab.id); 223 | browser.storage.local.get().then((settings) => {checkTabUrl(tab, settings)}, onError); 224 | } 225 | }); 226 | 227 | browser.runtime.onMessage.addListener((message) => { 228 | switch (message.type) { 229 | case "save-to-pinboard": 230 | bookmark(message.uri, message.desc, message.title); 231 | break; 232 | case "read-later": 233 | readLater(message.uri, message.title); 234 | break; 235 | case "api-key-saved": 236 | performApiPoll(); 237 | break; 238 | } 239 | }); 240 | 241 | browser.commands.onCommand.addListener(function(command) { 242 | switch (command) { 243 | case "command-save-to-pinboard": 244 | let querying = browser.tabs.query({currentWindow: true, active: true}); 245 | querying.then((tabs) => { 246 | for (let tab of tabs) { 247 | let title = tab.title; 248 | let desc = ''; 249 | let uri = tab.url; 250 | bookmark(uri, desc, title); 251 | break; 252 | } 253 | }); 254 | break; 255 | case "command-all-bookmarks": 256 | browser.tabs.create({active: true, url: "https://pinboard.in/"}); 257 | break; 258 | } 259 | }); 260 | 261 | // Fires on startup 262 | browser.storage.local.get().then(readSettings, onError); 263 | // Add alarm listener for api checks 264 | browser.alarms.onAlarm.addListener(handleAlarm); 265 | --------------------------------------------------------------------------------