├── .babelrc ├── .bowerrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── images │ ├── appicon_128.png │ ├── appicon_16.png │ ├── appicon_48.png │ ├── icon_black_16.png │ ├── icon_black_24.png │ ├── icon_black_32.png │ ├── icon_color_16.png │ ├── icon_color_24.png │ ├── icon_color_32.png │ ├── icon_colored_19.png │ ├── icon_grey_19.png │ └── icon_grey_saving_19.png ├── manifest.json ├── options.html ├── popup.html ├── scripts │ ├── background.js │ ├── chromereload.js │ ├── common.js │ ├── description.js │ ├── keywords_suggestions.js │ ├── notifications.js │ ├── options.js │ ├── pinboard.js │ ├── popup.js │ └── utils.js ├── styles │ ├── libs │ │ └── angular-csp.css │ ├── options.css │ └── popup.css └── tests │ ├── spec │ ├── BackgroundSpec.js │ ├── NotificationsSpec.js │ └── PinboardSpec.js │ └── tests.html ├── bower.json ├── gulpfile.babel.js ├── package-lock.json └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | node_modules 3 | temp 4 | .tmp 5 | dist 6 | .sass-cache 7 | app/bower_components 8 | test/bower_components 9 | package 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 15 4 | before_script: 5 | - npm install -g bower 6 | - npm install 7 | - bower install 8 | script: gulp build 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | See https://clvrobj.mit-license.org/ for specific license information. 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pinboard Plus [![Build Status](https://travis-ci.org/clvrobj/Pinboard-Plus.svg?branch=master)](https://travis-ci.org/clvrobj/Pinboard-Plus) 2 | ============= 3 | Pinboard Plus is a better Chrome extension for [Pinboard.in](http://pinboard.in). 4 | 5 | Easy to know current page has been saved or not. 6 | 7 | Features 8 | -------- 9 | 10 | * Icon changing to show current page has been saved or not 11 | * Add, modify and delete bookmarks from the popup window 12 | * Same UI style with Pinboard official site 13 | * Set `private` if in Incognito Mode 14 | 15 | 16 | Installation 17 | ------------ 18 | You can install in Chrome webstore: [Pinboard Plus](https://chrome.google.com/webstore/detail/mphdppdgoagghpmmhodmfajjlloijnbd) 19 | 20 | Development 21 | ----------- 22 | Software required for development: 23 | 24 | * [bower](https://bower.io/) 25 | * [npm](https://www.npmjs.com/) 26 | 27 | Install dependencies: 28 | 29 | ```bash 30 | $ npm install 31 | $ bower install 32 | ``` 33 | 34 | Follow the official instruction to [load the extenstion](https://developer.chrome.com/extensions/getstarted#unpacked). 35 | 36 | LiveReload: 37 | 38 | ```bash 39 | $ gulp watch 40 | ``` 41 | 42 | Build the extension: 43 | 44 | ```bash 45 | $ gulp build 46 | ``` 47 | 48 | For testing, open the `chrome-extension://[Extension ID]/tests/tests.html` in the browser to check the test results. 49 | 50 | ### [Contributors](https://github.com/clvrobj/Pinboard-Plus/graphs/contributors) 51 | -------------------------------------------------------------------------------- /app/images/appicon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/appicon_128.png -------------------------------------------------------------------------------- /app/images/appicon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/appicon_16.png -------------------------------------------------------------------------------- /app/images/appicon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/appicon_48.png -------------------------------------------------------------------------------- /app/images/icon_black_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_black_16.png -------------------------------------------------------------------------------- /app/images/icon_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_black_24.png -------------------------------------------------------------------------------- /app/images/icon_black_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_black_32.png -------------------------------------------------------------------------------- /app/images/icon_color_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_color_16.png -------------------------------------------------------------------------------- /app/images/icon_color_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_color_24.png -------------------------------------------------------------------------------- /app/images/icon_color_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_color_32.png -------------------------------------------------------------------------------- /app/images/icon_colored_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_colored_19.png -------------------------------------------------------------------------------- /app/images/icon_grey_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_grey_19.png -------------------------------------------------------------------------------- /app/images/icon_grey_saving_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clvrobj/Pinboard-Plus/32c7ddcd46c84ce5bda977fc47b66f7e8b3a0430/app/images/icon_grey_saving_19.png -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pinboard Plus", 3 | "version": "2.12.5", 4 | "description": "A better extension for Pinboard (http://pinboard.in). Easy to know current page has been saved or not.", 5 | "manifest_version": 2, 6 | "content_security_policy": "script-src 'self'; object-src 'self'", 7 | "browser_action": { 8 | "default_title": "Pinboard Plus", 9 | "default_icon": "images/icon_black_16.png", 10 | "default_popup": "popup.html" 11 | }, 12 | "permissions": [ 13 | "tabs", 14 | "http://*/*", 15 | "https://*/*" 16 | ], 17 | "background": { 18 | "scripts": [ 19 | "bower_components/jquery/dist/jquery.min.js", 20 | "bower_components/underscore/underscore-min.js", 21 | "scripts/common.js", 22 | "scripts/utils.js", 23 | "scripts/pinboard.js", 24 | "scripts/notifications.js", 25 | "scripts/chromereload.js", 26 | "scripts/background.js" 27 | ] 28 | }, 29 | "content_scripts": [{ 30 | "matches": ["http://*/*", "https://*/*"], 31 | "run_at": "document_start", 32 | "js": ["scripts/description.js", "scripts/keywords_suggestions.js"], 33 | "all_frames": true 34 | }], 35 | "icons": { 36 | "16": "images/appicon_16.png", 37 | "48": "images/appicon_48.png", 38 | "128": "images/appicon_128.png" 39 | }, 40 | "options_ui": { 41 | "page": "options.html", 42 | "open_in_tab": false 43 | }, 44 | "commands": { 45 | "_execute_browser_action": { 46 | "suggested_key": { 47 | "windows": "Ctrl+Shift+P", 48 | "mac": "Command+Shift+P", 49 | "chromeos": "Ctrl+Shift+P", 50 | "linux": "Ctrl+Shift+P" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/options.html: -------------------------------------------------------------------------------- 1 | 2 | Pinboard Plus Options 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | 17 | 18 |
19 | 20 | (Stop pinging the Pinboard API; Icon will be grey) 21 | 22 |

23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 |

32 | 33 | 34 |

35 |

36 | 37 | 38 |

39 |
40 | 41 | -------------------------------------------------------------------------------- /app/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
{{loadingText}}
15 |
16 | 19 |
20 | 32 |
33 |
34 | 35 |
36 | 47 |
48 | 97 |
98 |
99 |
100 | 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /app/scripts/background.js: -------------------------------------------------------------------------------- 1 | // {url: {title, desc, tag, time, isSaved[0: not saved; 1: saved; 2: saving]}} 2 | var pages = {}; 3 | 4 | var getPopup = function () { 5 | return chrome.extension.getViews({type: 'popup'})[0]; 6 | }; 7 | 8 | var logout = function () { 9 | Pinboard.logout(function () { 10 | var popup = getPopup(); 11 | popup && popup.$rootScope && 12 | popup.$rootScope.$broadcast('logged-out'); 13 | }); 14 | Notifications.clearAll(); 15 | }; 16 | 17 | var getUserInfo = function () { 18 | return Pinboard.getUserInfo(); 19 | }; 20 | 21 | // for popup.html to acquire page info 22 | // if there is no page info at local then get it from server 23 | var getPageInfo = function (url) { 24 | if (!url || url.indexOf('chrome://') == 0 || 25 | localStorage[nopingKey] === 'true') { 26 | return {url: url, isSaved:false}; 27 | } 28 | var pageInfo = pages[url]; 29 | if (pageInfo) { 30 | return pageInfo; 31 | } 32 | // download now 33 | updatePageInfo(url); 34 | return null; 35 | }; 36 | 37 | // refresh page info even page info has fetched from server 38 | var updatePageInfo = function (url) { 39 | var popup = getPopup(); 40 | popup && popup.$rootScope && 41 | popup.$rootScope.$broadcast('show-loading', 'Loading bookmark...'); 42 | var cb = function (pageInfo) { 43 | var popup = getPopup(); 44 | popup && popup.$rootScope && 45 | popup.$rootScope.$broadcast('render-page-info', pageInfo); 46 | updateSelectedTabExtIcon(); 47 | }; 48 | queryPinState({url: url, ready: cb}); 49 | }; 50 | 51 | var handleError = function (data) { 52 | var message; 53 | if (data.status == 0 || data.status == 500){ 54 | message = 'Please check your connection or Pinboard API is probably down.'; 55 | } if (data.status == 200 && data.responseText.includes('Pinboard is Down')) { 56 | message = 'Pinboard API is down.'; 57 | } else if (data.status == 401) { 58 | message = 'Something wrong with the auth. Please try to login again.'; 59 | } else { 60 | message = data.statusText || 'Something wrong'; 61 | } 62 | addAndShowNotification(message, 'error'); 63 | }; 64 | 65 | var addAndShowNotification = function (message, type) { 66 | Notifications.add(message, type); 67 | var popup = getPopup(); 68 | popup && popup.$rootScope && 69 | popup.$rootScope.$broadcast('show-notification'); 70 | }; 71 | 72 | var getNotification = function () { 73 | return Notifications.getTop(); 74 | }; 75 | 76 | var closeNotification = function () { 77 | Notifications.remove(); 78 | }; 79 | 80 | var login = function (token) { 81 | Pinboard.login( 82 | token, 83 | function (data) { 84 | var popup = getPopup(); 85 | if (data.result) { 86 | popup && popup.$rootScope && 87 | popup.$rootScope.$broadcast('login-succeed'); 88 | _getTags(); 89 | } else { 90 | // login error 91 | addAndShowNotification( 92 | 'Login Failed. The token format is user:TOKEN.', 'error'); 93 | } 94 | }, 95 | function (data) { 96 | var popup = getPopup(); 97 | if (data.status == 401 || data.status == 500) { 98 | addAndShowNotification( 99 | 'Login Failed. The token format is user:TOKEN.', 'error'); 100 | } else { 101 | handleError(data); 102 | } 103 | } 104 | ); 105 | }; 106 | 107 | 108 | var QUERY_INTERVAL = 3 * 1000, isQuerying = false, tQuery; 109 | var queryPinState = function (info) { 110 | var url = info.url, 111 | done = function (data) { 112 | isQuerying = false; 113 | clearTimeout(tQuery); 114 | var posts = data.posts, 115 | pageInfo = {isSaved: false}; 116 | if (posts.length) { 117 | var post = posts[0]; 118 | pageInfo = {url: post.href, 119 | title: post.description, 120 | desc: post.extended, 121 | tag: post.tags, 122 | time: post.time, 123 | shared: post.shared == 'no' ? false:true, 124 | toread: post.toread == 'yes' ? true:false, 125 | isSaved: true}; 126 | } 127 | pages[url] = pageInfo; 128 | info.ready && info.ready(pageInfo); 129 | }; 130 | if ((info.isForce || !isQuerying) && Pinboard.isLoggedin() && 131 | info.url && info.url != 'chrome://newtab/') { 132 | isQuerying = true; 133 | clearTimeout(tQuery); 134 | tQuery = setTimeout(function () { 135 | // to make the queries less frequently 136 | isQuerying = false; 137 | }, QUERY_INTERVAL); 138 | // The queryPinState is high frequently called 139 | // but without risk of lost of user data, it's OK to ignore error use noop 140 | Pinboard.queryPinState(url, done, $.noop); 141 | } 142 | }; 143 | 144 | var updateSelectedTabExtIcon = function () { 145 | chrome.tabs.query({active:true, currentWindow: true}, function (activetabs) { 146 | var tab = activetabs[0]; 147 | var pageInfo = pages[tab.url]; 148 | var iconPath = noIcon; 149 | if (pageInfo && pageInfo.isSaved == 1) { 150 | iconPath = yesIcon; 151 | } else if (pageInfo && pageInfo.isSaved == 2) { 152 | iconPath = savingIcon; 153 | } 154 | chrome.browserAction.setIcon( 155 | {path: iconPath, tabId: tab.id}); 156 | }); 157 | }; 158 | 159 | var addPost = function (info) { 160 | if (Pinboard.isLoggedin && info.url && info.title) { 161 | var url = info.url, title = info.title, desc = info.desc; 162 | if (desc.length > maxDescLen) { 163 | desc = desc.slice(0, maxDescLen) + '...'; 164 | } 165 | var doneFn = function (data) { 166 | var resCode = data.result_code; 167 | if (pages[url]) { 168 | pages[url].isSaved = resCode == 'done' ? true : false; 169 | } else { 170 | pages[url] = {isSaved: resCode == 'done' ? true : false}; 171 | } 172 | updateSelectedTabExtIcon(); 173 | queryPinState({url: url, isForce: true}); 174 | var popup = getPopup(); 175 | popup && popup.close(); 176 | }; 177 | var failFn = function (data) { 178 | if (pages[url]) { 179 | pages[url].isSaved = 0; 180 | } else { 181 | pages[url] = {isSaved: 0}; 182 | } 183 | updateSelectedTabExtIcon(); 184 | var saveFailedMsg, failReason; 185 | if (title.length > 47) { 186 | var _title = title.slice(0, 47) + '...'; 187 | saveFailedMsg = 'The post ' + _title + ' is not saved. '; 188 | } else { 189 | saveFailedMsg = 'The post ' + title + ' is not saved. '; 190 | } 191 | if (data.status == 0 || data.status == 500){ 192 | failReason = 'Please check your connection or Pinboard' + 193 | ' API is probably down.'; 194 | } if (data.status == 200 && data.responseText.includes('Pinboard is Down')) { 195 | failReason = 'Pinboard API is down.'; 196 | } else if (data.status == 401) { 197 | failReason = 'Something wrong with the auth. Please try to login again.'; 198 | } else { 199 | failReason = data.statusText || 'Something wrong.'; 200 | } 201 | var message = saveFailedMsg + failReason; 202 | // only store error and no need to show as popup is close 203 | Notifications.add(message, 'error'); 204 | }; 205 | Pinboard.addPost(info.title, info.url, desc, info.tag, 206 | info.shared, info.toread, doneFn, failFn); 207 | // change icon state 208 | if (pages[info.url]) { 209 | pages[info.url].isSaved = 2; 210 | } else { 211 | pages[info.url] = {isSaved: 2}; 212 | } 213 | updateSelectedTabExtIcon(); 214 | // add new tags into _tags 215 | if (info.tag) { 216 | _updateTags(info.tag.split(' ')); 217 | } 218 | } 219 | }; 220 | 221 | var deletePost = function (url) { 222 | if (Pinboard.isLoggedin() && url) { 223 | var doneFn = function (data) { 224 | var resCode = data.result_code; 225 | var popup = getPopup(); 226 | if (resCode == 'done' || resCode == 'item not found') { 227 | delete pages[url]; 228 | updateSelectedTabExtIcon(); 229 | } else { 230 | // error 231 | popup && popup.$rootScope && 232 | popup.$rootScope.$broadcast('error'); 233 | } 234 | popup && popup.close(); 235 | }; 236 | Pinboard.deletePost(url, doneFn, handleError); 237 | } 238 | }; 239 | 240 | var getSuggest = function (url, keywordTags) { 241 | if (Pinboard.isLoggedin() && url) { 242 | var doneFn = function (data) { 243 | var popularTags = [], recommendedTags = []; 244 | if (data && data.length > 0) { 245 | popularTags = data[0].popular; 246 | recommendedTags = data[1].recommended; 247 | } 248 | // default to popluar tags, add new recommended tags 249 | var suggests = popularTags.slice(); 250 | $.each(recommendedTags, function(index, tag){ 251 | if(popularTags.indexOf(tag) === -1){ 252 | suggests.push(tag); 253 | } 254 | }); 255 | 256 | if (keywordTags !== undefined && keywordTags.length !== 0) { 257 | suggests = suggests.concat(keywordTags); 258 | suggests = suggests.sort().filter(function(item, pos, ary) {return !pos || item != ary[pos - 1];}); 259 | } 260 | 261 | var popup = getPopup(); 262 | popup && popup.$rootScope && popup.$rootScope.$broadcast('render-suggests', suggests); 263 | }; 264 | Pinboard.getSuggest(url, doneFn); 265 | } 266 | }; 267 | 268 | var _tags = []; 269 | // acquire all user tags from server refresh _tags 270 | var _getTags = function () { 271 | if (Pinboard.isLoggedin()) { 272 | var doneFn = function (data) { 273 | if (data) { 274 | _tags = _.sortBy(_.keys(data), 275 | function (tag) { 276 | return data[tag].count; 277 | }).reverse(); 278 | } 279 | }; 280 | Pinboard.getTags(doneFn); 281 | } 282 | }; 283 | _getTags(); 284 | 285 | // add new tags into _tags 286 | var _updateTags = function (tags) { 287 | var newTags = _.difference(tags, _tags); 288 | if (newTags.length > 0) { 289 | _tags.push.apply(_tags, newTags) 290 | } 291 | }; 292 | 293 | var getTags = function () { 294 | if (!_tags || _tags.length === 0) { 295 | _getTags(); 296 | } 297 | return _tags; 298 | }; 299 | 300 | Notifications.init(); 301 | 302 | // query at first time extension loaded 303 | chrome.tabs.query({active:true, currentWindow: true}, function (activetabs) { 304 | var tab = activetabs[0]; 305 | if (localStorage[nopingKey] === 'true') { 306 | return; 307 | } 308 | queryPinState({url: tab.url, 309 | ready: function (pageInfo) { 310 | if (pageInfo && pageInfo.isSaved) { 311 | chrome.browserAction.setIcon( 312 | {path: yesIcon, tabId: tab.id}); 313 | } 314 | }}); 315 | }); 316 | 317 | chrome.tabs.onUpdated.addListener( 318 | function(id, changeInfo, tab) { 319 | if (localStorage[nopingKey] === 'true') { 320 | return; 321 | } 322 | if (changeInfo.url) { 323 | var url = changeInfo.url; 324 | if (!pages.hasOwnProperty(url)) { 325 | chrome.browserAction.setIcon({path: noIcon, tabId: tab.id}); 326 | queryPinState({url: url, 327 | ready: function (pageInfo) { 328 | if (pageInfo && pageInfo.isSaved) { 329 | chrome.browserAction.setIcon( 330 | {path: yesIcon, tabId: tab.id}); 331 | } 332 | }}); 333 | } 334 | } 335 | var url = changeInfo.url || tab.url; 336 | if (pages[url] && pages[url].isSaved) { 337 | chrome.browserAction.setIcon({path: yesIcon, tabId: tab.id}); 338 | } 339 | } 340 | ); 341 | 342 | chrome.tabs.onActivated.addListener( 343 | function(tabId, selectInfo) { 344 | if (localStorage[nopingKey] === 'true') { 345 | return; 346 | } 347 | chrome.tabs.query( 348 | {active:true, currentWindow: true}, function (activetabs) { 349 | var tab = activetabs[0]; 350 | var url = tab.url; 351 | if (!pages.hasOwnProperty(url)) { 352 | queryPinState({url: url, 353 | ready: function (pageInfo) { 354 | if (pageInfo && pageInfo.isSaved) { 355 | chrome.browserAction.setIcon( 356 | {path: yesIcon, tabId: tab.id}); 357 | } 358 | }}); 359 | } 360 | }); 361 | } 362 | ); 363 | -------------------------------------------------------------------------------- /app/scripts/chromereload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Reload client for Chrome Apps & Extensions. 4 | // The reload client has a compatibility with livereload. 5 | // WARNING: only supports reload command. 6 | 7 | var LIVERELOAD_HOST = 'localhost:'; 8 | var LIVERELOAD_PORT = 35729; 9 | var connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload'); 10 | 11 | connection.onerror = function (error) { 12 | console.log('reload connection got error:', error); 13 | }; 14 | 15 | connection.onmessage = function (e) { 16 | if (e.data) { 17 | var data = JSON.parse(e.data); 18 | if (data && data.command === 'reload') { 19 | chrome.runtime.reload(); 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /app/scripts/common.js: -------------------------------------------------------------------------------- 1 | // userInfo: name, pwd, isChecked 2 | var _userInfo = null, _tags = [], keyPrefix = 'pbuinfo', 3 | checkedkey = keyPrefix + 'c', 4 | namekey = keyPrefix + 'n', pwdkey = keyPrefix + 'p', 5 | authTokenKey = keyPrefix + '_auth_token', 6 | 7 | // config in the settings for not checking page pin state 8 | nopingKey = keyPrefix + 'np', 9 | // config in the settings for always check the private checkbox 10 | allprivateKey = keyPrefix + 'allprivate', 11 | // config in the settings for always check the "to read" checkbox 12 | alltoreadKey = keyPrefix + 'alltoread', 13 | // config in the settings for wrapping text with
14 | noblockquoteKey = keyPrefix + 'noblockquote', 15 | // config in the settings for enabling 'private' when in incognito mode 16 | privateWhenIncognitoKey = keyPrefix + 'privatewhenincognito', 17 | 18 | mainPath = 'https://api.pinboard.in/v1/', 19 | 20 | yesIcon = 'images/icon_color_16.png', 21 | noIcon = 'images/icon_black_16.png', 22 | savingIcon = 'images/icon_grey_saving_19.png'; 23 | 24 | var REQ_TIME_OUT = 125 * 1000, maxDescLen = 500; 25 | -------------------------------------------------------------------------------- /app/scripts/description.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener( 2 | function(message, sender, sendResponse) { 3 | if (message.method == 'getDescription') { 4 | var description = window.getSelection().toString(); 5 | if (!description || description == '') { 6 | var metas = document.getElementsByTagName('meta'); 7 | for (i=0; i 0; }).sort().filter(function(item, pos, ary) {return !pos || item != ary[pos - 1];}); 8 | break; 9 | } 10 | } 11 | sendResponse({data: tags}); 12 | } 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /app/scripts/notifications.js: -------------------------------------------------------------------------------- 1 | var Notifications = { 2 | init : function () { 3 | var strNotis = localStorage['notifications']; 4 | if (strNotis) { 5 | this.notis = JSON.parse(strNotis); 6 | } else { 7 | this.notis = []; 8 | } 9 | }, 10 | findNotification: function (messageKey) { 11 | for (var i=0; i < this.notis.length; i++) { 12 | if (this.notis[i].message === messageKey) { 13 | return i; 14 | } 15 | } 16 | return -1; 17 | }, 18 | add: function (message, type) { 19 | type = typeof type !== 'undefined' ? type : 'error'; 20 | // find if there is the item exists 21 | var ind = this.findNotification(message); 22 | if (ind >= 0) { 23 | // to put the dup item to the top 24 | this.notis.splice(ind, 1); 25 | } 26 | this.notis.push({message:message, type:type}); 27 | localStorage['notifications'] = JSON.stringify(this.notis); 28 | }, 29 | remove: function () { 30 | if (this.notis && this.notis.length) { 31 | this.notis.splice(-1,1); 32 | localStorage['notifications'] = JSON.stringify(this.notis); 33 | } 34 | }, 35 | getTop: function () { 36 | return this.notis.length ? this.notis[this.notis.length - 1] : null; 37 | }, 38 | clearAll: function () { 39 | this.notis = []; 40 | localStorage.removeItem('notifications'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /app/scripts/options.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // localStorage only stores string values 3 | $('#no-ping').attr('checked', localStorage[nopingKey] === 'true').click( 4 | function () { 5 | var value = $(this).is(':checked'); 6 | localStorage[nopingKey] = value; 7 | }); 8 | 9 | $('#all-private').attr( 10 | 'checked', localStorage[allprivateKey] === 'true').click( 11 | function () { 12 | var value = $(this).is(':checked'); 13 | localStorage[allprivateKey] = value; 14 | }); 15 | 16 | $('#private-when-incognito').attr( 17 | 'checked', localStorage[privateWhenIncognitoKey] === 'true').click( 18 | function () { 19 | var value = $(this).is(':checked'); 20 | localStorage[privateWhenIncognitoKey] = value; 21 | }); 22 | 23 | $('#all-toread').attr( 24 | 'checked', localStorage[alltoreadKey] === 'true').click( 25 | function () { 26 | var value = $(this).is(':checked'); 27 | localStorage[alltoreadKey] = value; 28 | }); 29 | 30 | $('#use-blockquote').attr('checked', isBlockquote()).click( 31 | function () { 32 | var value = $(this).is(':checked'); 33 | localStorage[noblockquoteKey] = !value; 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /app/scripts/pinboard.js: -------------------------------------------------------------------------------- 1 | var Pinboard = { 2 | _userInfo: {isChecked: false, authToken: '', 3 | name: '', pwd: ''}, 4 | getUserInfo: function () { 5 | if (this._userInfo.isChecked == false) { 6 | if (localStorage[checkedkey]) { 7 | this._userInfo = {isChecked: localStorage[checkedkey], 8 | authToken: localStorage[authTokenKey], 9 | name: localStorage[namekey], 10 | pwd: localStorage[pwdkey]}; 11 | } 12 | } 13 | return this._userInfo; 14 | }, 15 | login: function (token, done, fail) { 16 | // test auth 17 | var path = mainPath + 'user/api_token'; 18 | $.ajax({ 19 | url: path, 20 | data: {format: 'json', auth_token: token}, 21 | type : 'GET', 22 | timeout: REQ_TIME_OUT, 23 | dataType: 'json', 24 | crossDomain: true, 25 | contentType:'text/plain' 26 | }).done(function (data) { 27 | if (data.result) { 28 | Pinboard._userInfo.authToken = token; 29 | Pinboard._userInfo.name = token.split(':')[0]; 30 | Pinboard._userInfo.isChecked = true; 31 | localStorage[namekey] = token.split(':')[0]; 32 | localStorage[authTokenKey] = token; 33 | localStorage[checkedkey] = true; 34 | } 35 | done(data); 36 | }).fail( 37 | function (data) { 38 | fail(data); 39 | } 40 | ); 41 | }, 42 | logout: function (callback) { 43 | this._userInfo.isChecked = false; 44 | this._userInfo.name = ''; 45 | this._userInfo.authToken = ''; 46 | [checkedkey, namekey, pwdkey, authTokenKey, nopingKey].forEach( 47 | function (key) { 48 | localStorage.removeItem(key); 49 | } 50 | ); 51 | callback(); 52 | }, 53 | isLoggedin: function () { 54 | return this._userInfo && this._userInfo.isChecked; 55 | }, 56 | queryPinState: function (url, done, fail) { 57 | var path = mainPath + 'posts/get'; 58 | var settings = {url: path, 59 | type : 'GET', 60 | data: {url: url, format: 'json', 61 | auth_token: this._userInfo.authToken}, 62 | //timeout: REQ_TIME_OUT, 63 | dataType: 'json', 64 | crossDomain: true, 65 | contentType:'text/plain'}; 66 | $.ajax(settings).done( 67 | function (data) { 68 | done(data); 69 | } 70 | ).fail( 71 | function (data) { 72 | fail(data); 73 | } 74 | ); 75 | }, 76 | addPost: function (title, url, desc, tags, shared, toread, done, fail) { 77 | var path = mainPath + 'posts/add'; 78 | var data = {description: title, url: url, 79 | extended: desc, tags: tags, 80 | shared: shared, toread: toread, 81 | auth_token: this._userInfo.authToken, 82 | format: 'json'}; 83 | var settings = {url: path, 84 | type : 'GET', 85 | timeout: REQ_TIME_OUT, 86 | dataType: 'json', 87 | crossDomain: true, 88 | data: data, 89 | contentType:'text/plain'}; 90 | $.ajax(settings).done( 91 | function (data) { 92 | done(data); 93 | } 94 | ).fail( 95 | function (data) { 96 | fail(data); 97 | } 98 | ); 99 | }, 100 | deletePost: function (url, done, fail) { 101 | var path = mainPath + 'posts/delete'; 102 | var settings = {url: path, 103 | type : 'GET', 104 | timeout: REQ_TIME_OUT, 105 | dataType: 'json', 106 | crossDomain: true, 107 | data: { 108 | url: url, format: 'json', 109 | auth_token: this._userInfo.authToken 110 | }, 111 | contentType: 'text/plain'}; 112 | $.ajax(settings).done( 113 | function (data) { 114 | done(data); 115 | } 116 | ).fail( 117 | function (data) { 118 | fail(data); 119 | } 120 | ); 121 | }, 122 | getSuggest: function (url, done) { 123 | var path = mainPath + 'posts/suggest'; 124 | var settings = {url: path, 125 | type : 'GET', 126 | data: { 127 | url: url, format: 'json', 128 | auth_token: this._userInfo.authToken 129 | }, 130 | timeout: REQ_TIME_OUT, 131 | dataType: 'json', 132 | crossDomain: true, 133 | contentType:'text/plain'}; 134 | $.ajax(settings).done( 135 | function (data) { 136 | done(data); 137 | } 138 | ); 139 | }, 140 | getTags: function (done) { 141 | var path = mainPath + 'tags/get'; 142 | var settings = {url: path, 143 | type : 'GET', 144 | data: {auth_token: this._userInfo.authToken, 145 | format: 'json'}, 146 | timeout: REQ_TIME_OUT, 147 | dataType: 'json', 148 | crossDomain: true, 149 | contentType:'text/plain'}; 150 | $.ajax(settings).done( 151 | function (data) { 152 | done(data); 153 | } 154 | ); 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /app/scripts/popup.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('popupApp', ['ngNotify']); 2 | 3 | app.run([ 4 | 'ngNotify', 5 | function(ngNotify) { 6 | ngNotify.config({ 7 | position: 'bottom', 8 | theme: 'notificationTheme', 9 | html: true, 10 | sticky: 'true' 11 | }); 12 | } 13 | ]); 14 | 15 | app.controller( 16 | 'PopupCtrl', 17 | ['$rootScope', '$scope', '$window', 'ngNotify', 18 | function($rootScope, $scope, $window, ngNotify) { 19 | var bg = chrome.extension.getBackgroundPage(), 20 | keyCode = {enter:13, tab:9, up:38, down:40, ctrl:17, n:78, p:80, space:32}, 21 | SEC = 1000, MIN = SEC*60, HOUR = MIN*60, DAY = HOUR*24, WEEK = DAY*7; 22 | Date.prototype.getTimePassed = function () { 23 | var ret = {day: 0, hour: 0, min: 0, sec: 0, offset: -1}, 24 | offset = new Date() - this, r; 25 | if (offset<=0) return ret; 26 | ret.offset = offset; 27 | ret.week = Math.floor(offset/WEEK); r = offset%WEEK; 28 | ret.day = Math.floor(offset/DAY); r = offset%DAY; 29 | ret.hour = Math.floor(r/HOUR); r = r%HOUR; 30 | ret.min = Math.floor(r/MIN); r = r%MIN; 31 | ret.sec = Math.floor(r/SEC); 32 | return ret; 33 | }; 34 | 35 | $window.$rootScope = $rootScope; 36 | 37 | $scope.$on('login-succeed', function () { 38 | renderPageInfo(); 39 | }); 40 | 41 | $scope.$on('logged-out', function () { 42 | $scope.isAnony = true; 43 | $scope.isLoading = false; 44 | $scope.$apply(); 45 | }); 46 | 47 | $scope.$on('show-loading', function (e, loadingText) { 48 | $scope.isLoading = true; 49 | $scope.loadingText = loadingText || 'Loading...'; 50 | $scope.$apply(); 51 | }); 52 | 53 | var copySelOrMetaToDesc = function () { 54 | chrome.tabs.query( 55 | {active:true, currentWindow: true}, function (activetabs) { 56 | var tab = activetabs[0]; 57 | chrome.tabs.sendMessage( 58 | tab.id, {method: 'getDescription'}, 59 | function (response) { 60 | if (typeof response !== 'undefined' && 61 | response.data.length !== 0) { 62 | var desc = response.data; 63 | if (desc.length > maxDescLen) { 64 | desc = desc.slice(0, maxDescLen) + '...'; 65 | } 66 | if (isBlockquote()) { 67 | desc = '
' + desc + '
'; 68 | $scope.isQuoteHintShown = true; 69 | } 70 | $scope.pageInfo.desc = desc; 71 | $scope.$apply(); 72 | } 73 | }); 74 | }); 75 | }; 76 | 77 | var getKeywordsSuggestionTags = function () { 78 | chrome.tabs.query( 79 | {active:true, currentWindow: true}, function (activetabs) { 80 | var tab = activetabs[0]; 81 | chrome.tabs.sendMessage( 82 | tab.id, {method: 'getKeywordsSuggestionTags'}, 83 | function (response) { 84 | if (typeof response !== 'undefined') { 85 | var tags = response.data; 86 | bg.getSuggest(tab.url, tags); 87 | } 88 | else { 89 | bg.getSuggest(tab.url); 90 | } 91 | }); 92 | }); 93 | }; 94 | 95 | var initAutoComplete = function () { 96 | var tags = bg.getTags(); 97 | if (tags && tags.length) { 98 | $scope.allTags = tags; 99 | } 100 | }; 101 | 102 | var renderPageInfo = function () { 103 | chrome.tabs.query({active:true, currentWindow: true}, function (activetabs) { 104 | var tab = activetabs[0]; 105 | var pageInfo = bg.getPageInfo(tab.url); 106 | if (!pageInfo || pageInfo.isSaved == false) { 107 | pageInfo = {url: tab.url, title: tab.title, 108 | tag: '', desc: ''}; 109 | pageInfo.shared = (localStorage[allprivateKey] !== 'true') 110 | && !((localStorage[privateWhenIncognitoKey] === 'true') && tab.incognito); 111 | pageInfo.isPrivate = !pageInfo.shared; 112 | pageInfo.toread = (localStorage[alltoreadKey] === 'true'); 113 | pageInfo.isSaved = false; 114 | } 115 | if (pageInfo.tag) { 116 | pageInfo.tag = pageInfo.tag.concat(' '); 117 | } 118 | pageInfo.isPrivate = !pageInfo.shared; 119 | $scope.pageInfo = _.clone(pageInfo); // do not deep copy 120 | if (!pageInfo.desc) { 121 | copySelOrMetaToDesc(); 122 | } 123 | $scope.isLoading = false; 124 | $scope.isAnony = false; 125 | $scope.$apply(); 126 | //bg.getSuggest(tab.url); 127 | getKeywordsSuggestionTags(); 128 | initAutoComplete(); 129 | if (location.search != '?focusHack') { 130 | location.search = '?focusHack'; 131 | } 132 | }); 133 | }; 134 | 135 | $scope.chooseTag = function (e) { 136 | var code = e.charCode? e.charCode : e.keyCode; 137 | if (code && 138 | _.indexOf([keyCode.enter, keyCode.tab, keyCode.up, keyCode.down, 139 | keyCode.n, keyCode.p, keyCode.ctrl, keyCode.space], 140 | code) >= 0) { 141 | if (code == keyCode.enter || code == keyCode.tab) { 142 | if ($scope.isShowAutoComplete) { 143 | e.preventDefault(); 144 | // submit tag 145 | var items = $scope.pageInfo.tag.split(' '), 146 | tag = $scope.autoCompleteItems[$scope.activeItemIndex]; 147 | items.splice(items.length - 1, 1, tag.text); 148 | $scope.pageInfo.tag = items.join(' ') + ' '; 149 | $scope.isShowAutoComplete = false; 150 | } else if (code == keyCode.enter) { 151 | $scope.postSubmit(); 152 | } 153 | } else if (code == keyCode.down || 154 | (code == keyCode.n && e.ctrlKey == true)) { 155 | // move up one item 156 | e.preventDefault(); 157 | var idx = $scope.activeItemIndex + 1; 158 | if (idx >= $scope.autoCompleteItems.length) { 159 | idx = 0; 160 | } 161 | $scope.autoCompleteItems = 162 | _.map($scope.autoCompleteItems, 163 | function (item) { 164 | return {text:item.text, isActive:false}; 165 | }); 166 | $scope.activeItemIndex = idx; 167 | $scope.autoCompleteItems[idx].isActive = true; 168 | } else if (code == keyCode.up || 169 | (code == keyCode.p && e.ctrlKey == true)) { 170 | // move down one item 171 | e.preventDefault(); 172 | var idx = $scope.activeItemIndex - 1; 173 | if (idx < 0) { 174 | idx = $scope.autoCompleteItems.length - 1; 175 | } 176 | $scope.autoCompleteItems = 177 | _.map($scope.autoCompleteItems, 178 | function (item) { 179 | return {text:item.text, isActive:false}; 180 | }); 181 | $scope.activeItemIndex = idx; 182 | $scope.autoCompleteItems[idx].isActive = true; 183 | } else if (code == keyCode.space) { 184 | $scope.isShowAutoComplete = false; 185 | } 186 | } 187 | }; 188 | 189 | $scope.showAutoComplete = function () { 190 | var items = $scope.pageInfo.tag.split(' '), 191 | word = items[items.length - 1], 192 | MAX_SHOWN_ITEMS = 7; 193 | if (word) { 194 | word = word.toLowerCase(); 195 | if (typeof $scope.allTags === 'undefined') { 196 | initAutoComplete(); 197 | } 198 | var allTags = $scope.allTags, 199 | shownCount = 0, autoCompleteItems = []; 200 | for (var i=0, len=allTags.length; i WEEK) { 268 | dispStr = dispStr.concat(passed.week, ' ', 'weeks ago'); 269 | } else if (passed.offset > DAY) { 270 | dispStr = dispStr.concat(passed.day, ' ', 'days ago'); 271 | } else if (passed.offset > HOUR){ 272 | dispStr = dispStr.concat(passed.hour, ' ', 'hours ago'); 273 | } else { 274 | dispStr = dispStr.concat('just now'); 275 | } 276 | return dispStr; 277 | }; 278 | 279 | $scope.postSubmit = function () { 280 | $scope.isLoading = true; 281 | $scope.loadingText = 'Saving...'; 282 | var info = _.clone($scope.pageInfo); 283 | info.shared = $scope.pageInfo.isPrivate ? 'no' : 'yes'; 284 | info.toread = $scope.pageInfo.toread ? 'yes' : 'no'; 285 | bg.addPost(info); 286 | window.close(); 287 | }; 288 | 289 | $scope.showDeleteConfirm = function () { 290 | $scope.isShowDeleteConfirm = true; 291 | }; 292 | 293 | $scope.postDelete = function () { 294 | $scope.loadingText = 'Deleting...'; 295 | chrome.tabs.query( 296 | {active:true, currentWindow: true}, function (activetabs) { 297 | var tab = activetabs[0]; 298 | bg.deletePost(tab.url); 299 | }); 300 | }; 301 | 302 | $scope.loginSubmit = function () { 303 | if ($scope.userLogin.authToken) { 304 | $scope.loadingText = 'Logging in...'; 305 | $scope.isLoading = true; 306 | bg.login($scope.userLogin.authToken); 307 | } 308 | }; 309 | 310 | $scope.logout = function () { 311 | bg.logout(); 312 | }; 313 | 314 | var showNotification = function () { 315 | var noti = bg.getNotification(); 316 | if (noti) { 317 | ngNotify.set(noti.message, { 318 | html: true, 319 | type: noti.type 320 | }, bg.closeNotification); 321 | } 322 | $scope.isLoading = false; 323 | }; 324 | $scope.$on('show-notification', showNotification); 325 | 326 | var userInfo = bg.getUserInfo(); 327 | $scope.userInfo = userInfo; 328 | $scope.isAnony = !userInfo || !userInfo.isChecked; 329 | $scope.isLoading = false; 330 | $scope.loadingText = 'Loading...'; 331 | if (!$scope.isAnony) { 332 | renderPageInfo(); 333 | } 334 | ngNotify.addTheme('notificationTheme', 'notification-theme'); 335 | showNotification(); 336 | } 337 | ] 338 | ); 339 | -------------------------------------------------------------------------------- /app/scripts/utils.js: -------------------------------------------------------------------------------- 1 | var isBlockquote = function () { 2 | var noBlockquote = localStorage[noblockquoteKey]; 3 | return typeof noBlockquote == 'undefined' || noBlockquote === 'false'; 4 | }; 5 | -------------------------------------------------------------------------------- /app/styles/libs/angular-csp.css: -------------------------------------------------------------------------------- 1 | /* Include this file in your html if you are using the CSP mode. */ 2 | 3 | @charset "UTF-8"; 4 | 5 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], 6 | .ng-cloak, .x-ng-cloak, 7 | .ng-hide { 8 | display: none !important; 9 | } 10 | 11 | ng\:form { 12 | display: block; 13 | } 14 | -------------------------------------------------------------------------------- /app/styles/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-family: helvetica; 4 | font-size: 12px; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | header, section { 10 | padding: .5em 5%; 11 | } 12 | header { 13 | background: #efefef; 14 | border-bottom: 1px solid #ccc; 15 | } 16 | header + section { 17 | box-shadow: inset 0px 1px 2px rgba(0, 0, 0, 0.2); 18 | } 19 | 20 | span.checkbox { 21 | display: block; 22 | float: left; 23 | width: 3em; 24 | height: 3em; 25 | vertical-align: top; 26 | } 27 | 28 | .meta { 29 | color: #999; 30 | } 31 | 32 | p { 33 | height: 25px; 34 | } 35 | -------------------------------------------------------------------------------- /app/styles/popup.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 328px; 3 | } 4 | 5 | body { 6 | width: 480px; 7 | height: 305px; 8 | color: #333; 9 | font-family: helvetica; 10 | font-size: 12px; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: #11A; 16 | } 17 | 18 | ul { 19 | list-style: none; 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | ul li { 25 | margin: 0 0 10px 50px; 26 | list-style-type: none; 27 | } 28 | 29 | ul .item-title { 30 | text-align: right; 31 | width: 3.2em; 32 | display: inline-block; 33 | margin: 5px 0 0 -46px; 34 | display: block; 35 | text-align: right; 36 | position: absolute; 37 | } 38 | 39 | #suggest { 40 | display: inline-block; 41 | margin: 5px 0 0 5px; 42 | } 43 | 44 | #suggest a { 45 | margin: 0 3px; 46 | float: left; 47 | } 48 | 49 | #suggest a.selected { 50 | color: #BEBEBE 51 | } 52 | 53 | #suggest a.preexisting { 54 | text-decoration: underline; 55 | } 56 | 57 | #suggest a#add-all { 58 | color: #888 59 | } 60 | 61 | #state-mask { 62 | position: absolute; 63 | width: 100%; 64 | margin-top: 25px; 65 | height: 275px; 66 | background-color: #FFF; 67 | opacity: 0.7; 68 | z-index: 20; 69 | } 70 | 71 | #error-info-bar { 72 | position: absolute; 73 | width: 100%; 74 | height: 25px; 75 | margin: 0 95px; 76 | color: red; 77 | background-color: #FFF; 78 | opacity: 0.8; 79 | z-index: 30; 80 | } 81 | 82 | #login-window { 83 | position: absolute; 84 | width: 100%; 85 | height: 240px; 86 | background-color: #FFF; 87 | z-index: 10; 88 | } 89 | 90 | #login-window .meta { 91 | margin: 5px 0 0 64px; 92 | color: #999; 93 | } 94 | 95 | #header { 96 | height: 25px; 97 | color: #AAA; 98 | } 99 | 100 | #header .logout { 101 | position: absolute; 102 | right: 5px; 103 | } 104 | 105 | #header a { 106 | color: #AAA; 107 | } 108 | 109 | #header a:hover { 110 | color: #11A; 111 | } 112 | 113 | #header .logo { 114 | float: left; 115 | padding-top: 2px; 116 | } 117 | 118 | .logo-unlogin { 119 | float: left; 120 | position: absolute; 121 | width: 100%; 122 | background-color: #FFF; 123 | } 124 | 125 | #header .random { 126 | color: #BBB; 127 | } 128 | 129 | .alert { 130 | margin: auto 5px; 131 | color: #333; 132 | background: #CED; 133 | float: left; 134 | padding: 3px; 135 | padding-top: 1px; 136 | width: 55%; 137 | border: 1px solid #ACC; 138 | } 139 | 140 | .auto-complete { 141 | background-color: #FFF; 142 | border: 1px solid #AAA; 143 | box-shadow: 1px 1px 3px rgba(0, 0, 110, 0.33); 144 | margin-left: 8px; 145 | min-width: 200px; 146 | position: absolute; 147 | } 148 | 149 | .auto-complete li { 150 | background-color: #FFF; 151 | border-bottom: 0 solid #FFF; 152 | color: #2255AA; 153 | margin: 0; 154 | padding: 3px 5px; 155 | cursor: pointer; 156 | } 157 | 158 | .auto-complete .active { 159 | background-color: #969696; 160 | color: #FFF; 161 | } 162 | 163 | .auto-complete li.exclude { 164 | display: none; 165 | } 166 | 167 | #blockquote-hint { 168 | color: #BEBEBE; 169 | margin: 2px 0 0; 170 | } 171 | 172 | .notification-theme { 173 | font-size: inherit; 174 | padding: 12px 80px; 175 | } 176 | 177 | .notification-theme .ngn-dismiss { 178 | top: 10px; 179 | width: 20px; 180 | height: 20px; 181 | line-height: 20px; 182 | } 183 | -------------------------------------------------------------------------------- /app/tests/spec/BackgroundSpec.js: -------------------------------------------------------------------------------- 1 | describe("Test background", function() { 2 | 3 | beforeEach(function() { 4 | jasmine.Ajax.install(); 5 | pages = {'http://zhangchi.de/': 6 | {desc:"Personal blog of zhangchi", 7 | isSaved: true, 8 | shared: true, 9 | tag: "blog", 10 | time: "2011-03-02T17:39:26Z", 11 | title: "zhangchi", 12 | toread: false, 13 | url: "http://zhangchi.de/"}}; 14 | login('test_token'); 15 | jasmine.Ajax.requests.mostRecent().respondWith({ 16 | "status": 200, 17 | "contenttype": 'text/plain', 18 | "responseText": JSON.stringify({result:'token'}) 19 | }); 20 | }); 21 | 22 | afterEach(function() { 23 | logout(); 24 | pages = {}; 25 | jasmine.Ajax.uninstall(); 26 | }); 27 | 28 | it('user info in should return', function () { 29 | expect(getUserInfo()).toBeDefined(); 30 | }); 31 | 32 | it('should return page info', function () { 33 | expect(getPageInfo('http://zhangchi.de/')).not.toBeNull(); 34 | }); 35 | 36 | it('should not return page info', function () { 37 | expect(getPageInfo('http://twitter.com')).toBeNull(); 38 | expect(jasmine.Ajax.requests.mostRecent().url).toContain(mainPath + 'posts/get'); 39 | }); 40 | 41 | it('add post should be good', function () { 42 | var pageInfo = {url: 'http://twitter.com', 43 | title: 'Twitter', 44 | desc: '', 45 | shared: true, 46 | toread: false}; 47 | expect(addPost(pageInfo)).toBeUndefined(); 48 | expect(jasmine.Ajax.requests.mostRecent().url).toContain(mainPath + 'posts/add'); 49 | }); 50 | 51 | it('new tags should be add to the tags list', function () { 52 | _tags = ['pinboard', 'plus']; 53 | var pageInfo = {url: 'http://pinboard-plus.zhangchi.de/', 54 | title: 'Pinboard Plus', 55 | desc: '', 56 | tag: 'pinboard plus pinboardplus', 57 | shared: true, 58 | toread: false}; 59 | expect(addPost(pageInfo)).toBeUndefined(); 60 | expect(_tags).toEqual(['pinboard', 'plus'].concat(['pinboardplus'])); 61 | }); 62 | 63 | it('delete should be good', function () { 64 | expect(deletePost('http://twitter.com')).toBeUndefined(); 65 | expect(jasmine.Ajax.requests.mostRecent().url).toContain(mainPath + 'posts/delete'); 66 | }); 67 | 68 | it('get suggest should be good', function () { 69 | expect(getSuggest('http://twitter.com')).toBeUndefined(); 70 | expect(jasmine.Ajax.requests.mostRecent().url).toContain(mainPath + 'posts/suggest'); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /app/tests/spec/NotificationsSpec.js: -------------------------------------------------------------------------------- 1 | describe("Test notifications", function() { 2 | 3 | beforeEach(function() { 4 | Notifications.init(); 5 | Notifications.clearAll(); 6 | }); 7 | 8 | afterEach(function() { 9 | Notifications.clearAll(); 10 | }); 11 | 12 | it('init', function () { 13 | expect(Notifications.getTop()).toBe(null); 14 | }); 15 | 16 | it('add notification', function () { 17 | var firstNoti = {message:'First notify', type:'error'}; 18 | var secNoti = {message:'Second notify', type:'info'}; 19 | var thirdNoti = {message:'Third notify', type:'success'}; 20 | 21 | Notifications.add(firstNoti.message, firstNoti.type); 22 | expect(Notifications.getTop().message).toBe(firstNoti.message); 23 | expect(Notifications.getTop().type).toBe(firstNoti.type); 24 | expect(Notifications.notis.length).toBe(1); 25 | 26 | Notifications.add(secNoti.message, secNoti.type); 27 | expect(Notifications.getTop().message).toBe(secNoti.message); 28 | expect(Notifications.getTop().type).toBe(secNoti.type); 29 | expect(Notifications.notis.length).toBe(2); 30 | 31 | Notifications.add(thirdNoti.message, thirdNoti.type); 32 | expect(Notifications.getTop().message).toBe(thirdNoti.message); 33 | expect(Notifications.getTop().type).toBe(thirdNoti.type); 34 | expect(Notifications.notis.length).toBe(3); 35 | }); 36 | 37 | it('default type add notification', function () { 38 | // test default value of type param 39 | var defaultType = 'error'; 40 | Notifications.add('notify with default type'); 41 | expect(Notifications.getTop().type).toBe(defaultType); 42 | }); 43 | 44 | it('add duplicate notification', function () { 45 | var no1Noti = {message:'No. 1 notify', type:'error'}; 46 | var no2Noti = {message:'No. 2 notify', type:'error'}; 47 | 48 | for (var i = 0; i < 3; i++) { 49 | Notifications.add(no1Noti.message, no1Noti.type); 50 | } 51 | expect(Notifications.getTop().message).toBe(no1Noti.message); 52 | expect(Notifications.getTop().type).toBe(no1Noti.type); 53 | expect(Notifications.notis.length).toBe(1); 54 | 55 | Notifications.add(no2Noti.message, no2Noti.type); 56 | Notifications.add(no1Noti.message, no1Noti.type); 57 | expect(Notifications.getTop().message).toBe(no1Noti.message); 58 | expect(Notifications.getTop().type).toBe(no1Noti.type); 59 | expect(Notifications.notis.length).toBe(2); 60 | 61 | Notifications.add(no2Noti.message, no2Noti.type); 62 | expect(Notifications.getTop().message).toBe(no2Noti.message); 63 | expect(Notifications.getTop().type).toBe(no2Noti.type); 64 | expect(Notifications.notis.length).toBe(2); 65 | }); 66 | 67 | it('remove notification', function () { 68 | var cnt = 5; 69 | for (var i = 0; i < cnt; i++) { 70 | Notifications.add(i + ' notify', 'error'); 71 | } 72 | expect(Notifications.notis.length).toBe(cnt); 73 | 74 | var p = cnt; 75 | for (i = 0; i < cnt - 1; i++) { 76 | Notifications.remove(); 77 | expect(Notifications.notis.length).toBe(cnt - i - 1); 78 | expect(Notifications.getTop().message).toBe((cnt - i - 2) + ' notify'); 79 | } 80 | // remove the last one 81 | Notifications.remove(); 82 | expect(Notifications.notis.length).toBe(0); 83 | expect(Notifications.getTop()).toBe(null); 84 | }); 85 | 86 | it('clear all', function () { 87 | var cnt = 5; 88 | for (var i = 0; i < cnt; i++) { 89 | Notifications.add(i + 'notify', 'error'); 90 | } 91 | expect(Notifications.notis.length).toBe(cnt); 92 | 93 | Notifications.clearAll(); 94 | expect(Notifications.getTop()).toBe(null); 95 | expect(Notifications.notis.length).toBe(0); 96 | 97 | Notifications.add('notify again', 'error'); 98 | expect(Notifications.getTop().message).toBe('notify again'); 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /app/tests/spec/PinboardSpec.js: -------------------------------------------------------------------------------- 1 | describe("Test pinboard.js", function() { 2 | 3 | beforeEach(function() { 4 | jasmine.Ajax.install(); 5 | }); 6 | 7 | afterEach(function() { 8 | jasmine.Ajax.uninstall(); 9 | }); 10 | 11 | it('Login and logout', function () { 12 | var doneFn = jasmine.createSpy("success"); 13 | var failFn = jasmine.createSpy("failed"); 14 | 15 | var test_token = 'user:token'; 16 | Pinboard.login(test_token, doneFn, failFn); 17 | 18 | jasmine.Ajax.requests.mostRecent().respondWith({ 19 | "status": 200, 20 | "contenttype": 'text/plain', 21 | "responseText": JSON.stringify({result:'token'}) 22 | }); 23 | 24 | expect(doneFn).toHaveBeenCalledWith({result:'token'}); 25 | 26 | var userInfo = Pinboard.getUserInfo(); 27 | expect(userInfo.isChecked).toBe(true); 28 | expect(userInfo.authToken).toBe('user:token'); 29 | expect(userInfo.name).toBe('user'); 30 | expect(localStorage[checkedkey]).toEqual('true'); 31 | expect(localStorage[namekey]).toEqual(userInfo.name); 32 | expect(localStorage[authTokenKey]).toEqual(userInfo.authToken); 33 | 34 | expect(Pinboard.isLoggedin()).toBe(true); 35 | 36 | // test logout 37 | var cbFn = jasmine.createSpy("cb"); 38 | Pinboard.logout(cbFn); 39 | 40 | userInfo = Pinboard.getUserInfo(); 41 | expect(userInfo.isChecked).toBe(false); 42 | expect(userInfo.authToken).toBe(''); 43 | expect(userInfo.name).toBe(''); 44 | expect(localStorage[namekey]).toBeUndefined(); 45 | expect(localStorage[checkedkey]).toBeUndefined(); 46 | expect(localStorage[authTokenKey]).toBeUndefined(); 47 | expect(Pinboard.isLoggedin()).toBe(false); 48 | }); 49 | 50 | it('Query post pinned or not base on url', function () { 51 | var doneFn = jasmine.createSpy("success"); 52 | var failFn = jasmine.createSpy("failed"); 53 | 54 | Pinboard.queryPinState('http://abc.xyz/', doneFn, failFn); 55 | 56 | var resp = {"date":"2017-02-28T05:34:12Z", 57 | "user":"user", 58 | "posts":[{"href":"http:\/\/abc.xyz\/", 59 | "description":"id Software Programming Principles", 60 | "extended":"", 61 | "meta":"", 62 | "hash":"", 63 | "time":"2017-02-28T05:34:12Z", 64 | "shared":"yes", 65 | "toread":"no", 66 | "tags":"programming development principle"}]}; 67 | jasmine.Ajax.requests.mostRecent().respondWith({ 68 | "status": 200, 69 | "contenttype": 'text/plain', 70 | "responseText": JSON.stringify(resp) 71 | }); 72 | expect(doneFn).toHaveBeenCalledWith(resp); 73 | }); 74 | 75 | it('Add post', function () { 76 | var doneFn = jasmine.createSpy("success"); 77 | var failFn = jasmine.createSpy("failed"); 78 | 79 | Pinboard.addPost('Title of the page', 80 | 'http://abc.xyz', 81 | 'Description text', 82 | 'programming development', 83 | 'yes', 84 | 'no', 85 | doneFn, failFn); 86 | 87 | jasmine.Ajax.requests.mostRecent().respondWith({ 88 | "status": 200, 89 | "contenttype": 'text/plain', 90 | "responseText": JSON.stringify({result_code:'done'}) 91 | }); 92 | expect(doneFn).toHaveBeenCalledWith({result_code:'done'}); 93 | }); 94 | 95 | it('Delete post', function () { 96 | var doneFn = jasmine.createSpy("success"); 97 | var failFn = jasmine.createSpy("failed"); 98 | 99 | Pinboard.deletePost( 100 | 'http://abc.xyz/', doneFn, failFn 101 | ); 102 | 103 | jasmine.Ajax.requests.mostRecent().respondWith({ 104 | "status": 200, 105 | "contenttype": 'text/plain', 106 | "responseText": JSON.stringify({result_code:'done'}) 107 | }); 108 | expect(doneFn).toHaveBeenCalledWith({result_code:'done'}); 109 | }); 110 | 111 | it('Get suggest tags base on url', function () { 112 | var doneFn = jasmine.createSpy("success"); 113 | 114 | Pinboard.getSuggest('http://abc.xyz/', doneFn); 115 | 116 | var resp = [{"popular":["twitter","facebook","objective-c","ifttt"]}, 117 | {"recommended":["ifttt","twitter","facebook","WSH","objective-c","twitterlink","1960s","@codepo8","Aiviq","art"]}]; 118 | jasmine.Ajax.requests.mostRecent().respondWith({ 119 | "status": 200, 120 | "contenttype": 'text/plain', 121 | "responseText": JSON.stringify(resp) 122 | }); 123 | expect(doneFn).toHaveBeenCalledWith(resp); 124 | }); 125 | 126 | it('Get all tags of user', function () { 127 | var doneFn = jasmine.createSpy("success"); 128 | 129 | Pinboard.getTags(doneFn); 130 | 131 | var resp = {"pinboard":"46", "github":"500", "fun":"53"}; 132 | jasmine.Ajax.requests.mostRecent().respondWith({ 133 | "status": 200, 134 | "contenttype": 'text/plain', 135 | "responseText": JSON.stringify(resp) 136 | }); 137 | expect(doneFn).toHaveBeenCalledWith(resp); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /app/tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jasmine Spec Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinboard-plus", 3 | "private": true, 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "underscore": "^1.8.3", 7 | "jquery": "^3.1.1", 8 | "angular": "^1.6.2", 9 | "angular-sanitize": "^1.6.2", 10 | "ng-notify": "^0.8.0" 11 | }, 12 | "devDependencies": { 13 | "jasmine-core": "jasmine#^2.5.2", 14 | "jasmine-ajax": "^3.3.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import gulpLoadPlugins from 'gulp-load-plugins'; 3 | import del from 'del'; 4 | import {stream as wiredep} from 'wiredep'; 5 | 6 | const $ = gulpLoadPlugins(); 7 | 8 | gulp.task('extras', () => { 9 | return gulp.src([ 10 | 'app/*.*', 11 | 'app/_locales/**', 12 | '!app/*.json', 13 | '!app/*.html', 14 | ], { 15 | base: 'app', 16 | dot: true 17 | }).pipe(gulp.dest('dist')); 18 | }); 19 | 20 | function lint(files, options) { 21 | return () => { 22 | return gulp.src(files) 23 | .pipe($.eslint(options)) 24 | .pipe($.eslint.format()); 25 | }; 26 | } 27 | 28 | gulp.task('lint', lint('app/scripts/**/*.js', { 29 | env: { 30 | es6: false 31 | } 32 | })); 33 | 34 | gulp.task('images', () => { 35 | return gulp.src('app/images/**/*') 36 | .pipe($.if($.if.isFile, $.cache($.imagemin({ 37 | progressive: true, 38 | interlaced: true, 39 | // don't remove IDs from SVGs, they are often used 40 | // as hooks for embedding and styling 41 | svgoPlugins: [{cleanupIDs: false}] 42 | })) 43 | .on('error', function (err) { 44 | console.log(err); 45 | this.end(); 46 | }))) 47 | .pipe(gulp.dest('dist/images')); 48 | }); 49 | 50 | gulp.task('html', () => { 51 | return gulp.src('app/*.html') 52 | .pipe($.useref({searchPath: ['.tmp', 'app', '.']})) 53 | .pipe($.sourcemaps.init()) 54 | .pipe($.if('*.js', $.uglify())) 55 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 56 | .pipe($.if('*.html', $.htmlmin({removeComments: true, collapseWhitespace: true}))) 57 | .pipe(gulp.dest('dist')); 58 | }); 59 | 60 | gulp.task('chromeManifest', () => { 61 | return gulp.src('app/manifest.json') 62 | .pipe($.chromeManifest({ 63 | buildnumber: true, 64 | background: { 65 | target: 'scripts/background.js', 66 | exclude: ['scripts/chromereload.js'] 67 | } 68 | })) 69 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 70 | .pipe($.if('*.js', $.uglify())) 71 | .pipe(gulp.dest('dist', {cwd: '../'})); // do not remove the cwd otherwise the files goes to app folder 72 | }); 73 | 74 | gulp.task('clean', del.bind(null, ['.tmp', 'dist'])); 75 | 76 | gulp.task('watch', gulp.series('lint'), () => { 77 | $.livereload.listen(); 78 | 79 | gulp.watch([ 80 | 'app.html', 81 | 'app/scripts.js', 82 | 'app/images', 83 | 'app/styles', 84 | 'app/_locales.json', 85 | 'app/tests.js' 86 | ]).on('change', $.livereload.reload); 87 | 88 | gulp.watch('app/scripts.js', gulp.series('lint')); 89 | gulp.watch('bower.json', gulp.series('wiredep')); 90 | }); 91 | 92 | gulp.task('size', () => { 93 | return gulp.src('dist/**/*').pipe($.size({title: 'build', gzip: true})); 94 | }); 95 | 96 | gulp.task('wiredep', () => { 97 | gulp.src('app/*.html') 98 | .pipe(wiredep({ 99 | ignorePath: /^(\.\.\/)*\.\./ 100 | })) 101 | .pipe(gulp.dest('app')); 102 | }); 103 | 104 | gulp.task('package', function () { 105 | var manifest = require('./dist/manifest.json'); 106 | return gulp.src(['dist/**', '!**/*.map']) 107 | .pipe($.zip('Pinboard-Plus-' + manifest.version + '.zip')) 108 | .pipe(gulp.dest('package')); 109 | }); 110 | 111 | gulp.task('build', 112 | gulp.series( 113 | 'lint', 'chromeManifest', 114 | gulp.parallel('html', 'images', 'extras'), 115 | 'size') 116 | ); 117 | 118 | gulp.task('default', gulp.series('clean'), cb => { 119 | gulp.series('build'); 120 | cb(); 121 | }); 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pinboard-Plus", 3 | "version": "2.11.0", 4 | "private": true, 5 | "description": "Pinboard Plus is a better Chrome extension for Pinboard.in.", 6 | "engines": { 7 | "node": ">=0.8.0" 8 | }, 9 | "devDependencies": { 10 | "babel-core": "^6.7.2", 11 | "babel-preset-es2015": "^6.6.0", 12 | "babel-register": "^6.26.0", 13 | "del": "^2.2.0", 14 | "gulp": "^4.0.0", 15 | "gulp-cache": "^0.4.3", 16 | "gulp-chrome-manifest": "^0.0.13", 17 | "gulp-clean-css": "^2.0.3", 18 | "gulp-eslint": "^2.0.0", 19 | "gulp-htmlmin": "^5.0.1", 20 | "gulp-if": "^2.0.0", 21 | "gulp-imagemin": "^2.4.0", 22 | "gulp-livereload": "^3.8.1", 23 | "gulp-load-plugins": "^1.2.0", 24 | "gulp-size": "^2.1.0", 25 | "gulp-sourcemaps": "^1.6.0", 26 | "gulp-uglify": "^1.5.3", 27 | "gulp-useref": "^3.0.8", 28 | "gulp-zip": "^3.2.0", 29 | "main-bower-files": "^2.11.1", 30 | "wiredep": "^4.0.0" 31 | }, 32 | "eslintConfig": { 33 | "env": { 34 | "node": true, 35 | "browser": true 36 | }, 37 | "globals": { 38 | "chrome": true 39 | }, 40 | "rules": { 41 | "eol-last": 0, 42 | "quotes": [ 43 | 2, 44 | "single" 45 | ] 46 | } 47 | } 48 | } 49 | --------------------------------------------------------------------------------