├── .gitignore ├── LICENSE ├── README.md ├── icons ├── pinboard_icon.png ├── pinboard_icon_16.png ├── pinboard_icon_48.png ├── pinboard_icon_96.png ├── toolbar_icon.svg ├── toolbar_icon_16.png ├── toolbar_icon_16_light.png ├── toolbar_icon_32.png ├── toolbar_icon_32_light.png ├── toolbar_icon_48.png ├── toolbar_icon_48_light.png ├── toolbar_icon_96.png ├── toolbar_icon_96_light.png └── toolbar_icon_light.svg ├── link_saved.js ├── main.js ├── manifest.json ├── popup_menu.html ├── popup_menu.js ├── preferences.js ├── preferences_page.html └── preferences_page.js /.gitignore: -------------------------------------------------------------------------------- 1 | /*.code-workspace 2 | /*.sh 3 | web-ext-artifacts/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 George A. Pop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinboard WebExtension 2 | 3 | This is a Firefox extension for easily adding links to [Pinboard](https://pinboard.in). It is a remake of the official extension, developed on the WebExtension API. 4 | 5 | Install from AMO: https://addons.mozilla.org/en-US/firefox/addon/pinboard-webextension/. 6 | -------------------------------------------------------------------------------- /icons/pinboard_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/pinboard_icon.png -------------------------------------------------------------------------------- /icons/pinboard_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/pinboard_icon_16.png -------------------------------------------------------------------------------- /icons/pinboard_icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/pinboard_icon_48.png -------------------------------------------------------------------------------- /icons/pinboard_icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/pinboard_icon_96.png -------------------------------------------------------------------------------- /icons/toolbar_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/toolbar_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_16.png -------------------------------------------------------------------------------- /icons/toolbar_icon_16_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_16_light.png -------------------------------------------------------------------------------- /icons/toolbar_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_32.png -------------------------------------------------------------------------------- /icons/toolbar_icon_32_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_32_light.png -------------------------------------------------------------------------------- /icons/toolbar_icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_48.png -------------------------------------------------------------------------------- /icons/toolbar_icon_48_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_48_light.png -------------------------------------------------------------------------------- /icons/toolbar_icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_96.png -------------------------------------------------------------------------------- /icons/toolbar_icon_96_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapop/pinboard-webextension/9de5872acae2ec433358099cebaf1b714d5bc84c/icons/toolbar_icon_96_light.png -------------------------------------------------------------------------------- /icons/toolbar_icon_light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /link_saved.js: -------------------------------------------------------------------------------- 1 | browser.runtime.sendMessage({event: 'link_saved'}) 2 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const Pinboard = { 2 | url: { 3 | add_link: 'https://pinboard.in/add?showtags={show_tags}&url={url}&title={title}&description={description}', 4 | read_later: 'https://pinboard.in/add?later=yes&noui=yes&jump=close&url={url}&title={title}', 5 | save_tabs: 'https://pinboard.in/tabs/save/', 6 | show_tabs: 'https://pinboard.in/tabs/show/', 7 | login: 'https://pinboard.in/popup_login/' 8 | }, 9 | 10 | async get_endpoint(url_handle, bookmark_info) { 11 | const url_template = this.url[url_handle] 12 | const show_tags = await Preferences.get('show_tags') ? 'yes' : 'no' 13 | let endpoint = url_template.replace('{show_tags}', show_tags) 14 | if (bookmark_info) { 15 | endpoint = endpoint.replace('{url}', encodeURIComponent(bookmark_info.url || '')) 16 | .replace('{title}', encodeURIComponent(bookmark_info.title || '')) 17 | .replace('{description}', encodeURIComponent(bookmark_info.description || '')) 18 | } 19 | return endpoint 20 | } 21 | } 22 | 23 | const App = { 24 | toolbar_button_state: Preferences.defaults.toolbar_button, 25 | 26 | // Returns the original URL for a page opened in Firefox's reader mode 27 | async strip_reader_mode_url(url) { 28 | if (url.indexOf('about:reader?url=') == 0) { 29 | url = decodeURIComponent(url.substr(17)) 30 | } 31 | return url 32 | }, 33 | 34 | async get_bookmark_info_from_current_tab() { 35 | const tabs = await browser.tabs.query({currentWindow: true, active: true}) 36 | const info = { 37 | url: await this.strip_reader_mode_url(tabs[0].url), 38 | title: tabs[0].title 39 | } 40 | try { 41 | info.description = await browser.tabs.executeScript({code: 'getSelection().toString()'}) 42 | } catch (error) { 43 | info.description = '' 44 | } 45 | return info 46 | }, 47 | 48 | async get_bookmark_info_from_context_menu_target(info, tab) { 49 | let url 50 | let title = '' 51 | 52 | if (info.linkUrl) { 53 | url = info.linkUrl 54 | if (info.linkText) { 55 | title = info.linkText.substr(0, 200) 56 | if (title.length < info.linkText.length) { 57 | title += '...' 58 | } 59 | } 60 | } else { 61 | url = info.pageUrl 62 | title = tab.title 63 | } 64 | 65 | return { 66 | url: await this.strip_reader_mode_url(url), 67 | title: title, 68 | description: info.selectionText || '' 69 | } 70 | }, 71 | 72 | // Opens a window for interacting with Pinboard 73 | async open_add_link_window(url) { 74 | const show_tags = await Preferences.get('show_tags') 75 | const bg_window = await browser.windows.getCurrent() 76 | const pin_window = await browser.windows.create({ 77 | url: url, 78 | type: 'popup', 79 | width: 750, 80 | height: show_tags ? 550 : 350, 81 | incognito: bg_window.incognito 82 | }) 83 | return pin_window 84 | }, 85 | 86 | // Open the Add Link form in a new tab 87 | async open_add_link_tab(url) { 88 | const active_tabs = await browser.tabs.query({currentWindow: true, active: true}) 89 | const opener_tab = active_tabs[0] 90 | const form_tab = await browser.tabs.create({ 91 | url: url, 92 | openerTabId: opener_tab.id 93 | }) 94 | return form_tab 95 | }, 96 | 97 | // Opens the Pinboard "Add Link" form 98 | async open_save_form(bookmark_info) { 99 | const endpoint = await Pinboard.get_endpoint('add_link', bookmark_info) 100 | const add_link_form_in_tab = await Preferences.get('add_link_form_in_tab') 101 | if (add_link_form_in_tab) { 102 | const tab = await this.open_add_link_tab(endpoint) 103 | this.close_save_form = async () => { 104 | await browser.tabs.remove(tab.id) 105 | } 106 | } else { 107 | const win = await this.open_add_link_window(endpoint) 108 | this.close_save_form = async () => { 109 | await browser.windows.remove(win.id) 110 | } 111 | } 112 | }, 113 | 114 | async close_save_form() { 115 | throw 'No close function defined.' 116 | }, 117 | 118 | // Saves the bookmark to read later 119 | async save_for_later(bookmark_info) { 120 | const endpoint = await Pinboard.get_endpoint('read_later', bookmark_info) 121 | const bg_window = await browser.windows.getCurrent() 122 | if (bg_window.incognito) { 123 | 124 | // In private mode we actually have to open a window, 125 | // because Firefox doesn't support split incognito mode 126 | // and gets confused about cookie jars. 127 | this.open_save_form(endpoint) 128 | 129 | } else { 130 | 131 | const http_response = await fetch(endpoint, {credentials: 'include'}) 132 | if (http_response.redirected && http_response.url.startsWith(await Pinboard.get_endpoint('login'))) { 133 | this.open_save_form(http_response.url) 134 | } else if (http_response.status !== 200 || http_response.ok !== true) { 135 | this.show_notification('FAILED TO ADD LINK. ARE YOU LOGGED-IN?', true) 136 | } else { 137 | this.show_notification('Saved to read later.') 138 | } 139 | 140 | } 141 | }, 142 | 143 | async save_tab_set() { 144 | const bg_window = browser.windows.getCurrent() 145 | if (bg_window.incognito) { 146 | this.show_notification("Due to a Firefox limitation, saving tab sets does not work in Private mode. Try normal mode!", true) 147 | return 148 | } 149 | 150 | const window_info = await browser.windows.getAll({populate: true, windowTypes: ['normal']}) 151 | let windows = [] 152 | for (let i = 0; i < window_info.length; i++) { 153 | const current_window_tabs = window_info[i].tabs 154 | let tabs = [] 155 | for (let j = 0; j < current_window_tabs.length; j++) { 156 | tabs.push({ 157 | title: current_window_tabs[j].title, 158 | url: await this.strip_reader_mode_url(current_window_tabs[j].url) 159 | }) 160 | } 161 | windows.push(tabs) 162 | } 163 | 164 | let payload = new FormData() 165 | payload.append('data', JSON.stringify({browser: 'ffox', windows: windows})) 166 | const http_response = await fetch(await Pinboard.get_endpoint('save_tabs'), {method: 'POST', body: payload, credentials: 'include'}) 167 | if (http_response.status !== 200 || http_response.ok !== true) { 168 | this.show_notification('FAILED TO SAVE TAB SET.', true) 169 | } else { 170 | browser.tabs.create({url: await Pinboard.get_endpoint('show_tabs')}) 171 | } 172 | }, 173 | 174 | async show_notification(message, force) { 175 | const show_notifications = await Preferences.get('show_notifications') 176 | if (force || show_notifications) { 177 | browser.notifications.create({ 178 | 'type': 'basic', 179 | 'title': 'Pinboard', 180 | 'message': message, 181 | 'iconUrl': 'icons/pinboard_icon_48.png' 182 | }) 183 | } 184 | }, 185 | 186 | async update_toolbar_button() { 187 | const pref = await Preferences.get('toolbar_button') 188 | if (pref != this.toolbar_button_state) { 189 | switch (pref) { 190 | 191 | case 'show_menu': 192 | browser.browserAction.setPopup({popup: 'popup_menu.html'}) 193 | browser.browserAction.setTitle({title: 'Add to Pinboard'}) 194 | break 195 | 196 | case 'save_dialog': 197 | browser.browserAction.setPopup({popup: ''}) 198 | browser.browserAction.setTitle({title: 'Add to Pinboard'}) 199 | break 200 | 201 | case 'read_later': 202 | browser.browserAction.setPopup({popup: ''}) 203 | browser.browserAction.setTitle({title: 'Add to Pinboard (read later)'}) 204 | break 205 | 206 | } 207 | this.toolbar_button_state = pref 208 | } 209 | }, 210 | 211 | async update_context_menu() { 212 | const add_context_menu_items = await Preferences.get('context_menu_items') 213 | if (add_context_menu_items) { 214 | browser.contextMenus.create({ 215 | id: 'save_dialog', 216 | title: 'Save...', 217 | contexts: ['link', 'page', 'selection'] 218 | }) 219 | browser.contextMenus.create({ 220 | id: 'read_later', 221 | title: 'Read later', 222 | contexts: ['link', 'page', 'selection'] 223 | }) 224 | browser.contextMenus.create({ 225 | id: 'save_tab_set', 226 | title: 'Save tab set...', 227 | contexts: ['page', 'selection'] 228 | }) 229 | } else { 230 | browser.contextMenus.removeAll() 231 | } 232 | }, 233 | 234 | async handle_message(message) { 235 | let bookmark_info 236 | switch (message) { 237 | case 'save_dialog': 238 | bookmark_info = await this.get_bookmark_info_from_current_tab() 239 | this.open_save_form(bookmark_info) 240 | break 241 | 242 | case 'read_later': 243 | bookmark_info = await this.get_bookmark_info_from_current_tab() 244 | this.save_for_later(bookmark_info) 245 | break 246 | 247 | case 'save_tab_set': 248 | this.save_tab_set() 249 | break 250 | 251 | case 'link_saved': 252 | this.close_save_form() 253 | this.show_notification('Link added to Pinboard') 254 | break 255 | } 256 | }, 257 | 258 | async handle_context_menu(info, tab) { 259 | let bookmark_info 260 | switch (info.menuItemId) { 261 | case 'save_dialog': 262 | bookmark_info = await this.get_bookmark_info_from_context_menu_target(info, tab) 263 | this.open_save_form(bookmark_info) 264 | break 265 | 266 | case 'read_later': 267 | bookmark_info = await this.get_bookmark_info_from_context_menu_target(info, tab) 268 | this.save_for_later(bookmark_info) 269 | break 270 | 271 | case 'save_tab_set': 272 | this.save_tab_set() 273 | break 274 | } 275 | }, 276 | 277 | async handle_preferences_changes(changes, area) { 278 | if (area !== Preferences.storage_area) { 279 | return 280 | } 281 | const key = Object.keys(changes).pop() 282 | switch (key) { 283 | case 'toolbar_button': 284 | this.update_toolbar_button() 285 | break 286 | case 'context_menu_items': 287 | this.update_context_menu() 288 | break 289 | } 290 | }, 291 | 292 | async handle_upgrade(details) { 293 | // Migrate user preferences from sync to local storage 294 | if (details.reason === 'update' && parseFloat(details.previousVersion) < 1.4) { 295 | await Preferences.migrate_to_local_storage() 296 | await this.update_toolbar_button() 297 | await this.update_context_menu() 298 | } 299 | } 300 | } 301 | 302 | // Attach message event handler 303 | browser.runtime.onMessage.addListener(message => { 304 | App.handle_message(message.event) 305 | }) 306 | 307 | // Toolbar button event handler 308 | browser.browserAction.onClicked.addListener(() => { 309 | App.handle_message(App.toolbar_button_state) 310 | }) 311 | 312 | // Keyboard shortcut event handler 313 | browser.commands.onCommand.addListener(command => {App.handle_message(command)}) 314 | 315 | // Context menu event handler 316 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 317 | App.handle_context_menu(info, tab) 318 | }) 319 | 320 | // Preferences event handler 321 | browser.storage.onChanged.addListener((changes, area) => {App.handle_preferences_changes(changes, area)}) 322 | 323 | // Version update listener 324 | browser.runtime.onInstalled.addListener(details => {App.handle_upgrade(details)}) 325 | 326 | // Apply preferences when loading extension 327 | App.update_toolbar_button() 328 | App.update_context_menu() 329 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Pinboard WebExtension", 4 | "short_name": "Pinboard", 5 | "version": "1.64", 6 | "description": "Easily add links to Pinboard. A remake of the official Pinboard extension.", 7 | "author": "George Pop", 8 | "homepage_url": "https://github.com/gapop/pinboard-webextension", 9 | "applications": { 10 | "gecko": { 11 | "id": "pinboard-webextension@helloworld.ro", 12 | "strict_min_version": "60.0" 13 | } 14 | }, 15 | "icons": { 16 | "48": "icons/pinboard_icon_48.png", 17 | "96": "icons/pinboard_icon_96.png", 18 | "200": "icons/pinboard_icon.png" 19 | }, 20 | "permissions": [ 21 | "*://pinboard.in/*", 22 | "storage", 23 | "contextMenus", 24 | "activeTab", 25 | "notifications", 26 | "tabs" 27 | ], 28 | "browser_action": { 29 | "default_icon": { 30 | "16": "icons/toolbar_icon_16.png", 31 | "32": "icons/toolbar_icon_32.png", 32 | "48": "icons/toolbar_icon_48.png", 33 | "96": "icons/toolbar_icon_96.png" 34 | }, 35 | "theme_icons": [ 36 | { 37 | "dark": "icons/toolbar_icon_16.png", 38 | "light": "icons/toolbar_icon_16_light.png", 39 | "size": 16 40 | }, 41 | { 42 | "dark": "icons/toolbar_icon_32.png", 43 | "light": "icons/toolbar_icon_32_light.png", 44 | "size": 32 45 | }, 46 | { 47 | "dark": "icons/toolbar_icon_48.png", 48 | "light": "icons/toolbar_icon_48_light.png", 49 | "size": 48 50 | }, 51 | { 52 | "dark": "icons/toolbar_icon_96.png", 53 | "light": "icons/toolbar_icon_96_light.png", 54 | "size": 96 55 | } 56 | ], 57 | "default_title": "Add to Pinboard", 58 | "default_popup": "popup_menu.html", 59 | "browser_style": true 60 | }, 61 | "background": { 62 | "scripts": ["preferences.js", "main.js"] 63 | }, 64 | "content_scripts": [ 65 | { 66 | "matches": [ 67 | "https://pinboard.in/add", 68 | "https://pinboard.in/add*later=yes*" 69 | ], 70 | "js": ["link_saved.js"] 71 | } 72 | ], 73 | "commands": { 74 | "save_dialog": { 75 | "description": "Save to Pinboard (opens dialog)" 76 | }, 77 | "read_later": { 78 | "description": "Save to Read later" 79 | }, 80 | "save_tab_set": { 81 | "description": "Save tab set" 82 | } 83 | }, 84 | "options_ui": { 85 | "page": "preferences_page.html", 86 | "browser_style": true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /popup_menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 14 | 15 | 16 |