├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── extension ├── icon.png ├── manifest.json └── options.html ├── package.json ├── public └── browser-polyfill.min.js ├── size-plugin.json ├── src ├── content.ts ├── lib │ ├── console.ts │ ├── icons.ts │ ├── options.ts │ ├── toast.css │ └── toast.ts ├── options.tsx └── script.ts ├── tsconfig.json ├── types.d.ts ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | .DS_Store 5 | /extension.zip 6 | /extension.crx -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logbox 2 | 3 | View console logs and errors on any website without opening the devtools, always be aware of what's happening behind the scene! 4 | 5 | Get it on [Chrome Web Store](https://chrome.google.com/webstore/detail/logbox/cokkmeolkbchogcadikakhldbhhhichm) (the latest version is in review so it's not up to date). Firefox version coming soon or just build it yourself following the guide below. 6 | 7 | ## Preview 8 | 9 | preview 10 | preview 2 11 | preview 3 12 | 13 | ## Development 14 | 15 | ```bash 16 | yarn dev 17 | ``` 18 | 19 | Then the extension will be bundled into `extension` folder. Drag the folder to Chrome / Edge or Firefox's extension page to use it. 20 | 21 | If you want a production build instead, run: 22 | 23 | ```bash 24 | yarn build 25 | ``` 26 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-typescript', { 4 | jsxPragma: 'h' 5 | }], 6 | [ 7 | '@babel/preset-env', 8 | { 9 | modules: false, 10 | }, 11 | ], 12 | ], 13 | plugins: [ 14 | '@babel/plugin-proposal-optional-chaining', 15 | '@babel/plugin-proposal-class-properties', 16 | [ 17 | '@babel/plugin-transform-react-jsx', 18 | { 19 | pragma: 'h', 20 | }, 21 | ], 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/logbox/dc82215510d4f1e180230d0ce58f840e851c82e1/extension/icon.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LogBox", 3 | "version": "0.0.2", 4 | "description": "View logs on any webpage without opening devtools", 5 | "homepage_url": "https://logbox.egoist.sh", 6 | "manifest_version": 2, 7 | "minimum_chrome_version": "74", 8 | "icons": { 9 | "128": "icon.png" 10 | }, 11 | "permissions": ["storage"], 12 | "options_ui": { 13 | "page": "options.html" 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": ["*://*/*"], 18 | "run_at": "document_start", 19 | "js": ["./dist/browser-polyfill.min.js", "./dist/content.js"] 20 | } 21 | ], 22 | "web_accessible_resources": ["dist/script.js"] 23 | } 24 | -------------------------------------------------------------------------------- /extension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LogBox Options 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "logbox", 4 | "version": "0.0.0", 5 | "main": "index.js", 6 | "browserslist": "last 2 Chrome versions, last 2 Firefox versions", 7 | "scripts": { 8 | "dev": "webpack -w --mode development", 9 | "build": "rm -rf extension/dist && webpack --mode production" 10 | }, 11 | "license": "unlicensed", 12 | "devDependencies": { 13 | "@babel/core": "^7.9.6", 14 | "@babel/plugin-proposal-class-properties": "^7.8.3", 15 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 16 | "@babel/plugin-transform-react-jsx": "^7.9.4", 17 | "@babel/preset-env": "^7.9.6", 18 | "@babel/preset-typescript": "^7.9.0", 19 | "alpinejs": "^2.3.3", 20 | "babel-loader": "^8.1.0", 21 | "copy-webpack-plugin": "^5.1.1", 22 | "css-loader": "^3.5.3", 23 | "mini-css-extract-plugin": "^0.9.0", 24 | "preact": "^10.4.1", 25 | "size-plugin": "^2.0.1", 26 | "terser-webpack-plugin": "^3.0.1", 27 | "typescript": "^3.8.3", 28 | "vue-style-loader": "^4.1.2", 29 | "webextension-polyfill-ts": "^0.15.0", 30 | "webpack": "^4.43.0", 31 | "webpack-cli": "^3.3.11", 32 | "webpack-extension-reloader": "^1.1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/browser-polyfill.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if("undefined"==typeof browser||Object.getPrototypeOf(browser)!==Object.prototype){if("object"!=typeof chrome||!chrome||!chrome.runtime||!chrome.runtime.id)throw new Error("This script should only be loaded in a browser extension.");a.exports=(a=>{const b={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(a.runtime.lastError):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e={has(b,c){return c in a||c in d},get(e,f,k){if(f in d)return d[f];if(!(f in a))return;let l=a[f];if("function"==typeof l){if("function"==typeof b[f])l=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);l=h(a,a[f],b)}else l=l.bind(a);}else if("object"==typeof l&&null!==l&&(i(b,f)||i(c,f)))l=j(l,b[f],c[f]);else if(i(c,"*"))l=j(l,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),l;return d[f]=l,l},set(b,c,e,f){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}},f=Object.create(a);return new Proxy(f,e)},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}});let l=!1;const m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){l||(console.warn("Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)",new Error().stack),l=!0),h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(a.runtime.lastError):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=browser}); -------------------------------------------------------------------------------- /size-plugin.json: -------------------------------------------------------------------------------- 1 | [{"timestamp":1597841015518,"files":[{"filename":"content.js","previous":708,"size":708,"diff":0},{"filename":"script.js","previous":3875,"size":4322,"diff":447},{"filename":"options.js","previous":4833,"size":4833,"diff":0}]},{"timestamp":1589275901507,"files":[{"filename":"background.js","previous":559,"size":0,"diff":-559},{"filename":"content.js","previous":568,"size":708,"diff":140},{"filename":"script.js","previous":3702,"size":3875,"diff":173},{"filename":"options.js","previous":0,"size":4833,"diff":4833}]},{"timestamp":1589222915221,"files":[{"filename":"background.js","previous":591,"size":559,"diff":-32},{"filename":"content.js","previous":595,"size":568,"diff":-27},{"filename":"script.js","previous":3720,"size":3702,"diff":-18}]},{"timestamp":1589222822167,"files":[{"filename":"background.js","previous":838,"size":591,"diff":-247},{"filename":"content.css","previous":83,"size":0,"diff":-83},{"filename":"content.js","previous":42302,"size":595,"diff":-41707},{"filename":"script.js","previous":0,"size":3720,"diff":3720}]},{"timestamp":1583257389946,"files":[{"filename":"background.js","previous":1257,"size":838,"diff":-419},{"filename":"browser-polyfill.min.js","previous":3073,"size":0,"diff":-3073},{"filename":"content.css","previous":83,"size":83,"diff":0},{"filename":"content.js","previous":69964,"size":42302,"diff":-27662}]}] 2 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | import { getOptions } from './lib/options' 2 | 3 | getOptions().then((options) => { 4 | const script = document.createElement('script') 5 | script.src = browser.runtime.getURL('dist/script.js') 6 | script.dataset.options = JSON.stringify(options) 7 | script.onload = () => { 8 | script.remove() 9 | } 10 | script.id = `logbox-injected-script` 11 | ;(document.head || document.documentElement).appendChild(script) 12 | 13 | if (__DEV__) { 14 | console.log(`[logbox] injected script`) 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/lib/console.ts: -------------------------------------------------------------------------------- 1 | export const consoleError = console.error.bind(console) 2 | export const consoleLog = console.log.bind(console) 3 | export const consoleInfo = console.info.bind(console) 4 | export const consoleWarn = console.warn.bind(console) -------------------------------------------------------------------------------- /src/lib/icons.ts: -------------------------------------------------------------------------------- 1 | import { ToastType } from "./toast" 2 | 3 | export const warningIcon = `` 4 | 5 | export const errorIcon = `` 6 | 7 | export const infoIcon = `` 8 | 9 | export const icons: {[k in ToastType]: string} = { 10 | warning: warningIcon, 11 | error: errorIcon, 12 | info: infoIcon, 13 | log: infoIcon, 14 | success: infoIcon 15 | } 16 | 17 | export const xIcon = `` -------------------------------------------------------------------------------- /src/lib/options.ts: -------------------------------------------------------------------------------- 1 | let options: typeof defaultOptions | undefined 2 | 3 | export const defaultOptions = { 4 | hideTimeout: 5000, 5 | licenseKey: '' 6 | } 7 | 8 | export const getOptions = async () => { 9 | if (!options) { 10 | const storedOptions = await browser.storage.sync.get(['hideTimeout']) 11 | return { 12 | ...defaultOptions, 13 | ...storedOptions, 14 | } 15 | } 16 | return options 17 | } -------------------------------------------------------------------------------- /src/lib/toast.css: -------------------------------------------------------------------------------- 1 | .logbox_container { 2 | position: fixed; 3 | bottom: 20px; 4 | right: 20px; 5 | padding: 10px; 6 | overflow-y: auto; 7 | max-height: 70vh; 8 | z-index: 9999999999; 9 | border-radius: 4px; 10 | text-align: left; 11 | font-size: 16px; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 13 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 14 | } 15 | 16 | .logbox_container:hover { 17 | background-color: #f0f0f050; 18 | } 19 | 20 | .logbox { 21 | width: 350px; 22 | overflow: auto; 23 | border: 1px solid; 24 | } 25 | 26 | .logbox:last-child { 27 | margin-bottom: 0; 28 | } 29 | 30 | .logbox a { 31 | color: inherit; 32 | } 33 | 34 | .logbox_type__log, 35 | .logbox_type__info, 36 | .logbox_type__warning, 37 | .logbox_type__error { 38 | /* border: 1px solid; */ 39 | border-radius: 6px; 40 | padding: 10px; 41 | margin-bottom: 10px; 42 | color: white; 43 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 44 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 45 | } 46 | 47 | .logbox_type__info { 48 | background-color: #ebf5ff; 49 | color: #2760dd; 50 | border-color: #d5e0f7; 51 | } 52 | 53 | .logbox_type__success { 54 | background-color: #f2faf7; 55 | color: #258065; 56 | } 57 | 58 | .logbox_type__error { 59 | background-color: #fff0f0; 60 | color: #ff0303; 61 | border-color: #ffdfdf; 62 | } 63 | 64 | .logbox_type__warning { 65 | background-color: #fdfdea; 66 | color: #9b612b; 67 | border-color: #ebddd2; 68 | } 69 | 70 | .logbox_icon svg { 71 | width: 1.2em; 72 | height: 1.2em; 73 | } 74 | 75 | .logbox_icon { 76 | margin-right: 5px; 77 | margin-top: 2px; 78 | } 79 | 80 | .logbox_body { 81 | display: flex; 82 | } 83 | 84 | .logbox_icon__default { 85 | display: block; 86 | } 87 | 88 | .logbox_icon__x { 89 | display: none; 90 | } 91 | 92 | .logbox_icon__show-x .logbox_icon__default { 93 | display: none; 94 | } 95 | 96 | .logbox_icon__show-x .logbox_icon__x { 97 | display: block; 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/toast.ts: -------------------------------------------------------------------------------- 1 | import './toast.css' 2 | import { icons, xIcon } from './icons' 3 | 4 | export type ToastType = 'error' | 'info' | 'log' | 'success' | 'warning' 5 | 6 | export type ToastOptions = { 7 | type: ToastType 8 | html: string 9 | timeout?: number 10 | } 11 | 12 | const instances: Toast[] = [] 13 | 14 | const removeInstance = (instance: Toast) => { 15 | instances.splice(instances.indexOf(instance), 1) 16 | } 17 | 18 | const startAllTimers = () => { 19 | for (const i of instances) { 20 | i.startTimer() 21 | } 22 | } 23 | 24 | const stopAllTimers = () => { 25 | for (const i of instances) { 26 | i.stopTimer() 27 | } 28 | } 29 | 30 | export class Toast { 31 | private options: ToastOptions 32 | private $el?: HTMLDivElement 33 | private $container: HTMLDivElement 34 | private timerId?: number 35 | 36 | constructor(options: ToastOptions) { 37 | this.options = { 38 | ...options, 39 | } 40 | 41 | let $container = document.querySelector('.logbox_container') 42 | if (!$container) { 43 | $container = document.createElement('div') 44 | $container.className = 'logbox_container' 45 | $container.addEventListener('mouseenter', () => { 46 | stopAllTimers() 47 | }) 48 | $container.addEventListener('mouseleave', () => { 49 | startAllTimers() 50 | }) 51 | document.body.appendChild($container) 52 | } 53 | this.$container = $container 54 | 55 | this.$el = document.createElement('div') 56 | this.$el.className = `logbox logbox_type__${this.options.type}` 57 | const icon = icons[this.options.type] 58 | const $body = document.createElement('div') 59 | $body.className = `logbox_body` 60 | $body.innerHTML = `
61 | ${icon}${xIcon} 62 |
${this.options.html}
` 63 | $body.addEventListener('mouseenter', (e) => { 64 | const $icon = $body.querySelector('.logbox_icon') 65 | $icon?.classList.toggle('logbox_icon__show-x') 66 | }) 67 | $body.addEventListener('mouseleave', (e) => { 68 | const $icon = $body.querySelector('.logbox_icon') 69 | $icon?.classList.toggle('logbox_icon__show-x') 70 | }) 71 | $body.querySelector('.logbox_icon__x')?.addEventListener('click', () => { 72 | this.hide() 73 | }) 74 | this.$el.appendChild($body) 75 | 76 | instances.push(this) 77 | } 78 | 79 | show() { 80 | if (this.$el) { 81 | this.$container.appendChild(this.$el) 82 | } 83 | this.$container.scrollTop = this.$container.scrollHeight 84 | 85 | this.startTimer() 86 | } 87 | 88 | hide() { 89 | if (this.$el) { 90 | this.stopTimer() 91 | this.$container.removeChild(this.$el) 92 | this.$el = undefined 93 | removeInstance(this) 94 | } 95 | } 96 | 97 | startTimer() { 98 | if (this.options.timeout && !this.timerId && !this.isDebug()) { 99 | this.timerId = window.setTimeout(() => { 100 | this.hide() 101 | }, this.options.timeout) 102 | } 103 | } 104 | 105 | private isDebug() { 106 | return location.search.includes('logbox_debug=true') 107 | } 108 | 109 | stopTimer() { 110 | if (typeof this.timerId === 'number') { 111 | window.clearTimeout(this.timerId) 112 | this.timerId = undefined 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact' 2 | import { useState, useEffect } from 'preact/hooks' 3 | import { getOptions, defaultOptions } from './lib/options' 4 | 5 | const App = () => { 6 | const [options, setOptions] = useState({ 7 | ...defaultOptions, 8 | }) 9 | 10 | const updateOption = (key: string, value: any) => { 11 | const newOptions = { 12 | ...options, 13 | [key]: value, 14 | } 15 | setOptions(newOptions) 16 | browser.storage.sync.set(newOptions) 17 | } 18 | 19 | useEffect(() => { 20 | getOptions().then((options) => { 21 | setOptions(options) 22 | }) 23 | }, []) 24 | 25 | const handleSubmit = (e: any) => { 26 | e.preventDefault() 27 | } 28 | 29 | return ( 30 |
31 |
32 | Auto save is enabled. 33 |
34 |
35 | 36 | { 37 | // @ts-ignore 38 | updateOption('licenseKey', parseInt(e.target!.value)) 39 | }} /> 40 |
This extension is free to use during beta, no license key needed.
41 |
42 |
43 | 44 | { 49 | // @ts-ignore 50 | updateOption('hideTimeout', parseInt(e.target!.value)) 51 | }} 52 | /> 53 |
54 |
55 | ) 56 | } 57 | 58 | render(, document.body) 59 | -------------------------------------------------------------------------------- /src/script.ts: -------------------------------------------------------------------------------- 1 | import { Toast, ToastType } from './lib/toast' 2 | import { 3 | consoleLog, 4 | consoleError, 5 | consoleWarn, 6 | consoleInfo, 7 | } from './lib/console' 8 | 9 | if (__DEV__) { 10 | consoleLog('[logbox] hello from injected script') 11 | } 12 | 13 | const escapeHTML = (input: string) => { 14 | return input.replace(//g, '>') 15 | } 16 | 17 | const formatMessage = (input: string) => { 18 | return escapeHTML(input).replace(/\n/g, '
') 19 | } 20 | 21 | const stringifyArg = (arg: any, type: ToastType): string => { 22 | if (!arg) return '' 23 | 24 | const message = formatMessage(arg.message || String(arg)) 25 | let result = message 26 | if (arg instanceof PromiseRejectionEvent) { 27 | return stringifyArg(arg.reason, type) 28 | } 29 | if (typeof arg.filename !== 'undefined' || arg instanceof ErrorEvent) { 30 | if (arg.filename) { 31 | result += ` (${arg.filename.replace( 32 | `${location.origin}/`, 33 | '' 34 | )}:${arg.lineno}:${arg.colno})` 35 | } 36 | } 37 | if (type === 'error') { 38 | result += `
39 | 40 | 45 | 46 | Search in Google
` 49 | } 50 | 51 | if (arg instanceof Error) { 52 | result += `
${escapeHTML(
 53 |       (arg.stack || '')
 54 |         .replace(`${arg.name}: ${arg.message}`, '')
 55 |         .replace(/^\s+/gm, '')
 56 |     )}
` 57 | } 58 | 59 | return result 60 | } 61 | 62 | const options = JSON.parse(document.currentScript?.dataset.options || '{}') 63 | 64 | const toast = (args: any[], type: ToastType, location?: string) => { 65 | const message = args 66 | .map((arg) => stringifyArg(arg, type)) 67 | .filter(Boolean) 68 | .join(' ') 69 | new Toast({ 70 | html: `${message}${location ? ` (${location})` : ''}`, 71 | type, 72 | timeout: options.hideTimeout, 73 | }).show() 74 | } 75 | 76 | console.error = (...args: any[]) => { 77 | toast(args, 'error') 78 | consoleError(...args) 79 | } 80 | 81 | console.log = (...args: any[]) => { 82 | toast(args, 'info') 83 | consoleLog(...args) 84 | } 85 | 86 | console.warn = (...args: any[]) => { 87 | toast(args, 'warning') 88 | consoleWarn(...args) 89 | } 90 | 91 | console.info = (...args: any[]) => { 92 | toast(args, 'info') 93 | consoleInfo(...args) 94 | } 95 | 96 | window.addEventListener('error', (error) => { 97 | if (__DEV__) { 98 | consoleError(`[logbox] detected global error`) 99 | } 100 | toast([error], 'error') 101 | consoleError(error) 102 | }) 103 | 104 | window.addEventListener('unhandledrejection', (error) => { 105 | toast([error], 'error') 106 | consoleError(error) 107 | }) 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "jsxFactory": "h", 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true, /* Enable all strict type-checking options. */ 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 66 | }, 67 | "include": ["types.d.ts", "src/**/*"] 68 | } 69 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean 2 | 3 | declare const browser: import('webextension-polyfill-ts').Browser 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { DefinePlugin } = require('webpack') 3 | const SizePlugin = require('size-plugin') 4 | const CopyWebpackPlugin = require('copy-webpack-plugin') 5 | 6 | const isProd = process.argv.includes('production') 7 | 8 | /** @type {import('webpack').Configuration} */ 9 | module.exports = { 10 | devtool: isProd ? false : 'sourcemap', 11 | stats: 'errors-only', 12 | entry: { 13 | content: './src/content', 14 | script: './src/script', 15 | options: './src/options', 16 | }, 17 | output: { 18 | path: path.join(__dirname, 'extension/dist'), 19 | filename: '[name].js', 20 | }, 21 | resolve: { 22 | extensions: ['.ts', '.tsx', '.js', '.json', '.css'], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/i, 28 | use: ['vue-style-loader', 'css-loader'], 29 | }, 30 | { 31 | test: [/\.jsx?$/, /\.tsx?$/], 32 | exclude: /node_modules/, 33 | use: [ 34 | { 35 | loader: 'babel-loader', 36 | options: { 37 | cacheDirectory: true, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | plugins: [ 45 | new SizePlugin(), 46 | new CopyWebpackPlugin([ 47 | { 48 | from: 'public', 49 | to: '.', 50 | }, 51 | ]), 52 | new DefinePlugin({ 53 | __DEV__: process.argv.includes('development'), 54 | }), 55 | ], 56 | } 57 | --------------------------------------------------------------------------------