mirror of
https://github.com/abhijithvijayan/web-extension-starter.git
synced 2026-01-30 09:48:12 +01:00
Setup basic trading enhancements
This commit is contained in:
parent
041527e7a4
commit
5f9f1faa93
4
.gitignore
vendored
4
.gitignore
vendored
@ -170,7 +170,7 @@ typings/
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# react / gatsby
|
||||
# react / gatsby
|
||||
public/
|
||||
|
||||
# vuepress build output
|
||||
@ -201,4 +201,4 @@ dist/
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
.pnp.*
|
||||
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false
|
||||
}
|
||||
27598
package-lock.json
generated
Normal file
27598
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
source/ContentScript/element-factories/owns-badge.ts
Normal file
18
source/ContentScript/element-factories/owns-badge.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import Trader from "../trader";
|
||||
|
||||
export default class OwnsBadge {
|
||||
static create(trader: Trader): HTMLSpanElement {
|
||||
let badge = document.createElement("a");
|
||||
badge.classList.add("et-badge");
|
||||
|
||||
if (trader.ownsList !== null) {
|
||||
badge.href = trader.ownsList;
|
||||
}
|
||||
|
||||
let recordings = trader.wantsCount === 1 ? "recording" : "recordings";
|
||||
|
||||
badge.innerHTML = `Owns <strong style="font-weight: semibold; color: #0f172a;">${trader.ownsCount} ${recordings}</strong> that I want`;
|
||||
|
||||
return badge;
|
||||
}
|
||||
}
|
||||
26
source/ContentScript/element-factories/star.ts
Normal file
26
source/ContentScript/element-factories/star.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export default class Star {
|
||||
static create(): SVGElement {
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("height", "1em");
|
||||
svg.setAttribute("viewBox", "0 0 576 512");
|
||||
svg.style.position = "absolute";
|
||||
svg.style.top = "0";
|
||||
svg.style.left = "-28px";
|
||||
svg.style.width = "20px";
|
||||
svg.style.height = "20px";
|
||||
|
||||
let path = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"path"
|
||||
);
|
||||
path.setAttribute(
|
||||
"d",
|
||||
"M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
|
||||
);
|
||||
path.setAttribute("fill", "#f59e0b");
|
||||
|
||||
svg.appendChild(path);
|
||||
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
18
source/ContentScript/element-factories/wants-badge.ts
Normal file
18
source/ContentScript/element-factories/wants-badge.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import Trader from "../trader";
|
||||
|
||||
export default class WantsBadge {
|
||||
static create(trader: Trader): HTMLSpanElement {
|
||||
let badge = document.createElement("a");
|
||||
badge.classList.add("et-badge");
|
||||
|
||||
if (trader.wantsList !== null) {
|
||||
badge.href = trader.wantsList;
|
||||
}
|
||||
|
||||
let recordings = trader.wantsCount === 1 ? "recording" : "recordings";
|
||||
|
||||
badge.innerHTML = `Wants <strong style="font-weight: semibold; color: #0f172a;">${trader.wantsCount} ${recordings}</strong> that I own`;
|
||||
|
||||
return badge;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,10 @@
|
||||
console.log('helloworld from content script');
|
||||
import WhoOwnsEnhancer from "./page-enhancers/who-owns-enhancer";
|
||||
import WhoWantsEnhancer from "./page-enhancers/who-wants-enhancer";
|
||||
|
||||
export {};
|
||||
if (window.location.href.includes("who-owns.php")) {
|
||||
WhoOwnsEnhancer.enhance(document);
|
||||
}
|
||||
|
||||
if (window.location.href.includes("who-wants.php")) {
|
||||
WhoWantsEnhancer.enhance(document);
|
||||
}
|
||||
|
||||
79
source/ContentScript/page-elements/profile-link.ts
Normal file
79
source/ContentScript/page-elements/profile-link.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import OwnsBadge from "../element-factories/owns-badge";
|
||||
import Star from "../element-factories/star";
|
||||
import WantsBadge from "../element-factories/wants-badge";
|
||||
import Trader from "../trader";
|
||||
import TraderRepository from "../trader-repository";
|
||||
|
||||
export default class ProfileLink {
|
||||
constructor(
|
||||
public element: HTMLAnchorElement,
|
||||
public traderRepository: TraderRepository
|
||||
) {}
|
||||
|
||||
get username(): string {
|
||||
return this.element.innerText.trim();
|
||||
}
|
||||
|
||||
get profile(): string {
|
||||
return this.element.href;
|
||||
}
|
||||
|
||||
get trader(): Trader | null {
|
||||
return this.traderRepository.find(this.username);
|
||||
}
|
||||
|
||||
get lastElement(): Element {
|
||||
return this.element.nextElementSibling?.tagName === "SPAN"
|
||||
? this.element.nextElementSibling!
|
||||
: this.element;
|
||||
}
|
||||
|
||||
addOwnsBadge() {
|
||||
let trader = this.trader;
|
||||
|
||||
if (trader === null) {
|
||||
throw new Error(
|
||||
"Cannot add owns badge to a profile link that has no trader data"
|
||||
);
|
||||
}
|
||||
|
||||
let badge = OwnsBadge.create(this.trader!);
|
||||
|
||||
this.lastElement.insertAdjacentElement("afterend", badge);
|
||||
}
|
||||
|
||||
addWantsBadge() {
|
||||
let trader = this.trader;
|
||||
|
||||
if (trader === null) {
|
||||
throw new Error(
|
||||
"Cannot add wants badge to a profile link that has no trader data"
|
||||
);
|
||||
}
|
||||
|
||||
let badge = WantsBadge.create(this.trader!);
|
||||
|
||||
this.lastElement.insertAdjacentElement("afterend", badge);
|
||||
}
|
||||
|
||||
addStar() {
|
||||
let star = Star.create();
|
||||
|
||||
this.element.style.position = "relative";
|
||||
this.element.style.fontWeight = "bold";
|
||||
|
||||
this.element.appendChild(star);
|
||||
}
|
||||
|
||||
static getAll(page: Document, traderRepository: TraderRepository) {
|
||||
return Array.from(
|
||||
page.querySelectorAll('a[href*="/profile/view-profile.php?id="]')
|
||||
).map(
|
||||
(profileLink) =>
|
||||
new ProfileLink(
|
||||
profileLink as HTMLAnchorElement,
|
||||
traderRepository
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
source/ContentScript/page-enhancers/who-owns-enhancer.ts
Normal file
22
source/ContentScript/page-enhancers/who-owns-enhancer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import ProfileLink from "../page-elements/profile-link";
|
||||
import TraderRepository from "../trader-repository";
|
||||
|
||||
export default class WhoOwnsEnhancer {
|
||||
static async enhance(page: Document) {
|
||||
let repository = await TraderRepository.load();
|
||||
|
||||
ProfileLink.getAll(page, repository)
|
||||
.filter((profileLink) => profileLink.trader !== null)
|
||||
.filter((profileLink) => profileLink.trader!.wantsCount !== null)
|
||||
.filter((profileLink) => profileLink.trader!.wantsCount! > 0)
|
||||
.map((profileLink) => {
|
||||
profileLink.addWantsBadge();
|
||||
|
||||
return profileLink;
|
||||
})
|
||||
.filter((profileLink) => profileLink.trader!.open)
|
||||
.forEach((profileLink) => {
|
||||
profileLink.addStar();
|
||||
});
|
||||
}
|
||||
}
|
||||
22
source/ContentScript/page-enhancers/who-wants-enhancer.ts
Normal file
22
source/ContentScript/page-enhancers/who-wants-enhancer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import ProfileLink from "../page-elements/profile-link";
|
||||
import TraderRepository from "../trader-repository";
|
||||
|
||||
export default class WhoWantsEnhancer {
|
||||
static async enhance(page: Document) {
|
||||
let repository = await TraderRepository.load();
|
||||
|
||||
ProfileLink.getAll(page, repository)
|
||||
.filter((profileLink) => profileLink.trader !== null)
|
||||
.filter((profileLink) => profileLink.trader!.ownsCount !== null)
|
||||
.filter((profileLink) => profileLink.trader!.ownsCount! > 0)
|
||||
.map((profileLink) => {
|
||||
profileLink.addOwnsBadge();
|
||||
|
||||
return profileLink;
|
||||
})
|
||||
.filter((profileLink) => profileLink.trader!.open)
|
||||
.forEach((profileLink) => {
|
||||
profileLink.addStar();
|
||||
});
|
||||
}
|
||||
}
|
||||
127
source/ContentScript/trader-repository.ts
Normal file
127
source/ContentScript/trader-repository.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { browser } from "webextension-polyfill-ts";
|
||||
import Trader from "./trader";
|
||||
|
||||
export default class TraderRepository {
|
||||
page: Document;
|
||||
traders: Trader[];
|
||||
|
||||
constructor(traders: Trader[] = []) {
|
||||
this.traders = traders;
|
||||
}
|
||||
|
||||
static async load(): Promise<TraderRepository> {
|
||||
if (!(await this.hasTradersInStorage())) {
|
||||
await this.refreshGlobalTradersStorage();
|
||||
}
|
||||
|
||||
let traders = await this.loadTradersFromStorage();
|
||||
|
||||
return new TraderRepository(traders!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the traders who want my items from the Encora website.
|
||||
*/
|
||||
static async fetchTradersWhoWantMyItems(): Promise<Trader[]> {
|
||||
const response = await fetch(
|
||||
"https://encora.it/profile/trade_finder_theirs.php"
|
||||
);
|
||||
let text = await response.text();
|
||||
let page = new DOMParser().parseFromString(text, "text/html");
|
||||
|
||||
let table = page.querySelector("table")!;
|
||||
let rows = table.querySelectorAll("tr");
|
||||
|
||||
return Array.from(rows)
|
||||
.map((row) => Trader.fromWantsRow(row as HTMLTableRowElement))
|
||||
.filter((trader) => trader !== null) as Trader[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the traders who own my wants from the Encora website.
|
||||
*/
|
||||
static async fetchTradersWhoOwnMyWants(): Promise<Trader[]> {
|
||||
const response = await fetch(
|
||||
"https://encora.it/profile/trade_finder.php"
|
||||
);
|
||||
let text = await response.text();
|
||||
let page = new DOMParser().parseFromString(text, "text/html");
|
||||
|
||||
let table = page.querySelector("table")!;
|
||||
let rows = table.querySelectorAll("tr");
|
||||
|
||||
return Array.from(rows)
|
||||
.map((row) => Trader.fromOwnsRow(row as HTMLTableRowElement))
|
||||
.filter((trader) => trader !== null) as Trader[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the traders who want my items and the traders who own my wants
|
||||
* from the Encora website and stores them in the global storage.
|
||||
*/
|
||||
static async refreshGlobalTradersStorage() {
|
||||
let [wantsTraders, ownsTraders] = await Promise.all([
|
||||
this.fetchTradersWhoWantMyItems(),
|
||||
this.fetchTradersWhoOwnMyWants(),
|
||||
]);
|
||||
|
||||
let traders = Trader.mergeLists(wantsTraders, ownsTraders).map(
|
||||
(trader) => trader.toJSON()
|
||||
);
|
||||
|
||||
browser.storage.local.set({ traders: traders });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the global storage has traders.
|
||||
*/
|
||||
static async hasTradersInStorage(): Promise<boolean> {
|
||||
let storage = await browser.storage.local.get("traders");
|
||||
|
||||
return storage.traders != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the traders from the global storage.
|
||||
*/
|
||||
static async loadTradersFromStorage(): Promise<Trader[]> {
|
||||
let storage = await browser.storage.local.get("traders");
|
||||
let traders = storage.traders;
|
||||
|
||||
if (traders == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return traders.map((trader: any) => Trader.fromJSON(trader));
|
||||
}
|
||||
|
||||
find(username: string): Trader | null {
|
||||
return (
|
||||
this.traders.find((trader) => trader.username === username) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
static loadFromWhoOwnsPage(page: Document): TraderRepository {
|
||||
let traders = Array.from(
|
||||
page.querySelectorAll('a[href*="/profile/view-profile.php?id="]')
|
||||
)
|
||||
.map((profileLink) =>
|
||||
Trader.fromProfileLink(profileLink as HTMLAnchorElement)
|
||||
)
|
||||
.filter((trader) => trader !== null);
|
||||
|
||||
return new TraderRepository(traders);
|
||||
}
|
||||
|
||||
add(trader: Trader) {
|
||||
this.traders.push(trader);
|
||||
}
|
||||
|
||||
getAll(): Trader[] {
|
||||
return this.traders;
|
||||
}
|
||||
|
||||
getOpen(): Trader[] {
|
||||
return this.traders.filter((trader) => trader.open);
|
||||
}
|
||||
}
|
||||
189
source/ContentScript/trader.ts
Normal file
189
source/ContentScript/trader.ts
Normal file
@ -0,0 +1,189 @@
|
||||
interface RowData {
|
||||
username: string;
|
||||
profile: string;
|
||||
open: boolean;
|
||||
count: number;
|
||||
list: string | null;
|
||||
}
|
||||
|
||||
export default class Trader {
|
||||
constructor(
|
||||
public username: string,
|
||||
public profile: string,
|
||||
public open: boolean | null,
|
||||
public ownsCount: number | null = null,
|
||||
public ownsList: string | null = null,
|
||||
public wantsCount: number | null = null,
|
||||
public wantsList: string | null = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the underlying trader data from a row in a "Trade finder" table.
|
||||
*/
|
||||
static parseRow(row: HTMLTableRowElement): RowData | null {
|
||||
let cells = row.querySelectorAll("td");
|
||||
|
||||
if (cells.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let profileLink = cells[0].querySelector("a");
|
||||
|
||||
if (!profileLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let username =
|
||||
profileLink.querySelector("strong")?.innerText ??
|
||||
profileLink.innerText;
|
||||
let profile = profileLink.href;
|
||||
|
||||
let open =
|
||||
cells[0]
|
||||
.querySelector("span")
|
||||
?.innerText.includes("Open to trades") ?? false;
|
||||
|
||||
let wantsLink = cells[1].querySelector("a");
|
||||
|
||||
let count = wantsLink
|
||||
? parseInt(wantsLink.innerText.match(/\d+/)![0])
|
||||
: 0;
|
||||
let list = wantsLink ? wantsLink.href : null;
|
||||
|
||||
return {
|
||||
username,
|
||||
profile,
|
||||
open,
|
||||
count,
|
||||
list,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Trader from a row in a "Trade finder wants" table.
|
||||
*/
|
||||
static fromWantsRow(row: HTMLTableRowElement): Trader | null {
|
||||
let data = Trader.parseRow(row);
|
||||
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Trader(
|
||||
data.username,
|
||||
data.profile,
|
||||
data.open,
|
||||
null,
|
||||
null,
|
||||
data.count,
|
||||
data.list
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Trader from a row in a "Trade finder owns" table.
|
||||
*/
|
||||
static fromOwnsRow(row: HTMLTableRowElement): Trader | null {
|
||||
let data = Trader.parseRow(row);
|
||||
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Trader(
|
||||
data.username,
|
||||
data.profile,
|
||||
data.open,
|
||||
data.count,
|
||||
data.list,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Trader from a profile link.
|
||||
*/
|
||||
static fromProfileLink(profileLink: HTMLAnchorElement) {
|
||||
let username = profileLink.innerText.trim();
|
||||
let profile = profileLink.href;
|
||||
|
||||
let open = null;
|
||||
|
||||
if (profileLink.nextElementSibling?.nodeName === "SPAN") {
|
||||
open = (<HTMLSpanElement>(
|
||||
profileLink.nextElementSibling
|
||||
)).innerText.includes("Open to trades");
|
||||
}
|
||||
|
||||
return new Trader(username, profile, open, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two traders together.
|
||||
*/
|
||||
merge(trader: Trader): Trader {
|
||||
return new Trader(
|
||||
this.username,
|
||||
this.profile,
|
||||
this.open ?? trader.open,
|
||||
this.ownsCount ?? trader.ownsCount,
|
||||
this.ownsList ?? trader.ownsList,
|
||||
this.wantsCount ?? trader.wantsCount,
|
||||
this.wantsList ?? trader.wantsList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two lists of traders together.
|
||||
*/
|
||||
static mergeLists(list1: Trader[], list2: Trader[]): Trader[] {
|
||||
let merged = list1.map((trader) => {
|
||||
let trader2 = list2.find((t) => t.username === trader.username);
|
||||
|
||||
if (trader2 === undefined) {
|
||||
return trader;
|
||||
}
|
||||
|
||||
return trader.merge(trader2);
|
||||
});
|
||||
|
||||
merged.push(
|
||||
...list2.filter(
|
||||
(trader) => !merged.some((t) => t.username === trader.username)
|
||||
)
|
||||
);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Trader to a JSON object.
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
username: this.username,
|
||||
profile: this.profile,
|
||||
open: this.open,
|
||||
ownsCount: this.ownsCount,
|
||||
ownsList: this.ownsList,
|
||||
wantsCount: this.wantsCount,
|
||||
wantsList: this.wantsList,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON object to a Trader.
|
||||
*/
|
||||
static fromJSON(json: any) {
|
||||
return new Trader(
|
||||
json.username,
|
||||
json.profile,
|
||||
json.open,
|
||||
json.ownsCount,
|
||||
json.ownsList,
|
||||
json.wantsCount,
|
||||
json.wantsList
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
<label htmlFor="username">Your Name</label>
|
||||
<br />
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="logging">
|
||||
<input type="checkbox" name="logging" /> Show the features enabled
|
||||
on each page in the console
|
||||
</label>
|
||||
|
||||
<p>cool cool cool</p>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
||||
@ -1,6 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Options from './Options';
|
||||
|
||||
ReactDOM.render(<Options />, document.getElementById('options-root'));
|
||||
@ -1,10 +0,0 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
@import "~webext-base-css/webext-base.css";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {browser, Tabs} from 'webextension-polyfill-ts';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
function openWebPage(url: string): Promise<Tabs.Tab> {
|
||||
return browser.tabs.create({url});
|
||||
}
|
||||
|
||||
const Popup: React.FC = () => {
|
||||
return (
|
||||
<section id="popup">
|
||||
<h2>WEB-EXTENSION-STARTER</h2>
|
||||
<button
|
||||
id="options__button"
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage('options.html');
|
||||
}}
|
||||
>
|
||||
Options Page
|
||||
</button>
|
||||
<div className="links__holder">
|
||||
<ul>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage(
|
||||
'https://github.com/abhijithvijayan/web-extension-starter'
|
||||
);
|
||||
}}
|
||||
>
|
||||
GitHub
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage(
|
||||
'https://www.buymeacoffee.com/abhijithvijayan'
|
||||
);
|
||||
}}
|
||||
>
|
||||
Buy Me A Coffee
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
@ -1,6 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Popup from './Popup';
|
||||
|
||||
ReactDOM.render(<Popup />, document.getElementById('popup-root'));
|
||||
@ -1,53 +0,0 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
}
|
||||
|
||||
|
||||
#popup {
|
||||
min-width: 350px;
|
||||
padding: 30px 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#options__button {
|
||||
width: 50%;
|
||||
background: green;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
border-radius: 15px;
|
||||
padding: 5px 10px;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.links__holder {
|
||||
ul {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
justify-content: space-around;
|
||||
|
||||
li {
|
||||
button {
|
||||
border-radius: 25px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
padding: 10px 17px;
|
||||
background-color: rgba(0, 0, 255, 0.7);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
source/assets/css/extension.css
Normal file
14
source/assets/css/extension.css
Normal file
@ -0,0 +1,14 @@
|
||||
.et-badge {
|
||||
background-color: #e2e8f0;
|
||||
color: #334155;
|
||||
font-size: 0.6em;
|
||||
border-radius: 5px;
|
||||
padding: 2px 6px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.et-badge:hover {
|
||||
background-color: #cbd5e1;
|
||||
color: #020617;
|
||||
text-decoration: none;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
source/assets/icons/tools-48.png
Normal file
BIN
source/assets/icons/tools-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 916 B |
BIN
source/assets/icons/tools-96.png
Normal file
BIN
source/assets/icons/tools-96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,30 +1,27 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Sample WebExtension",
|
||||
"version": "0.0.0",
|
||||
"name": "Encora Tools",
|
||||
"version": "0.0.1",
|
||||
|
||||
"icons": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
"48": "assets/icons/tools-48.png",
|
||||
"96": "assets/icons/tools-96.png"
|
||||
},
|
||||
"description": "Sample description",
|
||||
"homepage_url": "https://github.com/abhijithvijayan/web-extension-starter",
|
||||
"short_name": "Sample Name",
|
||||
"description": "Adds extra features to the Encora trading site.",
|
||||
"homepage_url": "https://musicalbean.carrd.co",
|
||||
"short_name": "Encora Tools",
|
||||
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
"webRequest"
|
||||
],
|
||||
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
"__chrome|firefox__author": "abhijithvijayan",
|
||||
"__chrome|firefox__author": "musicalbean",
|
||||
"__opera__developer": {
|
||||
"name": "abhijithvijayan"
|
||||
"name": "musicalbean"
|
||||
},
|
||||
|
||||
"__firefox__applications": {
|
||||
@ -36,26 +33,6 @@
|
||||
"__chrome__minimum_chrome_version": "49",
|
||||
"__opera__minimum_opera_version": "36",
|
||||
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"default_title": "tiny title",
|
||||
"__chrome|opera__chrome_style": false,
|
||||
"__firefox__browser_style": false
|
||||
},
|
||||
|
||||
"__chrome|opera__options_page": "options.html",
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true,
|
||||
"__chrome__chrome_style": false
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/background.bundle.js"
|
||||
@ -65,11 +42,13 @@
|
||||
|
||||
"content_scripts": [{
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
"*://*.encora.it/*"
|
||||
],
|
||||
"js": [
|
||||
"js/contentScript.bundle.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/css/extension.css"
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito:400,600");
|
||||
@ -1,10 +0,0 @@
|
||||
@import '~advanced-css-reset/dist/reset.css';
|
||||
|
||||
// Add your custom reset rules here
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
// colors
|
||||
$black: #0d0d0d;
|
||||
$greyWhite: #f3f3f3;
|
||||
$skyBlue: #8892b0;
|
||||
|
||||
// fonts
|
||||
$nunito: "Nunito", sans-serif;
|
||||
|
||||
// font weights
|
||||
$thin: 100;
|
||||
$exlight: 200;
|
||||
$light: 300;
|
||||
$regular: 400;
|
||||
$medium: 500;
|
||||
$semibold: 600;
|
||||
$bold: 700;
|
||||
$exbold: 800;
|
||||
$exblack: 900;
|
||||
|
||||
// other variables
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="options-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=500" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,211 +1,203 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const FilemanagerPlugin = require('filemanager-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const ExtensionReloader = require('webpack-extension-reloader');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const FilemanagerPlugin = require("filemanager-webpack-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const ExtensionReloader = require("webpack-extension-reloader");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const WextManifestWebpackPlugin = require("wext-manifest-webpack-plugin");
|
||||
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
|
||||
const viewsPath = path.join(__dirname, 'views');
|
||||
const sourcePath = path.join(__dirname, 'source');
|
||||
const destPath = path.join(__dirname, 'extension');
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const viewsPath = path.join(__dirname, "views");
|
||||
const sourcePath = path.join(__dirname, "source");
|
||||
const destPath = path.join(__dirname, "extension");
|
||||
const nodeEnv = process.env.NODE_ENV || "development";
|
||||
const targetBrowser = process.env.TARGET_BROWSER;
|
||||
|
||||
const extensionReloaderPlugin =
|
||||
nodeEnv === 'development'
|
||||
? new ExtensionReloader({
|
||||
port: 9090,
|
||||
reloadPage: true,
|
||||
entries: {
|
||||
// TODO: reload manifest on update
|
||||
contentScript: 'contentScript',
|
||||
background: 'background',
|
||||
extensionPage: ['popup', 'options'],
|
||||
},
|
||||
})
|
||||
: () => {
|
||||
this.apply = () => {};
|
||||
};
|
||||
nodeEnv === "development"
|
||||
? new ExtensionReloader({
|
||||
port: 9090,
|
||||
reloadPage: true,
|
||||
entries: {
|
||||
// TODO: reload manifest on update
|
||||
contentScript: "contentScript",
|
||||
background: "background",
|
||||
extensionPage: ["popup", "options"],
|
||||
},
|
||||
})
|
||||
: () => {
|
||||
this.apply = () => {};
|
||||
};
|
||||
|
||||
const getExtensionFileType = (browser) => {
|
||||
if (browser === 'opera') {
|
||||
return 'crx';
|
||||
}
|
||||
if (browser === "opera") {
|
||||
return "crx";
|
||||
}
|
||||
|
||||
if (browser === 'firefox') {
|
||||
return 'xpi';
|
||||
}
|
||||
if (browser === "firefox") {
|
||||
return "xpi";
|
||||
}
|
||||
|
||||
return 'zip';
|
||||
return "zip";
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342
|
||||
devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342
|
||||
|
||||
stats: {
|
||||
all: false,
|
||||
builtAt: true,
|
||||
errors: true,
|
||||
hash: true,
|
||||
},
|
||||
|
||||
mode: nodeEnv,
|
||||
|
||||
entry: {
|
||||
manifest: path.join(sourcePath, 'manifest.json'),
|
||||
background: path.join(sourcePath, 'Background', 'index.ts'),
|
||||
contentScript: path.join(sourcePath, 'ContentScript', 'index.ts'),
|
||||
popup: path.join(sourcePath, 'Popup', 'index.tsx'),
|
||||
options: path.join(sourcePath, 'Options', 'index.tsx'),
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(destPath, targetBrowser),
|
||||
filename: 'js/[name].bundle.js',
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
alias: {
|
||||
'webextension-polyfill-ts': path.resolve(
|
||||
path.join(__dirname, 'node_modules', 'webextension-polyfill-ts')
|
||||
),
|
||||
stats: {
|
||||
all: false,
|
||||
builtAt: true,
|
||||
errors: true,
|
||||
hash: true,
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
type: 'javascript/auto', // prevent webpack handling json with its own loaders,
|
||||
test: /manifest\.json$/,
|
||||
use: {
|
||||
loader: 'wext-manifest-loader',
|
||||
options: {
|
||||
usePackageJSONVersion: true, // set to false to not use package.json version for manifest
|
||||
},
|
||||
mode: nodeEnv,
|
||||
|
||||
entry: {
|
||||
manifest: path.join(sourcePath, "manifest.json"),
|
||||
background: path.join(sourcePath, "Background", "index.ts"),
|
||||
contentScript: path.join(sourcePath, "ContentScript", "index.ts"),
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(destPath, targetBrowser),
|
||||
filename: "js/[name].bundle.js",
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".json"],
|
||||
alias: {
|
||||
"webextension-polyfill-ts": path.resolve(
|
||||
path.join(__dirname, "node_modules", "webextension-polyfill-ts")
|
||||
),
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(js|ts)x?$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS
|
||||
},
|
||||
{
|
||||
loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
[
|
||||
'autoprefixer',
|
||||
{
|
||||
// Options
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
type: "javascript/auto", // prevent webpack handling json with its own loaders,
|
||||
test: /manifest\.json$/,
|
||||
use: {
|
||||
loader: "wext-manifest-loader",
|
||||
options: {
|
||||
usePackageJSONVersion: true, // set to false to not use package.json version for manifest
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(js|ts)x?$/,
|
||||
loader: "babel-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS
|
||||
},
|
||||
{
|
||||
loader: "css-loader", // Takes the CSS files and returns the CSS with imports and url(...) for Webpack
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
[
|
||||
"autoprefixer",
|
||||
{
|
||||
// Options
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"resolve-url-loader", // Rewrites relative paths in url() statements
|
||||
"sass-loader", // Takes the Sass/SCSS file and compiles to the CSS
|
||||
],
|
||||
},
|
||||
},
|
||||
'resolve-url-loader', // Rewrites relative paths in url() statements
|
||||
'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// Plugin to not generate js bundle for manifest entry
|
||||
new WextManifestWebpackPlugin(),
|
||||
// Generate sourcemaps
|
||||
new webpack.SourceMapDevToolPlugin({filename: false}),
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
// environmental variables
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']),
|
||||
// delete previous build files
|
||||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: [
|
||||
path.join(process.cwd(), `extension/${targetBrowser}`),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
`extension/${targetBrowser}.${getExtensionFileType(targetBrowser)}`
|
||||
),
|
||||
],
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'popup.html'),
|
||||
inject: 'body',
|
||||
chunks: ['popup'],
|
||||
hash: true,
|
||||
filename: 'popup.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'options.html'),
|
||||
inject: 'body',
|
||||
chunks: ['options'],
|
||||
hash: true,
|
||||
filename: 'options.html',
|
||||
}),
|
||||
// write css file(s) to build folder
|
||||
new MiniCssExtractPlugin({filename: 'css/[name].css'}),
|
||||
// copy static assets
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{from: 'source/assets', to: 'assets'}],
|
||||
}),
|
||||
// plugin to enable browser reloading in development mode
|
||||
extensionReloaderPlugin,
|
||||
],
|
||||
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ['default', {discardComments: {removeAll: true}}],
|
||||
},
|
||||
}),
|
||||
new FilemanagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
archive: [
|
||||
{
|
||||
format: 'zip',
|
||||
source: path.join(destPath, targetBrowser),
|
||||
destination: `${path.join(destPath, targetBrowser)}.${getExtensionFileType(targetBrowser)}`,
|
||||
options: {zlib: {level: 6}},
|
||||
},
|
||||
plugins: [
|
||||
// Plugin to not generate js bundle for manifest entry
|
||||
new WextManifestWebpackPlugin(),
|
||||
// Generate sourcemaps
|
||||
new webpack.SourceMapDevToolPlugin({ filename: false }),
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
// environmental variables
|
||||
new webpack.EnvironmentPlugin(["NODE_ENV", "TARGET_BROWSER"]),
|
||||
// delete previous build files
|
||||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: [
|
||||
path.join(process.cwd(), `extension/${targetBrowser}`),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
`extension/${targetBrowser}.${getExtensionFileType(
|
||||
targetBrowser
|
||||
)}`
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
// write css file(s) to build folder
|
||||
new MiniCssExtractPlugin({ filename: "css/[name].css" }),
|
||||
// copy static assets
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: "source/assets", to: "assets" }],
|
||||
}),
|
||||
// plugin to enable browser reloading in development mode
|
||||
extensionReloaderPlugin,
|
||||
],
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorPluginOptions: {
|
||||
preset: [
|
||||
"default",
|
||||
{ discardComments: { removeAll: true } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
new FilemanagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
archive: [
|
||||
{
|
||||
format: "zip",
|
||||
source: path.join(destPath, targetBrowser),
|
||||
destination: `${path.join(
|
||||
destPath,
|
||||
targetBrowser
|
||||
)}.${getExtensionFileType(targetBrowser)}`,
|
||||
options: { zlib: { level: 6 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user