├── .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 |
10 |
11 |
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 |
--------------------------------------------------------------------------------