├── .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 | ![Chrome Reading List extension](src/images/search-screenshot.png) 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 ![Chrome Reading List icon](src/icons/icon32.png) 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 | *
8 | * 9 | * 10 | * 11 | *
12 | * 13 | *
14 | *
15 | * × 16 | *
17 | * 18 | * @param {object} info - object with url, title, and favIconUrl 19 | */ 20 | function createReadingItemEl (info) { 21 | const url = info.url 22 | const title = info.title 23 | const favIconUrl = `https://icons.duckduckgo.com/ip2/${new URL(info.url).hostname}.ico` 24 | const item = document.createElement('div') 25 | item.className = 'reading-item' 26 | 27 | if (info.favIconUrl) { 28 | removeAllFavIcons() 29 | } 30 | 31 | if (info.shiny) { 32 | item.className += ' shiny' 33 | } 34 | 35 | if (info.viewed) { 36 | item.classList.add('read') 37 | } else { 38 | item.classList.add('unread') 39 | } 40 | 41 | const link = document.createElement('a') 42 | link.className = 'item-link' 43 | link.href = url 44 | 45 | const linkTitle = document.createElement('span') 46 | linkTitle.className = 'title' 47 | linkTitle.textContent = title || '?' 48 | link.appendChild(linkTitle) 49 | 50 | const linkHost = document.createElement('span') 51 | linkHost.classList.add('host') 52 | linkHost.textContent = link.hostname || url 53 | link.appendChild(linkHost) 54 | 55 | if (favIconUrl && /^(https?:\/\/|\/icons\/)/.test(favIconUrl)) { 56 | const favicon = document.createElement('div') 57 | favicon.classList.add('favicon') 58 | const faviconImg = document.createElement('img') 59 | faviconImg.classList.add('favicon-img') 60 | faviconImg.onerror = () => faviconImg.classList.add('error') 61 | faviconImg.setAttribute('src', favIconUrl) 62 | favicon.appendChild(faviconImg) 63 | link.appendChild(favicon) 64 | } 65 | 66 | const delBtn = document.createElement('a') 67 | delBtn.textContent = '×' 68 | delBtn.id = url 69 | delBtn.classList.add('delete-button') 70 | item.appendChild(link) 71 | item.appendChild(delBtn) 72 | 73 | const editBtn = document.createElement('a') 74 | editBtn.value = url 75 | editBtn.classList.add('edit-button') 76 | item.appendChild(editBtn) 77 | 78 | const editImg = document.createElement('img') 79 | editImg.classList.add('edit-img') 80 | loadSVG('/icons/pencil.svg', editImg) 81 | editBtn.appendChild(editImg) 82 | item.appendChild(link) 83 | 84 | return item 85 | } 86 | 87 | /** 88 | * Temporary remove after version 5 89 | */ 90 | function removeAllFavIcons () { 91 | chrome.storage.sync.get(null, pages => { 92 | for (let index in pages) { 93 | if (pages[index].favIconUrl) { 94 | delete pages[index]['favIconUrl'] 95 | } 96 | } 97 | chrome.storage.sync.set(pages) 98 | }) 99 | } 100 | 101 | /** 102 | * Get the reading list from storage 103 | * 104 | * @param {function(array)} callback - called with an array of reading 105 | * list items 106 | */ 107 | function getReadingList (callback) { 108 | if (typeof callback !== 'function') { 109 | return 110 | } 111 | 112 | chrome.storage.sync.get(null, pages => { 113 | const settings = pages.settings 114 | delete pages['settings'] 115 | delete pages['index'] 116 | let pageList = [] 117 | 118 | for (let page in pages) { 119 | if (pages.hasOwnProperty(page)) { 120 | pageList.push(pages[page]) 121 | } 122 | } 123 | pageList.sort((a, b) => { 124 | if (b.index === a.index) { 125 | return b.addedAt - a.addedAt 126 | } 127 | return a.index - b.index 128 | }) 129 | // Ask for a review! 130 | if (pageList.length >= 6 && !settings.askedForReview) { 131 | settings.askedForReview = true 132 | const reviewUrl = isFirefox 133 | ? 'https://addons.mozilla.org/en-US/firefox/addon/reading_list/' 134 | : 'https://chrome.google.com/webstore/detail/reading-list/lloccabjgblebdmncjndmiibianflabo/reviews' 135 | 136 | const reviewReadingListItem = { 137 | title: 'Like the Reading List? Give us a review!', 138 | url: reviewUrl, 139 | shiny: true, 140 | addedAt: Date.now(), 141 | index: 0, 142 | favIconUrl: '/icons/icon48.png' 143 | } 144 | pageList.unshift(reviewReadingListItem) 145 | 146 | const setObj = { 147 | settings 148 | } 149 | setObj[reviewUrl] = reviewReadingListItem 150 | chrome.storage.sync.set(setObj) 151 | } 152 | callback(pageList) 153 | }) 154 | } 155 | 156 | /** 157 | * Updates the reading list index 158 | * 159 | * @param {Element} readingListEl - reading list DOM element 160 | */ 161 | function updateIndex (readingListEl) { 162 | chrome.storage.sync.get(null, pages => { 163 | readingListEl.querySelectorAll('.item-link').forEach((el, i) => { 164 | pages[el.getAttribute('href')].index = i + 1 165 | }) 166 | if (pages['index']) { 167 | delete pages['index'] 168 | chrome.storage.sync.remove('index') 169 | } 170 | chrome.storage.sync.set(pages) 171 | }) 172 | } 173 | 174 | /** 175 | * Sets the counts on the all and unread buttons 176 | * @param {array} pageList - list of reading items 177 | */ 178 | function setCount (pageList) { 179 | document.getElementById('all-count').textContent = `${pageList.length}` 180 | document.getElementById('unread-count').textContent = `${pageList.filter(page => !page.viewed).length}` 181 | } 182 | 183 | /** 184 | * Render the reading list 185 | * 186 | * @param {Element} readingListEl - reading list DOM element 187 | * @param {boolean} animateItems - animate incoming reading items? 188 | * @param {boolean} viewAll - view all items or not 189 | * @param {function()} callback - called when the list is rendered 190 | */ 191 | function renderReadingList (readingListEl, animateItems, settings) { 192 | getReadingList(pageList => { 193 | readingListEl.innerHTML = '' 194 | setCount(pageList) 195 | const sortedReadingList = sortReadingList(pageList, settings) 196 | const numItems = sortedReadingList.length 197 | 198 | // Animate up to 10 items 199 | const animateCount = animateItems ? 10 : 0 200 | const itemsToAnimate = (animateCount > numItems) ? numItems : animateCount 201 | let counter = 0 202 | let itemsAnimated = 0 203 | 204 | // Wait a bit, then create a DOM element for the next reading list item, 205 | // then recurse 206 | function waitAndCreate (waitTime) { 207 | // Stop if all items have been rendered. 208 | if (counter >= numItems) { 209 | return 210 | } 211 | 212 | // If we’ve rendered all the animated items 213 | if (itemsAnimated >= itemsToAnimate) { 214 | // Render any remaining items 215 | for (let i = counter; i < numItems; i++) { 216 | readingListEl.appendChild(createReadingItemEl(sortedReadingList[i])) 217 | } 218 | return 219 | } 220 | 221 | // Wait a bit, then make a reading item 222 | window.setTimeout(() => { 223 | const readingItemEl = createReadingItemEl(sortedReadingList[counter]) 224 | 225 | // Increment the animated counter if item is viewable 226 | if (!sortedReadingList[counter].viewed || (settings && settings.viewAll)) { 227 | // Add the “slidein” class for animation 228 | readingItemEl.classList.add('slidein') 229 | itemsAnimated++ 230 | } 231 | readingListEl.appendChild(readingItemEl) 232 | 233 | // Increment the counter 234 | counter++ 235 | waitTime = parseInt(waitTime * ((itemsToAnimate - counter) / itemsToAnimate), 10) 236 | 237 | // Recurse! 238 | waitAndCreate(waitTime) 239 | }, waitTime) 240 | } 241 | 242 | waitAndCreate(150) 243 | }) 244 | } 245 | 246 | function sortReadingList (pageList, settings) { 247 | if (!settings.sortOption) { 248 | return pageList 249 | } else { 250 | return pageList.sort((a, b) => { 251 | if (settings.sortOption === 'date') { 252 | return compareDate(a, b, settings.sortOrder) 253 | } else { 254 | return compareTitle(a, b, settings.sortOrder) 255 | } 256 | }) 257 | } 258 | 259 | function compareTitle (a, b, order) { 260 | if (order === 'up') { 261 | return b.title.localeCompare(a.title, undefined, {numeric: true, sensitivity: 'base'}) 262 | } else { 263 | return a.title.localeCompare(b.title, undefined, {numeric: true, sensitivity: 'base'}) 264 | } 265 | } 266 | 267 | function compareDate (a, b, order) { 268 | if (order === 'up') { 269 | return a.addedAt - b.addedAt 270 | } else { 271 | return b.addedAt - a.addedAt 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * Add an item to the reading list 278 | * 279 | * @param {object} info - page to add’s url, title, and favIconUrl 280 | * @param {Element} readingListEl - reading list DOM element 281 | * @param {function(object)} callback - called when the item is added 282 | */ 283 | function addReadingItem (info, readingListEl, callback) { 284 | if (!info.url) { 285 | return 286 | } 287 | 288 | // Handles all firefox preference pages 289 | if (info.url.startsWith('about')) { 290 | if (info.url.includes('http')) { 291 | const url = info.url.replace(/about:\w+\?url=/g, '') 292 | info.url = decodeURIComponent(url) 293 | } else { 294 | return 295 | } 296 | } 297 | 298 | // Restrict info’s values 299 | info = { 300 | url: info.url, 301 | title: info.title, 302 | addedAt: Date.now(), 303 | viewed: false 304 | } 305 | 306 | // Object for setting the storage 307 | const setObj = {} 308 | setObj[info.url] = info 309 | 310 | chrome.storage.sync.set(setObj, () => { 311 | if (chrome.runtime.lastError) { 312 | console.error(chrome.runtime.lastError) 313 | } 314 | 315 | // If the readingListEl was passed, create the DOM element for the 316 | // reading item 317 | let readingItemEl 318 | if (readingListEl) { 319 | // Look for a delete button with the ID of the url 320 | const currentItem = document.getElementById(info.url) 321 | 322 | // If it exists, remove it from the list (prevents duplicates) 323 | if (currentItem) { 324 | removeReadingItem(null, currentItem.parentNode) 325 | } 326 | 327 | // Create the reading item element 328 | readingItemEl = createReadingItemEl(info) 329 | 330 | // Add the animation class 331 | readingItemEl.className += ' slidein' 332 | 333 | // Add it to the top of the reading list 334 | readingListEl.insertBefore(readingItemEl, readingListEl.firstChild) 335 | 336 | updateIndex(readingListEl) 337 | } 338 | 339 | chrome.runtime.sendMessage({ 340 | 'type': 'add', 341 | 'url': info.url, 342 | 'info': info 343 | }) 344 | 345 | // Add the “✔” to the badge for matching tabs 346 | const queryInfo = {url: info.url.replace(/#.*/, '')} 347 | 348 | chrome.tabs.query(queryInfo, tabs => { 349 | for (let tab of tabs) { 350 | // If the URL is identical, add the “✔” to the badge 351 | if (tab.url === info.url && tab.id) { 352 | chrome.browserAction.setBadgeText({ 353 | text: '✔', 354 | tabId: tab.id 355 | }) 356 | } 357 | } 358 | 359 | if (typeof callback === 'function') { 360 | callback(info, readingItemEl) 361 | } 362 | getReadingList(pageList => { 363 | setCount(pageList) 364 | }) 365 | }) 366 | }) 367 | } 368 | 369 | /** 370 | * Remove a reading list item from the DOM, storage, or both 371 | * 372 | * @param {string} url (optional) - URL of the page to remove 373 | * @param {Element} element - (optional) reading list item 374 | */ 375 | function removeReadingItem (url, element) { 376 | // If url is truthy, remove the item from storage 377 | if (url) { 378 | chrome.storage.sync.remove(url, () => { 379 | // Find tabs with the reading item’s url 380 | const queryInfo = {url: url.replace(/#.*/, '')} 381 | 382 | chrome.tabs.query(queryInfo, tabs => { 383 | for (let tab of tabs) { 384 | // If the URL is identical, remove the “✔” from the badge 385 | if (tab.url === url) { 386 | chrome.browserAction.setBadgeText({ 387 | text: '', 388 | tabId: tab.id 389 | }) 390 | } 391 | } 392 | }) 393 | getReadingList(pageList => { 394 | setCount(pageList) 395 | }) 396 | }) 397 | 398 | chrome.runtime.sendMessage({ 399 | 'type': 'remove', 400 | 'url': url 401 | }) 402 | } 403 | 404 | // If element is truthy, remove the element 405 | if (element) { 406 | // Listen for the end of an animation 407 | element.addEventListener('animationend', () => { 408 | // Remove the item from the DOM when the animation is finished 409 | const readingListEl = element.parentNode 410 | element.remove() 411 | updateIndex(readingListEl) 412 | }) 413 | 414 | // Add the class to start the animation 415 | element.className += ' slideout' 416 | } 417 | } 418 | 419 | /** 420 | * Open the reading item 421 | * 422 | * @param {string} url - URL to open 423 | * @param {boolean} newTab - open in a new tab? 424 | */ 425 | function openLink (url, newTab) { 426 | if (newTab) { 427 | // Create a new tab with the URL 428 | chrome.tabs.create({url: url, active: false}) 429 | } else { 430 | // Query for the active tab 431 | chrome.tabs.query({ 432 | active: true, 433 | currentWindow: true 434 | }, tabs => { 435 | const tab = tabs[0] 436 | 437 | // Update the URL of the current tab 438 | chrome.tabs.update(tab.id, {url: url}) 439 | 440 | // Close the popup 441 | const isPopup = document.body.classList.contains('popup-page') 442 | if (isPopup) { 443 | window.close() 444 | } 445 | }) 446 | } 447 | } 448 | 449 | /** 450 | * Open or delete reading items (click event listener) 451 | * 452 | * @param {Event} e - click event 453 | */ 454 | function onReadingItemClick (e) { 455 | const isPopup = document.body.classList.contains('popup-page') 456 | const isSidebar = document.body.classList.contains('sidebar-page') 457 | let target = e.target 458 | if (target.tagName === 'INPUT') { 459 | e.preventDefault() 460 | return 461 | } 462 | 463 | // Set target to the closest a as we are using those to decide what to do 464 | target = target.closest('a') 465 | 466 | // If the target is a delete button, remove the reading item 467 | // Or if the target is a edit button, edit the title 468 | if (target.classList.contains('delete-button')) { 469 | removeReadingItem(target.id, target.parentNode) 470 | } else if (target.classList.contains('edit-button')) { 471 | switchToInput(target.parentNode) 472 | } else if ((isPopup || isSidebar) && target.classList.contains('item-link')) { 473 | e.preventDefault() 474 | chrome.storage.sync.get(defaultSettings, items => { 475 | // If the control or meta key (⌘ on Mac, ⊞ on Windows) is pressed or if options is selected… 476 | const modifierDown = (e.ctrlKey || e.metaKey || items.settings.openNewTab) 477 | openLink(target.href, modifierDown) 478 | }) 479 | } 480 | } 481 | 482 | /** 483 | * Filter the reading list DOM elements by search param 484 | * 485 | * @param {Event} e - keyup event 486 | */ 487 | function filterReadingList (e) { 488 | const options = { 489 | keys: ['title', 'url'], 490 | tokenize: true, 491 | threshold: 0.4 492 | } 493 | let displayAll = false 494 | // If nothing is being searched in list return. 495 | if (this.value.trim().length === 0) { 496 | displayAll = true 497 | } 498 | 499 | getReadingList(pageList => { 500 | const readingList = document.getElementsByClassName('reading-item') 501 | // Sort reading list by most recent to least recent 502 | const fuse = new Fuse(pageList, options) 503 | const filterList = fuse.search(this.value) 504 | 505 | // Loop through reading list items to see if they match search text 506 | for (let item of readingList) { 507 | let display = false 508 | for (let filteredItem of filterList) { 509 | const url = item.querySelector('.item-link').getAttribute('href') 510 | if (url === filteredItem.url) { 511 | display = true 512 | } 513 | } 514 | item.style.display = display || displayAll ? '' : 'none' 515 | } 516 | }) 517 | } 518 | 519 | /** 520 | * Toggles the buttons, and updates the options for which reading list to view. 521 | */ 522 | function changeFilter () { 523 | const viewAll = this.id === 'all' 524 | updateViewAllSetting(viewAll) 525 | updateFilterButton(viewAll) 526 | } 527 | 528 | /** 529 | * Saves viewAll option to chrome.storage 530 | * @param {boolean} viewAll The boolean value to set if all items have been viewed 531 | */ 532 | function updateViewAllSetting (viewAll) { 533 | chrome.storage.sync.get(defaultSettings, items => { 534 | items.settings.viewAll = viewAll 535 | chrome.storage.sync.set({ 536 | settings: items.settings 537 | }) 538 | chrome.runtime.sendMessage({ 539 | 'type': 'orderChanged' 540 | }) 541 | }) 542 | } 543 | 544 | /** 545 | * Updates the filter button 546 | * @param {boolean} viewAll - true is show all and false is show only unread 547 | */ 548 | function updateFilterButton (viewAll) { 549 | // Clear existing filter 550 | const filterButtons = document.querySelectorAll('div.filter button') 551 | for (let button of filterButtons) { 552 | button.classList.remove('active') 553 | } 554 | // Update the button on display 555 | if (viewAll) { 556 | document.getElementById('all').classList.add('active') 557 | document.getElementById('reading-list').classList.remove('unread-only') 558 | } else { 559 | document.getElementById('unread').classList.add('active') 560 | document.getElementById('reading-list').classList.add('unread-only') 561 | } 562 | } 563 | 564 | /** 565 | * Toggles the buttons, and updates the options for which reading list to view. 566 | */ 567 | function sortItems () { 568 | const childClassSortOrder = this.lastElementChild.classList 569 | let sortOrder 570 | let sortOption = this.id 571 | if (this.classList.contains('active')) { 572 | if (childClassSortOrder.contains('down')) { 573 | sortOrder = 'up' 574 | } else { 575 | sortOrder = null 576 | sortOption = null 577 | } 578 | } else { 579 | sortOrder = 'down' 580 | } 581 | updateSortSetting(sortOption, sortOrder) 582 | updateSortButton(sortOption, sortOrder) 583 | } 584 | 585 | /** 586 | * Updates the sort buttons 587 | * @param {string} sortOption - the option date or title 588 | * @param {string} sortOrder - the order up or down 589 | */ 590 | function updateSortButton (sortOption, sortOrder) { 591 | // Clear existing sort 592 | const sortButtons = document.querySelectorAll('div.sort button') 593 | for (let button of sortButtons) { 594 | button.classList.remove('active') 595 | button.lastElementChild.classList.remove(...button.lastElementChild.classList) 596 | } 597 | // Update sort buttons 598 | if (sortOption) { 599 | document.getElementById(sortOption).classList.add('active') 600 | document.getElementById(sortOption).lastElementChild.classList.add('arrow', sortOrder) 601 | } 602 | } 603 | 604 | /** 605 | * Saves sort option to chrome.storage 606 | * @param {string} sortOption The sort function 607 | * @param {string} sortOrder The sort function 608 | */ 609 | function updateSortSetting (sortOption, sortOrder) { 610 | const readingList = document.getElementById('reading-list') 611 | chrome.storage.sync.get(defaultSettings, store => { 612 | store.settings.sortOption = sortOption 613 | store.settings.sortOrder = sortOrder 614 | chrome.storage.sync.set({ 615 | settings: store.settings 616 | }) 617 | chrome.runtime.sendMessage({ 618 | 'type': 'orderChanged' 619 | }) 620 | renderReadingList(readingList, false, store.settings) 621 | }) 622 | } 623 | 624 | const isFirefox = typeof InstallTrigger !== 'undefined' 625 | const defaultSettings = { 626 | settings: { 627 | theme: 'light', 628 | addContextMenu: true, 629 | addPageAction: true, 630 | animateItems: !isFirefox, 631 | openNewTab: false, 632 | sortOption: '', 633 | sortOrder: '', 634 | viewAll: true 635 | } 636 | } 637 | 638 | /** 639 | * Update a reading list item's title 640 | * 641 | * @param {string} url - url of a reading list item 642 | * @param {string} title - title of a reading list item 643 | */ 644 | function setReadingItemTitle (url, title) { 645 | chrome.storage.sync.get(url, page => { 646 | page[url].title = title 647 | chrome.storage.sync.set(page) 648 | chrome.runtime.sendMessage({ 649 | 'type': 'update', 650 | 'url': url, 651 | 'info': page[url] 652 | }) 653 | }) 654 | } 655 | 656 | /** 657 | * Makes the title of the reading list item editable 658 | * @param {Element} element The reading list item being edited 659 | */ 660 | function switchToInput (element) { 661 | // Show overlay 662 | const overlay = document.getElementById('overlay') 663 | overlay.style.display = 'block' 664 | 665 | // Change pencil to a disk for save 666 | const button = element.querySelector('.edit-button') 667 | button.classList.add('store-button') 668 | button.classList.remove('edit-button') 669 | const image = element.querySelector('svg.edit-img') 670 | loadSVG('/icons/save.svg', image) 671 | 672 | // Replace the span with input 673 | const title = element.querySelector('span.title') 674 | const input = document.createElement('input') 675 | input.classList.add('edit-title') 676 | input.value = title.textContent 677 | input.original = title.textContent 678 | title.replaceWith(input) 679 | 680 | // Event listeners for when title is changed 681 | overlay.addEventListener('click', (e) => { 682 | switchToSpan(e, input, button) 683 | }) 684 | input.addEventListener('keydown', (e) => { 685 | switchToSpan(e, input, button) 686 | }) 687 | input.select() 688 | } 689 | 690 | /** 691 | * Switching the editable reading list item title to a span 692 | * 693 | * @param {Event} e - blur/keydown event 694 | * @param {Element} input - title box 695 | * @param {Element} button - button being pushed 696 | */ 697 | function switchToSpan (e, input, button) { 698 | let doSave = true 699 | // If not enter or escape on key down return 700 | if (e.key && e.key !== 'Enter' && e.key !== 'Escape') { 701 | return 702 | } 703 | // If escape do not save the title 704 | if (e.key === 'Escape') { 705 | e.preventDefault() 706 | doSave = false 707 | } 708 | 709 | // Remove Overlay 710 | document.getElementById('overlay').style.display = 'none' 711 | 712 | // Change the button from a disk to a pencil 713 | // let button = this.parentNode.parentNode.querySelector('.store-button') 714 | button.classList.add('edit-button') 715 | button.classList.remove('store-button') 716 | const url = button.value 717 | const title = doSave ? input.value : input.original 718 | const image = input.parentNode.parentNode.querySelector('svg.edit-img') 719 | loadSVG('/icons/pencil.svg', image) 720 | image.src = '/icons/pencil.svg' 721 | 722 | // Change the title back to a span 723 | const span = document.createElement('span') 724 | span.textContent = title 725 | span.classList.add('title') 726 | input.replaceWith(span) 727 | 728 | // Update the reading item 729 | if (doSave) { 730 | setReadingItemTitle(url, title) 731 | } 732 | } 733 | 734 | /** 735 | * Loads svg to a dom element 736 | * @param {string} url the url of the svg 737 | * @param {Element} element the element to be replaced with the svg 738 | */ 739 | function loadSVG (url, element) { 740 | const imgClass = element.getAttribute('class') 741 | 742 | const xhr = new XMLHttpRequest() 743 | xhr.onreadystatechange = function () { 744 | if (xhr.readyState === 4 && xhr.status === 200) { 745 | const svg = xhr.responseXML.getElementsByTagName('svg')[0] 746 | 747 | if (imgClass) { 748 | svg.setAttribute('class', imgClass + ' replaced-svg') 749 | } 750 | 751 | svg.removeAttribute('xmlns:a') 752 | 753 | if (!svg.hasAttribute('viewBox') && svg.hasAttribute('height') && svg.hasAttribute('width')) { 754 | svg.setAttribute('viewBox', '0 0 ' + svg.getAttribute('height') + ' ' + svg.getAttribute('width')) 755 | } 756 | element.parentElement.replaceChild(svg, element) 757 | } 758 | } 759 | 760 | xhr.open('GET', url, true) 761 | xhr.send(null) 762 | } 763 | 764 | export default { 765 | addReadingItem, 766 | changeFilter, 767 | createReadingItemEl, 768 | filterReadingList, 769 | onReadingItemClick, 770 | removeReadingItem, 771 | renderReadingList, 772 | sortItems, 773 | updateFilterButton, 774 | updateIndex, 775 | updateSortButton 776 | } 777 | -------------------------------------------------------------------------------- /src/manifest/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "__MSG_appName__", 5 | "description": "__MSG_appDesc__", 6 | "default_locale": "en", 7 | "icons": { 8 | "16": "icons/icon16.png", 9 | "32": "icons/icon32.png", 10 | "48": "icons/icon48.png", 11 | "128": "icons/icon128.png" 12 | }, 13 | "browser_action": { 14 | "default_popup": "popup.html", 15 | "default_icon": { 16 | "16": "icons/icon16.png", 17 | "32": "icons/icon32.png" 18 | } 19 | }, 20 | "background": { 21 | "scripts": [ 22 | "background.bundle.js" 23 | ] 24 | }, 25 | "permissions": [ 26 | "tabs", 27 | "contextMenus", 28 | "storage" 29 | ], 30 | "options_ui": { 31 | "page": "options.html" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/manifest/chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "minimum_chrome_version": "20", 3 | "options_page": "options.html", 4 | "options_ui": { 5 | "chrome_style": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/manifest/edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "minimum_edge_version" : "40", 3 | "options_page": "options.html" 4 | } 5 | -------------------------------------------------------------------------------- /src/manifest/firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "page_action": { 3 | "browser_style": true, 4 | "default_icon": { 5 | "16": "icons/icon16.png", 6 | "32": "icons/icon32.png" 7 | } 8 | }, 9 | "sidebar_action": { 10 | "default_title": "__MSG_appName__", 11 | "default_panel": "sidebar.html", 12 | "default_icon": { 13 | "16": "icons/icon16.png", 14 | "32": "icons/icon32.png" 15 | } 16 | }, 17 | "options_ui": { 18 | "browser_style": true 19 | }, 20 | "applications": { 21 | "gecko": { 22 | "id": "leetcat@cs.ubc.ca", 23 | "strict_min_version": "57.0" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 | 19 |
20 | 21 |
22 | 26 |
27 | 28 |
29 | 33 |
34 | 35 |
36 | 40 |
41 | 42 |
43 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reading List 7 | 8 | 9 |
10 |
11 | 14 |

15 | 16 | 19 |
20 | 21 | 24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reading List 7 | 8 | 9 |
10 |
11 |

12 | 13 | 16 |
17 | 18 | 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/style/base/animations.styl: -------------------------------------------------------------------------------- 1 | .slidein 2 | animation: 0.2s linear slidein 3 | 4 | .slidein .item-link 5 | animation: 0.8s ease-out slidein-bounce 6 | 7 | @keyframes slidein-bounce 8 | 0% 9 | transform: translateX(100%) scaleY(0) 10 | 40% 11 | transform: translateX(100%) scaleY(0) 12 | 50% 13 | transform: translateX(30px) scaleY(1) 14 | 60% 15 | transform: translateX(0) scaleY(1) 16 | 80% 17 | transform: translateX(35px) scaleY(1) 18 | 100% 19 | transform: translateX(0) scaleY(1) 20 | 21 | @keyframes slidein 22 | 0% 23 | max-height: 0px 24 | transform: translateX(100%) scaleY(0) 25 | 80% 26 | max-height: 100px 27 | 100% 28 | transform: translateX(0) scaleY(1) 29 | 30 | .slideout 31 | margin: 0 32 | animation: 0.65s slideout 33 | 34 | @keyframes slideout 35 | 0% 36 | max-height: 100px 37 | transform: translateX(0) scaleY(1) 38 | 40% 39 | max-height: 0px 40 | 100% 41 | max-height: 0px 42 | transform: translateX(100%) scaleY(0) 43 | -------------------------------------------------------------------------------- /src/style/base/index.styl: -------------------------------------------------------------------------------- 1 | @import 'variables' 2 | @import 'layout' 3 | @import 'typography' 4 | @import 'reading-item' 5 | @import 'animations' 6 | -------------------------------------------------------------------------------- /src/style/base/layout.styl: -------------------------------------------------------------------------------- 1 | html 2 | box-sizing: border-box 3 | 4 | *, 5 | *:before, 6 | *:after 7 | box-sizing: inherit 8 | 9 | body 10 | margin: 0 11 | padding: 0 12 | overflow-x: hidden 13 | 14 | .container 15 | position: relative 16 | padding: 0 spacer 17 | max-width: 100% 18 | margin: 0 19 | 20 | .popup-page 21 | .container 22 | width: containerWidth 23 | 24 | .sidebar-page 25 | .container 26 | width: 100% 27 | 28 | // Header 29 | .header 30 | width: 100% 31 | display: flex 32 | justify-content: center 33 | align-items: center 34 | padding: 30px 0 10px 35 | margin-bottom: 10px 36 | 37 | // Searchbox 38 | .search 39 | display: flex 40 | align-items: center 41 | padding-bottom: 10px 42 | 43 | .search input[type="search"] 44 | width: 100% 45 | 46 | // Save button 47 | .save-button 48 | width: 30px 49 | height: 30px 50 | line-height: 30px 51 | margin: 0 10px 52 | font-weight: bold 53 | border: 0 54 | border-radius: 50% 55 | text-align: center 56 | &:hover, 57 | &:focus 58 | cursor: pointer 59 | outline: none 60 | 61 | // Settings button 62 | .settings-button 63 | position: absolute 64 | top: spacer 65 | right: spacer 66 | width: spacer 67 | height: spacer 68 | padding: 0 69 | border: 0 70 | background-color: transparent 71 | &:hover, 72 | &:focus 73 | cursor: pointer 74 | outline: none 75 | 76 | // Sidebar button 77 | .sidebar-button 78 | position: absolute 79 | top: spacer 80 | left: spacer 81 | width: spacer 82 | height: spacer 83 | padding: 0 84 | border: 0 85 | background-color: transparent 86 | &:hover, 87 | &:focus 88 | cursor: pointer 89 | outline: none 90 | 91 | // Reading list 92 | .reading-list 93 | margin-bottom: spacer 94 | 95 | // Hide all read items when showing unread only 96 | .unread-only .read 97 | display: none 98 | 99 | .filter 100 | margin-bottom: 10px 101 | 102 | .sort 103 | .arrow 104 | border: solid black 105 | border-width: 0 3px 3px 0 106 | display: inline-block 107 | padding: 3px 108 | transition: transform .3s ease 109 | margin-left: 10px 110 | 111 | .up 112 | transform: rotate(-135deg) 113 | 114 | .down 115 | transform: rotate(45deg) 116 | 117 | .filter, .sort 118 | display: flex 119 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 120 | 0 1px 2px rgba(0, 0, 0, 0.05) 121 | border-radius: 4px 122 | width: 100% 123 | 124 | .button:first-child 125 | border-radius: 4px 0 0 4px 126 | 127 | .button:last-child 128 | border-radius: 0 4px 4px 0 129 | 130 | .count 131 | font-size: 80% 132 | display: inline-block 133 | border: 1px solid 134 | padding: 3px 135 | border-radius: 8px 136 | margin-left: 4px 137 | 138 | .button 139 | display: flex 140 | justify-content: center 141 | align-items: center 142 | line-height: 1 143 | 144 | .button 145 | border: 0 146 | flex-grow: 1 147 | flex-basis: 0 148 | text-transform: uppercase 149 | letter-spacing: 0.05em 150 | text-align: center 151 | line-height: 1 152 | padding: 8px 153 | &:focus, 154 | &:hover, 155 | &.active 156 | cursor: pointer 157 | outline: none 158 | 159 | // Dragables 160 | [draggable] 161 | -moz-user-select: none 162 | -khtml-user-select: none 163 | -webkit-user-select: none 164 | user-select: none 165 | 166 | [draggable] * 167 | -moz-user-drag: none 168 | -khtml-user-drag: none 169 | -webkit-user-drag: none 170 | user-drag: none 171 | -------------------------------------------------------------------------------- /src/style/base/reading-item.styl: -------------------------------------------------------------------------------- 1 | .reading-item 2 | border-radius: 3px 3 | padding: 0 4 | margin: 10px 0 0 0 5 | position: relative 6 | overflow: hidden 7 | transition: all 0.5s ease 0s 8 | 9 | .favicon 10 | position: absolute 11 | top: 10px 12 | left: 10px 13 | width: 36px 14 | height: 36px 15 | border-radius: 4px 16 | border: 1px solid #ccc 17 | padding: 1px 18 | 19 | .favicon-img 20 | width: 100% 21 | height: 100% 22 | border-radius: 2px 23 | &.error 24 | display: none 25 | 26 | .item-link 27 | text-decoration: none 28 | display: block 29 | width: 100% 30 | padding: 10px 50px 10px 56px 31 | min-height: 56px 32 | .title 33 | display: block 34 | font-weight: bold 35 | .host 36 | display: block 37 | span 38 | overflow-wrap: break-word 39 | @media screen and (max-width: 200px) 40 | white-space: nowrap 41 | text-overflow: ellipsis 42 | overflow: hidden 43 | 44 | .delete-button 45 | position: absolute 46 | text-align: center 47 | font-weight: bold 48 | top: 5px 49 | right: 5px 50 | border-radius: 100% 51 | padding: 0 52 | width: 20px 53 | height: 20px 54 | border: 0 55 | background: transparent 56 | color: #ccc 57 | transform: rotateZ(0) scale(1) 58 | transition: transform 0.3s ease, 59 | box-shadow 0.5s ease 60 | &:hover 61 | cursor: pointer 62 | background: #ccc 63 | color: #fff 64 | transform: rotateZ(90deg) scale(2) 65 | box-shadow: 1px 0 1px rgba(0, 0, 0, 0.15) 66 | 67 | .edit-button 68 | position: absolute 69 | bottom: 10px 70 | right: 10px 71 | padding: 0 72 | width: 10px 73 | height: 10px 74 | border: 0 75 | background-color: transparent 76 | color: #ccc 77 | border-radius: 0 78 | margin: 0 79 | text-align: center 80 | transition: all 0.5s ease 81 | .edit-img 82 | fill: #ccc 83 | &:hover 84 | cursor: pointer 85 | width: 25px 86 | height: 25px 87 | bottom: -2px 88 | right: 4px 89 | background-color: #ccc 90 | color: #fff 91 | transform: scale(1.5) 92 | border-radius: 100% 93 | box-shadow: 1px 0 0px 1px rgba(0, 0, 0, 0.15) 94 | .edit-img 95 | fill: #fff 96 | 97 | .edit-img 98 | width: 10px 99 | height: 10px 100 | margin-top: 5px 101 | 102 | .store-button 103 | position: absolute 104 | bottom: -2px 105 | right: 4px 106 | padding: 0 107 | width: 25px 108 | height: 25px 109 | background-color: #ccc 110 | color: #fff 111 | transform: scale(1.5) 112 | border-radius: 100% 113 | margin: 0 114 | box-shadow: 1px 0 0px 1px rgba(0, 0, 0, 0.15) 115 | text-align: center 116 | .edit-img 117 | fill: #fff 118 | 119 | .reading-list .reading-item.shiny 120 | background: linear-gradient(to right, #fa709a 0%, #fee140 100%) 121 | .delete-button 122 | color: #000 123 | &:focus, 124 | &:hover 125 | background: transparent 126 | color: #fa709a 127 | .item-link 128 | color: #000 129 | &:focus, 130 | &:hover 131 | color: #fee140 132 | background: transparent 133 | .favicon 134 | border-color: transparent 135 | .edit-button 136 | display: none 137 | .favicon 138 | border-color: transparent 139 | &:focus, 140 | &:hover 141 | box-shadow: 1px 3px 17px rgb(254, 225, 64, 0.9), 0px -1px 5px rgba(254, 225, 64, 0.7) 142 | 143 | .overlay 144 | top: 0 145 | bottom: 0 146 | right: 0 147 | left: 0 148 | position: absolute 149 | background: rgba(0,0,0,.4) 150 | z-index: 10 151 | display: none 152 | 153 | .edit-title 154 | background-color: #fff !important; 155 | position: relative 156 | z-index: 11 157 | -------------------------------------------------------------------------------- /src/style/base/typography.styl: -------------------------------------------------------------------------------- 1 | html 2 | font-size: baseFontSize 3 | 4 | body, 5 | input, 6 | optgroup, 7 | select, 8 | textarea 9 | font-family: baseFont 10 | 11 | body 12 | font-size: 1rem 13 | line-height: baseLineHeight 14 | 15 | // Headings 16 | h1 17 | margin: 0 18 | padding: 0 19 | line-height: 1 20 | font-size: 1.7rem 21 | 22 | // Labels & inputs 23 | label 24 | margin-right: 5px 25 | 26 | input 27 | font-size: 1rem 28 | border: 1px solid #eee 29 | border-radius: 4px 30 | padding: 8px 31 | background: transparent 32 | &:focus 33 | outline: none 34 | border-color: #ddd 35 | 36 | [type="search"] 37 | -webkit-appearance: textfield 38 | &::-webkit-search-cancel-button, 39 | &::-webkit-search-decoration 40 | -webkit-appearance: none 41 | -------------------------------------------------------------------------------- /src/style/base/variables.styl: -------------------------------------------------------------------------------- 1 | baseFont = -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif 2 | baseFontSize = 13px 3 | baseLineHeight = 1.4 4 | 5 | spacer = 15px 6 | containerWidth = 360px 7 | -------------------------------------------------------------------------------- /src/style/font.styl: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Fira Sans'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 8 | } 9 | /* cyrillic */ 10 | @font-face { 11 | font-family: 'Fira Sans'; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 16 | } 17 | /* greek-ext */ 18 | @font-face { 19 | font-family: 'Fira Sans'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 23 | unicode-range: U+1F00-1FFF; 24 | } 25 | /* greek */ 26 | @font-face { 27 | font-family: 'Fira Sans'; 28 | font-style: normal; 29 | font-weight: 400; 30 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 31 | unicode-range: U+0370-03FF; 32 | } 33 | /* vietnamese */ 34 | @font-face { 35 | font-family: 'Fira Sans'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 39 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 40 | } 41 | /* latin-ext */ 42 | @font-face { 43 | font-family: 'Fira Sans'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 48 | } 49 | /* latin */ 50 | @font-face { 51 | font-family: 'Fira Sans'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: local('Fira Sans Regular'), local('FiraSans-Regular'), url(../fonts/FiraSans-Regular.ttf); 55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 56 | } 57 | /* cyrillic-ext */ 58 | @font-face { 59 | font-family: 'Fira Sans'; 60 | font-style: normal; 61 | font-weight: 600; 62 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 63 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 64 | } 65 | /* cyrillic */ 66 | @font-face { 67 | font-family: 'Fira Sans'; 68 | font-style: normal; 69 | font-weight: 600; 70 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 72 | } 73 | /* greek-ext */ 74 | @font-face { 75 | font-family: 'Fira Sans'; 76 | font-style: normal; 77 | font-weight: 600; 78 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 79 | unicode-range: U+1F00-1FFF; 80 | } 81 | /* greek */ 82 | @font-face { 83 | font-family: 'Fira Sans'; 84 | font-style: normal; 85 | font-weight: 600; 86 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 87 | unicode-range: U+0370-03FF; 88 | } 89 | /* vietnamese */ 90 | @font-face { 91 | font-family: 'Fira Sans'; 92 | font-style: normal; 93 | font-weight: 600; 94 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 95 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 96 | } 97 | /* latin-ext */ 98 | @font-face { 99 | font-family: 'Fira Sans'; 100 | font-style: normal; 101 | font-weight: 600; 102 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 103 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 104 | } 105 | /* latin */ 106 | @font-face { 107 | font-family: 'Fira Sans'; 108 | font-style: normal; 109 | font-weight: 600; 110 | src: local('Fira Sans SemiBold'), local('FiraSans-SemiBold'), url(../fonts/FiraSans-SemiBold.ttf); 111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 112 | } 113 | -------------------------------------------------------------------------------- /src/style/options.styl: -------------------------------------------------------------------------------- 1 | // Accordion styles 2 | div.accordion 3 | background-color #eee 4 | color #444 5 | cursor pointer 6 | padding 1px 7 | width 100% 8 | transition 0.4s 9 | 10 | &.active, &:hover 11 | background-color #ccc 12 | 13 | div.panel 14 | padding 0 5px 15 | max-height 0 16 | overflow hidden 17 | transition max-height 0.2s ease-out 18 | 19 | button 20 | margin 2px 21 | 22 | // Overlay styles 23 | .dialog-overlay 24 | position absolute 25 | top 0 26 | left 0 27 | right 0 28 | bottom 0 29 | background-color rgba(0, 0, 0, 0.5) 30 | z-index 999999 31 | display none 32 | 33 | .dialog-overlay .dialog 34 | width 80% 35 | margin 10px auto 36 | background-color #fff 37 | border-radius 3px 38 | 39 | .dialog-msg 40 | padding 12px 10px 41 | 42 | p 43 | margin 0 44 | font-size 15px 45 | 46 | footer 47 | padding 8px 10px 48 | 49 | .controls 50 | text-align center 51 | 52 | .button 53 | text-shadow none 54 | padding 5px 15px 55 | border-radius 3px 56 | 57 | .button-info 58 | background #337ab7 59 | border 1px solid #2e6da4 60 | color #f5f5f5 61 | 62 | .button-danger 63 | background #f44336 64 | border 1px solid #d32f2f 65 | color #f5f5f5 66 | -------------------------------------------------------------------------------- /src/style/popup.styl: -------------------------------------------------------------------------------- 1 | @import 'base' 2 | 3 | @import 'themes/light' 4 | @import 'themes/dark' 5 | -------------------------------------------------------------------------------- /src/style/themes/dark.styl: -------------------------------------------------------------------------------- 1 | // Dark theme 2 | darkColorMain = #555 3 | darkColorLighter = #777 4 | darkColorLightest = #eee 5 | darkColorDarker = #333 6 | darkColorDarkest = #000 7 | 8 | .dark 9 | color: darkColorDarkest 10 | background: darkColorLighter 11 | 12 | input 13 | background: transparent 14 | border: 1px solid darkColorMain 15 | 16 | .reading-item 17 | background: darkColorMain 18 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 19 | 0 1px 2px rgba(0, 0, 0, 0.05) 20 | .item-link 21 | color: darkColorLightest 22 | &:hover, 23 | &:focus 24 | background: darkColorDarker 25 | .favicon 26 | border: 0 27 | 28 | .reading-item.sortable-dragging 29 | .item-link 30 | background: darkColorDarker 31 | 32 | .save-button 33 | color: #fff 34 | background: darkColorMain 35 | &:hover, 36 | &:focus 37 | background: darkColorDarker 38 | 39 | .button 40 | background: darkColorMain 41 | &:focus, 42 | &:hover, 43 | &.active 44 | color: darkColorLightest 45 | background: darkColorDarker 46 | -------------------------------------------------------------------------------- /src/style/themes/light.styl: -------------------------------------------------------------------------------- 1 | // Light theme 2 | .light 3 | color: #555 4 | background: #fff 5 | 6 | input 7 | background: transparent 8 | border: 1px solid #eee 9 | 10 | .reading-item 11 | background: #f7f7f7 12 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 13 | 0 1px 2px rgba(0, 0, 0, 0.05) 14 | .item-link 15 | color: #555 16 | &:hover, 17 | &:focus 18 | color: #66cc98 19 | background: #fff 20 | .favicon 21 | border-color: #66cc98 22 | 23 | .reading-item.sortable-dragging 24 | .item-link 25 | color: #66cc98 26 | background: #fff 27 | .favicon 28 | border-color: #66cc98 29 | 30 | .save-button 31 | color: #fff 32 | background: #66cc98 33 | &:hover, 34 | &:focus 35 | background: #44aa76 36 | 37 | .button 38 | &:focus, 39 | &:hover, 40 | &.active 41 | background: #f7f7f7 42 | --------------------------------------------------------------------------------