├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── ---feature-request.md │ └── ---bug-report.md ├── .gitignore ├── src ├── images │ ├── sh-at.png │ ├── logo-large.png │ ├── cross.svg │ ├── path.svg │ ├── link.svg │ ├── trash.svg │ ├── calendar.svg │ └── icon.svg ├── manifest.json ├── html │ ├── options.html │ └── ui.html ├── _locales │ ├── ja │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── hsb │ │ └── messages.json │ ├── dsb │ │ └── messages.json │ └── de │ │ └── messages.json ├── js │ ├── lib │ │ └── i18n.js │ └── core │ │ ├── options.js │ │ ├── ui.js │ │ └── background.js └── css │ └── ui.css ├── screenshots ├── keep-or-delete-bookmarks-en-1.png ├── keep-or-delete-bookmarks-en-2.png ├── keep-or-delete-bookmarks-en-3.png ├── keep-or-delete-bookmarks-en-4.png └── keep-or-delete-bookmarks-en-5.png ├── jsdoc.json ├── gulpfile.js ├── .htmllintrc.json ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md ├── CHANGELOG.md ├── .stylelintrc.json ├── .eslintrc.json └── LICENSE.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.com/paypalme/agenedia/3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | dist 4 | docs 5 | logo.psd 6 | node_modules 7 | _TODO.txt 8 | -------------------------------------------------------------------------------- /src/images/sh-at.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/src/images/sh-at.png -------------------------------------------------------------------------------- /src/images/logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/src/images/logo-large.png -------------------------------------------------------------------------------- /screenshots/keep-or-delete-bookmarks-en-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/screenshots/keep-or-delete-bookmarks-en-1.png -------------------------------------------------------------------------------- /screenshots/keep-or-delete-bookmarks-en-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/screenshots/keep-or-delete-bookmarks-en-2.png -------------------------------------------------------------------------------- /screenshots/keep-or-delete-bookmarks-en-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/screenshots/keep-or-delete-bookmarks-en-3.png -------------------------------------------------------------------------------- /screenshots/keep-or-delete-bookmarks-en-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/screenshots/keep-or-delete-bookmarks-en-4.png -------------------------------------------------------------------------------- /screenshots/keep-or-delete-bookmarks-en-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadeyrn/keep-or-delete-bookmarks/HEAD/screenshots/keep-or-delete-bookmarks-en-5.png -------------------------------------------------------------------------------- /src/images/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/path.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea! 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "node_modules/jsdoc-strip-async-await" 4 | ], 5 | "tags": { 6 | "allowUnknownTags": true 7 | }, 8 | "opts": { 9 | "destination": "./docs" 10 | }, 11 | "templates": { 12 | "cleverLinks": false, 13 | "monospaceLinks": false, 14 | "default": { 15 | "outputSourceFiles": true 16 | }, 17 | "systemName": "Keep or Delete Bookmarks", 18 | "copyright": "© 2023, soeren-hentzschel.at", 19 | "path": "ink-docstrap", 20 | "theme": "united", 21 | "linenums": true, 22 | "dateFormat": "DD.MM.YYYY, HH:mm:ss", 23 | "outputSourceFiles": true, 24 | "search": false, 25 | "sort": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/images/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Found an error? Please report! 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '…' 16 | 2. Click on '…' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Version information:** 26 | - Firefox version: [e.g. Firefox 109, Firefox ESR 102] 27 | - Keep or Delete Bookmarks version [e.g. 3.0.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const gulpEslint = require('gulp-eslint-new'); 5 | const gulpHtmllint = require('gulp-htmllint'); 6 | const gulpStylelint = require('gulp-stylelint'); 7 | const jsdoc = require('gulp-jsdoc3'); 8 | 9 | gulp.task('lint-html', () => gulp.src(['./src/html/*.html']) 10 | .pipe(gulpHtmllint({ config : '.htmllintrc.json' })) 11 | ); 12 | 13 | gulp.task('lint-js', () => gulp.src(['gulpfile.js', './src/js/**/*.js']) 14 | .pipe(gulpEslint({ overrideConfigFile : '.eslintrc.json' })) 15 | .pipe(gulpEslint.format()) 16 | ); 17 | 18 | gulp.task('lint-css', () => gulp.src(['./src/css/*.css']) 19 | .pipe(gulpStylelint({ 20 | failAfterError : false, 21 | reporters : [ 22 | { 23 | formatter : 'string', 24 | console : true 25 | } 26 | ] 27 | })) 28 | ); 29 | 30 | const jsdocsConfig = require('./jsdoc.json'); 31 | gulp.task('docs', () => gulp.src(['CHANGELOG.md', 'README.md', './src/js/*/*.js'], { read : false }) 32 | .pipe(jsdoc(jsdocsConfig)) 33 | ); 34 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extension_name__", 4 | "version": "3.0.0", 5 | "description": "__MSG_extension_description__", 6 | "icons": { 7 | "48": "images/icon.svg" 8 | }, 9 | "developer": { 10 | "name": "Sören Hentzschel", 11 | "url": "https://www.soeren-hentzschel.at/firefox-webextensions/keep-or-delete-bookmarks/?utm_campaign=webext&utm_term=keep-or-delete-bookmarks" 12 | }, 13 | "background": { 14 | "scripts": ["js/core/background.js"] 15 | }, 16 | "action": { 17 | "browser_style": false, 18 | "default_icon": "images/icon.svg", 19 | "default_title": "__MSG_extension_name__" 20 | }, 21 | "permissions": [ 22 | "bookmarks", 23 | "menus", 24 | "storage", 25 | "tabs" 26 | ], 27 | "host_permissions": [ 28 | "" 29 | ], 30 | "content_security_policy": { 31 | "extension_pages": "script-src 'self';" 32 | }, 33 | "options_ui": { 34 | "page": "html/options.html", 35 | "open_in_tab": true 36 | }, 37 | "default_locale": "en", 38 | "browser_specific_settings": { 39 | "gecko": { 40 | "id": "keep-or-delete-bookmarks@agenedia.com", 41 | "strict_min_version": "109.0" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 |
30 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/images/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.htmllintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "attr-bans": [ 3 | "align", 4 | "background", 5 | "bgcolor", 6 | "border", 7 | "frameborder", 8 | "longdesc", 9 | "marginwidth", 10 | "marginheight", 11 | "scrolling", 12 | "style", 13 | "width" 14 | ], 15 | "attr-name-ignore-regex": false, 16 | "attr-name-style": "dash", 17 | "attr-new-line": false, 18 | "attr-no-dup": true, 19 | "attr-no-unsafe-char": true, 20 | "attr-order": [ 21 | "id", 22 | "class" 23 | ], 24 | "attr-quote-style": "double", 25 | "attr-req-value": true, 26 | "class-no-dup": true, 27 | "class-style": "dash", 28 | "doctype-first": true, 29 | "doctype-html5": true, 30 | "fig-req-figcaption": true, 31 | "focusable-tabindex-style": true, 32 | "head-req-title": false, 33 | "head-valid-content-model": true, 34 | "href-style": false, 35 | "html-req-lang": false, 36 | "html-valid-content-model": true, 37 | "id-class-ignore-regex": false, 38 | "id-class-no-ad": true, 39 | "id-class-style": "dash", 40 | "id-no-dup": true, 41 | "img-req-alt": "allownull", 42 | "img-req-src": true, 43 | "indent-style": "spaces", 44 | "indent-width": 2, 45 | "indent-width-cont": false, 46 | "input-radio-req-name": true, 47 | "input-req-label": false, 48 | "label-req-for": true, 49 | "lang-style": "case", 50 | "line-end-style": "lf", 51 | "line-max-len": 180, 52 | "line-max-len-ignore-regex": false, 53 | "line-no-trailing-whitespace": true, 54 | "link-req-noopener": true, 55 | "spec-char-escape": false, 56 | "table-req-caption": false, 57 | "table-req-header": false, 58 | "tag-bans": [ 59 | "b", 60 | "i", 61 | "style" 62 | ], 63 | "tag-close": true, 64 | "tag-name-lowercase": true, 65 | "tag-name-match": true, 66 | "tag-self-close": "always", 67 | "title-max-len": 65, 68 | "title-no-dup": true 69 | } 70 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "これでバッチリです! もうブックマークを検査する必要はありません。" 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "削除する" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "維持する" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "開く" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "ホワイトリストを開く" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "戻る" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "次へ" 22 | }, 23 | "check_status_failure": { 24 | "message": "ブックマークが壊れている可能性があります。" 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Click to grant permission to check for broken bookmarks." 28 | }, 29 | "check_status_skipped": { 30 | "message": "照合できません。" 31 | }, 32 | "check_status_success": { 33 | "message": "ブックマークは大丈夫そうです。" 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "取り消す" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "OK" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "確認のダイアログを有効にする" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "ブックマークは、本当に削除して大丈夫でしょうか?" 46 | }, 47 | "date_added": { 48 | "message": "追加された日付" 49 | }, 50 | "extension_description": { 51 | "message": "ブックマークの整理は面倒でうんざりです。Firefox のアドオン Keep or Delete Bookmarks(ブックマークの削除や維持)を使用すれば、\",Tinder\", や類似のサービスのようにブックマークを並べ替えることができ、この退屈な作業にいくらかの楽しさが得られます。" 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks(ブックマークの削除や維持)" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "ホワイトリストから全てを削除する" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "ホワイトリストから外す" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "ホワイトリストには、まだブックマークはありません。" 64 | }, 65 | "whitelist_th_name": { 66 | "message": "名称" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "パス" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "Well done! No more bookmarks to check." 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "delete" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "keep" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "open" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "open whitelist" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "back" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "next" 22 | }, 23 | "check_status_failure": { 24 | "message": "The bookmark may be broken." 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Click to grant permission to check for broken bookmarks." 28 | }, 29 | "check_status_skipped": { 30 | "message": "No check possible." 31 | }, 32 | "check_status_success": { 33 | "message": "The bookmark seems to be okay." 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "Cancel" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "OK" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "Enable confirmation dialogs" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "Should the bookmark really be deleted?" 46 | }, 47 | "date_added": { 48 | "message": "Date added" 49 | }, 50 | "extension_description": { 51 | "message": "Cleaning up bookmarks is boring. The Firefox add-on Keep or Delete Bookmarks brings some fun to this task by allowing you to sort out the bookmarks like on \"Tinder\" or similar services." 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "remove all from whitelist" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "remove from whitelist" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "No bookmarks on the whitelist yet." 64 | }, 65 | "whitelist_th_name": { 66 | "message": "Name" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "Path" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/_locales/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "Goed gedaan! Er zijn geen favorieten meer te checken." 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "verwijder" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "bewaar" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "open" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "open whitelist" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "terug" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "volgende" 22 | }, 23 | "check_status_failure": { 24 | "message": "De favoriet kan stuk zijn." 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Klik om toestemming te geven om te controleren op favoriten die stuk zijn." 28 | }, 29 | "check_status_skipped": { 30 | "message": "Check is niet mogelijk." 31 | }, 32 | "check_status_success": { 33 | "message": "De favoriet lijkt in orde te zijn." 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "Annuleer" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "Oké" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "Activeer bevestigingsmeldingen" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "Moet de favoriet werkelijk worden verwijderd?" 46 | }, 47 | "date_added": { 48 | "message": "Datum toegevoegd" 49 | }, 50 | "extension_description": { 51 | "message": "Opruimen van favorieten kan saai werk zijn. Deze Firefox-add-on Keep or Delete Bookmarks maakt het leuker doordat je favorieten sorteert zoals \"Tinder\" of vergelijkbare diensten werken." 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "verwijder alles van de whitelist" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "verwijder favoriet" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "Nog geen favorieten in de whitelist." 64 | }, 65 | "whitelist_th_name": { 66 | "message": "Naam" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "Pad" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/_locales/hsb/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "Derje činił! Žane dalše zapołožki přepruwować." 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "zhašeć" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "wobchować" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "wočinić" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "Bełu lisćinu wočinić" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "wróćo" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "dale" 22 | }, 23 | "check_status_failure": { 24 | "message": "Zapołožka je snano zmylna." 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Klikńće, zo byšće prawo za pytanje za defektnymi zapołožkami dał." 28 | }, 29 | "check_status_skipped": { 30 | "message": "Žane přepruwowanje móžne." 31 | }, 32 | "check_status_success": { 33 | "message": "Zapołožka zda so w porjadku być." 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "Přetorhnyć" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "W porjadku" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "Wobkrućenske dialogi zmóžnić" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "Ma so zapołožka woprawdźe zhašeć?" 46 | }, 47 | "date_added": { 48 | "message": "Přidaty" 49 | }, 50 | "extension_description": { 51 | "message": "Rumowanje zapołožkow je wostudłe. Rozšěrjenje Firefox Keep or Delete Bookmarks trochu wjesela do tutoho nadawka přinjese, dokelž dowola, zapołožki kaž na „Tinder“ abo podobnych słužbach wusortěrować." 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "Wšě z běłeje lisćiny wotstronić" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "z běłeje lisćiny wotstronić" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "hišće žane zapołožki w běłej lisćinje." 64 | }, 65 | "whitelist_th_name": { 66 | "message": "Mjeno" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "Šćežka" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keep-or-delete-bookmarks", 3 | "version": "3.0.0", 4 | "description": "Cleaning up bookmarks is boring. The Firefox add-on Keep or Delete Bookmarks brings some fun to this task by allowing you to sort out the bookmarks like on \"Tinder\" or similar services.", 5 | "author": { 6 | "name": "Sören Hentzschel", 7 | "email": "kontakt@agenedia.com", 8 | "url": "https://firefox.agenedia.com" 9 | }, 10 | "homepage": "https://www.soeren-hentzschel.at/firefox-webextensions/keep-or-delete-bookmarks/?utm_campaign=webext&utm_term=keep-or-delete-bookmarks", 11 | "bugs": { 12 | "email": "kontakt@agenedia.com" 13 | }, 14 | "license": "MPL 2.0", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/cadeyrn/keep-or-delete-bookmarks/" 18 | }, 19 | "private": true, 20 | "browserslist": [ 21 | "Firefox >= 109" 22 | ], 23 | "devDependencies": { 24 | "eslint": "8.33.0", 25 | "eslint-plugin-compat": "4.0.2", 26 | "eslint-plugin-no-unsanitized": "4.0.2", 27 | "eslint-plugin-promise": "6.1.1", 28 | "eslint-plugin-sort-requires": "2.1.0", 29 | "eslint-plugin-xss": "0.1.12", 30 | "gulp": "4.0.2", 31 | "gulp-eslint-new": "1.7.1", 32 | "gulp-htmllint": "0.0.19", 33 | "gulp-jsdoc3": "3.0.0", 34 | "gulp-stylelint": "13.0.0", 35 | "htmllint": "0.8.0", 36 | "jsdoc": "4.0.0", 37 | "jsdoc-strip-async-await": "0.1.0", 38 | "npm-run-all": "4.1.5", 39 | "stylelint": "15.10.1", 40 | "stylelint-csstree-validator": "2.1.0", 41 | "stylelint-order": "6.0.1", 42 | "web-ext": "7.6.2" 43 | }, 44 | "scripts": { 45 | "build": "cd src && web-ext build -a ../dist", 46 | "docs": "gulp docs", 47 | "lint": "npm-run-all lint:*", 48 | "lint:html": "gulp lint-html", 49 | "lint:js": "gulp lint-js", 50 | "lint:css": "gulp lint-css", 51 | "lint:webext": "cd src && web-ext lint", 52 | "run:nightly": "cd src && web-ext run --firefox=nightly", 53 | "run:beta": "cd src && web-ext run --firefox=beta", 54 | "run:stable": "cd src && web-ext run --firefox=firefox", 55 | "run:esr": "cd src && web-ext run --firefox=\"/Applications/Firefox ESR.app\"" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/_locales/dsb/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "Derje gótował! Žedne dalšne cytańske znamjenja pśeglědowaś." 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "wulašowaś" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "wobchowaś" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "wócyniś" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "Bełu lisćinu wócyniś" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "slědk" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "dalej" 22 | }, 23 | "check_status_failure": { 24 | "message": "Cytańske znamje jo snaź zmólkate." 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Klikniśo, aby dał pšawo za pytanje defektnymi cytańskimi znamjenjami." 28 | }, 29 | "check_status_skipped": { 30 | "message": "Žedno pśeglědanje móžne." 31 | }, 32 | "check_status_success": { 33 | "message": "Cytańske znamje zda se w pórědku." 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "Pśetergnuś" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "W pórěźe" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "Wobkšuśeńske dialogi zmóžniś" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "Ma se cytańske znamje napšawdu lašowaś?" 46 | }, 47 | "date_added": { 48 | "message": "Pśidany" 49 | }, 50 | "extension_description": { 51 | "message": "Rumowanje cytańskich znamjenjow jo wóstudne. Rozšyrjenje Firefox Keep or Delete Bookmarks pitśku wjasela do toś togo nadawka pśinjaso, dokulaž dowólujo, cytańske znamjenja ako na „Tinder“ abo pódobnych słužbach wusortěrowaś." 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "wšykne z běłeje lisćiny wówónoźeś" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "z běłeje lisćiny wótwónoźeś" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "Hyšći žedne cytańske znamjenja w běłej lisćinje." 64 | }, 65 | "whitelist_th_name": { 66 | "message": "Mě" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "Sćažka" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "bookmarks_empty_state": { 3 | "message": "Gut gemacht! Keine weiteren Lesezeichen zu überprüfen." 4 | }, 5 | "btn_delete_bookmark": { 6 | "message": "löschen" 7 | }, 8 | "btn_keep_bookmark": { 9 | "message": "behalten" 10 | }, 11 | "btn_open_bookmark": { 12 | "message": "öffnen" 13 | }, 14 | "btn_open_whitelist": { 15 | "message": "Whitelist öffnen" 16 | }, 17 | "btn_previous_bookmark": { 18 | "message": "zurück" 19 | }, 20 | "btn_skip_bookmark": { 21 | "message": "weiter" 22 | }, 23 | "check_status_failure": { 24 | "message": "Das Lesezeichen ist möglicherweise fehlerhaft." 25 | }, 26 | "check_status_permission_needed": { 27 | "message": "Klicken, um Berechtigung zur Prüfung auf defekte Lesezeichen zu erteilen." 28 | }, 29 | "check_status_skipped": { 30 | "message": "Keine Überprüfung möglich." 31 | }, 32 | "check_status_success": { 33 | "message": "Das Lesezeichen scheint in Ordnung zu sein." 34 | }, 35 | "confirm_btn_cancel": { 36 | "message": "Abbrechen" 37 | }, 38 | "confirm_btn_ok": { 39 | "message": "OK" 40 | }, 41 | "confirm_checkbox": { 42 | "message": "Bestätigungsdialoge aktivieren" 43 | }, 44 | "confirm_delete_bookmark": { 45 | "message": "Soll das Lesezeichen wirklich gelöscht werden?" 46 | }, 47 | "date_added": { 48 | "message": "Hinzugefügt am" 49 | }, 50 | "extension_description": { 51 | "message": "Lesezeichen aufräumen ist langweilig. Die Firefox-Erweiterung Keep or Delete Bookmarks bringt etwas Spaß in diese Aufgabe, indem sie es erlaubt, die Lesezeichen wie auf „Tinder“ oder ähnlichen Diensten auszusortieren." 52 | }, 53 | "extension_name": { 54 | "message": "Keep or Delete Bookmarks" 55 | }, 56 | "btn_whitelist_remove_all": { 57 | "message": "alle von Whitelist entfernen" 58 | }, 59 | "whitelist_remove_bookmark": { 60 | "message": "von Whitelist entfernen" 61 | }, 62 | "whitelist_empty_state": { 63 | "message": "Noch keine Lesezeichen auf der Whitelist." 64 | }, 65 | "whitelist_th_name": { 66 | "message": "Name" 67 | }, 68 | "whitelist_th_path": { 69 | "message": "Pfad" 70 | }, 71 | "whitelist_th_url": { 72 | "message": "URL" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/js/lib/i18n.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @exports i18n 5 | */ 6 | const i18n = { 7 | /** 8 | * Fired when the initial HTML document has been completely loaded and parsed. Starts the translation of all the 9 | * strings. 10 | * 11 | * @returns {void} 12 | */ 13 | init () { 14 | i18n.translate(); 15 | i18n.setLangAttribute(); 16 | }, 17 | 18 | /** 19 | * This method is used to set the lang attribute to the element. 20 | * 21 | * @returns {void} 22 | */ 23 | setLangAttribute () { 24 | document.querySelector('html').setAttribute('lang', browser.i18n.getUILanguage()); 25 | }, 26 | 27 | /** 28 | * This method is used to get the translation for a given key. 29 | * 30 | * @param {string} key - translation key 31 | * 32 | * @returns {string} - translation 33 | */ 34 | getMessage (key) { 35 | return browser.i18n.getMessage(key); 36 | }, 37 | 38 | /** 39 | * Translates all strings in text nodes, placeholders and title attributes. 40 | * 41 | * @returns {void} 42 | */ 43 | translate () { 44 | document.removeEventListener('DOMContentLoaded', i18n.translate); 45 | 46 | // text node translation 47 | const nodes = document.querySelectorAll('[data-i18n]'); 48 | 49 | for (let i = 0, len = nodes.length; i < len; i++) { 50 | const node = nodes[i]; 51 | const children = Array.from(node.children); 52 | const text = i18n.getMessage(node.dataset.i18n); 53 | const parts = text.split(/({\d+})/); 54 | 55 | parts.forEach((part) => { 56 | if ((/{\d+}/).test(part)) { 57 | const index = parseInt(part.slice(1)); 58 | node.appendChild(children[index]); 59 | } 60 | else { 61 | node.appendChild(document.createTextNode(part)); 62 | } 63 | }); 64 | } 65 | 66 | // attribute translation 67 | const attributes = ['placeholder', 'title']; 68 | 69 | for (const attribute of attributes) { 70 | const i18nAttribute = `data-i18n-${attribute}`; 71 | const attrNodes = document.querySelectorAll(`[${i18nAttribute}]`); 72 | const { length } = attrNodes; 73 | 74 | for (let i = 0; i < length; i++) { 75 | const node = attrNodes[i]; 76 | const msg = node.getAttribute(i18nAttribute); 77 | node.setAttribute(attribute, i18n.getMessage(msg)); 78 | } 79 | } 80 | } 81 | }; 82 | 83 | window.addEventListener('DOMContentLoaded', i18n.init); 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kontakt@agenedia.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/html/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | 32 | 73 |
74 |
75 |
76 | 81 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firefox Add-on: Keep or Delete Bookmarks 2 | 3 | Logo 4 | 5 | ## Support the development 6 | 7 | **Please consider making [a donation](https://www.paypal.com/paypalme/agenedia/) to support the further development of 8 | Keep or Delete Bookmarks. Thank you very much!** 9 | 10 | ## Description 11 | 12 | **Cleaning up bookmarks is boring. The Firefox add-on Keep or Delete Bookmarks brings some fun to this task by 13 | allowing you to sort out the bookmarks the "Tinder way".** 14 | 15 | The add-on randomly displays one bookmark, including the title, the URL and the bookmark folder. The user has 16 | several options: 17 | 18 | 1) Keep the bookmark. The bookmark will be added to a whitelist and Keep or Delete Bookmarks will never ask again about 19 | this bookmark. 20 | 21 | 2) Delete the bookmark. The bookmark will be deleted from your Firefox. 22 | 23 | 3) Skip the bookmark and defer the decision. Keep or Delete Bookmarks will show you the next bookmark without any 24 | action. 25 | 26 | 4) Open the bookmark. Maybe you are not sure about the bookmark yet. This options lets you open the bookmark in a new 27 | tab before you make a decision. 28 | 29 | ### Features 30 | 31 | - Keep or Delete Bookmarks always shows one random bookmark 32 | - The add-on automatically checks whether the bookmark still works or is broken 33 | - An internal skip list is used for domains that are known to be unverifiable 34 | - You can keep or delete the bookmark, you can open the bookmark in a new tab, or you can defer the decision 35 | - After an action Keep or Delete Bookmarks shows you the next bookmark 36 | - Keep or Delete Bookmark makes sure that you never see the same bookmark two times in a row 37 | - There is a confirmation dialog when you press the delete button 38 | - You can disable the confirmation dialogs with one click 39 | - You can remove bookmarks from the whitelist at any time 40 | - You can also use keyboard shortcuts for the primary actions: 41 | - Left Arrow: show previous bookmark 42 | - Right Arrow: show next (random) bookmark 43 | - Enter: open bookmark 44 | - Space: add bookmark to whitelist 45 | - Backspace: delete bookmark (or opens confirmation dialog if enabled) 46 | - in bookmark deletion confirmation dialog: 47 | - ESC: close dialog 48 | - Enter: delete bookmark 49 | 50 | ### Planned features 51 | 52 | You can find the roadmap and request new features in the 53 | [issues tracker](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues). 54 | 55 | ### Languages 56 | 57 | The add-on is currently available in the following languages: 58 | 59 | - English 60 | - German 61 | - Dutch (Thanks, PanderMusubi!) 62 | - Japanese (Thanks, Shitennouji!) 63 | - Upper Sorbian (Thanks, milupo!) 64 | - Lower Sorbian (Thanks, milupo!) 65 | 66 | ### Screenhots 67 | 68 | | | | 69 | :-------------------------:|:-------------------------: 70 | ![](screenshots/keep-or-delete-bookmarks-en-1.png) | ![](screenshots/keep-or-delete-bookmarks-en-2.png) 71 | ![](screenshots/keep-or-delete-bookmarks-en-3.png) | ![](screenshots/keep-or-delete-bookmarks-en-4.png) 72 | ![](screenshots/keep-or-delete-bookmarks-en-5.png) | 73 | 74 | ### Permissions 75 | 76 | Keep or Delete Bookmarks needs several permissions to work properly. 77 | 78 | #### mandatory permissions 79 | 80 | Keep or Delete Bookmarks does not work without the following permissions: 81 | 82 | ##### access browser tabs 83 | 84 | The permission to access the browser tabs is needed so that Keep or Delete Bookmarks can jump to the already opened 85 | user interface if the user interface is already opened in another tab and you click the button in the browser's toolbar. 86 | 87 | ##### read and modify bookmarks 88 | 89 | You installed Keep or Delete Bookmarks to show and remove bookmarks, so it should be clear why the permission is needed 90 | to read and modify your bookmarks. 91 | 92 | #### optional permissions 93 | 94 | ##### access your data for all sites 95 | 96 | The add-on checks the bookmarks by sending a request to the appropriate URLs. This cannot work without the permission 97 | to access these sites. Keep or Delete Bookmarks asks at runtime for this permission if you want to execute this check. 98 | 99 | #### silent permissions 100 | 101 | Keep or Delete Bookmarks needs some more permissions, but Firefox does not prompt for the following permissions: 102 | 103 | ##### menus 104 | 105 | The menus permission is needed for providing a menu entry in the tools menu to access Keep or Delete Bookmarks's user 106 | interface. 107 | 108 | ##### storage 109 | 110 | The storage permission is needed so that Keep or Delete Bookmarks can remember which bookmarks you want to keep. 111 | 112 | ## Compatibility 113 | 114 | Keep or Delete Bookmarks is a WebExtension and compatible with Firefox Browser 68 and higher (Firefox Browser 109 or 115 | higher is required for the latest version of Keep or Delete Bookmarks). 116 | 117 | ## Download 118 | 119 | [Download Keep or Delete Bookmarks](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/) 120 | 121 | ## Release Notes 122 | 123 | [Release Notes](CHANGELOG.md "Release Notes") 124 | -------------------------------------------------------------------------------- /src/js/core/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const elBody = document.querySelector('body'); 4 | const elNoBookmarks = document.getElementById('no-bookmarks'); 5 | const elRemoveAllWrapper = document.getElementById('whitelist-remove-all-wrapper'); 6 | const elWhitelistTable = document.getElementById('whitelist-table'); 7 | 8 | /** 9 | * @exports options 10 | */ 11 | const options = { 12 | /** 13 | * Fired when the initial HTML document has been completely loaded and parsed. Shows the bookmark whitelist. 14 | * 15 | * @returns {void} 16 | */ 17 | init () { 18 | options.listBookmarks(); 19 | }, 20 | 21 | /** 22 | * Fired when one of the buttons is clicked. 23 | * 24 | * @param {MouseEvent} e - event 25 | * 26 | * @returns {void} 27 | */ 28 | handleButtonClicks (e) { 29 | if (e.target.getAttribute('data-action')) { 30 | e.preventDefault(); 31 | 32 | switch (e.target.getAttribute('data-action')) { 33 | case 'remove-all-from-whitelist': 34 | options.emptyWhitelist(); 35 | break; 36 | default: 37 | // do nothing 38 | } 39 | } 40 | }, 41 | 42 | /** 43 | * List all the bookmarks on the whitelist. 44 | * 45 | * @returns {void} 46 | */ 47 | async listBookmarks () { 48 | const { whitelist } = await browser.storage.local.get({ whitelist : {} }); 49 | const whitelistLength = Object.keys(whitelist).length; 50 | 51 | // show notice if the whitelist is empty, otherwise show the bookmarks on the whitelist 52 | if (whitelistLength === 0) { 53 | elNoBookmarks.removeAttribute('hidden'); 54 | elRemoveAllWrapper.setAttribute('hidden', 'true'); 55 | elWhitelistTable.setAttribute('hidden', 'true'); 56 | } 57 | else { 58 | elNoBookmarks.setAttribute('hidden', 'true'); 59 | elRemoveAllWrapper.removeAttribute('hidden'); 60 | elWhitelistTable.removeAttribute('hidden'); 61 | } 62 | 63 | if (whitelistLength > 0) { 64 | // tbody element, bookmark rows will be added here 65 | const elTableBody = elWhitelistTable.querySelector('tbody'); 66 | 67 | // remove old content 68 | while (elTableBody.firstChild) { 69 | elTableBody.removeChild(elTableBody.firstChild); 70 | } 71 | 72 | // add bookmarks to table in reversed order (newest at top) 73 | Object.keys(whitelist).reverse().forEach((id) => { 74 | // row 75 | const elRow = document.createElement('tr'); 76 | elTableBody.appendChild(elRow); 77 | 78 | // name column 79 | const elNameColumn = document.createElement('td'); 80 | elNameColumn.textContent = whitelist[id].title; 81 | elNameColumn.setAttribute('title', whitelist[id].title); 82 | elRow.appendChild(elNameColumn); 83 | 84 | // URL column 85 | const elUrlColumn = document.createElement('td'); 86 | const pattern = new RegExp(/^https?:\/\//, 'gi'); 87 | 88 | if (pattern.test(whitelist[id].url)) { 89 | const elUrl = document.createElement('a'); 90 | elUrl.setAttribute('href', whitelist[id].url); 91 | elUrl.setAttribute('target', '_blank'); 92 | elUrl.setAttribute('rel', 'noopener'); 93 | elUrl.textContent = whitelist[id].url; 94 | elUrlColumn.appendChild(elUrl); 95 | } 96 | else { 97 | elUrlColumn.textContent = whitelist[id].url; 98 | } 99 | 100 | elUrlColumn.setAttribute('title', whitelist[id].url); 101 | elRow.appendChild(elUrlColumn); 102 | 103 | // path column 104 | const elPathColumn = document.createElement('td'); 105 | elPathColumn.textContent = whitelist[id].path; 106 | elPathColumn.setAttribute('title', whitelist[id].path); 107 | elRow.appendChild(elPathColumn); 108 | 109 | // icon column 110 | const elIconColumn = document.createElement('td'); 111 | elIconColumn.classList.add('actions'); 112 | elRow.appendChild(elIconColumn); 113 | 114 | // remove icon 115 | const elRemoveLink = document.createElement('a'); 116 | elRemoveLink.setAttribute('title', browser.i18n.getMessage('whitelist_remove_bookmark')); 117 | elRemoveLink.setAttribute('data-idx', id); 118 | elRemoveLink.classList.add('icon', 'trash-icon'); 119 | elRemoveLink.addEventListener('click', options.removeFromWhitelist); 120 | elIconColumn.appendChild(elRemoveLink); 121 | 122 | const elRemoveIcon = document.createElement('img'); 123 | elRemoveIcon.src = '/images/cross.svg'; 124 | elRemoveIcon.setAttribute('alt', browser.i18n.getMessage('whitelist_remove_bookmark')); 125 | elRemoveLink.appendChild(elRemoveIcon); 126 | }); 127 | } 128 | }, 129 | 130 | /** 131 | * Removes a bookmark from the whitelist. 132 | * 133 | * @param {MouseEvent} e - event 134 | * 135 | * @returns {void} 136 | */ 137 | async removeFromWhitelist (e) { 138 | e.preventDefault(); 139 | 140 | const { whitelist } = await browser.storage.local.get({ whitelist : {} }); 141 | delete whitelist[e.target.parentNode.getAttribute('data-idx')]; 142 | browser.storage.local.set({ whitelist : whitelist }); 143 | 144 | options.listBookmarks(); 145 | }, 146 | 147 | /** 148 | * Removes all bookmarks from the whitelist. 149 | * 150 | * @returns {void} 151 | */ 152 | emptyWhitelist () { 153 | browser.storage.local.set({ whitelist : {} }); 154 | options.listBookmarks(); 155 | } 156 | }; 157 | 158 | document.addEventListener('DOMContentLoaded', options.init); 159 | elBody.addEventListener('click', options.handleButtonClicks); 160 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 3.0.0 (2023-02-08) 2 | 3 | #### Notable Changes 4 | 5 | - **Keep or Delete Bookmarks now uses Manifest v3**, fixes 6 | [#44](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/44) 7 | - **Keep or Delete Bookmarks now asks at runtime for permission** to access all website data if permission is not 8 | granted. The permission is technically needed to check for broken bookmarks. Therefore, the permission is no longer 9 | needed for installation. Keep or Delete Bookmarks also reacts to permission changes via the add-ons manager, fixes 10 | [#56](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/56) 11 | - bumped the minimum required Firefox version to Firefox 109, fixes 12 | [#55](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/55) 13 | - solved a few edge cases around added or removed bookmarks while the user interface of Keep or Delete Bookmarks was 14 | already opened, fixes [#61](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/61) 15 | - removed testpilot.firefox.com from internal skip list because this domain is not part of 16 | extensions.webextensions.restrictedDomains, fixes [#41](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/41) 17 | - changed copyright year from 2022 to 2023, fixes [#54](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/54) 18 | 19 | #### Code Quality 20 | 21 | - optimized image files to save a few bytes, fixes [#57](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/57) 22 | - updated the translation mechanism to the newest version to share more code with other extensions and to improve the 23 | maintainability, fixes [#40](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/40) 24 | 25 | #### Bugfixes 26 | 27 | - the confirmation dialog did not close if you used the Enter key to confirm, fixes 28 | [#59](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/59) 29 | - the 'remove all from whitelist' button was seen briefly on the whitelist screen if the whitelist was empty, 30 | fixes [#38](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/38) (Thanks, alexmajor!) 31 | 32 | #### Dependencies 33 | 34 | - updated eslint from version 8.5.0 to 8.33.0 and updated configuration 35 | - updated eslint-plugin-compat from version 4.0.0 to 4.0.2 36 | - updated eslint-plugin-no-unsanitized from version 4.0.1 to 4.0.2 37 | - updated eslint-plugin-promise from version 6.0.0 to 6.1.1 38 | - updated eslint-plugin-xss from version 0.1.11 to 0.1.12 39 | - updated gulp-eslint-new from version 1.1.0 to 1.7.1 40 | - updated jsdoc from version 3.6.7 to 4.0.0 41 | - updated stylelint from version 14.2.0 to 14.16.1 and updated configuration 42 | - updated stylelint-csstree-validator from version 2.0.0 to 2.1.0 43 | - updated stylelint-order from version 5.0.0 to 6.0.1 44 | - updated webext from version 6.6.0 to 7.5.0 45 | 46 | [All Changes](https://github.com/cadeyrn/keep-or-delete-bookmarks/compare/v2.0.1...v3.0.0)
47 | [Download Signed WebExtension](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/versions/?page=1#version-3.0.0) 48 | 49 | --- 50 | 51 | ### [Version 2.0.1](https://github.com/cadeyrn/keep-or-delete-bookmarks/releases/tag/v2.0.1) (2021-12-23) 52 | 53 | #### Translations 54 | 55 | - added Japanese translation (Thanks, Shitennouji!), fixes 56 | [#18](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/18) 57 | 58 | #### Dependencies 59 | 60 | - updated stylelint from version 14.1.0 to 14.2.0 61 | 62 | [All Changes](https://github.com/cadeyrn/keep-or-delete-bookmarks/compare/v2.0.0...v2.0.1)
63 | [Download Signed WebExtension](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/versions/?page=1#version-2.0.1) 64 | 65 | --- 66 | 67 | ### [Version 2.0.0](https://github.com/cadeyrn/keep-or-delete-bookmarks/releases/tag/v2.0.0) (2021-12-22) 68 | 69 | #### New Features 70 | 71 | - Keep or Delete Bookmarks now checks for broken bookmarks! An internal skip list is used for domains that are known 72 | to be unverifiable. This is the reason why the add-on needs the permission to access your data for all sites starting 73 | with this update, fixes [#9](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/9) 74 | - added ability to show previous bookmark after skipping, fixes 75 | [#13](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/13) 76 | - added keyboard support, fixes [#3](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/3) 77 | - Left Arrow: show previous bookmark 78 | - Right Arrow: show next (random) bookmark 79 | - Enter: open bookmark 80 | - Space: add bookmark to whitelist 81 | - Backspace: delete bookmark (or opens confirmation dialog if enabled) 82 | - in bookmark deletion confirmation dialog: 83 | - ESC: close dialog 84 | - Enter: delete bookmark 85 | - added the date when a bookmark was added to Firefox, fixes 86 | [#10](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/10) 87 | 88 | #### Notable Changes 89 | 90 | - bumped the minimum required Firefox version to Firefox 91, fixes 91 | [#31](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/31) 92 | - changed copyright year from 2019 to 2022, fixes [#30](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/30) 93 | 94 | #### Code Quality 95 | 96 | - replaced deprecated method call, fixes [#29](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/29) 97 | - fixed some code style issues and typos, fixes [#32](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/32) 98 | 99 | #### Bugfixes 100 | 101 | - fixed wrong tooltip for removing a bookmark from the whitelist, fixes 102 | [#33](https://github.com/cadeyrn/keep-or-delete-bookmarks/issues/33) 103 | 104 | #### Dependencies 105 | 106 | - replaced gulp-eslint 6.0.0 with gulp-eslint-new 1.1.0 107 | - updated eslint from version 6.0.1 to 8.5.0 and updated configuration 108 | - updated eslint-plugin-compat from version 3.0.2 to 4.0.0 109 | - updated eslint-plugin-no-unsanitized from version 3.0.2 to 4.0.1 110 | - updated eslint-plugin-promise from version 4.2.1 to 6.0.0 111 | - updated eslint-plugin-xss from version 0.1.9 to 0.1.11 112 | - updated gulp-htmllint from version 0.0.16 to 0.0.19 113 | - updated gulp-jsdoc3 from version 2.0.0 to 3.0.0 114 | - updated gulp-stylelint from version 9.0.0 to 13.0.0 115 | - updated jsdoc from version 3.6.3 to 3.6.7 116 | - updated stylelint from version 10.1.0 to 14.1.0 and updated configuration 117 | - updated stylelint-csstree-validator from version 1.5.2 to 2.0.0 118 | - updated stylelint-order from version 3.0.1 to 5.0.0 119 | - updated webext from version 3.1.0 to 6.6.0 120 | 121 | [All Changes](https://github.com/cadeyrn/keep-or-delete-bookmarks/compare/v1.0.1...v2.0.0)
122 | [Download Signed WebExtension](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/versions/?page=1#version-2.0.0) 123 | 124 | --- 125 | 126 | ### [Version 1.0.1](https://github.com/cadeyrn/keep-or-delete-bookmarks/releases/tag/v1.0.1) (2019-07-15) 127 | 128 | #### Translations 129 | 130 | - added Dutch translation (Thanks, PanderMusubi!) 131 | - added Upper Sorbian translation (Thanks, milupo!) 132 | - added Lower Sorbian translation (Thanks, milupo!) 133 | - fixed typos in English translation 134 | 135 | #### Dependencies 136 | 137 | - updated jsdoc from version 3.6.2 to 3.6.3 138 | 139 | [All Changes](https://github.com/cadeyrn/keep-or-delete-bookmarks/compare/v1.0.0...v1.0.1)
140 | [Download Signed WebExtension](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/versions/?page=1#version-1.0.1) 141 | 142 | --- 143 | 144 | ### [Version 1.0.0](https://github.com/cadeyrn/keep-or-delete-bookmarks/releases/tag/v1.0.0) (2019-07-13) 145 | 146 | - initial release for [addons.mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/) 147 | 148 | #### Features of the first version 149 | 150 | - Keep or Delete Bookmarks always shows one random bookmark 151 | - You can keep or delete the bookmark, you can open the bookmark in a new tab, or you can defer the decision 152 | - After an action Keep or Delete Bookmarks shows you the next bookmark 153 | - Keep or Delete Bookmark makes sure that you never see the same bookmark two times in a row 154 | - There is a confirmation dialog when you press the delete button 155 | - You can disable the confirmation dialogs with one click 156 | - You can also remove bookmarks from the whitelist at any time 157 | - Translations: English, German 158 | 159 | [Download Signed WebExtension](https://addons.mozilla.org/en-US/firefox/addon/keep-or-delete-bookmarks/versions/?page=1#version-1.0.0) 160 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "stylelint-csstree-validator", 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "alpha-value-notation": "number", 8 | "at-rule-allowed-list": null, 9 | "at-rule-disallowed-list": null, 10 | "at-rule-empty-line-before": "always", 11 | "at-rule-name-case": "lower", 12 | "at-rule-name-newline-after": null, 13 | "at-rule-name-space-after": "always", 14 | "at-rule-no-unknown": true, 15 | "at-rule-no-vendor-prefix": true, 16 | "at-rule-property-required-list": null, 17 | "at-rule-semicolon-newline-after": "always", 18 | "at-rule-semicolon-space-before": "never", 19 | "block-closing-brace-empty-line-before": "never", 20 | "block-closing-brace-newline-after": "always", 21 | "block-closing-brace-newline-before": "always", 22 | "block-closing-brace-space-after": "never-single-line", 23 | "block-closing-brace-space-before": "always-single-line", 24 | "block-no-empty": true, 25 | "block-opening-brace-newline-after": "always", 26 | "block-opening-brace-newline-before": null, 27 | "block-opening-brace-space-after": "never-single-line", 28 | "block-opening-brace-space-before": "always-single-line", 29 | "color-function-notation": "legacy", 30 | "color-hex-alpha": "never", 31 | "color-hex-case": "lower", 32 | "color-hex-length": "short", 33 | "color-named": "never", 34 | "color-no-hex": true, 35 | "color-no-invalid-hex": true, 36 | "comment-empty-line-before": ["always", { "ignore": ["stylelint-commands"] }], 37 | "comment-no-empty": true, 38 | "comment-whitespace-inside": "always", 39 | "comment-word-disallowed-list": ["todo", "fixme", "xxx"], 40 | "csstree/validator": true, 41 | "custom-media-pattern": null, 42 | "custom-property-empty-line-before": "never", 43 | "custom-property-no-missing-var-function": true, 44 | "custom-property-pattern": null, 45 | "declaration-bang-space-after": "never", 46 | "declaration-bang-space-before": "always", 47 | "declaration-block-no-duplicate-properties": true, 48 | "declaration-block-no-redundant-longhand-properties": true, 49 | "declaration-block-no-shorthand-property-overrides": true, 50 | "declaration-block-semicolon-newline-after": "always", 51 | "declaration-block-semicolon-newline-before": null, 52 | "declaration-block-semicolon-space-after": "always-single-line", 53 | "declaration-block-semicolon-space-before": "never", 54 | "declaration-block-single-line-max-declarations": 1, 55 | "declaration-block-trailing-semicolon": "always", 56 | "declaration-colon-newline-after": "always-multi-line", 57 | "declaration-colon-space-after": "always-single-line", 58 | "declaration-colon-space-before": "never", 59 | "declaration-empty-line-before": "never", 60 | "declaration-no-important": true, 61 | "declaration-property-unit-allowed-list": null, 62 | "declaration-property-unit-disallowed-list": null, 63 | "declaration-property-value-allowed-list": null, 64 | "declaration-property-value-disallowed-list": null, 65 | "font-family-name-quotes": "always-where-recommended", 66 | "font-family-no-duplicate-names": true, 67 | "font-family-no-missing-generic-family-keyword": true, 68 | "font-weight-notation": "numeric", 69 | "function-allowed-list": null, 70 | "function-calc-no-unspaced-operator": true, 71 | "function-comma-newline-after": "never-multi-line", 72 | "function-comma-newline-before": "never-multi-line", 73 | "function-comma-space-after": "always", 74 | "function-comma-space-before": "never", 75 | "function-disallowed-list": null, 76 | "function-linear-gradient-no-nonstandard-direction": true, 77 | "function-max-empty-lines": 0, 78 | "function-name-case": "lower", 79 | "function-no-unknown": true, 80 | "function-parentheses-newline-inside": "never-multi-line", 81 | "function-parentheses-space-inside": "never", 82 | "function-url-no-scheme-relative": null, 83 | "function-url-quotes": "always", 84 | "function-url-scheme-allowed-list": ["https", "data"], 85 | "function-url-scheme-disallowed-list": null, 86 | "function-whitespace-after": "always", 87 | "hue-degree-notation": "angle", 88 | "indentation": 2, 89 | "keyframe-declaration-no-important": true, 90 | "keyframes-name-pattern": null, 91 | "length-zero-no-unit": true, 92 | "linebreaks": "unix", 93 | "max-empty-lines": 1, 94 | "max-line-length": 120, 95 | "max-nesting-depth": 0, 96 | "media-feature-colon-space-after": "always", 97 | "media-feature-colon-space-before": "never", 98 | "media-feature-name-allowed-list": null, 99 | "media-feature-name-case": "lower", 100 | "media-feature-name-disallowed-list": null, 101 | "media-feature-name-no-unknown": true, 102 | "media-feature-name-no-vendor-prefix": true, 103 | "media-feature-name-value-allowed-list": null, 104 | "media-feature-parentheses-space-inside": "never", 105 | "media-feature-range-operator-space-after": "always", 106 | "media-feature-range-operator-space-before": "always", 107 | "media-query-list-comma-newline-after": "always-multi-line", 108 | "media-query-list-comma-newline-before": "never-multi-line", 109 | "media-query-list-comma-space-after": "always-single-line", 110 | "media-query-list-comma-space-before": "never-single-line", 111 | "media-feature-range-notation": "prefix", 112 | "named-grid-areas-no-invalid": true, 113 | "no-descending-specificity": null, 114 | "no-duplicate-at-import-rules": true, 115 | "no-duplicate-selectors": true, 116 | "no-empty-first-line": true, 117 | "no-empty-source": true, 118 | "no-eol-whitespace": true, 119 | "no-extra-semicolons": true, 120 | "no-invalid-double-slash-comments": true, 121 | "no-invalid-position-at-import-rule": true, 122 | "no-irregular-whitespace": true, 123 | "no-missing-end-of-source-newline": true, 124 | "no-unknown-animations": true, 125 | "number-leading-zero": "never", 126 | "number-max-precision": 2, 127 | "number-no-trailing-zeros": true, 128 | "order/properties-order": [ 129 | "appearance", 130 | "display", 131 | "position", 132 | "z-index", 133 | "top", 134 | "bottom", 135 | "left", 136 | "right", 137 | "float", 138 | "clear", 139 | "box-sizing", 140 | "margin", 141 | "padding", 142 | "width", 143 | "height", 144 | "line-height", 145 | "transform", 146 | "font", 147 | "border", 148 | "background", 149 | "color", 150 | "list-style", 151 | "vertical-align", 152 | "text-align", 153 | "text-decoration", 154 | "whitespace", 155 | "overflow", 156 | "cursor", 157 | "opacity", 158 | "animation", 159 | "transition" 160 | ], 161 | "property-allowed-list": null, 162 | "property-case": "lower", 163 | "property-disallowed-list": null, 164 | "property-no-unknown": true, 165 | "property-no-vendor-prefix": true, 166 | "rule-empty-line-before": ["always", { "except" : ["first-nested"] }], 167 | "rule-selector-property-disallowed-list": null, 168 | "selector-attribute-brackets-space-inside": "never", 169 | "selector-attribute-operator-allowed-list": null, 170 | "selector-attribute-operator-disallowed-list": null, 171 | "selector-attribute-operator-space-after": "never", 172 | "selector-attribute-operator-space-before": "never", 173 | "selector-attribute-quotes": "always", 174 | "selector-class-pattern": null, 175 | "selector-combinator-allowed-list": null, 176 | "selector-combinator-disallowed-list": null, 177 | "selector-combinator-space-after": "always", 178 | "selector-combinator-space-before": "always", 179 | "selector-descendant-combinator-no-non-space": true, 180 | "selector-disallowed-list": null, 181 | "selector-id-pattern": null, 182 | "selector-list-comma-newline-after": "always-multi-line", 183 | "selector-list-comma-newline-before": "never-multi-line", 184 | "selector-list-comma-space-after": "always-single-line", 185 | "selector-list-comma-space-before": "never-single-line", 186 | "selector-max-attribute": 2, 187 | "selector-max-class": 3, 188 | "selector-max-combinators": null, 189 | "selector-max-compound-selectors": 4, 190 | "selector-max-id": 2, 191 | "selector-max-pseudo-class": 3, 192 | "selector-max-specificity": null, 193 | "selector-max-type": 3, 194 | "selector-max-universal": 0, 195 | "selector-nested-pattern": null, 196 | "selector-no-qualifying-type": null, 197 | "selector-no-vendor-prefix": true, 198 | "selector-pseudo-class-allowed-list": null, 199 | "selector-pseudo-class-case": "lower", 200 | "selector-pseudo-class-disallowed-list": null, 201 | "selector-pseudo-class-no-unknown": true, 202 | "selector-pseudo-class-parentheses-space-inside": "never", 203 | "selector-pseudo-element-case": "lower", 204 | "selector-pseudo-element-colon-notation": "double", 205 | "selector-pseudo-element-no-unknown": true, 206 | "selector-type-case": "lower", 207 | "selector-type-no-unknown": true, 208 | "selector-max-empty-lines": 0, 209 | "shorthand-property-no-redundant-values": true, 210 | "string-no-newline": true, 211 | "string-quotes": "single", 212 | "time-min-milliseconds": 100, 213 | "unicode-bom": "never", 214 | "unit-allowed-list": null, 215 | "unit-case": "lower", 216 | "unit-disallowed-list": null, 217 | "unit-no-unknown": true, 218 | "value-keyword-case": "lower", 219 | "value-list-comma-newline-after": "always-multi-line", 220 | "value-list-comma-newline-before": "never-multi-line", 221 | "value-list-comma-space-after": "always-single-line", 222 | "value-list-comma-space-before": "never-single-line", 223 | "value-list-max-empty-lines": 0, 224 | "value-no-vendor-prefix": true 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "compat", 4 | "no-unsanitized", 5 | "promise", 6 | "sort-requires", 7 | "xss" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": "latest" 11 | }, 12 | "env": { 13 | "browser": true, 14 | "es6": true, 15 | "webextensions": true 16 | }, 17 | "globals": { 18 | "Promise": true, 19 | "require": true 20 | }, 21 | "rules": { 22 | "accessor-pairs": "error", 23 | "array-bracket-newline": ["error", { "multiline": true }], 24 | "array-bracket-spacing": "error", 25 | "array-callback-return": "error", 26 | "array-element-newline": "off", 27 | "arrow-body-style": "error", 28 | "arrow-parens": "error", 29 | "arrow-spacing": "error", 30 | "block-scoped-var": "error", 31 | "block-spacing": "error", 32 | "brace-style": ["error", "stroustrup"], 33 | "camelcase": "off", 34 | "capitalized-comments": "off", 35 | "class-methods-use-this": "error", 36 | "comma-dangle": "error", 37 | "comma-spacing": "error", 38 | "comma-style": "error", 39 | "compat/compat": "error", 40 | "complexity": "error", 41 | "computed-property-spacing": "error", 42 | "consistent-return": "error", 43 | "consistent-this": ["error", "self"], 44 | "constructor-super": "error", 45 | "curly": "error", 46 | "default-case": "error", 47 | "default-case-last": "error", 48 | "default-param-last": "error", 49 | "dot-location": ["error", "property"], 50 | "dot-notation": "off", 51 | "eol-last": "error", 52 | "eqeqeq": "error", 53 | "for-direction": "error", 54 | "func-call-spacing": "error", 55 | "function-call-argument-newline": ["error", "consistent"], 56 | "func-name-matching": "error", 57 | "func-names": "off", 58 | "func-style": "error", 59 | "function-paren-newline": "off", 60 | "generator-star-spacing": "error", 61 | "getter-return": "error", 62 | "grouped-accessor-pairs": "error", 63 | "guard-for-in": "error", 64 | "id-denylist": "error", 65 | "id-length": ["error", { "max": 80, "exceptions": ["e", "i", "p"] }], 66 | "id-match": "error", 67 | "implicit-arrow-linebreak": ["error", "beside"], 68 | "indent": ["error", 2, { "SwitchCase" : 1}], 69 | "init-declarations": "error", 70 | "jsx-quotes": "error", 71 | "key-spacing": ["error", { "beforeColon": true }], 72 | "keyword-spacing": "error", 73 | "lines-between-class-members": ["error", "always"], 74 | "line-comment-position": "error", 75 | "linebreak-style": ["error", "unix"], 76 | "lines-around-comment":[ "error", { "beforeBlockComment": false }], 77 | "logical-assignment-operators": ["error", "never"], 78 | "max-classes-per-file": "off", 79 | "max-depth": "error", 80 | "max-len": ["off"], 81 | "max-lines": "off", 82 | "max-lines-per-function": "off", 83 | "max-nested-callbacks": ["error", 4], 84 | "max-params": ["error", 7], 85 | "max-statements-per-line": "error", 86 | "max-statements": "off", 87 | "multiline-comment-style": "off", 88 | "multiline-ternary": ["error", "never"], 89 | "new-cap": "error", 90 | "new-parens": "error", 91 | "newline-per-chained-call": "off", 92 | "no-alert": "error", 93 | "no-array-constructor": "error", 94 | "no-async-promise-executor": "error", 95 | "no-await-in-loop": "error", 96 | "no-bitwise": "error", 97 | "no-buffer-constructor": "error", 98 | "no-caller": "error", 99 | "no-case-declarations": "off", 100 | "no-class-assign": "error", 101 | "no-compare-neg-zero": "error", 102 | "no-cond-assign": "error", 103 | "no-confusing-arrow": "error", 104 | "no-console": "error", 105 | "no-const-assign": "error", 106 | "no-constant-binary-expression": "error", 107 | "no-constant-condition": "error", 108 | "no-constructor-return": "error", 109 | "no-continue": "error", 110 | "no-control-regex": "error", 111 | "no-debugger": "error", 112 | "no-delete-var": "error", 113 | "no-div-regex": "error", 114 | "no-dupe-args": "error", 115 | "no-dupe-class-members": "error", 116 | "no-dupe-else-if": "error", 117 | "no-dupe-keys": "error", 118 | "no-duplicate-case": "error", 119 | "no-duplicate-imports": "error", 120 | "no-else-return": "error", 121 | "no-empty": "error", 122 | "no-empty-character-class": "error", 123 | "no-empty-function": "error", 124 | "no-empty-pattern": "error", 125 | "no-empty-static-block": "error", 126 | "no-eq-null": "error", 127 | "no-eval": "error", 128 | "no-ex-assign": "error", 129 | "no-extend-native": "error", 130 | "no-extra-bind": "error", 131 | "no-extra-boolean-cast": "error", 132 | "no-extra-label": "error", 133 | "no-extra-parens": ["error", "all", { "nestedBinaryExpressions": false }], 134 | "no-extra-semi": "error", 135 | "no-fallthrough": "error", 136 | "no-floating-decimal": "error", 137 | "no-func-assign": "error", 138 | "no-global-assign": "error", 139 | "no-implicit-coercion": "error", 140 | "no-implicit-globals": "error", 141 | "no-implied-eval": "error", 142 | "no-import-assign": "error", 143 | "no-inline-comments": "error", 144 | "no-inner-declarations": ["error", "both"], 145 | "no-invalid-regexp": "error", 146 | "no-invalid-this": "error", 147 | "no-irregular-whitespace": ["error", { "skipStrings": false }], 148 | "no-iterator": "error", 149 | "no-label-var": "error", 150 | "no-labels": "error", 151 | "no-lone-blocks": "error", 152 | "no-lonely-if": "error", 153 | "no-loop-func": "error", 154 | "no-loss-of-precision": "error", 155 | "no-magic-numbers": ["error", { "enforceConst": true, "ignore": [-1, 0, 1, 2, 5, 36], "ignoreArrayIndexes": true }], 156 | "no-misleading-character-class": "error", 157 | "no-mixed-operators": "error", 158 | "no-mixed-spaces-and-tabs": "error", 159 | "no-multi-assign": "error", 160 | "no-multi-spaces": "error", 161 | "no-multi-str": "error", 162 | "no-multiple-empty-lines": ["error", { "max": 1, "maxBOF": 0 }], 163 | "no-negated-condition": "error", 164 | "no-nested-ternary": "error", 165 | "no-new": "error", 166 | "no-new-func": "error", 167 | "no-new-native-nonconstructor": "error", 168 | "no-new-symbol": "error", 169 | "no-new-wrappers": "error", 170 | "no-new-object": "error", 171 | "no-nonoctal-decimal-escape": "error", 172 | "no-obj-calls": "error", 173 | "no-octal-escape": "error", 174 | "no-octal": "error", 175 | "no-param-reassign": "error", 176 | "no-plusplus": "off", 177 | "no-promise-executor-return": "error", 178 | "no-proto": "error", 179 | "no-prototype-builtins": "error", 180 | "no-redeclare": ["error", { "builtinGlobals": true }], 181 | "no-regex-spaces": "error", 182 | "no-restricted-exports": "error", 183 | "no-restricted-globals": "error", 184 | "no-restricted-imports": "error", 185 | "no-restricted-properties": "error", 186 | "no-restricted-syntax": "error", 187 | "no-return-assign": ["error", "always"], 188 | "no-return-await": "error", 189 | "no-script-url": "error", 190 | "no-self-assign": ["error", { "props": true }], 191 | "no-self-compare": "error", 192 | "no-sequences": "error", 193 | "no-setter-return": "error", 194 | "no-shadow": "off", 195 | "no-shadow-restricted-names": "error", 196 | "no-sparse-arrays": "error", 197 | "no-tabs": "error", 198 | "no-template-curly-in-string": "error", 199 | "no-ternary": "off", 200 | "no-this-before-super": "error", 201 | "no-throw-literal": "error", 202 | "no-trailing-spaces": "error", 203 | "no-undef-init": "error", 204 | "no-undef": ["error", { "typeof": true }], 205 | "no-undefined": "error", 206 | "no-underscore-dangle": "error", 207 | "no-unexpected-multiline": "error", 208 | "no-unmodified-loop-condition": "error", 209 | "no-unneeded-ternary": "error", 210 | "no-unreachable": "error", 211 | "no-unreachable-loop": "error", 212 | "no-unsafe-finally": "error", 213 | "no-unsafe-negation": "error", 214 | "no-unsafe-optional-chaining": "error", 215 | "no-unsanitized/method": "error", 216 | "no-unsanitized/property": "error", 217 | "no-unused-expressions": ["error", { "allowTernary": true }], 218 | "no-unused-labels": "error", 219 | "no-unused-private-class-members": "error", 220 | "no-unused-vars": ["error", { "vars": "local" }], 221 | "no-use-before-define": "error", 222 | "no-useless-backreference": "error", 223 | "no-useless-call": "error", 224 | "no-useless-catch": "error", 225 | "no-useless-computed-key": "error", 226 | "no-useless-concat": "error", 227 | "no-useless-constructor": "error", 228 | "no-useless-escape": "error", 229 | "no-useless-rename": "error", 230 | "no-useless-return": "error", 231 | "no-var": "error", 232 | "no-void": "off", 233 | "no-warning-comments": "error", 234 | "no-whitespace-before-property": "error", 235 | "no-with": "error", 236 | "nonblock-statement-body-position": ["error", "below"], 237 | "object-curly-newline": "error", 238 | "object-curly-spacing": ["error", "always"], 239 | "object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }], 240 | "object-shorthand": ["error", "methods"], 241 | "one-var-declaration-per-line": ["error", "always"], 242 | "one-var": ["error", "never"], 243 | "operator-assignment": "error", 244 | "operator-linebreak": "error", 245 | "padded-blocks": ["error", "never"], 246 | "padding-line-between-statements": [ 247 | "error", 248 | { "blankLine": "always", "prev": "*", "next": "return" }, 249 | { "blankLine": "always","prev": "directive", "next": "*" }, 250 | { "blankLine": "any", "prev": "directive", "next": "directive" } 251 | ], 252 | "prefer-arrow-callback": "error", 253 | "prefer-const": "error", 254 | "prefer-destructuring": ["error", { "array": false, "object": true }], 255 | "prefer-exponentiation-operator": "error", 256 | "prefer-named-capture-group": "off", 257 | "prefer-numeric-literals": "error", 258 | "prefer-object-has-own": "error", 259 | "prefer-object-spread": "off", 260 | "prefer-promise-reject-errors": "error", 261 | "prefer-regex-literals": "error", 262 | "prefer-rest-params": "error", 263 | "prefer-spread": "error", 264 | "prefer-template": "off", 265 | "promise/always-return": "error", 266 | "promise/avoid-new": "off", 267 | "promise/catch-or-return": "error", 268 | "promise/no-callback-in-promise": "error", 269 | "promise/no-multiple-resolved": "error", 270 | "promise/no-native": "off", 271 | "promise/no-nesting": "error", 272 | "promise/no-promise-in-callback": "error", 273 | "promise/no-return-wrap": "error", 274 | "promise/param-names": "error", 275 | "promise/prefer-await-to-callbacks": "error", 276 | "promise/prefer-await-to-then": "off", 277 | "quote-props": ["error", "as-needed"], 278 | "quotes": ["error", "single"], 279 | "radix": ["error", "as-needed"], 280 | "require-atomic-updates": "error", 281 | "require-await": "error", 282 | "require-jsdoc": ["error", { 283 | "require": { 284 | "FunctionDeclaration": true, 285 | "MethodDefinition": true, 286 | "ClassDeclaration": true, 287 | "ArrowFunctionExpression": false 288 | } 289 | }], 290 | "require-unicode-regexp": "off", 291 | "require-yield": "error", 292 | "rest-spread-spacing": "error", 293 | "semi": "error", 294 | "semi-style": ["error", "last"], 295 | "semi-spacing": "error", 296 | "sort-imports": "error", 297 | "sort-keys": "off", 298 | "sort-requires/sort-requires": "error", 299 | "sort-vars": "error", 300 | "space-before-blocks": "error", 301 | "space-before-function-paren": "error", 302 | "space-in-parens": "error", 303 | "space-infix-ops": "error", 304 | "space-unary-ops": "error", 305 | "spaced-comment": "error", 306 | "strict": ["error", "global"], 307 | "symbol-description": "error", 308 | "switch-colon-spacing": ["error", { "after": true, "before": false }], 309 | "template-curly-spacing": "error", 310 | "template-tag-spacing": ["error", "always"], 311 | "unicode-bom": "error", 312 | "use-isnan": "error", 313 | "valid-jsdoc": ["error", { 314 | "prefer": { 315 | "arg": "param", 316 | "argument": "param", 317 | "class": "constructor", 318 | "return": "returns", 319 | "virtual": "abstract" 320 | }, 321 | "preferType": { 322 | "Boolean": "boolean", 323 | "Number": "number", 324 | "object": "Object", 325 | "String": "string" 326 | } 327 | }], 328 | "valid-typeof": "error", 329 | "vars-on-top": "error", 330 | "wrap-iife": "error", 331 | "wrap-regex": "error", 332 | "xss/no-location-href-assign": "error", 333 | "yield-star-spacing": "error", 334 | "yoda": "error" 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/js/core/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WHITELIST_PAGE = 'options.html'; 4 | 5 | const elBody = document.querySelector('body'); 6 | const elBookmarkCard = document.getElementById('bookmark-card'); 7 | const elBookmarkDateAdded = document.getElementById('bookmark-date-added'); 8 | const elBookmarkId = document.getElementById('bookmark-id'); 9 | const elBookmarkPath = document.getElementById('bookmark-path'); 10 | const elBookmarkUrl = document.getElementById('bookmark-url'); 11 | const elBookmarkTitle = document.getElementById('bookmark-title'); 12 | const elButtonWrapper = document.getElementById('button-wrapper'); 13 | const elConfirmDialog = document.getElementById('confirm-dialog'); 14 | const elEmptyState = document.getElementById('empty-state'); 15 | const elEnableConfirmations = document.getElementById('enable-confirmations'); 16 | const elOpenBookmarkBtn = document.querySelector('button[data-action="open-bookmark"]'); 17 | const elPreviousBookmarkBtn = document.querySelector('button[data-action="previous-bookmark"]'); 18 | const elStatus = document.getElementById('bookmark-status'); 19 | const elStatusIndicator = elStatus.querySelector('.indicator'); 20 | const elStatusText = elStatus.querySelector('.text'); 21 | 22 | /** 23 | * @exports ui 24 | */ 25 | const ui = { 26 | /** 27 | * Whether or not confirmation dialogs are enabled or not. Default: true. 28 | * 29 | * @type {boolean} 30 | */ 31 | confirmations : true, 32 | 33 | /** 34 | * Fired when the initial HTML document has been completely loaded and parsed. Starts the collecting process. 35 | * 36 | * @returns {void} 37 | */ 38 | init () { 39 | browser.runtime.sendMessage({ message : 'collect' }); 40 | ui.setupPermissionGrantHandler(); 41 | }, 42 | 43 | /** 44 | * Set up the listener for the link to grant the permission for the broken bookmark check. 45 | * 46 | * @returns {void} 47 | */ 48 | setupPermissionGrantHandler () { 49 | elStatusText.onclick = async () => { 50 | if (elStatusText.classList.contains('permission-needed')) { 51 | const granted = await browser.permissions.request({ 52 | origins : [''] 53 | }); 54 | 55 | if (granted) { 56 | browser.runtime.sendMessage({ 57 | message : 'recheck-bookmark', 58 | id : elBookmarkId.textContent 59 | }); 60 | } 61 | } 62 | }; 63 | }, 64 | 65 | /** 66 | * Confirmation dialog implementation. 67 | * 68 | * @param {string} title - the title of the bookmark 69 | * 70 | * @returns {Promise} - resolves on success (OK button) 71 | */ 72 | confirm (title) { 73 | const elModal = document.getElementById('confirm-dialog'); 74 | elModal.classList.add('visible'); 75 | 76 | const elTitle = document.getElementById('confirm-title'); 77 | elTitle.textContent = title; 78 | 79 | const elSubmitButton = elModal.querySelector('#button-confirm-ok'); 80 | const elCloseButton = elModal.querySelector('#button-confirm-cancel'); 81 | 82 | const hideModal = () => { 83 | elModal.classList.remove('visible'); 84 | }; 85 | 86 | return new Promise((resolve) => { 87 | window.onkeydown = function (e) { 88 | e.preventDefault(); 89 | 90 | if (e.key === 'Escape') { 91 | hideModal(); 92 | } 93 | else if (e.key === 'Enter') { 94 | hideModal(); 95 | resolve(); 96 | } 97 | }; 98 | 99 | window.onclick = function (e) { 100 | if (e.target === elModal) { 101 | hideModal(); 102 | } 103 | }; 104 | 105 | elCloseButton.onclick = () => { 106 | hideModal(); 107 | }; 108 | 109 | elSubmitButton.onclick = () => { 110 | hideModal(); 111 | resolve(); 112 | }; 113 | }); 114 | }, 115 | 116 | /** 117 | * Fired when a message is sent from the background script to the UI script. 118 | * 119 | * @param {Object} response - contains the response from the background script 120 | * 121 | * @returns {void} 122 | */ 123 | handleResponse (response) { 124 | if (response.message === 'permission-change') { 125 | browser.runtime.sendMessage({ 126 | message : 'recheck-bookmark', 127 | id : elBookmarkId.textContent 128 | }); 129 | } 130 | else if (response.message === 'confirmations') { 131 | ui.confirmations = response.confirmations; 132 | 133 | if (response.confirmations) { 134 | elEnableConfirmations.setAttribute('checked', 'true'); 135 | } 136 | else { 137 | elEnableConfirmations.removeAttribute('checked'); 138 | } 139 | } 140 | else if (response.message === 'show-bookmark') { 141 | if (response.bookmark) { 142 | const pattern = new RegExp(/^https?:\/\//, 'gi'); 143 | const dateAdded = new Intl.DateTimeFormat('default', { 144 | day : '2-digit', month : '2-digit', year : 'numeric' 145 | }).format(new Date(response.bookmark.dateAdded)); 146 | 147 | elBookmarkCard.removeAttribute('hidden'); 148 | elButtonWrapper.removeAttribute('hidden'); 149 | elEmptyState.setAttribute('hidden', 'true'); 150 | elBookmarkId.textContent = response.bookmark.id; 151 | elBookmarkTitle.textContent = response.bookmark.title; 152 | elBookmarkPath.textContent = response.bookmark.path.join(' / '); 153 | elBookmarkDateAdded.textContent = browser.i18n.getMessage('date_added') + ': ' + dateAdded; 154 | 155 | if (elBookmarkUrl.firstChild) { 156 | elBookmarkUrl.removeChild(elBookmarkUrl.firstChild); 157 | } 158 | 159 | if (pattern.test(response.bookmark.url)) { 160 | const elUrl = document.createElement('a'); 161 | elUrl.setAttribute('href', response.bookmark.url); 162 | elUrl.setAttribute('target', '_blank'); 163 | elUrl.setAttribute('rel', 'noopener'); 164 | elUrl.textContent = response.bookmark.url; 165 | elBookmarkUrl.appendChild(elUrl); 166 | elOpenBookmarkBtn.removeAttribute('disabled'); 167 | } 168 | else { 169 | elBookmarkUrl.textContent = response.bookmark.url; 170 | elOpenBookmarkBtn.setAttribute('disabled', 'true'); 171 | } 172 | 173 | elPreviousBookmarkBtn.removeAttribute('disabled'); 174 | } 175 | else { 176 | elBookmarkCard.setAttribute('hidden', 'true'); 177 | elButtonWrapper.setAttribute('hidden', 'true'); 178 | elEmptyState.removeAttribute('hidden'); 179 | } 180 | } 181 | else if (response.message === 'disable-previous-button') { 182 | elPreviousBookmarkBtn.setAttribute('disabled', 'true'); 183 | } 184 | else if (response.message === 'enable-skip-button') { 185 | elButtonWrapper.querySelector('[data-action="skip-bookmark"]').removeAttribute('disabled'); 186 | } 187 | else if (response.message === 'disable-skip-button') { 188 | elButtonWrapper.querySelector('[data-action="skip-bookmark"]').setAttribute('disabled', 'true'); 189 | } 190 | else if (response.message === 'update-bookmark-status') { 191 | if (response.status === 'permission-needed') { 192 | elStatusIndicator.classList.add('failure'); 193 | elStatusIndicator.classList.remove('success'); 194 | elStatusText.classList.add('permission-needed'); 195 | elStatusText.textContent = browser.i18n.getMessage('check_status_permission_needed'); 196 | } 197 | else if (response.status === 'success') { 198 | elStatusIndicator.classList.add('success'); 199 | elStatusIndicator.classList.remove('failure'); 200 | elStatusText.classList.remove('permission-needed'); 201 | elStatusText.textContent = browser.i18n.getMessage('check_status_success'); 202 | } 203 | else if (response.status === 'failure') { 204 | elStatusIndicator.classList.add('failure'); 205 | elStatusIndicator.classList.remove('success'); 206 | elStatusText.classList.remove('permission-needed'); 207 | elStatusText.textContent = browser.i18n.getMessage('check_status_failure'); 208 | } 209 | else if (response.status === 'skip') { 210 | elStatusIndicator.classList.remove('success', 'failure'); 211 | elStatusText.classList.remove('permission-needed'); 212 | elStatusText.textContent = browser.i18n.getMessage('check_status_skipped'); 213 | } 214 | else { 215 | elStatusIndicator.classList.remove('success', 'failure'); 216 | elStatusText.classList.remove('permission-needed'); 217 | elStatusText.textContent = '…'; 218 | } 219 | } 220 | }, 221 | 222 | /** 223 | * Fired when a key is pressed. 224 | * 225 | * @param {KeyboardEvent} e - event 226 | * 227 | * @returns {void} 228 | */ 229 | handleKeyPress (e) { 230 | if (elConfirmDialog.classList.contains('visible')) { 231 | return; 232 | } 233 | 234 | if (e.key === 'Backspace') { 235 | ui.deleteBookmark(elBookmarkId.textContent, elBookmarkTitle.textContent); 236 | } 237 | else if (e.key === ' ') { 238 | ui.keepBookmark( 239 | elBookmarkId.textContent, elBookmarkTitle.textContent, elBookmarkUrl.textContent, elBookmarkPath.textContent 240 | ); 241 | } 242 | else if (e.key === 'Enter') { 243 | window.open(elBookmarkUrl.textContent, '_blank'); 244 | } 245 | else if (e.key === 'ArrowLeft') { 246 | ui.previousBookmark(); 247 | } 248 | else if (e.key === 'ArrowRight') { 249 | ui.skipBookmark(elBookmarkId.textContent); 250 | } 251 | }, 252 | 253 | /** 254 | * Fired when one of the buttons is clicked. 255 | * 256 | * @param {MouseEvent} e - event 257 | * 258 | * @returns {void} 259 | */ 260 | handleButtonClicks (e) { 261 | if (e.target.getAttribute('data-action')) { 262 | e.preventDefault(); 263 | 264 | switch (e.target.getAttribute('data-action')) { 265 | case 'delete-bookmark': 266 | ui.deleteBookmark(elBookmarkId.textContent, elBookmarkTitle.textContent); 267 | break; 268 | case 'keep-bookmark': 269 | ui.keepBookmark( 270 | elBookmarkId.textContent, elBookmarkTitle.textContent, elBookmarkUrl.textContent, elBookmarkPath.textContent 271 | ); 272 | break; 273 | case 'open-bookmark': 274 | window.open(elBookmarkUrl.textContent, '_blank'); 275 | break; 276 | case 'previous-bookmark': 277 | ui.previousBookmark(); 278 | break; 279 | case 'skip-bookmark': 280 | ui.skipBookmark(elBookmarkId.textContent); 281 | break; 282 | case 'open-options': 283 | ui.openWhitelist(); 284 | break; 285 | default: 286 | // do nothing 287 | } 288 | } 289 | }, 290 | 291 | /** 292 | * This method is used to delete a bookmark. 293 | * 294 | * @param {string} id - the id of the bookmark 295 | * @param {string} title - the title of the bookmark 296 | * 297 | * @returns {void} 298 | */ 299 | async deleteBookmark (id, title) { 300 | browser.runtime.sendMessage({ message : 'check-confirmations-state' }); 301 | 302 | if (ui.confirmations) { 303 | await ui.confirm(title); 304 | } 305 | 306 | browser.runtime.sendMessage({ 307 | message : 'delete', 308 | id : id 309 | }); 310 | }, 311 | 312 | /** 313 | * This method is used to keep a bookmark. 314 | * 315 | * @param {string} id - the id of the bookmark 316 | * @param {string} title - the title of the bookmark 317 | * @param {string} url - the URL of the bookmark 318 | * @param {string} path - the path of the bookmark 319 | * 320 | * @returns {void} 321 | */ 322 | keepBookmark (id, title, url, path) { 323 | browser.runtime.sendMessage({ 324 | message : 'keep', 325 | id : id, 326 | title : title, 327 | url : url, 328 | path : path 329 | }); 330 | }, 331 | 332 | /** 333 | * This method is used to show the previous bookmark. 334 | * 335 | * @returns {void} 336 | */ 337 | previousBookmark () { 338 | browser.runtime.sendMessage({ message : 'previous' }); 339 | }, 340 | 341 | /** 342 | * This method is used to skip a bookmark. 343 | * 344 | * @param {string} id - the id of the bookmark 345 | * 346 | * @returns {void} 347 | */ 348 | skipBookmark (id) { 349 | browser.runtime.sendMessage({ 350 | message : 'skip', 351 | id : id 352 | }); 353 | }, 354 | 355 | /** 356 | * This method is used to open the options. 357 | * 358 | * @returns {void} 359 | */ 360 | openWhitelist () { 361 | browser.tabs.update({ url : WHITELIST_PAGE }); 362 | } 363 | }; 364 | 365 | document.addEventListener('DOMContentLoaded', ui.init); 366 | window.addEventListener('keydown', ui.handleKeyPress); 367 | elBody.addEventListener('click', ui.handleButtonClicks); 368 | 369 | elEnableConfirmations.addEventListener('change', (e) => { 370 | ui.confirmations = e.target.checked; 371 | browser.storage.local.set({ confirmations : e.target.checked }); 372 | }); 373 | 374 | browser.runtime.onMessage.addListener(ui.handleResponse); 375 | -------------------------------------------------------------------------------- /src/css/ui.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-body-background: rgb(235, 235, 235); 3 | --color-body-text: rgb(33, 37, 42); 4 | --color-blue: rgb(10, 132, 255); 5 | --color-blue-dark: rgb(0, 102, 225); 6 | --color-dark-border: rgb(54, 57, 89); 7 | --color-disabled: rgb(194, 194, 194); 8 | --color-green: rgb(18, 188, 0); 9 | --color-green-dark: rgb(0, 158, 0); 10 | --color-grey: rgb(200, 200, 200); 11 | --color-grey-dark: rgb(115, 115, 115); 12 | --color-red: rgb(215, 0, 34); 13 | --color-red-dark: rgb(185, 0, 4); 14 | --color-white: rgb(255, 255, 255); 15 | } 16 | 17 | body { 18 | margin: 0; 19 | font-family: Verdana, sans-serif; 20 | font-size: 16px; 21 | background: var(--color-body-background); 22 | color: var(--color-body-text); 23 | overflow-x: hidden; 24 | } 25 | 26 | main { 27 | padding-top: 100px; 28 | padding-bottom: 64px; 29 | } 30 | 31 | .modal-dialog { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | position: fixed; 36 | z-index: 200; 37 | top: 0; 38 | bottom: 0; 39 | left: 0; 40 | right: 0; 41 | transform: scale(.7); 42 | opacity: 0; 43 | transition: all 300ms; 44 | pointer-events: none; 45 | } 46 | 47 | .modal-dialog.visible { 48 | transform: scale(1); 49 | opacity: 1; 50 | pointer-events: all; 51 | } 52 | 53 | .modal-dialog ~ .modal-dialog-bg { 54 | position: fixed; 55 | z-index: 100; 56 | top: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 100%; 60 | visibility: hidden; 61 | background: rgba(0, 0, 0, .8); 62 | opacity: 0; 63 | transition: all 300ms; 64 | } 65 | 66 | .modal-dialog.visible ~ .modal-dialog-bg { 67 | opacity: 1; 68 | visibility: visible; 69 | } 70 | 71 | .modal-dialog > div { 72 | width: 550px; 73 | max-width: 98%; 74 | max-height: 90vh; 75 | overflow: auto; 76 | border-width: 0; 77 | border-radius: 5px; 78 | background-color: var(--color-white); 79 | box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3); 80 | } 81 | 82 | .modal-dialog header { 83 | padding: 10px 15px; 84 | background-color: var(--color-blue); 85 | color: var(--color-white); 86 | } 87 | 88 | .modal-dialog section { 89 | padding: 30px 15px; 90 | } 91 | 92 | .modal-dialog aside { 93 | padding: 15px; 94 | background-color: var(--color-body-background); 95 | text-align: right; 96 | } 97 | 98 | .modal-dialog .primary-action { 99 | background-color: var(--color-green); 100 | } 101 | 102 | .modal-dialog .primary-action::before { 103 | background: var(--color-green-dark); 104 | } 105 | 106 | .modal-dialog .secondary-action { 107 | margin-right: 5px; 108 | background-color: var(--color-red); 109 | } 110 | 111 | .modal-dialog .secondary-action::before { 112 | background: var(--color-red-dark); 113 | } 114 | 115 | .checkbox { 116 | display: inline-block; 117 | position: relative; 118 | margin-bottom: 5px; 119 | margin-right: 15px; 120 | padding-left: 30px; 121 | padding-top: 3px; 122 | } 123 | 124 | .checkbox input { 125 | position: absolute; 126 | left: -9999px; 127 | opacity: 0; 128 | } 129 | 130 | .checkbox label { 131 | cursor: pointer; 132 | font-size: 14px; 133 | } 134 | 135 | .checkbox label::before { 136 | content: ''; 137 | position: absolute; 138 | z-index: 0; 139 | top: 1px; 140 | left: 0; 141 | margin-top: 2px; 142 | width: 18px; 143 | height: 18px; 144 | border: 2px solid var(--color-dark-border); 145 | border-radius: 2px; 146 | cursor: pointer; 147 | transition: all 250ms ease; 148 | } 149 | 150 | .checkbox input:checked + label::before { 151 | top: -1px; 152 | left: -3px; 153 | width: 8px; 154 | height: 17px; 155 | transform: rotate(40deg); 156 | border: 2px solid transparent; 157 | border-right-color: var(--color-green); 158 | border-bottom-color: var(--color-green); 159 | transform-origin: 100% 100%; 160 | backface-visibility: hidden; 161 | } 162 | 163 | button.circle-btn { 164 | border-radius: 50%; 165 | border: 5px solid var(--color-white); 166 | cursor: pointer; 167 | transition: background ease 250ms; 168 | } 169 | 170 | button.circle-btn[disabled], button.circle-btn[disabled]:hover { 171 | color: var(--color-disabled); 172 | } 173 | 174 | button.circle-btn:hover { 175 | color: var(--color-white); 176 | } 177 | 178 | button.circle-btn svg { 179 | display: block; 180 | pointer-events: none; 181 | } 182 | 183 | button.circle-btn[data-action='delete-bookmark'], 184 | button.circle-btn[data-action='keep-bookmark'] { 185 | width: 135px; 186 | height: 135px; 187 | } 188 | 189 | button.circle-btn[data-action='delete-bookmark'] svg, 190 | button.circle-btn[data-action='keep-bookmark'] svg { 191 | margin: 0 auto 8px; 192 | width: 32px; 193 | height: 32px; 194 | } 195 | 196 | button.circle-btn[data-action='previous-bookmark'], 197 | button.circle-btn[data-action='skip-bookmark'], 198 | button.circle-btn[data-action='open-bookmark'] { 199 | width: 90px; 200 | height: 90px; 201 | } 202 | 203 | button.circle-btn[data-action='open-bookmark'] { 204 | margin-top: -15px; 205 | } 206 | 207 | button.circle-btn[data-action='previous-bookmark'] svg, 208 | button.circle-btn[data-action='skip-bookmark'] svg { 209 | margin: 0 auto; 210 | width: 24px; 211 | height: 24px; 212 | } 213 | 214 | button.circle-btn[data-action='open-bookmark'] svg { 215 | margin: 0 auto 4px; 216 | width: 18px; 217 | height: 18px; 218 | } 219 | 220 | button.circle-btn[data-action='delete-bookmark']:not([disabled]):hover { 221 | background-color: rgba(215, 0, 34, .5); 222 | } 223 | 224 | button.circle-btn[data-action='delete-bookmark']:not([disabled]) svg path { 225 | fill: var(--color-red); 226 | } 227 | 228 | button.circle-btn[data-action='delete-bookmark']:not([disabled]):hover svg path { 229 | fill: var(--color-white); 230 | } 231 | 232 | button.circle-btn[data-action='keep-bookmark']:not([disabled]):hover { 233 | background-color: rgba(18, 188, 0, .5); 234 | } 235 | 236 | button.circle-btn[data-action='keep-bookmark']:not([disabled]) svg path { 237 | fill: var(--color-green); 238 | } 239 | 240 | button.circle-btn[data-action='keep-bookmark']:not([disabled]):hover svg path { 241 | fill: var(--color-white); 242 | } 243 | 244 | button.circle-btn[data-action='previous-bookmark']:not([disabled]):hover, 245 | button.circle-btn[data-action='skip-bookmark']:not([disabled]):hover, 246 | button.circle-btn[data-action='open-bookmark']:not([disabled]):hover { 247 | background-color: rgba(10, 132, 255, .5); 248 | } 249 | 250 | button.circle-btn[data-action='previous-bookmark']:not([disabled]) svg path:first-child, 251 | button.circle-btn[data-action='skip-bookmark']:not([disabled]) svg path:first-child { 252 | fill: var(--color-blue); 253 | } 254 | 255 | button.circle-btn[data-action='previous-bookmark']:not([disabled]):hover svg path:first-child, 256 | button.circle-btn[data-action='skip-bookmark']:not([disabled]):hover svg path:first-child { 257 | fill: var(--color-white); 258 | } 259 | 260 | button.circle-btn[data-action='previous-bookmark'][disabled] svg path:first-child, 261 | button.circle-btn[data-action='skip-bookmark'][disabled] svg path:first-child { 262 | fill: var(--color-disabled); 263 | } 264 | 265 | button.circle-btn[data-action='open-bookmark']:not([disabled]) svg { 266 | stroke: var(--color-blue); 267 | } 268 | 269 | button.circle-btn[data-action='open-bookmark']:not([disabled]):hover svg { 270 | stroke: var(--color-white); 271 | } 272 | 273 | button.circle-btn[data-action='open-bookmark'][disabled] svg { 274 | stroke: var(--color-disabled); 275 | } 276 | 277 | button[disabled] { 278 | cursor: not-allowed; 279 | } 280 | 281 | #header { 282 | position: fixed; 283 | z-index: 10; 284 | top: 0; 285 | left: 0; 286 | right: 0; 287 | height: 100px; 288 | background: var(--color-white); 289 | border-bottom: 1px solid var(--color-dark-border); 290 | transition: height 250ms ease-in-out; 291 | } 292 | 293 | #logo { 294 | display: block; 295 | width: 467px; 296 | height: 100px; 297 | background: url('../images/logo-large.png') 25px center / 442px 70px no-repeat transparent; 298 | } 299 | 300 | #header-btn-wrapper { 301 | position: absolute; 302 | z-index: 20; 303 | top: 30px; 304 | right: 15px; 305 | } 306 | 307 | button.action-btn { 308 | padding: 8px 15px; 309 | transform: perspective(1px) translateZ(0); 310 | font-size: 16px; 311 | border: none; 312 | border-radius: 2px; 313 | background: var(--color-blue); 314 | color: var(--color-white); 315 | cursor: pointer; 316 | transition: color ease 500ms; 317 | } 318 | 319 | button.action-btn::before { 320 | content: ''; 321 | position: absolute; 322 | z-index: -1; 323 | top: 0; 324 | bottom: 0; 325 | left: 0; 326 | right: 0; 327 | transform: scaleX(0); 328 | border-radius: 2px; 329 | background: var(--color-blue-dark); 330 | transform-origin: 0 50%; 331 | transition-property: transform; 332 | transition-duration: 500ms; 333 | transition-timing-function: ease-out; 334 | } 335 | 336 | button.action-btn:hover::before { 337 | transform: scaleX(1); 338 | transition-timing-function: cubic-bezier(.52, 1.64, .37, .66); 339 | } 340 | 341 | #button-wrapper { 342 | margin-top: 50px; 343 | text-align: center; 344 | } 345 | 346 | #bookmark-card-wrapper, 347 | #no-bookmarks:not([hidden]) { 348 | display: flex; 349 | justify-content: center; 350 | align-items: center; 351 | height: calc(100vh - 165px); 352 | } 353 | 354 | #bookmark-card { 355 | text-align: center; 356 | } 357 | 358 | #bookmark-title { 359 | font-weight: 700; 360 | font-size: 30px; 361 | } 362 | 363 | #bookmark-url { 364 | margin-top: 50px; 365 | } 366 | 367 | #bookmark-url::before, #whitelist-table td:nth-child(2)::before { 368 | content: ''; 369 | display: inline-block; 370 | margin-right: 10px; 371 | width: 24px; 372 | height: 24px; 373 | background: url('../images/link.svg') center center / 24px 24px no-repeat transparent; 374 | vertical-align: -6px; 375 | } 376 | 377 | #bookmark-url a, #whitelist-table a { 378 | color: var(--color-blue); 379 | text-decoration: none; 380 | transition: color ease 250ms; 381 | } 382 | 383 | #bookmark-url a:hover, #whitelist-table a:hover { 384 | color: var(--color-blue-dark); 385 | } 386 | 387 | #bookmark-path { 388 | margin-top: 10px; 389 | } 390 | 391 | #bookmark-path::before, #whitelist-table td:nth-child(3)::before { 392 | content: ''; 393 | display: inline-block; 394 | margin-right: 10px; 395 | width: 24px; 396 | height: 24px; 397 | background: url('../images/path.svg') center center / 24px 24px no-repeat transparent; 398 | vertical-align: -6px; 399 | } 400 | 401 | #bookmark-status { 402 | margin-top: 30px; 403 | font-size: 14px; 404 | } 405 | 406 | #bookmark-status .indicator { 407 | display: inline-block; 408 | margin-right: 10px; 409 | width: 16px; 410 | height: 16px; 411 | border-radius: 50%; 412 | background-color: var(--color-grey-dark); 413 | vertical-align: -3px; 414 | } 415 | 416 | #bookmark-status .indicator.success { 417 | background-color: var(--color-green-dark); 418 | } 419 | 420 | #bookmark-status .indicator.failure { 421 | background-color: var(--color-red-dark); 422 | } 423 | 424 | #bookmark-status .permission-needed { 425 | color: var(--color-red); 426 | cursor: pointer; 427 | transition: color ease 250ms; 428 | } 429 | 430 | #bookmark-status .permission-needed:hover { 431 | color: var(--color-red-dark); 432 | } 433 | 434 | #bookmark-date-added { 435 | margin-top: 10px; 436 | font-size: 14px; 437 | color: var(--color-grey-dark); 438 | } 439 | 440 | #bookmark-date-added::before { 441 | content: ''; 442 | display: inline-block; 443 | margin-right: 5px; 444 | width: 18px; 445 | height: 18px; 446 | background: url('../images/calendar.svg') center center / 18px 18px no-repeat transparent; 447 | vertical-align: -3px; 448 | } 449 | 450 | #whitelist-table { 451 | table-layout: fixed; 452 | margin: 30px; 453 | width: calc(100vw - 60px); 454 | border-collapse: collapse; 455 | } 456 | 457 | #whitelist-table th, #whitelist-table td { 458 | padding: 15px 5px; 459 | text-align: left; 460 | } 461 | 462 | #whitelist-table th { 463 | border-bottom: 1px solid var(--color-dark-border); 464 | } 465 | 466 | #whitelist-table th:not(:last-child) { 467 | width: 33%; 468 | } 469 | 470 | #whitelist-table tr:not(:last-child) td { 471 | border-bottom: 1px solid var(--color-grey); 472 | } 473 | 474 | #whitelist-table td { 475 | overflow: hidden; 476 | text-overflow: ellipsis; 477 | white-space: nowrap; 478 | } 479 | 480 | #whitelist-table td:first-child { 481 | padding-right: 10px; 482 | max-width: 230px; 483 | overflow: hidden; 484 | text-overflow: ellipsis; 485 | } 486 | 487 | #whitelist-table .actions { 488 | width: 90px; 489 | text-align: right; 490 | } 491 | 492 | #whitelist-table .icon img { 493 | width: 24px; 494 | height: 24px; 495 | vertical-align: middle; 496 | cursor: pointer; 497 | opacity: .5; 498 | transition: opacity ease 250ms; 499 | } 500 | 501 | #whitelist-table .icon img:hover { 502 | opacity: 1; 503 | } 504 | 505 | #whitelist-remove-all-wrapper { 506 | margin: 30px 0; 507 | text-align: center; 508 | } 509 | 510 | #footer { 511 | position: fixed; 512 | bottom: 0; 513 | left: 0; 514 | right: 0; 515 | padding: 0 20px; 516 | height: 64px; 517 | line-height: 64px; 518 | font-size: 14px; 519 | border-top: 1px solid var(--color-dark-border); 520 | background: var(--color-white); 521 | } 522 | 523 | #footer a { 524 | display: block; 525 | padding-left: 40px; 526 | background-image: url('../images/sh-at.png'); 527 | background-repeat: no-repeat; 528 | background-size: 32px 32px; 529 | background-position: center left; 530 | color: var(--color-body-text); 531 | text-decoration: none; 532 | } 533 | 534 | @media screen and (max-width: 899px) { 535 | .checkbox { 536 | display: none; 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /src/js/core/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const UI_PAGE = 'html/ui.html'; 4 | 5 | /** 6 | * @exports kodb 7 | */ 8 | const kodb = { 9 | /** 10 | * Timeout for reaching a server in milliseconds. It's always 15 seconds, there is no user setting. 11 | * 12 | * @type {int} 13 | */ 14 | CHECK_TIMEOUT_IN_MS : 15000, 15 | 16 | /** 17 | * Whether the internal skip list should be used or not. It's always true, there is no user setting. 18 | * 19 | * @type {boolean} 20 | */ 21 | CHECK_USE_SKIP_LIST : true, 22 | 23 | /** 24 | * An array of URL patterns which should be ignored while checking for broken bookmarks. Please only add patterns 25 | * if there are known problems and add a comment with the corresponding GitHub issue. 26 | * 27 | * @type {Array.} 28 | * 29 | */ 30 | CHECK_SKIP_LIST : [ 31 | /* eslint-disable no-useless-escape, line-comment-position, no-inline-comments */ 32 | '^https?:\/\/groups.google.com/group/', // issue #9 33 | '^https?:\/\/accounts-static.cdn.mozilla.net', // issue #9 34 | '^https?:\/\/accounts.firefox.com', // issue #9 35 | '^https?:\/\/addons.cdn.mozilla.net', // issue #9 36 | '^https?:\/\/addons.mozilla.org', // issue #9 37 | '^https?:\/\/api.accounts.firefox.com', // issue #9 38 | '^https?:\/\/content.cdn.mozilla.net', // issue #9 39 | '^https?:\/\/discovery.addons.mozilla.org', // issue #9 40 | '^https?:\/\/install.mozilla.org', // issue #9 41 | '^https?:\/\/oauth.accounts.firefox.com', // issue #9 42 | '^https?:\/\/profile.accounts.firefox.com', // issue #9 43 | '^https?:\/\/support.mozilla.org', // issue #9 44 | '^https?:\/\/sync.services.mozilla.com' // issue #9 45 | /* eslint-enable no-useless-escape, line-comment-position, no-inline-comments */ 46 | ], 47 | 48 | /** 49 | * Status code for a server response we are waiting for. 50 | * 51 | * @type {string} 52 | */ 53 | CHECK_STATUS_AWAIT : 'await', 54 | 55 | /** 56 | * Status code for a broken bookmark. 57 | * 58 | * @type {string} 59 | */ 60 | CHECK_STATUS_FAILURE : 'failure', 61 | 62 | /** 63 | * Status code for a broken bookmark. 64 | * 65 | * @type {string} 66 | */ 67 | CHECK_STATUS_PERMISSION_NEEDED : 'permission-needed', 68 | 69 | /** 70 | * Status code if a bookmark can not be checked. 71 | * 72 | * @type {string} 73 | */ 74 | CHECK_STATUS_SKIP : 'skip', 75 | 76 | /** 77 | * Status code for a working bookmark. 78 | * 79 | * @type {string} 80 | */ 81 | CHECK_STATUS_SUCCESS : 'success', 82 | 83 | /** 84 | * An object containing the IDs and paths of all bookmarks on the whitelist. 85 | * 86 | * @type {Object} 87 | */ 88 | whitelist : {}, 89 | 90 | /** 91 | * An array containing all the user's bookmarks. 92 | * 93 | * @type {Array.} 94 | */ 95 | collectedBookmarks : [], 96 | 97 | /** 98 | * Additional data stored for bookmarks. In current version it only contains the full bookmark path. 99 | * 100 | * @type {Array.} 101 | */ 102 | additionalData : [], 103 | 104 | /** 105 | * Fired when the permission to access all website data is granted or revoked. 106 | * 107 | * @returns {void} 108 | */ 109 | onPermissionChanged () { 110 | browser.runtime.sendMessage({ 111 | message : 'permission-change' 112 | }); 113 | }, 114 | 115 | /** 116 | * Fired when a bookmark changed. 117 | * 118 | * @returns {void} 119 | */ 120 | async onBookmarkChanged () { 121 | // refresh the collection if a new bookmark was added or an existing bookmark was changed 122 | await kodb.collectAllBookmarks(); 123 | 124 | if (kodb.collectedBookmarks.length === 1) { 125 | // in case a new bookmark was added, replace the empty state page with the only bookmark 126 | kodb.showNextBookmark(null); 127 | } 128 | else { 129 | // we have at least two bookmarks, so make sure that the skip button is enabled 130 | browser.runtime.sendMessage({ message : 'enable-skip-button' }); 131 | } 132 | }, 133 | 134 | /** 135 | * Fired when a bookmark is deleted. 136 | * 137 | * @param {int} id - id of the bookmark that was deleted 138 | * 139 | * @returns {void} 140 | */ 141 | async onBookmarkRemoved (id) { 142 | const { whitelist, stack } = await browser.storage.local.get({ whitelist : {}, stack : [] }); 143 | 144 | // remove bookmark from the whitelist 145 | if (whitelist[id]) { 146 | delete whitelist[id]; 147 | browser.storage.local.set({ whitelist : whitelist }); 148 | } 149 | 150 | // read bookmarks again to make sure that the removed bookmark is no longer available in the collection 151 | await kodb.collectAllBookmarks(); 152 | await kodb.showNextBookmark(null); 153 | 154 | // show the empty state if there are no remaining bookmarks 155 | if (kodb.collectedBookmarks.length === 0) { 156 | browser.runtime.sendMessage({ message : 'show-bookmark' }); 157 | } 158 | // make sure that neither the previous nor the skip button is available if there are not at least two bookmarks 159 | else if (kodb.collectedBookmarks.length < 2) { 160 | browser.runtime.sendMessage({ message : 'disable-previous-button' }); 161 | browser.runtime.sendMessage({ message : 'disable-skip-button' }); 162 | } 163 | 164 | // remove bookmark from the stack 165 | stack.splice(stack.indexOf(id), 1); 166 | browser.storage.local.set({ stack : stack }); 167 | 168 | if (stack.length === 0) { 169 | browser.runtime.sendMessage({ message : 'disable-previous-button' }); 170 | } 171 | }, 172 | 173 | /** 174 | * Fired when the toolbar icon is clicked. This method is used to open the user interface in a new tab or to switch 175 | * to the tab with the user interface if the user interface is already opened. 176 | * 177 | * @returns {void} 178 | */ 179 | openUserInterface () { 180 | const url = browser.runtime.getURL(UI_PAGE); 181 | 182 | browser.tabs.query({}, (tabs) => { 183 | let tabId = null; 184 | 185 | for (const tab of tabs) { 186 | if (tab.url === url) { 187 | tabId = tab.id; 188 | break; 189 | } 190 | } 191 | 192 | if (tabId) { 193 | browser.tabs.update(tabId, { active : true }); 194 | } 195 | else { 196 | browser.tabs.create({ url }); 197 | } 198 | }); 199 | }, 200 | 201 | /** 202 | * Fired when a message is sent from the UI script to the background script. 203 | * 204 | * @param {Object} response - contains the response from the UI script 205 | * 206 | * @returns {void} 207 | */ 208 | async handleResponse (response) { 209 | if (response.message === 'collect') { 210 | const { whitelist, confirmations } = await browser.storage.local.get({ whitelist : {}, confirmations : true }); 211 | kodb.whitelist = whitelist; 212 | 213 | browser.runtime.sendMessage({ message : 'confirmations', confirmations : confirmations }); 214 | 215 | await kodb.collectAllBookmarks(); 216 | await kodb.showNextBookmark(null); 217 | 218 | // start with an empty stack 219 | browser.storage.local.set({ stack : [] }); 220 | browser.runtime.sendMessage({ message : 'disable-previous-button' }); 221 | } 222 | else if (response.message === 'check-confirmations-state') { 223 | const { confirmations } = await browser.storage.local.get({ confirmations : true }); 224 | browser.runtime.sendMessage({ message : 'confirmations', confirmations : confirmations }); 225 | } 226 | else if (response.message === 'recheck-bookmark') { 227 | const bookmarks = await kodb.getBookmarks(); 228 | const bookmark = bookmarks[kodb.getIndexById(response.id)]; 229 | kodb.checkForBrokenBookmark(bookmark); 230 | } 231 | else if (response.message === 'delete') { 232 | // let's only remove the bookmark here, the onRemoved handler will do everything else 233 | browser.bookmarks.remove(response.id); 234 | } 235 | else if (response.message === 'keep') { 236 | kodb.addToWhitelist(response.id, response.title, response.url, response.path); 237 | kodb.removeFromCollectedBookmarks(response.id); 238 | kodb.showNextBookmark(response.id); 239 | } 240 | else if (response.message === 'previous') { 241 | kodb.showPreviousBookmark(); 242 | } 243 | else if (response.message === 'skip') { 244 | kodb.showNextBookmark(response.id); 245 | } 246 | }, 247 | 248 | /** 249 | * Calculates the full path of bookmarks. 250 | * 251 | * @param {bookmarks.BookmarkTreeNode} bookmark - a single bookmark 252 | * @param {Array.} path - an array with parts of the bookmark path 253 | * 254 | * @returns {Array.} - an array with the full path of all bookmarks 255 | */ 256 | calculateBookmarkPaths (bookmark, path) { 257 | if (bookmark.title) { 258 | path.push(bookmark.title); 259 | } 260 | 261 | if (bookmark.children) { 262 | for (const child of bookmark.children) { 263 | kodb.calculateBookmarkPaths(child, path); 264 | } 265 | } 266 | else { 267 | if (!kodb.additionalData[bookmark.id]) { 268 | kodb.additionalData[bookmark.id] = {}; 269 | } 270 | 271 | kodb.additionalData[bookmark.id].path = path.slice(0, -1); 272 | } 273 | 274 | path.pop(); 275 | 276 | return kodb.additionalData; 277 | }, 278 | 279 | /** 280 | * Returns the collected bookmarks. 281 | * 282 | * @returns {Array.} - an array with the collected bookmarks 283 | */ 284 | async getBookmarks () { 285 | if (kodb.collectedBookmarks.length === 0) { 286 | await kodb.collectAllBookmarks(); 287 | } 288 | 289 | return kodb.collectedBookmarks; 290 | }, 291 | 292 | /** 293 | * This method is used to start collecting all bookmarks. 294 | * 295 | * @returns {Promise} - resolves after completion 296 | */ 297 | async collectAllBookmarks () { 298 | const bookmarks = await browser.bookmarks.getTree(); 299 | 300 | return new Promise((resolve) => { 301 | kodb.collectedBookmarks = []; 302 | kodb.calculateBookmarkPaths(bookmarks[0], []); 303 | kodb.collectBookmark(bookmarks[0]); 304 | 305 | resolve(); 306 | }); 307 | }, 308 | 309 | /** 310 | * This recursive method pushes a single bookmark to a global array of bookmarks and calls itself for each child. 311 | * 312 | * @param {bookmarks.BookmarkTreeNode} bookmark - a single bookmark 313 | * 314 | * @returns {void} 315 | */ 316 | collectBookmark (bookmark) { 317 | // we only collect bookmarks, no folders or separators 318 | if (bookmark.type === 'bookmark' && !kodb.whitelist[bookmark.id]) { 319 | const { id, title, url, dateAdded } = bookmark; 320 | const { path } = kodb.additionalData[id]; 321 | 322 | kodb.collectedBookmarks.push({ id, title, url, dateAdded, path }); 323 | } 324 | 325 | if (bookmark.children) { 326 | for (const child of bookmark.children) { 327 | kodb.collectBookmark(child); 328 | } 329 | } 330 | }, 331 | 332 | /** 333 | * This method finds the index of a bookmark in the array of collected bookmarks. 334 | * 335 | * @param {string} id - the ID of the bookmark 336 | * 337 | * @returns {int} - the index in the array 338 | */ 339 | getIndexById (id) { 340 | return kodb.collectedBookmarks.findIndex((bookmark) => bookmark.id === id); 341 | }, 342 | 343 | /** 344 | * This method is used to load the previously shown bookmark. 345 | * 346 | * @returns {void} 347 | */ 348 | async showPreviousBookmark () { 349 | const { stack } = await browser.storage.local.get({ stack : [] }); 350 | const previousBookmarkId = stack.pop(); 351 | 352 | browser.storage.local.set({ stack : stack }); 353 | 354 | if (previousBookmarkId) { 355 | const bookmarks = await kodb.getBookmarks(); 356 | const previousBookmark = bookmarks[kodb.getIndexById(previousBookmarkId)]; 357 | browser.runtime.sendMessage({ message : 'show-bookmark', bookmark : previousBookmark }); 358 | kodb.checkForBrokenBookmark(previousBookmark); 359 | } 360 | 361 | if (stack.length === 0) { 362 | browser.runtime.sendMessage({ message : 'disable-previous-button' }); 363 | } 364 | }, 365 | 366 | /** 367 | * This method changes the bookmark that will be displayed next and makes sure that the same bookmark is never 368 | * displayed twice in a row. 369 | * 370 | * @param {string|null} id - the ID of the bookmark 371 | * 372 | * @returns {void} 373 | */ 374 | async showNextBookmark (id) { 375 | const bookmarks = await kodb.getBookmarks(); 376 | const { length } = bookmarks; 377 | let nextBookmarkId = id; 378 | let nextBookmark = null; 379 | 380 | if (length > 1) { 381 | if (id) { 382 | const { stack } = await browser.storage.local.get({ stack : [] }); 383 | stack.push(id); 384 | browser.storage.local.set({ stack : stack }); 385 | } 386 | 387 | while (id === nextBookmarkId) { 388 | const idx = Math.floor(Math.random() * length); 389 | nextBookmark = bookmarks[idx]; 390 | 391 | if (nextBookmark) { 392 | nextBookmarkId = nextBookmark.id; 393 | } 394 | } 395 | } 396 | else { 397 | nextBookmark = bookmarks[0]; 398 | browser.runtime.sendMessage({ message : 'disable-skip-button' }); 399 | } 400 | 401 | browser.runtime.sendMessage({ message : 'show-bookmark', bookmark : nextBookmark }); 402 | 403 | kodb.checkForBrokenBookmark(nextBookmark); 404 | }, 405 | 406 | /** 407 | * This method removes a bookmark from the collected bookmarks array. 408 | * 409 | * @param {string} id - the ID of the bookmark 410 | * 411 | * @returns {void} 412 | */ 413 | async removeFromCollectedBookmarks (id) { 414 | const bookmarks = await kodb.getBookmarks(); 415 | bookmarks.splice(kodb.getIndexById(id), 1); 416 | delete kodb.additionalData[id]; 417 | }, 418 | 419 | /** 420 | * Adds a bookmark to the whitelist. 421 | * 422 | * @param {string} id - the id of the bookmark 423 | * @param {string} title - the title of the bookmark 424 | * @param {string} url - the URL of the bookmark 425 | * @param {string} path - the path of the bookmark 426 | * 427 | * @returns {void} 428 | */ 429 | async addToWhitelist (id, title, url, path) { 430 | const { whitelist } = await browser.storage.local.get({ whitelist : {} }); 431 | 432 | if (!whitelist[id]) { 433 | whitelist[id] = { title : title, url : url, path : path }; 434 | browser.storage.local.set({ whitelist : whitelist }); 435 | } 436 | }, 437 | 438 | /** 439 | * This method is used to check for a broken bookmark. 440 | * 441 | * @param {bookmarks.BookmarkTreeNode} bookmark - the bookmark object 442 | * 443 | * @returns {void} 444 | */ 445 | async checkForBrokenBookmark (bookmark) { 446 | const granted = await browser.permissions.contains({ origins : [''] }); 447 | 448 | if (!granted) { 449 | browser.runtime.sendMessage({ message : 'update-bookmark-status', status : kodb.CHECK_STATUS_PERMISSION_NEEDED }); 450 | 451 | return; 452 | } 453 | 454 | browser.runtime.sendMessage({ message : 'update-bookmark-status', status : kodb.CHECK_STATUS_AWAIT }); 455 | 456 | if (kodb.CHECK_USE_SKIP_LIST && kodb.CHECK_SKIP_LIST.some((i) => new RegExp('\\b' + i + '\\b').test(bookmark.url))) { 457 | browser.runtime.sendMessage({ message : 'update-bookmark-status', status : kodb.CHECK_STATUS_SKIP }); 458 | 459 | return; 460 | } 461 | 462 | let status = kodb.CHECK_STATUS_SKIP; 463 | 464 | if ((/^https?:\/\//).test(bookmark.url)) { 465 | status = await kodb.checkHttpResponse(bookmark, 'HEAD'); 466 | } 467 | 468 | browser.runtime.sendMessage({ message : 'update-bookmark-status', status : status }); 469 | }, 470 | 471 | /** 472 | * This method sends a fetch request to check if a bookmark is broken or not. 473 | * 474 | * @param {bookmarks.BookmarkTreeNode} bookmark - a single bookmark 475 | * @param {string} method - the HTTP method to use (HEAD for first attempt, GET for second attempt) 476 | * 477 | * @returns {Promise} - status of the bookmark 478 | */ 479 | async checkHttpResponse (bookmark, method) { 480 | try { 481 | const controller = new AbortController(); 482 | const { signal } = controller; 483 | 484 | setTimeout(() => controller.abort(), kodb.CHECK_TIMEOUT_IN_MS); 485 | 486 | const response = await fetch(bookmark.url, { 487 | cache : 'no-store', 488 | method : method, 489 | mode : 'no-cors', 490 | signal : signal 491 | }); 492 | 493 | if (method === 'HEAD') { 494 | if (!response.redirected) { 495 | const { headers } = response; 496 | 497 | if (headers.has('Content-Length') && headers.get('Content-Length') === '0') { 498 | return kodb.checkHttpResponse(bookmark, 'GET'); 499 | } 500 | } 501 | 502 | if (!response.ok) { 503 | return kodb.checkHttpResponse(bookmark, 'GET'); 504 | } 505 | } 506 | 507 | return response.ok ? kodb.CHECK_STATUS_SUCCESS : kodb.CHECK_STATUS_FAILURE; 508 | } 509 | catch (error) { 510 | if (method === 'HEAD') { 511 | return kodb.checkHttpResponse(bookmark, 'GET'); 512 | } 513 | 514 | return kodb.CHECK_STATUS_FAILURE; 515 | } 516 | } 517 | }; 518 | 519 | browser.permissions.onAdded.addListener(kodb.onPermissionChanged); 520 | browser.permissions.onRemoved.addListener(kodb.onPermissionChanged); 521 | browser.bookmarks.onChanged.addListener(kodb.onBookmarkChanged); 522 | browser.bookmarks.onRemoved.addListener(kodb.onBookmarkRemoved); 523 | browser.action.onClicked.addListener(kodb.openUserInterface); 524 | browser.runtime.onMessage.addListener(kodb.handleResponse); 525 | 526 | browser.runtime.onInstalled.addListener(() => { 527 | browser.menus.create({ 528 | id : 'kodb-tools-menu-entry', 529 | title : browser.i18n.getMessage('extension_name'), 530 | contexts : ['tools_menu'], 531 | command : '_execute_action' 532 | }); 533 | }); 534 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------