├── src ├── _locales │ └── en │ │ └── messages.json ├── entry │ ├── eventPage_index.js │ ├── eventPage_sdk_index.js │ ├── permissions_index.js │ ├── index.js │ ├── sdk_index.js │ ├── eventPage_sdk_banner.js │ ├── gm_index.js │ └── monkey_banner.js ├── browser-webext.js ├── assets │ ├── images │ │ ├── icon.png │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-32.png │ │ └── icon-64.png │ └── styles │ │ ├── index.js │ │ └── patches │ │ └── query-builder.css ├── constants │ ├── Namespaces.js │ ├── Events.js │ └── ClassNames.ts ├── html │ ├── options.html │ └── permissions.html ├── browser.ts ├── class │ ├── Tabs.js │ ├── LocalStorage.js │ ├── I18N.js │ ├── Shared.js │ ├── ICloudStorage.js │ ├── Module.js │ ├── Lock.ts │ ├── EventDispatcher.js │ ├── Session.js │ ├── PermissionsUi.tsx │ ├── Button.jsx │ ├── Logger.jsx │ └── ToggleSwitch.jsx ├── types │ ├── common.type.js │ ├── Session.type.js │ ├── HeaderRefresher.type.js │ ├── Footer.type.js │ └── Header.type.js ├── modules │ ├── General │ │ ├── URLRedirector.jsx │ │ ├── NarrowSidebar.jsx │ │ ├── FixedFooter.jsx │ │ ├── FixedHeader.jsx │ │ ├── SameTabOpener.jsx │ │ ├── FixedSidebar.jsx │ │ ├── SearchClearButton.jsx │ │ ├── VisibleFullLevel.jsx │ │ ├── HiddenCommunityPoll.jsx │ │ ├── FixedMainPageHeading.jsx │ │ ├── HiddenBlacklistStats.jsx │ │ ├── SearchMagnifyingGlassButton.jsx │ │ ├── AttachedImageLoader.jsx │ │ ├── VisibleAttachedImages.jsx │ │ ├── TimeToPointCapCalculator.jsx │ │ ├── PageLoadTimestamp.jsx │ │ ├── PaginationNavigationOnTop.jsx │ │ ├── ImageBorders.jsx │ │ ├── ElementFilters.jsx │ │ ├── ScrollToTopButton.jsx │ │ ├── ScrollToBottomButton.jsx │ │ └── EmbeddedVideos.jsx │ ├── Giveaways_addToStorage.js │ ├── DiscussionPanels.js │ ├── Trades │ │ └── HeaderTradesButton.jsx │ ├── Comments │ │ ├── CommentReverser.jsx │ │ ├── ReplyBoxOnTop.jsx │ │ ├── ReplyMentionLink.tsx │ │ ├── ReplyBoxPopup.jsx │ │ └── ReceivedReplyBoxPopup.jsx │ ├── Giveaways │ │ ├── UnfadedEnteredGiveaway.jsx │ │ ├── GiveawayEndTimeHighlighter.jsx │ │ ├── GiveawayLevelHighlighter.jsx │ │ ├── GiveawayCopyHighlighter.jsx │ │ ├── PinnedGiveawaysButton.jsx │ │ ├── CommunityWishlistSearchLink.jsx │ │ ├── HiddenGamesEnterButtonDisabler.jsx │ │ ├── GiveawayWinnersLink.jsx │ │ ├── UnhideGiveawayButton.jsx │ │ ├── QuickGiveawaySearch.jsx │ │ ├── NewGiveawayDescriptionChecker.jsx │ │ ├── DeleteKeyConfirmation.jsx │ │ ├── GiveawayPopup.jsx │ │ ├── EnteredGiveawaysStats.jsx │ │ ├── GiveawayErrorSearchLinks.tsx │ │ └── CustomGiveawayBackground.jsx │ ├── Discussions │ │ ├── ReversedActiveDiscussions.jsx │ │ ├── MainPostSkipper.jsx │ │ ├── DiscussionTags.jsx │ │ ├── RefreshActiveDiscussionsButton.jsx │ │ ├── CloseOpenDiscussionButton.jsx │ │ ├── MainPostPopup.jsx │ │ └── DiscussionsSorter.jsx │ ├── Users │ │ ├── VisibleRealCV.jsx │ │ ├── LevelUpCalculator.jsx │ │ ├── UserTags.jsx │ │ ├── SteamFriendsIndicator.jsx │ │ ├── RealWonSentCVLink.jsx │ │ ├── SteamGiftsProfileButton.jsx │ │ ├── UserLinks.jsx │ │ └── VisibleGiftsBreakdown.jsx │ ├── Games │ │ ├── GameTags.jsx │ │ └── EnteredGameHighlighter.jsx │ ├── Groups │ │ ├── GroupTags.jsx │ │ └── GroupHighlighter.jsx │ └── Groups.js ├── dependencies.ts ├── browser-sdk.js ├── components │ ├── Collapsible.tsx │ └── Base.tsx ├── permissions.tsx └── models │ ├── Base.tsx │ └── CommentEntity.tsx ├── .prettierignore ├── html.d.ts ├── .eslintignore ├── .prettierrc.json ├── assets ├── chrome-badge.png ├── firefox-badge.png └── palemoon-badge.png ├── .gitignore ├── test ├── fixtures │ ├── sg │ │ └── notification-bar.html │ └── st │ │ └── notification-bar.html └── modules │ └── General │ └── FixedHeader.test.ts ├── .editorconfig ├── .lintstagedrc.js ├── test-helpers └── fixture-loader.ts ├── .gitlab-ci.yml ├── scripts ├── common.js └── generateCommit.js ├── tsconfig.json ├── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ ├── enhancement-request.md │ └── bug.md └── workflows │ ├── beta.yml │ └── stable.yml ├── .babelrc ├── karma.conf.js ├── LICENSE ├── messages.json ├── browser └── messages.json ├── .eslintrc.js ├── .eslintrc.typed.js └── PRIVACY_POLICY /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build/* 3 | dist/* 4 | -------------------------------------------------------------------------------- /src/entry/eventPage_index.js: -------------------------------------------------------------------------------- 1 | import '../eventPage'; 2 | -------------------------------------------------------------------------------- /src/entry/eventPage_sdk_index.js: -------------------------------------------------------------------------------- 1 | import '../eventPage_sdk'; 2 | -------------------------------------------------------------------------------- /src/entry/permissions_index.js: -------------------------------------------------------------------------------- 1 | import '../browser-webext'; 2 | import '../permissions'; 3 | -------------------------------------------------------------------------------- /html.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /src/browser-webext.js: -------------------------------------------------------------------------------- 1 | import { setBrowser } from './browser'; 2 | 3 | setBrowser(browser); 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | !.eslintrc.js 4 | !.eslintrc.typed.js 5 | !.lintstagedrc.js 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /assets/chrome-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/assets/chrome-badge.png -------------------------------------------------------------------------------- /src/entry/index.js: -------------------------------------------------------------------------------- 1 | import '../dependencies'; 2 | import '../browser-webext'; 3 | import '../main'; 4 | -------------------------------------------------------------------------------- /src/entry/sdk_index.js: -------------------------------------------------------------------------------- 1 | import '../dependencies'; 2 | import '../browser-sdk'; 3 | import '../main'; 4 | -------------------------------------------------------------------------------- /assets/firefox-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/assets/firefox-badge.png -------------------------------------------------------------------------------- /assets/palemoon-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/assets/palemoon-badge.png -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /src/assets/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/src/assets/images/icon-128.png -------------------------------------------------------------------------------- /src/assets/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/src/assets/images/icon-16.png -------------------------------------------------------------------------------- /src/assets/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/src/assets/images/icon-32.png -------------------------------------------------------------------------------- /src/assets/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/HEAD/src/assets/images/icon-64.png -------------------------------------------------------------------------------- /src/constants/Namespaces.js: -------------------------------------------------------------------------------- 1 | const Namespaces = { 2 | SG: 0, 3 | ST: 1, 4 | }; 5 | 6 | export { Namespaces }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | **/build 3 | **/dist 4 | **/coverage 5 | **/docs 6 | **/node_modules 7 | **/config.js 8 | **/package-lock.json 9 | -------------------------------------------------------------------------------- /src/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/sg/notification-bar.html: -------------------------------------------------------------------------------- 1 |
Success. Synced with Steam.
2 | -------------------------------------------------------------------------------- /src/assets/styles/index.js: -------------------------------------------------------------------------------- 1 | // That's custom build, so we import local version 2 | import './bootstrap/index.css'; 3 | 4 | import './patches/query-builder.css'; 5 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | let _browser: typeof browser; 2 | 3 | const setBrowser = (__browser: typeof browser) => { 4 | _browser = __browser; 5 | }; 6 | 7 | export { setBrowser, _browser as browser }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.{yml,yaml}] 10 | indent_style = space 11 | -------------------------------------------------------------------------------- /test/fixtures/st/notification-bar.html: -------------------------------------------------------------------------------- 1 |
Thanks for helping! It looks like you voted on 1 review.
2 | -------------------------------------------------------------------------------- /src/class/Tabs.js: -------------------------------------------------------------------------------- 1 | import { browser } from '../browser'; 2 | 3 | class _Tabs { 4 | open(url) { 5 | return browser.runtime.sendMessage({ 6 | action: 'open_tab', 7 | url, 8 | }); 9 | } 10 | } 11 | 12 | export const Tabs = new _Tabs(); 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{json,css,html,md,yml,yaml}': 'prettier --write', 3 | '*.{js,jsx,ts,tsx}': (filenames) => [ 4 | 'tsc --noEmit -p ./tsconfig.json', 5 | `eslint --fix --quiet -c ./.eslintrc.typed.js --no-eslintrc ${filenames.join(' ')}`, 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: alpine:latest 2 | 3 | pages: 4 | stage: deploy 5 | script: 6 | - echo 'Nothing to do...' 7 | artifacts: 8 | paths: 9 | - public/ 10 | rules: 11 | - if: '$CI_COMMIT_REF_NAME == "main"' 12 | changes: 13 | - public/* 14 | when: always 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | function getArguments(process) { 2 | const args = {}; 3 | 4 | const argv = process.argv.slice(2); 5 | 6 | for (const arg of argv) { 7 | const parts = arg.split(/=/); 8 | const key = parts[0]; 9 | const value = parts[1] || true; 10 | 11 | args[key] = value; 12 | } 13 | 14 | return args; 15 | } 16 | 17 | module.exports = { getArguments }; 18 | -------------------------------------------------------------------------------- /src/class/LocalStorage.js: -------------------------------------------------------------------------------- 1 | class _LocalStorage { 2 | set(key, value) { 3 | window.localStorage.setItem(`esgst_${key}`, value); 4 | } 5 | 6 | get(key, value = undefined) { 7 | return window.localStorage.getItem(`esgst_${key}`) || value; 8 | } 9 | 10 | delete(key) { 11 | window.localStorage.removeItem(`esgst_${key}`); 12 | } 13 | } 14 | 15 | const LocalStorage = new _LocalStorage(); 16 | 17 | export { LocalStorage }; 18 | -------------------------------------------------------------------------------- /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/constants/Events.js: -------------------------------------------------------------------------------- 1 | const Events = { 2 | CREATED_UPDATED: 0, 3 | WON_UPDATED: 1, 4 | MESSAGES_UPDATED: 2, 5 | POINTS_UPDATED: 3, 6 | LEVEL_UPDATED: 4, 7 | WISHLIST_UPDATED: 5, 8 | REPUTATION_UPDATED: 6, 9 | PAGE_REFRESHED: 7, 10 | HEADER_REFRESHED: 8, 11 | NOTIFICATION_BAR_BUILD: 9, 12 | BUTTON_BUILD: 10, 13 | PAGE_HEADING_BUILD: 11, 14 | GIVEAWAY_ENTER: 12, 15 | GIVEAWAY_LEAVE: 13, 16 | BEFORE_COMMENT_SUBMIT: 14, 17 | }; 18 | 19 | export { Events }; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/class/I18N.js: -------------------------------------------------------------------------------- 1 | import { Utils } from '../lib/jsUtils'; 2 | import { browser } from '../browser'; 3 | 4 | class I18N { 5 | getMessage(messageName, substitutions) { 6 | if (Utils.is(substitutions, 'number')) { 7 | if (substitutions === 1) { 8 | messageName += '_one'; 9 | } else { 10 | messageName += '_other'; 11 | } 12 | } 13 | return browser.i18n.getMessage(messageName, substitutions); 14 | } 15 | } 16 | 17 | const i18n = new I18N(); 18 | 19 | export { i18n }; 20 | -------------------------------------------------------------------------------- /src/entry/eventPage_sdk_banner.js: -------------------------------------------------------------------------------- 1 | const { Cu } = require('chrome'); 2 | var buttons = require('sdk/ui/button/action'); 3 | var data = require('sdk/self').data; 4 | var packageJson = require('./package.json'); 5 | var { setTimeout, clearTimeout } = require('sdk/timers'); 6 | var tabs = require('sdk/tabs'); 7 | var PageMod = require('sdk/page-mod').PageMod; 8 | var Request = require('sdk/request').Request; 9 | var Services = require('resource://gre/modules/Services.jsm').Services; 10 | var FileUtils = require('resource://gre/modules/FileUtils.jsm').FileUtils; 11 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": { 3 | "extensionId": "ibedmjbicclcdfmghnkfldnplocgihna", 4 | "extensionKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAih1koCChvaohyeTSrBrUcANi8zBmZT+4JWjI92p4kEeaVvno8mdUnOLwA5nwZEYLfQ6CdCmStWLR3SUeoj/PhIHJkuBYYsyv2fcUh3kALAnqJMHJ61epNhrD93l2xf4BV9/2bKb3o3NTA/u9UosQqljhYkPwkIed+yzRMwYCoOn+vMpbOdaAwfycncG0/eXO5NIIqC+Ov8xR2vGX7rwXvnUIgG84TvZvOcCtmn6PsijDm6/xFgNwW0xvUhHIa50rTwMxedItEhxFslGlCGhYNG2HzvVJpcLEE9qq2OHL/3SyidU5xCyMW+BV8ieZ03EBwMYnhGxV68UKSa+tmJEoKQIDAQAB" 5 | }, 6 | "firefox": { 7 | "extensionId": "{71de700c-ca62-4e31-9de6-93e3c30633d6}" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/entry/gm_index.js: -------------------------------------------------------------------------------- 1 | import '../browser-gm'; 2 | import '../main'; 3 | 4 | (async () => { 5 | const awesomeBootstrapCheckboxCss = document.createElement('link'); 6 | awesomeBootstrapCheckboxCss.rel = 'stylesheet'; 7 | awesomeBootstrapCheckboxCss.href = await GM.getResourceUrl('awesome-bootstrap-checkbox'); 8 | const jqueryQueryBuilderCss = document.createElement('link'); 9 | jqueryQueryBuilderCss.rel = 'stylesheet'; 10 | jqueryQueryBuilderCss.href = await GM.getResourceUrl('jquery-query-builder'); 11 | document.head.appendChild(awesomeBootstrapCheckboxCss); 12 | document.head.appendChild(jqueryQueryBuilderCss); 13 | })(); 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature. 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like.** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered.** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional Context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["istanbul"] 5 | } 6 | }, 7 | "plugins": [ 8 | "@babel/proposal-class-properties", 9 | [ 10 | "@babel/plugin-transform-runtime", 11 | { 12 | "regenerator": true 13 | } 14 | ] 15 | ], 16 | "presets": [ 17 | [ 18 | "@babel/preset-react", 19 | { 20 | "pragma": "DOM.element", 21 | "pragmaFrag": "DOM.fragment" 22 | } 23 | ], 24 | "@babel/typescript", 25 | [ 26 | "@babel/preset-env", 27 | { 28 | "targets": "defaults, Firefox 56, not IE 11", 29 | "exclude": ["es.promise"], 30 | "useBuiltIns": "usage", 31 | "corejs": 3 32 | } 33 | ] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Suggest an enhancement to an already existent feature. 4 | --- 5 | 6 | **Is your enhancement request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like.** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered.** 13 | A clear and concise description of any alternative solutions you've considered. 14 | 15 | **Additional Context** 16 | Add any other context or screenshots about the enhancement request here. 17 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /.github/workflows/beta.yml: -------------------------------------------------------------------------------- 1 | name: Beta Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - beta 7 | 8 | jobs: 9 | beta: 10 | name: Build, test and release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@v2 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14.x 19 | - name: Install pnpm 20 | run: npm install -g pnpm 21 | - name: Install dependencies 22 | run: pnpm install 23 | - name: Build 24 | run: pnpm run build-dev 25 | - name: Release 26 | run: pnpm run generate-release beta token=${{ secrets.GH_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/stable.yml: -------------------------------------------------------------------------------- 1 | name: Stable Release 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - beta 7 | 8 | jobs: 9 | stable: 10 | name: Build, test and release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@v2 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14.x 19 | - name: Install pnpm 20 | run: npm install -g pnpm 21 | - name: Install dependencies 22 | run: pnpm install 23 | - name: Build 24 | run: pnpm run build 25 | - name: Release 26 | run: pnpm run generate-release token=${{ secrets.GH_TOKEN }} 27 | -------------------------------------------------------------------------------- /src/class/Shared.js: -------------------------------------------------------------------------------- 1 | class _Shared { 2 | constructor() { 3 | /** 4 | * @type {import('../modules/Common').common} 5 | */ 6 | this.common = null; 7 | 8 | /** 9 | * @type {import('./Esgst').esgst} 10 | */ 11 | this.esgst = null; 12 | 13 | /** 14 | * @type {import('../components/Header').IHeader} 15 | */ 16 | this.header = null; 17 | 18 | /** 19 | * @type {import('../components/Footer').IFooter} 20 | */ 21 | this.footer = null; 22 | } 23 | 24 | add(objs) { 25 | for (let name in objs) { 26 | if (!objs.hasOwnProperty(name)) { 27 | continue; 28 | } 29 | 30 | this[name] = objs[name]; 31 | } 32 | } 33 | } 34 | 35 | const Shared = new _Shared(); 36 | 37 | export { Shared }; 38 | -------------------------------------------------------------------------------- /src/dependencies.ts: -------------------------------------------------------------------------------- 1 | // jQuery QueryBuilder want global interact object 2 | import interact from 'interactjs/dist/interact.min'; 3 | import 'jquery'; 4 | import 'jQuery-QueryBuilder/dist/js/query-builder.standalone.min'; 5 | import 'bootstrap/dist/js/bootstrap'; 6 | import 'jquery-ui/ui/widgets/progressbar'; 7 | import 'jquery-ui/ui/widgets/slider'; 8 | import JSZip from 'jszip'; 9 | import VDF from 'simple-vdf'; 10 | import * as emojisUtils from 'emojis-utils'; 11 | 12 | import 'awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css'; 13 | import 'jQuery-QueryBuilder/dist/css/query-builder.default.min.css'; 14 | 15 | window.interact = interact; 16 | window.JSZip = JSZip; 17 | window.VDF = VDF; 18 | window.emojisUtils = emojisUtils; 19 | -------------------------------------------------------------------------------- /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 | 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/html/permissions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ESGST Permissions 7 | 8 | 9 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/class/ICloudStorage.js: -------------------------------------------------------------------------------- 1 | import { Shared } from './Shared'; 2 | import { Utils } from '../lib/jsUtils'; 3 | 4 | class ICloudStorage { 5 | static get REDIRECT_URL() { 6 | return `https://www.steamgifts.com/account/settings/profile`; 7 | } 8 | 9 | static getToken(key) { 10 | return new Promise((resolve) => { 11 | ICloudStorage.checkToken(key, resolve); 12 | }); 13 | } 14 | 15 | static async checkToken(key, resolve, startTime = Date.now(), timeout = 60000) { 16 | const token = await Shared.common.getValue(key); 17 | if (Utils.isSet(token)) { 18 | resolve(token); 19 | } else if (startTime - Date.now() > timeout) { 20 | resolve(null); 21 | } else { 22 | window.setTimeout(ICloudStorage.checkToken, 1000, key, resolve, startTime, timeout); 23 | } 24 | } 25 | } 26 | 27 | export { ICloudStorage }; 28 | -------------------------------------------------------------------------------- /src/class/Module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} EsgstModuleInfo 3 | * @property {String} id 4 | * @property {String} [name] 5 | * @property {() => Node} [description] 6 | * @property {String} [type] 7 | * @property {Boolean|Object} [sg] 8 | * @property {Boolean|Object} [st] 9 | * @property {Boolean|Object} [sgt] 10 | * @property {boolean} [endless] 11 | * @property {Object} [featureMap] 12 | */ 13 | 14 | /** module interface */ 15 | class Module { 16 | constructor() { 17 | /** 18 | * @type {import('./Esgst').esgst} 19 | */ 20 | this.esgst = null; 21 | /** @type {EsgstModuleInfo} */ 22 | this.info = { 23 | id: 'unknown', 24 | name: 'Unknown', 25 | type: '', 26 | }; 27 | } 28 | 29 | init() {} 30 | 31 | setEsgst(esgst) { 32 | this.esgst = esgst; 33 | return this; 34 | } 35 | } 36 | 37 | export { Module }; 38 | -------------------------------------------------------------------------------- /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 | 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 | 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/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/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 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./webpack.config.js'); 2 | 3 | const browsers = []; 4 | //browsers.push('Chrome'); 5 | browsers.push('Firefox'); 6 | 7 | module.exports = (config) => { 8 | const karmaConfig = { 9 | autoWatch: false, 10 | browsers, 11 | concurrency: 1, 12 | files: ['test/**/*.+(js|jsx|ts|tsx)'], 13 | frameworks: ['mocha', 'chai'], 14 | logLevel: config.LOG_DISABLE, 15 | plugins: [ 16 | 'karma-chai', 17 | 'karma-chrome-launcher', 18 | 'karma-coverage', 19 | 'karma-firefox-launcher', 20 | 'karma-mocha', 21 | 'karma-mocha-reporter', 22 | 'karma-webpack', 23 | ], 24 | preprocessors: { 25 | 'test/**/*+(js|jsx|ts|tsx)': ['webpack'], 26 | }, 27 | reporters: ['mocha', 'coverage'], 28 | singleRun: true, 29 | webpack: webpackConfig({ development: true, test: true }), 30 | webpackMiddleware: { 31 | logLevel: 'silent', 32 | }, 33 | }; 34 | config.set(karmaConfig); 35 | }; 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Report a bug. 4 | --- 5 | 6 | **Description** 7 | A clear and concise description of what the bug is. 8 | 9 | **Steps to Reproduce** 10 | 11 | 1. Go to "..." 12 | 2. Click on "..." 13 | 3. Scroll down to "..." 14 | 4. See error 15 | 16 | **Expected Behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Console Errors** 20 | Always check the browser console for errors (by pressing Ctrl + Shift + J) when reproducing the bug and post any errors found. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **System (please complete the following information):** 26 | 27 | - ESGST Version: [e.g. Extension v8.1.0, Extension v8.1.0-dev.10, Userscript v8.1.0 (Greasemonkey), Userscript v8.1.0 (Tampermonkey)] 28 | - Browser + Version: [e.g. Chrome v60, Firefox v60] 29 | 30 | **Additional Context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /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 | 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/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 | 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/Comments/CommentReverser.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class CommentsCommentReverser extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 | 17 | ), 18 | id: 'cr', 19 | name: 'Comment Reverser', 20 | sg: true, 21 | st: true, 22 | type: 'comments', 23 | }; 24 | } 25 | 26 | init() { 27 | if (!Shared.esgst.discussionPath || !Shared.esgst.pagination) return; 28 | const context = Shared.esgst.pagination.previousElementSibling; 29 | if (context.classList.contains('comments')) { 30 | Shared.common.reverseComments(context); 31 | } 32 | } 33 | } 34 | 35 | const commentsCommentReverser = new CommentsCommentReverser(); 36 | 37 | export { commentsCommentReverser }; 38 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rafael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | 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/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 | 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/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 | 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/class/Lock.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { browser } from '../browser'; 3 | 4 | type LockData = { 5 | uuid: string; 6 | key: string; 7 | } & LockOptions; 8 | 9 | interface LockOptions { 10 | threshold: number; 11 | timeout: number; 12 | tryOnce: boolean; 13 | } 14 | 15 | export class Lock { 16 | private data: LockData; 17 | private locked = false; 18 | 19 | constructor(key: string, data: Partial = {}) { 20 | this.data = { 21 | uuid: uuidv4(), 22 | key: `${key}Lock`, 23 | threshold: 100, 24 | timeout: 15000, 25 | tryOnce: false, 26 | ...data, 27 | }; 28 | } 29 | 30 | get isLocked(): boolean { 31 | return this.locked; 32 | } 33 | 34 | lock = async (): Promise => { 35 | const response = await browser.runtime.sendMessage({ 36 | action: 'do_lock', 37 | lock: JSON.stringify(this.data), 38 | }); 39 | this.locked = JSON.parse(response); 40 | }; 41 | 42 | update = (): Promise => { 43 | return browser.runtime.sendMessage({ 44 | action: 'update_lock', 45 | lock: JSON.stringify(this.data), 46 | }); 47 | }; 48 | 49 | unlock = (): Promise => { 50 | return browser.runtime.sendMessage({ 51 | action: 'do_unlock', 52 | lock: JSON.stringify(this.data), 53 | }); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /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/class/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | class _EventDispatcher { 2 | constructor() { 3 | /** @type {Object} */ 4 | this.subscribers = {}; 5 | } 6 | 7 | /** 8 | * @param {number} event 9 | * @param {Function} callback 10 | */ 11 | subscribe(event, callback) { 12 | if (!this.subscribers[event]) { 13 | this.subscribers[event] = []; 14 | } 15 | 16 | this.subscribers[event].push(callback); 17 | 18 | return this.unsubscribe.bind(this, event, callback); 19 | } 20 | 21 | /** 22 | * @param {number} event 23 | * @param {Function} callback 24 | */ 25 | unsubscribe(event, callback) { 26 | if (!this.subscribers[event]) { 27 | return; 28 | } 29 | 30 | this.subscribers[event] = this.subscribers[event].filter( 31 | (subscriber) => subscriber !== callback 32 | ); 33 | } 34 | 35 | /** 36 | * @param {number} event 37 | * @param {Array} params 38 | */ 39 | async dispatch(event, ...params) { 40 | if (!this.subscribers[event]) { 41 | return; 42 | } 43 | 44 | for (const subscriber of this.subscribers[event]) { 45 | try { 46 | await subscriber(...params); 47 | } catch (error) { 48 | window.console.log(error.message); 49 | } 50 | } 51 | } 52 | } 53 | 54 | const EventDispatcher = new _EventDispatcher(); 55 | 56 | export { EventDispatcher }; 57 | -------------------------------------------------------------------------------- /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/Comments/ReplyBoxOnTop.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class CommentsReplyBoxOnTop extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Moves the reply box over the comments (in any page) so that you do not need to scroll 13 | down to the bottom of the page to add a comment. 14 |
  • 15 |
16 | ), 17 | id: 'rbot', 18 | name: 'Reply Box On Top', 19 | sg: true, 20 | st: true, 21 | type: 'comments', 22 | }; 23 | } 24 | 25 | init() { 26 | let element = Shared.esgst.mainPageHeading; 27 | const box = Shared.esgst.replyBox || Shared.esgst.reviewBox; 28 | if (!box) { 29 | return; 30 | } 31 | let boxContainer; 32 | DOM.insert( 33 | element, 34 | 'afterend', 35 |
(boxContainer = ref)} /> 36 | ); 37 | boxContainer.appendChild(box); 38 | let button = boxContainer.getElementsByClassName(Shared.esgst.cancelButtonClass)[0]; 39 | if (!button) return; 40 | button.addEventListener('click', () => 41 | window.setTimeout(() => boxContainer.appendChild(box), 0) 42 | ); 43 | } 44 | } 45 | 46 | const commentsReplyBoxOnTop = new CommentsReplyBoxOnTop(); 47 | 48 | export { commentsReplyBoxOnTop }; 49 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 100000, 4 | "timestamp": 1576724400000, 5 | "message": "Hi there! The ability to specify paths for features had been removed from ESGST in v8.5.9, but it's been added back since v8.5.11. Unfortunately, your previous path preferences could not be carried over, and I apologize for that, but if you have a backup from a version prior to v8.5.9, you should be able to restore them. Have a good day and thanks for using ESGST!" 6 | }, 7 | { 8 | "id": 100001, 9 | "timestamp": 1577145600000, 10 | "dependency": "ge", 11 | "message": "Hi there! You can now extract giveaways from any URL with Giveaway Extractor. There isn't a UI for accessing this feature at the moment, but you can access it manually by going to [https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL](https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL). For example, to extract giveaways from the SteamGifts Community Christmas Calendar 2019, go to [https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/](https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/). You'll be asked to grant permission to all URLs. Happy holidays and thanks for using ESGST!" 12 | }, 13 | { 14 | "id": 100002, 15 | "timestamp": 1595376000000, 16 | "message": "Testing **this** like [yeah](no)!" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /src/class/Session.js: -------------------------------------------------------------------------------- 1 | import { Namespaces } from '../constants/Namespaces'; 2 | 3 | class ISession { 4 | /** 5 | * @param {string} text 6 | * @returns {string} 7 | */ 8 | static extractXsrfToken(text) { 9 | return text.match(/xsrf_token=(.+)/)[1]; 10 | } 11 | } 12 | 13 | class _Session extends ISession { 14 | constructor() { 15 | super(); 16 | 17 | /** @type {ISessionCounters} */ 18 | this.counters = { 19 | created: 0, 20 | level: { 21 | base: 0, 22 | full: 0, 23 | }, 24 | messages: 0, 25 | points: 0, 26 | reputation: { 27 | negative: 0, 28 | positive: 0, 29 | }, 30 | won: 0, 31 | wonDelivered: false, 32 | }; 33 | 34 | /** @type {boolean} */ 35 | this.isLoggedIn = false; 36 | 37 | /** @type {number} */ 38 | this.namespace = Namespaces.SG; 39 | 40 | /** @type {import('../models/User').UserData | null} */ 41 | this.user = null; 42 | 43 | /** @type {string} */ 44 | this.xsrfToken = null; 45 | } 46 | 47 | init() { 48 | switch (window.location.hostname) { 49 | case 'www.steamgifts.com': { 50 | this.namespace = Namespaces.SG; 51 | 52 | break; 53 | } 54 | 55 | case 'www.steamtrades.com': { 56 | this.namespace = Namespaces.ST; 57 | 58 | break; 59 | } 60 | 61 | default: { 62 | throw 'Invalid namespace.'; 63 | } 64 | } 65 | } 66 | } 67 | 68 | const Session = new _Session(); 69 | 70 | export { ISession, Session }; 71 | -------------------------------------------------------------------------------- /src/modules/Discussions/MainPostSkipper.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const goToComment = common.goToComment.bind(common); 6 | class DiscussionsMainPostSkipper extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Skips to the comments of a discussion if you have used the pagination navigation. For 14 | example, if you enter a discussion and use the pagination navigation to go to page 2, on 15 | page 2 the feature will skip the main post and take you directly to the comments. 16 |
  • 17 |
18 | ), 19 | id: 'mps', 20 | name: 'Main Post Skipper', 21 | sg: true, 22 | type: 'discussions', 23 | }; 24 | } 25 | 26 | init() { 27 | if ( 28 | !window.location.hash && 29 | this.esgst.discussionPath && 30 | this.esgst.pagination && 31 | document.referrer.match( 32 | new RegExp(`/discussion/${[window.location.pathname.match(/^\/discussion\/(.+?)\//)[1]]}/`) 33 | ) 34 | ) { 35 | const context = this.esgst.pagination.previousElementSibling; 36 | if (context.classList.contains('comments')) { 37 | goToComment('', context.firstElementChild.firstElementChild, true); 38 | } 39 | } 40 | } 41 | } 42 | 43 | const discussionsMainPostSkipper = new DiscussionsMainPostSkipper(); 44 | 45 | export { discussionsMainPostSkipper }; 46 | -------------------------------------------------------------------------------- /src/assets/styles/patches/query-builder.css: -------------------------------------------------------------------------------- 1 | .rules-group-container { 2 | border: 1px solid #ccc !important; 3 | background: none !important; 4 | } 5 | 6 | .query-builder .btn-primary { 7 | text-shadow: none !important; 8 | } 9 | 10 | .query-builder .form-control { 11 | height: 22px !important; 12 | font-size: 12px !important; 13 | padding: 2px !important; 14 | } 15 | 16 | .query-builder .radio-default { 17 | margin-right: 5px !important; 18 | } 19 | 20 | .query-builder .radio-default input { 21 | display: none !important; 22 | } 23 | 24 | .query-builder .rules-group-container [data-resume='group'] { 25 | display: none !important; 26 | } 27 | 28 | .query-builder .rules-group-container[data-esgst-paused='true'] [data-resume='group'] { 29 | display: block !important; 30 | } 31 | 32 | .query-builder .rules-group-container[data-esgst-paused='true'] [data-pause='group'] { 33 | display: none !important; 34 | } 35 | 36 | .query-builder .rule-container [data-resume='rule'] { 37 | display: none !important; 38 | } 39 | 40 | .query-builder .rule-container[data-esgst-paused='true'] [data-resume='rule'] { 41 | display: block !important; 42 | } 43 | 44 | .query-builder .rule-container[data-esgst-paused='true'] [data-pause='rule'] { 45 | display: none !important; 46 | } 47 | 48 | .query-builder .rule-container[data-esgst-paused='true'] { 49 | opacity: 0.5 !important; 50 | } 51 | 52 | .query-builder .rules-group-container[data-esgst-paused='true'] { 53 | opacity: 0.5 !important; 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/General/FixedMainPageHeading.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { EventDispatcher } from '../../class/EventDispatcher'; 3 | import { Module } from '../../class/Module'; 4 | import { Events } from '../../constants/Events'; 5 | 6 | class GeneralFixedMainPageHeading extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Keeps the main page heading (usually the first heading of the page, for example, the 14 | heading that says "Giveaways" in the main page) of any page at the top of the window 15 | while you scroll down the page. 16 |
  • 17 |
18 | ), 19 | id: 'fmph', 20 | name: 'Fixed Main Page Heading', 21 | sg: true, 22 | st: true, 23 | type: 'general', 24 | }; 25 | } 26 | 27 | init() { 28 | EventDispatcher.subscribe(Events.PAGE_HEADING_BUILD, (builtHeading) => 29 | builtHeading.nodes.outer.classList.add('esgst-fmph') 30 | ); 31 | 32 | if (!this.esgst.pageHeadings.length) { 33 | return; 34 | } 35 | 36 | this.esgst.style.insertAdjacentText( 37 | 'beforeend', 38 | ` 39 | .esgst-fmph { 40 | top: ${this.esgst.pageTop}px; 41 | } 42 | ` 43 | ); 44 | 45 | for (const pageHeading of this.esgst.pageHeadings) { 46 | pageHeading.classList.add('esgst-fmph'); 47 | } 48 | } 49 | } 50 | 51 | const generalFixedMainPageHeading = new GeneralFixedMainPageHeading(); 52 | 53 | export { generalFixedMainPageHeading }; 54 | -------------------------------------------------------------------------------- /src/modules/Users/VisibleRealCV.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class UsersVisibleRealCV extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Displays the real sent/won CV next to the raw value in a user's{' '} 12 | profile page. 13 |
  • 14 |
  • 15 | This also extends to , if you have that feature 16 | enabled. 17 |
  • 18 |
  • 19 | With this feature disabled, you can still view the real CV, as provided by SteamGifts, 20 | by hovering over the raw value. 21 |
  • 22 |
23 | ), 24 | id: 'vrcv', 25 | name: 'Visible Real CV', 26 | sg: true, 27 | type: 'users', 28 | featureMap: { 29 | profile: this.vrcv_add.bind(this), 30 | }, 31 | }; 32 | } 33 | 34 | vrcv_add(profile) { 35 | /** 36 | * @property realSentCV.toLocaleString 37 | * @property realWonCV.toLocaleString 38 | */ 39 | profile.sentCvContainer.insertAdjacentText( 40 | 'beforeend', 41 | ` / $${profile.realSentCV.toLocaleString('en')}` 42 | ); 43 | profile.wonCvContainer.insertAdjacentText( 44 | 'beforeend', 45 | ` / $${profile.realWonCV.toLocaleString('en')}` 46 | ); 47 | } 48 | } 49 | 50 | const usersVisibleRealCV = new UsersVisibleRealCV(); 51 | 52 | export { usersVisibleRealCV }; 53 | -------------------------------------------------------------------------------- /src/class/PermissionsUi.tsx: -------------------------------------------------------------------------------- 1 | import { browser } from '../browser'; 2 | import { PageHeading } from '../components/PageHeading'; 3 | import { DOM } from './DOM'; 4 | import { permissions } from './Permissions'; 5 | import { Popup } from './Popup'; 6 | 7 | class _PermissionsUi { 8 | check = async (keys: string[]): Promise => { 9 | if (!browser.runtime.getURL) { 10 | return true; 11 | } 12 | if (await permissions.contains([keys])) { 13 | return true; 14 | } 15 | return new Promise((resolve) => { 16 | const popup = new Popup({ isTemp: true }); 17 | PageHeading.create('sm', { 18 | breadcrumbs: ['Required Permissions'], 19 | }).insert(popup.description, 'beforeend'); 20 | const scrollableArea = popup.getScrollable(); 21 | scrollableArea.classList.add('markdown'); 22 | DOM.insert( 23 | scrollableArea, 24 | 'atinner', 25 | 26 |

27 | In order to perform this action, you need to grant some permissions to the extension. Go{' '} 28 | 32 | here 33 | {' '} 34 | and click the "Grant" button to grant them. 35 |

36 |

When you are done, close this popup to continue.

37 |
38 | ); 39 | popup.onClose = async () => resolve(await permissions.contains([keys])); 40 | popup.open(); 41 | }); 42 | }; 43 | } 44 | 45 | export const PermissionsUi = new _PermissionsUi(); 46 | -------------------------------------------------------------------------------- /src/modules/General/HiddenBlacklistStats.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralHiddenBlacklistStats extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Hides the blacklist stats of your{' '} 12 | stats page. 13 |
  • 14 |
15 | ), 16 | id: 'hbs', 17 | name: 'Hidden Blacklist Stats', 18 | sg: true, 19 | type: 'general', 20 | }; 21 | } 22 | 23 | init() { 24 | if (!window.location.pathname.match(/^\/stats\/personal\/community/)) return; 25 | 26 | let chart = document.getElementsByClassName('chart')[4]; 27 | 28 | // remove any "blacklist" text from the chart 29 | let heading = chart.firstElementChild; 30 | heading.lastElementChild.remove(); 31 | heading.lastElementChild.remove(); 32 | let subHeading = heading.nextElementSibling; 33 | subHeading.textContent = subHeading.textContent.replace(/and\sblacklists\s/, ''); 34 | 35 | // create a new graph without the blacklist points 36 | let script = document.createElement('script'); 37 | script.textContent = chart.previousElementSibling.textContent.replace( 38 | /,{name:\s"Blacklists".+?}/, 39 | '' 40 | ); 41 | document.body.appendChild(script); 42 | script.remove(); 43 | } 44 | } 45 | 46 | const generalHiddenBlacklistStats = new GeneralHiddenBlacklistStats(); 47 | 48 | export { generalHiddenBlacklistStats }; 49 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayEndTimeHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayEndTimeHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Allows you to highlight the end time of a giveaway (in any page) by coloring it based on 13 | how many hours there are left. 14 |
  • 15 |
16 | ), 17 | id: 'geth', 18 | name: 'Giveaway End Time Highlighter', 19 | sg: true, 20 | type: 'giveaways', 21 | featureMap: { 22 | giveaway: this.geth_getGiveaways.bind(this), 23 | }, 24 | }; 25 | } 26 | 27 | geth_getGiveaways(giveaways) { 28 | if (!Settings.get('geth_colors').length) { 29 | return; 30 | } 31 | 32 | for (const giveaway of giveaways) { 33 | if (!giveaway.started) { 34 | continue; 35 | } 36 | 37 | const hoursLeft = (giveaway.endTime - Date.now()) / 3600000; 38 | for (let i = Settings.get('geth_colors').length - 1; i > -1; i--) { 39 | const colors = Settings.get('geth_colors')[i]; 40 | if (hoursLeft >= parseFloat(colors.lower) && hoursLeft <= parseFloat(colors.upper)) { 41 | (giveaway.endTimeColumn_gv || giveaway.endTimeColumn).style.color = colors.color; 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | const giveawaysGiveawayEndTimeHighlighter = new GiveawaysGiveawayEndTimeHighlighter(); 50 | 51 | export { giveawaysGiveawayEndTimeHighlighter }; 52 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayLevelHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayLevelHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Highlights the level of a giveaway (in any page) by coloring it with the specified 13 | colors. 14 |
  • 15 |
16 | ), 17 | featureMap: { 18 | giveaway: this.highlight.bind(this), 19 | }, 20 | id: 'glh', 21 | name: 'Giveaway Level Highlighter', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | highlight(giveaways) { 28 | for (const giveaway of giveaways) { 29 | if (!giveaway.levelColumn) { 30 | continue; 31 | } 32 | const { color, bgColor } = Settings.get('glh_colors').filter( 33 | (colors) => 34 | giveaway.level >= parseInt(colors.lower) && giveaway.level <= parseInt(colors.upper) 35 | )[0] || { color: undefined, bgColor: undefined }; 36 | if (!color || !bgColor) { 37 | continue; 38 | } 39 | giveaway.levelColumn.setAttribute( 40 | 'style', 41 | `${color ? `color: ${color} !important;` : ''}${ 42 | bgColor ? `background-color: ${bgColor};` : '' 43 | }` 44 | ); 45 | giveaway.levelColumn.classList.add('esgst-glh-highlight'); 46 | } 47 | } 48 | } 49 | 50 | const giveawaysGiveawayLevelHighlighter = new GiveawaysGiveawayLevelHighlighter(); 51 | 52 | export { giveawaysGiveawayLevelHighlighter }; 53 | -------------------------------------------------------------------------------- /browser/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 100000, 4 | "message": "Hi there! The ability to specify paths for features had been removed from ESGST in v8.5.9, but it's been added back since v8.5.11. Unfortunately, your previous path preferences could not be carried over, and I apologize for that, but if you have a backup from a version prior to v8.5.9, you should be able to restore them. Have a good day and thanks for using ESGST!", 5 | "timestamp": 1576724400000 6 | }, 7 | { 8 | "id": 100001, 9 | "message": [ 10 | "Hi there! You can now extract giveaways from any URL with Giveaway Extractor. There isn't a UI for accessing this feature at the moment, but you can access it manually by going to ", 11 | [ 12 | "a", 13 | { 14 | "class": "table__column__secondary-link", 15 | "href": "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL" 16 | }, 17 | "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=URL" 18 | ], 19 | ". For example, to extract giveaways from the SteamGifts Community Christmas Calendar 2019, go to ", 20 | [ 21 | "a", 22 | { 23 | "class": "table__column__secondary-link", 24 | "href": "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/" 25 | }, 26 | "https://www.steamgifts.com/account/settings/profile?esgst=ge&url=https://www.steamgiftscalendar.lima-city.de/" 27 | ], 28 | ". You'll be asked to grant permission to all URLs. Happy holidays and thanks for using ESGST!" 29 | ], 30 | "timestamp": 1577145600000 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/class/Button.jsx: -------------------------------------------------------------------------------- 1 | import { Shared } from './Shared'; 2 | import { DOM } from './DOM'; 3 | 4 | class Button { 5 | constructor(context, position, details) { 6 | this.callbacks = details.callbacks; 7 | this.states = this.callbacks.length; 8 | this.icons = details.icons; 9 | this.id = details.id; 10 | this.index = details.index; 11 | this.titles = details.titles; 12 | DOM.insert( 13 | context, 14 | position, 15 |
(this.button = ref)}>
16 | ); 17 | // noinspection JSIgnoredPromiseFromCall 18 | this.change(); 19 | return this; 20 | } 21 | 22 | async change(mainCallback, index = this.index, event) { 23 | if (index >= this.states) { 24 | index = 0; 25 | } 26 | this.index = index + 1; 27 | this.button.title = Shared.common.getFeatureTooltip(this.id, this.titles[index]); 28 | DOM.insert(this.button, 'atinner', ); 29 | if (mainCallback) { 30 | if (await mainCallback(event)) { 31 | // noinspection JSIgnoredPromiseFromCall 32 | this.change(); 33 | } else { 34 | DOM.insert( 35 | this.button, 36 | 'atinner', 37 | 38 | ); 39 | } 40 | } else if (this.callbacks[index]) { 41 | this.button.firstElementChild.addEventListener( 42 | 'click', 43 | this.change.bind(this, this.callbacks[index], undefined) 44 | ); 45 | } 46 | } 47 | 48 | async triggerCallback() { 49 | await this.change(this.callbacks[this.index - 1]); 50 | } 51 | } 52 | 53 | export { Button }; 54 | -------------------------------------------------------------------------------- /src/modules/Games/GameTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GamesGameTags extends Tags { 6 | constructor() { 7 | super('gt'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next to a game's name (in any page) that 13 | allows you to save tags for the game (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
28 | ), 29 | features: { 30 | gt_s: { 31 | name: 'Show tag suggestions while typing.', 32 | sg: true, 33 | st: true, 34 | }, 35 | }, 36 | id: 'gt', 37 | name: 'Game Tags', 38 | sg: true, 39 | type: 'games', 40 | }; 41 | } 42 | 43 | init() { 44 | Shared.esgst.gameFeatures.push(this.tags_addButtons.bind(this)); 45 | // noinspection JSIgnoredPromiseFromCall 46 | this.tags_getTags(); 47 | } 48 | } 49 | 50 | const gamesGameTags = new GamesGameTags(); 51 | 52 | export { gamesGameTags }; 53 | -------------------------------------------------------------------------------- /src/modules/Discussions/DiscussionTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class DiscussionsDiscussionTags extends Tags { 5 | constructor() { 6 | super('dt'); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Adds a button ( ) next a discussion's title (in any page) 12 | that allows you to save tags for the discussion (only visible to you). 13 |
  • 14 |
  • You can press Enter to save the tags.
  • 15 |
  • Each tag can be colored individually.
  • 16 |
  • 17 | There is a button ( ) in the tags popup that allows you to 18 | view a list with all of the tags that you have used ordered from most used to least 19 | used. 20 |
  • 21 |
  • 22 | Adds a button ( ) to the 23 | page heading of this menu that allows you to manage all of the tags that have been 24 | saved. 25 |
  • 26 |
27 | ), 28 | features: { 29 | dt_s: { 30 | name: 'Show tag suggestions while typing.', 31 | sg: true, 32 | }, 33 | }, 34 | id: 'dt', 35 | name: 'Discussion Tags', 36 | sg: true, 37 | type: 'discussions', 38 | }; 39 | } 40 | 41 | init() { 42 | this.esgst.discussionFeatures.push(this.tags_addButtons.bind(this)); 43 | // noinspection JSIgnoredPromiseFromCall 44 | this.tags_getTags(); 45 | } 46 | } 47 | 48 | const discussionsDiscussionTags = new DiscussionsDiscussionTags(); 49 | 50 | export { discussionsDiscussionTags }; 51 | -------------------------------------------------------------------------------- /src/modules/General/SearchMagnifyingGlassButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralSearchMagnifyingGlassButton extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Turns the magnifying glass icon () in the search field 13 | of any page into a button that submits the search when you click on it. 14 |
  • 15 |
16 | ), 17 | id: 'smgb', 18 | name: 'Search Magnifying Glass Button', 19 | sg: true, 20 | type: 'general', 21 | }; 22 | } 23 | 24 | init() { 25 | let buttons, i; 26 | buttons = document.querySelectorAll( 27 | `.sidebar__search-container .fa-search, .esgst-qgs-container .fa-search` 28 | ); 29 | for (i = buttons.length - 1; i > -1; --i) { 30 | let button, input; 31 | button = buttons[i]; 32 | input = button.previousElementSibling; 33 | button.classList.add('esgst-clickable'); 34 | button.addEventListener('click', () => { 35 | let value = input.value.trim(); 36 | if (value) { 37 | if (Settings.get('as') && value.match(/"|id:/)) { 38 | this.esgst.modules.giveawaysArchiveSearcher.as_openPage(input); 39 | } else { 40 | window.location.href = `${this.esgst.searchUrl.replace(/page=/, '')}q=${value}`; 41 | } 42 | } 43 | }); 44 | } 45 | } 46 | } 47 | 48 | const generalSearchMagnifyingGlassButton = new GeneralSearchMagnifyingGlassButton(); 49 | 50 | export { generalSearchMagnifyingGlassButton }; 51 | -------------------------------------------------------------------------------- /src/modules/Groups/GroupTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GroupsGroupTags extends Tags { 6 | constructor() { 7 | super('gpt'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next to a group's name (in any page) that 13 | allows you to save tags for the group (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
28 | ), 29 | features: { 30 | gpt_s: { 31 | name: 'Show tag suggestions while typing.', 32 | sg: true, 33 | st: true, 34 | }, 35 | }, 36 | id: 'gpt', 37 | name: 'Group Tags', 38 | sg: true, 39 | type: 'groups', 40 | }; 41 | } 42 | 43 | init() { 44 | Shared.esgst.groupFeatures.push(this.tags_addButtons.bind(this)); 45 | // noinspection JSIgnoredPromiseFromCall 46 | this.tags_getTags(); 47 | } 48 | } 49 | 50 | const groupsGroupTags = new GroupsGroupTags(); 51 | 52 | export { groupsGroupTags }; 53 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayCopyHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayCopyHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Highlights the number of copies next a giveaway's game name (in any page) by coloring it 13 | as red and changing the font to bold. 14 |
  • 15 |
16 | ), 17 | featureMap: { 18 | giveaway: this.highlight.bind(this), 19 | }, 20 | id: 'gch', 21 | name: 'Giveaway Copy Highlighter', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | highlight(giveaways) { 28 | for (const giveaway of giveaways) { 29 | if (!giveaway.copiesContainer) { 30 | continue; 31 | } 32 | const { color, bgColor } = Settings.get('gch_colors').filter( 33 | (colors) => 34 | giveaway.copies >= parseInt(colors.lower) && giveaway.copies <= parseInt(colors.upper) 35 | )[0] || { color: undefined, bgColor: undefined }; 36 | giveaway.copiesContainer.classList.add('esgst-bold'); 37 | if (!color) { 38 | giveaway.copiesContainer.classList.add('esgst-red'); 39 | continue; 40 | } 41 | giveaway.copiesContainer.style.color = color; 42 | if (!bgColor) { 43 | continue; 44 | } 45 | giveaway.copiesContainer.classList.add('esgst-gch-highlight'); 46 | giveaway.copiesContainer.style.backgroundColor = bgColor; 47 | } 48 | } 49 | } 50 | 51 | const giveawaysGiveawayCopyHighlighter = new GiveawaysGiveawayCopyHighlighter(); 52 | 53 | export { giveawaysGiveawayCopyHighlighter }; 54 | -------------------------------------------------------------------------------- /src/browser-sdk.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { setBrowser } from './browser'; 3 | 4 | const browser = { 5 | gm: null, 6 | runtime: { 7 | getBrowserInfo: () => Promise.resolve({ name: '?' }), 8 | onMessage: { 9 | addListener: (callback) => { 10 | // @ts-ignore 11 | self.port.on('esgstMessage', (obj) => callback(obj)); 12 | }, 13 | }, 14 | getManifest: () => { 15 | return new Promise((resolve) => { 16 | browser.runtime 17 | .sendMessage({ 18 | action: 'getPackageJson', 19 | }) 20 | .then((result) => { 21 | resolve(JSON.parse(result)); 22 | }); 23 | }); 24 | }, 25 | sendMessage: (obj) => { 26 | return new Promise((resolve) => { 27 | obj.uuid = uuidv4(); 28 | // @ts-ignore 29 | self.port.emit(obj.action, obj); 30 | // @ts-ignore 31 | self.port.on(`${obj.action}_${obj.uuid}_response`, function onResponse(result) { 32 | // @ts-ignore 33 | self.port.removeListener(`${obj.action}_${obj.uuid}_response`, 'onResponse'); 34 | resolve(result); 35 | }); 36 | }); 37 | }, 38 | }, 39 | storage: { 40 | local: { 41 | get: async () => { 42 | return JSON.parse( 43 | await browser.runtime.sendMessage({ 44 | action: 'getStorage', 45 | }) 46 | ); 47 | }, 48 | remove: async (keys) => { 49 | await browser.runtime.sendMessage({ 50 | action: 'delValues', 51 | keys: JSON.stringify(keys), 52 | }); 53 | }, 54 | set: async (values) => { 55 | await browser.runtime.sendMessage({ 56 | action: 'setValues', 57 | values: JSON.stringify(values), 58 | }); 59 | }, 60 | }, 61 | onChanged: { 62 | addListener: () => {}, 63 | }, 64 | }, 65 | }; 66 | 67 | setBrowser(browser); 68 | -------------------------------------------------------------------------------- /src/modules/Users/LevelUpCalculator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { Settings } from '../../class/Settings'; 5 | import { DOM } from '../../class/DOM'; 6 | 7 | class UsersLevelUpCalculator extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Shows how much real CV a user needs to level up in their profile page.
  • 14 |
  • 15 | Uses the values mentioned on{' '} 16 | this discussion for the 17 | calculation. 18 |
  • 19 |
20 | ), 21 | features: { 22 | luc_c: { 23 | name: 'Display current user level.', 24 | sg: true, 25 | }, 26 | }, 27 | id: 'luc', 28 | name: 'Level Up Calculator', 29 | sg: true, 30 | type: 'users', 31 | featureMap: { 32 | profile: this.luc_calculate.bind(this), 33 | }, 34 | }; 35 | } 36 | 37 | luc_calculate(profile) { 38 | for (const [index, value] of Shared.esgst.cvLevels.entries()) { 39 | const cvRounded = Math.round(profile.realSentCV); 40 | if (cvRounded < value) { 41 | DOM.insert( 42 | profile.levelRowRight, 43 | 'beforeend', 44 | 45 | {`(${Settings.get('luc_c') ? `${profile.level} / ` : ''}~$${Shared.common.round( 46 | value - cvRounded 47 | )} real CV to level ${index})`} 48 | 49 | ); 50 | break; 51 | } 52 | } 53 | } 54 | } 55 | 56 | const usersLevelUpCalculator = new UsersLevelUpCalculator(); 57 | 58 | export { usersLevelUpCalculator }; 59 | -------------------------------------------------------------------------------- /src/modules/General/AttachedImageLoader.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralAttachedImageLoader extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | conflicts: ['vai'], 10 | description: () => ( 11 |
    12 |
  • 13 | Only loads an attached image (in any page) when you click on its "View attached image" 14 | button, instead of loading it on page load, which should speed up page loads. 15 |
  • 16 |
17 | ), 18 | id: 'ail', 19 | name: 'Attached Image Loader', 20 | sg: true, 21 | st: true, 22 | type: 'general', 23 | }; 24 | } 25 | 26 | init() { 27 | if (Settings.get('vai')) return; 28 | this.esgst.endlessFeatures.push(this.ail_getImages.bind(this)); 29 | } 30 | 31 | ail_getImages(context, main, source, endless) { 32 | const buttons = context.querySelectorAll( 33 | `${ 34 | endless 35 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached` 36 | : '.comment__toggle-attached' 37 | }, ${ 38 | endless 39 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached` 40 | : '.view_attached' 41 | }` 42 | ); 43 | for (let i = 0, n = buttons.length; i < n; i++) { 44 | const button = buttons[i], 45 | image = button.nextElementSibling.firstElementChild, 46 | url = image.getAttribute('src'); 47 | image.removeAttribute('src'); 48 | button.addEventListener('click', image.setAttribute.bind(image, 'src', url)); 49 | } 50 | } 51 | } 52 | 53 | const generalAttachedImageLoader = new GeneralAttachedImageLoader(); 54 | 55 | export { generalAttachedImageLoader }; 56 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | greasemonkey: true, 6 | jquery: true, 7 | node: true, 8 | webextensions: true, 9 | }, 10 | rules: {}, 11 | overrides: [ 12 | { 13 | files: ['**/*.{js,jsx}'], 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:react/recommended', 20 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.** 21 | ], 22 | rules: { 23 | quotes: [ 24 | 'error', 25 | 'single', 26 | { 27 | avoidEscape: true, 28 | allowTemplateLiterals: false, 29 | }, 30 | ], 31 | 'react/react-in-jsx-scope': 'off', 32 | }, 33 | }, 34 | { 35 | files: ['**/*.{ts,tsx}'], 36 | plugins: ['prefer-arrow'], 37 | extends: [ 38 | 'eslint:recommended', 39 | 'plugin:react/recommended', 40 | 'plugin:@typescript-eslint/recommended', 41 | 'prettier/@typescript-eslint', // Disables TypeScript rules that conflict with Prettier. 42 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.** 43 | ], 44 | rules: { 45 | quotes: 'off', 46 | '@typescript-eslint/quotes': [ 47 | 'error', 48 | 'single', 49 | { 50 | avoidEscape: true, 51 | allowTemplateLiterals: false, 52 | }, 53 | ], 54 | 'prefer-arrow/prefer-arrow-functions': [ 55 | 'error', 56 | { 57 | disallowPrototype: true, 58 | classPropertiesAllowed: true, 59 | }, 60 | ], 61 | 'react/react-in-jsx-scope': 'off', 62 | }, 63 | }, 64 | ], 65 | settings: { 66 | react: { 67 | version: 'detect', 68 | }, 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/modules/Giveaways/PinnedGiveawaysButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysPinnedGiveawaysButton extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Modifies the arrow button in the pinned giveaways box of the main page so that you are 14 | able to collapse the box again after expanding it. 15 |
  • 16 |
17 | ), 18 | id: 'pgb', 19 | name: 'Pinned Giveaways Button', 20 | sg: true, 21 | type: 'giveaways', 22 | }; 23 | } 24 | 25 | init() { 26 | let button = document.getElementsByClassName('pinned-giveaways__button')[0]; 27 | if (!button) return; 28 | const container = button.previousElementSibling; 29 | container.classList.add('esgst-pgb-container'); 30 | button.remove(); 31 | button = createElements(container, 'afterend', [ 32 | { 33 | attributes: { 34 | class: 'esgst-pgb-button', 35 | }, 36 | type: 'div', 37 | children: [ 38 | { 39 | attributes: { 40 | class: 'esgst-pgb-icon fa fa-angle-down', 41 | }, 42 | type: 'i', 43 | }, 44 | ], 45 | }, 46 | ]); 47 | const icon = button.firstElementChild; 48 | button.addEventListener('click', this.pgb_toggle.bind(this, container, icon)); 49 | } 50 | 51 | pgb_toggle(container, icon) { 52 | container.classList.toggle('pinned-giveaways__inner-wrap--minimized'); 53 | icon.classList.toggle('fa-angle-down'); 54 | icon.classList.toggle('fa-angle-up'); 55 | } 56 | } 57 | 58 | const giveawaysPinnedGiveawaysButton = new GiveawaysPinnedGiveawaysButton(); 59 | 60 | export { giveawaysPinnedGiveawaysButton }; 61 | -------------------------------------------------------------------------------- /src/modules/Giveaways/CommunityWishlistSearchLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysCommunityWishlistSearchLink extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Turns the numbers in the "Giveaways" column of any{' '} 14 | community wishlist page into 15 | links that allow you to search for all of the active giveaways for the game (that are 16 | visible to you). 17 |
  • 18 |
19 | ), 20 | id: 'cwsl', 21 | name: 'Community Wishlist Search Link', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | init() { 28 | if (this.esgst.wishlistPath) { 29 | this.esgst.gameFeatures.push(this.cwsl_getGames.bind(this)); 30 | } 31 | } 32 | 33 | cwsl_getGames(games, main) { 34 | if (!main) { 35 | return; 36 | } 37 | for (const game of games.all) { 38 | let giveawayCount = game.heading.parentElement.nextElementSibling.nextElementSibling; 39 | createElements(giveawayCount, 'atinner', [ 40 | { 41 | attributes: { 42 | class: 'table__column__secondary-link', 43 | href: `/giveaways/search?${game.type.slice(0, -1)}=${game.id}`, 44 | }, 45 | type: 'a', 46 | children: [ 47 | ...Array.from(giveawayCount.childNodes).map((x) => { 48 | return { 49 | context: x, 50 | }; 51 | }), 52 | ], 53 | }, 54 | ]); 55 | } 56 | } 57 | } 58 | 59 | const giveawaysCommunityWishlistSearchLink = new GiveawaysCommunityWishlistSearchLink(); 60 | 61 | export { giveawaysCommunityWishlistSearchLink }; 62 | -------------------------------------------------------------------------------- /src/modules/Groups/GroupHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const getValue = common.getValue.bind(common); 7 | class GroupsGroupHighlighter extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Adds a green background to a group that you are a member of (in any page).
  • 14 |
15 | ), 16 | id: 'gh', 17 | name: 'Group Highlighter', 18 | sg: true, 19 | sync: 'Steam Groups', 20 | syncKeys: ['Groups'], 21 | type: 'groups', 22 | }; 23 | } 24 | 25 | init() { 26 | if (Shared.common.isCurrentPath('Steam - Groups')) return; 27 | Shared.esgst.endlessFeatures.push(this.gh_highlightGroups.bind(this)); 28 | } 29 | 30 | async gh_highlightGroups(context, main, source, endless) { 31 | const elements = context.querySelectorAll( 32 | `${ 33 | endless 34 | ? `.esgst-es-page-${endless} .table__column__heading[href*="/group/"], .esgst-es-page-${endless}.table__column__heading[href*="/group/"]` 35 | : `.table__column__heading[href*="/group/"]` 36 | }` 37 | ); 38 | if (!elements.length) return; 39 | const savedGroups = JSON.parse(getValue('groups', '[]')); 40 | for (let i = 0, n = elements.length; i < n; ++i) { 41 | const element = elements[i], 42 | code = element.getAttribute('href').match(/\/group\/(.+?)\//)[1]; 43 | let j; 44 | for (j = savedGroups.length - 1; j >= 0 && savedGroups[j].code !== code; --j) {} 45 | if (j >= 0 && savedGroups[j].member) { 46 | element.closest('.table__row-outer-wrap').classList.add('esgst-gh-highlight'); 47 | } 48 | } 49 | } 50 | } 51 | 52 | const groupsGroupHighlighter = new GroupsGroupHighlighter(); 53 | 54 | export { groupsGroupHighlighter }; 55 | -------------------------------------------------------------------------------- /src/modules/Users/UserTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class UsersUserTags extends Tags { 6 | constructor() { 7 | super('ut'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next a user's username (in any page) that 13 | allows you to save tags for the user (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
  • 28 | This feature is recommended for cases where you want to associate a short text with a 29 | user, since the tags are displayed next to their username.For a long text, check 30 | . 31 |
  • 32 |
33 | ), 34 | features: { 35 | ut_s: { 36 | name: 'Show tag suggestions while typing.', 37 | sg: true, 38 | st: true, 39 | }, 40 | }, 41 | id: 'ut', 42 | name: 'User Tags', 43 | sg: true, 44 | st: true, 45 | type: 'users', 46 | }; 47 | } 48 | 49 | init() { 50 | Shared.esgst.userFeatures.push(this.tags_addButtons.bind(this)); 51 | // noinspection JSIgnoredPromiseFromCall 52 | this.tags_getTags(); 53 | } 54 | } 55 | 56 | const usersUserTags = new UsersUserTags(); 57 | 58 | export { usersUserTags }; 59 | -------------------------------------------------------------------------------- /src/modules/Users/SteamFriendsIndicator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersSteamFriendsIndicator extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Adds an icon () next to the a user's username (in any 14 | page) to indicate that they are on your Steam friends list. 15 |
  • 16 |
  • If you hover over the icon, it shows the date when you became friends.
  • 17 |
18 | ), 19 | id: 'sfi', 20 | inputItems: [ 21 | { 22 | id: 'sfi_icon', 23 | prefix: `Icon: `, 24 | }, 25 | ], 26 | name: 'Steam Friends Indicator', 27 | sg: true, 28 | st: true, 29 | sync: 'Steam Friends', 30 | syncKeys: ['SteamFriends'], 31 | type: 'users', 32 | featureMap: { 33 | user: this.addIcons.bind(this), 34 | }, 35 | }; 36 | } 37 | 38 | addIcons(users) { 39 | for (const user of users) { 40 | if ( 41 | user.saved && 42 | user.saved.steamFriend && 43 | !user.context.parentElement.querySelector('.esgst-sfi-icon') 44 | ) { 45 | DOM.insert( 46 | user.context, 47 | 'afterend', 48 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | 65 | const usersSteamFriendsIndicator = new UsersSteamFriendsIndicator(); 66 | 67 | export { usersSteamFriendsIndicator }; 68 | -------------------------------------------------------------------------------- /src/modules/General/VisibleAttachedImages.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralVisibleAttachedImages extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | conflicts: ['ail'], 10 | description: () => ( 11 |
    12 |
  • 13 | Displays all of the attached images (in any page) by default so that you do not need to 14 | click on "View attached image" to view them. 15 |
  • 16 |
17 | ), 18 | features: { 19 | vai_gifv: { 20 | name: 'Rename .gifv images to .gif so that they are properly attached.', 21 | sg: true, 22 | st: true, 23 | }, 24 | }, 25 | id: 'vai', 26 | name: 'Visible Attached Images', 27 | sg: true, 28 | st: true, 29 | type: 'general', 30 | featureMap: { 31 | endless: this.vai_getImages.bind(this), 32 | }, 33 | }; 34 | } 35 | 36 | vai_getImages(context, main, source, endless) { 37 | let buttons = context.querySelectorAll( 38 | `${ 39 | endless 40 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached` 41 | : '.comment__toggle-attached' 42 | }, ${ 43 | endless 44 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached` 45 | : '.view_attached' 46 | }` 47 | ); 48 | for (let i = 0, n = buttons.length; i < n; i++) { 49 | let button = buttons[i]; 50 | let image = button.nextElementSibling.firstElementChild; 51 | let url = image.getAttribute('src'); 52 | if (url && Settings.get('vai_gifv')) { 53 | url = url.replace(/\.gifv/, '.gif'); 54 | image.setAttribute('src', url); 55 | } 56 | image.classList.remove('is_hidden', 'is-hidden'); 57 | } 58 | } 59 | } 60 | 61 | const generalVisibleAttachedImages = new GeneralVisibleAttachedImages(); 62 | 63 | export { generalVisibleAttachedImages }; 64 | -------------------------------------------------------------------------------- /.eslintrc.typed.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | greasemonkey: true, 6 | jquery: true, 7 | node: true, 8 | webextensions: true, 9 | }, 10 | rules: {}, 11 | overrides: [ 12 | { 13 | files: ['**/*.{js,jsx}'], 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:react/recommended', 20 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.** 21 | ], 22 | rules: { 23 | quotes: [ 24 | 'error', 25 | 'single', 26 | { 27 | avoidEscape: true, 28 | allowTemplateLiterals: false, 29 | }, 30 | ], 31 | 'react/react-in-jsx-scope': 'off', 32 | }, 33 | }, 34 | { 35 | files: ['**/*.{ts,tsx}'], 36 | parserOptions: { 37 | tsconfigRootDir: __dirname, 38 | project: ['./tsconfig.json'], 39 | }, 40 | plugins: ['prefer-arrow'], 41 | extends: [ 42 | 'eslint:recommended', 43 | 'plugin:react/recommended', 44 | 'plugin:@typescript-eslint/recommended', 45 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 46 | 'prettier/@typescript-eslint', // Disables TypeScript rules that conflict with Prettier. 47 | 'plugin:prettier/recommended', // Displays Prettier errors as ESLint errors. **Make sure this is always the last configuration.** 48 | ], 49 | rules: { 50 | quotes: 'off', 51 | '@typescript-eslint/quotes': [ 52 | 'error', 53 | 'single', 54 | { 55 | avoidEscape: true, 56 | allowTemplateLiterals: false, 57 | }, 58 | ], 59 | 'prefer-arrow/prefer-arrow-functions': [ 60 | 'error', 61 | { 62 | disallowPrototype: true, 63 | classPropertiesAllowed: true, 64 | }, 65 | ], 66 | 'react/react-in-jsx-scope': 'off', 67 | }, 68 | }, 69 | ], 70 | settings: { 71 | react: { 72 | version: 'detect', 73 | }, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../class/DOM'; 2 | import { Settings } from '../class/Settings'; 3 | import { Shared } from '../class/Shared'; 4 | 5 | class _Collapsible { 6 | create = (header: HTMLElement, body: HTMLElement, id?: string): HTMLElement => { 7 | const [collapseNode] = DOM.insert( 8 | header, 9 | 'afterbegin', 10 | 11 | {' '} 12 | 13 | ); 14 | const [expandNode] = DOM.insert( 15 | header, 16 | 'afterbegin', 17 | 18 | {' '} 19 | 20 | ); 21 | collapseNode.addEventListener('click', () => this.collapse(collapseNode, expandNode, body, id)); 22 | expandNode.addEventListener('click', () => this.expand(collapseNode, expandNode, body, id)); 23 | if (id && Settings.get(`${id}_collapsed`)) { 24 | this.collapse(collapseNode, expandNode, body); 25 | } 26 | return ( 27 | 28 | {header} 29 | {body} 30 | 31 | ); 32 | }; 33 | 34 | collapse = async ( 35 | collapseNode: HTMLElement, 36 | expandNode: HTMLElement, 37 | body: HTMLElement, 38 | id?: string 39 | ): Promise => { 40 | collapseNode.classList.add('esgst-hidden'); 41 | expandNode.classList.remove('esgst-hidden'); 42 | body.classList.add('esgst-hidden'); 43 | if (id) { 44 | await Shared.common.setSetting(`${id}_collapsed`, true); 45 | } 46 | }; 47 | 48 | expand = async ( 49 | collapseNode: HTMLElement, 50 | expandNode: HTMLElement, 51 | body: HTMLElement, 52 | id?: string 53 | ): Promise => { 54 | expandNode.classList.add('esgst-hidden'); 55 | collapseNode.classList.remove('esgst-hidden'); 56 | body.classList.remove('esgst-hidden'); 57 | if (id) { 58 | await Shared.common.setSetting(`${id}_collapsed`, false); 59 | } 60 | }; 61 | } 62 | 63 | export const Collapsible = new _Collapsible(); 64 | -------------------------------------------------------------------------------- /src/modules/General/TimeToPointCapCalculator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { EventDispatcher } from '../../class/EventDispatcher'; 5 | import { Events } from '../../constants/Events'; 6 | import { Session } from '../../class/Session'; 7 | import { Shared } from '../../class/Shared'; 8 | import { DOM } from '../../class/DOM'; 9 | 10 | class GeneralTimeToPointCapCalculator extends Module { 11 | constructor() { 12 | super(); 13 | this.info = { 14 | description: () => ( 15 |
    16 |
  • 17 | If you have less than 400P and you hover over the number of points at the header of any 18 | page, it shows how much time you have to wait until you have 400P. 19 |
  • 20 |
21 | ), 22 | features: { 23 | ttpcc_a: { 24 | name: 'Show time alongside points.', 25 | sg: true, 26 | }, 27 | }, 28 | id: 'ttpcc', 29 | name: 'Time To Point Cap Calculator', 30 | sg: true, 31 | type: 'general', 32 | }; 33 | } 34 | 35 | init() { 36 | EventDispatcher.subscribe(Events.POINTS_UPDATED, this.update.bind(this)); 37 | 38 | this.update(null, Session.counters.points); 39 | } 40 | 41 | update(oldPoints, newPoints) { 42 | if (newPoints >= 400) { 43 | return; 44 | } 45 | 46 | let nextRefresh = 60 - new Date().getMinutes(); 47 | 48 | while (nextRefresh > 15) { 49 | nextRefresh -= 15; 50 | } 51 | 52 | const time = this.esgst.modules.giveawaysTimeToEnterCalculator.ttec_getTime( 53 | Math.round((nextRefresh + 15 * Math.floor((400 - newPoints) / 6)) * 100) / 100 54 | ); 55 | 56 | const pointsNode = Shared.header.buttonContainers['account'].nodes.points; 57 | pointsNode.textContent = `${newPoints.toLocaleString('en-US')}${ 58 | Settings.get('ttpcc_a') ? `P / ${time} to 400` : '' 59 | }`; 60 | pointsNode.title = common.getFeatureTooltip('ttpcc', `${time} to 400P`); 61 | } 62 | } 63 | 64 | const generalTimeToPointCapCalculator = new GeneralTimeToPointCapCalculator(); 65 | 66 | export { generalTimeToPointCapCalculator }; 67 | -------------------------------------------------------------------------------- /src/modules/General/PageLoadTimestamp.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import dateFns_format from 'date-fns/format'; 3 | import { common } from '../Common'; 4 | import { Settings } from '../../class/Settings'; 5 | import { DOM } from '../../class/DOM'; 6 | import { Shared } from '../../class/Shared'; 7 | 8 | class GeneralPageLoadTimestamp extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a timestamp indicating when the page was loaded to any page, in the preferred 16 | location. 17 |
  • 18 |
19 | ), 20 | id: 'plt', 21 | name: 'Page Load Timestamp', 22 | inputItems: [ 23 | { 24 | id: 'plt_format', 25 | prefix: `Timestamp format: `, 26 | tooltip: `ESGST uses date-fns v2.0.0-alpha.25, so check the accepted tokens here: https://date-fns.org/v2.0.0-alpha.25/docs/Getting-Started.`, 27 | }, 28 | ], 29 | options: { 30 | title: `Position:`, 31 | values: ['Sidebar', 'Footer'], 32 | }, 33 | sg: true, 34 | st: true, 35 | type: 'general', 36 | }; 37 | } 38 | 39 | init() { 40 | const timestamp = dateFns_format( 41 | Date.now(), 42 | Settings.get('plt_format') || `MMM dd, yyyy, HH:mm:ss` 43 | ); 44 | switch (Settings.get('plt_index')) { 45 | case 0: 46 | if (this.esgst.sidebar) { 47 | DOM.insert( 48 | this.esgst.sidebar, 49 | 'afterbegin', 50 | 51 |

Page Load Timestamp

52 |
{timestamp}
53 |
54 | ); 55 | break; 56 | } 57 | case 1: { 58 | if (!Shared.footer) { 59 | return; 60 | } 61 | 62 | const linkContainer = Shared.footer.addLinkContainer({ 63 | name: `Page loaded on ${timestamp}`, 64 | side: 'left', 65 | }); 66 | 67 | linkContainer.nodes.outer.classList.add('esgst-plt'); 68 | 69 | break; 70 | } 71 | } 72 | } 73 | } 74 | 75 | const generalPageLoadTimestamp = new GeneralPageLoadTimestamp(); 76 | 77 | export { generalPageLoadTimestamp }; 78 | -------------------------------------------------------------------------------- /src/modules/Users/RealWonSentCVLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const createElements = common.createElements.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class UsersRealWonSentCVLink extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Turns "Gifts Won" and "Gifts Sent" in a user's{' '} 16 | profile page into links that take you 17 | to their real won/sent CV pages on SGTools. 18 |
  • 19 |
20 | ), 21 | features: { 22 | rwscvl_r: { 23 | name: `Link SGTools' reverse pages (from newest to oldest).`, 24 | sg: true, 25 | }, 26 | }, 27 | id: 'rwscvl', 28 | name: 'Real Won/Sent CV Link', 29 | sg: true, 30 | type: 'users', 31 | featureMap: { 32 | profile: this.rwscvl_add.bind(this), 33 | }, 34 | }; 35 | } 36 | 37 | rwscvl_add(profile) { 38 | let sentUrl, wonUrl; 39 | wonUrl = `http://www.sgtools.info/won/${profile.username}`; 40 | sentUrl = `http://www.sgtools.info/sent/${profile.username}`; 41 | if (Settings.get('rwscvl_r')) { 42 | wonUrl += '/newestfirst'; 43 | sentUrl += '/newestfirst'; 44 | } 45 | createElements(profile.wonRowLeft, 'atinner', [ 46 | { 47 | attributes: { 48 | class: 'esgst-rwscvl-link', 49 | href: wonUrl, 50 | target: '_blank', 51 | title: getFeatureTooltip('rwscvl'), 52 | }, 53 | text: 'Gifts Won', 54 | type: 'a', 55 | }, 56 | ]); 57 | createElements(profile.sentRowLeft, 'atinner', [ 58 | { 59 | attributes: { 60 | class: 'esgst-rwscvl-link', 61 | href: sentUrl, 62 | target: '_blank', 63 | title: getFeatureTooltip('rwscvl'), 64 | }, 65 | text: 'Gifts Sent', 66 | type: 'a', 67 | }, 68 | ]); 69 | } 70 | } 71 | 72 | const usersRealWonSentCVLink = new UsersRealWonSentCVLink(); 73 | 74 | export { usersRealWonSentCVLink }; 75 | -------------------------------------------------------------------------------- /src/modules/Giveaways/HiddenGamesEnterButtonDisabler.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysHiddenGamesEnterButtonDisabler extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Disables the enter button of any giveaway if you have hidden the game on SteamGifts so 14 | that you do not accidentally enter it. 15 |
  • 16 |
17 | ), 18 | id: 'hgebd', 19 | name: "Hidden Game's Enter Button Disabler", 20 | sg: true, 21 | sync: 'Hidden Games', 22 | syncKeys: ['HiddenGames'], 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | init() { 28 | if (!this.esgst.giveawayPath || document.getElementsByClassName('table--summary')[0]) { 29 | return; 30 | } 31 | const hideButton = document.getElementsByClassName('featured__giveaway__hide')[0]; 32 | if ( 33 | (this.esgst.enterGiveawayButton || 34 | (this.esgst.giveawayErrorButton && 35 | !this.esgst.giveawayErrorButton.textContent.match(/Exists\sin\sAccount/))) && 36 | !hideButton 37 | ) { 38 | const parent = (this.esgst.enterGiveawayButton || this.esgst.giveawayErrorButton) 39 | .parentElement; 40 | if (this.esgst.enterGiveawayButton) { 41 | this.esgst.enterGiveawayButton.remove(); 42 | } 43 | if (this.esgst.giveawayErrorButton) { 44 | this.esgst.giveawayErrorButton.remove(); 45 | } 46 | createElements(parent, 'afterbegin', [ 47 | { 48 | attributes: { 49 | class: 'sidebar__error is-disabled', 50 | }, 51 | type: 'div', 52 | children: [ 53 | { 54 | attributes: { 55 | class: 'fa fa-exclamation-circle', 56 | }, 57 | type: 'i', 58 | }, 59 | { 60 | text: ' Hidden Game', 61 | type: 'node', 62 | }, 63 | ], 64 | }, 65 | ]); 66 | } 67 | } 68 | } 69 | 70 | const giveawaysHiddenGamesEnterButtonDisabler = new GiveawaysHiddenGamesEnterButtonDisabler(); 71 | 72 | export { giveawaysHiddenGamesEnterButtonDisabler }; 73 | -------------------------------------------------------------------------------- /src/modules/General/PaginationNavigationOnTop.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const getFeatureTooltip = common.getFeatureTooltip.bind(common); 7 | class GeneralPaginationNavigationOnTop extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Moves the pagination navigation of any page to the main page heading of the page.
  • 14 |
15 | ), 16 | features: { 17 | pnot_s: { 18 | name: `Enable simplified view (will show only the numbers and arrows).`, 19 | sg: true, 20 | st: true, 21 | }, 22 | }, 23 | id: 'pnot', 24 | name: 'Pagination Navigation On Top', 25 | sg: true, 26 | st: true, 27 | type: 'general', 28 | }; 29 | } 30 | 31 | init() { 32 | if (!this.esgst.paginationNavigation || !this.esgst.mainPageHeading) return; 33 | 34 | if (this.esgst.st) { 35 | this.esgst.paginationNavigation.classList.add('page_heading_btn'); 36 | } 37 | this.esgst.paginationNavigation.title = getFeatureTooltip('pnot'); 38 | this.pnot_simplify(); 39 | DOM.insert( 40 | this.esgst.mainPageHeading.querySelector( 41 | `.page__heading__breadcrumbs, .page_heading_breadcrumbs` 42 | ), 43 | 'afterend', 44 | this.esgst.paginationNavigation 45 | ); 46 | } 47 | 48 | pnot_simplify() { 49 | if (Settings.get('pnot') && Settings.get('pnot_s')) { 50 | const elements = this.esgst.paginationNavigation.querySelectorAll('span'); 51 | // @ts-ignore 52 | for (const element of elements) { 53 | if (element.textContent.match(/[A-Za-z]+/)) { 54 | element.textContent = element.textContent.replace(/[A-Za-z]+/g, ''); 55 | if (element.previousElementSibling) { 56 | element.appendChild(element.previousElementSibling); 57 | } 58 | if (element.nextElementSibling) { 59 | element.appendChild(element.nextElementSibling); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | const generalPaginationNavigationOnTop = new GeneralPaginationNavigationOnTop(); 68 | 69 | export { generalPaginationNavigationOnTop }; 70 | -------------------------------------------------------------------------------- /src/modules/Discussions/RefreshActiveDiscussionsButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const checkMissingDiscussions = common.checkMissingDiscussions.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class DiscussionsRefreshActiveDiscussionsButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button () to the page heading of the active 16 | discussions (in the main page) that allows you to refresh the active discussions without 17 | having to refresh the entire page. 18 |
  • 19 |
20 | ), 21 | id: 'radb', 22 | name: 'Refresh Active Discussions Button', 23 | sg: true, 24 | type: 'discussions', 25 | }; 26 | } 27 | 28 | radb_addButtons() { 29 | let elements, i; 30 | elements = this.esgst.activeDiscussions.querySelectorAll( 31 | `.block_header, .esgst-heading-button` 32 | ); 33 | for (i = elements.length - 1; i > -1; --i) { 34 | DOM.insert( 35 | elements[i], 36 | 'beforebegin', 37 |
{ 41 | let icon = event.currentTarget.firstElementChild; 42 | icon.classList.add('fa-spin'); 43 | if (Settings.get('oadd')) { 44 | // noinspection JSIgnoredPromiseFromCall 45 | this.esgst.modules.discussionsOldActiveDiscussionsDesign.oadd_load(true, () => { 46 | icon.classList.remove('fa-spin'); 47 | }); 48 | } else { 49 | checkMissingDiscussions(true, () => { 50 | icon.classList.remove('fa-spin'); 51 | }); 52 | } 53 | }} 54 | > 55 | 56 |
57 | ); 58 | } 59 | } 60 | } 61 | 62 | const discussionsRefreshActiveDiscussionsButton = new DiscussionsRefreshActiveDiscussionsButton(); 63 | 64 | export { discussionsRefreshActiveDiscussionsButton }; 65 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayWinnersLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysGiveawayWinnersLink extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Adds a link next to an ended giveaway's "Entries" link (in any page) that shows how many 14 | winners the giveaway has and takes you to the giveaway's{' '} 15 | winners page. 16 |
  • 17 |
18 | ), 19 | id: 'gwl', 20 | name: 'Giveaway Winners Link', 21 | sg: true, 22 | type: 'giveaways', 23 | featureMap: { 24 | giveaway: this.gwl_addLinks.bind(this), 25 | }, 26 | }; 27 | } 28 | 29 | gwl_addLinks(giveaways, main) { 30 | if ( 31 | ((!this.esgst.createdPath && 32 | !this.esgst.enteredPath && 33 | !this.esgst.wonPath && 34 | !this.esgst.giveawayPath && 35 | !this.esgst.archivePath) || 36 | main) && 37 | (this.esgst.giveawayPath || 38 | this.esgst.createdPath || 39 | this.esgst.enteredPath || 40 | this.esgst.wonPath || 41 | this.esgst.archivePath) 42 | ) 43 | return; 44 | giveaways.forEach((giveaway) => { 45 | if (giveaway.innerWrap.getElementsByClassName('esgst-gwl')[0] || !giveaway.ended) return; 46 | const attributes = { 47 | class: 'esgst-gwl', 48 | ['data-draggable-id']: 'winners_count', 49 | }; 50 | if (giveaway.url) { 51 | attributes.href = `${giveaway.url}/winners`; 52 | } 53 | createElements(giveaway.entriesLink, 'afterend', [ 54 | { 55 | attributes, 56 | type: 'a', 57 | children: [ 58 | { 59 | attributes: { 60 | class: 'fa fa-trophy', 61 | }, 62 | type: 'i', 63 | }, 64 | { 65 | text: `${giveaway.numWinners} winners`, 66 | type: 'span', 67 | }, 68 | ], 69 | }, 70 | ]); 71 | }); 72 | } 73 | } 74 | 75 | const giveawaysGiveawayWinnersLink = new GiveawaysGiveawayWinnersLink(); 76 | 77 | export { giveawaysGiveawayWinnersLink }; 78 | -------------------------------------------------------------------------------- /src/modules/Users/SteamGiftsProfileButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const createElements = common.createElements.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class UsersSteamGiftsProfileButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button next to the "Visit Steam Profile" button of a user's{' '} 16 | profile page that 17 | allows you to go to their SteamGifts profile page. 18 |
  • 19 |
20 | ), 21 | id: 'sgpb', 22 | name: 'SteamGifts Profile Button', 23 | st: true, 24 | type: 'users', 25 | }; 26 | } 27 | 28 | init() { 29 | if (!Shared.esgst.userPath) return; 30 | Shared.esgst.profileFeatures.push(this.sgpb_add.bind(this)); 31 | } 32 | 33 | sgpb_add(profile) { 34 | let button; 35 | button = createElements(profile.steamButtonContainer, 'beforeend', [ 36 | { 37 | attributes: { 38 | class: 'esgst-sgpb-container', 39 | title: getFeatureTooltip('sgpb'), 40 | }, 41 | type: 'div', 42 | children: [ 43 | { 44 | attributes: { 45 | class: 'esgst-sgpb-button', 46 | href: `https://www.steamgifts.com/go/user/${profile.steamId}`, 47 | rel: 'nofollow', 48 | target: '_blank', 49 | }, 50 | type: 'a', 51 | children: [ 52 | { 53 | attributes: { 54 | class: 'fa', 55 | }, 56 | type: 'i', 57 | children: [ 58 | { 59 | attributes: { 60 | src: Shared.esgst.sgIcon, 61 | }, 62 | type: 'img', 63 | }, 64 | ], 65 | }, 66 | { 67 | text: 'Visit SteamGifts Profile', 68 | type: 'span', 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, 74 | ]); 75 | button.insertBefore(profile.steamButton, button.firstElementChild); 76 | } 77 | } 78 | 79 | const usersSteamGiftsProfileButton = new UsersSteamGiftsProfileButton(); 80 | 81 | export { usersSteamGiftsProfileButton }; 82 | -------------------------------------------------------------------------------- /src/modules/Users/UserLinks.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersUserLinks extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • Allows you to add custom links next to a user's username in their profile page.
  • 13 |
  • 14 | Can be used in other pages through . 15 |
  • 16 |
  • 17 | Comes by default with 5 links to BLAEO, Playing Appreciated, Touhou Giveaways, AStats 18 | and SteamRep. 19 |
  • 20 |
21 | ), 22 | id: 'ul', 23 | name: 'User Links', 24 | sg: true, 25 | type: 'users', 26 | featureMap: { 27 | profile: this.ul_add.bind(this), 28 | }, 29 | }; 30 | } 31 | 32 | ul_add(profile) { 33 | const items = []; 34 | const iconRegex = /^(fa-.+?)($|\s)/; 35 | const imageRegex = /^(https?:\/\/.+?)($|\s)/; 36 | const textRegex = /^(.+?)($|\s(fa-|https?:\/\/))/; 37 | for (const link of Settings.get('ul_links')) { 38 | const children = []; 39 | let label = link.label; 40 | while (label) { 41 | const icon = label.match(iconRegex); 42 | if (icon) { 43 | label = label.replace(iconRegex, ''); 44 | children.push(); 45 | continue; 46 | } 47 | const image = label.match(imageRegex); 48 | if (image) { 49 | label = label.replace(imageRegex, ''); 50 | children.push( 51 | 52 | ); 53 | continue; 54 | } 55 | const text = label.match(textRegex); 56 | if (text) { 57 | label = label.replace(textRegex, `$3`); 58 | children.push(text[1]); 59 | } 60 | } 61 | items.push( 62 | 68 | {children} 69 | 70 | ); 71 | } 72 | DOM.insert(profile.heading, 'beforeend', {items}); 73 | } 74 | } 75 | 76 | const usersUserLinks = new UsersUserLinks(); 77 | 78 | export { usersUserLinks }; 79 | -------------------------------------------------------------------------------- /src/modules/Comments/ReplyMentionLink.tsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class CommentsReplyMentionLink extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Adds a link (@user) next to a reply's "Permalink" (in any page) that mentions the user 12 | being replied to and links to their comment. 13 |
  • 14 |
  • 15 | This feature is useful for conversations that have very deep nesting levels, which makes 16 | it impossible to know who replied to whom. 17 |
  • 18 |
19 | ), 20 | id: 'rml', 21 | name: 'Reply Mention Link', 22 | sg: true, 23 | st: true, 24 | type: 'comments', 25 | featureMap: { 26 | commentV2: this.addLinks.bind(this), 27 | }, 28 | }; 29 | } 30 | 31 | addLinks(comments: IComment[]) { 32 | for (const comment of comments) { 33 | this.addLink(comment); 34 | this.addLinks(comment.children); 35 | } 36 | } 37 | 38 | addLink(comment: IComment) { 39 | if (comment.parent && !comment.nodes.rmlLink) { 40 | DOM.insert( 41 | comment.nodes.actions, 42 | 'beforeend', 43 | (comment.nodes.rmlLink = ref)} 47 | > 48 | {`@${comment.data.isDeleted ? '[Deleted]' : comment.parent.author.data.username}`} 49 | 50 | ); 51 | } 52 | } 53 | 54 | rml_addLink(parent: HTMLElement, children: HTMLElement[]) { 55 | const authorUsername = parent 56 | .querySelector('.comment__username, .author_name') 57 | .textContent.trim(); 58 | const commentCode = parent.id; 59 | for (const child of children) { 60 | const actions = child.querySelector('.comment__actions, .action_list'); 61 | const rmlLink = actions.querySelector('.esgst-rml-link'); 62 | if (rmlLink) { 63 | rmlLink.textContent = `@${authorUsername}`; 64 | } else { 65 | DOM.insert( 66 | actions, 67 | 'beforeend', 68 | 69 | {`@${authorUsername}`} 70 | 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | 77 | const commentsReplyMentionLink = new CommentsReplyMentionLink(); 78 | 79 | export { commentsReplyMentionLink }; 80 | -------------------------------------------------------------------------------- /src/modules/Comments/ReplyBoxPopup.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { Module } from '../../class/Module'; 3 | import { Popup } from '../../class/Popup'; 4 | import { Shared } from '../../class/Shared'; 5 | import { Button } from '../../components/Button'; 6 | 7 | class CommentsReplyBoxPopup extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • 14 | Adds a button () to the main page heading of any page 15 | that allows you to add comments to the page through a popup. 16 |
  • 17 |
  • 18 | This feature is useful if you have enabled, 19 | which allows you to add comments to the page from any scrolling position. 20 |
  • 21 |
22 | ), 23 | id: 'rbp', 24 | name: 'Reply Box Popup', 25 | sg: true, 26 | st: true, 27 | type: 'comments', 28 | }; 29 | } 30 | 31 | init() { 32 | if (!Shared.esgst.replyBox) return; 33 | 34 | let button = Shared.common.createHeadingButton({ 35 | id: 'rbp', 36 | icons: ['fa-comment'], 37 | title: 'Add a comment', 38 | }); 39 | let popup = new Popup({ 40 | addProgress: true, 41 | addScrollable: true, 42 | icon: 'fa-comment', 43 | title: `Add a comment:`, 44 | }); 45 | DOM.insert( 46 | popup.scrollable, 47 | 'beforeend', 48 | 49 | ); 50 | Button.create([ 51 | { 52 | template: 'success', 53 | name: 'Save', 54 | onClick: async () => { 55 | await Shared.common.saveComment( 56 | null, 57 | Shared.esgst.sg ? '' : document.querySelector(`[name="trade_code"]`).value, 58 | '', 59 | popup.textArea.value, 60 | Shared.esgst.sg ? Shared.esgst.locationHref.match(/(.+?)(#.+?)?$/)[1] : '/ajax.php', 61 | popup.progressBar, 62 | true 63 | ); 64 | }, 65 | }, 66 | { 67 | template: 'loading', 68 | isDisabled: true, 69 | name: 'Saving...', 70 | }, 71 | ]).insert(popup.description, 'beforeend'); 72 | button.addEventListener( 73 | 'click', 74 | popup.open.bind(popup, popup.textArea.focus.bind(popup.textArea)) 75 | ); 76 | } 77 | } 78 | 79 | const commentsReplyBoxPopup = new CommentsReplyBoxPopup(); 80 | 81 | export { commentsReplyBoxPopup }; 82 | -------------------------------------------------------------------------------- /src/modules/Users/VisibleGiftsBreakdown.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersVisibleGiftsBreakdown extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Shows the gifts breakdown of a user in their profile page, with the following initials: 14 |
  • 15 |
      16 |
    • FCV - Full CV
    • 17 |
    • RCV - Reduced CV
    • 18 |
    • NCV - No CV
    • 19 |
    • A - Awaiting Feedback
    • 20 |
    • NR - Not Received
    • 21 |
    22 |
23 | ), 24 | id: 'vgb', 25 | inputItems: [ 26 | { 27 | id: 'vgb_wonFormat', 28 | prefix: `Won Format: `, 29 | tooltip: `[FCV], [RCV], [NCV] and [NR] will be replaced with their respective values.`, 30 | }, 31 | { 32 | id: 'vgb_sentFormat', 33 | prefix: `Sent Format: `, 34 | tooltip: `[FCV], [RCV], [NCV], [A] and [NR] will be replaced with their respective values.`, 35 | }, 36 | ], 37 | name: 'Visible Gifts Breakdown', 38 | options: { 39 | title: `Position: `, 40 | values: ['Left', 'Right'], 41 | }, 42 | sg: true, 43 | type: 'users', 44 | featureMap: { 45 | profile: this.vgb_add.bind(this), 46 | }, 47 | }; 48 | } 49 | 50 | vgb_add(profile) { 51 | const position = Settings.get('vgb_index') === 0 ? 'afterbegin' : 'beforeend'; 52 | DOM.insert( 53 | profile.wonRowRight.firstElementChild.firstElementChild, 54 | position, 55 | {` ${Settings.get('vgb_wonFormat') 56 | .replace(/\[FCV]/, profile.wonFull) 57 | .replace(/\[RCV]/, profile.wonReduced) 58 | .replace(/\[NCV]/, profile.wonZero) 59 | .replace(/\[NR]/, profile.wonNotReceived)} `} 60 | ); 61 | DOM.insert( 62 | profile.sentRowRight.firstElementChild.firstElementChild, 63 | position, 64 | {` ${Settings.get('vgb_sentFormat') 65 | .replace(/\[FCV]/, profile.sentFull) 66 | .replace(/\[RCV]/, profile.sentReduced) 67 | .replace(/\[NCV]/, profile.sentZero) 68 | .replace(/\[A]/, profile.sentAwaiting) 69 | .replace(/\[NR]/, profile.sentNotReceived)} `} 70 | ); 71 | } 72 | } 73 | 74 | const usersVisibleGiftsBreakdown = new UsersVisibleGiftsBreakdown(); 75 | 76 | export { usersVisibleGiftsBreakdown }; 77 | -------------------------------------------------------------------------------- /src/modules/General/ImageBorders.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralImageBorders extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • Brings back image borders to SteamGifts.
  • 11 |
12 | ), 13 | id: 'ib', 14 | name: 'Image Borders', 15 | sg: true, 16 | type: 'general', 17 | featureMap: { 18 | endless: this.ib_addBorders.bind(this), 19 | }, 20 | }; 21 | } 22 | 23 | ib_addBorders(context, main, source, endless) { 24 | const userElements = context.querySelectorAll( 25 | `${ 26 | endless 27 | ? `.esgst-es-page-${endless} .giveaway_image_avatar, .esgst-es-page-${endless}.giveaway_image_avatar` 28 | : '.giveaway_image_avatar' 29 | }, ${ 30 | endless 31 | ? `.esgst-es-page-${endless} .featured_giveaway_image_avatar, .esgst-es-page-${endless}.featured_giveaway_image_avatar` 32 | : '.featured_giveaway_image_avatar' 33 | }, ${ 34 | endless 35 | ? `.esgst-es-page-${endless} :not(.esgst-ggl-panel) .table_image_avatar, .esgst-es-page-${endless}:not(.esgst-ggl-panel) .table_image_avatar` 36 | : `:not(.esgst-ggl-panel) .table_image_avatar` 37 | }` 38 | ); 39 | for (let i = 0, n = userElements.length; i < n; ++i) { 40 | userElements[i].classList.add('esgst-ib-user'); 41 | } 42 | const gameElements = context.querySelectorAll( 43 | `${ 44 | endless 45 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail, .esgst-es-page-${endless}.giveaway_image_thumbnail` 46 | : '.giveaway_image_thumbnail' 47 | }, ${ 48 | endless 49 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail_missing, .esgst-es-page-${endless}.giveaway_image_thumbnail_missing` 50 | : '.giveaway_image_thumbnail_missing' 51 | }, ${ 52 | endless 53 | ? `.esgst-es-page-${endless} .table_image_thumbnail, .esgst-es-page-${endless}.table_image_thumbnail` 54 | : '.table_image_thumbnail' 55 | }, ${ 56 | endless 57 | ? `.esgst-es-page-${endless} .table_image_thumbnail_missing, .esgst-es-page-${endless}.table_image_thumbnail_missing` 58 | : '.table_image_thumbnail_missing' 59 | }` 60 | ); 61 | for (let i = 0, n = gameElements.length; i < n; ++i) { 62 | gameElements[i].classList.add('esgst-ib-game'); 63 | } 64 | } 65 | } 66 | 67 | const generalImageBorders = new GeneralImageBorders(); 68 | 69 | export { generalImageBorders }; 70 | -------------------------------------------------------------------------------- /src/modules/Comments/ReceivedReplyBoxPopup.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { Module } from '../../class/Module'; 3 | import { Popup } from '../../class/Popup'; 4 | import { Settings } from '../../class/Settings'; 5 | import { Shared } from '../../class/Shared'; 6 | import { Button } from '../../components/Button'; 7 | 8 | class CommentsReceivedReplyBoxPopup extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Pops up a reply box when you mark a giveaway as received (in your{' '} 16 | won page) so that you can add a 17 | comment thanking the creator. 18 |
  • 19 |
20 | ), 21 | id: 'rrbp', 22 | name: 'Received Reply Box Popup', 23 | sg: true, 24 | type: 'comments', 25 | }; 26 | } 27 | 28 | init() { 29 | if (!Shared.esgst.wonPath) return; 30 | Shared.esgst.giveawayFeatures.push(this.rrbp_addEvent.bind(this)); 31 | } 32 | 33 | rrbp_addEvent(giveaways) { 34 | giveaways.forEach((giveaway) => { 35 | let feedback = giveaway.outerWrap.getElementsByClassName( 36 | 'table__gift-feedback-awaiting-reply' 37 | )[0]; 38 | if (feedback) { 39 | feedback.addEventListener('click', this.rrbp_openPopup.bind(this, giveaway)); 40 | } 41 | }); 42 | } 43 | 44 | rrbp_openPopup(giveaway) { 45 | let popup, textArea; 46 | popup = new Popup({ 47 | addProgress: true, 48 | addScrollable: true, 49 | icon: 'fa-comment', 50 | title: `Add a comment:`, 51 | }); 52 | DOM.insert(popup.scrollable, 'beforeend',