using my boilerplate yuh
This commit is contained in:
50
src/background/background.ts
Normal file
50
src/background/background.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { MessageListener } from 'chrome-extension-toolkit';
|
||||
import { BACKGROUND_MESSAGES } from 'src/shared/messages';
|
||||
import { generateRandomId } from 'src/shared/util/random';
|
||||
import onHistoryStateUpdated from './events/onHistoryStateUpdated';
|
||||
import onInstall from './events/onInstall';
|
||||
import onNewChromeSession from './events/onNewChromeSession';
|
||||
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
|
||||
import onUpdate from './events/onUpdate';
|
||||
import { sessionStore } from '../shared/storage/sessionStore';
|
||||
import browserActionHandler from './handler/browserActionHandler';
|
||||
import hotReloadingHandler from './handler/hotReloadingHandler';
|
||||
import tabManagementHandler from './handler/tabManagementHandler';
|
||||
|
||||
onServiceWorkerAlive();
|
||||
|
||||
/**
|
||||
* will be triggered on either install or update
|
||||
* (will also be triggered on a user's sync'd browsers (on other devices)))
|
||||
*/
|
||||
chrome.runtime.onInstalled.addListener(details => {
|
||||
switch (details.reason) {
|
||||
case 'install':
|
||||
onInstall();
|
||||
break;
|
||||
case 'update':
|
||||
onUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// This event is fired when any tab's url changes.
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(onHistoryStateUpdated);
|
||||
|
||||
// initialize the message listener that will listen for messages from the content script
|
||||
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
|
||||
...browserActionHandler,
|
||||
...hotReloadingHandler,
|
||||
...tabManagementHandler,
|
||||
});
|
||||
|
||||
messageListener.listen();
|
||||
|
||||
sessionStore.getChromeSessionId().then(async chromeSessionId => {
|
||||
if (!chromeSessionId) {
|
||||
await sessionStore.setChromeSessionId(generateRandomId(10));
|
||||
onNewChromeSession();
|
||||
}
|
||||
});
|
||||
11
src/background/events/onHistoryStateUpdated.ts
Normal file
11
src/background/events/onHistoryStateUpdated.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This event is fired when any tab's url changes.
|
||||
* This is useful for content scripts to know when SPA navigations occur.
|
||||
* @param details
|
||||
*/
|
||||
export default function onHistoryStateUpdated(
|
||||
details: chrome.webNavigation.WebNavigationTransitionCallbackDetails
|
||||
): void {
|
||||
const { tabId, url } = details;
|
||||
// TODO: send a message to tab with tabId to reanalyze the page
|
||||
}
|
||||
26
src/background/events/onInstall.ts
Normal file
26
src/background/events/onInstall.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { SECOND } from 'src/shared/util/time';
|
||||
|
||||
/**
|
||||
* Called when the extension is first installed or synced onto a new machine
|
||||
*/
|
||||
export default async function onInstall() {
|
||||
// set the uninstall url
|
||||
chrome.runtime.setUninstallURL('https://www.google.com');
|
||||
logOnInstallEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* making sure we are not sending duplicate install event for users that have synced browsers
|
||||
* sync storage get's cleared on browser uninstall, so re-installing the browser will trigger the event
|
||||
*/
|
||||
function logOnInstallEvent() {
|
||||
setTimeout(async () => {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const INSTALL_KEY = `${manifest.short_name}-installed`;
|
||||
const storage = await chrome.storage.sync.get(INSTALL_KEY);
|
||||
if (!storage[INSTALL_KEY]) {
|
||||
// TODO: send install event
|
||||
await chrome.storage.sync.set({ [INSTALL_KEY]: true });
|
||||
}
|
||||
}, 5 * SECOND);
|
||||
}
|
||||
4
src/background/events/onNewChromeSession.ts
Normal file
4
src/background/events/onNewChromeSession.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This function is called when the user's browser opens for the first time
|
||||
*/
|
||||
export default function onNewChromeSession() {}
|
||||
9
src/background/events/onServiceWorkerAlive.ts
Normal file
9
src/background/events/onServiceWorkerAlive.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { openDebugTab } from '../util/openDebugTab';
|
||||
|
||||
/**
|
||||
* Called whenever the background service worker comes alive
|
||||
* (usually around 30 seconds to 5 minutes after it was last alive)
|
||||
*/
|
||||
export default function onServiceWorkerAlive() {
|
||||
openDebugTab();
|
||||
}
|
||||
10
src/background/events/onUpdate.ts
Normal file
10
src/background/events/onUpdate.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { hotReloadTab } from 'src/background/util/hotReloadTab';
|
||||
|
||||
/**
|
||||
* Called when the extension is updated (or when the extension is reloaded in development mode)
|
||||
*/
|
||||
export default function onUpdate() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
hotReloadTab();
|
||||
}
|
||||
}
|
||||
20
src/background/handler/browserActionHandler.ts
Normal file
20
src/background/handler/browserActionHandler.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import BrowserActionMessages from 'src/shared/messages/BrowserActionMessages';
|
||||
|
||||
const browserActionHandler: MessageHandler<BrowserActionMessages> = {
|
||||
disableBrowserAction({ sender, sendResponse }) {
|
||||
// by setting the popup to an empty string, clicking the browser action will not open the popup.html.
|
||||
// we can then add an onClickListener to it from the content script
|
||||
chrome.action.setPopup({ tabId: sender.tab?.id, popup: '' }).then(sendResponse);
|
||||
},
|
||||
enableBrowserAction({ sender, sendResponse }) {
|
||||
chrome.action
|
||||
.setPopup({
|
||||
tabId: sender.tab?.id,
|
||||
popup: 'popup.html',
|
||||
})
|
||||
.then(sendResponse);
|
||||
},
|
||||
};
|
||||
|
||||
export default browserActionHandler;
|
||||
21
src/background/handler/hotReloadingHandler.ts
Normal file
21
src/background/handler/hotReloadingHandler.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages';
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import { devStore } from 'src/shared/storage/devStore';
|
||||
|
||||
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
|
||||
async reloadExtension({ sendResponse }) {
|
||||
const isExtensionReloading = await devStore.getIsExtensionReloading();
|
||||
if (!isExtensionReloading) return sendResponse();
|
||||
|
||||
const isTabReloading = await devStore.getIsExtensionReloading();
|
||||
if (isTabReloading) {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const tabToReload = tabs[0];
|
||||
|
||||
await devStore.setReloadTabId(tabToReload?.id);
|
||||
}
|
||||
chrome.runtime.reload();
|
||||
},
|
||||
};
|
||||
|
||||
export default hotReloadingHandler;
|
||||
18
src/background/handler/tabManagementHandler.ts
Normal file
18
src/background/handler/tabManagementHandler.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||
import TabManagementMessages from 'src/shared/messages/TabManagementMessages';
|
||||
|
||||
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
|
||||
getTabId({ sendResponse, sender }) {
|
||||
sendResponse(sender.tab?.id ?? -1);
|
||||
},
|
||||
openNewTab({ data, sendResponse }) {
|
||||
const { url } = data;
|
||||
chrome.tabs.create({ url }).then(sendResponse);
|
||||
},
|
||||
removeTab({ data, sendResponse }) {
|
||||
const { tabId } = data;
|
||||
chrome.tabs.remove(tabId).then(sendResponse);
|
||||
},
|
||||
};
|
||||
|
||||
export default tabManagementHandler;
|
||||
39
src/background/util/hotReloadTab.ts
Normal file
39
src/background/util/hotReloadTab.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { devStore } from 'src/shared/storage/devStore';
|
||||
|
||||
/**
|
||||
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
|
||||
*/
|
||||
const HOT_RELOADING_WHITELIST = [
|
||||
'youtube.com',
|
||||
'twitch.tv',
|
||||
'github.dev',
|
||||
'figma.com',
|
||||
'netflix.com',
|
||||
'disneyplus.com',
|
||||
'hbomax.com',
|
||||
'spotify.com',
|
||||
'localhost:6006',
|
||||
'docs.google.com',
|
||||
'reddit.com',
|
||||
'gmail.com',
|
||||
'photopea.com',
|
||||
];
|
||||
|
||||
/**
|
||||
* Reloads the tab that was open when the extension was reloaded
|
||||
* @returns a promise that resolves when the tab is reloaded
|
||||
*/
|
||||
export async function hotReloadTab(): Promise<void> {
|
||||
const { getIsTabReloading, getReloadTabId } = devStore;
|
||||
|
||||
const [isTabReloading, reloadTabId] = await Promise.all([getIsTabReloading(), getReloadTabId()]);
|
||||
|
||||
if (!isTabReloading || !reloadTabId) return;
|
||||
|
||||
chrome.tabs.get(reloadTabId, tab => {
|
||||
if (!tab?.id) return;
|
||||
if (!HOT_RELOADING_WHITELIST.find(url => tab.url?.includes(url))) {
|
||||
chrome.tabs.reload(tab.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
24
src/background/util/openDebugTab.ts
Normal file
24
src/background/util/openDebugTab.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { devStore } from 'src/shared/storage/devStore';
|
||||
|
||||
/**
|
||||
* Open the debug tab as the first tab
|
||||
*/
|
||||
export async function openDebugTab() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const debugTabId = await devStore.getDebugTabId();
|
||||
|
||||
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
|
||||
if (isAlreadyOpen) return;
|
||||
|
||||
const wasVisible = await devStore.getWasDebugTabVisible();
|
||||
|
||||
const tab = await chrome.tabs.create({
|
||||
url: chrome.runtime.getURL('debug.html'),
|
||||
active: wasVisible,
|
||||
pinned: true,
|
||||
index: 0,
|
||||
});
|
||||
|
||||
await devStore.setDebugTabId(tab.id);
|
||||
}
|
||||
}
|
||||
33
src/debug/hotReload.ts
Normal file
33
src/debug/hotReload.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import io from 'socket.io-client';
|
||||
import { bMessenger } from 'src/shared/messages';
|
||||
|
||||
const socket = io('http://localhost:9090');
|
||||
let reBuilding = false;
|
||||
|
||||
socket.on('disconnect', async reason => {
|
||||
reBuilding = reason.includes('transport') && !reason.includes('client');
|
||||
});
|
||||
|
||||
socket.onAny(args => {
|
||||
console.log(args);
|
||||
});
|
||||
|
||||
socket.on('connect', async () => {
|
||||
if (!reBuilding) {
|
||||
console.log('%c[hot-reloading] listening for changes...', 'color:white; background-color: orange;');
|
||||
} else {
|
||||
console.log(
|
||||
'%c[hot-reloading] changes detected, rebuilding and refreshing...',
|
||||
'color:white; background-color: orange;'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reload', async () => {
|
||||
console.log('%c[hot-reloading] reloading...', 'color:white; background-color: orange;');
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
if (tabs?.[0]?.id) {
|
||||
bMessenger.reloadExtension();
|
||||
}
|
||||
});
|
||||
});
|
||||
129
src/debug/index.tsx
Normal file
129
src/debug/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import './hotReload';
|
||||
import React, { useEffect } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { devStore } from 'src/shared/storage/devStore';
|
||||
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
|
||||
interface JSONEditorProps {
|
||||
data: any;
|
||||
onChange: (updates: any) => void;
|
||||
}
|
||||
|
||||
function JSONEditor(props: JSONEditorProps) {
|
||||
const { data, onChange } = props;
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [json, setJson] = React.useState(JSON.stringify(data, null, 2));
|
||||
|
||||
useEffect(() => {
|
||||
setJson(JSON.stringify(data, null, 2));
|
||||
}, [data]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setJson(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
const updates = JSON.parse(json);
|
||||
onChange(updates);
|
||||
setIsEditing(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEditing ? (
|
||||
<div>
|
||||
<div style={{ flex: 1, marginBottom: 10, gap: 10, display: 'flex' }}>
|
||||
<button style={{ color: 'green' }} onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
<button style={{ color: 'red' }} onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<textarea style={{ width: '100%', height: '300px' }} value={json} onChange={handleChange} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<pre onClick={() => setIsEditing(true)}>{json}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// const PrettyPrintJson = React.memo(({ data }: any) => (
|
||||
// <div>
|
||||
// <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
// </div>
|
||||
// ));
|
||||
|
||||
function DevDashboard() {
|
||||
const [localStorage, setLocalStorage] = React.useState<any>({});
|
||||
const [syncStorage, setSyncStorage] = React.useState<any>({});
|
||||
const [sessionStorage, setSessionStorage] = React.useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
devStore.setWasDebugTabVisible(true);
|
||||
} else {
|
||||
devStore.setWasDebugTabVisible(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(null, result => {
|
||||
setLocalStorage(result);
|
||||
});
|
||||
|
||||
chrome.storage.sync.get(null, result => {
|
||||
setSyncStorage(result);
|
||||
});
|
||||
|
||||
chrome.storage.session.get(null, result => {
|
||||
setSessionStorage(result);
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName === 'local') {
|
||||
setLocalStorage({ ...localStorage, ...changes });
|
||||
} else if (areaName === 'sync') {
|
||||
setSyncStorage({ ...syncStorage, ...changes });
|
||||
} else if (areaName === 'session') {
|
||||
setSessionStorage({ ...sessionStorage, ...changes });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleEditStorage = (areaName: string) => (changes: Record<string, any>) => {
|
||||
chrome.storage[areaName].set(changes);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
{manifest.name} {manifest.version} - {process.env.NODE_ENV}
|
||||
</h1>
|
||||
<p>This tab is used for hot reloading and debugging. We will update this tab further in the future.</p>
|
||||
<h2>Local Storage</h2>
|
||||
<JSONEditor data={localStorage} onChange={handleEditStorage('local')} />
|
||||
<h2>Sync Storage</h2>
|
||||
<JSONEditor data={syncStorage} onChange={handleEditStorage('sync')} />
|
||||
<h2>Session Storage</h2>
|
||||
<JSONEditor data={sessionStorage} onChange={handleEditStorage('session')} />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<DevDashboard />, document.getElementById('root'));
|
||||
6
src/shared/messages/BrowserActionMessages.ts
Normal file
6
src/shared/messages/BrowserActionMessages.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface BrowserActionMessages {
|
||||
/** make it so that clicking the browser action will open the popup.html */
|
||||
enableBrowserAction: () => void;
|
||||
/** make it so that clicking the browser action will respond to interactions from the content script */
|
||||
disableBrowserAction: () => void;
|
||||
}
|
||||
3
src/shared/messages/HotReloadingMessages.ts
Normal file
3
src/shared/messages/HotReloadingMessages.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default interface HotReloadingMessages {
|
||||
reloadExtension: () => void;
|
||||
}
|
||||
21
src/shared/messages/TabManagementMessages.ts
Normal file
21
src/shared/messages/TabManagementMessages.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Messages for managing the user's open tabs list
|
||||
*/
|
||||
export default interface TabManagementMessages {
|
||||
/**
|
||||
* Opens a new tab with the given URL
|
||||
* @param data The URL to open
|
||||
*/
|
||||
openNewTab: (data: { url: string }) => chrome.tabs.Tab;
|
||||
/**
|
||||
* Gets the ID of the current tab (the tab that sent the message)
|
||||
* @returns The ID of the current tab
|
||||
*/
|
||||
getTabId: () => number;
|
||||
/**
|
||||
* Removes the tab with the given ID
|
||||
* @param data The ID of the tab to remove
|
||||
* @returns The ID of the tab that was removed
|
||||
*/
|
||||
removeTab: (data: { tabId: number }) => void;
|
||||
}
|
||||
6
src/shared/messages/TabMessages.ts
Normal file
6
src/shared/messages/TabMessages.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* This is a type with all the message definitions that can be sent TO specific tabs
|
||||
*/
|
||||
export default interface TAB_MESSAGES {
|
||||
reAnalyzePage: (data: { url: string }) => void;
|
||||
}
|
||||
21
src/shared/messages/index.ts
Normal file
21
src/shared/messages/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createMessenger } from 'chrome-extension-toolkit';
|
||||
import TAB_MESSAGES from './TabMessages';
|
||||
import BrowserActionMessages from './BrowserActionMessages';
|
||||
import HotReloadingMessages from './HotReloadingMessages';
|
||||
import TabManagementMessages from './TabManagementMessages';
|
||||
|
||||
/**
|
||||
* This is a type with all the message definitions that can be sent TO the background script
|
||||
*/
|
||||
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & HotReloadingMessages;
|
||||
|
||||
/**
|
||||
* A utility object that can be used to send type-safe messages to the background script
|
||||
*/
|
||||
export const bMessenger = createMessenger<BACKGROUND_MESSAGES>('background');
|
||||
|
||||
|
||||
/**
|
||||
* A utility object that can be used to send type-safe messages to specific tabs
|
||||
*/
|
||||
export const tabMessenger = createMessenger<TAB_MESSAGES>('tab');
|
||||
25
src/shared/storage/devStore.ts
Normal file
25
src/shared/storage/devStore.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createStore } from 'chrome-extension-toolkit';
|
||||
|
||||
/**
|
||||
* A store that is used to store data that is only relevant during development
|
||||
*/
|
||||
interface IDevStore {
|
||||
/** the tabId for the debug tab */
|
||||
debugTabId?: number;
|
||||
/** whether the debug tab is visible */
|
||||
wasDebugTabVisible?: boolean;
|
||||
/** whether we should enable extension reloading */
|
||||
isExtensionReloading?: boolean;
|
||||
/** whether we should enable tab reloading */
|
||||
isTabReloading?: boolean;
|
||||
/** The id of the tab that we want to reload (after the extension reloads itself ) */
|
||||
reloadTabId?: number;
|
||||
}
|
||||
|
||||
export const devStore = createStore<IDevStore>('DEV_STORE', {
|
||||
debugTabId: undefined,
|
||||
isTabReloading: true,
|
||||
wasDebugTabVisible: false,
|
||||
isExtensionReloading: true,
|
||||
reloadTabId: undefined,
|
||||
});
|
||||
15
src/shared/storage/sessionStore.ts
Normal file
15
src/shared/storage/sessionStore.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createStore, Store } from 'chrome-extension-toolkit';
|
||||
|
||||
interface ISessionStore {
|
||||
chromeSessionId?: string;
|
||||
}
|
||||
|
||||
export const sessionStore = createStore<ISessionStore>(
|
||||
'SESSION_STORE',
|
||||
{
|
||||
chromeSessionId: undefined,
|
||||
},
|
||||
{
|
||||
area: 'session',
|
||||
}
|
||||
);
|
||||
25
src/shared/util/random.ts
Normal file
25
src/shared/util/random.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Generate a random ID
|
||||
*
|
||||
* @returns string of size 10 made up of random numbers and letters
|
||||
* @param length the length of the ID to generate
|
||||
* @example "cdtl9l88pj"
|
||||
*/
|
||||
export function generateRandomId(length: number = 10): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random number between min and max
|
||||
* @param min the minimum number
|
||||
* @param max the maximum number
|
||||
* @returns a random number between min and max
|
||||
*/
|
||||
export function rangeRandom(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
34
src/shared/util/string.ts
Normal file
34
src/shared/util/string.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Given a string, returns a string with the first letter capitalized.
|
||||
* @input The string to capitalize.
|
||||
*/
|
||||
export function capitalize(input: string): string {
|
||||
try {
|
||||
return input.charAt(0).toUpperCase() + input.substring(1).toLowerCase();
|
||||
} catch (err) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, returns a string with the first letter capitalized.
|
||||
* @param input capitalize the first letter of this string
|
||||
* @returns the string with the first letter capitalized
|
||||
*/
|
||||
export function capitalizeFirstLetter(input: string): string {
|
||||
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts the
|
||||
* @param input The string to ellipsify.
|
||||
* @param length The length of the string to return.
|
||||
* @returns The ellipsified string.
|
||||
*/
|
||||
export const ellipsify = (input: string, chars: number): string => {
|
||||
let ellipisifed = input;
|
||||
if (input && input.length > chars) {
|
||||
ellipisifed = `${input.substring(0, chars)}...`;
|
||||
}
|
||||
return ellipisifed;
|
||||
};
|
||||
19
src/shared/util/time.ts
Normal file
19
src/shared/util/time.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const MILLISECOND = 1;
|
||||
export const SECOND = 1000 * MILLISECOND;
|
||||
export const MINUTE = 60 * SECOND;
|
||||
export const HOUR = 60 * MINUTE;
|
||||
export const DAY = 24 * HOUR;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const sleep = (milliseconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
|
||||
/**
|
||||
* Checks to see if expired by the time first stored and the time frame that it is stored for
|
||||
*
|
||||
* @param time time it was stored
|
||||
* @param threshold time frame it can be stored for
|
||||
* @return true if expired, false if the time frame is still in range
|
||||
*/
|
||||
export const didExpire = (time: number, threshold: number): boolean => time + threshold <= Date.now();
|
||||
17
src/tsconfig.json
Normal file
17
src/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"es2021"
|
||||
],
|
||||
"types": [
|
||||
"chrome",
|
||||
"node"
|
||||
],
|
||||
},
|
||||
"exclude": [
|
||||
"../webpack"
|
||||
]
|
||||
}
|
||||
19
src/views/content/components/Button/Button.module.scss
Normal file
19
src/views/content/components/Button/Button.module.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@import 'src/views/styles/base.module.scss';
|
||||
|
||||
.button {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Inter';
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
15
src/views/content/components/Button/Button.tsx
Normal file
15
src/views/content/components/Button/Button.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { bMessenger } from 'src/shared/messages';
|
||||
import styles from './Button.module.scss';
|
||||
|
||||
export function Button(): JSX.Element {
|
||||
const handleOpenUrl = (url: string) => () => {
|
||||
bMessenger.openNewTab({ url });
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={styles.button} onClick={handleOpenUrl('https://www.google.com')}>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
0
src/views/content/content.module.scss
Normal file
0
src/views/content/content.module.scss
Normal file
26
src/views/content/content.tsx
Normal file
26
src/views/content/content.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { bMessenger } from 'src/shared/messages';
|
||||
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from 'chrome-extension-toolkit';
|
||||
import { Button } from './components/Button/Button';
|
||||
|
||||
bMessenger.getTabId().then(tabId => {
|
||||
console.log('tabId', tabId);
|
||||
});
|
||||
|
||||
injectReact();
|
||||
|
||||
async function injectReact() {
|
||||
const shadowDom = createShadowDOM('extension-dom-container');
|
||||
render(<Button />, shadowDom.shadowRoot);
|
||||
await shadowDom.addStyle('static/css/content.css');
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
onContextInvalidated(() => {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'context-invalidated-container';
|
||||
document.body.appendChild(div);
|
||||
render(<ContextInvalidated color='black' backgroundColor='orange' />, div);
|
||||
});
|
||||
}
|
||||
4
src/views/hooks/useTabMessage.ts
Normal file
4
src/views/hooks/useTabMessage.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createUseMessage } from 'chrome-extension-toolkit';
|
||||
import TAB_MESSAGES from 'src/shared/messages/TabMessages';
|
||||
|
||||
export const useTabMessage = createUseMessage<TAB_MESSAGES>();
|
||||
17
src/views/popup/popup.tsx
Normal file
17
src/views/popup/popup.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
console.log('test');
|
||||
|
||||
console.log('test2');
|
||||
|
||||
// Path: src/views/popup/popup.tsx
|
||||
|
||||
console.log('test3');
|
||||
|
||||
render(
|
||||
<div>
|
||||
<h1>Test</h1>
|
||||
</div>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
24
src/views/reactDevtools.ts
Normal file
24
src/views/reactDevtools.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// this is a custom wrapper around react-devtools
|
||||
// that changes it so that we only send messages to the devtools when the current tab is active;
|
||||
import { connectToDevTools } from 'react-devtools-core';
|
||||
|
||||
// connect to the devtools server
|
||||
let ws = new WebSocket('ws://localhost:8097');
|
||||
|
||||
connectToDevTools({
|
||||
websocket: ws,
|
||||
});
|
||||
|
||||
// when the tab's visibile state changes, we connect or disconnect from the devtools
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
ws = new WebSocket('ws://localhost:8097');
|
||||
connectToDevTools({
|
||||
websocket: ws,
|
||||
});
|
||||
} else {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
2
src/views/styles/base.module.scss
Normal file
2
src/views/styles/base.module.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import './colors.module.scss';
|
||||
@import './fonts.module.scss';
|
||||
0
src/views/styles/colors.module.scss
Normal file
0
src/views/styles/colors.module.scss
Normal file
9
src/views/styles/fonts.module.scss
Normal file
9
src/views/styles/fonts.module.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('chrome-extension://__MSG_@@extension_id__/fonts/inter-#{$weights}.woff2') format('woff2');
|
||||
font-display: auto;
|
||||
font-style: normal;
|
||||
font-weight: #{$weights};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user