(boxContainer = ref)} />
36 | );
37 | boxContainer.appendChild(box);
38 | let button = boxContainer.getElementsByClassName(Shared.esgst.cancelButtonClass)[0];
39 | if (!button) return;
40 | button.addEventListener('click', () =>
41 | window.setTimeout(() => boxContainer.appendChild(box), 0)
42 | );
43 | }
44 | }
45 |
46 | const commentsReplyBoxOnTop = new CommentsReplyBoxOnTop();
47 |
48 | export { commentsReplyBoxOnTop };
49 |
--------------------------------------------------------------------------------
/messages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 100000,
4 | "timestamp": 1576724400000,
5 | "message": "Hi there! The ability to specify paths for features had been removed from ESGST in v8.5.9, but it's been added back since v8.5.11. Unfortunately, your previous path preferences could not be carried over, and I apologize for that, but if you have a backup from a version prior to v8.5.9, you should be able to restore them. Have a good day and thanks for using ESGST!"
6 | },
7 | {
8 | "id": 100001,
9 | "timestamp": 1577145600000,
10 | "dependency": "ge",
11 | "message": "Hi there! You can now extract giveaways from any URL with Giveaway Extractor. There isn't a UI for accessing this feature at the moment, but you can access it manually by going to [https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL](https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL). For example, to extract giveaways from the SteamGifts Community Christmas Calendar 2019, go to [https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/](https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/). You'll be asked to grant permission to all URLs. Happy holidays and thanks for using ESGST!"
12 | },
13 | {
14 | "id": 100002,
15 | "timestamp": 1595376000000,
16 | "message": "Testing **this** like [yeah](no)!"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/src/class/Session.js:
--------------------------------------------------------------------------------
1 | import { Namespaces } from '../constants/Namespaces';
2 |
3 | class ISession {
4 | /**
5 | * @param {string} text
6 | * @returns {string}
7 | */
8 | static extractXsrfToken(text) {
9 | return text.match(/xsrf_token=(.+)/)[1];
10 | }
11 | }
12 |
13 | class _Session extends ISession {
14 | constructor() {
15 | super();
16 |
17 | /** @type {ISessionCounters} */
18 | this.counters = {
19 | created: 0,
20 | level: {
21 | base: 0,
22 | full: 0,
23 | },
24 | messages: 0,
25 | points: 0,
26 | reputation: {
27 | negative: 0,
28 | positive: 0,
29 | },
30 | won: 0,
31 | wonDelivered: false,
32 | };
33 |
34 | /** @type {boolean} */
35 | this.isLoggedIn = false;
36 |
37 | /** @type {number} */
38 | this.namespace = Namespaces.SG;
39 |
40 | /** @type {import('../models/User').UserData | null} */
41 | this.user = null;
42 |
43 | /** @type {string} */
44 | this.xsrfToken = null;
45 | }
46 |
47 | init() {
48 | switch (window.location.hostname) {
49 | case 'www.steamgifts.com': {
50 | this.namespace = Namespaces.SG;
51 |
52 | break;
53 | }
54 |
55 | case 'www.steamtrades.com': {
56 | this.namespace = Namespaces.ST;
57 |
58 | break;
59 | }
60 |
61 | default: {
62 | throw 'Invalid namespace.';
63 | }
64 | }
65 | }
66 | }
67 |
68 | const Session = new _Session();
69 |
70 | export { ISession, Session };
71 |
--------------------------------------------------------------------------------
/src/modules/Discussions/MainPostSkipper.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const goToComment = common.goToComment.bind(common);
6 | class DiscussionsMainPostSkipper extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Skips to the comments of a discussion if you have used the pagination navigation. For
14 | example, if you enter a discussion and use the pagination navigation to go to page 2, on
15 | page 2 the feature will skip the main post and take you directly to the comments.
16 |
17 |
18 | ),
19 | id: 'mps',
20 | name: 'Main Post Skipper',
21 | sg: true,
22 | type: 'discussions',
23 | };
24 | }
25 |
26 | init() {
27 | if (
28 | !window.location.hash &&
29 | this.esgst.discussionPath &&
30 | this.esgst.pagination &&
31 | document.referrer.match(
32 | new RegExp(`/discussion/${[window.location.pathname.match(/^\/discussion\/(.+?)\//)[1]]}/`)
33 | )
34 | ) {
35 | const context = this.esgst.pagination.previousElementSibling;
36 | if (context.classList.contains('comments')) {
37 | goToComment('', context.firstElementChild.firstElementChild, true);
38 | }
39 | }
40 | }
41 | }
42 |
43 | const discussionsMainPostSkipper = new DiscussionsMainPostSkipper();
44 |
45 | export { discussionsMainPostSkipper };
46 |
--------------------------------------------------------------------------------
/src/assets/styles/patches/query-builder.css:
--------------------------------------------------------------------------------
1 | .rules-group-container {
2 | border: 1px solid #ccc !important;
3 | background: none !important;
4 | }
5 |
6 | .query-builder .btn-primary {
7 | text-shadow: none !important;
8 | }
9 |
10 | .query-builder .form-control {
11 | height: 22px !important;
12 | font-size: 12px !important;
13 | padding: 2px !important;
14 | }
15 |
16 | .query-builder .radio-default {
17 | margin-right: 5px !important;
18 | }
19 |
20 | .query-builder .radio-default input {
21 | display: none !important;
22 | }
23 |
24 | .query-builder .rules-group-container [data-resume='group'] {
25 | display: none !important;
26 | }
27 |
28 | .query-builder .rules-group-container[data-esgst-paused='true'] [data-resume='group'] {
29 | display: block !important;
30 | }
31 |
32 | .query-builder .rules-group-container[data-esgst-paused='true'] [data-pause='group'] {
33 | display: none !important;
34 | }
35 |
36 | .query-builder .rule-container [data-resume='rule'] {
37 | display: none !important;
38 | }
39 |
40 | .query-builder .rule-container[data-esgst-paused='true'] [data-resume='rule'] {
41 | display: block !important;
42 | }
43 |
44 | .query-builder .rule-container[data-esgst-paused='true'] [data-pause='rule'] {
45 | display: none !important;
46 | }
47 |
48 | .query-builder .rule-container[data-esgst-paused='true'] {
49 | opacity: 0.5 !important;
50 | }
51 |
52 | .query-builder .rules-group-container[data-esgst-paused='true'] {
53 | opacity: 0.5 !important;
54 | }
55 |
--------------------------------------------------------------------------------
/src/modules/General/FixedMainPageHeading.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { EventDispatcher } from '../../class/EventDispatcher';
3 | import { Module } from '../../class/Module';
4 | import { Events } from '../../constants/Events';
5 |
6 | class GeneralFixedMainPageHeading extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Keeps the main page heading (usually the first heading of the page, for example, the
14 | heading that says "Giveaways" in the main page) of any page at the top of the window
15 | while you scroll down the page.
16 |
17 |
18 | ),
19 | id: 'fmph',
20 | name: 'Fixed Main Page Heading',
21 | sg: true,
22 | st: true,
23 | type: 'general',
24 | };
25 | }
26 |
27 | init() {
28 | EventDispatcher.subscribe(Events.PAGE_HEADING_BUILD, (builtHeading) =>
29 | builtHeading.nodes.outer.classList.add('esgst-fmph')
30 | );
31 |
32 | if (!this.esgst.pageHeadings.length) {
33 | return;
34 | }
35 |
36 | this.esgst.style.insertAdjacentText(
37 | 'beforeend',
38 | `
39 | .esgst-fmph {
40 | top: ${this.esgst.pageTop}px;
41 | }
42 | `
43 | );
44 |
45 | for (const pageHeading of this.esgst.pageHeadings) {
46 | pageHeading.classList.add('esgst-fmph');
47 | }
48 | }
49 | }
50 |
51 | const generalFixedMainPageHeading = new GeneralFixedMainPageHeading();
52 |
53 | export { generalFixedMainPageHeading };
54 |
--------------------------------------------------------------------------------
/src/modules/Users/VisibleRealCV.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class UsersVisibleRealCV extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | -
11 | Displays the real sent/won CV next to the raw value in a user's{' '}
12 | profile page.
13 |
14 | -
15 | This also extends to , if you have that feature
16 | enabled.
17 |
18 | -
19 | With this feature disabled, you can still view the real CV, as provided by SteamGifts,
20 | by hovering over the raw value.
21 |
22 |
23 | ),
24 | id: 'vrcv',
25 | name: 'Visible Real CV',
26 | sg: true,
27 | type: 'users',
28 | featureMap: {
29 | profile: this.vrcv_add.bind(this),
30 | },
31 | };
32 | }
33 |
34 | vrcv_add(profile) {
35 | /**
36 | * @property realSentCV.toLocaleString
37 | * @property realWonCV.toLocaleString
38 | */
39 | profile.sentCvContainer.insertAdjacentText(
40 | 'beforeend',
41 | ` / $${profile.realSentCV.toLocaleString('en')}`
42 | );
43 | profile.wonCvContainer.insertAdjacentText(
44 | 'beforeend',
45 | ` / $${profile.realWonCV.toLocaleString('en')}`
46 | );
47 | }
48 | }
49 |
50 | const usersVisibleRealCV = new UsersVisibleRealCV();
51 |
52 | export { usersVisibleRealCV };
53 |
--------------------------------------------------------------------------------
/src/class/PermissionsUi.tsx:
--------------------------------------------------------------------------------
1 | import { browser } from '../browser';
2 | import { PageHeading } from '../components/PageHeading';
3 | import { DOM } from './DOM';
4 | import { permissions } from './Permissions';
5 | import { Popup } from './Popup';
6 |
7 | class _PermissionsUi {
8 | check = async (keys: string[]): Promise
=> {
9 | if (!browser.runtime.getURL) {
10 | return true;
11 | }
12 | if (await permissions.contains([keys])) {
13 | return true;
14 | }
15 | return new Promise((resolve) => {
16 | const popup = new Popup({ isTemp: true });
17 | PageHeading.create('sm', {
18 | breadcrumbs: ['Required Permissions'],
19 | }).insert(popup.description, 'beforeend');
20 | const scrollableArea = popup.getScrollable();
21 | scrollableArea.classList.add('markdown');
22 | DOM.insert(
23 | scrollableArea,
24 | 'atinner',
25 |
26 |
27 | In order to perform this action, you need to grant some permissions to the extension. Go{' '}
28 |
32 | here
33 | {' '}
34 | and click the "Grant" button to grant them.
35 |
36 | When you are done, close this popup to continue.
37 |
38 | );
39 | popup.onClose = async () => resolve(await permissions.contains([keys]));
40 | popup.open();
41 | });
42 | };
43 | }
44 |
45 | export const PermissionsUi = new _PermissionsUi();
46 |
--------------------------------------------------------------------------------
/src/modules/General/HiddenBlacklistStats.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GeneralHiddenBlacklistStats extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | -
11 | Hides the blacklist stats of your{' '}
12 | stats page.
13 |
14 |
15 | ),
16 | id: 'hbs',
17 | name: 'Hidden Blacklist Stats',
18 | sg: true,
19 | type: 'general',
20 | };
21 | }
22 |
23 | init() {
24 | if (!window.location.pathname.match(/^\/stats\/personal\/community/)) return;
25 |
26 | let chart = document.getElementsByClassName('chart')[4];
27 |
28 | // remove any "blacklist" text from the chart
29 | let heading = chart.firstElementChild;
30 | heading.lastElementChild.remove();
31 | heading.lastElementChild.remove();
32 | let subHeading = heading.nextElementSibling;
33 | subHeading.textContent = subHeading.textContent.replace(/and\sblacklists\s/, '');
34 |
35 | // create a new graph without the blacklist points
36 | let script = document.createElement('script');
37 | script.textContent = chart.previousElementSibling.textContent.replace(
38 | /,{name:\s"Blacklists".+?}/,
39 | ''
40 | );
41 | document.body.appendChild(script);
42 | script.remove();
43 | }
44 | }
45 |
46 | const generalHiddenBlacklistStats = new GeneralHiddenBlacklistStats();
47 |
48 | export { generalHiddenBlacklistStats };
49 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/GiveawayEndTimeHighlighter.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GiveawaysGiveawayEndTimeHighlighter extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Allows you to highlight the end time of a giveaway (in any page) by coloring it based on
13 | how many hours there are left.
14 |
15 |
16 | ),
17 | id: 'geth',
18 | name: 'Giveaway End Time Highlighter',
19 | sg: true,
20 | type: 'giveaways',
21 | featureMap: {
22 | giveaway: this.geth_getGiveaways.bind(this),
23 | },
24 | };
25 | }
26 |
27 | geth_getGiveaways(giveaways) {
28 | if (!Settings.get('geth_colors').length) {
29 | return;
30 | }
31 |
32 | for (const giveaway of giveaways) {
33 | if (!giveaway.started) {
34 | continue;
35 | }
36 |
37 | const hoursLeft = (giveaway.endTime - Date.now()) / 3600000;
38 | for (let i = Settings.get('geth_colors').length - 1; i > -1; i--) {
39 | const colors = Settings.get('geth_colors')[i];
40 | if (hoursLeft >= parseFloat(colors.lower) && hoursLeft <= parseFloat(colors.upper)) {
41 | (giveaway.endTimeColumn_gv || giveaway.endTimeColumn).style.color = colors.color;
42 | break;
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | const giveawaysGiveawayEndTimeHighlighter = new GiveawaysGiveawayEndTimeHighlighter();
50 |
51 | export { giveawaysGiveawayEndTimeHighlighter };
52 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/GiveawayLevelHighlighter.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GiveawaysGiveawayLevelHighlighter extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Highlights the level of a giveaway (in any page) by coloring it with the specified
13 | colors.
14 |
15 |
16 | ),
17 | featureMap: {
18 | giveaway: this.highlight.bind(this),
19 | },
20 | id: 'glh',
21 | name: 'Giveaway Level Highlighter',
22 | sg: true,
23 | type: 'giveaways',
24 | };
25 | }
26 |
27 | highlight(giveaways) {
28 | for (const giveaway of giveaways) {
29 | if (!giveaway.levelColumn) {
30 | continue;
31 | }
32 | const { color, bgColor } = Settings.get('glh_colors').filter(
33 | (colors) =>
34 | giveaway.level >= parseInt(colors.lower) && giveaway.level <= parseInt(colors.upper)
35 | )[0] || { color: undefined, bgColor: undefined };
36 | if (!color || !bgColor) {
37 | continue;
38 | }
39 | giveaway.levelColumn.setAttribute(
40 | 'style',
41 | `${color ? `color: ${color} !important;` : ''}${
42 | bgColor ? `background-color: ${bgColor};` : ''
43 | }`
44 | );
45 | giveaway.levelColumn.classList.add('esgst-glh-highlight');
46 | }
47 | }
48 | }
49 |
50 | const giveawaysGiveawayLevelHighlighter = new GiveawaysGiveawayLevelHighlighter();
51 |
52 | export { giveawaysGiveawayLevelHighlighter };
53 |
--------------------------------------------------------------------------------
/browser/messages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 100000,
4 | "message": "Hi there! The ability to specify paths for features had been removed from ESGST in v8.5.9, but it's been added back since v8.5.11. Unfortunately, your previous path preferences could not be carried over, and I apologize for that, but if you have a backup from a version prior to v8.5.9, you should be able to restore them. Have a good day and thanks for using ESGST!",
5 | "timestamp": 1576724400000
6 | },
7 | {
8 | "id": 100001,
9 | "message": [
10 | "Hi there! You can now extract giveaways from any URL with Giveaway Extractor. There isn't a UI for accessing this feature at the moment, but you can access it manually by going to ",
11 | [
12 | "a",
13 | {
14 | "class": "table__column__secondary-link",
15 | "href": "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL"
16 | },
17 | "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL"
18 | ],
19 | ". For example, to extract giveaways from the SteamGifts Community Christmas Calendar 2019, go to ",
20 | [
21 | "a",
22 | {
23 | "class": "table__column__secondary-link",
24 | "href": "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/"
25 | },
26 | "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/"
27 | ],
28 | ". You'll be asked to grant permission to all URLs. Happy holidays and thanks for using ESGST!"
29 | ],
30 | "timestamp": 1577145600000
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------
/src/class/Button.jsx:
--------------------------------------------------------------------------------
1 | import { Shared } from './Shared';
2 | import { DOM } from './DOM';
3 |
4 | class Button {
5 | constructor(context, position, details) {
6 | this.callbacks = details.callbacks;
7 | this.states = this.callbacks.length;
8 | this.icons = details.icons;
9 | this.id = details.id;
10 | this.index = details.index;
11 | this.titles = details.titles;
12 | DOM.insert(
13 | context,
14 | position,
15 | (this.button = ref)}>
16 | );
17 | // noinspection JSIgnoredPromiseFromCall
18 | this.change();
19 | return this;
20 | }
21 |
22 | async change(mainCallback, index = this.index, event) {
23 | if (index >= this.states) {
24 | index = 0;
25 | }
26 | this.index = index + 1;
27 | this.button.title = Shared.common.getFeatureTooltip(this.id, this.titles[index]);
28 | DOM.insert(this.button, 'atinner', );
29 | if (mainCallback) {
30 | if (await mainCallback(event)) {
31 | // noinspection JSIgnoredPromiseFromCall
32 | this.change();
33 | } else {
34 | DOM.insert(
35 | this.button,
36 | 'atinner',
37 |
38 | );
39 | }
40 | } else if (this.callbacks[index]) {
41 | this.button.firstElementChild.addEventListener(
42 | 'click',
43 | this.change.bind(this, this.callbacks[index], undefined)
44 | );
45 | }
46 | }
47 |
48 | async triggerCallback() {
49 | await this.change(this.callbacks[this.index - 1]);
50 | }
51 | }
52 |
53 | export { Button };
54 |
--------------------------------------------------------------------------------
/src/modules/Games/GameTags.jsx:
--------------------------------------------------------------------------------
1 | import { Tags } from '../Tags';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GamesGameTags extends Tags {
6 | constructor() {
7 | super('gt');
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Adds a button () next to a game's name (in any page) that
13 | allows you to save tags for the game (only visible to you).
14 |
15 | - You can press Enter to save the tags.
16 | - Each tag can be colored individually.
17 | -
18 | There is a button () in the tags popup that allows you to
19 | view a list with all of the tags that you have used ordered from most used to least
20 | used.
21 |
22 | -
23 | Adds a button ( ) to the
24 | page heading of this menu that allows you to manage all of the tags that have been
25 | saved.
26 |
27 |
28 | ),
29 | features: {
30 | gt_s: {
31 | name: 'Show tag suggestions while typing.',
32 | sg: true,
33 | st: true,
34 | },
35 | },
36 | id: 'gt',
37 | name: 'Game Tags',
38 | sg: true,
39 | type: 'games',
40 | };
41 | }
42 |
43 | init() {
44 | Shared.esgst.gameFeatures.push(this.tags_addButtons.bind(this));
45 | // noinspection JSIgnoredPromiseFromCall
46 | this.tags_getTags();
47 | }
48 | }
49 |
50 | const gamesGameTags = new GamesGameTags();
51 |
52 | export { gamesGameTags };
53 |
--------------------------------------------------------------------------------
/src/modules/Discussions/DiscussionTags.jsx:
--------------------------------------------------------------------------------
1 | import { Tags } from '../Tags';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class DiscussionsDiscussionTags extends Tags {
5 | constructor() {
6 | super('dt');
7 | this.info = {
8 | description: () => (
9 |
10 | -
11 | Adds a button ( ) next a discussion's title (in any page)
12 | that allows you to save tags for the discussion (only visible to you).
13 |
14 | - You can press Enter to save the tags.
15 | - Each tag can be colored individually.
16 | -
17 | There is a button ( ) in the tags popup that allows you to
18 | view a list with all of the tags that you have used ordered from most used to least
19 | used.
20 |
21 | -
22 | Adds a button ( ) to the
23 | page heading of this menu that allows you to manage all of the tags that have been
24 | saved.
25 |
26 |
27 | ),
28 | features: {
29 | dt_s: {
30 | name: 'Show tag suggestions while typing.',
31 | sg: true,
32 | },
33 | },
34 | id: 'dt',
35 | name: 'Discussion Tags',
36 | sg: true,
37 | type: 'discussions',
38 | };
39 | }
40 |
41 | init() {
42 | this.esgst.discussionFeatures.push(this.tags_addButtons.bind(this));
43 | // noinspection JSIgnoredPromiseFromCall
44 | this.tags_getTags();
45 | }
46 | }
47 |
48 | const discussionsDiscussionTags = new DiscussionsDiscussionTags();
49 |
50 | export { discussionsDiscussionTags };
51 |
--------------------------------------------------------------------------------
/src/modules/General/SearchMagnifyingGlassButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralSearchMagnifyingGlassButton extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Turns the magnifying glass icon () in the search field
13 | of any page into a button that submits the search when you click on it.
14 |
15 |
16 | ),
17 | id: 'smgb',
18 | name: 'Search Magnifying Glass Button',
19 | sg: true,
20 | type: 'general',
21 | };
22 | }
23 |
24 | init() {
25 | let buttons, i;
26 | buttons = document.querySelectorAll(
27 | `.sidebar__search-container .fa-search, .esgst-qgs-container .fa-search`
28 | );
29 | for (i = buttons.length - 1; i > -1; --i) {
30 | let button, input;
31 | button = buttons[i];
32 | input = button.previousElementSibling;
33 | button.classList.add('esgst-clickable');
34 | button.addEventListener('click', () => {
35 | let value = input.value.trim();
36 | if (value) {
37 | if (Settings.get('as') && value.match(/"|id:/)) {
38 | this.esgst.modules.giveawaysArchiveSearcher.as_openPage(input);
39 | } else {
40 | window.location.href = `${this.esgst.searchUrl.replace(/page=/, '')}q=${value}`;
41 | }
42 | }
43 | });
44 | }
45 | }
46 | }
47 |
48 | const generalSearchMagnifyingGlassButton = new GeneralSearchMagnifyingGlassButton();
49 |
50 | export { generalSearchMagnifyingGlassButton };
51 |
--------------------------------------------------------------------------------
/src/modules/Groups/GroupTags.jsx:
--------------------------------------------------------------------------------
1 | import { Tags } from '../Tags';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GroupsGroupTags extends Tags {
6 | constructor() {
7 | super('gpt');
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Adds a button () next to a group's name (in any page) that
13 | allows you to save tags for the group (only visible to you).
14 |
15 | - You can press Enter to save the tags.
16 | - Each tag can be colored individually.
17 | -
18 | There is a button () in the tags popup that allows you to
19 | view a list with all of the tags that you have used ordered from most used to least
20 | used.
21 |
22 | -
23 | Adds a button ( ) to the
24 | page heading of this menu that allows you to manage all of the tags that have been
25 | saved.
26 |
27 |
28 | ),
29 | features: {
30 | gpt_s: {
31 | name: 'Show tag suggestions while typing.',
32 | sg: true,
33 | st: true,
34 | },
35 | },
36 | id: 'gpt',
37 | name: 'Group Tags',
38 | sg: true,
39 | type: 'groups',
40 | };
41 | }
42 |
43 | init() {
44 | Shared.esgst.groupFeatures.push(this.tags_addButtons.bind(this));
45 | // noinspection JSIgnoredPromiseFromCall
46 | this.tags_getTags();
47 | }
48 | }
49 |
50 | const groupsGroupTags = new GroupsGroupTags();
51 |
52 | export { groupsGroupTags };
53 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/GiveawayCopyHighlighter.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GiveawaysGiveawayCopyHighlighter extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Highlights the number of copies next a giveaway's game name (in any page) by coloring it
13 | as red and changing the font to bold.
14 |
15 |
16 | ),
17 | featureMap: {
18 | giveaway: this.highlight.bind(this),
19 | },
20 | id: 'gch',
21 | name: 'Giveaway Copy Highlighter',
22 | sg: true,
23 | type: 'giveaways',
24 | };
25 | }
26 |
27 | highlight(giveaways) {
28 | for (const giveaway of giveaways) {
29 | if (!giveaway.copiesContainer) {
30 | continue;
31 | }
32 | const { color, bgColor } = Settings.get('gch_colors').filter(
33 | (colors) =>
34 | giveaway.copies >= parseInt(colors.lower) && giveaway.copies <= parseInt(colors.upper)
35 | )[0] || { color: undefined, bgColor: undefined };
36 | giveaway.copiesContainer.classList.add('esgst-bold');
37 | if (!color) {
38 | giveaway.copiesContainer.classList.add('esgst-red');
39 | continue;
40 | }
41 | giveaway.copiesContainer.style.color = color;
42 | if (!bgColor) {
43 | continue;
44 | }
45 | giveaway.copiesContainer.classList.add('esgst-gch-highlight');
46 | giveaway.copiesContainer.style.backgroundColor = bgColor;
47 | }
48 | }
49 | }
50 |
51 | const giveawaysGiveawayCopyHighlighter = new GiveawaysGiveawayCopyHighlighter();
52 |
53 | export { giveawaysGiveawayCopyHighlighter };
54 |
--------------------------------------------------------------------------------
/src/browser-sdk.js:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 | import { setBrowser } from './browser';
3 |
4 | const browser = {
5 | gm: null,
6 | runtime: {
7 | getBrowserInfo: () => Promise.resolve({ name: '?' }),
8 | onMessage: {
9 | addListener: (callback) => {
10 | // @ts-ignore
11 | self.port.on('esgstMessage', (obj) => callback(obj));
12 | },
13 | },
14 | getManifest: () => {
15 | return new Promise((resolve) => {
16 | browser.runtime
17 | .sendMessage({
18 | action: 'getPackageJson',
19 | })
20 | .then((result) => {
21 | resolve(JSON.parse(result));
22 | });
23 | });
24 | },
25 | sendMessage: (obj) => {
26 | return new Promise((resolve) => {
27 | obj.uuid = uuidv4();
28 | // @ts-ignore
29 | self.port.emit(obj.action, obj);
30 | // @ts-ignore
31 | self.port.on(`${obj.action}_${obj.uuid}_response`, function onResponse(result) {
32 | // @ts-ignore
33 | self.port.removeListener(`${obj.action}_${obj.uuid}_response`, 'onResponse');
34 | resolve(result);
35 | });
36 | });
37 | },
38 | },
39 | storage: {
40 | local: {
41 | get: async () => {
42 | return JSON.parse(
43 | await browser.runtime.sendMessage({
44 | action: 'getStorage',
45 | })
46 | );
47 | },
48 | remove: async (keys) => {
49 | await browser.runtime.sendMessage({
50 | action: 'delValues',
51 | keys: JSON.stringify(keys),
52 | });
53 | },
54 | set: async (values) => {
55 | await browser.runtime.sendMessage({
56 | action: 'setValues',
57 | values: JSON.stringify(values),
58 | });
59 | },
60 | },
61 | onChanged: {
62 | addListener: () => {},
63 | },
64 | },
65 | };
66 |
67 | setBrowser(browser);
68 |
--------------------------------------------------------------------------------
/src/modules/Users/LevelUpCalculator.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Shared } from '../../class/Shared';
4 | import { Settings } from '../../class/Settings';
5 | import { DOM } from '../../class/DOM';
6 |
7 | class UsersLevelUpCalculator extends Module {
8 | constructor() {
9 | super();
10 | this.info = {
11 | description: () => (
12 |
13 | - Shows how much real CV a user needs to level up in their profile page.
14 | -
15 | Uses the values mentioned on{' '}
16 | this discussion for the
17 | calculation.
18 |
19 |
20 | ),
21 | features: {
22 | luc_c: {
23 | name: 'Display current user level.',
24 | sg: true,
25 | },
26 | },
27 | id: 'luc',
28 | name: 'Level Up Calculator',
29 | sg: true,
30 | type: 'users',
31 | featureMap: {
32 | profile: this.luc_calculate.bind(this),
33 | },
34 | };
35 | }
36 |
37 | luc_calculate(profile) {
38 | for (const [index, value] of Shared.esgst.cvLevels.entries()) {
39 | const cvRounded = Math.round(profile.realSentCV);
40 | if (cvRounded < value) {
41 | DOM.insert(
42 | profile.levelRowRight,
43 | 'beforeend',
44 |
45 | {`(${Settings.get('luc_c') ? `${profile.level} / ` : ''}~$${Shared.common.round(
46 | value - cvRounded
47 | )} real CV to level ${index})`}
48 |
49 | );
50 | break;
51 | }
52 | }
53 | }
54 | }
55 |
56 | const usersLevelUpCalculator = new UsersLevelUpCalculator();
57 |
58 | export { usersLevelUpCalculator };
59 |
--------------------------------------------------------------------------------
/src/modules/General/AttachedImageLoader.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralAttachedImageLoader extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | conflicts: ['vai'],
10 | description: () => (
11 |
12 | -
13 | Only loads an attached image (in any page) when you click on its "View attached image"
14 | button, instead of loading it on page load, which should speed up page loads.
15 |
16 |
17 | ),
18 | id: 'ail',
19 | name: 'Attached Image Loader',
20 | sg: true,
21 | st: true,
22 | type: 'general',
23 | };
24 | }
25 |
26 | init() {
27 | if (Settings.get('vai')) return;
28 | this.esgst.endlessFeatures.push(this.ail_getImages.bind(this));
29 | }
30 |
31 | ail_getImages(context, main, source, endless) {
32 | const buttons = context.querySelectorAll(
33 | `${
34 | endless
35 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached`
36 | : '.comment__toggle-attached'
37 | }, ${
38 | endless
39 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached`
40 | : '.view_attached'
41 | }`
42 | );
43 | for (let i = 0, n = buttons.length; i < n; i++) {
44 | const button = buttons[i],
45 | image = button.nextElementSibling.firstElementChild,
46 | url = image.getAttribute('src');
47 | image.removeAttribute('src');
48 | button.addEventListener('click', image.setAttribute.bind(image, 'src', url));
49 | }
50 | }
51 | }
52 |
53 | const generalAttachedImageLoader = new GeneralAttachedImageLoader();
54 |
55 | export { generalAttachedImageLoader };
56 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true,
5 | greasemonkey: true,
6 | jquery: true,
7 | node: true,
8 | webextensions: true,
9 | },
10 | rules: {},
11 | overrides: [
12 | {
13 | files: ['**/*.{js,jsx}'],
14 | parserOptions: {
15 | sourceType: 'module',
16 | },
17 | extends: [
18 | 'eslint:recommended',
19 | 'plugin:react/recommended',
20 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.**
21 | ],
22 | rules: {
23 | quotes: [
24 | 'error',
25 | 'single',
26 | {
27 | avoidEscape: true,
28 | allowTemplateLiterals: false,
29 | },
30 | ],
31 | 'react/react-in-jsx-scope': 'off',
32 | },
33 | },
34 | {
35 | files: ['**/*.{ts,tsx}'],
36 | plugins: ['prefer-arrow'],
37 | extends: [
38 | 'eslint:recommended',
39 | 'plugin:react/recommended',
40 | 'plugin:@typescript-eslint/recommended',
41 | 'prettier/@typescript-eslint', // Disables TypeScript rules that conflict with Prettier.
42 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.**
43 | ],
44 | rules: {
45 | quotes: 'off',
46 | '@typescript-eslint/quotes': [
47 | 'error',
48 | 'single',
49 | {
50 | avoidEscape: true,
51 | allowTemplateLiterals: false,
52 | },
53 | ],
54 | 'prefer-arrow/prefer-arrow-functions': [
55 | 'error',
56 | {
57 | disallowPrototype: true,
58 | classPropertiesAllowed: true,
59 | },
60 | ],
61 | 'react/react-in-jsx-scope': 'off',
62 | },
63 | },
64 | ],
65 | settings: {
66 | react: {
67 | version: 'detect',
68 | },
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/PinnedGiveawaysButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const createElements = common.createElements.bind(common);
6 | class GiveawaysPinnedGiveawaysButton extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Modifies the arrow button in the pinned giveaways box of the main page so that you are
14 | able to collapse the box again after expanding it.
15 |
16 |
17 | ),
18 | id: 'pgb',
19 | name: 'Pinned Giveaways Button',
20 | sg: true,
21 | type: 'giveaways',
22 | };
23 | }
24 |
25 | init() {
26 | let button = document.getElementsByClassName('pinned-giveaways__button')[0];
27 | if (!button) return;
28 | const container = button.previousElementSibling;
29 | container.classList.add('esgst-pgb-container');
30 | button.remove();
31 | button = createElements(container, 'afterend', [
32 | {
33 | attributes: {
34 | class: 'esgst-pgb-button',
35 | },
36 | type: 'div',
37 | children: [
38 | {
39 | attributes: {
40 | class: 'esgst-pgb-icon fa fa-angle-down',
41 | },
42 | type: 'i',
43 | },
44 | ],
45 | },
46 | ]);
47 | const icon = button.firstElementChild;
48 | button.addEventListener('click', this.pgb_toggle.bind(this, container, icon));
49 | }
50 |
51 | pgb_toggle(container, icon) {
52 | container.classList.toggle('pinned-giveaways__inner-wrap--minimized');
53 | icon.classList.toggle('fa-angle-down');
54 | icon.classList.toggle('fa-angle-up');
55 | }
56 | }
57 |
58 | const giveawaysPinnedGiveawaysButton = new GiveawaysPinnedGiveawaysButton();
59 |
60 | export { giveawaysPinnedGiveawaysButton };
61 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/CommunityWishlistSearchLink.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const createElements = common.createElements.bind(common);
6 | class GiveawaysCommunityWishlistSearchLink extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Turns the numbers in the "Giveaways" column of any{' '}
14 | community wishlist page into
15 | links that allow you to search for all of the active giveaways for the game (that are
16 | visible to you).
17 |
18 |
19 | ),
20 | id: 'cwsl',
21 | name: 'Community Wishlist Search Link',
22 | sg: true,
23 | type: 'giveaways',
24 | };
25 | }
26 |
27 | init() {
28 | if (this.esgst.wishlistPath) {
29 | this.esgst.gameFeatures.push(this.cwsl_getGames.bind(this));
30 | }
31 | }
32 |
33 | cwsl_getGames(games, main) {
34 | if (!main) {
35 | return;
36 | }
37 | for (const game of games.all) {
38 | let giveawayCount = game.heading.parentElement.nextElementSibling.nextElementSibling;
39 | createElements(giveawayCount, 'atinner', [
40 | {
41 | attributes: {
42 | class: 'table__column__secondary-link',
43 | href: `/giveaways/search?${game.type.slice(0, -1)}=${game.id}`,
44 | },
45 | type: 'a',
46 | children: [
47 | ...Array.from(giveawayCount.childNodes).map((x) => {
48 | return {
49 | context: x,
50 | };
51 | }),
52 | ],
53 | },
54 | ]);
55 | }
56 | }
57 | }
58 |
59 | const giveawaysCommunityWishlistSearchLink = new GiveawaysCommunityWishlistSearchLink();
60 |
61 | export { giveawaysCommunityWishlistSearchLink };
62 |
--------------------------------------------------------------------------------
/src/modules/Groups/GroupHighlighter.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Shared } from '../../class/Shared';
4 | import { DOM } from '../../class/DOM';
5 |
6 | const getValue = common.getValue.bind(common);
7 | class GroupsGroupHighlighter extends Module {
8 | constructor() {
9 | super();
10 | this.info = {
11 | description: () => (
12 |
13 | - Adds a green background to a group that you are a member of (in any page).
14 |
15 | ),
16 | id: 'gh',
17 | name: 'Group Highlighter',
18 | sg: true,
19 | sync: 'Steam Groups',
20 | syncKeys: ['Groups'],
21 | type: 'groups',
22 | };
23 | }
24 |
25 | init() {
26 | if (Shared.common.isCurrentPath('Steam - Groups')) return;
27 | Shared.esgst.endlessFeatures.push(this.gh_highlightGroups.bind(this));
28 | }
29 |
30 | async gh_highlightGroups(context, main, source, endless) {
31 | const elements = context.querySelectorAll(
32 | `${
33 | endless
34 | ? `.esgst-es-page-${endless} .table__column__heading[href*="/group/"], .esgst-es-page-${endless}.table__column__heading[href*="/group/"]`
35 | : `.table__column__heading[href*="/group/"]`
36 | }`
37 | );
38 | if (!elements.length) return;
39 | const savedGroups = JSON.parse(getValue('groups', '[]'));
40 | for (let i = 0, n = elements.length; i < n; ++i) {
41 | const element = elements[i],
42 | code = element.getAttribute('href').match(/\/group\/(.+?)\//)[1];
43 | let j;
44 | for (j = savedGroups.length - 1; j >= 0 && savedGroups[j].code !== code; --j) {}
45 | if (j >= 0 && savedGroups[j].member) {
46 | element.closest('.table__row-outer-wrap').classList.add('esgst-gh-highlight');
47 | }
48 | }
49 | }
50 | }
51 |
52 | const groupsGroupHighlighter = new GroupsGroupHighlighter();
53 |
54 | export { groupsGroupHighlighter };
55 |
--------------------------------------------------------------------------------
/src/modules/Users/UserTags.jsx:
--------------------------------------------------------------------------------
1 | import { Tags } from '../Tags';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class UsersUserTags extends Tags {
6 | constructor() {
7 | super('ut');
8 | this.info = {
9 | description: () => (
10 |
11 | -
12 | Adds a button () next a user's username (in any page) that
13 | allows you to save tags for the user (only visible to you).
14 |
15 | - You can press Enter to save the tags.
16 | - Each tag can be colored individually.
17 | -
18 | There is a button () in the tags popup that allows you to
19 | view a list with all of the tags that you have used ordered from most used to least
20 | used.
21 |
22 | -
23 | Adds a button ( ) to the
24 | page heading of this menu that allows you to manage all of the tags that have been
25 | saved.
26 |
27 | -
28 | This feature is recommended for cases where you want to associate a short text with a
29 | user, since the tags are displayed next to their username.For a long text, check
30 | .
31 |
32 |
33 | ),
34 | features: {
35 | ut_s: {
36 | name: 'Show tag suggestions while typing.',
37 | sg: true,
38 | st: true,
39 | },
40 | },
41 | id: 'ut',
42 | name: 'User Tags',
43 | sg: true,
44 | st: true,
45 | type: 'users',
46 | };
47 | }
48 |
49 | init() {
50 | Shared.esgst.userFeatures.push(this.tags_addButtons.bind(this));
51 | // noinspection JSIgnoredPromiseFromCall
52 | this.tags_getTags();
53 | }
54 | }
55 |
56 | const usersUserTags = new UsersUserTags();
57 |
58 | export { usersUserTags };
59 |
--------------------------------------------------------------------------------
/src/modules/Users/SteamFriendsIndicator.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Shared } from '../../class/Shared';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | class UsersSteamFriendsIndicator extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Adds an icon () next to the a user's username (in any
14 | page) to indicate that they are on your Steam friends list.
15 |
16 | - If you hover over the icon, it shows the date when you became friends.
17 |
18 | ),
19 | id: 'sfi',
20 | inputItems: [
21 | {
22 | id: 'sfi_icon',
23 | prefix: `Icon: `,
24 | },
25 | ],
26 | name: 'Steam Friends Indicator',
27 | sg: true,
28 | st: true,
29 | sync: 'Steam Friends',
30 | syncKeys: ['SteamFriends'],
31 | type: 'users',
32 | featureMap: {
33 | user: this.addIcons.bind(this),
34 | },
35 | };
36 | }
37 |
38 | addIcons(users) {
39 | for (const user of users) {
40 | if (
41 | user.saved &&
42 | user.saved.steamFriend &&
43 | !user.context.parentElement.querySelector('.esgst-sfi-icon')
44 | ) {
45 | DOM.insert(
46 | user.context,
47 | 'afterend',
48 |
57 |
58 |
59 | );
60 | }
61 | }
62 | }
63 | }
64 |
65 | const usersSteamFriendsIndicator = new UsersSteamFriendsIndicator();
66 |
67 | export { usersSteamFriendsIndicator };
68 |
--------------------------------------------------------------------------------
/src/modules/General/VisibleAttachedImages.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralVisibleAttachedImages extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | conflicts: ['ail'],
10 | description: () => (
11 |
12 | -
13 | Displays all of the attached images (in any page) by default so that you do not need to
14 | click on "View attached image" to view them.
15 |
16 |
17 | ),
18 | features: {
19 | vai_gifv: {
20 | name: 'Rename .gifv images to .gif so that they are properly attached.',
21 | sg: true,
22 | st: true,
23 | },
24 | },
25 | id: 'vai',
26 | name: 'Visible Attached Images',
27 | sg: true,
28 | st: true,
29 | type: 'general',
30 | featureMap: {
31 | endless: this.vai_getImages.bind(this),
32 | },
33 | };
34 | }
35 |
36 | vai_getImages(context, main, source, endless) {
37 | let buttons = context.querySelectorAll(
38 | `${
39 | endless
40 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached`
41 | : '.comment__toggle-attached'
42 | }, ${
43 | endless
44 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached`
45 | : '.view_attached'
46 | }`
47 | );
48 | for (let i = 0, n = buttons.length; i < n; i++) {
49 | let button = buttons[i];
50 | let image = button.nextElementSibling.firstElementChild;
51 | let url = image.getAttribute('src');
52 | if (url && Settings.get('vai_gifv')) {
53 | url = url.replace(/\.gifv/, '.gif');
54 | image.setAttribute('src', url);
55 | }
56 | image.classList.remove('is_hidden', 'is-hidden');
57 | }
58 | }
59 | }
60 |
61 | const generalVisibleAttachedImages = new GeneralVisibleAttachedImages();
62 |
63 | export { generalVisibleAttachedImages };
64 |
--------------------------------------------------------------------------------
/.eslintrc.typed.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true,
5 | greasemonkey: true,
6 | jquery: true,
7 | node: true,
8 | webextensions: true,
9 | },
10 | rules: {},
11 | overrides: [
12 | {
13 | files: ['**/*.{js,jsx}'],
14 | parserOptions: {
15 | sourceType: 'module',
16 | },
17 | extends: [
18 | 'eslint:recommended',
19 | 'plugin:react/recommended',
20 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.**
21 | ],
22 | rules: {
23 | quotes: [
24 | 'error',
25 | 'single',
26 | {
27 | avoidEscape: true,
28 | allowTemplateLiterals: false,
29 | },
30 | ],
31 | 'react/react-in-jsx-scope': 'off',
32 | },
33 | },
34 | {
35 | files: ['**/*.{ts,tsx}'],
36 | parserOptions: {
37 | tsconfigRootDir: __dirname,
38 | project: ['./tsconfig.json'],
39 | },
40 | plugins: ['prefer-arrow'],
41 | extends: [
42 | 'eslint:recommended',
43 | 'plugin:react/recommended',
44 | 'plugin:@typescript-eslint/recommended',
45 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
46 | 'prettier/@typescript-eslint', // Disables TypeScript rules that conflict with Prettier.
47 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.**
48 | ],
49 | rules: {
50 | quotes: 'off',
51 | '@typescript-eslint/quotes': [
52 | 'error',
53 | 'single',
54 | {
55 | avoidEscape: true,
56 | allowTemplateLiterals: false,
57 | },
58 | ],
59 | 'prefer-arrow/prefer-arrow-functions': [
60 | 'error',
61 | {
62 | disallowPrototype: true,
63 | classPropertiesAllowed: true,
64 | },
65 | ],
66 | 'react/react-in-jsx-scope': 'off',
67 | },
68 | },
69 | ],
70 | settings: {
71 | react: {
72 | version: 'detect',
73 | },
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/src/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../class/DOM';
2 | import { Settings } from '../class/Settings';
3 | import { Shared } from '../class/Shared';
4 |
5 | class _Collapsible {
6 | create = (header: HTMLElement, body: HTMLElement, id?: string): HTMLElement => {
7 | const [collapseNode] = DOM.insert(
8 | header,
9 | 'afterbegin',
10 |
11 | {' '}
12 |
13 | );
14 | const [expandNode] = DOM.insert(
15 | header,
16 | 'afterbegin',
17 |
18 | {' '}
19 |
20 | );
21 | collapseNode.addEventListener('click', () => this.collapse(collapseNode, expandNode, body, id));
22 | expandNode.addEventListener('click', () => this.expand(collapseNode, expandNode, body, id));
23 | if (id && Settings.get(`${id}_collapsed`)) {
24 | this.collapse(collapseNode, expandNode, body);
25 | }
26 | return (
27 |
28 | {header}
29 | {body}
30 |
31 | );
32 | };
33 |
34 | collapse = async (
35 | collapseNode: HTMLElement,
36 | expandNode: HTMLElement,
37 | body: HTMLElement,
38 | id?: string
39 | ): Promise => {
40 | collapseNode.classList.add('esgst-hidden');
41 | expandNode.classList.remove('esgst-hidden');
42 | body.classList.add('esgst-hidden');
43 | if (id) {
44 | await Shared.common.setSetting(`${id}_collapsed`, true);
45 | }
46 | };
47 |
48 | expand = async (
49 | collapseNode: HTMLElement,
50 | expandNode: HTMLElement,
51 | body: HTMLElement,
52 | id?: string
53 | ): Promise => {
54 | expandNode.classList.add('esgst-hidden');
55 | collapseNode.classList.remove('esgst-hidden');
56 | body.classList.remove('esgst-hidden');
57 | if (id) {
58 | await Shared.common.setSetting(`${id}_collapsed`, false);
59 | }
60 | };
61 | }
62 |
63 | export const Collapsible = new _Collapsible();
64 |
--------------------------------------------------------------------------------
/src/modules/General/TimeToPointCapCalculator.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { EventDispatcher } from '../../class/EventDispatcher';
5 | import { Events } from '../../constants/Events';
6 | import { Session } from '../../class/Session';
7 | import { Shared } from '../../class/Shared';
8 | import { DOM } from '../../class/DOM';
9 |
10 | class GeneralTimeToPointCapCalculator extends Module {
11 | constructor() {
12 | super();
13 | this.info = {
14 | description: () => (
15 |
16 | -
17 | If you have less than 400P and you hover over the number of points at the header of any
18 | page, it shows how much time you have to wait until you have 400P.
19 |
20 |
21 | ),
22 | features: {
23 | ttpcc_a: {
24 | name: 'Show time alongside points.',
25 | sg: true,
26 | },
27 | },
28 | id: 'ttpcc',
29 | name: 'Time To Point Cap Calculator',
30 | sg: true,
31 | type: 'general',
32 | };
33 | }
34 |
35 | init() {
36 | EventDispatcher.subscribe(Events.POINTS_UPDATED, this.update.bind(this));
37 |
38 | this.update(null, Session.counters.points);
39 | }
40 |
41 | update(oldPoints, newPoints) {
42 | if (newPoints >= 400) {
43 | return;
44 | }
45 |
46 | let nextRefresh = 60 - new Date().getMinutes();
47 |
48 | while (nextRefresh > 15) {
49 | nextRefresh -= 15;
50 | }
51 |
52 | const time = this.esgst.modules.giveawaysTimeToEnterCalculator.ttec_getTime(
53 | Math.round((nextRefresh + 15 * Math.floor((400 - newPoints) / 6)) * 100) / 100
54 | );
55 |
56 | const pointsNode = Shared.header.buttonContainers['account'].nodes.points;
57 | pointsNode.textContent = `${newPoints.toLocaleString('en-US')}${
58 | Settings.get('ttpcc_a') ? `P / ${time} to 400` : ''
59 | }`;
60 | pointsNode.title = common.getFeatureTooltip('ttpcc', `${time} to 400P`);
61 | }
62 | }
63 |
64 | const generalTimeToPointCapCalculator = new GeneralTimeToPointCapCalculator();
65 |
66 | export { generalTimeToPointCapCalculator };
67 |
--------------------------------------------------------------------------------
/src/modules/General/PageLoadTimestamp.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import dateFns_format from 'date-fns/format';
3 | import { common } from '../Common';
4 | import { Settings } from '../../class/Settings';
5 | import { DOM } from '../../class/DOM';
6 | import { Shared } from '../../class/Shared';
7 |
8 | class GeneralPageLoadTimestamp extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 | -
15 | Adds a timestamp indicating when the page was loaded to any page, in the preferred
16 | location.
17 |
18 |
19 | ),
20 | id: 'plt',
21 | name: 'Page Load Timestamp',
22 | inputItems: [
23 | {
24 | id: 'plt_format',
25 | prefix: `Timestamp format: `,
26 | tooltip: `ESGST uses date-fns v2.0.0-alpha.25, so check the accepted tokens here: https://date-fns.org/v2.0.0-alpha.25/docs/Getting-Started.`,
27 | },
28 | ],
29 | options: {
30 | title: `Position:`,
31 | values: ['Sidebar', 'Footer'],
32 | },
33 | sg: true,
34 | st: true,
35 | type: 'general',
36 | };
37 | }
38 |
39 | init() {
40 | const timestamp = dateFns_format(
41 | Date.now(),
42 | Settings.get('plt_format') || `MMM dd, yyyy, HH:mm:ss`
43 | );
44 | switch (Settings.get('plt_index')) {
45 | case 0:
46 | if (this.esgst.sidebar) {
47 | DOM.insert(
48 | this.esgst.sidebar,
49 | 'afterbegin',
50 |
51 | Page Load Timestamp
52 | {timestamp}
53 |
54 | );
55 | break;
56 | }
57 | case 1: {
58 | if (!Shared.footer) {
59 | return;
60 | }
61 |
62 | const linkContainer = Shared.footer.addLinkContainer({
63 | name: `Page loaded on ${timestamp}`,
64 | side: 'left',
65 | });
66 |
67 | linkContainer.nodes.outer.classList.add('esgst-plt');
68 |
69 | break;
70 | }
71 | }
72 | }
73 | }
74 |
75 | const generalPageLoadTimestamp = new GeneralPageLoadTimestamp();
76 |
77 | export { generalPageLoadTimestamp };
78 |
--------------------------------------------------------------------------------
/src/modules/Users/RealWonSentCVLink.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | const createElements = common.createElements.bind(common),
7 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
8 | class UsersRealWonSentCVLink extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 | -
15 | Turns "Gifts Won" and "Gifts Sent" in a user's{' '}
16 | profile page into links that take you
17 | to their real won/sent CV pages on SGTools.
18 |
19 |
20 | ),
21 | features: {
22 | rwscvl_r: {
23 | name: `Link SGTools' reverse pages (from newest to oldest).`,
24 | sg: true,
25 | },
26 | },
27 | id: 'rwscvl',
28 | name: 'Real Won/Sent CV Link',
29 | sg: true,
30 | type: 'users',
31 | featureMap: {
32 | profile: this.rwscvl_add.bind(this),
33 | },
34 | };
35 | }
36 |
37 | rwscvl_add(profile) {
38 | let sentUrl, wonUrl;
39 | wonUrl = `http://www.sgtools.info/won/${profile.username}`;
40 | sentUrl = `http://www.sgtools.info/sent/${profile.username}`;
41 | if (Settings.get('rwscvl_r')) {
42 | wonUrl += '/newestfirst';
43 | sentUrl += '/newestfirst';
44 | }
45 | createElements(profile.wonRowLeft, 'atinner', [
46 | {
47 | attributes: {
48 | class: 'esgst-rwscvl-link',
49 | href: wonUrl,
50 | target: '_blank',
51 | title: getFeatureTooltip('rwscvl'),
52 | },
53 | text: 'Gifts Won',
54 | type: 'a',
55 | },
56 | ]);
57 | createElements(profile.sentRowLeft, 'atinner', [
58 | {
59 | attributes: {
60 | class: 'esgst-rwscvl-link',
61 | href: sentUrl,
62 | target: '_blank',
63 | title: getFeatureTooltip('rwscvl'),
64 | },
65 | text: 'Gifts Sent',
66 | type: 'a',
67 | },
68 | ]);
69 | }
70 | }
71 |
72 | const usersRealWonSentCVLink = new UsersRealWonSentCVLink();
73 |
74 | export { usersRealWonSentCVLink };
75 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/HiddenGamesEnterButtonDisabler.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const createElements = common.createElements.bind(common);
6 | class GiveawaysHiddenGamesEnterButtonDisabler extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Disables the enter button of any giveaway if you have hidden the game on SteamGifts so
14 | that you do not accidentally enter it.
15 |
16 |
17 | ),
18 | id: 'hgebd',
19 | name: "Hidden Game's Enter Button Disabler",
20 | sg: true,
21 | sync: 'Hidden Games',
22 | syncKeys: ['HiddenGames'],
23 | type: 'giveaways',
24 | };
25 | }
26 |
27 | init() {
28 | if (!this.esgst.giveawayPath || document.getElementsByClassName('table--summary')[0]) {
29 | return;
30 | }
31 | const hideButton = document.getElementsByClassName('featured__giveaway__hide')[0];
32 | if (
33 | (this.esgst.enterGiveawayButton ||
34 | (this.esgst.giveawayErrorButton &&
35 | !this.esgst.giveawayErrorButton.textContent.match(/Exists\sin\sAccount/))) &&
36 | !hideButton
37 | ) {
38 | const parent = (this.esgst.enterGiveawayButton || this.esgst.giveawayErrorButton)
39 | .parentElement;
40 | if (this.esgst.enterGiveawayButton) {
41 | this.esgst.enterGiveawayButton.remove();
42 | }
43 | if (this.esgst.giveawayErrorButton) {
44 | this.esgst.giveawayErrorButton.remove();
45 | }
46 | createElements(parent, 'afterbegin', [
47 | {
48 | attributes: {
49 | class: 'sidebar__error is-disabled',
50 | },
51 | type: 'div',
52 | children: [
53 | {
54 | attributes: {
55 | class: 'fa fa-exclamation-circle',
56 | },
57 | type: 'i',
58 | },
59 | {
60 | text: ' Hidden Game',
61 | type: 'node',
62 | },
63 | ],
64 | },
65 | ]);
66 | }
67 | }
68 | }
69 |
70 | const giveawaysHiddenGamesEnterButtonDisabler = new GiveawaysHiddenGamesEnterButtonDisabler();
71 |
72 | export { giveawaysHiddenGamesEnterButtonDisabler };
73 |
--------------------------------------------------------------------------------
/src/modules/General/PaginationNavigationOnTop.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | const getFeatureTooltip = common.getFeatureTooltip.bind(common);
7 | class GeneralPaginationNavigationOnTop extends Module {
8 | constructor() {
9 | super();
10 | this.info = {
11 | description: () => (
12 |
13 | - Moves the pagination navigation of any page to the main page heading of the page.
14 |
15 | ),
16 | features: {
17 | pnot_s: {
18 | name: `Enable simplified view (will show only the numbers and arrows).`,
19 | sg: true,
20 | st: true,
21 | },
22 | },
23 | id: 'pnot',
24 | name: 'Pagination Navigation On Top',
25 | sg: true,
26 | st: true,
27 | type: 'general',
28 | };
29 | }
30 |
31 | init() {
32 | if (!this.esgst.paginationNavigation || !this.esgst.mainPageHeading) return;
33 |
34 | if (this.esgst.st) {
35 | this.esgst.paginationNavigation.classList.add('page_heading_btn');
36 | }
37 | this.esgst.paginationNavigation.title = getFeatureTooltip('pnot');
38 | this.pnot_simplify();
39 | DOM.insert(
40 | this.esgst.mainPageHeading.querySelector(
41 | `.page__heading__breadcrumbs, .page_heading_breadcrumbs`
42 | ),
43 | 'afterend',
44 | this.esgst.paginationNavigation
45 | );
46 | }
47 |
48 | pnot_simplify() {
49 | if (Settings.get('pnot') && Settings.get('pnot_s')) {
50 | const elements = this.esgst.paginationNavigation.querySelectorAll('span');
51 | // @ts-ignore
52 | for (const element of elements) {
53 | if (element.textContent.match(/[A-Za-z]+/)) {
54 | element.textContent = element.textContent.replace(/[A-Za-z]+/g, '');
55 | if (element.previousElementSibling) {
56 | element.appendChild(element.previousElementSibling);
57 | }
58 | if (element.nextElementSibling) {
59 | element.appendChild(element.nextElementSibling);
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | const generalPaginationNavigationOnTop = new GeneralPaginationNavigationOnTop();
68 |
69 | export { generalPaginationNavigationOnTop };
70 |
--------------------------------------------------------------------------------
/src/modules/Discussions/RefreshActiveDiscussionsButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | const checkMissingDiscussions = common.checkMissingDiscussions.bind(common),
7 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
8 | class DiscussionsRefreshActiveDiscussionsButton extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 | -
15 | Adds a button () to the page heading of the active
16 | discussions (in the main page) that allows you to refresh the active discussions without
17 | having to refresh the entire page.
18 |
19 |
20 | ),
21 | id: 'radb',
22 | name: 'Refresh Active Discussions Button',
23 | sg: true,
24 | type: 'discussions',
25 | };
26 | }
27 |
28 | radb_addButtons() {
29 | let elements, i;
30 | elements = this.esgst.activeDiscussions.querySelectorAll(
31 | `.block_header, .esgst-heading-button`
32 | );
33 | for (i = elements.length - 1; i > -1; --i) {
34 | DOM.insert(
35 | elements[i],
36 | 'beforebegin',
37 | {
41 | let icon = event.currentTarget.firstElementChild;
42 | icon.classList.add('fa-spin');
43 | if (Settings.get('oadd')) {
44 | // noinspection JSIgnoredPromiseFromCall
45 | this.esgst.modules.discussionsOldActiveDiscussionsDesign.oadd_load(true, () => {
46 | icon.classList.remove('fa-spin');
47 | });
48 | } else {
49 | checkMissingDiscussions(true, () => {
50 | icon.classList.remove('fa-spin');
51 | });
52 | }
53 | }}
54 | >
55 |
56 |
57 | );
58 | }
59 | }
60 | }
61 |
62 | const discussionsRefreshActiveDiscussionsButton = new DiscussionsRefreshActiveDiscussionsButton();
63 |
64 | export { discussionsRefreshActiveDiscussionsButton };
65 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/GiveawayWinnersLink.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const createElements = common.createElements.bind(common);
6 | class GiveawaysGiveawayWinnersLink extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Adds a link next to an ended giveaway's "Entries" link (in any page) that shows how many
14 | winners the giveaway has and takes you to the giveaway's{' '}
15 | winners page.
16 |
17 |
18 | ),
19 | id: 'gwl',
20 | name: 'Giveaway Winners Link',
21 | sg: true,
22 | type: 'giveaways',
23 | featureMap: {
24 | giveaway: this.gwl_addLinks.bind(this),
25 | },
26 | };
27 | }
28 |
29 | gwl_addLinks(giveaways, main) {
30 | if (
31 | ((!this.esgst.createdPath &&
32 | !this.esgst.enteredPath &&
33 | !this.esgst.wonPath &&
34 | !this.esgst.giveawayPath &&
35 | !this.esgst.archivePath) ||
36 | main) &&
37 | (this.esgst.giveawayPath ||
38 | this.esgst.createdPath ||
39 | this.esgst.enteredPath ||
40 | this.esgst.wonPath ||
41 | this.esgst.archivePath)
42 | )
43 | return;
44 | giveaways.forEach((giveaway) => {
45 | if (giveaway.innerWrap.getElementsByClassName('esgst-gwl')[0] || !giveaway.ended) return;
46 | const attributes = {
47 | class: 'esgst-gwl',
48 | ['data-draggable-id']: 'winners_count',
49 | };
50 | if (giveaway.url) {
51 | attributes.href = `${giveaway.url}/winners`;
52 | }
53 | createElements(giveaway.entriesLink, 'afterend', [
54 | {
55 | attributes,
56 | type: 'a',
57 | children: [
58 | {
59 | attributes: {
60 | class: 'fa fa-trophy',
61 | },
62 | type: 'i',
63 | },
64 | {
65 | text: `${giveaway.numWinners} winners`,
66 | type: 'span',
67 | },
68 | ],
69 | },
70 | ]);
71 | });
72 | }
73 | }
74 |
75 | const giveawaysGiveawayWinnersLink = new GiveawaysGiveawayWinnersLink();
76 |
77 | export { giveawaysGiveawayWinnersLink };
78 |
--------------------------------------------------------------------------------
/src/modules/Users/SteamGiftsProfileButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Shared } from '../../class/Shared';
4 | import { DOM } from '../../class/DOM';
5 |
6 | const createElements = common.createElements.bind(common),
7 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
8 | class UsersSteamGiftsProfileButton extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 | -
15 | Adds a button next to the "Visit Steam Profile" button of a user's{' '}
16 | profile page that
17 | allows you to go to their SteamGifts profile page.
18 |
19 |
20 | ),
21 | id: 'sgpb',
22 | name: 'SteamGifts Profile Button',
23 | st: true,
24 | type: 'users',
25 | };
26 | }
27 |
28 | init() {
29 | if (!Shared.esgst.userPath) return;
30 | Shared.esgst.profileFeatures.push(this.sgpb_add.bind(this));
31 | }
32 |
33 | sgpb_add(profile) {
34 | let button;
35 | button = createElements(profile.steamButtonContainer, 'beforeend', [
36 | {
37 | attributes: {
38 | class: 'esgst-sgpb-container',
39 | title: getFeatureTooltip('sgpb'),
40 | },
41 | type: 'div',
42 | children: [
43 | {
44 | attributes: {
45 | class: 'esgst-sgpb-button',
46 | href: `https://www.steamgifts.com/go/user/${profile.steamId}`,
47 | rel: 'nofollow',
48 | target: '_blank',
49 | },
50 | type: 'a',
51 | children: [
52 | {
53 | attributes: {
54 | class: 'fa',
55 | },
56 | type: 'i',
57 | children: [
58 | {
59 | attributes: {
60 | src: Shared.esgst.sgIcon,
61 | },
62 | type: 'img',
63 | },
64 | ],
65 | },
66 | {
67 | text: 'Visit SteamGifts Profile',
68 | type: 'span',
69 | },
70 | ],
71 | },
72 | ],
73 | },
74 | ]);
75 | button.insertBefore(profile.steamButton, button.firstElementChild);
76 | }
77 | }
78 |
79 | const usersSteamGiftsProfileButton = new UsersSteamGiftsProfileButton();
80 |
81 | export { usersSteamGiftsProfileButton };
82 |
--------------------------------------------------------------------------------
/src/modules/Users/UserLinks.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | class UsersUserLinks extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | - Allows you to add custom links next to a user's username in their profile page.
13 | -
14 | Can be used in other pages through .
15 |
16 | -
17 | Comes by default with 5 links to BLAEO, Playing Appreciated, Touhou Giveaways, AStats
18 | and SteamRep.
19 |
20 |
21 | ),
22 | id: 'ul',
23 | name: 'User Links',
24 | sg: true,
25 | type: 'users',
26 | featureMap: {
27 | profile: this.ul_add.bind(this),
28 | },
29 | };
30 | }
31 |
32 | ul_add(profile) {
33 | const items = [];
34 | const iconRegex = /^(fa-.+?)($|\s)/;
35 | const imageRegex = /^(https?:\/\/.+?)($|\s)/;
36 | const textRegex = /^(.+?)($|\s(fa-|https?:\/\/))/;
37 | for (const link of Settings.get('ul_links')) {
38 | const children = [];
39 | let label = link.label;
40 | while (label) {
41 | const icon = label.match(iconRegex);
42 | if (icon) {
43 | label = label.replace(iconRegex, '');
44 | children.push();
45 | continue;
46 | }
47 | const image = label.match(imageRegex);
48 | if (image) {
49 | label = label.replace(imageRegex, '');
50 | children.push(
51 |
52 | );
53 | continue;
54 | }
55 | const text = label.match(textRegex);
56 | if (text) {
57 | label = label.replace(textRegex, `$3`);
58 | children.push(text[1]);
59 | }
60 | }
61 | items.push(
62 |
68 | {children}
69 |
70 | );
71 | }
72 | DOM.insert(profile.heading, 'beforeend', {items});
73 | }
74 | }
75 |
76 | const usersUserLinks = new UsersUserLinks();
77 |
78 | export { usersUserLinks };
79 |
--------------------------------------------------------------------------------
/src/modules/Comments/ReplyMentionLink.tsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class CommentsReplyMentionLink extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | -
11 | Adds a link (@user) next to a reply's "Permalink" (in any page) that mentions the user
12 | being replied to and links to their comment.
13 |
14 | -
15 | This feature is useful for conversations that have very deep nesting levels, which makes
16 | it impossible to know who replied to whom.
17 |
18 |
19 | ),
20 | id: 'rml',
21 | name: 'Reply Mention Link',
22 | sg: true,
23 | st: true,
24 | type: 'comments',
25 | featureMap: {
26 | commentV2: this.addLinks.bind(this),
27 | },
28 | };
29 | }
30 |
31 | addLinks(comments: IComment[]) {
32 | for (const comment of comments) {
33 | this.addLink(comment);
34 | this.addLinks(comment.children);
35 | }
36 | }
37 |
38 | addLink(comment: IComment) {
39 | if (comment.parent && !comment.nodes.rmlLink) {
40 | DOM.insert(
41 | comment.nodes.actions,
42 | 'beforeend',
43 | (comment.nodes.rmlLink = ref)}
47 | >
48 | {`@${comment.data.isDeleted ? '[Deleted]' : comment.parent.author.data.username}`}
49 |
50 | );
51 | }
52 | }
53 |
54 | rml_addLink(parent: HTMLElement, children: HTMLElement[]) {
55 | const authorUsername = parent
56 | .querySelector('.comment__username, .author_name')
57 | .textContent.trim();
58 | const commentCode = parent.id;
59 | for (const child of children) {
60 | const actions = child.querySelector('.comment__actions, .action_list');
61 | const rmlLink = actions.querySelector('.esgst-rml-link');
62 | if (rmlLink) {
63 | rmlLink.textContent = `@${authorUsername}`;
64 | } else {
65 | DOM.insert(
66 | actions,
67 | 'beforeend',
68 |
69 | {`@${authorUsername}`}
70 |
71 | );
72 | }
73 | }
74 | }
75 | }
76 |
77 | const commentsReplyMentionLink = new CommentsReplyMentionLink();
78 |
79 | export { commentsReplyMentionLink };
80 |
--------------------------------------------------------------------------------
/src/modules/Comments/ReplyBoxPopup.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { Module } from '../../class/Module';
3 | import { Popup } from '../../class/Popup';
4 | import { Shared } from '../../class/Shared';
5 | import { Button } from '../../components/Button';
6 |
7 | class CommentsReplyBoxPopup extends Module {
8 | constructor() {
9 | super();
10 | this.info = {
11 | description: () => (
12 |
13 | -
14 | Adds a button () to the main page heading of any page
15 | that allows you to add comments to the page through a popup.
16 |
17 | -
18 | This feature is useful if you have enabled,
19 | which allows you to add comments to the page from any scrolling position.
20 |
21 |
22 | ),
23 | id: 'rbp',
24 | name: 'Reply Box Popup',
25 | sg: true,
26 | st: true,
27 | type: 'comments',
28 | };
29 | }
30 |
31 | init() {
32 | if (!Shared.esgst.replyBox) return;
33 |
34 | let button = Shared.common.createHeadingButton({
35 | id: 'rbp',
36 | icons: ['fa-comment'],
37 | title: 'Add a comment',
38 | });
39 | let popup = new Popup({
40 | addProgress: true,
41 | addScrollable: true,
42 | icon: 'fa-comment',
43 | title: `Add a comment:`,
44 | });
45 | DOM.insert(
46 | popup.scrollable,
47 | 'beforeend',
48 |
49 | );
50 | Button.create([
51 | {
52 | template: 'success',
53 | name: 'Save',
54 | onClick: async () => {
55 | await Shared.common.saveComment(
56 | null,
57 | Shared.esgst.sg ? '' : document.querySelector(`[name="trade_code"]`).value,
58 | '',
59 | popup.textArea.value,
60 | Shared.esgst.sg ? Shared.esgst.locationHref.match(/(.+?)(#.+?)?$/)[1] : '/ajax.php',
61 | popup.progressBar,
62 | true
63 | );
64 | },
65 | },
66 | {
67 | template: 'loading',
68 | isDisabled: true,
69 | name: 'Saving...',
70 | },
71 | ]).insert(popup.description, 'beforeend');
72 | button.addEventListener(
73 | 'click',
74 | popup.open.bind(popup, popup.textArea.focus.bind(popup.textArea))
75 | );
76 | }
77 | }
78 |
79 | const commentsReplyBoxPopup = new CommentsReplyBoxPopup();
80 |
81 | export { commentsReplyBoxPopup };
82 |
--------------------------------------------------------------------------------
/src/modules/Users/VisibleGiftsBreakdown.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { DOM } from '../../class/DOM';
5 |
6 | class UsersVisibleGiftsBreakdown extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 | -
13 | Shows the gifts breakdown of a user in their profile page, with the following initials:
14 |
15 |
16 | - FCV - Full CV
17 | - RCV - Reduced CV
18 | - NCV - No CV
19 | - A - Awaiting Feedback
20 | - NR - Not Received
21 |
22 |
23 | ),
24 | id: 'vgb',
25 | inputItems: [
26 | {
27 | id: 'vgb_wonFormat',
28 | prefix: `Won Format: `,
29 | tooltip: `[FCV], [RCV], [NCV] and [NR] will be replaced with their respective values.`,
30 | },
31 | {
32 | id: 'vgb_sentFormat',
33 | prefix: `Sent Format: `,
34 | tooltip: `[FCV], [RCV], [NCV], [A] and [NR] will be replaced with their respective values.`,
35 | },
36 | ],
37 | name: 'Visible Gifts Breakdown',
38 | options: {
39 | title: `Position: `,
40 | values: ['Left', 'Right'],
41 | },
42 | sg: true,
43 | type: 'users',
44 | featureMap: {
45 | profile: this.vgb_add.bind(this),
46 | },
47 | };
48 | }
49 |
50 | vgb_add(profile) {
51 | const position = Settings.get('vgb_index') === 0 ? 'afterbegin' : 'beforeend';
52 | DOM.insert(
53 | profile.wonRowRight.firstElementChild.firstElementChild,
54 | position,
55 | {` ${Settings.get('vgb_wonFormat')
56 | .replace(/\[FCV]/, profile.wonFull)
57 | .replace(/\[RCV]/, profile.wonReduced)
58 | .replace(/\[NCV]/, profile.wonZero)
59 | .replace(/\[NR]/, profile.wonNotReceived)} `}
60 | );
61 | DOM.insert(
62 | profile.sentRowRight.firstElementChild.firstElementChild,
63 | position,
64 | {` ${Settings.get('vgb_sentFormat')
65 | .replace(/\[FCV]/, profile.sentFull)
66 | .replace(/\[RCV]/, profile.sentReduced)
67 | .replace(/\[NCV]/, profile.sentZero)
68 | .replace(/\[A]/, profile.sentAwaiting)
69 | .replace(/\[NR]/, profile.sentNotReceived)} `}
70 | );
71 | }
72 | }
73 |
74 | const usersVisibleGiftsBreakdown = new UsersVisibleGiftsBreakdown();
75 |
76 | export { usersVisibleGiftsBreakdown };
77 |
--------------------------------------------------------------------------------
/src/modules/General/ImageBorders.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GeneralImageBorders extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | - Brings back image borders to SteamGifts.
11 |
12 | ),
13 | id: 'ib',
14 | name: 'Image Borders',
15 | sg: true,
16 | type: 'general',
17 | featureMap: {
18 | endless: this.ib_addBorders.bind(this),
19 | },
20 | };
21 | }
22 |
23 | ib_addBorders(context, main, source, endless) {
24 | const userElements = context.querySelectorAll(
25 | `${
26 | endless
27 | ? `.esgst-es-page-${endless} .giveaway_image_avatar, .esgst-es-page-${endless}.giveaway_image_avatar`
28 | : '.giveaway_image_avatar'
29 | }, ${
30 | endless
31 | ? `.esgst-es-page-${endless} .featured_giveaway_image_avatar, .esgst-es-page-${endless}.featured_giveaway_image_avatar`
32 | : '.featured_giveaway_image_avatar'
33 | }, ${
34 | endless
35 | ? `.esgst-es-page-${endless} :not(.esgst-ggl-panel) .table_image_avatar, .esgst-es-page-${endless}:not(.esgst-ggl-panel) .table_image_avatar`
36 | : `:not(.esgst-ggl-panel) .table_image_avatar`
37 | }`
38 | );
39 | for (let i = 0, n = userElements.length; i < n; ++i) {
40 | userElements[i].classList.add('esgst-ib-user');
41 | }
42 | const gameElements = context.querySelectorAll(
43 | `${
44 | endless
45 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail, .esgst-es-page-${endless}.giveaway_image_thumbnail`
46 | : '.giveaway_image_thumbnail'
47 | }, ${
48 | endless
49 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail_missing, .esgst-es-page-${endless}.giveaway_image_thumbnail_missing`
50 | : '.giveaway_image_thumbnail_missing'
51 | }, ${
52 | endless
53 | ? `.esgst-es-page-${endless} .table_image_thumbnail, .esgst-es-page-${endless}.table_image_thumbnail`
54 | : '.table_image_thumbnail'
55 | }, ${
56 | endless
57 | ? `.esgst-es-page-${endless} .table_image_thumbnail_missing, .esgst-es-page-${endless}.table_image_thumbnail_missing`
58 | : '.table_image_thumbnail_missing'
59 | }`
60 | );
61 | for (let i = 0, n = gameElements.length; i < n; ++i) {
62 | gameElements[i].classList.add('esgst-ib-game');
63 | }
64 | }
65 | }
66 |
67 | const generalImageBorders = new GeneralImageBorders();
68 |
69 | export { generalImageBorders };
70 |
--------------------------------------------------------------------------------
/src/modules/Comments/ReceivedReplyBoxPopup.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { Module } from '../../class/Module';
3 | import { Popup } from '../../class/Popup';
4 | import { Settings } from '../../class/Settings';
5 | import { Shared } from '../../class/Shared';
6 | import { Button } from '../../components/Button';
7 |
8 | class CommentsReceivedReplyBoxPopup extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 | -
15 | Pops up a reply box when you mark a giveaway as received (in your{' '}
16 | won page) so that you can add a
17 | comment thanking the creator.
18 |
19 |
20 | ),
21 | id: 'rrbp',
22 | name: 'Received Reply Box Popup',
23 | sg: true,
24 | type: 'comments',
25 | };
26 | }
27 |
28 | init() {
29 | if (!Shared.esgst.wonPath) return;
30 | Shared.esgst.giveawayFeatures.push(this.rrbp_addEvent.bind(this));
31 | }
32 |
33 | rrbp_addEvent(giveaways) {
34 | giveaways.forEach((giveaway) => {
35 | let feedback = giveaway.outerWrap.getElementsByClassName(
36 | 'table__gift-feedback-awaiting-reply'
37 | )[0];
38 | if (feedback) {
39 | feedback.addEventListener('click', this.rrbp_openPopup.bind(this, giveaway));
40 | }
41 | });
42 | }
43 |
44 | rrbp_openPopup(giveaway) {
45 | let popup, textArea;
46 | popup = new Popup({
47 | addProgress: true,
48 | addScrollable: true,
49 | icon: 'fa-comment',
50 | title: `Add a comment:`,
51 | });
52 | DOM.insert(popup.scrollable, 'beforeend',