├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── manifest.js └── webpack.config.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── package-lock.json ├── package.json ├── postcss.config.js └── src ├── _locales ├── bg │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── gl │ └── messages.json ├── it │ └── messages.json └── zh_CN │ └── messages.json ├── fonts ├── FiraSans-Regular.ttf └── FiraSans-SemiBold.ttf ├── icons ├── cog.svg ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── pencil.svg └── save.svg ├── images ├── add-item-gif.gif ├── screenshot.png └── search-screenshot.png ├── js ├── background.js ├── options.js ├── popup.js └── readinglist.js ├── manifest ├── base.json ├── chrome.json ├── edge.json └── firefox.json ├── options.html ├── popup.html ├── sidebar.html └── style ├── base ├── animations.styl ├── index.styl ├── layout.styl ├── reading-item.styl ├── typography.styl └── variables.styl ├── font.styl ├── options.styl ├── popup.styl └── themes ├── dark.styl └── light.styl /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": [ 7 | "Chrome >= 20", 8 | "Firefox >= 53" 9 | ] 10 | } 11 | }] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // add your custom rules here 15 | 'rules': { 16 | // allow paren-less arrow functions 17 | 'arrow-parens': 0, 18 | // allow debugger during development 19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 20 | 'prefer-arrow-callback': 'error' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | notes.txt 2 | node_modules 3 | dist 4 | .DS_Store 5 | /.idea 6 | /reading-list.iml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reading List 2 | 3 | A Chrome and Firefox extension for saving pages to read later. Free on the [Chrome Web Store](https://chrome.google.com/webstore/detail/lloccabjgblebdmncjndmiibianflabo) and [Firefox Addons](https://addons.mozilla.org/firefox/addon/reading_list/). 4 | 5 |  6 | 7 | ## Features 8 | 9 | - Nifty animations 10 | - Search 11 | - Syncing with Google/Mozilla accounts 12 | - A light and dark theme 13 | 14 | ## Installation 15 | 16 | Get it from the [Chrome Web Store](https://chrome.google.com/webstore/detail/lloccabjgblebdmncjndmiibianflabo) or [Firefox Addons](https://addons.mozilla.org/firefox/addon/reading_list/) for free. 17 | 18 | ### Building 19 | 20 | Or, if you would rather do it the hard way, you can build the extension from the source code: 21 | 22 | 1. Make sure you have Node and NPM installed 23 | 1. Download/clone this repo 24 | 1. Install all the dependencies: 25 | ```bash 26 | # From the project folder 27 | npm install 28 | ``` 29 | 1. Run the build command: 30 | ```bash 31 | npm run build [chrome/firefox] 32 | ``` 33 | 34 | The build command assembles all the files in the `dist` folder. After it’s built, you can load it into Chrome or Firefox. 35 | 36 | #### Load into Chrome 37 | 38 | 1. Go to [chrome://extensions/](chrome://extensions/) 39 | 1. Check “Developer Mode” 40 | 1. Click “Load unpacked extension…” 41 | 1. Load up the “dist” folder 42 | 43 | #### Load into Firefox 44 | 45 | 1. Go to [about:addons](about:addons) 46 | 1. Select “Extensions” 47 | 1. Click the settings cog, and select “Install Add-on From File…” 48 | 1. Load up the “dist” folder 49 | 50 | ## Using the extension 51 | 52 | 1. Go to a page you want to save for later 53 | 1. Click the reading list icon on the top right of your browser  54 | 1. Click the `+` button 55 | - You can also right-click anywhere on the page and select “Add page to Reading List” 56 | 1. When you want to read a page you saved, open up the extension and click the reading item you want to read 57 | - `Control + click` or `command ⌘/windows key ⊞ + click` to open the page in a new tab 58 | 1. Done with a page? Click the `×` next to said page in your reading list, and it will magically vanish. 59 | 60 | #### Firefox verification 61 | 1a74cd646f4e4485b271d4cfe035bdf7 -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production' 2 | 3 | const targetBrowser = process.argv.length >= 3 4 | ? process.argv[2] 5 | : '' 6 | 7 | const chalk = require('chalk') 8 | 9 | if (!['chrome', 'firefox', 'edge'].includes(targetBrowser)) { 10 | return console.log(chalk.red( 11 | 'Specify “chrome”, “firefox” or “edge” as the target browser')) 12 | } 13 | 14 | const ora = require('ora') 15 | const path = require('path') 16 | const webpack = require('webpack') 17 | const webpackConfig = require('./webpack.config') 18 | const rm = require('rimraf') 19 | const manifestBuilder = require('./manifest') 20 | 21 | const spinner = ora('building for production…') 22 | spinner.start() 23 | 24 | rm(path.resolve(__dirname, '..', 'dist'), err1 => { 25 | if (err1) throw err1 26 | webpack(webpackConfig, (err2, stats) => { 27 | spinner.stop() 28 | if (err1) throw err2 29 | process.stdout.write(stats.toString({ 30 | colors: true, 31 | modules: false, 32 | children: false, 33 | chunks: false, 34 | chunkModules: false 35 | }) + '\n\n') 36 | 37 | // Build the manifest 38 | manifestBuilder(targetBrowser, err3 => { 39 | if (err3) throw err3 40 | console.log(chalk.cyan(' Build complete.\n')) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /build/manifest.js: -------------------------------------------------------------------------------- 1 | const base = require('../src/manifest/base.json') 2 | const chromeOptions = require('../src/manifest/chrome.json') 3 | const firefoxOptions = require('../src/manifest/firefox.json') 4 | const edgeOptions = require('../src/manifest/edge.json') 5 | const fs = require('fs') 6 | const path = require('path') 7 | const _ = require('lodash') 8 | 9 | const manifestOptions = { 10 | chrome: chromeOptions, 11 | firefox: firefoxOptions, 12 | edge: edgeOptions 13 | } 14 | 15 | module.exports = function (browser, callback) { 16 | const filePath = path.resolve(__dirname, '..', 'dist', 'manifest.json') 17 | const manifest = _.merge({}, base, manifestOptions[browser]) 18 | 19 | // generates the manifest file using the package.json version 20 | manifest.version = process.env.npm_package_version 21 | 22 | fs.writeFile(filePath, JSON.stringify(manifest), callback) 23 | } 24 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const webpack = require('webpack') 3 | const config = require('../config') 4 | const path = require('path') 5 | const WriteFilePlugin = require('write-file-webpack-plugin') 6 | const CopyWebpackPlugin = require('copy-webpack-plugin') 7 | const env = config.build.env 8 | 9 | const webpackConfig = { 10 | entry: { 11 | popup: path.resolve(__dirname, '..', 'src', 'js', 'popup.js'), 12 | options: path.resolve(__dirname, '..', 'src', 'js', 'options.js'), 13 | background: path.resolve(__dirname, '..', 'src', 'js', 'background.js') 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, '..', 'dist'), 17 | filename: '[name].bundle.js' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'eslint-loader', 24 | enforce: 'pre', 25 | include: [path.resolve(__dirname, '..', 'src')], 26 | options: { 27 | formatter: require('eslint-friendly-formatter') 28 | } 29 | }, 30 | { 31 | test: /\.styl$/, 32 | include: [path.resolve(__dirname, '..', 'src', 'style')], 33 | use: [ 34 | { 35 | loader: 'style-loader', 36 | options: { sourceMap: true } 37 | }, 38 | { 39 | loader: 'css-loader', 40 | options: { sourceMap: true } 41 | }, 42 | { 43 | loader: 'postcss-loader', 44 | options: { sourceMap: true } 45 | }, 46 | { 47 | loader: 'stylus-loader', 48 | options: { sourceMap: true } 49 | } 50 | ] 51 | }, 52 | { 53 | test: /\.js$/, 54 | loader: 'babel-loader', 55 | include: [path.resolve(__dirname, '..', 'src')] 56 | } 57 | ] 58 | }, 59 | plugins: [ 60 | new webpack.DefinePlugin({ 61 | 'process.env': env 62 | }), 63 | new webpack.optimize.UglifyJsPlugin({ 64 | compress: { 65 | warnings: false 66 | }, 67 | sourceMap: true 68 | }), 69 | new HtmlWebpackPlugin({ 70 | template: path.resolve(__dirname, '..', 'src', 'popup.html'), 71 | filename: 'popup.html', 72 | chunks: ['popup'] 73 | }), 74 | new HtmlWebpackPlugin({ 75 | template: path.resolve(__dirname, '..', 'src', 'sidebar.html'), 76 | filename: 'sidebar.html', 77 | chunks: ['popup'] 78 | }), 79 | new HtmlWebpackPlugin({ 80 | template: path.resolve(__dirname, '..', 'src', 'options.html'), 81 | filename: 'options.html', 82 | chunks: ['options'] 83 | }), 84 | new CopyWebpackPlugin([ 85 | { 86 | from: path.resolve(__dirname, '..', 'src', '_locales'), 87 | to: path.resolve(__dirname, '..', 'dist', '_locales'), 88 | ignore: ['.*'] 89 | }, 90 | { 91 | from: path.resolve(__dirname, '..', 'src', 'icons'), 92 | to: path.resolve(__dirname, '..', 'dist', 'icons'), 93 | ignore: ['.*'] 94 | } 95 | ]), 96 | new WriteFilePlugin() 97 | ] 98 | } 99 | 100 | if (config.build.productionGzip) { 101 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 102 | 103 | webpackConfig.plugins.push( 104 | new CompressionWebpackPlugin({ 105 | asset: '[path].gz[query]', 106 | algorithm: 'gzip', 107 | test: new RegExp( 108 | '\\.(' + 109 | config.build.productionGzipExtensions.join('|') + 110 | ')$' 111 | ), 112 | threshold: 10240, 113 | minRatio: 0.8 114 | }) 115 | ) 116 | } 117 | 118 | if (config.build.bundleAnalyzerReport) { 119 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 120 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 121 | } 122 | 123 | module.exports = webpackConfig 124 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | build: { 5 | env: require('./prod.env'), 6 | productionSourceMap: true, 7 | productionGzip: false, 8 | productionGzipExtensions: ['js', 'css'], 9 | // Run the build command with an extra argument to 10 | // View the bundle analyzer report after build finishes: 11 | // `npm run build --report` 12 | // Set to `true` or `false` to always turn it on or off 13 | bundleAnalyzerReport: process.env.npm_config_report 14 | }, 15 | dev: { 16 | env: require('./dev.env'), 17 | port: 8080, 18 | proxyTable: {}, 19 | // CSS Sourcemaps off by default because relative paths are "buggy" 20 | // with this option, according to the CSS-Loader README 21 | // (https://github.com/webpack/css-loader#sourcemaps) 22 | // In our experience, they generally work as expected, 23 | // just be aware of this issue when enabling this option. 24 | cssSourceMap: false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reading-list", 3 | "version": "2.4.9", 4 | "description": "A Chrome/Firefox extension that saves a list of webpages to read later.", 5 | "scripts": { 6 | "build": "node build/build.js", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/alexpdraper/reading-list.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/alexpdraper/reading-list/issues" 15 | }, 16 | "homepage": "https://github.com/alexpdraper/reading-list#readme", 17 | "browserslist": [ 18 | "Chrome >= 20", 19 | "Edge >= 40", 20 | "Firefox >= 57" 21 | ], 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.6", 24 | "babel-core": "^6.26.0", 25 | "babel-eslint": "^8.0.1", 26 | "babel-loader": "^7.1.2", 27 | "babel-preset-env": "^1.6.1", 28 | "chalk": "^2.2.0", 29 | "compression-webpack-plugin": "^1.0.1", 30 | "copy-webpack-plugin": "^4.2.0", 31 | "css-loader": "^0.28.7", 32 | "eslint": "^4.18.2", 33 | "eslint-config-standard": "^10.2.1", 34 | "eslint-friendly-formatter": "^3.0.0", 35 | "eslint-loader": "^1.9.0", 36 | "eslint-plugin-import": "^2.8.0", 37 | "eslint-plugin-node": "^5.2.0", 38 | "eslint-plugin-promise": "^3.6.0", 39 | "eslint-plugin-standard": "^3.0.1", 40 | "html-webpack-plugin": "^2.30.1", 41 | "lodash": "^4.17.20", 42 | "ora": "^1.3.0", 43 | "postcss-loader": "^2.0.8", 44 | "rimraf": "^2.6.2", 45 | "style-loader": "^0.19.0", 46 | "stylus": "^0.54.5", 47 | "stylus-loader": "^3.0.1", 48 | "webpack": "^3.8.1", 49 | "webpack-bundle-analyzer": "^3.3.2", 50 | "webpack-dev-server": "^3.11.0", 51 | "webpack-merge": "^4.1.0", 52 | "write-file-webpack-plugin": "^4.2.0" 53 | }, 54 | "dependencies": { 55 | "fuse.js": "^3.2.0", 56 | "nativesortable": "^0.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/_locales/bg/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Добавете тази връзка към вашия „Списък за четене“" 4 | }, 5 | "addPage": { 6 | "message": "Добавяне на страницата към „Списък за четене“" 7 | }, 8 | "advancedOptions": { 9 | "message": "Разширени настройки" 10 | }, 11 | "allButton": { 12 | "message": "всичко" 13 | }, 14 | "animation": { 15 | "message": "Включване на анимация:" 16 | }, 17 | "appDesc": { 18 | "message": "Това разширение запазва списък от връзки към страници за по-късно четене." 19 | }, 20 | "appName": { 21 | "message": "Списък за четене" 22 | }, 23 | "backup": { 24 | "message": "Резервно копие:" 25 | }, 26 | "clearData": { 27 | "message": "Изчистване на „Списък за четене“" 28 | }, 29 | "confirmMsg": { 30 | "message": "На път сте да изтриете всичко от списъка за четене. Сигурни ли сте?" 31 | }, 32 | "context": { 33 | "message": "Показване в контекстното меню:" 34 | }, 35 | "dark": { 36 | "message": "тъмна" 37 | }, 38 | "dateButton": { 39 | "message": "дата" 40 | }, 41 | "deleteAll": { 42 | "message": "Изтриване на всичко!" 43 | }, 44 | "export": { 45 | "message": "Изнасяне" 46 | }, 47 | "goBack": { 48 | "message": "Връщане назад" 49 | }, 50 | "import": { 51 | "message": "Внасяне" 52 | }, 53 | "light": { 54 | "message": "светла" 55 | }, 56 | "openNewTab": { 57 | "message": "Отваряне на връзка в нов раздел:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Настройки на „Списък за четене“" 61 | }, 62 | "pageActionOption": { 63 | "message": "Показване в адресната лента:" 64 | }, 65 | "search": { 66 | "message": "Търсене" 67 | }, 68 | "theme": { 69 | "message": "Тема:" 70 | }, 71 | "titleButton": { 72 | "message": "заглавие" 73 | }, 74 | "unreadButton": { 75 | "message": "непрочетен" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Seite zur Leseliste hinzufügen" 4 | }, 5 | "addPage": { 6 | "message": "Seite zur Leseliste hinzufügen" 7 | }, 8 | "advancedOptions": { 9 | "message": "Erweiterte Optionen" 10 | }, 11 | "allButton": { 12 | "message": "alles" 13 | }, 14 | "animation": { 15 | "message": "Animation anschalten:" 16 | }, 17 | "appDesc": { 18 | "message": "Diese Erweiterung speichert eine liste von webseiten, die später gelesen werden." 19 | }, 20 | "appName": { 21 | "message": "Leseliste" 22 | }, 23 | "backup": { 24 | "message": "Datensicherung:" 25 | }, 26 | "clearData": { 27 | "message": "Leseliste löschen" 28 | }, 29 | "confirmMsg": { 30 | "message": "Sie sind dabei, alles in der Leseliste zu löschen. Bist du sicher?" 31 | }, 32 | "context": { 33 | "message": "Im Kontextmenü anzeigen:" 34 | }, 35 | "dark": { 36 | "message": "Dunkles" 37 | }, 38 | "dateButton": { 39 | "message": "Datum" 40 | }, 41 | "deleteAll": { 42 | "message": "Alles löschen!" 43 | }, 44 | "export": { 45 | "message": "Export" 46 | }, 47 | "goBack": { 48 | "message": "Zurückgehen" 49 | }, 50 | "import": { 51 | "message": "Import" 52 | }, 53 | "light": { 54 | "message": "Licht" 55 | }, 56 | "openNewTab": { 57 | "message": "Link in neuem Tab öffnen:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Leselistenoptionen" 61 | }, 62 | "pageActionOption": { 63 | "message": "Im Adressleiste anzeigen:" 64 | }, 65 | "search": { 66 | "message": "Suchbegriff" 67 | }, 68 | "theme": { 69 | "message": "Theme:" 70 | }, 71 | "titleButton": { 72 | "message": "Titel" 73 | }, 74 | "unreadButton": { 75 | "message": "ungelesen" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Add link to Reading List", 4 | "description": "The link message in the context menu." 5 | }, 6 | "addPage": { 7 | "message": "Add page to Reading List", 8 | "description": "The message in the context menu." 9 | }, 10 | "advancedOptions": { 11 | "message": "Advanced options", 12 | "description": "The label for the advanced options dropdown" 13 | }, 14 | "allButton": { 15 | "message": "all", 16 | "description": "Button to show all reading items" 17 | }, 18 | "animation": { 19 | "message": "Enable animation:", 20 | "description": "The label for the animation option." 21 | }, 22 | "appDesc": { 23 | "message": "This extension saves a list of links to pages to read later.", 24 | "description": "The description of the application, displayed in the web store." 25 | }, 26 | "appName": { 27 | "message": "Reading List", 28 | "description": "The title of the application, displayed in the web store and in the popup’s heading." 29 | }, 30 | "backup": { 31 | "message": "Backup:", 32 | "description": "Label for import and export button." 33 | }, 34 | "clearData": { 35 | "message": "Clear Reading List", 36 | "description": "The label for the button to clear all the reading list items" 37 | }, 38 | "confirmMsg": { 39 | "message": "You are about to delete everything in the reading list. Are you sure?", 40 | "description": "Message to ensure user knows they are deleting all app data" 41 | }, 42 | "context": { 43 | "message": "Show in context menu:", 44 | "description": "The label for the context option." 45 | }, 46 | "dark": { 47 | "message": "Dark", 48 | "description": "The name for the dark theme." 49 | }, 50 | "dateButton": { 51 | "message": "date", 52 | "description": "Button to sort by date." 53 | }, 54 | "deleteAll": { 55 | "message": "Delete everything!", 56 | "description": "Confirm message on clearing data" 57 | }, 58 | "export": { 59 | "message": "Export", 60 | "description": "Button to Export reading list." 61 | }, 62 | "goBack": { 63 | "message": "Go back", 64 | "description": "Cancel message on clearning data" 65 | }, 66 | "import": { 67 | "message": "Import", 68 | "description": "Button to Import reading list." 69 | }, 70 | "light": { 71 | "message": "Light", 72 | "description": "The name for the light theme." 73 | }, 74 | "openNewTab": { 75 | "message": "Open link in new tab:", 76 | "description": "Label for the option to open a new tab when a link is clicked" 77 | }, 78 | "optionsTitle": { 79 | "message": "Reading List Options", 80 | "description": "The title of the application's options menu." 81 | }, 82 | "pageActionOption": { 83 | "message": "Show in address bar:", 84 | "description": "The label for the address bar option." 85 | }, 86 | "search": { 87 | "message": "Search", 88 | "description": "The label for the search box." 89 | }, 90 | "theme": { 91 | "message": "Theme:", 92 | "description": "The label for the theme option." 93 | }, 94 | "titleButton": { 95 | "message": "title", 96 | "description": "Button to sort by title." 97 | }, 98 | "unreadButton": { 99 | "message": "unread", 100 | "description": "Button to show unread reading items." 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Agregar enlace a la lista de lectura" 4 | }, 5 | "addPage": { 6 | "message": "Agregar página a la lista de lectura" 7 | }, 8 | "advancedOptions": { 9 | "message": "Opciones avanzadas" 10 | }, 11 | "allButton": { 12 | "message": "todo" 13 | }, 14 | "animation": { 15 | "message": "Permitir animación:" 16 | }, 17 | "appDesc": { 18 | "message": "Esta extensión de internet guarda una lista de enlaces de páginas para leer más tarde." 19 | }, 20 | "appName": { 21 | "message": "Lista de lectura" 22 | }, 23 | "backup": { 24 | "message": "Copia de seguridad:" 25 | }, 26 | "clearData": { 27 | "message": "Borrar la lista de lectura" 28 | }, 29 | "confirmMsg": { 30 | "message": "Estás a punto de eliminar todo el contenido de la lista de lectura. ¿Estás seguro?" 31 | }, 32 | "context": { 33 | "message": "Mostrar en el menú contextual:" 34 | }, 35 | "dark": { 36 | "message": "oscuro" 37 | }, 38 | "dateButton": { 39 | "message": "fecha" 40 | }, 41 | "deleteAll": { 42 | "message": "Eliminar todo!" 43 | }, 44 | "export": { 45 | "message": "Exportar" 46 | }, 47 | "goBack": { 48 | "message": "Regresar" 49 | }, 50 | "import": { 51 | "message": "Importar" 52 | }, 53 | "light": { 54 | "message": "claro" 55 | }, 56 | "openNewTab": { 57 | "message": "Abrir enlace en una nueva pestaña:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Opciones para la lista de lectura" 61 | }, 62 | "pageActionOption": { 63 | "message": "Mostrar en la barra de direcciones:" 64 | }, 65 | "search": { 66 | "message": "Buscar" 67 | }, 68 | "theme": { 69 | "message": "Tema:" 70 | }, 71 | "titleButton": { 72 | "message": "título" 73 | }, 74 | "unreadButton": { 75 | "message": "no leído" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Ajouter le lien à la liste de lecture" 4 | }, 5 | "addPage": { 6 | "message": "Ajouter la page à la liste de lecture" 7 | }, 8 | "advancedOptions": { 9 | "message": "Options avancées" 10 | }, 11 | "allButton": { 12 | "message": "tout" 13 | }, 14 | "animation": { 15 | "message": "Activer l'animation:" 16 | }, 17 | "appDesc": { 18 | "message": "Cette extension sauvegarde une liste de liens vers des pages a lire plus ulterieurement." 19 | }, 20 | "appName": { 21 | "message": "Liste de Lecture" 22 | }, 23 | "backup": { 24 | "message": "Sauvegarder:" 25 | }, 26 | "clearData": { 27 | "message": "Effacer Liste de Lecture" 28 | }, 29 | "confirmMsg": { 30 | "message": "Vous êtes sur le point de supprimer tout le contenu de la liste de lecture. Êtes-vous sûr?" 31 | }, 32 | "context": { 33 | "message": "Afficher dans le menu contextuel:" 34 | }, 35 | "dark": { 36 | "message": "foncé" 37 | }, 38 | "dateButton": { 39 | "message": "date" 40 | }, 41 | "deleteAll": { 42 | "message": "Supprimer tout!" 43 | }, 44 | "export": { 45 | "message": "Exportation" 46 | }, 47 | "goBack": { 48 | "message": "Retourner" 49 | }, 50 | "import": { 51 | "message": "Importer" 52 | }, 53 | "light": { 54 | "message": "lumière" 55 | }, 56 | "openNewTab": { 57 | "message": "Ouvrir un lien dans un nouvel onglet:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Options de la liste de lecture" 61 | }, 62 | "pageActionOption": { 63 | "message": "Afficher dans la barre d'adresse:" 64 | }, 65 | "search": { 66 | "message": "Recherche" 67 | }, 68 | "theme": { 69 | "message": "Thème:" 70 | }, 71 | "titleButton": { 72 | "message": "titre" 73 | }, 74 | "unreadButton": { 75 | "message": "non lu" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/gl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Engadir unha ligazón a Reading List" 4 | }, 5 | "addPage": { 6 | "message": "Engadir unha páxina a Reading List" 7 | }, 8 | "advancedOptions": { 9 | "message": "Axustes avanazados" 10 | }, 11 | "allButton": { 12 | "message": "todo" 13 | }, 14 | "animation": { 15 | "message": "Habilitar animación:" 16 | }, 17 | "appDesc": { 18 | "message": "Esta extensión garda unha lista das ligazóns a páxinas para ler máis tarde." 19 | }, 20 | "appName": { 21 | "message": "Reading List" 22 | }, 23 | "backup": { 24 | "message": "Respaldo:" 25 | }, 26 | "clearData": { 27 | "message": "Limpar Reading List" 28 | }, 29 | "confirmMsg": { 30 | "message": "Vai borrar todo o contido la lista de lectura. Está seguro?" 31 | }, 32 | "context": { 33 | "message": "Mostrar no menú contextual:" 34 | }, 35 | "dark": { 36 | "message": "Oscuro" 37 | }, 38 | "dateButton": { 39 | "message": "data" 40 | }, 41 | "deleteAll": { 42 | "message": "Quita todo!" 43 | }, 44 | "export": { 45 | "message": "Exportar" 46 | }, 47 | "goBack": { 48 | "message": "Atrás" 49 | }, 50 | "import": { 51 | "message": "Importar" 52 | }, 53 | "light": { 54 | "message": "Claro" 55 | }, 56 | "openNewTab": { 57 | "message": "Abrir ligazón en nova lapela:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Axustes de Reading List" 61 | }, 62 | "pageActionOption": { 63 | "message": "Amosar na barra de enderezos:" 64 | }, 65 | "search": { 66 | "message": "Busca" 67 | }, 68 | "theme": { 69 | "message": "Decorado:" 70 | }, 71 | "titleButton": { 72 | "message": "título" 73 | }, 74 | "unreadButton": { 75 | "message": "sen ler" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "Aggiungere il link all'Elenco Lettura" 4 | }, 5 | "addPage": { 6 | "message": "Aggiungere la pagina all'Elenco Lettura" 7 | }, 8 | "advancedOptions": { 9 | "message": "Opzioni avanzate" 10 | }, 11 | "allButton": { 12 | "message": "tutto" 13 | }, 14 | "animation": { 15 | "message": "Attivare l'animazione:" 16 | }, 17 | "appDesc": { 18 | "message": "Questa estensione risparmia una lista di pagine web per leggere più tarde." 19 | }, 20 | "appName": { 21 | "message": "Elenco Lettura" 22 | }, 23 | "backup": { 24 | "message": "Backup:" 25 | }, 26 | "clearData": { 27 | "message": "Ripristinare Elenco Lettura" 28 | }, 29 | "confirmMsg": { 30 | "message": "Stai per eliminare tutti i contenuti della lista di lettura. Sei sicuro?" 31 | }, 32 | "context": { 33 | "message": "Mostrare nel menu di contesto:" 34 | }, 35 | "dark": { 36 | "message": "scuro" 37 | }, 38 | "dateButton": { 39 | "message": "data" 40 | }, 41 | "deleteAll": { 42 | "message": "Eliminare tutto!" 43 | }, 44 | "export": { 45 | "message": "Esportare" 46 | }, 47 | "goBack": { 48 | "message": "Tornare indietro" 49 | }, 50 | "import": { 51 | "message": "Importare" 52 | }, 53 | "light": { 54 | "message": "luce" 55 | }, 56 | "openNewTab": { 57 | "message": "Aprire il link in una nuova scheda:" 58 | }, 59 | "optionsTitle": { 60 | "message": "Opzioni dell'Elenco Lettura" 61 | }, 62 | "pageActionOption": { 63 | "message": "Mostra nella barra degli indirizzi:" 64 | }, 65 | "search": { 66 | "message": "Cerca" 67 | }, 68 | "theme": { 69 | "message": "Tema:" 70 | }, 71 | "titleButton": { 72 | "message": "titulo" 73 | }, 74 | "unreadButton": { 75 | "message": "non letto" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addLink": { 3 | "message": "将链接添加到阅读列表" 4 | }, 5 | "addPage": { 6 | "message": "增加页面到阅读列表" 7 | }, 8 | "advancedOptions": { 9 | "message": "高级选项" 10 | }, 11 | "allButton": { 12 | "message": "所有" 13 | }, 14 | "animation": { 15 | "message": "支持动画:" 16 | }, 17 | "appDesc": { 18 | "message": "这个扩展工具可以帮助收藏很多用于以后阅读的页面。" 19 | }, 20 | "appName": { 21 | "message": "阅读列表" 22 | }, 23 | "backup": { 24 | "message": "备份:" 25 | }, 26 | "clearData": { 27 | "message": "清除阅读列表" 28 | }, 29 | "confirmMsg": { 30 | "message": "你准备删除阅读列表中的所有内容,请确认。" 31 | }, 32 | "context": { 33 | "message": "展示上下文菜单:" 34 | }, 35 | "dark": { 36 | "message": "暗黑" 37 | }, 38 | "dateButton": { 39 | "message": "日期" 40 | }, 41 | "deleteAll": { 42 | "message": "删除所有!" 43 | }, 44 | "export": { 45 | "message": "导出" 46 | }, 47 | "goBack": { 48 | "message": "退回" 49 | }, 50 | "import": { 51 | "message": "导入" 52 | }, 53 | "light": { 54 | "message": "明亮主题" 55 | }, 56 | "openNewTab": { 57 | "message": "在新标签页中打开链接:" 58 | }, 59 | "optionsTitle": { 60 | "message": "阅读列表选项" 61 | }, 62 | "pageActionOption": { 63 | "message": "在地址栏中显示:" 64 | }, 65 | "search": { 66 | "message": "搜索" 67 | }, 68 | "theme": { 69 | "message": "主题:" 70 | }, 71 | "titleButton": { 72 | "message": "标题" 73 | }, 74 | "unreadButton": { 75 | "message": "未读" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/fonts/FiraSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/fonts/FiraSans-Regular.ttf -------------------------------------------------------------------------------- /src/fonts/FiraSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/fonts/FiraSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/icons/cog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/icons/icon128.png -------------------------------------------------------------------------------- /src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/icons/icon16.png -------------------------------------------------------------------------------- /src/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/icons/icon32.png -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/icons/icon48.png -------------------------------------------------------------------------------- /src/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/images/add-item-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/images/add-item-gif.gif -------------------------------------------------------------------------------- /src/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/images/screenshot.png -------------------------------------------------------------------------------- /src/images/search-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpdraper/reading-list/3385565951708e67f94a6b5df93a7fafca3aef3a/src/images/search-screenshot.png -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | 3 | chrome.browserAction.setBadgeText({text: ''}) 4 | chrome.browserAction.setBadgeBackgroundColor({ 5 | color: '#2ea99c' 6 | }) 7 | 8 | const isFirefox = typeof InstallTrigger !== 'undefined' 9 | const defaultSettings = { 10 | settings: { 11 | addContextMenu: true, 12 | addPageAction: true, 13 | animateItems: !isFirefox, 14 | openNewTab: false, 15 | sortOption: '', 16 | sortOrder: '', 17 | theme: 'light', 18 | viewAll: true 19 | } 20 | } 21 | 22 | chrome.storage.sync.get(defaultSettings, store => { 23 | if (store.settings.addContextMenu) { 24 | createContextMenus() 25 | } 26 | }) 27 | 28 | window.createContextMenus = createContextMenus 29 | 30 | /** 31 | * Creates context menus for both link and when clicking on page. 32 | */ 33 | function createContextMenus () { 34 | createPageContextMenu() 35 | createLinkContextMenu() 36 | } 37 | 38 | /** 39 | * Add option to add current tab to reading list 40 | */ 41 | function createPageContextMenu () { 42 | chrome.management.getSelf(result => { 43 | let menuTitle = chrome.i18n.getMessage('addPage') 44 | menuTitle += (result.installType === 'development') ? ' (dev)' : '' 45 | chrome.contextMenus.create({ 46 | title: menuTitle, 47 | contexts: ['page'], 48 | onclick: addPageToList 49 | }) 50 | }) 51 | } 52 | 53 | /** 54 | * Add option to add link to reading list 55 | */ 56 | function createLinkContextMenu () { 57 | chrome.management.getSelf(result => { 58 | let menuTitle = chrome.i18n.getMessage('addLink') 59 | menuTitle += (result.installType === 'development') ? ' (dev)' : '' 60 | 61 | chrome.contextMenus.create({ 62 | title: menuTitle, 63 | contexts: ['link'], 64 | onclick: addLinkToList 65 | }) 66 | }) 67 | } 68 | 69 | /** 70 | * Add a tab to the reading list (context menu item onclick function) 71 | * 72 | * @param {object} info 73 | * @param {object} tab 74 | */ 75 | function addLinkToList (info, tab) { 76 | const setObj = {} 77 | 78 | const parser = document.createElement('a') 79 | parser.href = info.linkUrl 80 | // Removes google's strange url when it is clicked on 81 | if (parser.hostname.toLowerCase().indexOf('google') !== -1 && parser.pathname === '/url') { 82 | info.linkUrl = (getQueryVariable(parser, 'url')) 83 | } 84 | // Firefox uses linkText, Chrome uses selectionText 85 | let title 86 | if (isFirefox) { 87 | title = info.linkText 88 | } else { 89 | title = info.selectionText 90 | } 91 | 92 | setObj[info.linkUrl] = { 93 | url: info.linkUrl, 94 | title, 95 | index: 0, 96 | addedAt: Date.now() 97 | } 98 | 99 | chrome.storage.sync.set(setObj) 100 | } 101 | 102 | /** 103 | * Gets a variable value from the query string of a url 104 | * @param {string} url The url to parse 105 | * @param {string} variable The variable desired from the query string 106 | */ 107 | function getQueryVariable (url, variable) { 108 | const query = url.search.substring(1) 109 | const vars = query.split('&') 110 | for (let var1 of vars) { 111 | const pair = var1.split('=') 112 | if (decodeURIComponent(pair[0]) === variable) { 113 | return decodeURIComponent(pair[1]) 114 | } 115 | } 116 | return url 117 | } 118 | 119 | /** 120 | * Add the page to the reading list (context menu item onclick function) 121 | * 122 | * @param {object} info 123 | * @param {object} tab 124 | */ 125 | function addPageToList (info, tab) { 126 | const setObj = {} 127 | 128 | setObj[tab.url] = { 129 | url: tab.url, 130 | title: tab.title, 131 | index: 0, 132 | addedAt: Date.now() 133 | } 134 | 135 | chrome.storage.sync.set(setObj, () => updateBadge(tab.url, tab.id)) 136 | chrome.runtime.sendMessage({ 137 | 'type': 'add', 138 | 'url': tab.url, 139 | 'info': setObj[tab.url] 140 | }) 141 | } 142 | 143 | /** 144 | * Set the tab’s badge text to “✔” if it is on the reading list, otherwise remove it. 145 | * 146 | * @param {string} url - the tab’s URL 147 | * @param {number} tabId - the ID of the tab to update 148 | * @param {function(boolean)} callback - called when the badge text is updated 149 | */ 150 | function updateBadge (url, tabId, callback) { 151 | if (!tabId) { 152 | chrome.browserAction.setBadgeText({text: ''}) 153 | return 154 | } else if (!url) { 155 | return 156 | } 157 | 158 | // Check the reading list for the url 159 | chrome.storage.sync.get(url, item => { 160 | const onList = (item && item.hasOwnProperty(url)) 161 | 162 | // If the page is on the reading list, add a “✔” to the badge, 163 | // otherwise, no badge 164 | chrome.browserAction.setBadgeText({ 165 | text: onList ? '✔' : '', 166 | tabId: tabId 167 | }) 168 | 169 | if (typeof callback === 'function') { 170 | callback(onList, item) 171 | } 172 | }) 173 | } 174 | 175 | /** 176 | * Sets the item in the reading list to viewed. 177 | * 178 | * @param {string} url the url of the reading item to set to true. 179 | */ 180 | function setReadingItemViewed (url) { 181 | chrome.storage.sync.get(url, page => { 182 | if (page[url]) { 183 | page[url].viewed = true 184 | chrome.storage.sync.set(page) 185 | } 186 | }) 187 | } 188 | 189 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 190 | if (isFirefox) { 191 | chrome.storage.sync.get(defaultSettings, store => { 192 | if (store.settings.addPageAction) { 193 | chrome.pageAction.show(tabId) 194 | } else { 195 | chrome.pageAction.hide(tabId) 196 | } 197 | }) 198 | } 199 | 200 | // If the tab is loaded, update the badge text 201 | if (tabId && changeInfo.status === 'complete' && tab.url) { 202 | updateBadge(tab.url, tabId, () => { 203 | if (tab.active) { 204 | setReadingItemViewed(tab.url) 205 | } 206 | }) 207 | } 208 | }) 209 | 210 | chrome.tabs.onActivated.addListener(() => { 211 | chrome.tabs.query({currentWindow: true, active: true}, (tabs) => { 212 | if (tabs[0].url) { 213 | setReadingItemViewed(tabs[0].url) 214 | } 215 | }) 216 | }) 217 | 218 | if (isFirefox) { 219 | chrome.pageAction.onClicked.addListener(() => { 220 | chrome.tabs.query({currentWindow: true, active: true}, (tabs) => { 221 | const tab = tabs[0] 222 | if (tab.url) { 223 | chrome.storage.sync.get(tab.url, item => { 224 | const onList = (item && item.hasOwnProperty(tab.url)) 225 | if (onList) { 226 | chrome.storage.sync.remove(tab.url, () => updateBadge(tab.url, tab.id)) 227 | chrome.runtime.sendMessage({ 228 | 'type': 'remove', 229 | 'url': tab.url 230 | }) 231 | } else { 232 | addPageToList(null, tab) 233 | } 234 | }) 235 | } 236 | }) 237 | }) 238 | } 239 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | 3 | import '../style/options.styl' 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | // Localize! 7 | document.querySelectorAll('[data-localize]').forEach(el => { 8 | el.textContent = chrome.i18n.getMessage(el.dataset.localize) 9 | }) 10 | // Use default value theme = 'light' and animateItems = false if on firefox true on everything else. 11 | const isFirefox = typeof InstallTrigger !== 'undefined' 12 | const defaultSettings = { 13 | settings: { 14 | addContextMenu: true, 15 | addPageAction: true, 16 | animateItems: !isFirefox, 17 | openNewTab: false, 18 | sortOption: '', 19 | sortOrder: '', 20 | theme: 'light', 21 | viewAll: true 22 | } 23 | } 24 | 25 | // Icon in address bar only for firefox does not work in chrome 26 | if (!isFirefox) { 27 | document.getElementById('addPageAction').parentElement.style.display = 'none' 28 | } 29 | 30 | // Saves options to chrome.storage 31 | function saveOptions () { 32 | const theme = document.getElementById('theme').value 33 | const animateItems = document.getElementById('animateItems').checked 34 | const addContextMenu = document.getElementById('addContextMenu').checked 35 | const addPageAction = document.getElementById('addPageAction').checked 36 | const openNewTab = document.getElementById('openNewTab').checked 37 | 38 | // Remove all the context menus 39 | chrome.contextMenus.removeAll(() => { 40 | // If context menu is clicked, add the context menu 41 | if (addContextMenu) { 42 | // Create the context menu from the background page 43 | // see “background.js” 44 | chrome.runtime.getBackgroundPage(bgPage => { 45 | bgPage.createContextMenus() 46 | }) 47 | } 48 | }) 49 | 50 | // Shows/hides pageAction in all tabs 51 | chrome.tabs.query({}, tabs => { 52 | for (let tab of tabs) { 53 | if (addPageAction) { 54 | chrome.pageAction.show(tab.id) 55 | } else { 56 | chrome.pageAction.hide(tab.id) 57 | } 58 | } 59 | }) 60 | 61 | // Get updating the settings on the options page 62 | chrome.storage.sync.get(defaultSettings, items => { 63 | const needsReload = isFirefox && items.settings.theme !== theme 64 | items.settings.theme = theme 65 | items.settings.animateItems = animateItems 66 | items.settings.addContextMenu = addContextMenu 67 | items.settings.addPageAction = addPageAction 68 | items.settings.openNewTab = openNewTab 69 | chrome.storage.sync.set({ 70 | settings: items.settings 71 | }, () => { 72 | if (needsReload) { 73 | chrome.runtime.reload() 74 | } 75 | }) 76 | }) 77 | } 78 | 79 | // Restores select box and checkbox state using the preferences 80 | // stored in chrome.storage. 81 | function restoreOptions () { 82 | chrome.storage.sync.get(defaultSettings, items => { 83 | document.getElementById('theme').value = items.settings.theme 84 | document.getElementById('animateItems').checked = items.settings.animateItems 85 | document.getElementById('addContextMenu').checked = items.settings.addContextMenu 86 | document.getElementById('addPageAction').checked = items.settings.addPageAction 87 | document.getElementById('openNewTab').checked = items.settings.openNewTab 88 | }) 89 | } 90 | 91 | // Helper for export function 92 | let textFile = null 93 | 94 | function makeTextFile (text) { 95 | const data = new Blob([text], {type: 'text/plain'}) 96 | // If we are replacing a previously generated file we need to 97 | // manually revoke the object URL to avoid memory leaks. 98 | if (textFile) { 99 | window.URL.revokeObjectURL(textFile) 100 | } 101 | textFile = window.URL.createObjectURL(data) 102 | return textFile 103 | } 104 | 105 | // Exports app data to a file 106 | function exportFunc () { 107 | // Get the storage element 108 | chrome.storage.sync.get(null, pages => { 109 | const exportText = JSON.stringify(pages) 110 | 111 | // Add storage element to file 112 | const link = document.createElement('a') 113 | link.setAttribute('download', 'readinglist.json') 114 | link.href = makeTextFile(exportText) 115 | document.body.appendChild(link) 116 | 117 | // wait for the link to be added to the document 118 | window.requestAnimationFrame(() => { 119 | const event = new MouseEvent('click') 120 | link.dispatchEvent(event) 121 | document.body.removeChild(link) 122 | }) 123 | }) 124 | } 125 | 126 | const importOrig = document.getElementById('importOrig') 127 | 128 | // Helper for import function 129 | function impLoad () { 130 | const myImportedData = removeAllFavIcons(JSON.parse(this.result)) 131 | // Un-comment this line if we want to replace the storage 132 | // chrome.storage.sync.clear() 133 | // Here the chrome storage is imported, it aggregates the reading list and replaces setting 134 | chrome.storage.sync.set(myImportedData, () => { 135 | restoreOptions() 136 | if (isFirefox) { 137 | chrome.runtime.reload() 138 | } 139 | if (chrome.runtime.lastError) { 140 | console.error(chrome.runtime.lastError) 141 | } 142 | }) 143 | importOrig.value = '' // make sure to clear input value after every import 144 | } 145 | 146 | // Remove favIcons from importList 147 | function removeAllFavIcons (importData) { 148 | for (let index in importData) { 149 | if (importData[index].favIconUrl) { 150 | delete importData[index]['favIconUrl'] 151 | } 152 | } 153 | return importData 154 | } 155 | 156 | // Gets json file and imports to the app 157 | function importFunc (e) { 158 | const files = e.target.files 159 | const reader = new FileReader() 160 | reader.onload = impLoad 161 | reader.readAsText(files[0]) 162 | } 163 | 164 | // Deletes all settings, and items in the app 165 | function confirmDelete () { 166 | const popup = document.getElementById('popup') 167 | popup.style.display = 'block' 168 | popup.style.opacity = 1 169 | document.body.insertBefore(popup, document.body.firstChild) 170 | document.getElementById('ok').onclick = function () { 171 | fade(popup, 10) 172 | chrome.storage.sync.clear(() => { 173 | restoreOptions() 174 | chrome.runtime.sendMessage({ 175 | 'type': 'listUpdated' 176 | }) 177 | }) 178 | } 179 | document.getElementById('cancel').onclick = function () { 180 | fade(popup, 10) 181 | } 182 | } 183 | 184 | // Fades html element 185 | function fade (element, time) { 186 | let op = 1 // initial opacity 187 | const timer = setInterval(() => { 188 | if (op <= 0.1) { 189 | clearInterval(timer) 190 | element.style.display = 'none' 191 | } 192 | element.style.opacity = op 193 | element.style.filter = 'alpha(opacity=' + op * 100 + ')' 194 | op -= op * 0.1 195 | }, time) 196 | } 197 | 198 | function accordion () { 199 | this.classList.toggle('active') 200 | const panel = this.nextElementSibling 201 | if (panel.style.maxHeight) { 202 | panel.style.maxHeight = null 203 | } else { 204 | panel.style.maxHeight = panel.scrollHeight + 'px' 205 | } 206 | } 207 | 208 | restoreOptions() 209 | 210 | const importBtn = document.getElementById('importBtn') 211 | const exportBtn = document.getElementById('exportBtn') 212 | const resetBtn = document.getElementById('resetBtn') 213 | 214 | // Import listeners 215 | importOrig.addEventListener('change', importFunc, false) 216 | importBtn.onclick = () => { 217 | importOrig.click() 218 | } 219 | // Export listener 220 | exportBtn.addEventListener('click', exportFunc, false) 221 | // Reset button listener 222 | resetBtn.addEventListener('click', confirmDelete, false) 223 | // Advanced settings listener, opens accordion 224 | document.getElementById('advanced').addEventListener('click', accordion) 225 | // Listeners to save options when changed 226 | document.getElementById('theme').addEventListener('change', saveOptions) 227 | document.getElementById('animateItems').addEventListener('click', saveOptions) 228 | document.getElementById('addContextMenu').addEventListener('click', saveOptions) 229 | document.getElementById('addPageAction').addEventListener('click', saveOptions) 230 | document.getElementById('openNewTab').addEventListener('click', saveOptions) 231 | }) 232 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | 3 | import list from './readinglist' 4 | import nativesortable from 'nativesortable' 5 | import '../style/popup.styl' 6 | 7 | document.addEventListener('DOMContentLoaded', () => { 8 | // Localize! 9 | document.querySelectorAll('[data-localize]').forEach(el => { 10 | el.textContent = chrome.i18n.getMessage(el.dataset.localize) 11 | }) 12 | 13 | const searchBar = document.getElementById('my-search') 14 | searchBar.setAttribute('placeholder', chrome.i18n.getMessage('Search')) 15 | 16 | const RL = document.getElementById('reading-list') 17 | 18 | RL.addEventListener('animationend', e => { 19 | if (e.target.parentNode) { 20 | e.target.parentNode.classList.remove('slidein') 21 | } 22 | }) 23 | 24 | const isFirefox = typeof InstallTrigger !== 'undefined' 25 | const defaultSettings = { 26 | settings: { 27 | addContextMenu: true, 28 | addPageAction: true, 29 | animateItems: !isFirefox, 30 | openNewTab: false, 31 | sortOption: '', 32 | sortOrder: '', 33 | theme: 'light', 34 | viewAll: true 35 | } 36 | } 37 | 38 | chrome.storage.sync.get(defaultSettings, store => { 39 | const settings = store.settings 40 | document.body.classList.add(settings.theme || 'light') 41 | 42 | // Sets the all/unread button 43 | document.getElementById(settings.viewAll ? 'all' : 'unread').classList.add('active') 44 | // Set sort button 45 | if (settings.sortOption) { 46 | document.getElementById(settings.sortOption).classList.add('active') 47 | document.getElementById(settings.sortOption).lastElementChild.classList.add('arrow', settings.sortOrder) 48 | } 49 | 50 | // Sets the list of items which are shown 51 | if (settings.viewAll) { 52 | document.getElementById('reading-list').classList.remove('unread-only') 53 | } else { 54 | document.getElementById('reading-list').classList.add('unread-only') 55 | } 56 | 57 | if (settings.animateItems) { 58 | // Wait a bit before rendering the reading list 59 | // Gives the popup window time to render, preventing weird resizing bugs 60 | // See: https://bugs.chromium.org/p/chromium/issues/detail?id=457887 61 | window.setTimeout(list.renderReadingList, 150, RL, true, settings) 62 | } else { 63 | list.renderReadingList(RL, false, settings) 64 | } 65 | 66 | nativesortable(RL, { 67 | change: function () { 68 | list.updateIndex(RL) 69 | chrome.runtime.sendMessage({ 70 | 'type': 'orderChanged' 71 | }) 72 | }, 73 | childClass: 'sortable-child', 74 | draggingClass: 'sortable-dragging', 75 | overClass: 'sortable-over' 76 | }) 77 | }) 78 | 79 | // Listen for click events in the reading list 80 | RL.addEventListener('click', list.onReadingItemClick) 81 | 82 | const searchbox = document.getElementById('my-search') 83 | 84 | if (searchbox) { 85 | // Filter reading list based on search box 86 | searchbox.addEventListener('keyup', list.filterReadingList) 87 | } 88 | 89 | // The button for adding pages to the reading list 90 | const savepageButton = document.getElementById('savepage') 91 | 92 | if (savepageButton) { 93 | // Save the page open in the current tab to the reading list 94 | savepageButton.addEventListener('click', () => { 95 | const queryInfo = {active: true, currentWindow: true} 96 | 97 | chrome.tabs.query(queryInfo, tabs => { 98 | list.addReadingItem(tabs[0], RL) 99 | }) 100 | }) 101 | } 102 | 103 | // Listen for click events in the sidebar button 104 | // Hide if not Firefox 105 | const sidebarButton = document.getElementById('open-sidebar') 106 | if (sidebarButton) { 107 | if (isFirefox) { 108 | let sidebarIsOpen = false 109 | if (window.browser.sidebarAction.hasOwnProperty('isOpen')) { 110 | window.browser.sidebarAction.isOpen({}).then(result => { 111 | sidebarIsOpen = result 112 | }) 113 | } 114 | document.getElementById('open-sidebar').addEventListener('click', () => { 115 | if (sidebarIsOpen) { 116 | chrome.sidebarAction.close() 117 | sidebarIsOpen = false 118 | } else { 119 | chrome.sidebarAction.open() 120 | sidebarIsOpen = true 121 | } 122 | }) 123 | } else { 124 | sidebarButton.style.display = 'none' 125 | } 126 | } 127 | 128 | // Listen for click events in the settings 129 | document.getElementById('settings').addEventListener('click', () => { 130 | if (chrome.runtime.openOptionsPage) { 131 | // New way to open options pages, if supported (Chrome 42+). 132 | chrome.runtime.openOptionsPage() 133 | const isPopup = document.body.classList.contains('popup-page') 134 | if (isPopup) { 135 | window.close() 136 | } 137 | } else { 138 | // Reasonable fallback. 139 | window.open(chrome.runtime.getURL('/options.html')) 140 | } 141 | }) 142 | 143 | document.getElementById('all').addEventListener('click', list.changeFilter) 144 | document.getElementById('unread').addEventListener('click', list.changeFilter) 145 | 146 | document.getElementById('date').addEventListener('click', list.sortItems) 147 | document.getElementById('title').addEventListener('click', list.sortItems) 148 | 149 | chrome.runtime.onMessage.addListener((request) => { 150 | let currentItem = null 151 | if (request.hasOwnProperty('url')) { 152 | currentItem = document.getElementById(request.url) 153 | } 154 | 155 | if (request.type === 'add') { 156 | if (currentItem) { 157 | list.removeReadingItem(null, currentItem.parentNode) 158 | } 159 | 160 | // Create the reading item element 161 | const readingItemEl = list.createReadingItemEl(request.info) 162 | 163 | // Add the animation class 164 | readingItemEl.className += ' slidein' 165 | 166 | // Add it to the top of the reading list 167 | RL.insertBefore(readingItemEl, RL.firstChild) 168 | } else if (request.type === 'remove') { 169 | if (currentItem) { 170 | list.removeReadingItem(null, currentItem.parentNode) 171 | } 172 | } else if (request.type === 'update') { 173 | // If updated replace current item with a new one 174 | if (currentItem) { 175 | RL.insertBefore(list.createReadingItemEl(request.info), currentItem.parentNode) 176 | currentItem.parentNode.remove() 177 | } 178 | } else if (request.type === 'orderChanged' || request.type === 'listUpdated') { 179 | while (RL.firstChild) { 180 | RL.removeChild(RL.firstChild) 181 | } 182 | 183 | chrome.storage.sync.get(defaultSettings, store => { 184 | list.updateFilterButton(store.settings.viewAll) 185 | list.updateSortButton(store.settings.sortOption, store.settings.sortOrder) 186 | list.renderReadingList(RL, false, store.settings) 187 | }) 188 | } 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /src/js/readinglist.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | import Fuse from 'fuse.js' 3 | 4 | /** 5 | * Create and return the DOM element for a reading list item. 6 | * 7 | *