├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── BUILD.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── DeleteHistory.ts ├── MessageInterface.ts ├── OptionsInterface.ts ├── PermissionCheckbox.ts ├── Photon │ ├── Checkbox.css │ ├── DefaultButton.css │ ├── Dropdown.css │ └── Input.css ├── ToggleButton.ts ├── _locales │ └── en_US │ │ └── messages.json ├── cr_background.ts ├── cr_manifest.json ├── cr_popup.html ├── cr_popup.ts ├── ff_background.ts ├── ff_manifest.json ├── ff_popup.html ├── ff_popup.ts ├── i18n.ts ├── icons │ ├── alarm.svg │ ├── amo.png │ ├── cloud-download.svg │ ├── cloud-upload.svg │ ├── cws.png │ ├── delete-clock.svg │ ├── delete-forever.svg │ ├── delete-off.svg │ ├── delete.svg │ ├── file-download.svg │ ├── file-upload.svg │ ├── history.svg │ ├── icon-48.png │ ├── icon-96.png │ ├── icon-dark.png │ ├── icon.svg │ ├── icon_red_circle.png │ ├── icon_red_circle_gradient.png │ ├── icon_red_square.png │ ├── icon_red_square_gradient.png │ ├── kofi.png │ ├── power.svg │ └── timer-sand.svg ├── options.css ├── options.html ├── options.ts ├── popup.css └── we.ts ├── tsconfig.json ├── tsglobal.d.ts ├── webpack.chrome.config.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [*.json] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | *.html 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | "indent": ["error", 4, { "SwitchCase": 1 }], 13 | "quotes": ["warn", "double"], 14 | "semi": ["warn", "always"], 15 | "eqeqeq": ["error", "always"], 16 | "no-trailing-spaces": "warn", 17 | "comma-spacing": "warn", 18 | "no-nested-ternary": "warn", 19 | "max-depth": ["warn", 4] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.ts text eol=lf 4 | *.js text eol=lf 5 | *.json text eol=lf 6 | *.html text eol=lf 7 | *.css text eol=lf 8 | 9 | *.png binary 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: arnaught 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules/ 3 | dist/ 4 | .cache/ 5 | web-ext-artifacts/ 6 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Building History Cleaner 1.7.0 2 | 3 | ## System Info 4 | * Fedora 41 5 | * node v22.11.0 6 | * npm 10.9.2 7 | * bash 5.2.32 8 | * For tool and library versions, check package.json 9 | 10 | # Building Manifest v2 (Firefox) 11 | 12 | To build the extension, run: 13 | 14 | ```shell 15 | # Install node modules 16 | npm install 17 | 18 | # Build the extension 19 | # Moves / bundles assets to dist 20 | npm run build:ff:prod 21 | 22 | # Zip extension for distribution 23 | npm run build:extension 24 | ``` 25 | 26 | The built files will be in `./dist` and the complete extension will be `./web-ext-artifacts/history_cleaner-1.6.0.zip` 27 | 28 | # Testing the extension 29 | 30 | To test the extension, run: 31 | 32 | ```shell 33 | # build manifest v2 version 34 | npm run build:ff:dev 35 | 36 | # build manifest v3 version 37 | npm run build:cr:dev 38 | ``` 39 | 40 | This will build the extension to `./dist` and rebuild it on file changes. `./dist` can be loaded as a temporary extension in Chrome or Firefox. 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.7.0 Changelog 2 | 3 | * Add option to clear download history. 4 | * Add option to change icon. 5 | * Download and Notification permissions are now optional. 6 | 7 | [Chrome] 8 | 9 | * Change default icon. 10 | * Disable theme icon, as theme icons aren't supported in Chrome. 11 | * [Known Issue] The optional permission checkboxes close the popup in Chrome. They *should* work as expected, but you may want to toggle them from `chrome://extensions` instead. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rayquaza01 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HistoryCleaner 2 | [![](https://img.shields.io/amo/v/history-cleaner)](https://addons.mozilla.org/en-US/firefox/addon/history-cleaner/) 3 | [![](https://img.shields.io/chrome-web-store/v/epoabannnmjdknejdggkgjoebomipene)](https://chrome.google.com/webstore/detail/history-cleaner/epoabannnmjdknejdggkgjoebomipene/) 4 | 5 | Firefox addon that deletes history older than a specified amount of days. 6 | 7 | ## Options 8 | 9 | * Behavior 10 | * Decides what history the extension will delete when it triggers. 11 | * Either disabled, delete old history, or delete all history. 12 | * Defaults to disabled, so the extension doesn't do anything until you configure it. 13 | * Number of Days to Keep History 14 | * Delete history older than midnight on the specified number of days ago. 15 | * Only has effect if behavior is set to delete old history. 16 | * Defaults to 0. 17 | * Trigger Mode 18 | * Whether the extension triggers on idle, on browser startup, or at a set interval. 19 | * Defaults to idle (Firefox) 20 | * Defaults to timer (Chrome) 21 | * Idle Length (Firefox Only) 22 | * Amount of time in seconds the browser should idle before triggering. 23 | * Only has effect if trigger mode is set to idle. 24 | * Defaults to 60, minimum 15. 25 | * Timer Interval 26 | * Interval in minutes between triggering. 27 | * Only has effect if trigger mode is set to timer. 28 | * Defaults to 1440 (24 hours), Minimum 1 29 | 30 | ## Permissions 31 | 32 | * `history` 33 | * Used to clear browser history 34 | * `storage` 35 | * Used to save user options 36 | * `idle` 37 | * Used to detect when the browser is idle for the idle trigger mode 38 | * `notifications` 39 | * Used to send a notification when history is cleared 40 | * Notifications are only sent if the user enables notifications in options 41 | * `downloads` 42 | * Used to clear download history 43 | * Download history is only cleared if user enables the option 44 | * `alarms` 45 | * Used to set a timer for the timer trigger mode. 46 | 47 | ## Building and Running 48 | 49 | Clone this repository, and run `npm install` to install necessary dependencies and build tools. 50 | 51 | * `npm run build:ff:watch` will build the extension in watch mode for development. 52 | * `npm run build:ff:prod` will build the extension for production. 53 | * `build:cr:watch` and `build:cr:prod` will build the manifest v3 version of the extension for Chrome. 54 | * The manifest v3 (Chrome) source is located under `./cr-src/`, whereas the manifest v2 (Firefox) source is under `./src/` 55 | * `npm run firefox` will load `./dist/` as a temporary extension in Firefox. 56 | 57 | ## Acknowledgements 58 | 59 | Icons used in History Cleaner are from [Pictogrammers](https://pictogrammers.com/). ([Pictogrammers Free License](https://pictogrammers.com/docs/general/license/)) 60 | 61 | ## Links 62 | 63 | [![](https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/master/src/icons/amo.png)](https://addons.mozilla.org/en-US/firefox/addon/history-cleaner/) 64 | [![](https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/master/src/icons/cws.png)](https://chrome.google.com/webstore/detail/history-cleaner/epoabannnmjdknejdggkgjoebomipene/) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "history-cleaner", 3 | "version": "1.7.0", 4 | "description": "Firefox addon that deletes history older than a specified amount of days.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:ff:watch": "webpack --config webpack.config.js --mode=development --watch", 8 | "build:ff:prod": "webpack --config webpack.config.js --mode=production", 9 | "build:cr:watch": "webpack --config webpack.chrome.config.js --mode=development --watch", 10 | "build:cr:prod": "webpack --config webpack.chrome.config.js --mode=production", 11 | "build:extension": "web-ext build -s dist --overwrite-dest", 12 | "firefox": "web-ext run -s dist", 13 | "lint:extension": "web-ext lint -s dist", 14 | "lint:eslint": "eslint src --ext .ts", 15 | "lint:typescript": "tsc --noEmit" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Rayquaza01/HistoryCleaner.git" 20 | }, 21 | "author": "Joe Jarvis", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Rayquaza01/HistoryCleaner/issues" 25 | }, 26 | "homepage": "https://github.com/Rayquaza01/HistoryCleaner#readme", 27 | "devDependencies": { 28 | "@types/webextension-polyfill": "^0.12.1", 29 | "@typescript-eslint/eslint-plugin": "^7.16.0", 30 | "@typescript-eslint/parser": "^7.16.0", 31 | "clean-webpack-plugin": "^4.0.0", 32 | "copy-webpack-plugin": "^12.0.2", 33 | "css-loader": "^7.1.2", 34 | "css-minimizer-webpack-plugin": "^7.0.0", 35 | "eslint": "^8.57.0", 36 | "html-webpack-plugin": "^5.6.3", 37 | "mini-css-extract-plugin": "^2.9.2", 38 | "style-loader": "^4.0.0", 39 | "terser-webpack-plugin": "^5.3.10", 40 | "ts-loader": "^9.5.1", 41 | "typescript": "^5.7.2", 42 | "web-ext": "^8.3.0", 43 | "webpack": "^5.97.1", 44 | "webpack-cli": "^5.1.4" 45 | }, 46 | "dependencies": { 47 | "webextension-polyfill": "^0.12.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DeleteHistory.ts: -------------------------------------------------------------------------------- 1 | import browser from "./we"; 2 | import { Options } from "./OptionsInterface"; 3 | 4 | /** 5 | * Deletes history older than specified days 6 | * * Takes no action if behavior is set to disable 7 | * * Deletes older than days if behavior is set to days 8 | * * Deletes all history if behavior is set to all 9 | * * Creates notification if notifications are enabled 10 | */ 11 | export async function deleteHistory(opts?: Options): Promise { 12 | const res = opts ?? new Options(await browser.storage.local.get()); 13 | if (res.behavior === "days") { 14 | const end = new Date(); 15 | end.setHours(0); 16 | end.setMinutes(0); 17 | end.setSeconds(0); 18 | end.setMilliseconds(0); 19 | end.setDate(end.getDate() - res.days); 20 | await browser.history.deleteRange({ 21 | startTime: 0, 22 | endTime: end.getTime() 23 | }); 24 | 25 | if (res.downloads) { 26 | await browser.downloads.erase({ endedBefore: end.toISOString() }); 27 | } 28 | 29 | const notificationBody: string = browser.i18n.getMessage( 30 | "historyDeletedNotificationBody", 31 | [ 32 | end.toLocaleString(), 33 | new Date().toLocaleString() 34 | ] 35 | ); 36 | console.log(notificationBody); 37 | if (res.notifications) { 38 | browser.notifications.create({ 39 | type: "basic", 40 | iconUrl: "icons/icon-96.png", 41 | title: browser.i18n.getMessage("historyDeletedNotification"), 42 | message: notificationBody 43 | }); 44 | } 45 | 46 | browser.storage.local.set({ lastRun: notificationBody }); 47 | } else if (res.behavior === "all") { 48 | const notificationBody = browser.i18n.getMessage("historyAllDeleted", [new Date().toLocaleString()]); 49 | 50 | await browser.history.deleteAll(); 51 | 52 | if (res.downloads) { 53 | await browser.downloads.erase({ endedBefore: new Date().toISOString() }); 54 | } 55 | 56 | console.log(notificationBody); 57 | if (res.notifications) { 58 | browser.notifications.create({ 59 | type: "basic", 60 | iconUrl: "icons/icon-96.png", 61 | title: browser.i18n.getMessage("historyDeletedNotification"), 62 | message: notificationBody 63 | }); 64 | } 65 | 66 | browser.storage.local.set({ lastRun: notificationBody }); 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/MessageInterface.ts: -------------------------------------------------------------------------------- 1 | /** Possible states for a message */ 2 | export enum MessageState { 3 | INVALID = -1, 4 | DELETE, 5 | SET_IDLE, 6 | SET_STARTUP, 7 | SET_TIMER, 8 | SET_ICON, 9 | } 10 | 11 | /** Shape of message */ 12 | export interface MessageInterface extends Record { 13 | state: MessageState; 14 | idleLength: number; 15 | timerInterval: number; 16 | icon: string; 17 | } 18 | 19 | /** Creates Message object */ 20 | export class Message implements MessageInterface { 21 | state: MessageState; 22 | idleLength: number; 23 | timerInterval: number; 24 | icon: string; 25 | 26 | [key: string]: unknown; 27 | 28 | constructor(msgObj?: Partial) { 29 | this.state = msgObj?.state ?? -1; 30 | this.idleLength = msgObj?.idleLength ?? -1; 31 | this.timerInterval = msgObj?.timerInterval ?? -1; 32 | this.icon = msgObj?.icon ?? "theme"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/OptionsInterface.ts: -------------------------------------------------------------------------------- 1 | import browser from "./we"; 2 | 3 | /** Shape of options object */ 4 | export interface OptionsInterface extends Record { 5 | behavior: "disable" | "days" | "all" | string; 6 | days: number; 7 | idleLength: number; 8 | timerInterval: number; 9 | deleteMode: "idle" | "startup" | "timer" | string; 10 | notifications: boolean; 11 | downloads: boolean; 12 | // filterHistory: boolean; 13 | // filterList: string[]; 14 | 15 | // statistics 16 | lastRun: string; 17 | 18 | icon: "theme" | "icon_circle" | "icon_square" | "icon_circle_gradient" | "icon_square_gradient" | string; 19 | } 20 | 21 | export interface FormElements extends HTMLFormControlsCollection { 22 | behavior: RadioNodeList; 23 | days: HTMLInputElement; 24 | idleLength: HTMLInputElement; 25 | timerInterval: HTMLInputElement; 26 | deleteMode: RadioNodeList; 27 | notifications: HTMLInputElement; 28 | // notificationsPermission: HTMLInputElement; 29 | downloads: HTMLInputElement; 30 | // downloadsPermission: HTMLInputElement; 31 | // filterHistory: HTMLInputElement; 32 | // filterList: HTMLTextAreaElement; 33 | 34 | // statistics 35 | lastRun: HTMLInputElement; 36 | deleteCount: HTMLInputElement; 37 | 38 | icon: HTMLSelectElement 39 | } 40 | 41 | /** Creates Options object */ 42 | export class Options implements OptionsInterface { 43 | behavior = "disable"; 44 | days = 0; 45 | idleLength = 60; 46 | timerInterval = 1440; 47 | deleteMode = "timer"; 48 | notifications = false; 49 | downloads = false; 50 | icon = "theme"; 51 | // filterHistory = false 52 | // filterList = ["example.com", "example.org"] 53 | 54 | lastRun = browser.i18n.getMessage("lastRunNever"); 55 | deleteCount = 0; 56 | 57 | [key: string]: unknown 58 | 59 | /** 60 | * Creates default options object, with overrides from optionsObj 61 | * @param optionsObj Initial options object, likely from storage 62 | */ 63 | constructor(optionsObj?: Record) { 64 | if (optionsObj === undefined) { 65 | return; 66 | } 67 | 68 | const manifest_version = browser.runtime.getManifest().manifest_version; 69 | 70 | if (typeof optionsObj.behavior === "string" && ["disable", "days", "all"].includes(optionsObj.behavior)) { 71 | this.behavior = optionsObj.behavior; 72 | } else { 73 | if (typeof optionsObj.days === "number" && optionsObj.days > 0) { 74 | this.behavior = "days"; 75 | } 76 | } 77 | 78 | if (typeof optionsObj.days === "number" && optionsObj.days >= 0) { 79 | this.days = optionsObj.days; 80 | } 81 | 82 | if (typeof optionsObj.idleLength === "number" && optionsObj.idleLength >= 15) { 83 | this.idleLength = optionsObj.idleLength; 84 | } 85 | 86 | if (typeof optionsObj.timerInterval === "number" && optionsObj.timerInterval >= 1) { 87 | this.timerInterval = optionsObj.timerInterval; 88 | } 89 | 90 | if (typeof optionsObj.deleteMode === "string" && ["idle", "startup", "timer"].includes(optionsObj.deleteMode)) { 91 | this.deleteMode = optionsObj.deleteMode; 92 | } 93 | 94 | // if set to idle on manifest v3, switch to timer 95 | if (manifest_version === 3 && this.deleteMode === "idle") { 96 | this.deleteMode = "timer"; 97 | } 98 | 99 | if (typeof optionsObj.notifications === "boolean") { 100 | this.notifications = optionsObj.notifications; 101 | } 102 | 103 | if (typeof optionsObj.downloads === "boolean") { 104 | this.downloads = optionsObj.downloads; 105 | } 106 | 107 | // if (typeof optionsObj.filterHistory === "boolean") { 108 | // this.filterHistory = optionsObj.filterHistory; 109 | // } 110 | 111 | // if (Array.isArray(optionsObj.filterList) && optionsObj.filterList.every(item => typeof item === "string")) { 112 | // this.filterList = optionsObj.filterList; 113 | // } 114 | 115 | if (typeof optionsObj.lastRun === "string") { 116 | this.lastRun = optionsObj.lastRun; 117 | } 118 | 119 | if (typeof optionsObj.icon === "string") { 120 | this.icon = optionsObj.icon; 121 | } 122 | 123 | if (manifest_version === 3 && this.icon === "theme") { 124 | this.icon = "icon_circle_gradient"; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/PermissionCheckbox.ts: -------------------------------------------------------------------------------- 1 | import browser from "./we"; 2 | import { Manifest } from "webextension-polyfill"; 3 | 4 | export async function PermissionCheckbox(permissions: Manifest.OptionalPermission[], e: Event, save: () => void) { 5 | const target = e.target as HTMLInputElement; 6 | 7 | // if the user checked the box, make sure permission is granted first 8 | if (target.checked) { 9 | // prevent default stops the form listener from saving until *after* permission is granted 10 | e.preventDefault(); 11 | 12 | const granted = await browser.permissions.request({ permissions }); 13 | target.checked = granted; 14 | 15 | // I don't really like this. 16 | // because we prevent default, the checkbox never triggers the save function 17 | // but if we don't prevent default, it saves the wrong value! 18 | // as a fix, we manually call the save function once we know if the permission was granted 19 | save(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Photon/Checkbox.css: -------------------------------------------------------------------------------- 1 | input.checkbox:focus { 2 | box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3) 3 | } 4 | 5 | input.checkbox:disabled + label { 6 | opacity: .4; 7 | } 8 | -------------------------------------------------------------------------------- /src/Photon/DefaultButton.css: -------------------------------------------------------------------------------- 1 | button.default { 2 | color: #0c0c0d; 3 | text-align: center; 4 | background-color: rgba(12, 12, 13, 0.1); 5 | border-radius: 2px; 6 | height: 32px; 7 | padding-left: 8px; 8 | padding-right: 8px; 9 | font-size: 13px; 10 | font-weight: 400; 11 | min-width: 132px; 12 | border: none; 13 | } 14 | 15 | button.default:hover { 16 | background-color: rgba(12, 12, 13, 0.2); 17 | } 18 | 19 | button.default:active { 20 | background-color: rgba(12, 12, 13, 0.3); 21 | } 22 | 23 | button.default:focus { 24 | box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); 25 | } 26 | -------------------------------------------------------------------------------- /src/Photon/Dropdown.css: -------------------------------------------------------------------------------- 1 | select { 2 | color: #0c0c0d; 3 | background-color: rgba(12, 12, 13, 0.1); 4 | border-radius: 2px; 5 | height: 32px; 6 | padding-left: 8px; 7 | padding-right: 8px; 8 | font-size: 13px; 9 | font-weight: 400; 10 | min-width: 132px; 11 | border: none; 12 | } 13 | 14 | select:hover { 15 | background-color: rgba(12, 12, 13, 0.2); 16 | } 17 | 18 | select:active { 19 | background-color: rgba(12, 12, 13, 0.3); 20 | } 21 | 22 | select:focus { 23 | box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); 24 | } 25 | -------------------------------------------------------------------------------- /src/Photon/Input.css: -------------------------------------------------------------------------------- 1 | input.text { 2 | border-radius: 2px; 3 | padding-left: 8px; 4 | padding-right: 8px; 5 | width: auto; 6 | height: 32px; 7 | border: 1px solid rgba(12, 12, 13, 0.2); 8 | } 9 | 10 | input.text:hover { 11 | border-color: rgba(12, 12, 13, 0.3); 12 | } 13 | 14 | input.text:focus { 15 | border-color: #0a84ff; 16 | box-shadow: 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); 17 | } 18 | 19 | input.text:invalid { 20 | border-color: #d70022; 21 | box-shadow: 0 0 0 1px #d70022, 0 0 0 4px rgba(251, 0, 34, 0.3); 22 | } 23 | -------------------------------------------------------------------------------- /src/ToggleButton.ts: -------------------------------------------------------------------------------- 1 | export enum ToggleButtonState { 2 | NO_PERMISSION, 3 | PERMISSION 4 | } 5 | 6 | /** 7 | * @class 8 | * Manages toggling an HTMLButton between two states 9 | */ 10 | export class ToggleButton { 11 | private element: HTMLButtonElement; 12 | private textOptions: string[]; 13 | private state: ToggleButtonState; 14 | 15 | /** 16 | * @param element - the HTMLButton element 17 | * @param textOptions - An array of text options for the button 18 | */ 19 | constructor(element: HTMLButtonElement, textOptions: string[]) { 20 | this.element = element; 21 | this.textOptions = textOptions; 22 | this.state = ToggleButtonState.NO_PERMISSION; 23 | this.element.innerText = this.textOptions[this.state]; 24 | } 25 | 26 | /** Get the state of the button */ 27 | getState(): ToggleButtonState { 28 | return this.state; 29 | } 30 | 31 | /** Set the state of the button */ 32 | setState(state: ToggleButtonState): void { 33 | this.state = state; 34 | this.element.innerText = this.textOptions[this.state]; 35 | } 36 | 37 | /** Get the button element */ 38 | getElement(): HTMLButtonElement { 39 | return this.element; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "History Cleaner", 4 | "description": "Name of extension" 5 | }, 6 | "extensionDescription": { 7 | "message": "Deletes history older than a specified amount of days.", 8 | "description": "Description of extension" 9 | }, 10 | "optionDaysText": { 11 | "message": "Number of Days to Keep History", 12 | "description": "" 13 | }, 14 | "optionDaysInfo": { 15 | "message": "Delete history older than midnight on the specified number of days ago. Will have no effect if behavior is set to disabled or delete all. (Defaults to 0)", 16 | "description": "" 17 | }, 18 | "optionIdleLengthText": { 19 | "message": "Idle Length", 20 | "description": "" 21 | }, 22 | "optionIdleLengthInfo": { 23 | "message": "Amount of time in seconds the browser should idle before triggering. (Defaults to 60, Minimum 15)", 24 | "description": "" 25 | }, 26 | "optionTimerIntervalText": { 27 | "message": "Timer Interval", 28 | "description": "" 29 | }, 30 | "optionTimerIntervalInfo": { 31 | "message": "Interval in minutes between triggering. (Defaults to 1440 minutes or 24 hours, Minimum 1)", 32 | "description": "" 33 | }, 34 | "optionDeleteModeText": { 35 | "message": "Trigger Mode", 36 | "description": "" 37 | }, 38 | "optionDeleteModeInfo": { 39 | "message": "Whether the extension triggers on idle, on browser startup, or at a set interval. (Defaults to idle)", 40 | "description": "" 41 | }, 42 | "optionDeleteModeInfoManifestv3": { 43 | "message": "Whether the extension triggers on on browser startup or at a set interval. (Defaults to timer)", 44 | "description": "" 45 | }, 46 | "optionDeleteModeIdle": { 47 | "message": "Idle", 48 | "description": "" 49 | }, 50 | "optionDeleteModeStartup": { 51 | "message": "Startup", 52 | "description": "" 53 | }, 54 | "optionDeleteModeAlarm": { 55 | "message": "Timer", 56 | "description": "" 57 | }, 58 | "optionNotificationsText": { 59 | "message": "Show Notifications", 60 | "description": "" 61 | }, 62 | "optionNotificationsInfo": { 63 | "message": "Request notifications permission using the below button, and then set the option to Show Notifications to enable notifications when history is deleted.", 64 | "description": "" 65 | }, 66 | "optionNotificationsInfoNew": { 67 | "message": "Whether a notification should be shown when history is deleted.", 68 | "description": "" 69 | }, 70 | "optionNotificationsCheckbox": { 71 | "message": "Show Notifications on Delete", 72 | "description": "" 73 | }, 74 | "optionDownloadsText": { 75 | "message": "Downloads", 76 | "description": "" 77 | }, 78 | "optionDownloadsInfo": { 79 | "message": "Clear download history", 80 | "description": "" 81 | }, 82 | "optionDownloadsCheckbox": { 83 | "message": "Clear Download History", 84 | "description": "" 85 | }, 86 | "optionIconText": { 87 | "message": "Icon", 88 | "description": "" 89 | }, 90 | "optionIconInfo": { 91 | "message": "Choose the extension's icon (Defaults to theme icon in Firefox. Defaults to circle gradient icon in Chrome. Theme icons are not supported in Chrome)", 92 | "description": "" 93 | }, 94 | "optionIcon_theme": { 95 | "message": "Theme Icon", 96 | "description": "" 97 | }, 98 | "optionIcon_circle": { 99 | "message": "Circle", 100 | "description": "" 101 | }, 102 | "optionIcon_square": { 103 | "message": "Square", 104 | "description": "" 105 | }, 106 | "optionIcon_circle_gradient": { 107 | "message": "Circle Gradient", 108 | "description": "" 109 | }, 110 | "optionIcon_square_gradient": { 111 | "message": "Square Gradient", 112 | "description": "" 113 | }, 114 | "manualDelete": { 115 | "message": "Delete History Now", 116 | "description": "" 117 | }, 118 | "manualDeleteInfo": { 119 | "message": "Press the button below to delete history manually.", 120 | "description": "" 121 | }, 122 | "sync": { 123 | "message": "Sync", 124 | "description": "" 125 | }, 126 | "syncUp": { 127 | "message": "Upload Config to Sync", 128 | "description": "" 129 | }, 130 | "syncDown": { 131 | "message": "Download Config from Sync", 132 | "description": "" 133 | }, 134 | "exportConfig": { 135 | "message": "Export Config to File", 136 | "description": "" 137 | }, 138 | "importConfig": { 139 | "message": "Import Config from File", 140 | "description": "" 141 | }, 142 | "notificationRequest": { 143 | "message": "Request Notification Permission", 144 | "description": "" 145 | }, 146 | "notificationRevoke": { 147 | "message": "Revoke Notification Permission", 148 | "description": "" 149 | }, 150 | "notificationEnabled": { 151 | "message": "Notifications Enabled!", 152 | "description": "" 153 | }, 154 | "notificationEnabledBody": { 155 | "message": "Notifications will now appear when history is deleted", 156 | "description": "" 157 | }, 158 | "historyDeletedNotification": { 159 | "message": "History deleted", 160 | "description": "" 161 | }, 162 | "historyDeletedNotificationBody": { 163 | "message": "History from before $END$ was deleted on $NOW$.", 164 | "description": "", 165 | "placeholders": { 166 | "end": { 167 | "content": "$1", 168 | "example": "" 169 | }, 170 | "now": { 171 | "content": "$2", 172 | "example": "" 173 | } 174 | } 175 | }, 176 | "historyAllDeleted": { 177 | "message": "All history was deleted on $NOW$.", 178 | "description": "", 179 | "placeholders": { 180 | "now": { 181 | "content": "$1", 182 | "example": "" 183 | } 184 | } 185 | }, 186 | "optionBehaviorText": { 187 | "message": "Extension Behavior", 188 | "description": "" 189 | }, 190 | "optionBehaviorInfo": { 191 | "message": "Decides what history the extension should delete when it triggers.", 192 | "description": "" 193 | }, 194 | "behaviorDisable": { 195 | "message": "Disabled", 196 | "description": "" 197 | }, 198 | "behaviorDays": { 199 | "message": "Delete history older than a specified number of days", 200 | "description": "" 201 | }, 202 | "behaviorAll": { 203 | "message": "Delete all history", 204 | "description": "" 205 | }, 206 | "optionFilterHistoryText": { 207 | "message": "Filter History", 208 | "description": "" 209 | }, 210 | "optionFilterHistoryInfo": { 211 | "message": "Whether to prevent certain sites from being deleted. If enabled, the total number of history items deleted will be counted.", 212 | "description": "" 213 | }, 214 | "optionFilterHistoryCheckbox": { 215 | "message": "Exclude sites from deletion", 216 | "description": "" 217 | }, 218 | "optionFilterListText": { 219 | "message": "Filter List", 220 | "description": "" 221 | }, 222 | "optionFilterListInfo": { 223 | "message": "A list of sites that will not be deleted. Filter History must be enabled for this to take effect. Every line should contain a hostname that will be excluded from deletion.", 224 | "description": "" 225 | }, 226 | "statisticsSection": { 227 | "message": "Statistics", 228 | "description": "" 229 | }, 230 | "optionsSection": { 231 | "message": "Options", 232 | "description": "" 233 | }, 234 | "syncSection": { 235 | "message": "Sync", 236 | "description": "" 237 | }, 238 | "extrasSection": { 239 | "message": "Extras", 240 | "description": "" 241 | }, 242 | "statisticsLastRun": { 243 | "message": "History was last deleted on", 244 | "description": "" 245 | }, 246 | "statisticsNextRunDisable": { 247 | "message": "History will not be automatically cleared.", 248 | "description": "" 249 | }, 250 | "statisticsNextRunIdle": { 251 | "message": "History will be cleared on browser idle.", 252 | "description": "" 253 | }, 254 | "statisticsNextRunStartup": { 255 | "message": "History will be cleared on browser startup.", 256 | "description": "" 257 | }, 258 | "statisticsNextRunTimer": { 259 | "message": "History will be cleared on $TIMER$.", 260 | "description": "", 261 | "placeholders": { 262 | "timer": { 263 | "content": "$1", 264 | "example": "" 265 | } 266 | } 267 | }, 268 | "statisticsDeleteCount": { 269 | "message": "Total history entries deleted", 270 | "description": "" 271 | }, 272 | "statisticsDeleteCountWarning": { 273 | "message": "(History filtering must be enabled to track statistics)", 274 | "description": "" 275 | }, 276 | "lastRunNever": { 277 | "message": "History has not been cleared yet.", 278 | "description": "" 279 | }, 280 | "resetStatistics": { 281 | "message": "Delete Statistics", 282 | "description": "" 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/cr_background.ts: -------------------------------------------------------------------------------- 1 | import { Runtime, Alarms } from "webextension-polyfill"; 2 | import { deleteHistory } from "./DeleteHistory"; 3 | import { Options } from "./OptionsInterface"; 4 | import { MessageInterface, MessageState, Message } from "./MessageInterface"; 5 | import browser from "./we"; 6 | 7 | function IconLookup(icon: string): string { 8 | switch (icon) { 9 | case "theme": 10 | return "icons/icon-96.png"; 11 | case "icon_circle": 12 | return "icons/icon_red_circle.png"; 13 | case "icon_circle_gradient": 14 | return "icons/icon_red_circle_gradient.png"; 15 | case "icon_square": 16 | return "icons/icon_red_square.png"; 17 | case "icon_square_gradient": 18 | return "icons/icon_red_square_gradient.png"; 19 | default: 20 | return "icons/icon-96.png"; 21 | } 22 | } 23 | 24 | /** 25 | * Message listener 26 | * 27 | * Listens for messages from options page 28 | * * Deletes message when Manual delete button is pressed 29 | * * Sets delete mode to idle (updates detection interval and adds event listener) 30 | * * Sets delete mode to startup (removes event listener) 31 | * @param msg The message from the options page 32 | */ 33 | async function onMessage(msg: unknown): Promise { 34 | const message = new Message(msg as Partial); 35 | switch (message.state) { 36 | // manual delete button 37 | case MessageState.DELETE: 38 | deleteHistory(); 39 | break; 40 | // set idle mode 41 | case MessageState.SET_IDLE: 42 | // this should never run on chrome! 43 | break; 44 | // set startup mode 45 | case MessageState.SET_STARTUP: 46 | browser.alarms.clear("DeleteHistoryAlarm"); 47 | break; 48 | case MessageState.SET_TIMER: 49 | // delete old alarm 50 | browser.alarms.clear("DeleteHistoryAlarm"); 51 | 52 | // create a new one with new period 53 | browser.alarms.create("DeleteHistoryAlarm", { delayInMinutes: 1, periodInMinutes: message.timerInterval }); 54 | break; 55 | case MessageState.SET_ICON: 56 | browser.action.setIcon({ path: IconLookup(message.icon) }); 57 | break; 58 | } 59 | } 60 | 61 | async function onAlarm(alarm: Alarms.Alarm) { 62 | if (alarm.name === "DeleteHistoryAlarm") { 63 | deleteHistory(); 64 | } 65 | } 66 | 67 | /** 68 | * Runs at browser startup 69 | * * Sets event listener and detection length if delete mode set to idle 70 | * * Deletes history if set delete mode set to startup 71 | */ 72 | async function startup(): Promise { 73 | const res = new Options(await browser.storage.local.get()); 74 | switch (res.deleteMode) { 75 | case "idle": 76 | // should never run on chrome! 77 | break; 78 | // if delete mode is startup, delete history right now 79 | case "startup": 80 | deleteHistory(); 81 | break; 82 | // if delete mode is timer, set alarm to run at timer interval 83 | case "timer": 84 | browser.alarms.create("DeleteHistoryAlarm", { delayInMinutes: 1, periodInMinutes: res.timerInterval }); 85 | break; 86 | } 87 | 88 | browser.action.setIcon({ path: IconLookup(res.icon) }); 89 | } 90 | 91 | /** 92 | * Runs on extension install or update (not browser update) 93 | * * Initializes local and sync storage 94 | * * Opens options page on first install 95 | * @param installed Reason for install 96 | */ 97 | async function setup(installed: Runtime.OnInstalledDetailsType): Promise { 98 | if (installed.reason === "install" || installed.reason === "update") { 99 | // apply default values to storage 100 | const res = new Options(await browser.storage.local.get()); 101 | await browser.storage.local.set(res); 102 | 103 | // initialize sync object 104 | // doing this in manifest v3 causes the bg script to get unloaded 105 | // unsure of why 106 | // 107 | // const syncRes = new Options(await browser.storage.sync.get()); 108 | // await browser.storage.sync.set(syncRes); 109 | 110 | startup(); 111 | // open options page on first install 112 | if (installed.reason === "install") { 113 | browser.runtime.openOptionsPage(); 114 | } 115 | } 116 | } 117 | 118 | browser.alarms.onAlarm.addListener(onAlarm); 119 | browser.runtime.onMessage.addListener(onMessage); 120 | browser.runtime.onInstalled.addListener(setup); 121 | browser.runtime.onStartup.addListener(startup); 122 | -------------------------------------------------------------------------------- /src/cr_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extensionName__", 4 | "version": "1.7.0", 5 | "description": "__MSG_extensionDescription__", 6 | "icons": { 7 | "96": "icons/icon-96.png" 8 | }, 9 | "background": { 10 | "service_worker": "background.bundle.js", 11 | "type": "module" 12 | }, 13 | "permissions": [ 14 | "history", 15 | "storage", 16 | "alarms" 17 | ], 18 | "optional_permissions": [ 19 | "notifications", 20 | "downloads" 21 | ], 22 | "options_ui": { 23 | "page": "popup.html" 24 | }, 25 | "action": { 26 | "default_icon": "icons/icon-96.png", 27 | "default_popup": "popup.html", 28 | "theme_icons": [{ 29 | "light": "icons/icon-dark.png", 30 | "dark": "icons/icon-96.png", 31 | "size": 96 32 | }] 33 | }, 34 | "default_locale": "en_US" 35 | } 36 | -------------------------------------------------------------------------------- /src/cr_popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

11 | 12 | 13 |

14 |
15 | 16 |
17 |
18 | 19 | 20 |

21 | 22 | 23 |

24 | 25 | 29 |
30 | 31 |
32 | 33 |

34 |

35 |
36 | 43 | 50 | 57 | 58 | 63 |
64 | 65 |

66 |

67 | 68 | 69 |

70 |

71 |
72 | 79 | 86 |
87 | 88 | 89 | 90 |

91 |

92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |

109 |

110 | 114 | 115 |

116 |

117 | 125 |
126 | 127 |
128 | 129 |

130 | 134 | 138 |

139 |

140 | 141 | 145 | 146 | 150 |

151 |
152 | 153 |
154 | 155 |

156 | Available on Firefox 157 | Available on Chrome Web Store 158 |

159 |

160 | Source Code 161 |

162 |

163 | Issue Tracker 164 |

165 |

166 | The icons used in History Cleaner are from Pictogrammers, formerly Material Design Icons. (Pictogrammers Free License) 167 |

168 |
169 |
170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/cr_popup.ts: -------------------------------------------------------------------------------- 1 | import { Options, OptionsInterface, FormElements } from "./OptionsInterface"; 2 | import { Message, MessageState } from "./MessageInterface"; 3 | 4 | import browser from "./we"; 5 | 6 | import { i18n } from "./i18n"; 7 | 8 | import { PermissionCheckbox } from "./PermissionCheckbox"; 9 | 10 | import "./popup.css"; 11 | 12 | i18n(); 13 | 14 | const form = document.querySelector("form") as HTMLFormElement; 15 | const formElements = form.elements as FormElements; 16 | 17 | const lastRunVisible = document.querySelector("#last-run") as HTMLSpanElement; 18 | const nextRun = document.querySelector("#next-run") as HTMLSpanElement; 19 | 20 | // sync buttons 21 | const uploadButton = document.querySelector("#sync-up") as HTMLButtonElement; 22 | const downloadButton = document.querySelector("#sync-down") as HTMLButtonElement; 23 | 24 | const exportButton = document.querySelector("#export") as HTMLAnchorElement; 25 | const importButton = document.querySelector("#import") as HTMLButtonElement; 26 | const importFile = document.querySelector("#import-file") as HTMLInputElement; 27 | 28 | // manual delete button 29 | const manualDeleteButton = document.querySelector("#manual-delete") as HTMLButtonElement; 30 | 31 | /** 32 | * Resets the trigger mode 33 | * Used when importing from file or sync 34 | * @param opts - The imported options to apply 35 | */ 36 | function resetTriggerMode(opts: Options) { 37 | // typically the trigger mode is initialized on startup or right as it's changed in the form 38 | // but if you import, the form is not updated and startup is not ran 39 | // so we have to reset the trigger mode manually 40 | const msg = new Message(); 41 | 42 | switch (opts.deleteMode) { 43 | case "idle": 44 | msg.state = MessageState.SET_IDLE; 45 | msg.idleLength = opts.idleLength; 46 | break; 47 | case "startup": 48 | msg.state = MessageState.SET_STARTUP; 49 | break; 50 | case "timer": 51 | msg.state = MessageState.SET_TIMER; 52 | msg.timerInterval = opts.timerInterval; 53 | break; 54 | } 55 | 56 | browser.runtime.sendMessage(msg); 57 | } 58 | 59 | /** 60 | * Sends a message to the background script telling it to delete history 61 | */ 62 | function manualDelete(e: MouseEvent): void { 63 | e.preventDefault(); 64 | console.log("Here?"); 65 | 66 | const msg = new Message({ state: MessageState.DELETE }); 67 | browser.runtime.sendMessage(msg); 68 | } 69 | 70 | /** 71 | * Upload current local storage to sync storage 72 | */ 73 | async function upload(e: MouseEvent): Promise { 74 | e.preventDefault(); 75 | 76 | const res = new Options(await browser.storage.local.get()); 77 | await browser.storage.sync.set(res); 78 | // location.reload(); 79 | } 80 | 81 | /** 82 | * Download current sync storage to local storage 83 | * 84 | * Sets idle or startup based on the contents of the downloaded options 85 | */ 86 | async function download(e: MouseEvent): Promise { 87 | e.preventDefault(); 88 | 89 | const res = new Options(await browser.storage.sync.get()); 90 | 91 | resetTriggerMode(res); 92 | 93 | browser.storage.local.set(res); 94 | // location.reload(); 95 | } 96 | 97 | /** 98 | * Imports the config from a file. 99 | */ 100 | function importConfig() { 101 | const reader = new FileReader(); 102 | reader.addEventListener("load", () => { 103 | if (typeof reader.result === "string") { 104 | const importedConfig = new Options(JSON.parse(reader.result)); 105 | 106 | resetTriggerMode(importedConfig); 107 | 108 | browser.storage.local.set(importedConfig); 109 | } 110 | }); 111 | 112 | if (importFile.files !== null && importFile.files.length > 0) { 113 | reader.readAsText(importFile.files[0]); 114 | } 115 | } 116 | 117 | /** 118 | * Saves inputs on options page to storage 119 | * * Runs when input is changed by user 120 | * * If user input is not valid, does not save 121 | * * Set idle or startup based on input 122 | * @param e event object 123 | */ 124 | async function save(e?: Event): Promise { 125 | if (form.checkValidity()) { 126 | const opts: OptionsInterface = { 127 | behavior: formElements.behavior.value, 128 | days: parseInt(formElements.days.value), 129 | deleteMode: formElements.deleteMode.value, 130 | idleLength: parseInt(formElements.idleLength.value), 131 | timerInterval: parseInt(formElements.timerInterval.value), 132 | notifications: formElements.notifications.checked, 133 | downloads: formElements.downloads.checked, 134 | // filterHistory: formElements.filterHistory.checked, 135 | // filterList: formElements.filterList.value.split("\n"), 136 | 137 | lastRun: formElements.lastRun.value, 138 | 139 | icon: formElements.icon.value, 140 | }; 141 | 142 | if (opts.behavior === "disable") { 143 | manualDeleteButton.disabled = true; 144 | console.log("Disabled"); 145 | } else { 146 | manualDeleteButton.disabled = false; 147 | console.log("Enabled"); 148 | } 149 | 150 | if (e !== undefined) { 151 | const target = e.target as HTMLFieldSetElement; 152 | 153 | // if changing the setting will update idle / startup 154 | const msg = new Message(); 155 | if ((target.name === "idleLength" || target.name === "deleteMode") && opts.deleteMode === "idle") { 156 | msg.state = MessageState.SET_IDLE; 157 | msg.idleLength = opts.idleLength; 158 | browser.runtime.sendMessage(msg); 159 | } else if (target.name === "deleteMode" && opts.deleteMode === "startup") { 160 | msg.state = MessageState.SET_STARTUP; 161 | browser.runtime.sendMessage(msg); 162 | } else if ((target.name === "timerInterval" || target.name === "deleteMode") && opts.deleteMode === "timer") { 163 | msg.state = MessageState.SET_TIMER; 164 | msg.timerInterval = opts.timerInterval; 165 | browser.runtime.sendMessage(msg); 166 | } 167 | 168 | const isNotificationPermissionGranted = await browser.permissions.contains({ permissions: ["notifications"] }); 169 | if (!isNotificationPermissionGranted) { 170 | opts.notifications = false; 171 | console.log("Notification not granted yet"); 172 | } 173 | 174 | // if notifications were enabled 175 | // if (target.name === "notifications" && opts.notifications) { 176 | // browser.notifications.create({ 177 | // type: "basic", 178 | // iconUrl: "icons/icon-96.png", 179 | // title: browser.i18n.getMessage("notificationEnabled"), 180 | // message: browser.i18n.getMessage("notificationEnabledBody") 181 | // }); 182 | // } 183 | } 184 | 185 | console.log("Saving", opts); 186 | 187 | 188 | // save options 189 | browser.storage.local.set(opts); 190 | 191 | if (e !== undefined && (e.target as HTMLInputElement).name === "notifications") { 192 | load(); 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * Runs on page load 199 | * Loads current options to inputs on page 200 | */ 201 | async function load(): Promise { 202 | const res = new Options(await browser.storage.local.get()); 203 | console.log("Loading", res); 204 | 205 | formElements.behavior.value = res.behavior.toString(); 206 | formElements.days.value = res.days.toString(); 207 | formElements.idleLength.value = res.idleLength.toString(); 208 | formElements.timerInterval.value = res.timerInterval.toString(); 209 | formElements.deleteMode.value = res.deleteMode; 210 | formElements.notifications.checked = res.notifications; 211 | // formElements.filterHistory.checked = res.filterHistory; 212 | // formElements.filterList.value = res.filterList.join("\n"); 213 | 214 | formElements.icon.value = res.icon; 215 | 216 | if (res.behavior === "disable") { 217 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunDisable"); 218 | } else { 219 | const alarm = await browser.alarms.get("DeleteHistoryAlarm"); 220 | if (res.deleteMode === "timer" && alarm !== undefined) { 221 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunTimer", [ new Date(alarm.scheduledTime).toLocaleString() ]); 222 | } 223 | 224 | if (res.deleteMode === "idle") { 225 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunIdle"); 226 | } 227 | 228 | if (res.deleteMode === "startup") { 229 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunStartup"); 230 | } 231 | } 232 | 233 | 234 | formElements.lastRun.value = res.lastRun; 235 | lastRunVisible.innerText = res.lastRun; 236 | 237 | if (res.behavior === "disable") { 238 | manualDeleteButton.disabled = true; 239 | } else { 240 | manualDeleteButton.disabled = false; 241 | } 242 | 243 | // allow config to be exported 244 | exportButton.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(res)); 245 | } 246 | 247 | document.addEventListener("DOMContentLoaded", load); 248 | // notificationRequestButton.getElement().addEventListener("click", togglePermission); 249 | form.addEventListener("input", save); 250 | manualDeleteButton.addEventListener("click", manualDelete); 251 | 252 | formElements.notifications.addEventListener("change", (e) => PermissionCheckbox(["notifications"], e, save)); 253 | formElements.downloads.addEventListener("change", (e) => PermissionCheckbox(["downloads"], e, save)); 254 | 255 | uploadButton.addEventListener("click", upload); 256 | downloadButton.addEventListener("click", download); 257 | 258 | importButton.addEventListener("click", () => importFile.click()); 259 | importFile.addEventListener("change", importConfig); 260 | 261 | browser.storage.onChanged.addListener(load); 262 | -------------------------------------------------------------------------------- /src/ff_background.ts: -------------------------------------------------------------------------------- 1 | import { Idle, Runtime, Alarms } from "webextension-polyfill"; 2 | import { deleteHistory } from "./DeleteHistory"; 3 | import { Options } from "./OptionsInterface"; 4 | import { MessageInterface, MessageState, Message } from "./MessageInterface"; 5 | import browser from "./we"; 6 | 7 | function IconLookup(icon: string): string { 8 | switch (icon) { 9 | case "theme": 10 | return "icons/icon.svg"; 11 | case "icon_circle": 12 | return "icons/icon_red_circle.png"; 13 | case "icon_circle_gradient": 14 | return "icons/icon_red_circle_gradient.png"; 15 | case "icon_square": 16 | return "icons/icon_red_square.png"; 17 | case "icon_square_gradient": 18 | return "icons/icon_red_square_gradient.png"; 19 | default: 20 | return "icons/icon.svg"; 21 | } 22 | } 23 | 24 | /** 25 | * Message listener 26 | * 27 | * Listens for messages from options page 28 | * * Deletes message when Manual delete button is pressed 29 | * * Sets delete mode to idle (updates detection interval and adds event listener) 30 | * * Sets delete mode to startup (removes event listener) 31 | * @param msg The message from the options page 32 | */ 33 | async function onMessage(msg: unknown): Promise { 34 | const message = new Message(msg as Partial); 35 | 36 | switch (message.state) { 37 | // manual delete button 38 | case MessageState.DELETE: 39 | deleteHistory(); 40 | break; 41 | // set idle mode 42 | case MessageState.SET_IDLE: 43 | // remove idle listener if one exists 44 | if (browser.idle.onStateChanged.hasListener(idleListener)) { 45 | browser.idle.onStateChanged.removeListener(idleListener); 46 | } 47 | // set idle length 48 | browser.idle.setDetectionInterval(message.idleLength); 49 | // add idle listener 50 | browser.idle.onStateChanged.addListener(idleListener); 51 | 52 | browser.alarms.clear("DeleteHistoryAlarm"); 53 | break; 54 | // set startup mode 55 | case MessageState.SET_STARTUP: 56 | // remove idle listener 57 | if (browser.idle.onStateChanged.hasListener(idleListener)) { 58 | browser.idle.onStateChanged.removeListener(idleListener); 59 | } 60 | 61 | browser.alarms.clear("DeleteHistoryAlarm"); 62 | break; 63 | case MessageState.SET_TIMER: 64 | if (browser.idle.onStateChanged.hasListener(idleListener)) { 65 | browser.idle.onStateChanged.removeListener(idleListener); 66 | } 67 | 68 | // delete old alarm 69 | browser.alarms.clear("DeleteHistoryAlarm"); 70 | 71 | // create a new one with new period 72 | browser.alarms.create("DeleteHistoryAlarm", { delayInMinutes: 1, periodInMinutes: message.timerInterval }); 73 | break; 74 | case MessageState.SET_ICON: 75 | browser.browserAction.setIcon({ path: IconLookup(message.icon) }); 76 | break; 77 | 78 | } 79 | } 80 | 81 | async function onAlarm(alarm: Alarms.Alarm) { 82 | if (alarm.name === "DeleteHistoryAlarm") { 83 | deleteHistory(); 84 | } 85 | } 86 | 87 | /** 88 | * Attached to idle onStateChanged listener 89 | * 90 | * Deletes history if new state is "idle" 91 | * @param state New state on idle change 92 | */ 93 | function idleListener(state: Idle.IdleState): void { 94 | // delete history if state is idle 95 | if (state === "idle") { 96 | deleteHistory(); 97 | } 98 | } 99 | 100 | /** 101 | * Runs at browser startup 102 | * * Sets event listener and detection length if delete mode set to idle 103 | * * Deletes history if set delete mode set to startup 104 | */ 105 | async function startup(): Promise { 106 | const res = new Options(await browser.storage.local.get()); 107 | 108 | switch (res.deleteMode) { 109 | // if delete mode is idle, set interval and add listener 110 | case "idle": 111 | browser.idle.setDetectionInterval(res.idleLength); 112 | browser.idle.onStateChanged.addListener(idleListener); 113 | break; 114 | // if delete mode is startup, delete history right now 115 | case "startup": 116 | deleteHistory(); 117 | break; 118 | // if delete mode is timer, set alarm to run at timer interval 119 | case "timer": 120 | browser.alarms.create("DeleteHistoryAlarm", { delayInMinutes: 1, periodInMinutes: res.timerInterval }); 121 | break; 122 | } 123 | 124 | browser.browserAction.setIcon({ path: IconLookup(res.icon) }); 125 | } 126 | 127 | /** 128 | * Runs on extension install or update (not browser update) 129 | * * Initializes local and sync storage 130 | * * Opens options page on first install 131 | * @param installed Reason for install 132 | */ 133 | async function setup(installed: Runtime.OnInstalledDetailsType): Promise { 134 | if (installed.reason === "install" || installed.reason === "update") { 135 | // apply default values to storage 136 | const res = new Options(await browser.storage.local.get()); 137 | await browser.storage.local.set(res); 138 | 139 | // initialize sync object 140 | const syncRes = new Options(await browser.storage.sync.get()); 141 | await browser.storage.sync.set(syncRes); 142 | 143 | startup(); 144 | // open options page on first install 145 | if (installed.reason === "install") { 146 | browser.runtime.openOptionsPage(); 147 | } 148 | } 149 | } 150 | 151 | browser.alarms.onAlarm.addListener(onAlarm); 152 | browser.runtime.onMessage.addListener(onMessage); 153 | browser.runtime.onInstalled.addListener(setup); 154 | browser.runtime.onStartup.addListener(startup); 155 | -------------------------------------------------------------------------------- /src/ff_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "1.7.0", 5 | "description": "__MSG_extensionDescription__", 6 | "browser_specific_settings": { 7 | "gecko": { 8 | "id": "{a138007c-5ff6-4d10-83d9-0afaf0efbe5e}" 9 | } 10 | }, 11 | "icons": { 12 | "96": "icons/icon.svg" 13 | }, 14 | "background": { 15 | "scripts": ["background.bundle.js"] 16 | }, 17 | "permissions": [ 18 | "history", 19 | "storage", 20 | "idle", 21 | "alarms" 22 | ], 23 | "optional_permissions": [ 24 | "notifications", 25 | "downloads" 26 | ], 27 | "options_ui": { 28 | "page": "popup.html", 29 | "browser_style": true 30 | }, 31 | "browser_action": { 32 | "default_icon": "icons/icon.svg", 33 | "default_popup": "popup.html" 34 | }, 35 | "default_locale": "en_US" 36 | } 37 | -------------------------------------------------------------------------------- /src/ff_popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

11 | 12 | 13 |

14 |
15 | 16 |
17 |
18 | 19 | 20 |

21 | 22 | 23 |

24 | 25 | 29 |
30 | 31 |
32 | 33 |

34 |

35 |
36 | 43 | 50 | 57 | 58 | 63 |
64 | 65 |

66 |

67 | 68 | 69 |

70 |

71 |
72 | 79 | 86 | 93 |
94 | 95 | 96 |

97 |

98 | 101 | 102 |

103 |

104 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |

120 |

121 | 126 | 127 |

128 |

129 | 138 |
139 | 140 |
141 | 142 |

143 | 147 | 151 |

152 |

153 | 154 | 158 | 159 | 163 |

164 |
165 | 166 |
167 | 168 |

169 | Available on Firefox 170 | Available on Chrome Web Store 171 |

172 |

173 | Source Code 174 |

175 |

176 | Issue Tracker 177 |

178 |

179 | The icons used in History Cleaner are from Pictogrammers (Pictogrammers Free License) 180 |

181 | 182 |

183 | Support me on Ko-fi 184 |

185 |
186 |
187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/ff_popup.ts: -------------------------------------------------------------------------------- 1 | import { Options, OptionsInterface, FormElements } from "./OptionsInterface"; 2 | import { Message, MessageState } from "./MessageInterface"; 3 | 4 | import browser from "./we"; 5 | 6 | import { PermissionCheckbox } from "./PermissionCheckbox"; 7 | 8 | import { i18n } from "./i18n"; 9 | 10 | import "./popup.css"; 11 | 12 | i18n(); 13 | 14 | const form = document.querySelector("form") as HTMLFormElement; 15 | const formElements = form.elements as FormElements; 16 | 17 | const lastRunVisible = document.querySelector("#last-run") as HTMLSpanElement; 18 | const nextRun = document.querySelector("#next-run") as HTMLSpanElement; 19 | 20 | // sync buttons 21 | const uploadButton = document.querySelector("#sync-up") as HTMLButtonElement; 22 | const downloadButton = document.querySelector("#sync-down") as HTMLButtonElement; 23 | 24 | const exportButton = document.querySelector("#export") as HTMLAnchorElement; 25 | const importButton = document.querySelector("#import") as HTMLButtonElement; 26 | const importFile = document.querySelector("#import-file") as HTMLInputElement; 27 | 28 | // manual delete button 29 | const manualDeleteButton = document.querySelector("#manual-delete") as HTMLButtonElement; 30 | 31 | /** 32 | * Resets the trigger mode 33 | * Used when importing from file or sync 34 | * @param opts - The imported options to apply 35 | */ 36 | function resetTriggerMode(opts: Options) { 37 | // typically the trigger mode is initialized on startup or right as it's changed in the form 38 | // but if you import, the form is not updated and startup is not ran 39 | // so we have to reset the trigger mode manually 40 | const msg = new Message(); 41 | 42 | switch (opts.deleteMode) { 43 | case "idle": 44 | msg.state = MessageState.SET_IDLE; 45 | msg.idleLength = opts.idleLength; 46 | break; 47 | case "startup": 48 | msg.state = MessageState.SET_STARTUP; 49 | break; 50 | case "timer": 51 | msg.state = MessageState.SET_TIMER; 52 | msg.timerInterval = opts.timerInterval; 53 | break; 54 | } 55 | 56 | browser.runtime.sendMessage(msg); 57 | } 58 | 59 | /** 60 | * Sends a message to the background script telling it to delete history 61 | */ 62 | function manualDelete(e: MouseEvent): void { 63 | e.preventDefault(); 64 | 65 | const msg = new Message({ state: MessageState.DELETE }); 66 | browser.runtime.sendMessage(msg); 67 | } 68 | 69 | /** 70 | * Upload current local storage to sync storage 71 | */ 72 | async function upload(e: MouseEvent): Promise { 73 | e.preventDefault(); 74 | 75 | const res = new Options(await browser.storage.local.get()); 76 | await browser.storage.sync.set(res); 77 | // location.reload(); 78 | } 79 | 80 | /** 81 | * Download current sync storage to local storage 82 | * 83 | * Sets idle or startup based on the contents of the downloaded options 84 | */ 85 | async function download(e: MouseEvent): Promise { 86 | e.preventDefault(); 87 | 88 | const res = new Options(await browser.storage.sync.get()); 89 | 90 | resetTriggerMode(res); 91 | 92 | browser.storage.local.set(res); 93 | // location.reload(); 94 | } 95 | 96 | /** 97 | * Imports the config from a file. 98 | */ 99 | function importConfig() { 100 | const reader = new FileReader(); 101 | reader.addEventListener("load", () => { 102 | if (typeof reader.result === "string") { 103 | const importedConfig = new Options(JSON.parse(reader.result)); 104 | 105 | resetTriggerMode(importedConfig); 106 | 107 | browser.storage.local.set(importedConfig); 108 | } 109 | }); 110 | 111 | if (importFile.files !== null && importFile.files.length > 0) { 112 | reader.readAsText(importFile.files[0]); 113 | } 114 | } 115 | 116 | /** 117 | * Saves inputs on options page to storage 118 | * * Runs when input is changed by user 119 | * * If user input is not valid, does not save 120 | * * Set idle or startup based on input 121 | * @param e event object 122 | */ 123 | async function save(e?: Event): Promise { 124 | if (form.checkValidity()) { 125 | const opts: OptionsInterface = { 126 | behavior: formElements.behavior.value, 127 | days: parseInt(formElements.days.value), 128 | deleteMode: formElements.deleteMode.value, 129 | idleLength: parseInt(formElements.idleLength.value), 130 | timerInterval: parseInt(formElements.timerInterval.value), 131 | notifications: formElements.notifications.checked, 132 | downloads: formElements.downloads.checked, 133 | // filterHistory: formElements.filterHistory.checked, 134 | // filterList: formElements.filterList.value.split("\n"), 135 | 136 | lastRun: formElements.lastRun.value, 137 | 138 | icon: formElements.icon.value, 139 | }; 140 | 141 | if (opts.behavior === "disable") { 142 | manualDeleteButton.disabled = true; 143 | console.log("Disabled"); 144 | } else { 145 | manualDeleteButton.disabled = false; 146 | console.log("Enabled"); 147 | } 148 | 149 | if (e !== undefined) { 150 | const target = e.target as HTMLFieldSetElement; 151 | 152 | // if changing the setting will update idle / startup 153 | const msg = new Message(); 154 | if ((target.name === "idleLength" || target.name === "deleteMode") && opts.deleteMode === "idle") { 155 | msg.state = MessageState.SET_IDLE; 156 | msg.idleLength = opts.idleLength; 157 | browser.runtime.sendMessage(msg); 158 | } else if (target.name === "deleteMode" && opts.deleteMode === "startup") { 159 | msg.state = MessageState.SET_STARTUP; 160 | browser.runtime.sendMessage(msg); 161 | } else if ((target.name === "timerInterval" || target.name === "deleteMode") && opts.deleteMode === "timer") { 162 | msg.state = MessageState.SET_TIMER; 163 | msg.timerInterval = opts.timerInterval; 164 | browser.runtime.sendMessage(msg); 165 | } else if (target.name === "icon") { 166 | msg.state = MessageState.SET_ICON; 167 | msg.icon = opts.icon; 168 | browser.runtime.sendMessage(msg); 169 | } 170 | 171 | // const isNotificationPermissionGranted = await browser.permissions.contains({ permissions: ["notifications"] }); 172 | // if (!isNotificationPermissionGranted) { 173 | // opts.notifications = false; 174 | // } 175 | 176 | // const isDownloadPermissionGranted = await browser.permissions.contains({ permissions: ["downloads"] }); 177 | // if (!isDownloadPermissionGranted) { 178 | // opts.downloads = false; 179 | // } 180 | 181 | // if notifications were enabled 182 | // if (isNotificationPermissionGranted && target.name === "notifications" && opts.notifications) { 183 | // browser.notifications.create({ 184 | // type: "basic", 185 | // iconUrl: "icons/icon-96.png", 186 | // title: browser.i18n.getMessage("notificationEnabled"), 187 | // message: browser.i18n.getMessage("notificationEnabledBody") 188 | // }); 189 | // } 190 | } 191 | 192 | console.log("Saving", opts); 193 | 194 | // save options 195 | browser.storage.local.set(opts); 196 | } 197 | } 198 | 199 | /** 200 | * Runs on page load 201 | * Loads current options to inputs on page 202 | */ 203 | async function load(): Promise { 204 | const res = new Options(await browser.storage.local.get()); 205 | console.log("Loading", res); 206 | 207 | formElements.behavior.value = res.behavior.toString(); 208 | formElements.days.value = res.days.toString(); 209 | formElements.idleLength.value = res.idleLength.toString(); 210 | formElements.timerInterval.value = res.timerInterval.toString(); 211 | formElements.deleteMode.value = res.deleteMode; 212 | // formElements.filterHistory.checked = res.filterHistory; 213 | // formElements.filterList.value = res.filterList.join("\n"); 214 | 215 | formElements.icon.value = res.icon; 216 | 217 | if (await browser.permissions.contains({ permissions: ["notifications"] })) { 218 | formElements.notifications.checked = res.notifications; 219 | } else { 220 | formElements.notifications.checked = false; 221 | } 222 | 223 | if (await browser.permissions.contains({ permissions: ["downloads"] })) { 224 | formElements.downloads.checked = res.downloads; 225 | } else { 226 | formElements.downloads.checked = false; 227 | } 228 | 229 | if (res.behavior === "disable") { 230 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunDisable"); 231 | } else { 232 | const alarm = await browser.alarms.get("DeleteHistoryAlarm"); 233 | if (res.deleteMode === "timer" && alarm !== undefined) { 234 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunTimer", [ new Date(alarm.scheduledTime).toLocaleString() ]); 235 | } 236 | 237 | if (res.deleteMode === "idle") { 238 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunIdle"); 239 | } 240 | 241 | if (res.deleteMode === "startup") { 242 | nextRun.innerText = browser.i18n.getMessage("statisticsNextRunStartup"); 243 | } 244 | } 245 | 246 | 247 | formElements.lastRun.value = res.lastRun; 248 | lastRunVisible.innerText = res.lastRun; 249 | 250 | if (res.behavior === "disable") { 251 | manualDeleteButton.disabled = true; 252 | } else { 253 | manualDeleteButton.disabled = false; 254 | } 255 | 256 | // allow config to be exported 257 | exportButton.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(res)); 258 | } 259 | 260 | document.addEventListener("DOMContentLoaded", load); 261 | // notificationRequestButton.getElement().addEventListener("click", togglePermission); 262 | form.addEventListener("input", save); 263 | manualDeleteButton.addEventListener("click", manualDelete); 264 | 265 | formElements.notifications.addEventListener("change", (e) => PermissionCheckbox(["notifications"], e, save)); 266 | formElements.downloads.addEventListener("change", (e) => PermissionCheckbox(["downloads"], e, save)); 267 | 268 | uploadButton.addEventListener("click", upload); 269 | downloadButton.addEventListener("click", download); 270 | 271 | importButton.addEventListener("click", () => importFile.click()); 272 | importFile.addEventListener("change", importConfig); 273 | 274 | browser.storage.onChanged.addListener(load); 275 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import browser from "./we"; 2 | 3 | /** 4 | * Loads i18n text to page 5 | * 6 | * Affects innerText of elements with class i18n 7 | * 8 | * Elements must have dataset.i18n present with the id of the i18n string 9 | */ 10 | export function i18n(): void { 11 | ([...document.getElementsByClassName("i18n")] as HTMLElement[]) 12 | .forEach(item => { 13 | if (typeof item.dataset.i18n === "string") { 14 | item.innerText = browser.i18n.getMessage(item.dataset.i18n); 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/alarm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/amo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/amo.png -------------------------------------------------------------------------------- /src/icons/cloud-download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/cloud-upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/cws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/cws.png -------------------------------------------------------------------------------- /src/icons/delete-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/delete-forever.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/delete-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/file-download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/file-upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/history.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon-48.png -------------------------------------------------------------------------------- /src/icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon-96.png -------------------------------------------------------------------------------- /src/icons/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon-dark.png -------------------------------------------------------------------------------- /src/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/icon_red_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon_red_circle.png -------------------------------------------------------------------------------- /src/icons/icon_red_circle_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon_red_circle_gradient.png -------------------------------------------------------------------------------- /src/icons/icon_red_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon_red_square.png -------------------------------------------------------------------------------- /src/icons/icon_red_square_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/icon_red_square_gradient.png -------------------------------------------------------------------------------- /src/icons/kofi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rayquaza01/HistoryCleaner/94dc180f682eda85a03704d19c1833067c40b504/src/icons/kofi.png -------------------------------------------------------------------------------- /src/icons/power.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/timer-sand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options.css: -------------------------------------------------------------------------------- 1 | @import url("./Photon/DefaultButton.css"); 2 | @import url("./Photon/Input.css"); 3 | @import url("./Photon/Dropdown.css"); 4 | @import url("./Photon/Checkbox.css"); 5 | 6 | html, body { 7 | font-family: sans-serif; 8 | } 9 | 10 | h2 { 11 | font-size: 17px; 12 | font-weight: 500; 13 | } 14 | 15 | .vertcenter { 16 | display: flex; 17 | align-items: center; 18 | } 19 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

26 |

27 |
28 | 29 |

30 |

31 | 32 | 33 |

34 |

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |

45 |

46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 |

54 |

55 | 56 | 57 |

58 | 59 | 60 | 61 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | // import { ToggleButton, ToggleButtonState } from "./ToggleButton"; 3 | import { Message, MessageState } from "./MessageInterface"; 4 | import { i18n } from "./i18n"; 5 | import { Options, OptionsInterface, FormElements } from "./OptionsInterface"; 6 | 7 | require("./options.css"); 8 | 9 | // parent to input elements 10 | const form = document.querySelector("#box") as HTMLFormElement; 11 | const formElements = form.elements as FormElements; 12 | 13 | // sync buttons 14 | const uploadButton = document.querySelector("#syncUp") as HTMLButtonElement; 15 | const downloadButton = document.querySelector("#syncDown") as HTMLButtonElement; 16 | 17 | // permission toggle button 18 | // const notificationRequestButton: ToggleButton = new ToggleButton( 19 | // document.querySelector("#notification-permission-request") as HTMLButtonElement, 20 | // [browser.i18n.getMessage("notificationRequest"), browser.i18n.getMessage("notificationRevoke")] 21 | // ); 22 | 23 | // manual delete button 24 | const manualDeleteButton = document.querySelector("#manual-delete") as HTMLButtonElement; 25 | 26 | /** 27 | * Sends a message to the background script telling it to delete history 28 | */ 29 | function manualDelete(): void { 30 | const msg = new Message({ state: MessageState.DELETE }); 31 | browser.runtime.sendMessage(msg); 32 | } 33 | 34 | /** 35 | * Toggle notifications permission 36 | * 37 | * Activates when the notification request button is pressed. 38 | * Requests or revokes notification permission based on the state of the button. 39 | * 40 | * Updates the button state afterwards 41 | */ 42 | // function togglePermission(): void { 43 | // // if permission is not currently granted 44 | // if (notificationRequestButton.getState() === ToggleButtonState.NO_PERMISSION) { 45 | // // attempt to get permission 46 | // browser.permissions.request({ permissions: ["notifications"] }) 47 | // .then((request: boolean) => { 48 | // // if user gives permission 49 | // // switch button state, enable option, send demo notification 50 | // if (request) { 51 | // notificationRequestButton.setState(ToggleButtonState.PERMISSION); 52 | // formElements.notifications.disabled = false; 53 | // } 54 | // // otherwise, keep button state same, turn off notifications, disable option 55 | // else { 56 | // notificationRequestButton.setState(ToggleButtonState.NO_PERMISSION); 57 | // formElements.notifications.checked = false; 58 | // formElements.notifications.disabled = true; 59 | // browser.storage.local.set({ notifications: false }); 60 | // } 61 | // }); 62 | // } 63 | // // if permission currently granted 64 | // // revoke permission, switch button state, disable notifications, and disable option 65 | // else if (notificationRequestButton.getState() === ToggleButtonState.PERMISSION) { 66 | // browser.permissions.remove({ permissions: ["notifications"] }); 67 | // notificationRequestButton.setState(ToggleButtonState.NO_PERMISSION); 68 | // formElements.notifications.checked = false; 69 | // formElements.notifications.disabled = true; 70 | // browser.storage.local.set({ notifications: false }); 71 | // } 72 | // } 73 | 74 | /** 75 | * Upload current local storage to sync storage 76 | */ 77 | async function upload(): Promise { 78 | const res = new Options(await browser.storage.local.get()); 79 | await browser.storage.sync.set(res); 80 | location.reload(); 81 | } 82 | 83 | /** 84 | * Download current sync storage to local storage 85 | * 86 | * Sets idle or startup based on the contents of the downloaded options 87 | */ 88 | async function download(): Promise { 89 | const res = new Options(await browser.storage.sync.get()); 90 | 91 | // set delete mode from sync get 92 | const msg = new Message(); 93 | if (res.deleteMode === "idle") { 94 | msg.state = MessageState.SET_IDLE; 95 | msg.idleLength = res.idleLength; 96 | browser.runtime.sendMessage(msg); 97 | } else if (res.deleteMode === "startup") { 98 | msg.state = MessageState.SET_STARTUP; 99 | browser.runtime.sendMessage(msg); 100 | } 101 | 102 | // disable notifications if permission not allowed 103 | // if (notificationRequestButton.getState() === ToggleButtonState.NO_PERMISSION) { 104 | // res.notifications = false; 105 | // } 106 | 107 | await browser.storage.local.set(res); 108 | location.reload(); 109 | } 110 | 111 | /** 112 | * Saves inputs on options page to storage 113 | * * Runs when input is changed by user 114 | * * If user input is not valid, does not save 115 | * * Set idle or startup based on input 116 | * @param e event object 117 | */ 118 | function save(e: Event): void { 119 | const target = e.target as HTMLFieldSetElement; 120 | 121 | if (form.checkValidity()) { 122 | const opts: OptionsInterface = { 123 | behavior: formElements.behavior.value, 124 | days: parseInt(formElements.days.value), 125 | deleteMode: formElements.deleteMode.value, 126 | idleLength: parseInt(formElements.idleLength.value), 127 | notifications: formElements.notifications.checked, 128 | lastRun: parseInt(formElements.lastRun.value) 129 | }; 130 | 131 | // if changing the setting will update idle / startup 132 | const msg = new Message(); 133 | if ((target.name === "idleLength" || target.name === "deleteMode") && opts.deleteMode === "idle") { 134 | msg.state = MessageState.SET_IDLE; 135 | msg.idleLength = opts.idleLength; 136 | browser.runtime.sendMessage(msg); 137 | } else if (target.name === "deleteMode" && opts.deleteMode === "startup") { 138 | msg.state = MessageState.SET_STARTUP; 139 | browser.runtime.sendMessage(msg); 140 | } 141 | 142 | // if notifications were enabled 143 | if (target.name === "notifications" && opts.notifications) { 144 | browser.notifications.create({ 145 | type: "basic", 146 | iconUrl: "icons/icon-96.png", 147 | title: browser.i18n.getMessage("notificationEnabled"), 148 | message: browser.i18n.getMessage("notificationEnabledBody") 149 | }); 150 | } 151 | 152 | // save options 153 | browser.storage.local.set(opts); 154 | } 155 | } 156 | 157 | /** 158 | * Runs on page load 159 | * * Adds i18n text to the page 160 | * * Loads current options to inputs on page 161 | */ 162 | async function load(): Promise { 163 | i18n(); 164 | 165 | const res = new Options(await browser.storage.local.get()); 166 | formElements.behavior.value = res.behavior.toString(); 167 | formElements.days.value = res.days.toString(); 168 | formElements.idleLength.value = res.idleLength.toString(); 169 | formElements.deleteMode.value = res.deleteMode; 170 | formElements.notifications.checked = res.notifications; 171 | formElements.lastRun.value = res.lastRun.toString(); 172 | 173 | // check permissions 174 | // const permissions = await browser.permissions.getAll(); 175 | // // if notification permission 176 | // // enable notification option, set button to revoke 177 | // if (Array.isArray(permissions.permissions) && permissions.permissions.includes("notifications")) { 178 | // formElements.notifications.disabled = false; 179 | // notificationRequestButton.setState(ToggleButtonState.PERMISSION); 180 | // } 181 | // // otherise disable option, set button to enable 182 | // else { 183 | // formElements.notifications.disabled = true; 184 | // notificationRequestButton.setState(ToggleButtonState.NO_PERMISSION); 185 | // } 186 | } 187 | 188 | document.addEventListener("DOMContentLoaded", load); 189 | // notificationRequestButton.getElement().addEventListener("click", togglePermission); 190 | form.addEventListener("input", save); 191 | manualDeleteButton.addEventListener("click", manualDelete); 192 | 193 | uploadButton.addEventListener("click", upload); 194 | downloadButton.addEventListener("click", download); 195 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #f6f5f4; 3 | --color: #000000; 4 | 5 | --link-color: #1a5fb4; 6 | 7 | --button-color: #ffffff; 8 | 9 | --hover: #deddda; 10 | --pressed: #c0bfbc; 11 | 12 | --disabled-text: #77767b; 13 | 14 | --accent-bg: #a51d2d; 15 | --accent-color: #ffffff; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | :root { 20 | --bg-color: #241f31; 21 | --color: #ffffff; 22 | 23 | --link-color: #99c1f1; 24 | 25 | --button-color: #000000; 26 | 27 | --disabled-text: #77767b; 28 | 29 | --hover: #3d3846; 30 | --pressed: #5e5c64; 31 | } 32 | } 33 | 34 | html, body { 35 | margin: 0; 36 | 37 | background-color: var(--bg-color); 38 | color: var(--color); 39 | font-family: sans-serif; 40 | 41 | width: 600px; 42 | min-height: 300px; 43 | 44 | display: flex; 45 | flex-direction: column; 46 | align-items: center; 47 | } 48 | 49 | header { 50 | background-color: var(--accent-bg); 51 | color: var(--accent-color); 52 | margin: 0; 53 | width: 100%; 54 | 55 | margin-bottom: 10px; 56 | } 57 | 58 | svg path { 59 | fill: var(--color); 60 | } 61 | 62 | h1 { 63 | margin: 10px; 64 | padding: 0; 65 | font-size: x-large; 66 | 67 | display: inline-flex; 68 | flex-direction: row; 69 | align-items: center; 70 | } 71 | 72 | h1 svg { 73 | height: 32px; 74 | margin-right: 5px; 75 | } 76 | 77 | h1 svg path { 78 | fill: var(--accent-color); 79 | } 80 | 81 | h2 { 82 | font-size: larger; 83 | text-align: left; 84 | } 85 | 86 | form { 87 | display: flex; 88 | flex-direction: column; 89 | 90 | gap: 20px; 91 | 92 | margin: 10px 5px 10px 5px; 93 | 94 | align-items: center; 95 | 96 | width: 90%; 97 | } 98 | 99 | .radio-text { 100 | display: flex; 101 | flex-direction: row; 102 | 103 | align-items: center; 104 | text-align: center; 105 | 106 | height: 100%; 107 | 108 | padding: 5px; 109 | border-radius: 5px; 110 | border: 1px solid; 111 | } 112 | 113 | .radio-group-buttons { 114 | display: flex; 115 | flex-direction: column; 116 | 117 | gap: 10px; 118 | } 119 | 120 | label { 121 | cursor: pointer; 122 | } 123 | 124 | button svg, 125 | label.radio svg { 126 | width: 24px; 127 | height: 24px; 128 | 129 | margin-right: 5px; 130 | } 131 | 132 | label.radio { 133 | display: inline-flex; 134 | gap: 5px; 135 | /* flex-direction: row-reverse; */ 136 | } 137 | 138 | label.radio > input:checked + span { 139 | background-color: var(--accent-bg); 140 | color: white; 141 | } 142 | 143 | label.radio > span { 144 | flex-grow: 1; 145 | } 146 | 147 | button, 148 | label.radio > span { 149 | color: var(--color); 150 | background-color: var(--button-color); 151 | } 152 | 153 | button:hover, 154 | label.radio:hover > span { 155 | background-color: var(--hover); 156 | } 157 | 158 | button:active, 159 | label.radio:active > span { 160 | background-color: var(--pressed); 161 | } 162 | 163 | input[type="radio"]:disabled + .radio-text { 164 | color: var(--disabled-text); 165 | background-color: var(--bg-color); 166 | } 167 | 168 | input[type="radio"]:disabled + .radio-text > svg path { 169 | fill: var(--disabled-text); 170 | } 171 | 172 | button:disabled { 173 | color: var(--disabled-text); 174 | background-color: var(--bg-color); 175 | } 176 | 177 | button:disabled svg path { 178 | fill: var(--disabled-text); 179 | } 180 | 181 | label.radio > input:checked + span > svg path { 182 | fill: var(--accent-color); 183 | } 184 | 185 | input[type="text"], 186 | input[type="number"], 187 | textarea { 188 | width: 100%; 189 | padding: 10px; 190 | border-radius: 5px; 191 | border: 1px solid; 192 | 193 | box-sizing: border-box; 194 | 195 | background-color: var(--button-color); 196 | color: var(--color); 197 | } 198 | 199 | textarea { 200 | min-height: 100px; 201 | } 202 | 203 | button { 204 | padding: 5px; 205 | border-radius: 5px; 206 | border: 1px solid; 207 | 208 | line-height: 24px; 209 | 210 | display: inline-flex; 211 | flex-direction: row; 212 | align-items: center; 213 | } 214 | 215 | button:not(:disabled) { 216 | cursor: pointer; 217 | } 218 | 219 | details { 220 | border: 1px solid; 221 | border-radius: 5px; 222 | padding: 0.5em 0.5em 0; 223 | 224 | transition: 0.5s; 225 | 226 | flex-grow: 1; 227 | width: 100%; 228 | } 229 | 230 | summary { 231 | padding: 0.5em; 232 | margin: -0.5em -0.5em 0; 233 | 234 | cursor: pointer; 235 | 236 | font-size: larger; 237 | font-weight: bold; 238 | } 239 | 240 | details[open] summary { 241 | border-bottom: 1px solid; 242 | margin-bottom: 0.5em; 243 | } 244 | 245 | details[open] { 246 | border-left: 10px solid; 247 | border-left-color: var(--accent-bg); 248 | padding: 0.5em; 249 | } 250 | 251 | img.badge { 252 | height: 3em; 253 | } 254 | 255 | a { 256 | color: var(--link-color); 257 | text-decoration: none; 258 | } 259 | 260 | a:hover { 261 | text-decoration: underline; 262 | } 263 | 264 | #import-file { 265 | display: none; 266 | } 267 | 268 | .kofi { 269 | height: 3em; 270 | } 271 | -------------------------------------------------------------------------------- /src/we.ts: -------------------------------------------------------------------------------- 1 | export default "browser" in self ? browser : chrome; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "ES2021", 5 | "module": "ES6", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "files": ["src/ff_popup.ts", "src/ff_background.ts", "src/cr_background.ts", "src/cr_popup.ts"], 13 | "include": ["tsglobal.d.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /tsglobal.d.ts: -------------------------------------------------------------------------------- 1 | import { Browser } from "webextension-polyfill"; 2 | 3 | declare global { 4 | const chrome: Browser; 5 | const browser: Browser; 6 | } 7 | -------------------------------------------------------------------------------- /webpack.chrome.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 3 | const copyWebpackPlugin = require("copy-webpack-plugin"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const MiniCssExtrackPlugin = require("mini-css-extract-plugin"); 6 | const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin"); 7 | const TerserWebpackPlugin = require("terser-webpack-plugin"); 8 | 9 | module.exports = { 10 | entry: { 11 | background: __dirname + "/src/cr_background.ts", 12 | // options: __dirname + "/src/options.ts", 13 | popup: __dirname + "/src/ff_popup.ts" 14 | }, 15 | devtool: "source-map", 16 | output: { 17 | path: __dirname + "/dist", 18 | filename: "[name].bundle.js" 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | use: "ts-loader", 25 | exclude: /node_modules/ 26 | }, 27 | { 28 | test: /\.css$/i, 29 | use: [MiniCssExtrackPlugin.loader, "css-loader"] 30 | } 31 | ] 32 | }, 33 | resolve: { 34 | extensions: [ ".ts", ".tsx", ".js", ".jsx" ], 35 | }, 36 | plugins: [ 37 | new CleanWebpackPlugin(), 38 | new MiniCssExtrackPlugin(), 39 | // new HtmlWebpackPlugin({ 40 | // template: "src/options.html", 41 | // filename: "options.html", 42 | // chunks: ["options"], 43 | // }), 44 | new HtmlWebpackPlugin({ 45 | template: "src/cr_popup.html", 46 | filename: "popup.html", 47 | chunks: ["popup"], 48 | }), 49 | new copyWebpackPlugin({ 50 | patterns: [ 51 | { from: "src/cr_manifest.json", to: "manifest.json" }, 52 | { 53 | from: "src/icons/", 54 | to: "icons", 55 | toType: "dir" 56 | }, 57 | { 58 | from: "src/_locales/", 59 | to: "_locales", 60 | toType: "dir" 61 | } 62 | ] 63 | }) 64 | ], 65 | externals: { 66 | "webextension-polyfill": "browser" 67 | }, 68 | optimization: { 69 | usedExports: true, 70 | minimizer: [ 71 | new CssMinimizerWebpackPlugin(), 72 | new TerserWebpackPlugin() 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 3 | const copyWebpackPlugin = require("copy-webpack-plugin"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const MiniCssExtrackPlugin = require("mini-css-extract-plugin"); 6 | const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin"); 7 | const TerserWebpackPlugin = require("terser-webpack-plugin"); 8 | 9 | module.exports = { 10 | entry: { 11 | background: __dirname + "/src/ff_background.ts", 12 | // options: __dirname + "/src/options.ts", 13 | popup: __dirname + "/src/ff_popup.ts" 14 | }, 15 | devtool: "source-map", 16 | output: { 17 | path: __dirname + "/dist", 18 | filename: "[name].bundle.js" 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | use: "ts-loader", 25 | exclude: /node_modules/ 26 | }, 27 | { 28 | test: /\.css$/i, 29 | use: [MiniCssExtrackPlugin.loader, "css-loader"] 30 | } 31 | ] 32 | }, 33 | resolve: { 34 | extensions: [ ".ts", ".tsx", ".js", ".jsx" ], 35 | }, 36 | plugins: [ 37 | new CleanWebpackPlugin(), 38 | new MiniCssExtrackPlugin(), 39 | // new HtmlWebpackPlugin({ 40 | // template: "src/options.html", 41 | // filename: "options.html", 42 | // chunks: ["options"], 43 | // }), 44 | new HtmlWebpackPlugin({ 45 | template: "src/ff_popup.html", 46 | filename: "popup.html", 47 | chunks: ["popup"], 48 | }), 49 | new copyWebpackPlugin({ 50 | patterns: [ 51 | { from: "src/ff_manifest.json", to: "manifest.json" }, 52 | { 53 | from: "src/icons/", 54 | to: "icons", 55 | toType: "dir" 56 | }, 57 | { 58 | from: "src/_locales/", 59 | to: "_locales", 60 | toType: "dir" 61 | }, 62 | // { from: "node_modules/webextension-polyfill/dist/browser-polyfill.min.js" }, 63 | // { from: "node_modules/webextension-polyfill/dist/browser-polyfill.min.js.map" } 64 | ] 65 | }) 66 | ], 67 | externals: { 68 | "webextension-polyfill": "browser" 69 | }, 70 | optimization: { 71 | usedExports: true, 72 | minimizer: [ 73 | new CssMinimizerWebpackPlugin(), 74 | new TerserWebpackPlugin() 75 | ] 76 | } 77 | } 78 | --------------------------------------------------------------------------------