(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 |
--------------------------------------------------------------------------------
/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/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/DiscussionPanels.js:
--------------------------------------------------------------------------------
1 | import { Module } from '../class/Module';
2 | import { Settings } from '../class/Settings';
3 |
4 | class DiscussionPanels extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | endless: true,
9 | id: 'discussionPanels',
10 | };
11 | }
12 |
13 | init() {
14 | if (
15 | (Settings.get('ct') && (this.esgst.giveawaysPath || this.esgst.discussionsPath)) ||
16 | (Settings.get('gdttt') &&
17 | (this.esgst.giveawaysPath ||
18 | this.esgst.discussionsPath ||
19 | this.esgst.discussionsTicketsTradesPath))
20 | ) {
21 | this.esgst.endlessFeatures.push(
22 | this.esgst.modules.commentsCommentTracker.ct_addDiscussionPanels.bind(
23 | this.esgst.modules.commentsCommentTracker
24 | )
25 | );
26 | }
27 | }
28 | }
29 |
30 | const discussionPanelsModule = new DiscussionPanels();
31 |
32 | export { discussionPanelsModule };
33 |
--------------------------------------------------------------------------------
/src/modules/Discussions/CloseOpenDiscussionButton.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '../../class/Button';
2 | import { DOM } from '../../class/DOM';
3 | import { FetchRequest } from '../../class/FetchRequest';
4 | import { Module } from '../../class/Module';
5 | import { Session } from '../../class/Session';
6 | import { Settings } from '../../class/Settings';
7 |
8 | class DiscussionsCloseOpenDiscussionButton extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 |
15 | Adds a button ( if the discussion is open and{' '}
16 | if it is closed) next to the title of a
17 | discussion created by yourself (in any{' '}
18 | discussions page) that allows you
19 | to close/open the discussion without having to access it.
20 |
21 |
22 | ),
23 | id: 'codb',
24 | name: 'Close/Open Discussion Button',
25 | sg: true,
26 | type: 'discussions',
27 | featureMap: {
28 | discussion: this.codb_addButtons.bind(this),
29 | },
30 | };
31 | }
32 |
33 | codb_addButtons(discussions) {
34 | for (const discussion of discussions) {
35 | if (
36 | discussion.author === Settings.get('username') &&
37 | !discussion.heading.parentElement.getElementsByClassName('esgst-codb-button')[0]
38 | ) {
39 | if (discussion.closed) {
40 | discussion.closed.remove();
41 | discussion.closed = true;
42 | }
43 | new Button(discussion.headingContainer.firstElementChild, 'beforebegin', {
44 | callbacks: [
45 | this.codb_close.bind(this, discussion),
46 | null,
47 | this.codb_open.bind(this, discussion),
48 | null,
49 | ],
50 | className: 'esgst-codb-button',
51 | icons: [
52 | 'fa-lock esgst-clickable',
53 | 'fa-circle-o-notch fa-spin',
54 | 'fa-lock esgst-clickable esgst-red',
55 | 'fa-circle-o-notch fa-spin',
56 | ],
57 | id: 'codb',
58 | index: discussion.closed ? 2 : 0,
59 | titles: [
60 | 'Close discussion',
61 | 'Closing discussion...',
62 | 'Open discussion',
63 | 'Opening discussion...',
64 | ],
65 | });
66 | }
67 | }
68 | }
69 |
70 | async codb_close(discussion) {
71 | let response = await FetchRequest.post(discussion.url, {
72 | data: `xsrf_token=${Session.xsrfToken}&do=close_discussion`,
73 | });
74 | if (response.html.getElementsByClassName('page__heading__button--red')[0]) {
75 | discussion.closed = true;
76 | discussion.innerWrap.classList.add('is-faded');
77 | return true;
78 | }
79 | return false;
80 | }
81 |
82 | async codb_open(discussion) {
83 | let response = await FetchRequest.post(discussion.url, {
84 | data: `xsrf_token=${Session.xsrfToken}&do=reopen_discussion`,
85 | });
86 | if (!response.html.getElementsByClassName('page__heading__button--red')[0]) {
87 | discussion.closed = false;
88 | discussion.innerWrap.classList.remove('is-faded');
89 | return true;
90 | }
91 | return false;
92 | }
93 | }
94 |
95 | const discussionsCloseOpenDiscussionButton = new DiscussionsCloseOpenDiscussionButton();
96 |
97 | export { discussionsCloseOpenDiscussionButton };
98 |
--------------------------------------------------------------------------------
/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/Discussions/DiscussionsSorter.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { Module } from '../../class/Module';
3 | import { Popout } from '../../class/Popout';
4 | import { Scope } from '../../class/Scope';
5 | import { Settings } from '../../class/Settings';
6 | import { ToggleSwitch } from '../../class/ToggleSwitch';
7 | import { Button } from '../../components/Button';
8 | import { common } from '../Common';
9 |
10 | const createHeadingButton = common.createHeadingButton.bind(common),
11 | saveAndSortContent = common.saveAndSortContent.bind(common);
12 | class DiscussionsDiscussionsSorter extends Module {
13 | constructor() {
14 | super();
15 | this.info = {
16 | description: () => (
17 |
18 |
19 | Adds a button ( ) to the main page heading of any{' '}
20 | discussions page that allows you to
21 | sort the discussions in the page by title, category, created time, author and number of
22 | comments.
23 |
24 |
25 | There is also an option to automatically sort the discussions so that every time you
26 | open the page the discussions are already sorted by whatever option you prefer.
27 |
28 |
29 | ),
30 | id: 'ds',
31 | name: 'Discussions Sorter',
32 | sg: true,
33 | type: 'discussions',
34 | };
35 | }
36 |
37 | init() {
38 | if (!this.esgst.discussionsPath) return;
39 |
40 | let object = {
41 | button: createHeadingButton({ id: 'ds', icons: ['fa-sort'], title: 'Sort discussions' }),
42 | };
43 | object.button.addEventListener('click', this.ds_openPopout.bind(this, object));
44 | }
45 |
46 | ds_openPopout(obj) {
47 | if (obj.popout) return;
48 | obj.popout = new Popout('esgst-ds-popout', obj.button, 0, true);
49 | new ToggleSwitch(
50 | obj.popout.popout,
51 | 'ds_auto',
52 | false,
53 | 'Auto Sort',
54 | false,
55 | false,
56 | 'Automatically sorts the discussions by the selected option when loading the page.',
57 | Settings.get('ds_auto')
58 | );
59 | let options;
60 | DOM.insert(
61 | obj.popout.popout,
62 | 'beforeend',
63 |
(options = ref)}>
64 | Default
65 | Title - Ascending
66 | Title - Descending
67 | Category - Ascending
68 | Category - Descending
69 | Created Time - Ascending
70 | Created Time - Descending
71 | Author - Ascending
72 | Author - Descending
73 | Comments - Ascending
74 | Comments - Descending
75 |
76 | );
77 | options.value = Settings.get('ds_option');
78 | let callback = saveAndSortContent.bind(
79 | common,
80 | Scope.findData('main', 'discussions'),
81 | 'ds_option',
82 | options
83 | );
84 | options.addEventListener('change', callback);
85 | Button.create({
86 | color: 'green',
87 | icons: ['fa-arrow-circle-right'],
88 | name: 'Sort',
89 | onClick: callback,
90 | }).insert(obj.popout.popout, 'beforeend');
91 | obj.popout.open();
92 | }
93 | }
94 |
95 | const discussionsDiscussionsSorter = new DiscussionsDiscussionsSorter();
96 |
97 | export { discussionsDiscussionsSorter };
98 |
--------------------------------------------------------------------------------
/src/modules/Discussions/MainPostPopup.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 { common } from '../Common';
7 |
8 | const createHeadingButton = common.createHeadingButton.bind(common);
9 | class DiscussionsMainPostPopup extends Module {
10 | constructor() {
11 | super();
12 | this.info = {
13 | description: () => (
14 |
15 |
16 | Hides the main post of a discussion and adds a button ( )
17 | to its main page heading that allows you to open the main post through a popup.
18 |
19 |
20 | This feature is useful if you have enabled,
21 | which allows you to view the main post of a discussion from any scrolling position.
22 |
23 |
24 | ),
25 | features: {
26 | mpp_r: {
27 | dependencies: ['ct'],
28 | name: 'Only hide the main post if it has been marked as read.',
29 | sg: true,
30 | },
31 | },
32 | id: 'mpp',
33 | name: 'Main Post Popup',
34 | sg: true,
35 | type: 'discussions',
36 | };
37 | }
38 |
39 | init() {
40 | if (!this.esgst.discussionPath) {
41 | return;
42 | }
43 | let button = createHeadingButton({
44 | id: 'mpp',
45 | icons: ['fa-home'],
46 | title: 'Open the main post',
47 | });
48 | let MPPPost = document.createElement('div');
49 | MPPPost.className = 'page__outer-wrap';
50 | let Sibling;
51 | do {
52 | Sibling = this.esgst.mainPageHeading.previousElementSibling;
53 | if (Sibling) {
54 | MPPPost.insertBefore(Sibling, MPPPost.firstElementChild);
55 | }
56 | } while (Sibling);
57 | this.esgst.mainPageHeading.parentElement.insertBefore(MPPPost, this.esgst.mainPageHeading);
58 | let Hidden;
59 | if (Settings.get('mpp_r')) {
60 | let discussion = JSON.parse(Shared.common.getValue('discussions', '{}'))[
61 | window.location.pathname.match(/^\/discussion\/(.+?)\//)[1]
62 | ];
63 | if (discussion) {
64 | if (discussion.readComments && discussion.readComments['']) {
65 | Hidden = true;
66 | window.scrollTo(0, 0);
67 | } else {
68 | Hidden = false;
69 | }
70 | } else {
71 | Hidden = false;
72 | }
73 | } else {
74 | Hidden = true;
75 | window.scrollTo(0, 0);
76 | }
77 | MPPPost.classList.add(Hidden ? 'esgst-mpp-hidden' : 'esgst-mpp-visible', 'esgst-text-left');
78 | button.addEventListener('click', () => {
79 | if (!Hidden) {
80 | MPPPost.classList.remove('esgst-mpp-visible');
81 | MPPPost.classList.add('esgst-mpp-hidden');
82 | }
83 | let popup = new Popup({ icon: '', title: '', popup: MPPPost });
84 | MPPPost.classList.add('esgst-mpp-popup');
85 | popup.open();
86 | popup.onClose = () => {
87 | MPPPost.classList.remove('esgst-mpp-popup');
88 | if (!Hidden) {
89 | MPPPost.classList.remove('esgst-mpp-hidden');
90 | MPPPost.classList.add('esgst-mpp-visible');
91 | MPPPost.removeAttribute('style');
92 | this.esgst.mainPageHeading.parentElement.insertBefore(
93 | MPPPost,
94 | this.esgst.mainPageHeading
95 | );
96 | }
97 | };
98 | });
99 | }
100 | }
101 |
102 | const discussionsMainPostPopup = new DiscussionsMainPostPopup();
103 |
104 | export { discussionsMainPostPopup };
105 |
--------------------------------------------------------------------------------
/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/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/Discussions/ReversedActiveDiscussions.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { Shared } from '../../class/Shared';
4 | import { DOM } from '../../class/DOM';
5 |
6 | class DiscussionsReversedActiveDiscussions extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 |
13 | Reverses the active discussions (in the main page) so that discussions come before deals
14 | (original order).
15 |
16 |
17 | ),
18 | id: 'rad',
19 | name: 'Reversed Active Discussions',
20 | sg: true,
21 | sgPaths: /^Browse\sGiveaways/,
22 | type: 'discussions',
23 | };
24 | }
25 |
26 | async init() {
27 | if (!Shared.esgst.giveawaysPath || !Shared.esgst.activeDiscussions || Settings.get('oadd')) {
28 | return;
29 | }
30 | Shared.esgst.activeDiscussions.insertBefore(
31 | Shared.esgst.activeDiscussions.lastElementChild,
32 | Shared.esgst.activeDiscussions.firstElementChild
33 | );
34 | }
35 | }
36 |
37 | const discussionsReversedActiveDiscussions = new DiscussionsReversedActiveDiscussions();
38 |
39 | export { discussionsReversedActiveDiscussions };
40 |
--------------------------------------------------------------------------------
/src/modules/Games/EnteredGameHighlighter.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 GamesEnteredGameHighlighter extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 |
13 | Adds an icon ( ) next to a game's name (in any page) to
14 | indicate that you have entered giveaways for the game in the past. Clicking on the icon
15 | unhighlights the game.
16 |
17 |
18 | A game is only highlighted if you entered a giveaway for it after this feature was
19 | enabled.
20 |
21 |
22 | ),
23 | id: 'egh',
24 | name: 'Entered Game Highlighter',
25 | sg: true,
26 | type: 'games',
27 | featureMap: {
28 | game: this.egh_getGames.bind(this),
29 | },
30 | features: {
31 | egh_c: {
32 | name: 'Show a counter with the number of giveaways that have been entered for the game.',
33 | sg: true,
34 | },
35 | },
36 | };
37 | }
38 |
39 | egh_getGames(games) {
40 | for (const game of games.all) {
41 | if (Shared.esgst.giveawayPath) {
42 | const button = document.querySelector('.sidebar__entry-insert');
43 | if (button) {
44 | button.addEventListener('click', this.egh_saveGame.bind(this, game.id, game.type));
45 | }
46 | }
47 | const savedGame = Shared.esgst.games[game.type][game.id];
48 | if (savedGame && savedGame.entered && !game.container.querySelector('.esgst-egh-button')) {
49 | const count = Number(savedGame.entered);
50 | DOM.insert(
51 | (game.container.closest('.poll') &&
52 | game.container.querySelector('.table__column__heading')) ||
53 | game.headingName,
54 | 'beforebegin',
55 |
64 |
65 | {Settings.get('egh_c') ? ` ${count}` : null}
66 |
67 | );
68 | }
69 | }
70 | }
71 |
72 | async egh_saveGame(id, type) {
73 | if (!id || !type) {
74 | return;
75 | }
76 | let game = Shared.esgst.games[type][id];
77 | if (!game) {
78 | game = {};
79 | }
80 | if (!game.entered) {
81 | game.entered = 0;
82 | }
83 | game.entered += 1;
84 | await Shared.common.lockAndSaveGames({ [type]: { [id]: game } });
85 | }
86 |
87 | async egh_unhighlightGame(id, type, event) {
88 | const icon = event.currentTarget;
89 | if (icon.classList.contains('fa-spin')) {
90 | return;
91 | }
92 | DOM.insert(icon, 'atinner',
);
93 | let game = Shared.esgst.games[type][id];
94 | if (game && game.entered) {
95 | game.entered = null;
96 | await Shared.common.lockAndSaveGames({ [type]: { [id]: game } });
97 | }
98 | icon.remove();
99 | }
100 | }
101 |
102 | const gamesEnteredGameHighlighter = new GamesEnteredGameHighlighter();
103 |
104 | export { gamesEnteredGameHighlighter };
105 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/modules/General/ElementFilters.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralElementFilters extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | Allows you to hide elements in any page using CSS selectors.
12 |
13 | If you do not know how to use CSS selectors or you are having trouble hiding an element,
14 | leave a comment in the ESGST thread with a description/image of the element that you
15 | want to hide and I will give you the selector that you have to use.
16 |
17 | Here are some quick examples:
18 |
19 |
20 | To hide the "Redeem" button in your{' '}
21 | won page, use:{' '}
22 | .table__column__key__redeem
23 |
24 |
25 | To hide the featured giveaway container (the big giveaway) in the main page, use:{' '}
26 | [esgst.giveawaysPath].featured__container
27 |
28 |
29 | To hide the pinned giveaways (the multiple copy giveaways) in the main page, use:{' '}
30 | [esgst.giveawaysPath].pinned-giveaways__outer-wrap
31 |
32 |
33 |
34 | ),
35 | inputItems: [
36 | {
37 | id: 'ef_filters',
38 | prefix: `Filters: `,
39 | tooltip: `Separate each selector by a comma followed by a space, for example: .class_1, .class_2, #id`,
40 | },
41 | ],
42 | id: 'ef',
43 | name: 'Element Filters',
44 | sg: true,
45 | st: true,
46 | type: 'general',
47 | };
48 | }
49 |
50 | init() {
51 | this.ef_hideElements(document);
52 | this.esgst.endlessFeatures.push(this.ef_hideElements.bind(this));
53 | if (Settings.get('sal') || !this.esgst.wonPath) return;
54 | this.esgst.endlessFeatures.push(
55 | this.esgst.modules.giveawaysSteamActivationLinks.sal_addObservers.bind(
56 | this.esgst.modules.giveawaysSteamActivationLinks
57 | )
58 | );
59 | }
60 |
61 | ef_hideElements(context, main, source, endless) {
62 | if (context === document && main) return;
63 | Settings.get('ef_filters')
64 | .split(`, `)
65 | .forEach((filter) => {
66 | if (!filter) return;
67 | try {
68 | const property = filter.match(/\[esgst\.(.+)]/);
69 | if (property) {
70 | if (!this.esgst[property[1]]) return;
71 | filter = filter.replace(/\[esgst\..+]/, '');
72 | }
73 | const elements = context.querySelectorAll(
74 | `${
75 | endless
76 | ? `.esgst-es-page-${endless} ${filter}, .esgst-es-page-${endless}${filter}`
77 | : `${filter}`
78 | }`
79 | );
80 | for (let i = elements.length - 1; i > -1; i--) {
81 | elements[i].classList.add('esgst-hidden');
82 | }
83 | } catch (e) {
84 | /**/
85 | }
86 | });
87 | }
88 | }
89 |
90 | const generalElementFilters = new GeneralElementFilters();
91 |
92 | export { generalElementFilters };
93 |
--------------------------------------------------------------------------------
/src/modules/General/EmbeddedVideos.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 GeneralEmbeddedVideos extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 |
13 | Embeds any YouTube/Vimeo videos found in a comment (in any page) into the comment.
14 |
15 |
16 | Videos are only embedded if their links are in the[URL](URL) format and are the only
17 | content in a line.For example,
18 | "[https://youtu.be/ihd9dKek2gc](https://youtu.be/ihd9dKek2gc)" gets embedded, but
19 | "[Watch this!](https://youtu.be/ihd9dKek2gc)" and "Watch this:
20 | [https://youtu.be/ihd9dKek2gc](https://youtu.be/ihd9dKek2gc)" do not.
21 |
22 |
23 | ),
24 | id: 'ev',
25 | name: 'Embedded Videos',
26 | sg: true,
27 | st: true,
28 | type: 'general',
29 | featureMap: {
30 | endless: this.ev_getVideos.bind(this),
31 | },
32 | };
33 | }
34 |
35 | ev_getVideos(context, main, source, endless) {
36 | let types,
37 | i,
38 | numTypes,
39 | type,
40 | videos,
41 | j,
42 | numVideos,
43 | video,
44 | previous,
45 | next,
46 | embedUrl,
47 | url,
48 | text,
49 | title;
50 | types = ['youtube.com', 'youtu.be', 'vimeo.com'];
51 | for (i = 0, numTypes = types.length; i < numTypes; ++i) {
52 | type = types[i];
53 | videos = context.querySelectorAll(
54 | `${
55 | endless
56 | ? `.esgst-es-page-${endless} a[href*="${type}"], .esgst-es-page-${endless}a[href*="${type}"]`
57 | : `a[href*="${type}"]`
58 | }`
59 | );
60 | for (j = 0, numVideos = videos.length; j < numVideos; ++j) {
61 | video = videos[j];
62 | previous = video.previousSibling;
63 | next = video.nextSibling;
64 | if ((!previous || !previous.textContent.trim()) && (!next || !next.textContent.trim())) {
65 | // video is the only content in the line
66 | url = video.getAttribute('href');
67 | embedUrl = this.ev_getEmbedUrl(i, url);
68 | if (embedUrl) {
69 | text = video.textContent;
70 | if (url !== text) {
71 | title = `
${text}
`;
72 | } else {
73 | title = '';
74 | }
75 | createElements(video, 'atouter', [
76 | {
77 | type: 'div',
78 | children: [
79 | {
80 | text: title,
81 | type: 'node',
82 | },
83 | {
84 | attributes: {
85 | allowfullscreen: '0',
86 | frameborder: '0',
87 | height: '360',
88 | src: embedUrl,
89 | width: '640',
90 | },
91 | type: 'iframe',
92 | },
93 | ],
94 | },
95 | ]);
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 | ev_getEmbedUrl(i, url) {
103 | let regExps, regExp, match, baseUrls, baseUrl, code;
104 | regExps = [
105 | /youtube.com\/watch\?v=(.+?)(\/.*)?(&.*)?$/,
106 | /youtu.be\/(.+?)(\/.*)?$/,
107 | /vimeo.com\/(.+?)(\/.*)?$/,
108 | ];
109 | regExp = regExps[i];
110 | match = url.match(regExp);
111 | if (match) {
112 | baseUrls = [
113 | `https://www.youtube.com/embed/`,
114 | `https://www.youtube.com/embed/`,
115 | `https://player.vimeo.com/video/`,
116 | ];
117 | baseUrl = baseUrls[i];
118 | code = match[1];
119 | return `${baseUrl}${code}`;
120 | } else {
121 | return null;
122 | }
123 | }
124 | }
125 |
126 | const generalEmbeddedVideos = new GeneralEmbeddedVideos();
127 |
128 | export { generalEmbeddedVideos };
129 |
--------------------------------------------------------------------------------
/src/modules/General/FixedFooter.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralFixedFooter extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 |
12 | Keeps the footer of any page at the bottom of the window while you scroll down the page.
13 |
14 |
15 | ),
16 | id: 'ff',
17 | name: 'Fixed Footer',
18 | sg: true,
19 | st: true,
20 | type: 'general',
21 | };
22 | }
23 |
24 | init() {
25 | if (!Shared.footer) {
26 | return;
27 | }
28 |
29 | Shared.footer.nodes.outer.classList.add('esgst-ff');
30 | }
31 | }
32 |
33 | const generalFixedFooter = new GeneralFixedFooter();
34 |
35 | export { generalFixedFooter };
36 |
--------------------------------------------------------------------------------
/src/modules/General/FixedHeader.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralFixedHeader extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 |
12 | Keeps the header of any page at the top of the window while you scroll down the page.
13 |
14 |
15 | ),
16 | id: 'fh',
17 | name: 'Fixed Header',
18 | sg: true,
19 | st: true,
20 | type: 'general',
21 | };
22 | }
23 |
24 | init() {
25 | if (!Shared.header?.nodes.outer) {
26 | return;
27 | }
28 | Shared.header.nodes.outer.classList.add('esgst-fh');
29 | }
30 | }
31 |
32 | const generalFixedHeader = new GeneralFixedHeader();
33 |
34 | export { generalFixedHeader };
35 |
--------------------------------------------------------------------------------
/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/General/FixedSidebar.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralFixedSidebar extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 |
12 | Keeps the sidebar of any page at the left side of the window while you scroll down the
13 | page.
14 |
15 |
16 | ),
17 | id: 'fs',
18 | name: 'Fixed Sidebar',
19 | sg: true,
20 | type: 'general',
21 | };
22 | }
23 |
24 | init() {
25 | if (!this.esgst.sidebar) {
26 | return;
27 | }
28 |
29 | const top = this.esgst.pageTop + 25;
30 | this.esgst.style.insertAdjacentText(
31 | 'beforeend',
32 | `
33 | .esgst-fs {
34 | max-height: calc(100vh - ${top + 30 + (Settings.get('ff') ? 39 : 0)}px);
35 | top: ${top}px;
36 | }
37 | `
38 | );
39 |
40 | this.esgst.sidebar.classList.add('esgst-fs');
41 | }
42 | }
43 |
44 | const generalFixedSidebar = new GeneralFixedSidebar();
45 |
46 | export { generalFixedSidebar };
47 |
--------------------------------------------------------------------------------
/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/General/HiddenCommunityPoll.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralHiddenCommunityPoll extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | Hides the community poll (if there is one) of the main page.
12 |
13 | ),
14 | features: {
15 | hcp_v: {
16 | name: 'Only hide the poll if you already voted in it.',
17 | sg: true,
18 | },
19 | },
20 | id: 'hcp',
21 | name: 'Hidden Community Poll',
22 | sg: true,
23 | type: 'general',
24 | };
25 | }
26 |
27 | init() {
28 | if (!this.esgst.giveawaysPath || !this.esgst.activeDiscussions) return;
29 | let poll = this.esgst.activeDiscussions.previousElementSibling;
30 | if (
31 | poll &&
32 | poll.classList.contains('widget-container') &&
33 | !poll.querySelector(`.block_header[href="/happy-holidays"]`)
34 | ) {
35 | if (!Settings.get('hcp_v') || poll.querySelector('.table__row-outer-wrap.is-selected')) {
36 | poll.classList.add('esgst-hidden');
37 | }
38 | }
39 | }
40 | }
41 |
42 | const generalHiddenCommunityPoll = new GeneralHiddenCommunityPoll();
43 |
44 | export { generalHiddenCommunityPoll };
45 |
--------------------------------------------------------------------------------
/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/General/NarrowSidebar.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GeneralNarrowSidebar extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | Keeps the sidebar narrowed in all pages.
11 |
12 | ),
13 | id: 'ns',
14 | name: 'Narrow Sidebar',
15 | sg: true,
16 | type: 'general',
17 | };
18 | }
19 |
20 | init() {
21 | if (!this.esgst.sidebar) return;
22 | this.esgst.sidebar.classList.remove('sidebar--wide');
23 | this.esgst.sidebar.classList.add('esgst-ns');
24 | }
25 | }
26 |
27 | const generalNarrowSidebar = new GeneralNarrowSidebar();
28 |
29 | export { generalNarrowSidebar };
30 |
--------------------------------------------------------------------------------
/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/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/General/SameTabOpener.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GeneralSameTabOpener extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | Opens any link in the page in the same tab.
11 |
12 | ),
13 | id: 'sto',
14 | name: 'Same Tab Opener',
15 | sg: true,
16 | st: true,
17 | type: 'general',
18 | featureMap: {
19 | endless: this.sto_setLinks.bind(this),
20 | },
21 | };
22 | }
23 |
24 | sto_setLinks(context, main, source, endless) {
25 | const elements = context.querySelectorAll(
26 | `${
27 | endless
28 | ? `.esgst-es-page-${endless} [target="_blank"], .esgst-es-page-${endless}[target="_blank"]`
29 | : `[target="_blank"]`
30 | }`
31 | );
32 | for (let i = 0, n = elements.length; i < n; ++i) {
33 | elements[i].removeAttribute('target');
34 | }
35 | }
36 | }
37 |
38 | const generalSameTabOpener = new GeneralSameTabOpener();
39 |
40 | export { generalSameTabOpener };
41 |
--------------------------------------------------------------------------------
/src/modules/General/ScrollToBottomButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { Shared } from '../../class/Shared';
5 | import { DOM } from '../../class/DOM';
6 |
7 | const animateScroll = common.animateScroll.bind(common),
8 | createElements = common.createElements.bind(common),
9 | createHeadingButton = common.createHeadingButton.bind(common),
10 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
11 | class GeneralScrollToBottomButton extends Module {
12 | constructor() {
13 | super();
14 | this.info = {
15 | description: () => (
16 |
17 |
18 | Adds a button ( ) either to the bottom right
19 | corner, the main page heading or the footer (you can decide where) of any page that
20 | takes you to the bottom of the page.
21 |
22 |
23 | ),
24 | id: 'stbb',
25 | name: 'Scroll To Bottom Button',
26 | options: {
27 | title: `Show in:`,
28 | values: ['Bottom Right Corner', 'Main Page Heading', 'Footer'],
29 | },
30 | sg: true,
31 | st: true,
32 | type: 'general',
33 | };
34 | }
35 |
36 | init() {
37 | let button;
38 | switch (Settings.get('stbb_index')) {
39 | case 0:
40 | button = createElements(document.body, 'beforeend', [
41 | {
42 | attributes: {
43 | class: 'esgst-stbb-button esgst-stbb-button-fixed',
44 | title: `${getFeatureTooltip('stbb', 'Scroll to bottom')}`,
45 | },
46 | type: 'div',
47 | children: [
48 | {
49 | attributes: {
50 | class: 'fa fa-chevron-down',
51 | },
52 | type: 'i',
53 | },
54 | ],
55 | },
56 | ]);
57 | window.addEventListener('scroll', () => {
58 | if (document.documentElement.offsetHeight - window.innerHeight >= window.scrollY + 100) {
59 | button.classList.remove('esgst-hidden');
60 | } else {
61 | button.classList.add('esgst-hidden');
62 | }
63 | });
64 | break;
65 | case 1:
66 | button = createHeadingButton({
67 | id: 'stbb',
68 | icons: ['fa-chevron-down'],
69 | title: 'Scroll to bottom',
70 | });
71 | button.classList.add('esgst-stbb-button');
72 | break;
73 | case 2: {
74 | const linkContainer = Shared.footer.addLinkContainer({
75 | icon: 'fa fa-chevron-down',
76 | position: 'beforeend',
77 | side: 'right',
78 | });
79 |
80 | linkContainer.nodes.outer.classList.add('esgst-stbb-button');
81 | linkContainer.nodes.outer.title = getFeatureTooltip('stbb', 'Scroll to bottom');
82 |
83 | button = linkContainer.nodes.outer;
84 |
85 | break;
86 | }
87 | }
88 | button.addEventListener('click', () =>
89 | animateScroll(document.documentElement.offsetHeight, () => {
90 | if (Settings.get('es') && this.esgst.es.paginations) {
91 | this.esgst.modules.generalEndlessScrolling.es_changePagination(
92 | this.esgst.es,
93 | this.esgst.es.reverseScrolling ? 1 : this.esgst.es.paginations.length
94 | );
95 | }
96 | })
97 | );
98 | }
99 | }
100 |
101 | const generalScrollToBottomButton = new GeneralScrollToBottomButton();
102 |
103 | export { generalScrollToBottomButton };
104 |
--------------------------------------------------------------------------------
/src/modules/General/ScrollToTopButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { Shared } from '../../class/Shared';
5 | import { DOM } from '../../class/DOM';
6 |
7 | const animateScroll = common.animateScroll.bind(common),
8 | createElements = common.createElements.bind(common),
9 | createHeadingButton = common.createHeadingButton.bind(common),
10 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
11 | class GeneralScrollToTopButton extends Module {
12 | constructor() {
13 | super();
14 | this.info = {
15 | description: () => (
16 |
17 |
18 | Adds a button ( ) either to the bottom right corner,
19 | the main page heading or the footer (you can decide where) of any page that takes you to
20 | the top of the page.
21 |
22 |
23 | ),
24 | id: 'sttb',
25 | name: 'Scroll To Top Button',
26 | options: {
27 | title: `Show in:`,
28 | values: ['Bottom Right Corner', 'Main Page Heading', 'Footer'],
29 | },
30 | sg: true,
31 | st: true,
32 | type: 'general',
33 | };
34 | }
35 |
36 | init() {
37 | let button;
38 | switch (Settings.get('sttb_index')) {
39 | case 0:
40 | button = createElements(document.body, 'beforeend', [
41 | {
42 | attributes: {
43 | class: 'esgst-sttb-button esgst-sttb-button-fixed',
44 | title: `${getFeatureTooltip('sttb', 'Scroll to top')}`,
45 | },
46 | type: 'div',
47 | children: [
48 | {
49 | attributes: {
50 | class: 'fa fa-chevron-up',
51 | },
52 | type: 'i',
53 | },
54 | ],
55 | },
56 | ]);
57 | button.classList.add('esgst-hidden');
58 | window.addEventListener('scroll', () => {
59 | if (window.scrollY > 100) {
60 | button.classList.remove('esgst-hidden');
61 | } else {
62 | button.classList.add('esgst-hidden');
63 | }
64 | });
65 | break;
66 | case 1:
67 | button = createHeadingButton({
68 | id: 'sttb',
69 | icons: ['fa-chevron-up'],
70 | title: 'Scroll to top',
71 | });
72 | button.classList.add('esgst-sttb-button');
73 | break;
74 | case 2: {
75 | const linkContainer = Shared.footer.addLinkContainer({
76 | icon: 'fa fa-chevron-up',
77 | position: 'beforeend',
78 | side: 'right',
79 | });
80 |
81 | linkContainer.nodes.outer.classList.add('esgst-sttb-button');
82 | linkContainer.nodes.outer.title = getFeatureTooltip('sttb', 'Scroll to top');
83 |
84 | button = linkContainer.nodes.outer;
85 |
86 | break;
87 | }
88 | }
89 | button.addEventListener(
90 | 'click',
91 | animateScroll.bind(common, 0, () => {
92 | if (Settings.get('es') && this.esgst.es.paginations) {
93 | this.esgst.modules.generalEndlessScrolling.es_changePagination(
94 | this.esgst.es,
95 | this.esgst.es.reverseScrolling ? this.esgst.es.paginations.length : 1
96 | );
97 | }
98 | })
99 | );
100 | }
101 | }
102 |
103 | const generalScrollToTopButton = new GeneralScrollToTopButton();
104 |
105 | export { generalScrollToTopButton };
106 |
--------------------------------------------------------------------------------
/src/modules/General/SearchClearButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GeneralSearchClearButton extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | Adds a clear button to each search input in the page.
12 |
13 | ),
14 | id: 'scb',
15 | name: 'Search Clear Button',
16 | sg: true,
17 | type: 'general',
18 | };
19 | }
20 |
21 | init() {
22 | this.getInputs(document);
23 | }
24 |
25 | getInputs(context) {
26 | const inputs = context.querySelectorAll('.sidebar__search-input');
27 | for (const input of inputs) {
28 | input.parentElement.classList.add('esgst-scb');
29 | DOM.insert(
30 | input.parentElement,
31 | 'beforeend',
32 |
{
36 | input.value = '';
37 | input.dispatchEvent(new Event('change'));
38 | input.focus();
39 | }}
40 | >
41 | );
42 | }
43 | }
44 | }
45 |
46 | const generalSearchClearButton = new GeneralSearchClearButton();
47 |
48 | export { generalSearchClearButton };
49 |
--------------------------------------------------------------------------------
/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/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/URLRedirector.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GeneralURLRedirector extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 |
11 | Redirects broken URLs to the correct URLs. For example, "/giveaway/XXXXX" redirects to
12 | "/giveaway/XXXXX/".
13 |
14 |
15 | ),
16 | id: 'urlr',
17 | name: 'URL Redirector',
18 | sg: true,
19 | st: true,
20 | type: 'general',
21 | };
22 | }
23 | }
24 |
25 | const generalURLRedirector = new GeneralURLRedirector();
26 |
27 | export { generalURLRedirector };
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/modules/General/VisibleFullLevel.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { EventDispatcher } from '../../class/EventDispatcher';
3 | import { Events } from '../../constants/Events';
4 | import { Session } from '../../class/Session';
5 | import { Shared } from '../../class/Shared';
6 | import { DOM } from '../../class/DOM';
7 |
8 | class GeneralVisibleFullLevel extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 |
15 | Displays the full level at the header, instead of only showing it when hovering over the
16 | level. For example, "Level 5" becomes "Lvl 5.25".
17 |
18 |
19 | ),
20 | id: 'vfl',
21 | name: 'Visible Full Level',
22 | sg: true,
23 | type: 'general',
24 | };
25 | }
26 |
27 | init() {
28 | EventDispatcher.subscribe(Events.LEVEL_UPDATED, this.update.bind(this));
29 |
30 | this.update(null, Session.counters.level);
31 | }
32 |
33 | async update(oldLevel, newLevel) {
34 | const levelNode = Shared.header.buttonContainers['account'].nodes.level;
35 | levelNode.textContent = `Lvl ${newLevel.full}`;
36 | }
37 | }
38 |
39 | const generalVisibleFullLevel = new GeneralVisibleFullLevel();
40 |
41 | export { generalVisibleFullLevel };
42 |
--------------------------------------------------------------------------------
/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/Giveaways/CustomGiveawayBackground.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Settings } from '../../class/Settings';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GiveawaysCustomGiveawayBackground extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 |
12 | Allows you to color the background of giveaways based on their type (public, invite
13 | only, region restricted, group or whitelist) and level.
14 |
15 |
16 | ),
17 | features: {
18 | cgb_b: {
19 | background: true,
20 | name: 'Color giveaways that cannot be entered because of blacklist reasons.',
21 | sg: true,
22 | },
23 | cgb_p: {
24 | background: true,
25 | name: 'Color public giveaways.',
26 | sg: true,
27 | },
28 | cgb_io: {
29 | background: true,
30 | name: 'Color invite only giveaways.',
31 | sg: true,
32 | },
33 | cgb_rr: {
34 | background: true,
35 | name: 'Color region restricted giveaways.',
36 | sg: true,
37 | },
38 | cgb_g: {
39 | background: true,
40 | name: 'Color group giveaways.',
41 | sg: true,
42 | },
43 | cgb_w: {
44 | background: true,
45 | name: 'Color whitelist giveaways.',
46 | sg: true,
47 | },
48 | cgb_sgt: {
49 | background: true,
50 | name: 'Color SGTools giveaways.',
51 | sg: true,
52 | },
53 | },
54 | featureMap: {
55 | giveaway: this.color.bind(this),
56 | },
57 | id: 'cgb',
58 | name: 'Custom Giveaway Background',
59 | sg: true,
60 | type: 'giveaways',
61 | };
62 | }
63 |
64 | color(giveaways) {
65 | for (const giveaway of giveaways) {
66 | if (Settings.get('cgb_b') && giveaway.outerWrap.getAttribute('data-blacklist')) {
67 | giveaway.outerWrap.setAttribute(
68 | 'style',
69 | `background-color: ${Settings.get('cgb_b_bgColor')} !important`
70 | );
71 | } else if (Settings.get('cgb_sgt') && giveaway.sgTools) {
72 | giveaway.outerWrap.setAttribute(
73 | 'style',
74 | `background-color: ${Settings.get('cgb_sgt_bgColor')} !important`
75 | );
76 | }
77 | const { color } = Settings.get('cgb_levelColors').filter(
78 | (colors) =>
79 | giveaway.level >= parseInt(colors.lower) && giveaway.level <= parseInt(colors.upper)
80 | )[0] || { color: undefined };
81 | if (color) {
82 | giveaway.outerWrap.setAttribute('style', `background-color: ${color} !important`);
83 | } else if (Settings.get('cgb_w') && giveaway.whitelist) {
84 | giveaway.outerWrap.setAttribute(
85 | 'style',
86 | `background-color: ${Settings.get('cgb_w_bgColor')} !important`
87 | );
88 | } else if (Settings.get('cgb_g') && giveaway.group) {
89 | giveaway.outerWrap.setAttribute(
90 | 'style',
91 | `background-color: ${Settings.get('cgb_g_bgColor')} !important`
92 | );
93 | } else if (Settings.get('cgb_rr') && giveaway.regionRestricted) {
94 | giveaway.outerWrap.setAttribute(
95 | 'style',
96 | `background-color: ${Settings.get('cgb_rr_bgColor')} !important`
97 | );
98 | } else if (Settings.get('cgb_io') && giveaway.inviteOnly) {
99 | giveaway.outerWrap.setAttribute(
100 | 'style',
101 | `background-color: ${Settings.get('cgb_io_bgColor')} !important`
102 | );
103 | } else if (Settings.get('cgb_p') && giveaway.public) {
104 | giveaway.outerWrap.setAttribute(
105 | 'style',
106 | `background-color: ${Settings.get('cgb_p_bgColor')} !important`
107 | );
108 | } else {
109 | giveaway.outerWrap.style.backgroundColor = '';
110 | }
111 | if (giveaway.outerWrap.style.backgroundColor) {
112 | giveaway.outerWrap.style.backgroundImage = 'none';
113 | }
114 | }
115 | }
116 | }
117 |
118 | const giveawaysCustomGiveawayBackground = new GiveawaysCustomGiveawayBackground();
119 |
120 | export { giveawaysCustomGiveawayBackground };
121 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/DeleteKeyConfirmation.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { FetchRequest } from '../../class/FetchRequest';
3 | import { Module } from '../../class/Module';
4 | import { Session } from '../../class/Session';
5 | import { common } from '../Common';
6 |
7 | const createConfirmation = common.createConfirmation.bind(common),
8 | createElements = common.createElements.bind(common);
9 | class GiveawaysDeleteKeyConfirmation extends Module {
10 | constructor() {
11 | super();
12 | this.info = {
13 | description: () => (
14 |
15 |
16 | Shows a confirmation popup if you try to delete a giveaway's key(s) (in any{' '}
17 | winners {' '}
18 | page).
19 |
20 |
21 | ),
22 | id: 'dkc',
23 | name: 'Delete Key Confirmation',
24 | sg: true,
25 | type: 'giveaways',
26 | };
27 | }
28 |
29 | init() {
30 | if (!this.esgst.giveawayPath) return;
31 | this.esgst.endlessFeatures.push(this.dkc_getLinks.bind(this));
32 | }
33 |
34 | dkc_getLinks(context, main, source, endless) {
35 | const elements = context.querySelectorAll(
36 | `${
37 | endless
38 | ? `.esgst-es-page-${endless} .form__key-btn-delete, .esgst-es-page-${endless}.form__key-btn-delete`
39 | : '.form__key-btn-delete'
40 | }`
41 | );
42 | for (let i = elements.length - 1; i > -1; --i) {
43 | const element = elements[i];
44 | const newElement = createElements(element, 'afterend', [
45 | {
46 | attributes: {
47 | class: 'table__column__secondary-link esgst-clickable',
48 | },
49 | text: 'Delete',
50 | type: 'span',
51 | },
52 | ]);
53 | element.remove();
54 | newElement.addEventListener(
55 | 'click',
56 | createConfirmation.bind(
57 | common,
58 | 'Are you sure you want to delete this key?',
59 | this.dkc_deleteKey.bind(createConfirmation, newElement),
60 | null
61 | )
62 | );
63 | }
64 | }
65 |
66 | async dkc_deleteKey(link) {
67 | let row = link.closest('.table__row-inner-wrap');
68 | row.getElementsByClassName('form__key-read')[0].classList.add('is-hidden');
69 | row.getElementsByClassName('form__key-loading')[0].classList.remove('is-hidden');
70 | row.querySelector(`[name="key_value"]`).value = '';
71 | row.getElementsByClassName('form__key-value')[0].textContent = '';
72 | await FetchRequest.post('/ajax.php', {
73 | data: `xsrf_token=${Session.xsrfToken}&do=set_gift_key&key_value=&winner_id=${
74 | row.querySelector(`[name="winner_id"]`).value
75 | }`,
76 | });
77 | row.getElementsByClassName('form__key-loading')[0].classList.add('is-hidden');
78 | row.getElementsByClassName('form__key-insert')[0].classList.remove('is-hidden');
79 | row.getElementsByClassName('js__sent-text')[0].textContent = 'Sent Gift';
80 | row.getElementsByClassName('js__sent-text')[1].textContent = 'Sent Gift';
81 | }
82 | }
83 |
84 | const giveawaysDeleteKeyConfirmation = new GiveawaysDeleteKeyConfirmation();
85 |
86 | export { giveawaysDeleteKeyConfirmation };
87 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/EnteredGiveawaysStats.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 GiveawaysEnteredGiveawaysStats extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | description: () => (
11 |
12 |
13 | Allows you to see stats for your entered giveaways in the sidebar of the entered page.
14 |
15 |
16 | ),
17 | features: {
18 | egs_e: {
19 | name: 'Include ended giveaways in the stats.',
20 | sgPaths: 'My Giveaways - Entered',
21 | sg: true,
22 | },
23 | },
24 | id: 'egs',
25 | name: 'Entered Giveaways Stats',
26 | sg: true,
27 | sgPaths: 'My Giveaways - Entered',
28 | type: 'giveaways',
29 | };
30 | }
31 |
32 | init() {
33 | if (!this.esgst.enteredPath) {
34 | return;
35 | }
36 | common.createSidebarNavigation(this.esgst.sidebar, 'beforeend', {
37 | name: 'Entered Giveaways Stats',
38 | items: [
39 | {
40 | id: 'egs_chance',
41 | name: 'Average Chance',
42 | count: 0,
43 | },
44 | {
45 | id: 'egs_level',
46 | name: 'Average Level',
47 | count: 0,
48 | },
49 | {
50 | id: 'egs_entries',
51 | name: 'Average Entries',
52 | count: 0,
53 | },
54 | {
55 | id: 'egs_points',
56 | name: 'Average Points Spent',
57 | count: 0,
58 | },
59 | {
60 | id: 'egs_simple_points',
61 | name: 'Total Points Spent',
62 | count: 0,
63 | },
64 | ],
65 | });
66 | const obj = {
67 | counters: {
68 | chance: 0.0,
69 | level: 0.0,
70 | entries: 0.0,
71 | points: 0,
72 | },
73 | simpleCounters: {
74 | points: 0,
75 | },
76 | elements: {},
77 | total: 0,
78 | };
79 | for (const key in obj.counters) {
80 | obj.elements[key] = document
81 | .querySelector(`#egs_${key}`)
82 | .querySelector('.sidebar__navigation__item__count');
83 | }
84 | for (const key in obj.simpleCounters) {
85 | obj.elements[`simple_${key}`] = document
86 | .querySelector(`#egs_simple_${key}`)
87 | .querySelector('.sidebar__navigation__item__count');
88 | }
89 | this.esgst.giveawayFeatures.push((giveaways, main) => this.addStats(obj, giveaways, main));
90 | }
91 |
92 | addStats(obj, giveaways, main) {
93 | if (!main) {
94 | return;
95 | }
96 | for (const giveaway of giveaways) {
97 | if (!giveaway.ended || Settings.get('egs_e')) {
98 | for (const key in obj.counters) {
99 | obj.counters[key] += giveaway[key];
100 | }
101 | for (const key in obj.simpleCounters) {
102 | obj.simpleCounters[key] += giveaway[key];
103 | }
104 | obj.total += 1;
105 | }
106 | }
107 | for (const key in obj.counters) {
108 | obj.elements[key].textContent = common.round(obj.counters[key] / obj.total);
109 | }
110 | for (const key in obj.simpleCounters) {
111 | obj.elements[`simple_${key}`].textContent = obj.simpleCounters[key];
112 | }
113 | }
114 | }
115 |
116 | const giveawaysEnteredGiveawaysStats = new GiveawaysEnteredGiveawaysStats();
117 |
118 | export { giveawaysEnteredGiveawaysStats };
119 |
--------------------------------------------------------------------------------
/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/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/GiveawayErrorSearchLinks.tsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { DOM } from '../../class/DOM';
4 |
5 | const getFeatureTooltip = common.getFeatureTooltip.bind(common);
6 | class GiveawaysGiveawayErrorSearchLinks extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | // by Revadike
11 | // eslint-disable-next-line react/display-name
12 | description: () => (
13 |
14 |
15 | If you cannot access a giveaway because of many different reasons, a "Search
16 | Links" row is added to the table of the{' '}
17 | error page containing 4 links
18 | that allow you to search for the game elsewhere:
19 |
20 |
21 |
22 | A SteamGifts icon that allows you to search for open giveaways of the game on
23 | SteamGifts.
24 |
25 |
26 | allows you to search for the game on Steam.
27 |
28 |
29 |
30 |
31 | {' '}
32 | allows you to search for the game on SteamDB.
33 |
34 |
35 |
36 |
37 | {' '}
38 | allows you to search for the game on Barter.vg.
39 |
40 |
41 |
42 | ),
43 | id: 'gesl',
44 | name: 'Giveaway Error Search Links',
45 | sg: true,
46 | type: 'giveaways',
47 | };
48 | }
49 |
50 | init = () => {
51 | const table = document.getElementsByClassName('table--summary')[0];
52 | if (!this.esgst.giveawayPath || !table) return;
53 | const name = encodeURIComponent(
54 | table.getElementsByClassName('table__column__secondary-link')[0].textContent
55 | );
56 | DOM.insert(
57 | table.getElementsByClassName('table__row-outer-wrap')[0],
58 | 'afterend',
59 |
60 |
61 |
62 | Search Links
63 |
64 |
104 |
105 |
106 | );
107 | };
108 | }
109 |
110 | const giveawaysGiveawayErrorSearchLinks = new GiveawaysGiveawayErrorSearchLinks();
111 |
112 | export { giveawaysGiveawayErrorSearchLinks };
113 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/GiveawayPopup.jsx:
--------------------------------------------------------------------------------
1 | import { DOM } from '../../class/DOM';
2 | import { Module } from '../../class/Module';
3 | import { Button } from '../../components/Button';
4 | import { common } from '../Common';
5 |
6 | const getFeatureTooltip = common.getFeatureTooltip.bind(common);
7 | class GiveawaysGiveawayPopup extends Module {
8 | constructor() {
9 | super();
10 | this.info = {
11 | description: () => (
12 |
13 |
14 | Adds a button ( ) below a giveaway's start time
15 | (in any page) that allows you to read the description of the giveaway and/or add a
16 | comment to it without having to access it.
17 |
18 | You can move the button around by dragging and dropping it.
19 |
20 | ),
21 | id: 'gp',
22 | name: 'Giveaway Popup',
23 | sg: true,
24 | type: 'giveaways',
25 | featureMap: {
26 | giveaway: this.gp_addButton.bind(this),
27 | },
28 | };
29 | }
30 |
31 | gp_addButton(giveaways, main, source) {
32 | giveaways.forEach((giveaway) => {
33 | if (
34 | giveaway.sgTools ||
35 | (main &&
36 | (this.esgst.createdPath ||
37 | this.esgst.enteredPath ||
38 | this.esgst.wonPath ||
39 | this.esgst.giveawayPath ||
40 | this.esgst.newGiveawayPath))
41 | )
42 | return;
43 | if (
44 | !giveaway.innerWrap.getElementsByClassName('esgst-gp-button')[0] &&
45 | (!giveaway.inviteOnly || giveaway.url)
46 | ) {
47 | let button;
48 | const onClick = () => {
49 | return new Promise((resolve) => {
50 | // noinspection JSIgnoredPromiseFromCall
51 | this.esgst.modules.giveawaysEnterLeaveGiveawayButton.elgb_openPopup(
52 | giveaway,
53 | main,
54 | source,
55 | (error) => {
56 | if (error) {
57 | button.build(3);
58 | } else {
59 | button.build(1);
60 | }
61 | resolve();
62 | }
63 | );
64 | });
65 | };
66 | button = Button.create({
67 | additionalContainerClass: 'esgst-gp-button',
68 | states: [
69 | {
70 | color: 'white',
71 | tooltip: getFeatureTooltip('gp', 'View giveaway description / add a comment'),
72 | icons: ['fa-external-link'],
73 | name: '',
74 | switchTo: { onReturn: 1 },
75 | onClick,
76 | },
77 | {
78 | template: 'loading',
79 | isDisabled: true,
80 | name: '',
81 | },
82 | {
83 | template: 'error',
84 | tooltip: getFeatureTooltip('gp', 'Could not access giveaway'),
85 | name: '',
86 | switchTo: { onReturn: 3 },
87 | onClick,
88 | },
89 | {
90 | template: 'loading',
91 | isDisabled: true,
92 | name: '',
93 | },
94 | ],
95 | }).insert(giveaway.panel, 'beforeend');
96 | button.nodes.outer.setAttribute('data-draggable-id', 'gp');
97 | }
98 | });
99 | }
100 | }
101 |
102 | const giveawaysGiveawayPopup = new GiveawaysGiveawayPopup();
103 |
104 | export { giveawaysGiveawayPopup };
105 |
--------------------------------------------------------------------------------
/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/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/Giveaways/NewGiveawayDescriptionChecker.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class GiveawaysNewGiveawayDescriptionChecker extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 |
12 | When you click on "Review Giveaway" in the new giveaway page (also extends to the
13 | "Create Giveaway" button from and the "Add"
14 | button from ), this feature checks if there are
15 | possible Steam keys / Humble Bundle gift links in the description and warns you about
16 | it, in case you pasted them there by mistake.
17 |
18 |
19 | This feature replaces the native "Review Giveaway" button, so that ESGST can intercept
20 | the click.
21 |
22 |
23 | ),
24 | id: 'ngdc',
25 | name: 'New Giveaway Description Checker',
26 | sg: true,
27 | type: 'giveaways',
28 | };
29 | }
30 |
31 | init() {
32 | if (!Shared.esgst.newGiveawayPath) {
33 | return;
34 | }
35 |
36 | const reviewButton = document.querySelector('.js__submit-form');
37 | const textArea = document.querySelector(`[name=description]`);
38 |
39 | if (!reviewButton || !textArea) {
40 | return;
41 | }
42 |
43 | reviewButton.classList.remove('js__submit-form');
44 |
45 | const newReviewButton = reviewButton.cloneNode(true);
46 | reviewButton.parentElement.insertBefore(newReviewButton, reviewButton);
47 | reviewButton.remove();
48 | newReviewButton.setAttribute('data-esgst', 'reviewButton');
49 |
50 | newReviewButton.addEventListener('click', async () => {
51 | if (await this.check(textArea.value)) {
52 | return;
53 | }
54 |
55 | const form = newReviewButton.closest('form');
56 | if (newReviewButton.classList.contains('js__edit-giveaway')) {
57 | form.querySelector(`[name=next_step]`).value = 1;
58 | }
59 | form.submit();
60 | });
61 | }
62 |
63 | check(value) {
64 | return new Promise(async (resolve) => {
65 | let message;
66 |
67 | if (value.match(/[\d\w]{5}(-[\d\w]{5}){2,}/)) {
68 | message = 'There appears to be a Steam key in the description of the giveaway.';
69 | } else if (value.match(/https?:\/\/(www\.)?humblebundle\.com\/gift/)) {
70 | message =
71 | 'There appears to be a Humble Bundle gift link in the description of the giveaway.';
72 | }
73 |
74 | if (message) {
75 | message = `${message} Are you sure you want to continue?`;
76 | await Shared.common.createConfirmation(
77 | message,
78 | () => resolve(false),
79 | () => resolve(true)
80 | );
81 | } else {
82 | resolve(false);
83 | }
84 | });
85 | }
86 | }
87 |
88 | const giveawaysNewGiveawayDescriptionChecker = new GiveawaysNewGiveawayDescriptionChecker();
89 |
90 | export { giveawaysNewGiveawayDescriptionChecker };
91 |
--------------------------------------------------------------------------------
/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/QuickGiveawaySearch.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { common } from '../Common';
3 | import { Settings } from '../../class/Settings';
4 | import { Shared } from '../../class/Shared';
5 | import { DOM } from '../../class/DOM';
6 |
7 | const createElements = common.createElements.bind(common),
8 | getFeatureTooltip = common.getFeatureTooltip.bind(common);
9 | class GiveawaysQuickGiveawaySearch extends Module {
10 | constructor() {
11 | super();
12 | this.info = {
13 | description: () => (
14 |
15 |
16 | Adds a search box before the "Giveaways" box at the header of any page that allows you
17 | to quickly search for giveaways from any page.
18 |
19 |
20 | Has built-in.
21 |
22 |
23 | ),
24 | features: {
25 | qgs_h: {
26 | name: 'Hide the native search on the main page.',
27 | sg: true,
28 | },
29 | },
30 | id: 'qgs',
31 | name: 'Quick Giveaway Search',
32 | options: {
33 | title: `Position:`,
34 | values: ['Left', 'Right'],
35 | },
36 | sg: true,
37 | type: 'giveaways',
38 | };
39 | }
40 |
41 | init() {
42 | let container = createElements(
43 | Shared.header.nodes.leftNav,
44 | Settings.get('qgs_index') === 0 ? 'afterbegin' : 'beforeend',
45 | [
46 | {
47 | attributes: {
48 | class: 'esgst-qgs-container',
49 | title: getFeatureTooltip('qgs'),
50 | },
51 | type: 'div',
52 | children: [
53 | {
54 | attributes: {
55 | class: 'esgst-qgs-input',
56 | placeholder: 'Search...',
57 | type: 'text',
58 | },
59 | type: 'input',
60 | },
61 | {
62 | attributes: {
63 | class: 'fa fa-search',
64 | },
65 | type: 'i',
66 | },
67 | ],
68 | },
69 | ]
70 | );
71 | container.addEventListener('mouseenter', this.qgs_expand.bind(this));
72 | container.addEventListener('mouseleave', this.qgs_collapse.bind(this));
73 | container.firstElementChild.addEventListener('keypress', this.qgs_trigger.bind(this));
74 | if (Settings.get('qgs_h') && this.esgst.giveawaysPath) {
75 | document.getElementsByClassName('sidebar__search-container')[0].remove();
76 | }
77 | }
78 |
79 | qgs_expand(event) {
80 | event.currentTarget.classList.add('esgst-qgs-container-expanded');
81 | }
82 |
83 | qgs_collapse(event) {
84 | if (event.relatedTarget && event.relatedTarget.closest('.esgst-popout')) return;
85 | event.currentTarget.classList.remove('esgst-qgs-container-expanded');
86 | }
87 |
88 | qgs_trigger(event) {
89 | if (event.key !== 'Enter') return;
90 | event.preventDefault();
91 | window.location.href = `/giveaways/search?q=${encodeURIComponent(event.currentTarget.value)}`;
92 | }
93 | }
94 |
95 | const giveawaysQuickGiveawaySearch = new GiveawaysQuickGiveawaySearch();
96 |
97 | export { giveawaysQuickGiveawaySearch };
98 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/UnfadedEnteredGiveaway.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { DOM } from '../../class/DOM';
3 |
4 | class GiveawaysUnfadedEnteredGiveaway extends Module {
5 | constructor() {
6 | super();
7 | this.info = {
8 | description: () => (
9 |
10 | Removes SteamGifts' default fade for entered giveaways.
11 |
12 | ),
13 | id: 'ueg',
14 | name: 'Unfaded Entered Giveaway',
15 | sg: true,
16 | type: 'giveaways',
17 | featureMap: {
18 | endless: this.ueg_remove.bind(this),
19 | },
20 | };
21 | }
22 |
23 | ueg_remove(context, main, source, endless) {
24 | const elements = context.querySelectorAll(
25 | `${
26 | endless
27 | ? `.esgst-es-page-${endless} .giveaway__row-inner-wrap.is-faded, .esgst-es-page-${endless}.giveaway__row-inner-wrap.is-faded`
28 | : '.giveaway__row-inner-wrap.is-faded'
29 | }`
30 | );
31 | for (let i = 0, n = elements.length; i < n; ++i) {
32 | elements[i].classList.add('esgst-ueg');
33 | }
34 | }
35 | }
36 |
37 | const giveawaysUnfadedEnteredGiveaway = new GiveawaysUnfadedEnteredGiveaway();
38 |
39 | export { giveawaysUnfadedEnteredGiveaway };
40 |
--------------------------------------------------------------------------------
/src/modules/Giveaways/UnhideGiveawayButton.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 | getFeatureTooltip = common.getFeatureTooltip.bind(common),
7 | unhideGame = common.unhideGame.bind(common);
8 | class GiveawaysUnhideGiveawayButton extends Module {
9 | constructor() {
10 | super();
11 | this.info = {
12 | description: () => (
13 |
14 |
15 | Adds a button ( ) next to a giveaway's game name (in any
16 | page), if you have hidden the game on SteamGifts, that allows you to unhide the game
17 | without having to access your{' '}
18 |
19 | giveaway filters
20 | {' '}
21 | page.
22 |
23 |
24 | ),
25 | id: 'ugb',
26 | name: 'Unhide Giveaway Button',
27 | sg: true,
28 | type: 'giveaways',
29 | featureMap: {
30 | giveaway: this.ugb_add.bind(this),
31 | },
32 | };
33 | }
34 |
35 | ugb_add(giveaways, main) {
36 | giveaways.forEach((giveaway) => {
37 | let hideButton = giveaway.innerWrap.querySelector(
38 | `.giveaway__hide, .featured__giveaway__hide`
39 | );
40 | if (!hideButton && (!main || this.esgst.giveawaysPath || this.esgst.giveawayPath)) {
41 | if (this.esgst.giveawayPath && main) {
42 | hideButton = createElements(giveaway.headingName, 'afterend', [
43 | {
44 | type: 'a',
45 | children: [
46 | {
47 | attributes: {
48 | class: 'fa fa-eye giveaway__hide',
49 | title: getFeatureTooltip('ugb', 'Unhide all giveaways for this game'),
50 | },
51 | type: 'i',
52 | },
53 | ],
54 | },
55 | ]);
56 | } else {
57 | hideButton = createElements(giveaway.headingName, 'afterend', [
58 | {
59 | attributes: {
60 | class: 'fa fa-eye giveaway__hide giveaway__icon',
61 | title: getFeatureTooltip('ugb', 'Unhide all giveaways for this game'),
62 | },
63 | type: 'i',
64 | },
65 | ]);
66 | }
67 | hideButton.addEventListener(
68 | 'click',
69 | unhideGame.bind(
70 | common,
71 | hideButton,
72 | giveaway.gameId,
73 | giveaway.name,
74 | giveaway.id,
75 | giveaway.type
76 | )
77 | );
78 | }
79 | });
80 | }
81 | }
82 |
83 | const giveawaysUnhideGiveawayButton = new GiveawaysUnhideGiveawayButton();
84 |
85 | export { giveawaysUnhideGiveawayButton };
86 |
--------------------------------------------------------------------------------
/src/modules/Giveaways_addToStorage.js:
--------------------------------------------------------------------------------
1 | import { Module } from '../class/Module';
2 | import { common } from './Common';
3 | import { Settings } from '../class/Settings';
4 |
5 | const addGiveawayToStorage = common.addGiveawayToStorage.bind(common);
6 | class Giveaways_addToStorage extends Module {
7 | constructor() {
8 | super();
9 | this.info = {
10 | endless: true,
11 | id: 'giveaways_addToStorage',
12 | };
13 | }
14 |
15 | init() {
16 | if (
17 | (Settings.get('lpv') ||
18 | Settings.get('cewgd') ||
19 | (Settings.get('gc') && Settings.get('gc_gi'))) &&
20 | this.esgst.giveawayPath &&
21 | document.referrer === `https://www.steamgifts.com/giveaways/new`
22 | ) {
23 | addGiveawayToStorage();
24 | }
25 | }
26 | }
27 |
28 | const giveaways_addToStorage = new Giveaways_addToStorage();
29 |
30 | export { giveaways_addToStorage };
31 |
--------------------------------------------------------------------------------
/src/modules/Groups.js:
--------------------------------------------------------------------------------
1 | import { Module } from '../class/Module';
2 | import { Scope } from '../class/Scope';
3 | import { Settings } from '../class/Settings';
4 |
5 | class Groups extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | endless: true,
10 | id: 'groups',
11 | featureMap: {
12 | endless: this.groups_load.bind(this),
13 | },
14 | };
15 | }
16 |
17 | async groups_load(context, main, source, endless) {
18 | const elements = context.querySelectorAll(
19 | `${
20 | endless
21 | ? `.esgst-es-page-${endless} a[href*="/group/"], .esgst-es-page-${endless}a[href*="/group/"]`
22 | : `a[href*="/group/"]`
23 | }, .form_list_item_summary_name`
24 | );
25 | if (!elements.length) {
26 | return;
27 | }
28 | const groups = [];
29 | for (const element of elements) {
30 | if (!element.textContent || element.children.length || element.closest('.markdown')) {
31 | continue;
32 | }
33 | const group = {
34 | saved: null,
35 | url: element.getAttribute('href'),
36 | };
37 | if (group.url) {
38 | const match = group.url.match(/\/group\/(.+?)\//);
39 | if (match) {
40 | group.id = match[1];
41 | group.saved = this.esgst.groups.filter((x) => x.code === group.id)[0];
42 | }
43 | }
44 | if (!group.id) {
45 | const avatarImage = element.parentElement.previousElementSibling;
46 | const avatar = avatarImage.style.backgroundImage;
47 | group.saved = this.esgst.groups.filter((x) => avatar.match(x.avatar))[0];
48 | group.id = group.saved && group.saved.code;
49 | }
50 | if (!group.id) {
51 | continue;
52 | }
53 | group.code = group.id;
54 | if (!this.esgst.currentGroups[group.id]) {
55 | this.esgst.currentGroups[group.id] = {
56 | elements: [],
57 | savedGroup: group.saved,
58 | };
59 | }
60 | if (this.esgst.currentGroups[group.id].elements.indexOf(element) > -1) {
61 | continue;
62 | }
63 | group.name = element.textContent.trim();
64 | const container = element.parentElement;
65 | group.oldElement = element;
66 | if (this.esgst.groupPath && container.classList.contains('page__heading__breadcrumbs')) {
67 | group.element = document.getElementsByClassName('featured__heading__medium')[0];
68 | group.container = group.element.parentElement;
69 | } else {
70 | group.element = element;
71 | group.container = container;
72 | }
73 | group.context = group.element;
74 | this.esgst.currentGroups[group.id].elements.push(group.element);
75 | group.innerWrap = element.closest('.table__row-inner-wrap') || group.container;
76 | group.outerWrap = element.closest('.table__row-outer-wrap') || group.container;
77 | const isHeading = group.context.classList.contains('featured__heading__medium');
78 | if (isHeading) {
79 | group.tagContext = group.container;
80 | group.tagPosition = 'beforeend';
81 | } else {
82 | group.tagContext = group.context;
83 | group.tagPosition = 'afterend';
84 | }
85 | groups.push(group);
86 | }
87 | Scope.addData('current', 'groups', groups, endless);
88 | if (
89 | main &&
90 | this.esgst.gpf &&
91 | this.esgst.gpf.filteredCount &&
92 | Settings.get(`gpf_enable${this.esgst.gpf.type}`)
93 | ) {
94 | this.esgst.modules.groupsGroupFilters.filters_filter(this.esgst.gpf, false, endless);
95 | }
96 | for (const feature of this.esgst.groupFeatures) {
97 | await feature(groups, main, source, endless);
98 | }
99 | }
100 | }
101 |
102 | const groupsModule = new Groups();
103 |
104 | export { groupsModule };
105 |
--------------------------------------------------------------------------------
/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/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/Trades/HeaderTradesButton.jsx:
--------------------------------------------------------------------------------
1 | import { Module } from '../../class/Module';
2 | import { Shared } from '../../class/Shared';
3 | import { DOM } from '../../class/DOM';
4 |
5 | class TradesHeaderTradesButton extends Module {
6 | constructor() {
7 | super();
8 | this.info = {
9 | description: () => (
10 |
11 | Brings back the Trades button to the SteamGifts header.
12 |
13 | ),
14 | id: 'htb',
15 | name: 'Header Trades Button',
16 | sg: true,
17 | type: 'trades',
18 | };
19 | }
20 |
21 | init() {
22 | const tradesButton = Shared.header.addButtonContainer({
23 | buttonName: 'Trades',
24 | position: 'beforeend',
25 | openInNewTab: true,
26 | side: 'left',
27 | url: 'https://www.steamtrades.com',
28 | });
29 | Shared.header.nodes.leftNav.insertBefore(
30 | tradesButton.nodes.outer,
31 | Shared.header.buttonContainers.discussions.nodes.outer
32 | );
33 | }
34 | }
35 |
36 | const tradesHeaderTradesButton = new TradesHeaderTradesButton();
37 |
38 | export { tradesHeaderTradesButton };
39 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/permissions.tsx:
--------------------------------------------------------------------------------
1 | import { DOM } from './class/DOM';
2 | import { permissions } from './class/Permissions';
3 | import { Utils } from './lib/jsUtils';
4 |
5 | const grantedPermissions = new Set();
6 | const deniedPermissions = new Set();
7 | let messageNode: HTMLElement | undefined;
8 |
9 | const loadPermissions = async (): Promise
=> {
10 | const params = Utils.getQueryParams();
11 | const rows = [];
12 | const keys = params.keys ? params.keys.split(',') : Object.keys(permissions.permissions);
13 | for (const key of keys) {
14 | const permission = permissions.permissions[key];
15 | if (!permission) {
16 | continue;
17 | }
18 | const permissionCell = permission.values.map((value) => [value, ]).flat();
19 | const usageCell = Object.values(permission.messages)
20 | .map((value) => [value, , ])
21 | .flat();
22 | let checkboxNode: HTMLInputElement | undefined;
23 | rows.push(
24 |
25 | {params.keys ? null : (
26 |
27 | (checkboxNode = ref)} />
28 |
29 | )}
30 | {permissionCell}
31 | {usageCell}
32 |
33 | );
34 | if (params.keys) {
35 | grantedPermissions.add(key);
36 | }
37 | if (!checkboxNode) {
38 | continue;
39 | }
40 | checkboxNode.checked = await permissions.contains([[key]]);
41 | checkboxNode.addEventListener('change', () => {
42 | if (checkboxNode?.checked) {
43 | grantedPermissions.add(key);
44 | deniedPermissions.delete(key);
45 | } else {
46 | grantedPermissions.delete(key);
47 | deniedPermissions.add(key);
48 | }
49 | });
50 | }
51 | DOM.insert(
52 | document.body,
53 | 'beforeend',
54 |
55 |
56 |
57 | {params.keys ? null : Granted }
58 | Permission
59 | Usage
60 |
61 | {rows}
62 |
63 |
64 | {params.keys ? 'Grant' : 'Save'}
65 |
66 | (messageNode = ref)}>
67 |
68 | );
69 | };
70 |
71 | const savePermissions = () => {
72 | permissions.request(Array.from(grantedPermissions), (granted: boolean) => {
73 | permissions.remove(Array.from(deniedPermissions), (denied: boolean) => {
74 | if (!messageNode) {
75 | return;
76 | }
77 | if (granted && denied) {
78 | messageNode.textContent = 'Permissions saved!';
79 | } else {
80 | messageNode.textContent = 'Error saving permissions!';
81 | }
82 | window.setTimeout(() => {
83 | if (messageNode) {
84 | messageNode.textContent = '';
85 | }
86 | }, 2000);
87 | });
88 | });
89 | };
90 |
91 | loadPermissions();
92 |
--------------------------------------------------------------------------------
/src/types/Footer.type.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} IFooterNodes
3 | * @property {HTMLElement} inner
4 | * @property {HTMLElement} leftNav
5 | * @property {HTMLElement} nav
6 | * @property {HTMLElement} outer
7 | * @property {HTMLElement} rightNav
8 | */
9 |
10 | /**
11 | * @typedef {Object} IFooterLinkContainer
12 | * @property {Object} nodes
13 | * @property {HTMLElement} [nodes.icon]
14 | * @property {HTMLElement} [nodes.link]
15 | * @property {HTMLElement} nodes.outer
16 | * @property {Object} data
17 | * @property {string} data.id
18 | * @property {string} [data.icon]
19 | * @property {string} data.name
20 | * @property {string} [data.url]
21 | */
22 |
23 | /**
24 | * @typedef {Object} IFooterLinkContainerParams
25 | * @property {HTMLElement} [context]
26 | * @property {string} [icon]
27 | * @property {string} name
28 | * @property {string} [position]
29 | * @property {'left' | 'right'} [side]
30 | * @property {string} [url]
31 | */
32 |
--------------------------------------------------------------------------------
/src/types/Header.type.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} IHeaderNodes
3 | * @property {HTMLElement} inner
4 | * @property {HTMLElement} leftNav
5 | * @property {HTMLElement} [logo]
6 | * @property {HTMLElement} nav
7 | * @property {HTMLElement} outer
8 | * @property {HTMLElement} rightNav
9 | */
10 |
11 | /**
12 | * @typedef {Object} IHeaderButtonContainer
13 | * @property {Object} nodes
14 | * @property {HTMLElement} [nodes.absoluteDropdown]
15 | * @property {HTMLElement} [nodes.arrow]
16 | * @property {HTMLElement} [nodes.arrowIcon]
17 | * @property {HTMLElement} [nodes.button]
18 | * @property {HTMLElement} [nodes.buttonIcon]
19 | * @property {HTMLElement} [nodes.buttonImage]
20 | * @property {HTMLElement} [nodes.buttonName]
21 | * @property {HTMLElement} [nodes.counter]
22 | * @property {HTMLElement} [nodes.level]
23 | * @property {HTMLElement} nodes.outer
24 | * @property {HTMLElement} [nodes.points]
25 | * @property {HTMLElement} [nodes.relativeDropdown]
26 | * @property {HTMLElement} [nodes.reputation]
27 | * @property {Object} data
28 | * @property {string} data.id
29 | * @property {string} [data.buttonIcon]
30 | * @property {string} [data.buttonImage]
31 | * @property {string} data.buttonName
32 | * @property {number} data.counter
33 | * @property {boolean} [data.isActive]
34 | * @property {boolean} [data.isDropdown]
35 | * @property {boolean} data.isFlashing
36 | * @property {boolean} [data.isNotification]
37 | * @property {ILevel} data.level
38 | * @property {number} data.points
39 | * @property {IReputation} data.reputation
40 | * @property {string} [data.url]
41 | * @property {Object} [dropdownItems]
42 | * @property {import('../models/User').User} [user]
43 | */
44 |
45 | /**
46 | * @typedef {Object} IHeaderButtonContainerParams
47 | * @property {string} [buttonIcon]
48 | * @property {string} [buttonImage]
49 | * @property {string} buttonName
50 | * @property {string} [context]
51 | * @property {number} [counter]
52 | * @property {boolean} [isActive]
53 | * @property {boolean} [isDropdown]
54 | * @property {boolean} [isFlashing]
55 | * @property {boolean} [isNotification]
56 | * @property {Function} [onClick]
57 | * @property {boolean} [openInNewTab]
58 | * @property {string} [position]
59 | * @property {'left' | 'right'} [side]
60 | * @property {string} [url]
61 | * @property {Object} [dropdownItems]
62 | */
63 |
64 | /**
65 | * @typedef {Object} IHeaderDropdownItem
66 | * @property {Object} nodes
67 | * @property {HTMLElement} [nodes.description]
68 | * @property {HTMLElement} nodes.icon
69 | * @property {HTMLElement} nodes.name
70 | * @property {HTMLElement} nodes.outer
71 | * @property {HTMLElement} [nodes.summary]
72 | * @property {Object} data
73 | * @property {string} data.id
74 | * @property {string} [data.description]
75 | * @property {string} data.icon
76 | * @property {string} data.name
77 | * @property {string} data.url
78 | */
79 |
80 | /**
81 | * @typedef {Object} IHeaderDropdownItemParams
82 | * @property {string} [buttonContainerId]
83 | * @property {string} [description]
84 | * @property {string} icon
85 | * @property {string} name
86 | * @property {Function} [onClick]
87 | * @property {boolean} [openInNewTab]
88 | * @property {string} url
89 | */
90 |
--------------------------------------------------------------------------------
/src/types/HeaderRefresher.type.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} IHeaderRefresherCache
3 | * @property {number} created
4 | * @property {ILevel} level
5 | * @property {number} messages
6 | * @property {number} newWishlist
7 | * @property {number} points
8 | * @property {number} timestamp
9 | * @property {string} username
10 | * @property {number} wishlist
11 | * @property {number} won
12 | * @property {boolean} wonDelivered
13 | */
14 |
--------------------------------------------------------------------------------
/src/types/Session.type.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ISessionCounters
3 | * @property {number} created
4 | * @property {ILevel} level
5 | * @property {number} messages
6 | * @property {number} points
7 | * @property {IReputation} reputation
8 | * @property {number} won
9 | * @property {boolean} wonDelivered
10 | */
11 |
--------------------------------------------------------------------------------
/src/types/common.type.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ILevel
3 | * @property {number} base
4 | * @property {number} full
5 | */
6 |
7 | /**
8 | * @typedef {Object} IReputation
9 | * @property {number} positive
10 | * @property {number} negative
11 | */
12 |
--------------------------------------------------------------------------------
/test-helpers/fixture-loader.ts:
--------------------------------------------------------------------------------
1 | const loadFixture = (fixture: string): Element => {
2 | document.body.insertAdjacentHTML('afterbegin', '
');
3 | const fixtureEl = document.body.children[0];
4 | fixtureEl.innerHTML = fixture;
5 | return fixtureEl;
6 | };
7 |
8 | export { loadFixture };
9 |
--------------------------------------------------------------------------------
/test/fixtures/sg/notification-bar.html:
--------------------------------------------------------------------------------
1 | Success. Synced with Steam.
2 |
--------------------------------------------------------------------------------
/test/fixtures/st/notification-bar.html:
--------------------------------------------------------------------------------
1 | Thanks for helping! It looks like you voted on 1 review .
2 |
--------------------------------------------------------------------------------
/test/modules/General/FixedHeader.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { Shared } from '../../../src/class/Shared';
3 | import { Header, IHeader } from '../../../src/components/Header';
4 | import { Namespaces } from '../../../src/constants/Namespaces';
5 | import { generalFixedHeader } from '../../../src/modules/General/FixedHeader';
6 | import { loadFixture } from '../../../test-helpers/fixture-loader';
7 | import sgHeaderFixture from '../../fixtures/sg/header.html';
8 | import stHeaderFixture from '../../fixtures/st/header.html';
9 |
10 | let fixtureEl: Element;
11 |
12 | describe('Fixed Header', () => {
13 | describe('on SG', () => {
14 | describe('when there is a header', () => {
15 | before(() => {
16 | fixtureEl = loadFixture(sgHeaderFixture);
17 | Shared.header = Header(Namespaces.SG);
18 | Shared.header.parse(document.body);
19 | });
20 |
21 | after(() => {
22 | fixtureEl.remove();
23 | });
24 |
25 | it('should load successfully', () => {
26 | expect(Shared.header).to.be.instanceOf(IHeader);
27 | generalFixedHeader.init();
28 | expect(Array.from(Shared.header.nodes.outer.classList)).to.include('esgst-fh');
29 | });
30 | });
31 |
32 | describe('when there is no header', () => {
33 | before(() => {
34 | Shared.header = Header(Namespaces.SG);
35 | });
36 |
37 | after(() => {
38 | fixtureEl.remove();
39 | });
40 |
41 | it('should silently fail to load', () => {
42 | expect(Shared.header.nodes.outer).to.be.null;
43 | generalFixedHeader.init();
44 | });
45 | });
46 | });
47 |
48 | describe('on ST', () => {
49 | describe('when there is a header', () => {
50 | before(() => {
51 | fixtureEl = loadFixture(stHeaderFixture);
52 | Shared.header = Header(Namespaces.ST);
53 | Shared.header.parse(document.body);
54 | });
55 |
56 | after(() => {
57 | fixtureEl.remove();
58 | });
59 |
60 | it('should load successfully', () => {
61 | expect(Shared.header).to.be.instanceOf(IHeader);
62 | generalFixedHeader.init();
63 | expect(Array.from(Shared.header.nodes.outer.classList)).to.include('esgst-fh');
64 | });
65 | });
66 |
67 | describe('when there is no header', () => {
68 | before(() => {
69 | Shared.header = Header(Namespaces.SG);
70 | });
71 |
72 | after(() => {
73 | fixtureEl.remove();
74 | });
75 |
76 | it('should silently fail to load', () => {
77 | expect(Shared.header.nodes.outer).to.be.null;
78 | generalFixedHeader.init();
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["./node_modules", "./build", "./dist"],
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "jsx": "react",
7 | "jsxFactory": "DOM.element",
8 | "module": "CommonJS",
9 | "outDir": "./build",
10 | "sourceMap": true,
11 | "target": "ES2020",
12 | "strict": true,
13 | "typeRoots": ["./node_modules/@types", "./node_modules/web-ext-types"],
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------