├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .eslintrc.typed.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── enhancement-request.md │ └── feature-request.md └── workflows │ ├── beta.yml │ └── stable.yml ├── .gitignore ├── .gitlab-ci.yml ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── PRIVACY_POLICY ├── README.md ├── assets ├── chrome-badge.png ├── firefox-badge.png └── palemoon-badge.png ├── browser └── messages.json ├── config.json ├── html.d.ts ├── karma.conf.js ├── messages.json ├── package.json ├── pnpm-lock.yaml ├── scripts ├── common.js ├── convertElementsToV2.js ├── generateCommit.js ├── generateRelease.js ├── notifyIssue.js └── updateEmojis.js ├── src ├── _locales │ └── en │ │ └── messages.json ├── assets │ ├── images │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-32.png │ │ ├── icon-64.png │ │ └── icon.png │ └── styles │ │ ├── bootstrap │ │ └── index.css │ │ ├── index.js │ │ └── patches │ │ └── query-builder.css ├── browser-gm.js ├── browser-sdk.js ├── browser-webext.js ├── browser.ts ├── class │ ├── Button.jsx │ ├── Checkbox.jsx │ ├── DOM.ts │ ├── DropboxStorage.js │ ├── Esgst.js │ ├── EventDispatcher.js │ ├── FetchRequest.ts │ ├── GoogleDriveStorage.js │ ├── I18N.js │ ├── ICloudStorage.js │ ├── LocalStorage.js │ ├── Lock.ts │ ├── Logger.jsx │ ├── Module.js │ ├── OneDriveStorage.js │ ├── Permissions.ts │ ├── PermissionsUi.tsx │ ├── PersistentStorage.js │ ├── Popout.jsx │ ├── Popup.jsx │ ├── Process.jsx │ ├── Queue.ts │ ├── Scope.ts │ ├── Session.js │ ├── Settings.js │ ├── SettingsWizard.tsx │ ├── Shared.js │ ├── Table.jsx │ ├── Tabs.js │ └── ToggleSwitch.jsx ├── components │ ├── Base.tsx │ ├── Button.tsx │ ├── Collapsible.tsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── NotificationBar.tsx │ └── PageHeading.tsx ├── constants │ ├── ClassNames.ts │ ├── Events.js │ └── Namespaces.js ├── dependencies.ts ├── entry │ ├── eventPage_index.js │ ├── eventPage_sdk_banner.js │ ├── eventPage_sdk_index.js │ ├── gm_index.js │ ├── index.js │ ├── monkey_banner.js │ ├── permissions_index.js │ └── sdk_index.js ├── eventPage.js ├── eventPage_sdk.js ├── global.d.ts ├── html │ ├── options.html │ └── permissions.html ├── lib │ ├── jsUtils │ │ └── index.js │ └── parsedown │ │ └── index.js ├── main.js ├── models │ ├── AttachedImage.tsx │ ├── Base.tsx │ ├── Comment.tsx │ ├── CommentBox.tsx │ ├── CommentEntity.tsx │ ├── Game.tsx │ ├── Giveaway.tsx │ └── User.tsx ├── modules │ ├── CloudStorage.jsx │ ├── Comments.js │ ├── Comments │ │ ├── CollapseExpandReplyButton.jsx │ │ ├── CommentFilters.jsx │ │ ├── CommentFormattingHelper.jsx │ │ ├── CommentHistory.jsx │ │ ├── CommentReverser.jsx │ │ ├── CommentSearcher.jsx │ │ ├── CommentTracker.jsx │ │ ├── CommentVariables.jsx │ │ ├── MultiReply.jsx │ │ ├── ReceivedReplyBoxPopup.jsx │ │ ├── ReplyBoxOnTop.jsx │ │ ├── ReplyBoxPopup.jsx │ │ ├── ReplyFromInbox.jsx │ │ └── ReplyMentionLink.tsx │ ├── Common.jsx │ ├── DiscussionPanels.js │ ├── Discussions.js │ ├── Discussions │ │ ├── ActiveDiscussionsOnTopSidebar.jsx │ │ ├── CloseOpenDiscussionButton.jsx │ │ ├── DiscussionFilters.jsx │ │ ├── DiscussionTags.jsx │ │ ├── DiscussionsSorter.jsx │ │ ├── ImprovedDiscussionBookmarks.jsx │ │ ├── MainPostPopup.jsx │ │ ├── MainPostSkipper.jsx │ │ ├── OldActiveDiscussionsDesign.jsx │ │ ├── PuzzleMarker.jsx │ │ ├── RefreshActiveDiscussionsButton.jsx │ │ └── ReversedActiveDiscussions.jsx │ ├── Filters.jsx │ ├── Games.js │ ├── Games │ │ ├── EnteredGameHighlighter.jsx │ │ ├── GameCategories.jsx │ │ ├── GameFilters.jsx │ │ └── GameTags.jsx │ ├── General │ │ ├── AccurateTimestamp.jsx │ │ ├── AttachedImageCarousel.jsx │ │ ├── AttachedImageLoader.jsx │ │ ├── CakeDayReminder.jsx │ │ ├── ContentLoader.jsx │ │ ├── CustomHeaderFooterLinks.jsx │ │ ├── ElementFilters.jsx │ │ ├── EmbeddedVideos.jsx │ │ ├── EndlessScrolling.jsx │ │ ├── FixedFooter.jsx │ │ ├── FixedHeader.jsx │ │ ├── FixedMainPageHeading.jsx │ │ ├── FixedSidebar.jsx │ │ ├── GiveawayDiscussionTicketTradeTracker.jsx │ │ ├── HeaderRefresher.jsx │ │ ├── HiddenBlacklistStats.jsx │ │ ├── HiddenCommunityPoll.jsx │ │ ├── ImageBorders.jsx │ │ ├── LastPageLink.jsx │ │ ├── LevelProgressVisualizer.jsx │ │ ├── MultiManager.jsx │ │ ├── NarrowSidebar.jsx │ │ ├── NotificationMerger.jsx │ │ ├── PageLoadTimestamp.jsx │ │ ├── PaginationNavigationOnTop.jsx │ │ ├── PointsVisualizer.jsx │ │ ├── QuickInboxView.jsx │ │ ├── SameTabOpener.jsx │ │ ├── ScrollToBottomButton.jsx │ │ ├── ScrollToTopButton.jsx │ │ ├── SearchClearButton.jsx │ │ ├── SearchMagnifyingGlassButton.jsx │ │ ├── ShortcutKeys.jsx │ │ ├── TableSorter.jsx │ │ ├── ThreadSubscription.jsx │ │ ├── TimeToPointCapCalculator.jsx │ │ ├── URLRedirector.jsx │ │ ├── VisibleAttachedImages.jsx │ │ └── VisibleFullLevel.jsx │ ├── Giveaways.jsx │ ├── Giveaways │ │ ├── AdvancedGiveawaySearch.jsx │ │ ├── ArchiveSearcher.jsx │ │ ├── BlacklistGiveawayLoader.jsx │ │ ├── CommentEntryChecker.jsx │ │ ├── CommunityWishlistSearchLink.jsx │ │ ├── CreatedEnteredWonGiveawayDetails.jsx │ │ ├── CustomGiveawayBackground.jsx │ │ ├── CustomGiveawayCalendar.jsx │ │ ├── DeleteKeyConfirmation.jsx │ │ ├── EnterLeaveGiveawayButton.jsx │ │ ├── EnteredGiveawaysStats.jsx │ │ ├── EntryTracker.jsx │ │ ├── FollowedGamesPage.jsx │ │ ├── GiveawayBookmarks.jsx │ │ ├── GiveawayCopyHighlighter.jsx │ │ ├── GiveawayEncrypterDecrypter.jsx │ │ ├── GiveawayEndTimeHighlighter.jsx │ │ ├── GiveawayErrorSearchLinks.tsx │ │ ├── GiveawayExtractor.jsx │ │ ├── GiveawayFilters.jsx │ │ ├── GiveawayLevelHighlighter.jsx │ │ ├── GiveawayPointsToWin.jsx │ │ ├── GiveawayPopup.jsx │ │ ├── GiveawayRecreator.jsx │ │ ├── GiveawayTemplates.jsx │ │ ├── GiveawayWinnersLink.jsx │ │ ├── GiveawayWinningChance.jsx │ │ ├── GiveawayWinningRatio.jsx │ │ ├── GiveawaysSorter.jsx │ │ ├── GridView.jsx │ │ ├── HiddenGamesEnterButtonDisabler.jsx │ │ ├── HiddenGamesManager.jsx │ │ ├── IsThereAnyDealInfo.jsx │ │ ├── MultipleGiveawayCreator.jsx │ │ ├── NewGiveawayDescriptionChecker.jsx │ │ ├── NextPreviousTrainHotkeys.jsx │ │ ├── OneClickHideGiveawayButton.jsx │ │ ├── PinnedGiveawaysButton.jsx │ │ ├── QuickGiveawaySearch.jsx │ │ ├── RealCVCalculator.jsx │ │ ├── SentKeySearcher.jsx │ │ ├── SteamActivationLinks.jsx │ │ ├── StickiedGiveawayCountries.jsx │ │ ├── StickiedGiveawayGroups.jsx │ │ ├── TimeToEnterCalculator.jsx │ │ ├── UnfadedEnteredGiveaway.jsx │ │ ├── UnhideGiveawayButton.jsx │ │ ├── UnsentGiftSender.jsx │ │ └── VisibleInviteOnlyGiveaways.jsx │ ├── Giveaways_addToStorage.js │ ├── Groups.js │ ├── Groups │ │ ├── GroupFilters.jsx │ │ ├── GroupHighlighter.jsx │ │ ├── GroupLibraryWishlistChecker.jsx │ │ ├── GroupStats.jsx │ │ └── GroupTags.jsx │ ├── Profile.js │ ├── Settings.jsx │ ├── Storage.jsx │ ├── Style.js │ ├── Sync.jsx │ ├── Tags.jsx │ ├── Trades │ │ ├── HaveWantListChecker.jsx │ │ ├── HeaderTradesButton.jsx │ │ ├── TradeBumper.jsx │ │ └── TradeFilters.jsx │ ├── Users.js │ ├── Users │ │ ├── InboxWinnerHighlighter.jsx │ │ ├── LevelUpCalculator.jsx │ │ ├── NotActivatedMultipleWinChecker.jsx │ │ ├── NotReceivedFinder.jsx │ │ ├── ProfileLinks.jsx │ │ ├── RealWonSentCVLink.jsx │ │ ├── SentWonRatio.jsx │ │ ├── SharedGroupChecker.jsx │ │ ├── SteamFriendsIndicator.jsx │ │ ├── SteamGiftsProfileButton.jsx │ │ ├── UserFilters.jsx │ │ ├── UserGiveawayData.jsx │ │ ├── UserLinks.jsx │ │ ├── UserNotes.jsx │ │ ├── UserStats.jsx │ │ ├── UserSuspensionChecker.jsx │ │ ├── UserTags.jsx │ │ ├── UsernameHistory.jsx │ │ ├── VisibleGiftsBreakdown.jsx │ │ ├── VisibleRealCV.jsx │ │ ├── WhitelistBlacklistChecker.jsx │ │ ├── WhitelistBlacklistHighlighter.jsx │ │ ├── WhitelistBlacklistManager.jsx │ │ └── WhitelistBlacklistSorter.jsx │ └── index.js ├── permissions.tsx ├── typedefs.js └── types │ ├── Footer.type.js │ ├── Header.type.js │ ├── HeaderRefresher.type.js │ ├── Session.type.js │ └── common.type.js ├── test-helpers └── fixture-loader.ts ├── test ├── components │ ├── Base.test.ts │ └── NotificationBar.test.ts ├── fixtures │ ├── sg │ │ ├── header.html │ │ ├── main-page.html │ │ └── notification-bar.html │ └── st │ │ ├── header.html │ │ ├── main-page.html │ │ └── notification-bar.html └── modules │ └── General │ └── FixedHeader.test.ts ├── tsconfig.json └── webpack.config.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | !.eslintrc.js 4 | !.eslintrc.typed.js 5 | !.lintstagedrc.js 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | **/build 3 | **/dist 4 | **/coverage 5 | **/docs 6 | **/node_modules 7 | **/config.js 8 | **/package-lock.json 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build/* 3 | dist/* 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PRIVACY_POLICY: -------------------------------------------------------------------------------- 1 | ESGST does not exchange any of your information with other services, except for when you want to make a backup of your data to the cloud, in which case the data needs to be uploaded to one of the available services. 2 | 3 | ESGST collects the following personal information for internal use: 4 | 5 | * Your SteamGifts avatar. 6 | * Your SteamTrades avatar. 7 | * Your SteamGifts username. 8 | * Your SteamTrades username. 9 | * Your SteamGifts user id. 10 | * Your Steam id. 11 | 12 | Without the information listed above the add-on does not function correctly. 13 | 14 | Every other information collected by ESGST is information that you yourself generate and that is relevant for the correct functioning of the add-on. For example, if you bookmark a giveaway, that information needs to be stored so that the giveaway appears in your list of bookmarked giveaways. 15 | 16 | ESGST stores the information it collects in two places: 17 | 18 | * Your computer, through the browser.storage API. Most of the information is stored here. Apart from being able to backup/restore this data, you can also delete it at any point, giving you full control over it. 19 | * The localStorage of your browser. Only temporary information is stored here, which consists mostly of caches. For example, if you have the feature Game Categories enabled, a cache containing all of the games that you load are stored here for quicker access in a period of 7 days. 20 | 21 | Every feature offered by ESGST is disabled by default, so you have to opt-in to everything. 22 | 23 | With the add-on installed, a button with the title "ESGST" is added to the header of every SteamGifts and SteamTrades page, making it very transparent that the add-on is installed and enabled. 24 | 25 | ESGST does not set cookies for internal use, but it does manipulate them in two occasions: 26 | 27 | * If you use Firefox containers, you can opt-in to allowing the add-on to manipulate your cookies when making requests. Since external requests made by ESGST happen in the background page and the background page's context is not the same as the content script's context, they use different cookies. With this option enabled, whenever the add-on has to make an external request it temporarily backs up your default cookies, then retrieves the cookies from whatever container you are using, then sets those cookies as default, then makes the request, and then restores your default cookies. 28 | * If you opt-in to using the feature Game Categories, whenever a request is made to a game page in the Steam store (coming from that feature only), the add-on will set the temporary cookies "birthtime=0" and "mature_content=1" to bypass Steam's age verification so that the content can be retrieved correctly. -------------------------------------------------------------------------------- /assets/chrome-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/assets/chrome-badge.png -------------------------------------------------------------------------------- /assets/firefox-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/assets/firefox-badge.png -------------------------------------------------------------------------------- /assets/palemoon-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/assets/palemoon-badge.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /html.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/generateCommit.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | const { exec } = require('child_process'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const { getArguments } = require(path.resolve(__dirname, './common')); 7 | const args = getArguments(process); 8 | 9 | const packageJson = require(path.resolve(__dirname, '../package.json')); 10 | 11 | const octokit = new Octokit({ 12 | auth: args.token, 13 | userAgent: 'ESGST', 14 | }); 15 | 16 | const defaultParams = { 17 | owner: packageJson.author, 18 | repo: packageJson.name, 19 | }; 20 | 21 | function bumpVersion(args) { 22 | let version; 23 | if (args.stable) { 24 | const versionParts = packageJson.version.split('.').map((part) => parseInt(part)); 25 | if (args.major) { 26 | versionParts[0] += 1; 27 | versionParts[1] = 0; 28 | versionParts[2] = 0; 29 | } else if (args.minor) { 30 | versionParts[1] += 1; 31 | versionParts[2] = 0; 32 | } else { 33 | versionParts[2] += 1; 34 | } 35 | version = versionParts.map((part) => part.toString()).join('.'); 36 | } else { 37 | const versionParts = packageJson.betaVersion.split('-'); 38 | if (versionParts.length > 1) { 39 | const betaVersionParts = versionParts[1].split('.'); 40 | betaVersionParts[1] = (parseInt(betaVersionParts[1]) + 1).toString(); 41 | versionParts[1] = betaVersionParts.join('.'); 42 | version = versionParts.join('-'); 43 | } else { 44 | const nextVersion = args.nextVersion || bumpVersion({ ...args, stable: true }); 45 | version = `${nextVersion}-beta.1`; 46 | } 47 | } 48 | return version; 49 | } 50 | 51 | async function generateCommit() { 52 | const version = bumpVersion(args); 53 | if (args.stable) { 54 | packageJson.version = version; 55 | } 56 | packageJson.betaVersion = version; 57 | fs.writeFileSync(path.join(__dirname, '../package.json'), JSON.stringify(packageJson)); 58 | 59 | let commitMessage = ''; 60 | if (args.stable) { 61 | commitMessage = `Bump version to ${version}`; 62 | } else if (args.issue) { 63 | if (args.msg) { 64 | commitMessage = `${args.msg} (#${args.issue})`; 65 | } else { 66 | const issue = await octokit.issues.get({ 67 | ...defaultParams, 68 | issue_number: args.issue, 69 | }); 70 | commitMessage = args.keepOpen 71 | ? `#${args.issue} ${issue.data.title} (WIP)` 72 | : `${issue.data.title} (close #${args.issue})`; 73 | } 74 | } else { 75 | commitMessage = args.msg; 76 | } 77 | 78 | exec(`npx prettier --write "${path.join(__dirname, '../package.json')}"`, (err) => { 79 | if (err) { 80 | console.log(err); 81 | } else { 82 | exec(`git add "${path.join(__dirname, '../package.json')}"`, (err) => { 83 | if (err) { 84 | console.log(err); 85 | } else { 86 | exec(`git commit -m "${commitMessage}"`, (err) => console.log(err)); 87 | } 88 | }); 89 | } 90 | }); 91 | } 92 | 93 | generateCommit(); 94 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/_locales/en/messages.json -------------------------------------------------------------------------------- /src/assets/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/assets/images/icon-128.png -------------------------------------------------------------------------------- /src/assets/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/assets/images/icon-16.png -------------------------------------------------------------------------------- /src/assets/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/assets/images/icon-32.png -------------------------------------------------------------------------------- /src/assets/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/assets/images/icon-64.png -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgomesxyz/esgst/344235dae2a92a53d991333211dd2bf21928a7d2/src/assets/images/icon.png -------------------------------------------------------------------------------- /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/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/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/browser-webext.js: -------------------------------------------------------------------------------- 1 | import { setBrowser } from './browser'; 2 | 3 | setBrowser(browser); 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/class/Logger.jsx: -------------------------------------------------------------------------------- 1 | import { Settings } from './Settings'; 2 | import { Popup } from './Popup'; 3 | import { Shared } from './Shared'; 4 | import { DOM } from './DOM'; 5 | 6 | const INFO = 'info'; 7 | const WARNING = 'warning'; 8 | const ERROR = 'error'; 9 | const PRIORITY = [ERROR, WARNING, INFO]; 10 | 11 | class _Logger { 12 | constructor() { 13 | this.logs = []; 14 | this.button = null; 15 | this.currentMaxLevel = INFO; 16 | } 17 | 18 | info() { 19 | const message = this.getMessage(arguments); 20 | window.console.info(message); 21 | this.logs.push({ level: INFO, message }); 22 | if (Settings.get('notifyLogs')) { 23 | this.addButton(INFO); 24 | } 25 | } 26 | 27 | warning() { 28 | const message = this.getMessage(arguments); 29 | window.console.warn(message); 30 | this.logs.push({ level: WARNING, message }); 31 | if (Settings.get('notifyLogs')) { 32 | this.addButton(WARNING); 33 | } 34 | } 35 | 36 | error() { 37 | const message = this.getMessage(arguments); 38 | window.console.error(message); 39 | this.logs.push({ level: ERROR, message }); 40 | if (Settings.get('notifyLogs')) { 41 | this.addButton(ERROR); 42 | } 43 | } 44 | 45 | getMessage(args) { 46 | return `[ESGST] ${Array.from(args) 47 | .map((x) => (typeof x === 'string' ? x : JSON.stringify(x))) 48 | .join(' ')}`; 49 | } 50 | 51 | addButton(level) { 52 | if (this.button) { 53 | if (PRIORITY.indexOf(level) < PRIORITY.indexOf(this.currentMaxLevel)) { 54 | this.button.nodes.outer.classList.remove(`esgst-logs-${this.currentMaxLevel}`); 55 | this.button.nodes.outer.classList.add(`esgst-logs-${level}`); 56 | this.currentMaxLevel = level; 57 | } 58 | return; 59 | } 60 | 61 | this.button = Shared.header.addButtonContainer({ 62 | buttonIcon: 'fa fa-bug', 63 | buttonName: 'ESGST Logs', 64 | isActive: true, 65 | isNotification: true, 66 | side: 'right', 67 | }); 68 | 69 | this.button.nodes.outer.classList.add('esgst-logs', `esgst-logs-${level}`); 70 | this.button.nodes.buttonIcon.title = Shared.common.getFeatureTooltip('notifyLogs', 'View logs'); 71 | 72 | this.button.nodes.outer.addEventListener('click', this.showPopup.bind(this)); 73 | this.currentMaxLevel = level; 74 | } 75 | 76 | showPopup() { 77 | const popup = new Popup({ 78 | addScrollable: 'left', 79 | icon: 'fa-bug', 80 | isTemp: true, 81 | title: 'Logs', 82 | }); 83 | DOM.insert( 84 | popup.scrollable, 85 | 'beforeend', 86 |
87 | {this.logs.map((x) => ( 88 |
{x.message}
89 | ))} 90 |
91 | ); 92 | popup.open(); 93 | } 94 | } 95 | 96 | const Logger = new _Logger(); 97 | 98 | export { Logger }; 99 | -------------------------------------------------------------------------------- /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/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/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/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/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/class/ToggleSwitch.jsx: -------------------------------------------------------------------------------- 1 | import { Shared } from './Shared'; 2 | import { Settings } from './Settings'; 3 | import { DOM } from './DOM'; 4 | import { Button } from '../components/Button'; 5 | 6 | class ToggleSwitch { 7 | /** 8 | * @param context 9 | * @param id 10 | * @param inline 11 | * @param name 12 | * @param sg 13 | * @param st 14 | * @param tooltip 15 | * @param value 16 | * @property {HTMLElement} input 17 | */ 18 | constructor(context, id, inline, name, sg, st, tooltip, value) { 19 | this.onChange = undefined; 20 | this.onEnabled = null; 21 | this.onDisabled = null; 22 | this.dependencies = []; 23 | this.exclusions = []; 24 | this.id = id; 25 | this.sg = sg; 26 | this.st = st; 27 | this.value = value; 28 | this.container = ( 29 |
30 | 34 | {name} 35 | {tooltip ? : null} 36 |
37 | ); 38 | if (context) { 39 | DOM.insert(context, 'beforeend', this.container); 40 | } 41 | this.switch = this.container.firstElementChild; 42 | this.input = /** @type {HTMLElement} */ this.switch.firstElementChild; 43 | this.name = this.switch.nextElementSibling; 44 | this.input.checked = this.value; 45 | this.input.addEventListener('change', () => this.change()); 46 | } 47 | 48 | async change(settings) { 49 | let setting; 50 | this.value = this.input.checked; 51 | if (this.id) { 52 | let key = this.id; 53 | if (this.sg) { 54 | key += '_sg'; 55 | } else if (this.st) { 56 | key += '_st'; 57 | } 58 | setting = Settings.get(key); 59 | if (typeof setting === 'undefined' || !setting.include) { 60 | setting = this.value; 61 | } else { 62 | setting.enabled = this.value ? 1 : 0; 63 | } 64 | if (!settings) { 65 | let message; 66 | DOM.insert( 67 | this.container, 68 | 'beforeend', 69 |
(message = ref)}> 70 | 71 |
72 | ); 73 | await Shared.common.setSetting(key, setting); 74 | message.classList.add('esgst-green'); 75 | DOM.insert(message, 'atinner', ); 76 | window.setTimeout(() => message.remove(), 2500); 77 | } 78 | } 79 | if (this.value) { 80 | this.dependencies.forEach((dependency) => 81 | dependency instanceof Button 82 | ? dependency.show() 83 | : dependency.classList.remove('esgst-hidden') 84 | ); 85 | this.exclusions.forEach((exclusion) => exclusion.classList.add('esgst-hidden')); 86 | if (!settings && this.onEnabled) { 87 | this.onEnabled(); 88 | } 89 | } else { 90 | this.dependencies.forEach((dependency) => 91 | dependency instanceof Button ? dependency.hide() : dependency.classList.add('esgst-hidden') 92 | ); 93 | this.exclusions.forEach((exclusion) => exclusion.classList.remove('esgst-hidden')); 94 | if (!settings && this.onDisabled) { 95 | this.onDisabled(); 96 | } 97 | } 98 | if (settings) { 99 | return setting; 100 | } 101 | if (this.onChange) { 102 | this.onChange(this.value); 103 | } 104 | } 105 | 106 | enable(settings) { 107 | this.input.checked = true; 108 | // noinspection JSIgnoredPromiseFromCall 109 | return this.change(settings); 110 | } 111 | 112 | disable(settings) { 113 | this.input.checked = false; 114 | // noinspection JSIgnoredPromiseFromCall 115 | return this.change(settings); 116 | } 117 | 118 | toggle(settings) { 119 | this.input.checked = !this.input.checked; 120 | // noinspection JSIgnoredPromiseFromCall 121 | return this.change(settings); 122 | } 123 | } 124 | 125 | export { ToggleSwitch }; 126 | -------------------------------------------------------------------------------- /src/components/Base.tsx: -------------------------------------------------------------------------------- 1 | import { DOM, ExtendedInsertPosition } from '../class/DOM'; 2 | import { ClassNames } from '../constants/ClassNames'; 3 | 4 | export interface BaseData { 5 | isHidden: boolean; 6 | } 7 | 8 | export interface BaseNodes { 9 | outer: HTMLElement | null; 10 | } 11 | 12 | export abstract class Base { 13 | protected _namespace: number; 14 | protected _data: BaseData = { 15 | isHidden: false, 16 | }; 17 | protected _nodes: BaseNodes = { 18 | outer: null, 19 | }; 20 | protected _hasBuilt = false; 21 | 22 | constructor(namespace: number) { 23 | this._namespace = namespace; 24 | } 25 | 26 | static getError(message: string): Error { 27 | return new Error(`${this.name}: ${message}`); 28 | } 29 | 30 | get namespace(): number { 31 | return this._namespace; 32 | } 33 | 34 | get data(): TData { 35 | return this._data as TData; 36 | } 37 | 38 | get nodes(): TNodes { 39 | return this._nodes as TNodes; 40 | } 41 | 42 | get hasBuilt(): boolean { 43 | return this._hasBuilt; 44 | } 45 | 46 | insert = (referenceNode: Element, position: ExtendedInsertPosition): T => { 47 | if (!this._nodes.outer) { 48 | this.build(); 49 | } 50 | if (!this._nodes.outer) { 51 | throw this.getError('failed to insert'); 52 | } 53 | DOM.insert(referenceNode, position, this._nodes.outer); 54 | return (this as unknown) as T; 55 | }; 56 | 57 | destroy = (): T => { 58 | if (!this._nodes.outer) { 59 | throw this.getError('failed to destroy'); 60 | } 61 | this._nodes.outer.remove(); 62 | this._nodes.outer = null; 63 | this._hasBuilt = false; 64 | this.reset(); 65 | return (this as unknown) as T; 66 | }; 67 | 68 | hide = (): T => { 69 | if (!this._nodes.outer) { 70 | throw this.getError('failed to hide'); 71 | } 72 | this._data.isHidden = true; 73 | this._nodes.outer.classList.add(ClassNames[this._namespace].hidden); 74 | return (this as unknown) as T; 75 | }; 76 | 77 | show = (): T => { 78 | if (!this._nodes.outer) { 79 | throw this.getError('failed to show'); 80 | } 81 | this._data.isHidden = false; 82 | this._nodes.outer.classList.remove(ClassNames[this._namespace].hidden); 83 | return (this as unknown) as T; 84 | }; 85 | 86 | toggleHidden = (isHidden: boolean): T => { 87 | if (!this._nodes.outer) { 88 | throw this.getError('failed to show'); 89 | } 90 | this._data.isHidden = isHidden; 91 | this._nodes.outer.classList.toggle(ClassNames[this._namespace].hidden, this._data.isHidden); 92 | return (this as unknown) as T; 93 | }; 94 | 95 | getError = (message: string): Error => { 96 | return new Error(`${this.constructor.name}: ${message}`); 97 | }; 98 | 99 | abstract build(): T; 100 | abstract reset(): T; 101 | abstract parse(referenceEl: Element): T; 102 | } 103 | -------------------------------------------------------------------------------- /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/constants/ClassNames.ts: -------------------------------------------------------------------------------- 1 | import { Namespaces } from './Namespaces'; 2 | 3 | export type Color = 'white' | 'blue' | 'green' | 'yellow' | 'red' | 'gray'; 4 | 5 | export type ButtonColor = Exclude | 'alternate-white'; 6 | 7 | export type NotificationColor = Exclude; 8 | 9 | export const EsgstClassNames = { 10 | button: 'esgst-button', 11 | buttonContainer: 'esgst-button-container', 12 | mmButtonGroup: 'esgst-mm-button-group', 13 | notification: 'esgst-notification-bar', 14 | }; 15 | 16 | export const ClassNames = { 17 | [Namespaces.SG]: { 18 | button: { 19 | root: '', 20 | colors: { 21 | white: 'form__saving-button', 22 | 'alternate-white': 'page__heading__button', 23 | green: 'form__submit-button', 24 | yellow: 'sidebar__entry-delete', 25 | red: 'sidebar__error', 26 | gray: 'form__saving-button form__saving-button--gray', 27 | }, 28 | reversedColors: { 29 | 'form__saving-button': 'white', 30 | page__heading__button: 'alternate-white', 31 | 'form__submit-button': 'green', 32 | 'sidebar__entry-delete': 'yellow', 33 | sidebar__error: 'red', 34 | 'form__saving-button form__saving-button--gray': 'gray', 35 | }, 36 | }, 37 | disabled: 'is-disabled', 38 | giveawayColumns: 'giveaway__columns', 39 | hidden: 'is-hidden', 40 | notification: { 41 | root: 'notification', 42 | marginTop: 'notification--margin-top-small', 43 | colors: { 44 | blue: 'notification--info', 45 | green: 'notification--success', 46 | yellow: 'notification--warning', 47 | red: 'notification--danger', 48 | gray: 'notification--default', 49 | }, 50 | reversedColors: { 51 | 'notification--info': 'blue', 52 | 'notification--success': 'green', 53 | 'notification--warning': 'yellow', 54 | 'notification--danger': 'red', 55 | 'notification--default': 'gray', 56 | }, 57 | }, 58 | pageHeading: 'page__heading', 59 | pageHeadingBreadcrumbs: 'page__heading__breadcrumbs', 60 | pageHeadingButton: 'page__heading__button', 61 | selected: 'is-selected', 62 | }, 63 | [Namespaces.ST]: { 64 | button: { 65 | root: 'btn_action', 66 | colors: { 67 | white: 'white', 68 | 'alternate-white': 'page_heading_btn', 69 | green: 'green', 70 | yellow: 'yellow', 71 | red: 'red', 72 | gray: 'grey', 73 | }, 74 | reversedColors: { 75 | white: 'white', 76 | page_heading_btn: 'alternate-white', 77 | green: 'green', 78 | yellow: 'yellow', 79 | red: 'red', 80 | grey: 'gray', 81 | }, 82 | }, 83 | disabled: 'is_disabled', 84 | hidden: 'is_hidden', 85 | notification: { 86 | root: 'notification', 87 | marginTop: '', 88 | colors: { 89 | blue: 'blue', 90 | green: 'green', 91 | yellow: 'yellow', 92 | red: 'red', 93 | gray: 'gray', 94 | }, 95 | reversedColors: { 96 | blue: 'blue', 97 | green: 'green', 98 | yellow: 'yellow', 99 | red: 'red', 100 | gray: 'gray', 101 | }, 102 | }, 103 | pageHeading: 'page_heading', 104 | pageHeadingBreadcrumbs: 'page_heading_breadcrumbs', 105 | pageHeadingButton: 'page_heading_btn', 106 | selected: 'is_selected', 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/constants/Namespaces.js: -------------------------------------------------------------------------------- 1 | const Namespaces = { 2 | SG: 0, 3 | ST: 1, 4 | }; 5 | 6 | export { Namespaces }; 7 | -------------------------------------------------------------------------------- /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/entry/eventPage_index.js: -------------------------------------------------------------------------------- 1 | import '../eventPage'; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/entry/eventPage_sdk_index.js: -------------------------------------------------------------------------------- 1 | import '../eventPage_sdk'; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/entry/index.js: -------------------------------------------------------------------------------- 1 | import '../dependencies'; 2 | import '../browser-webext'; 3 | import '../main'; 4 | -------------------------------------------------------------------------------- /src/entry/monkey_banner.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | // ==UserScript== 3 | // @name Enhanced SteamGifts & SteamTrades (ESGST) 4 | // @namespace https://rafaelgomesxyz.github.io/esgst 5 | // @description Enhances SteamGifts and SteamTrades by adding some cool features to them. 6 | // @icon https://github.com/rafaelgomesxyz/esgst/raw/main/src/assets/images/icon.png 7 | // @version <% package.version %> 8 | // @author <% package.author %> 9 | // @contributor Revadike 10 | // @updateURL https://github.com/rafaelgomesxyz/esgst/releases/latest/download/userscript.meta.js 11 | // @downloadURL https://github.com/rafaelgomesxyz/esgst/releases/latest/download/userscript.user.js 12 | // @match https://www.steamgifts.com/* 13 | // @match https://www.steamtrades.com/* 14 | // @connect steamtrades.com 15 | // @connect steamgifts.com 16 | // @connect sgtools.info 17 | // @connect api.dropboxapi.com 18 | // @connect api.imgur.com 19 | // @connect api.steampowered.com 20 | // @connect content.dropboxapi.com 21 | // @connect files.1drv.com 22 | // @connect github.com 23 | // @connect googleapis.com 24 | // @connect graph.microsoft.com 25 | // @connect isthereanydeal.com 26 | // @connect esgst.rafaelgomes.xyz 27 | // @connect raw.githubusercontent.com 28 | // @connect script.google.com 29 | // @connect script.googleusercontent.com 30 | // @connect steam-tracker.com 31 | // @connect steamcommunity.com 32 | // @connect store.steampowered.com 33 | // @connect userstyles.org 34 | // @grant GM_addValueChangeListener 35 | // @grant GM_deleteValue 36 | // @grant GM_getValue 37 | // @grant GM_info 38 | // @grant GM_listValues 39 | // @grant GM_setValue 40 | // @grant GM_xmlhttpRequest 41 | // @grant GM_openInTab 42 | // @grant GM_getResourceURL 43 | // @grant GM.addValueChangeListener 44 | // @grant GM.deleteValue 45 | // @grant GM.getValue 46 | // @grant GM.info 47 | // @grant GM.listValues 48 | // @grant GM.setValue 49 | // @grant GM.xmlHttpRequest 50 | // @grant GM.openInTab 51 | // @grant GM.getResourceUrl 52 | // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js 53 | // @require https://cdn.jsdelivr.net/npm/interactjs@1.3.4/dist/interact.min.js 54 | // @require https://cdn.jsdelivr.net/npm/jquery@3.5.0/dist/jquery.min.js 55 | // @require https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder@2.5.2/dist/js/query-builder.standalone.min.js 56 | // @require https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js 57 | // @require https://cdn.jsdelivr.net/npm/jquery-ui-dist@1.12.1/jquery-ui.min.js 58 | // @require https://cdn.jsdelivr.net/npm/jszip@3.2.2/dist/jszip.min.js 59 | // @require https://cdn.jsdelivr.net/gh/rossengeorgiev/vdf-parser@0d210ec51a2be4d6186777addf8f98df59f9eb53/vdf.js 60 | // @require https://cdn.jsdelivr.net/npm/emojis-utils@1.0.2/dist/emojis-utils.min.js 61 | // @resource awesome-bootstrap-checkbox https://cdn.jsdelivr.net/npm/awesome-bootstrap-checkbox@0.3.7/awesome-bootstrap-checkbox.css 62 | // @resource jquery-query-builder https://cdn.jsdelivr.net/npm/jQuery-QueryBuilder@2.5.2/dist/css/query-builder.default.min.css 63 | // @run-at document-start 64 | // @noframes 65 | // ==/UserScript== 66 | */ 67 | -------------------------------------------------------------------------------- /src/entry/permissions_index.js: -------------------------------------------------------------------------------- 1 | import '../browser-webext'; 2 | import '../permissions'; 3 | -------------------------------------------------------------------------------- /src/entry/sdk_index.js: -------------------------------------------------------------------------------- 1 | import '../dependencies'; 2 | import '../browser-sdk'; 3 | import '../main'; 4 | -------------------------------------------------------------------------------- /src/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/html/permissions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ESGST Permissions 7 | 8 | 9 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/models/Base.tsx: -------------------------------------------------------------------------------- 1 | import { DOM, ExtendedInsertPosition } from '../class/DOM'; 2 | import { ClassNames } from '../constants/ClassNames'; 3 | 4 | export interface BaseNodes { 5 | outer: HTMLElement | null; 6 | } 7 | 8 | export interface BaseData { 9 | hidden: boolean; 10 | } 11 | 12 | export abstract class Base { 13 | namespace: number; 14 | nodes = Base.getDefaultNodes() as TNodes; 15 | data = Base.getDefaultData() as TData; 16 | built = false; 17 | 18 | constructor(namespace: number) { 19 | this.namespace = namespace; 20 | } 21 | 22 | static getError(message: string): Error { 23 | return new Error(`${this.name}: ${message}`); 24 | } 25 | 26 | static getDefaultNodes(): BaseNodes { 27 | return { 28 | outer: null, 29 | }; 30 | } 31 | 32 | static getDefaultData(): BaseData { 33 | return { 34 | hidden: false, 35 | }; 36 | } 37 | 38 | getError = (message: string): Error => { 39 | return new Error(`${this.constructor.name}: ${message}`); 40 | }; 41 | 42 | insert = (refNode: HTMLElement, position: ExtendedInsertPosition): T => { 43 | if (!this.nodes.outer) { 44 | this.build(); 45 | } 46 | 47 | if (!this.nodes.outer) { 48 | throw this.getError('failed to insert'); 49 | } 50 | 51 | DOM.insert(refNode, position, this.nodes.outer); 52 | 53 | return (this as unknown) as T; 54 | }; 55 | 56 | destroy = (): T => { 57 | if (!this.nodes.outer) { 58 | throw this.getError('failed to destroy'); 59 | } 60 | 61 | this.nodes.outer.remove(); 62 | this.nodes.outer = null; 63 | this.built = false; 64 | this.reset(); 65 | 66 | return (this as unknown) as T; 67 | }; 68 | 69 | hide = (): T => { 70 | if (!this.nodes.outer) { 71 | throw this.getError('failed to hide'); 72 | } 73 | 74 | this.nodes.outer.classList.add(ClassNames[this.namespace].hidden); 75 | this.data.hidden = true; 76 | 77 | return (this as unknown) as T; 78 | }; 79 | 80 | show = (): T => { 81 | if (!this.nodes.outer) { 82 | throw this.getError('failed to show'); 83 | } 84 | 85 | this.nodes.outer.classList.remove(ClassNames[this.namespace].hidden); 86 | this.data.hidden = false; 87 | 88 | return (this as unknown) as T; 89 | }; 90 | 91 | toggleHidden = (hidden: boolean): T => { 92 | if (!this.nodes.outer) { 93 | throw this.getError('failed to show'); 94 | } 95 | 96 | this.nodes.outer.classList.toggle(ClassNames[this.namespace].hidden, this.data.hidden); 97 | this.data.hidden = hidden; 98 | 99 | return (this as unknown) as T; 100 | }; 101 | 102 | abstract parse(refNode: HTMLElement): T; 103 | abstract parseNodes(refNode: HTMLElement): T; 104 | abstract parseData(): T; 105 | abstract parseExtraData(): T; 106 | abstract build(): T; 107 | abstract reset(): T; 108 | } 109 | -------------------------------------------------------------------------------- /src/models/CommentEntity.tsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../class/DOM'; 2 | import { Session } from '../class/Session'; 3 | import { Namespaces } from '../constants/Namespaces'; 4 | import { Comment } from './Comment'; 5 | 6 | abstract class CommentEntity implements ICommentEntity { 7 | nodes: ICommentEntityNodes; 8 | data: ICommentEntityData; 9 | comments: IComment[]; 10 | 11 | constructor() { 12 | this.nodes = CommentEntity.getDefaultNodes(); 13 | this.data = CommentEntity.getDefaultData(); 14 | this.comments = []; 15 | } 16 | 17 | static getDefaultNodes(): ICommentEntityNodes { 18 | return { 19 | outer: null, 20 | title: null, 21 | description: null, 22 | comments: null, 23 | }; 24 | } 25 | 26 | static getDefaultData(): ICommentEntityData { 27 | return { 28 | url: '', 29 | title: '', 30 | description: '', 31 | }; 32 | } 33 | 34 | static create(): ICommentEntity { 35 | switch (Session.namespace) { 36 | case Namespaces.SG: { 37 | return new SgCommentEntity(); 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | static parseAll(context: HTMLElement): ICommentEntity[] { 44 | switch (Session.namespace) { 45 | case Namespaces.SG: { 46 | return SgCommentEntity.parseAll(context); 47 | } 48 | } 49 | return null; 50 | } 51 | 52 | abstract parse(outer: HTMLDivElement): void; 53 | abstract parseNodes(outer: HTMLDivElement): void; 54 | abstract parseData(): void; 55 | abstract build(context: HTMLElement, position: string): void; 56 | } 57 | 58 | class SgCommentEntity extends CommentEntity { 59 | constructor() { 60 | super(); 61 | } 62 | 63 | static parseAll(context: HTMLElement): SgCommentEntity[] { 64 | const entities: SgCommentEntity[] = []; 65 | const elements = context.querySelectorAll( 66 | '.comments > .comments__entity:not([data-esgst-parsed]), :scope > .comments__entity:not([data-esgst-parsed])' 67 | ); 68 | for (const element of elements) { 69 | const entity = new SgCommentEntity(); 70 | entity.parse(element as HTMLDivElement); 71 | entities.push(entity); 72 | } 73 | return entities; 74 | } 75 | 76 | parse(outer: HTMLDivElement): void { 77 | this.parseNodes(outer); 78 | this.parseData(); 79 | this.comments = Comment.parseAll(this.nodes.comments); 80 | } 81 | 82 | parseNodes(outer: HTMLDivElement): void { 83 | const nodes = CommentEntity.getDefaultNodes(); 84 | nodes.outer = outer; 85 | nodes.title = nodes.outer.querySelector('.comments__entity__name a'); 86 | nodes.description = nodes.outer.querySelector('.comments__entity__description'); 87 | nodes.comments = nodes.outer.nextElementSibling as HTMLDivElement; 88 | nodes.outer.dataset.esgstParsed = ''; 89 | this.nodes = nodes; 90 | } 91 | 92 | parseData(): void { 93 | const nodes = this.nodes; 94 | const data = CommentEntity.getDefaultData(); 95 | data.url = nodes.title.getAttribute('href'); 96 | data.title = nodes.title.textContent.trim(); 97 | data.description = nodes.description.innerHTML; 98 | this.data = data; 99 | } 100 | 101 | build(context: HTMLElement, position: string): void { 102 | if (this.nodes.outer) { 103 | this.nodes.outer.remove(); 104 | } 105 | let outer: HTMLDivElement | undefined; 106 | DOM.insert( 107 | context, 108 | position, 109 |
(outer = ref)}> 110 |

111 | {this.data.title} 112 |

113 |
114 | {this.data.description} 115 |
116 |
117 | ); 118 | DOM.insert(outer, 'afterend',
); 119 | this.parseNodes(outer); 120 | for (const comment of this.comments) { 121 | comment.build(this.nodes.comments, 'beforeend'); 122 | } 123 | } 124 | } 125 | 126 | export { CommentEntity }; 127 | -------------------------------------------------------------------------------- /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 |
    11 |
  • 12 | Reverses the comments of any{' '} 13 | discussion page so that they 14 | are ordered from newest to oldest. 15 |
  • 16 |
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/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', 49 | ); 50 | Button.create([ 51 | { 52 | template: 'success', 53 | name: 'Save', 54 | onClick: async () => { 55 | await Shared.common.saveComment( 56 | null, 57 | Shared.esgst.sg ? '' : document.querySelector(`[name="trade_code"]`).value, 58 | '', 59 | popup.textArea.value, 60 | Shared.esgst.sg ? Shared.esgst.locationHref.match(/(.+?)(#.+?)?$/)[1] : '/ajax.php', 61 | popup.progressBar, 62 | true 63 | ); 64 | }, 65 | }, 66 | { 67 | template: 'loading', 68 | isDisabled: true, 69 | name: 'Saving...', 70 | }, 71 | ]).insert(popup.description, 'beforeend'); 72 | button.addEventListener( 73 | 'click', 74 | popup.open.bind(popup, popup.textArea.focus.bind(popup.textArea)) 75 | ); 76 | } 77 | } 78 | 79 | const commentsReplyBoxPopup = new CommentsReplyBoxPopup(); 80 | 81 | export { commentsReplyBoxPopup }; 82 | -------------------------------------------------------------------------------- /src/modules/Comments/ReplyMentionLink.tsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class CommentsReplyMentionLink extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Adds a link (@user) next to a reply's "Permalink" (in any page) that mentions the user 12 | being replied to and links to their comment. 13 |
  • 14 |
  • 15 | This feature is useful for conversations that have very deep nesting levels, which makes 16 | it impossible to know who replied to whom. 17 |
  • 18 |
19 | ), 20 | id: 'rml', 21 | name: 'Reply Mention Link', 22 | sg: true, 23 | st: true, 24 | type: 'comments', 25 | featureMap: { 26 | commentV2: this.addLinks.bind(this), 27 | }, 28 | }; 29 | } 30 | 31 | addLinks(comments: IComment[]) { 32 | for (const comment of comments) { 33 | this.addLink(comment); 34 | this.addLinks(comment.children); 35 | } 36 | } 37 | 38 | addLink(comment: IComment) { 39 | if (comment.parent && !comment.nodes.rmlLink) { 40 | DOM.insert( 41 | comment.nodes.actions, 42 | 'beforeend', 43 | (comment.nodes.rmlLink = ref)} 47 | > 48 | {`@${comment.data.isDeleted ? '[Deleted]' : comment.parent.author.data.username}`} 49 | 50 | ); 51 | } 52 | } 53 | 54 | rml_addLink(parent: HTMLElement, children: HTMLElement[]) { 55 | const authorUsername = parent 56 | .querySelector('.comment__username, .author_name') 57 | .textContent.trim(); 58 | const commentCode = parent.id; 59 | for (const child of children) { 60 | const actions = child.querySelector('.comment__actions, .action_list'); 61 | const rmlLink = actions.querySelector('.esgst-rml-link'); 62 | if (rmlLink) { 63 | rmlLink.textContent = `@${authorUsername}`; 64 | } else { 65 | DOM.insert( 66 | actions, 67 | 'beforeend', 68 | 69 | {`@${authorUsername}`} 70 | 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | 77 | const commentsReplyMentionLink = new CommentsReplyMentionLink(); 78 | 79 | export { commentsReplyMentionLink }; 80 | -------------------------------------------------------------------------------- /src/modules/DiscussionPanels.js: -------------------------------------------------------------------------------- 1 | import { Module } from '../class/Module'; 2 | import { Settings } from '../class/Settings'; 3 | 4 | class DiscussionPanels extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | endless: true, 9 | id: 'discussionPanels', 10 | }; 11 | } 12 | 13 | init() { 14 | if ( 15 | (Settings.get('ct') && (this.esgst.giveawaysPath || this.esgst.discussionsPath)) || 16 | (Settings.get('gdttt') && 17 | (this.esgst.giveawaysPath || 18 | this.esgst.discussionsPath || 19 | this.esgst.discussionsTicketsTradesPath)) 20 | ) { 21 | this.esgst.endlessFeatures.push( 22 | this.esgst.modules.commentsCommentTracker.ct_addDiscussionPanels.bind( 23 | this.esgst.modules.commentsCommentTracker 24 | ) 25 | ); 26 | } 27 | } 28 | } 29 | 30 | const discussionPanelsModule = new DiscussionPanels(); 31 | 32 | export { discussionPanelsModule }; 33 | -------------------------------------------------------------------------------- /src/modules/Discussions/CloseOpenDiscussionButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from '../../class/Button'; 2 | import { DOM } from '../../class/DOM'; 3 | import { FetchRequest } from '../../class/FetchRequest'; 4 | import { Module } from '../../class/Module'; 5 | import { Session } from '../../class/Session'; 6 | import { Settings } from '../../class/Settings'; 7 | 8 | class DiscussionsCloseOpenDiscussionButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button ( if the discussion is open and{' '} 16 | if it is closed) next to the title of a 17 | discussion created by yourself (in any{' '} 18 | discussions page) that allows you 19 | to close/open the discussion without having to access it. 20 |
  • 21 |
22 | ), 23 | id: 'codb', 24 | name: 'Close/Open Discussion Button', 25 | sg: true, 26 | type: 'discussions', 27 | featureMap: { 28 | discussion: this.codb_addButtons.bind(this), 29 | }, 30 | }; 31 | } 32 | 33 | codb_addButtons(discussions) { 34 | for (const discussion of discussions) { 35 | if ( 36 | discussion.author === Settings.get('username') && 37 | !discussion.heading.parentElement.getElementsByClassName('esgst-codb-button')[0] 38 | ) { 39 | if (discussion.closed) { 40 | discussion.closed.remove(); 41 | discussion.closed = true; 42 | } 43 | new Button(discussion.headingContainer.firstElementChild, 'beforebegin', { 44 | callbacks: [ 45 | this.codb_close.bind(this, discussion), 46 | null, 47 | this.codb_open.bind(this, discussion), 48 | null, 49 | ], 50 | className: 'esgst-codb-button', 51 | icons: [ 52 | 'fa-lock esgst-clickable', 53 | 'fa-circle-o-notch fa-spin', 54 | 'fa-lock esgst-clickable esgst-red', 55 | 'fa-circle-o-notch fa-spin', 56 | ], 57 | id: 'codb', 58 | index: discussion.closed ? 2 : 0, 59 | titles: [ 60 | 'Close discussion', 61 | 'Closing discussion...', 62 | 'Open discussion', 63 | 'Opening discussion...', 64 | ], 65 | }); 66 | } 67 | } 68 | } 69 | 70 | async codb_close(discussion) { 71 | let response = await FetchRequest.post(discussion.url, { 72 | data: `xsrf_token=${Session.xsrfToken}&do=close_discussion`, 73 | }); 74 | if (response.html.getElementsByClassName('page__heading__button--red')[0]) { 75 | discussion.closed = true; 76 | discussion.innerWrap.classList.add('is-faded'); 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | async codb_open(discussion) { 83 | let response = await FetchRequest.post(discussion.url, { 84 | data: `xsrf_token=${Session.xsrfToken}&do=reopen_discussion`, 85 | }); 86 | if (!response.html.getElementsByClassName('page__heading__button--red')[0]) { 87 | discussion.closed = false; 88 | discussion.innerWrap.classList.remove('is-faded'); 89 | return true; 90 | } 91 | return false; 92 | } 93 | } 94 | 95 | const discussionsCloseOpenDiscussionButton = new DiscussionsCloseOpenDiscussionButton(); 96 | 97 | export { discussionsCloseOpenDiscussionButton }; 98 | -------------------------------------------------------------------------------- /src/modules/Discussions/DiscussionTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class DiscussionsDiscussionTags extends Tags { 5 | constructor() { 6 | super('dt'); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Adds a button ( ) next a discussion's title (in any page) 12 | that allows you to save tags for the discussion (only visible to you). 13 |
  • 14 |
  • You can press Enter to save the tags.
  • 15 |
  • Each tag can be colored individually.
  • 16 |
  • 17 | There is a button ( ) in the tags popup that allows you to 18 | view a list with all of the tags that you have used ordered from most used to least 19 | used. 20 |
  • 21 |
  • 22 | Adds a button ( ) to the 23 | page heading of this menu that allows you to manage all of the tags that have been 24 | saved. 25 |
  • 26 |
27 | ), 28 | features: { 29 | dt_s: { 30 | name: 'Show tag suggestions while typing.', 31 | sg: true, 32 | }, 33 | }, 34 | id: 'dt', 35 | name: 'Discussion Tags', 36 | sg: true, 37 | type: 'discussions', 38 | }; 39 | } 40 | 41 | init() { 42 | this.esgst.discussionFeatures.push(this.tags_addButtons.bind(this)); 43 | // noinspection JSIgnoredPromiseFromCall 44 | this.tags_getTags(); 45 | } 46 | } 47 | 48 | const discussionsDiscussionTags = new DiscussionsDiscussionTags(); 49 | 50 | export { discussionsDiscussionTags }; 51 | -------------------------------------------------------------------------------- /src/modules/Discussions/DiscussionsSorter.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { Module } from '../../class/Module'; 3 | import { Popout } from '../../class/Popout'; 4 | import { Scope } from '../../class/Scope'; 5 | import { Settings } from '../../class/Settings'; 6 | import { ToggleSwitch } from '../../class/ToggleSwitch'; 7 | import { Button } from '../../components/Button'; 8 | import { common } from '../Common'; 9 | 10 | const createHeadingButton = common.createHeadingButton.bind(common), 11 | saveAndSortContent = common.saveAndSortContent.bind(common); 12 | class DiscussionsDiscussionsSorter extends Module { 13 | constructor() { 14 | super(); 15 | this.info = { 16 | description: () => ( 17 |
    18 |
  • 19 | Adds a button ( ) to the main page heading of any{' '} 20 | discussions page that allows you to 21 | sort the discussions in the page by title, category, created time, author and number of 22 | comments. 23 |
  • 24 |
  • 25 | There is also an option to automatically sort the discussions so that every time you 26 | open the page the discussions are already sorted by whatever option you prefer. 27 |
  • 28 |
29 | ), 30 | id: 'ds', 31 | name: 'Discussions Sorter', 32 | sg: true, 33 | type: 'discussions', 34 | }; 35 | } 36 | 37 | init() { 38 | if (!this.esgst.discussionsPath) return; 39 | 40 | let object = { 41 | button: createHeadingButton({ id: 'ds', icons: ['fa-sort'], title: 'Sort discussions' }), 42 | }; 43 | object.button.addEventListener('click', this.ds_openPopout.bind(this, object)); 44 | } 45 | 46 | ds_openPopout(obj) { 47 | if (obj.popout) return; 48 | obj.popout = new Popout('esgst-ds-popout', obj.button, 0, true); 49 | new ToggleSwitch( 50 | obj.popout.popout, 51 | 'ds_auto', 52 | false, 53 | 'Auto Sort', 54 | false, 55 | false, 56 | 'Automatically sorts the discussions by the selected option when loading the page.', 57 | Settings.get('ds_auto') 58 | ); 59 | let options; 60 | DOM.insert( 61 | obj.popout.popout, 62 | 'beforeend', 63 | 76 | ); 77 | options.value = Settings.get('ds_option'); 78 | let callback = saveAndSortContent.bind( 79 | common, 80 | Scope.findData('main', 'discussions'), 81 | 'ds_option', 82 | options 83 | ); 84 | options.addEventListener('change', callback); 85 | Button.create({ 86 | color: 'green', 87 | icons: ['fa-arrow-circle-right'], 88 | name: 'Sort', 89 | onClick: callback, 90 | }).insert(obj.popout.popout, 'beforeend'); 91 | obj.popout.open(); 92 | } 93 | } 94 | 95 | const discussionsDiscussionsSorter = new DiscussionsDiscussionsSorter(); 96 | 97 | export { discussionsDiscussionsSorter }; 98 | -------------------------------------------------------------------------------- /src/modules/Discussions/MainPostPopup.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { Module } from '../../class/Module'; 3 | import { Popup } from '../../class/Popup'; 4 | import { Settings } from '../../class/Settings'; 5 | import { Shared } from '../../class/Shared'; 6 | import { common } from '../Common'; 7 | 8 | const createHeadingButton = common.createHeadingButton.bind(common); 9 | class DiscussionsMainPostPopup extends Module { 10 | constructor() { 11 | super(); 12 | this.info = { 13 | description: () => ( 14 |
    15 |
  • 16 | Hides the main post of a discussion and adds a button () 17 | to its main page heading that allows you to open the main post through a popup. 18 |
  • 19 |
  • 20 | This feature is useful if you have enabled, 21 | which allows you to view the main post of a discussion from any scrolling position. 22 |
  • 23 |
24 | ), 25 | features: { 26 | mpp_r: { 27 | dependencies: ['ct'], 28 | name: 'Only hide the main post if it has been marked as read.', 29 | sg: true, 30 | }, 31 | }, 32 | id: 'mpp', 33 | name: 'Main Post Popup', 34 | sg: true, 35 | type: 'discussions', 36 | }; 37 | } 38 | 39 | init() { 40 | if (!this.esgst.discussionPath) { 41 | return; 42 | } 43 | let button = createHeadingButton({ 44 | id: 'mpp', 45 | icons: ['fa-home'], 46 | title: 'Open the main post', 47 | }); 48 | let MPPPost = document.createElement('div'); 49 | MPPPost.className = 'page__outer-wrap'; 50 | let Sibling; 51 | do { 52 | Sibling = this.esgst.mainPageHeading.previousElementSibling; 53 | if (Sibling) { 54 | MPPPost.insertBefore(Sibling, MPPPost.firstElementChild); 55 | } 56 | } while (Sibling); 57 | this.esgst.mainPageHeading.parentElement.insertBefore(MPPPost, this.esgst.mainPageHeading); 58 | let Hidden; 59 | if (Settings.get('mpp_r')) { 60 | let discussion = JSON.parse(Shared.common.getValue('discussions', '{}'))[ 61 | window.location.pathname.match(/^\/discussion\/(.+?)\//)[1] 62 | ]; 63 | if (discussion) { 64 | if (discussion.readComments && discussion.readComments['']) { 65 | Hidden = true; 66 | window.scrollTo(0, 0); 67 | } else { 68 | Hidden = false; 69 | } 70 | } else { 71 | Hidden = false; 72 | } 73 | } else { 74 | Hidden = true; 75 | window.scrollTo(0, 0); 76 | } 77 | MPPPost.classList.add(Hidden ? 'esgst-mpp-hidden' : 'esgst-mpp-visible', 'esgst-text-left'); 78 | button.addEventListener('click', () => { 79 | if (!Hidden) { 80 | MPPPost.classList.remove('esgst-mpp-visible'); 81 | MPPPost.classList.add('esgst-mpp-hidden'); 82 | } 83 | let popup = new Popup({ icon: '', title: '', popup: MPPPost }); 84 | MPPPost.classList.add('esgst-mpp-popup'); 85 | popup.open(); 86 | popup.onClose = () => { 87 | MPPPost.classList.remove('esgst-mpp-popup'); 88 | if (!Hidden) { 89 | MPPPost.classList.remove('esgst-mpp-hidden'); 90 | MPPPost.classList.add('esgst-mpp-visible'); 91 | MPPPost.removeAttribute('style'); 92 | this.esgst.mainPageHeading.parentElement.insertBefore( 93 | MPPPost, 94 | this.esgst.mainPageHeading 95 | ); 96 | } 97 | }; 98 | }); 99 | } 100 | } 101 | 102 | const discussionsMainPostPopup = new DiscussionsMainPostPopup(); 103 | 104 | export { discussionsMainPostPopup }; 105 | -------------------------------------------------------------------------------- /src/modules/Discussions/MainPostSkipper.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const goToComment = common.goToComment.bind(common); 6 | class DiscussionsMainPostSkipper extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Skips to the comments of a discussion if you have used the pagination navigation. For 14 | example, if you enter a discussion and use the pagination navigation to go to page 2, on 15 | page 2 the feature will skip the main post and take you directly to the comments. 16 |
  • 17 |
18 | ), 19 | id: 'mps', 20 | name: 'Main Post Skipper', 21 | sg: true, 22 | type: 'discussions', 23 | }; 24 | } 25 | 26 | init() { 27 | if ( 28 | !window.location.hash && 29 | this.esgst.discussionPath && 30 | this.esgst.pagination && 31 | document.referrer.match( 32 | new RegExp(`/discussion/${[window.location.pathname.match(/^\/discussion\/(.+?)\//)[1]]}/`) 33 | ) 34 | ) { 35 | const context = this.esgst.pagination.previousElementSibling; 36 | if (context.classList.contains('comments')) { 37 | goToComment('', context.firstElementChild.firstElementChild, true); 38 | } 39 | } 40 | } 41 | } 42 | 43 | const discussionsMainPostSkipper = new DiscussionsMainPostSkipper(); 44 | 45 | export { discussionsMainPostSkipper }; 46 | -------------------------------------------------------------------------------- /src/modules/Discussions/RefreshActiveDiscussionsButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const checkMissingDiscussions = common.checkMissingDiscussions.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class DiscussionsRefreshActiveDiscussionsButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button () to the page heading of the active 16 | discussions (in the main page) that allows you to refresh the active discussions without 17 | having to refresh the entire page. 18 |
  • 19 |
20 | ), 21 | id: 'radb', 22 | name: 'Refresh Active Discussions Button', 23 | sg: true, 24 | type: 'discussions', 25 | }; 26 | } 27 | 28 | radb_addButtons() { 29 | let elements, i; 30 | elements = this.esgst.activeDiscussions.querySelectorAll( 31 | `.block_header, .esgst-heading-button` 32 | ); 33 | for (i = elements.length - 1; i > -1; --i) { 34 | DOM.insert( 35 | elements[i], 36 | 'beforebegin', 37 |
{ 41 | let icon = event.currentTarget.firstElementChild; 42 | icon.classList.add('fa-spin'); 43 | if (Settings.get('oadd')) { 44 | // noinspection JSIgnoredPromiseFromCall 45 | this.esgst.modules.discussionsOldActiveDiscussionsDesign.oadd_load(true, () => { 46 | icon.classList.remove('fa-spin'); 47 | }); 48 | } else { 49 | checkMissingDiscussions(true, () => { 50 | icon.classList.remove('fa-spin'); 51 | }); 52 | } 53 | }} 54 | > 55 | 56 |
57 | ); 58 | } 59 | } 60 | } 61 | 62 | const discussionsRefreshActiveDiscussionsButton = new DiscussionsRefreshActiveDiscussionsButton(); 63 | 64 | export { discussionsRefreshActiveDiscussionsButton }; 65 | -------------------------------------------------------------------------------- /src/modules/Discussions/ReversedActiveDiscussions.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { Shared } from '../../class/Shared'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class DiscussionsReversedActiveDiscussions extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Reverses the active discussions (in the main page) so that discussions come before deals 14 | (original order). 15 |
  • 16 |
17 | ), 18 | id: 'rad', 19 | name: 'Reversed Active Discussions', 20 | sg: true, 21 | sgPaths: /^Browse\sGiveaways/, 22 | type: 'discussions', 23 | }; 24 | } 25 | 26 | async init() { 27 | if (!Shared.esgst.giveawaysPath || !Shared.esgst.activeDiscussions || Settings.get('oadd')) { 28 | return; 29 | } 30 | Shared.esgst.activeDiscussions.insertBefore( 31 | Shared.esgst.activeDiscussions.lastElementChild, 32 | Shared.esgst.activeDiscussions.firstElementChild 33 | ); 34 | } 35 | } 36 | 37 | const discussionsReversedActiveDiscussions = new DiscussionsReversedActiveDiscussions(); 38 | 39 | export { discussionsReversedActiveDiscussions }; 40 | -------------------------------------------------------------------------------- /src/modules/Games/EnteredGameHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class GamesEnteredGameHighlighter extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Adds an icon () next to a game's name (in any page) to 14 | indicate that you have entered giveaways for the game in the past. Clicking on the icon 15 | unhighlights the game. 16 |
  • 17 |
  • 18 | A game is only highlighted if you entered a giveaway for it after this feature was 19 | enabled. 20 |
  • 21 |
22 | ), 23 | id: 'egh', 24 | name: 'Entered Game Highlighter', 25 | sg: true, 26 | type: 'games', 27 | featureMap: { 28 | game: this.egh_getGames.bind(this), 29 | }, 30 | features: { 31 | egh_c: { 32 | name: 'Show a counter with the number of giveaways that have been entered for the game.', 33 | sg: true, 34 | }, 35 | }, 36 | }; 37 | } 38 | 39 | egh_getGames(games) { 40 | for (const game of games.all) { 41 | if (Shared.esgst.giveawayPath) { 42 | const button = document.querySelector('.sidebar__entry-insert'); 43 | if (button) { 44 | button.addEventListener('click', this.egh_saveGame.bind(this, game.id, game.type)); 45 | } 46 | } 47 | const savedGame = Shared.esgst.games[game.type][game.id]; 48 | if (savedGame && savedGame.entered && !game.container.querySelector('.esgst-egh-button')) { 49 | const count = Number(savedGame.entered); 50 | DOM.insert( 51 | (game.container.closest('.poll') && 52 | game.container.querySelector('.table__column__heading')) || 53 | game.headingName, 54 | 'beforebegin', 55 | 64 | 65 | {Settings.get('egh_c') ? ` ${count}` : null} 66 | 67 | ); 68 | } 69 | } 70 | } 71 | 72 | async egh_saveGame(id, type) { 73 | if (!id || !type) { 74 | return; 75 | } 76 | let game = Shared.esgst.games[type][id]; 77 | if (!game) { 78 | game = {}; 79 | } 80 | if (!game.entered) { 81 | game.entered = 0; 82 | } 83 | game.entered += 1; 84 | await Shared.common.lockAndSaveGames({ [type]: { [id]: game } }); 85 | } 86 | 87 | async egh_unhighlightGame(id, type, event) { 88 | const icon = event.currentTarget; 89 | if (icon.classList.contains('fa-spin')) { 90 | return; 91 | } 92 | DOM.insert(icon, 'atinner', ); 93 | let game = Shared.esgst.games[type][id]; 94 | if (game && game.entered) { 95 | game.entered = null; 96 | await Shared.common.lockAndSaveGames({ [type]: { [id]: game } }); 97 | } 98 | icon.remove(); 99 | } 100 | } 101 | 102 | const gamesEnteredGameHighlighter = new GamesEnteredGameHighlighter(); 103 | 104 | export { gamesEnteredGameHighlighter }; 105 | -------------------------------------------------------------------------------- /src/modules/Games/GameTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GamesGameTags extends Tags { 6 | constructor() { 7 | super('gt'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next to a game's name (in any page) that 13 | allows you to save tags for the game (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
28 | ), 29 | features: { 30 | gt_s: { 31 | name: 'Show tag suggestions while typing.', 32 | sg: true, 33 | st: true, 34 | }, 35 | }, 36 | id: 'gt', 37 | name: 'Game Tags', 38 | sg: true, 39 | type: 'games', 40 | }; 41 | } 42 | 43 | init() { 44 | Shared.esgst.gameFeatures.push(this.tags_addButtons.bind(this)); 45 | // noinspection JSIgnoredPromiseFromCall 46 | this.tags_getTags(); 47 | } 48 | } 49 | 50 | const gamesGameTags = new GamesGameTags(); 51 | 52 | export { gamesGameTags }; 53 | -------------------------------------------------------------------------------- /src/modules/General/AttachedImageLoader.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralAttachedImageLoader extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | conflicts: ['vai'], 10 | description: () => ( 11 |
    12 |
  • 13 | Only loads an attached image (in any page) when you click on its "View attached image" 14 | button, instead of loading it on page load, which should speed up page loads. 15 |
  • 16 |
17 | ), 18 | id: 'ail', 19 | name: 'Attached Image Loader', 20 | sg: true, 21 | st: true, 22 | type: 'general', 23 | }; 24 | } 25 | 26 | init() { 27 | if (Settings.get('vai')) return; 28 | this.esgst.endlessFeatures.push(this.ail_getImages.bind(this)); 29 | } 30 | 31 | ail_getImages(context, main, source, endless) { 32 | const buttons = context.querySelectorAll( 33 | `${ 34 | endless 35 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached` 36 | : '.comment__toggle-attached' 37 | }, ${ 38 | endless 39 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached` 40 | : '.view_attached' 41 | }` 42 | ); 43 | for (let i = 0, n = buttons.length; i < n; i++) { 44 | const button = buttons[i], 45 | image = button.nextElementSibling.firstElementChild, 46 | url = image.getAttribute('src'); 47 | image.removeAttribute('src'); 48 | button.addEventListener('click', image.setAttribute.bind(image, 'src', url)); 49 | } 50 | } 51 | } 52 | 53 | const generalAttachedImageLoader = new GeneralAttachedImageLoader(); 54 | 55 | export { generalAttachedImageLoader }; 56 | -------------------------------------------------------------------------------- /src/modules/General/ElementFilters.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralElementFilters extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • Allows you to hide elements in any page using CSS selectors.
  • 12 |
  • 13 | If you do not know how to use CSS selectors or you are having trouble hiding an element, 14 | leave a comment in the ESGST thread with a description/image of the element that you 15 | want to hide and I will give you the selector that you have to use. 16 |
  • 17 |
  • Here are some quick examples:
  • 18 |
      19 |
    • 20 | To hide the "Redeem" button in your{' '} 21 | won page, use:{' '} 22 | .table__column__key__redeem 23 |
    • 24 |
    • 25 | To hide the featured giveaway container (the big giveaway) in the main page, use:{' '} 26 | [esgst.giveawaysPath].featured__container 27 |
    • 28 |
    • 29 | To hide the pinned giveaways (the multiple copy giveaways) in the main page, use:{' '} 30 | [esgst.giveawaysPath].pinned-giveaways__outer-wrap 31 |
    • 32 |
    33 |
34 | ), 35 | inputItems: [ 36 | { 37 | id: 'ef_filters', 38 | prefix: `Filters: `, 39 | tooltip: `Separate each selector by a comma followed by a space, for example: .class_1, .class_2, #id`, 40 | }, 41 | ], 42 | id: 'ef', 43 | name: 'Element Filters', 44 | sg: true, 45 | st: true, 46 | type: 'general', 47 | }; 48 | } 49 | 50 | init() { 51 | this.ef_hideElements(document); 52 | this.esgst.endlessFeatures.push(this.ef_hideElements.bind(this)); 53 | if (Settings.get('sal') || !this.esgst.wonPath) return; 54 | this.esgst.endlessFeatures.push( 55 | this.esgst.modules.giveawaysSteamActivationLinks.sal_addObservers.bind( 56 | this.esgst.modules.giveawaysSteamActivationLinks 57 | ) 58 | ); 59 | } 60 | 61 | ef_hideElements(context, main, source, endless) { 62 | if (context === document && main) return; 63 | Settings.get('ef_filters') 64 | .split(`, `) 65 | .forEach((filter) => { 66 | if (!filter) return; 67 | try { 68 | const property = filter.match(/\[esgst\.(.+)]/); 69 | if (property) { 70 | if (!this.esgst[property[1]]) return; 71 | filter = filter.replace(/\[esgst\..+]/, ''); 72 | } 73 | const elements = context.querySelectorAll( 74 | `${ 75 | endless 76 | ? `.esgst-es-page-${endless} ${filter}, .esgst-es-page-${endless}${filter}` 77 | : `${filter}` 78 | }` 79 | ); 80 | for (let i = elements.length - 1; i > -1; i--) { 81 | elements[i].classList.add('esgst-hidden'); 82 | } 83 | } catch (e) { 84 | /**/ 85 | } 86 | }); 87 | } 88 | } 89 | 90 | const generalElementFilters = new GeneralElementFilters(); 91 | 92 | export { generalElementFilters }; 93 | -------------------------------------------------------------------------------- /src/modules/General/EmbeddedVideos.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GeneralEmbeddedVideos extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Embeds any YouTube/Vimeo videos found in a comment (in any page) into the comment. 14 |
  • 15 |
  • 16 | Videos are only embedded if their links are in the[URL](URL) format and are the only 17 | content in a line.For example, 18 | "[https://youtu.be/ihd9dKek2gc](https://youtu.be/ihd9dKek2gc)" gets embedded, but 19 | "[Watch this!](https://youtu.be/ihd9dKek2gc)" and "Watch this: 20 | [https://youtu.be/ihd9dKek2gc](https://youtu.be/ihd9dKek2gc)" do not. 21 |
  • 22 |
23 | ), 24 | id: 'ev', 25 | name: 'Embedded Videos', 26 | sg: true, 27 | st: true, 28 | type: 'general', 29 | featureMap: { 30 | endless: this.ev_getVideos.bind(this), 31 | }, 32 | }; 33 | } 34 | 35 | ev_getVideos(context, main, source, endless) { 36 | let types, 37 | i, 38 | numTypes, 39 | type, 40 | videos, 41 | j, 42 | numVideos, 43 | video, 44 | previous, 45 | next, 46 | embedUrl, 47 | url, 48 | text, 49 | title; 50 | types = ['youtube.com', 'youtu.be', 'vimeo.com']; 51 | for (i = 0, numTypes = types.length; i < numTypes; ++i) { 52 | type = types[i]; 53 | videos = context.querySelectorAll( 54 | `${ 55 | endless 56 | ? `.esgst-es-page-${endless} a[href*="${type}"], .esgst-es-page-${endless}a[href*="${type}"]` 57 | : `a[href*="${type}"]` 58 | }` 59 | ); 60 | for (j = 0, numVideos = videos.length; j < numVideos; ++j) { 61 | video = videos[j]; 62 | previous = video.previousSibling; 63 | next = video.nextSibling; 64 | if ((!previous || !previous.textContent.trim()) && (!next || !next.textContent.trim())) { 65 | // video is the only content in the line 66 | url = video.getAttribute('href'); 67 | embedUrl = this.ev_getEmbedUrl(i, url); 68 | if (embedUrl) { 69 | text = video.textContent; 70 | if (url !== text) { 71 | title = `
${text}
`; 72 | } else { 73 | title = ''; 74 | } 75 | createElements(video, 'atouter', [ 76 | { 77 | type: 'div', 78 | children: [ 79 | { 80 | text: title, 81 | type: 'node', 82 | }, 83 | { 84 | attributes: { 85 | allowfullscreen: '0', 86 | frameborder: '0', 87 | height: '360', 88 | src: embedUrl, 89 | width: '640', 90 | }, 91 | type: 'iframe', 92 | }, 93 | ], 94 | }, 95 | ]); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | ev_getEmbedUrl(i, url) { 103 | let regExps, regExp, match, baseUrls, baseUrl, code; 104 | regExps = [ 105 | /youtube.com\/watch\?v=(.+?)(\/.*)?(&.*)?$/, 106 | /youtu.be\/(.+?)(\/.*)?$/, 107 | /vimeo.com\/(.+?)(\/.*)?$/, 108 | ]; 109 | regExp = regExps[i]; 110 | match = url.match(regExp); 111 | if (match) { 112 | baseUrls = [ 113 | `https://www.youtube.com/embed/`, 114 | `https://www.youtube.com/embed/`, 115 | `https://player.vimeo.com/video/`, 116 | ]; 117 | baseUrl = baseUrls[i]; 118 | code = match[1]; 119 | return `${baseUrl}${code}`; 120 | } else { 121 | return null; 122 | } 123 | } 124 | } 125 | 126 | const generalEmbeddedVideos = new GeneralEmbeddedVideos(); 127 | 128 | export { generalEmbeddedVideos }; 129 | -------------------------------------------------------------------------------- /src/modules/General/FixedFooter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralFixedFooter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Keeps the footer of any page at the bottom of the window while you scroll down the page. 13 |
  • 14 |
15 | ), 16 | id: 'ff', 17 | name: 'Fixed Footer', 18 | sg: true, 19 | st: true, 20 | type: 'general', 21 | }; 22 | } 23 | 24 | init() { 25 | if (!Shared.footer) { 26 | return; 27 | } 28 | 29 | Shared.footer.nodes.outer.classList.add('esgst-ff'); 30 | } 31 | } 32 | 33 | const generalFixedFooter = new GeneralFixedFooter(); 34 | 35 | export { generalFixedFooter }; 36 | -------------------------------------------------------------------------------- /src/modules/General/FixedHeader.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralFixedHeader extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Keeps the header of any page at the top of the window while you scroll down the page. 13 |
  • 14 |
15 | ), 16 | id: 'fh', 17 | name: 'Fixed Header', 18 | sg: true, 19 | st: true, 20 | type: 'general', 21 | }; 22 | } 23 | 24 | init() { 25 | if (!Shared.header?.nodes.outer) { 26 | return; 27 | } 28 | Shared.header.nodes.outer.classList.add('esgst-fh'); 29 | } 30 | } 31 | 32 | const generalFixedHeader = new GeneralFixedHeader(); 33 | 34 | export { generalFixedHeader }; 35 | -------------------------------------------------------------------------------- /src/modules/General/FixedMainPageHeading.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { EventDispatcher } from '../../class/EventDispatcher'; 3 | import { Module } from '../../class/Module'; 4 | import { Events } from '../../constants/Events'; 5 | 6 | class GeneralFixedMainPageHeading extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Keeps the main page heading (usually the first heading of the page, for example, the 14 | heading that says "Giveaways" in the main page) of any page at the top of the window 15 | while you scroll down the page. 16 |
  • 17 |
18 | ), 19 | id: 'fmph', 20 | name: 'Fixed Main Page Heading', 21 | sg: true, 22 | st: true, 23 | type: 'general', 24 | }; 25 | } 26 | 27 | init() { 28 | EventDispatcher.subscribe(Events.PAGE_HEADING_BUILD, (builtHeading) => 29 | builtHeading.nodes.outer.classList.add('esgst-fmph') 30 | ); 31 | 32 | if (!this.esgst.pageHeadings.length) { 33 | return; 34 | } 35 | 36 | this.esgst.style.insertAdjacentText( 37 | 'beforeend', 38 | ` 39 | .esgst-fmph { 40 | top: ${this.esgst.pageTop}px; 41 | } 42 | ` 43 | ); 44 | 45 | for (const pageHeading of this.esgst.pageHeadings) { 46 | pageHeading.classList.add('esgst-fmph'); 47 | } 48 | } 49 | } 50 | 51 | const generalFixedMainPageHeading = new GeneralFixedMainPageHeading(); 52 | 53 | export { generalFixedMainPageHeading }; 54 | -------------------------------------------------------------------------------- /src/modules/General/FixedSidebar.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralFixedSidebar extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Keeps the sidebar of any page at the left side of the window while you scroll down the 13 | page. 14 |
  • 15 |
16 | ), 17 | id: 'fs', 18 | name: 'Fixed Sidebar', 19 | sg: true, 20 | type: 'general', 21 | }; 22 | } 23 | 24 | init() { 25 | if (!this.esgst.sidebar) { 26 | return; 27 | } 28 | 29 | const top = this.esgst.pageTop + 25; 30 | this.esgst.style.insertAdjacentText( 31 | 'beforeend', 32 | ` 33 | .esgst-fs { 34 | max-height: calc(100vh - ${top + 30 + (Settings.get('ff') ? 39 : 0)}px); 35 | top: ${top}px; 36 | } 37 | ` 38 | ); 39 | 40 | this.esgst.sidebar.classList.add('esgst-fs'); 41 | } 42 | } 43 | 44 | const generalFixedSidebar = new GeneralFixedSidebar(); 45 | 46 | export { generalFixedSidebar }; 47 | -------------------------------------------------------------------------------- /src/modules/General/HiddenBlacklistStats.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralHiddenBlacklistStats extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Hides the blacklist stats of your{' '} 12 | stats page. 13 |
  • 14 |
15 | ), 16 | id: 'hbs', 17 | name: 'Hidden Blacklist Stats', 18 | sg: true, 19 | type: 'general', 20 | }; 21 | } 22 | 23 | init() { 24 | if (!window.location.pathname.match(/^\/stats\/personal\/community/)) return; 25 | 26 | let chart = document.getElementsByClassName('chart')[4]; 27 | 28 | // remove any "blacklist" text from the chart 29 | let heading = chart.firstElementChild; 30 | heading.lastElementChild.remove(); 31 | heading.lastElementChild.remove(); 32 | let subHeading = heading.nextElementSibling; 33 | subHeading.textContent = subHeading.textContent.replace(/and\sblacklists\s/, ''); 34 | 35 | // create a new graph without the blacklist points 36 | let script = document.createElement('script'); 37 | script.textContent = chart.previousElementSibling.textContent.replace( 38 | /,{name:\s"Blacklists".+?}/, 39 | '' 40 | ); 41 | document.body.appendChild(script); 42 | script.remove(); 43 | } 44 | } 45 | 46 | const generalHiddenBlacklistStats = new GeneralHiddenBlacklistStats(); 47 | 48 | export { generalHiddenBlacklistStats }; 49 | -------------------------------------------------------------------------------- /src/modules/General/HiddenCommunityPoll.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralHiddenCommunityPoll extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • Hides the community poll (if there is one) of the main page.
  • 12 |
13 | ), 14 | features: { 15 | hcp_v: { 16 | name: 'Only hide the poll if you already voted in it.', 17 | sg: true, 18 | }, 19 | }, 20 | id: 'hcp', 21 | name: 'Hidden Community Poll', 22 | sg: true, 23 | type: 'general', 24 | }; 25 | } 26 | 27 | init() { 28 | if (!this.esgst.giveawaysPath || !this.esgst.activeDiscussions) return; 29 | let poll = this.esgst.activeDiscussions.previousElementSibling; 30 | if ( 31 | poll && 32 | poll.classList.contains('widget-container') && 33 | !poll.querySelector(`.block_header[href="/happy-holidays"]`) 34 | ) { 35 | if (!Settings.get('hcp_v') || poll.querySelector('.table__row-outer-wrap.is-selected')) { 36 | poll.classList.add('esgst-hidden'); 37 | } 38 | } 39 | } 40 | } 41 | 42 | const generalHiddenCommunityPoll = new GeneralHiddenCommunityPoll(); 43 | 44 | export { generalHiddenCommunityPoll }; 45 | -------------------------------------------------------------------------------- /src/modules/General/ImageBorders.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralImageBorders extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • Brings back image borders to SteamGifts.
  • 11 |
12 | ), 13 | id: 'ib', 14 | name: 'Image Borders', 15 | sg: true, 16 | type: 'general', 17 | featureMap: { 18 | endless: this.ib_addBorders.bind(this), 19 | }, 20 | }; 21 | } 22 | 23 | ib_addBorders(context, main, source, endless) { 24 | const userElements = context.querySelectorAll( 25 | `${ 26 | endless 27 | ? `.esgst-es-page-${endless} .giveaway_image_avatar, .esgst-es-page-${endless}.giveaway_image_avatar` 28 | : '.giveaway_image_avatar' 29 | }, ${ 30 | endless 31 | ? `.esgst-es-page-${endless} .featured_giveaway_image_avatar, .esgst-es-page-${endless}.featured_giveaway_image_avatar` 32 | : '.featured_giveaway_image_avatar' 33 | }, ${ 34 | endless 35 | ? `.esgst-es-page-${endless} :not(.esgst-ggl-panel) .table_image_avatar, .esgst-es-page-${endless}:not(.esgst-ggl-panel) .table_image_avatar` 36 | : `:not(.esgst-ggl-panel) .table_image_avatar` 37 | }` 38 | ); 39 | for (let i = 0, n = userElements.length; i < n; ++i) { 40 | userElements[i].classList.add('esgst-ib-user'); 41 | } 42 | const gameElements = context.querySelectorAll( 43 | `${ 44 | endless 45 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail, .esgst-es-page-${endless}.giveaway_image_thumbnail` 46 | : '.giveaway_image_thumbnail' 47 | }, ${ 48 | endless 49 | ? `.esgst-es-page-${endless} .giveaway_image_thumbnail_missing, .esgst-es-page-${endless}.giveaway_image_thumbnail_missing` 50 | : '.giveaway_image_thumbnail_missing' 51 | }, ${ 52 | endless 53 | ? `.esgst-es-page-${endless} .table_image_thumbnail, .esgst-es-page-${endless}.table_image_thumbnail` 54 | : '.table_image_thumbnail' 55 | }, ${ 56 | endless 57 | ? `.esgst-es-page-${endless} .table_image_thumbnail_missing, .esgst-es-page-${endless}.table_image_thumbnail_missing` 58 | : '.table_image_thumbnail_missing' 59 | }` 60 | ); 61 | for (let i = 0, n = gameElements.length; i < n; ++i) { 62 | gameElements[i].classList.add('esgst-ib-game'); 63 | } 64 | } 65 | } 66 | 67 | const generalImageBorders = new GeneralImageBorders(); 68 | 69 | export { generalImageBorders }; 70 | -------------------------------------------------------------------------------- /src/modules/General/NarrowSidebar.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralNarrowSidebar extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • Keeps the sidebar narrowed in all pages.
  • 11 |
12 | ), 13 | id: 'ns', 14 | name: 'Narrow Sidebar', 15 | sg: true, 16 | type: 'general', 17 | }; 18 | } 19 | 20 | init() { 21 | if (!this.esgst.sidebar) return; 22 | this.esgst.sidebar.classList.remove('sidebar--wide'); 23 | this.esgst.sidebar.classList.add('esgst-ns'); 24 | } 25 | } 26 | 27 | const generalNarrowSidebar = new GeneralNarrowSidebar(); 28 | 29 | export { generalNarrowSidebar }; 30 | -------------------------------------------------------------------------------- /src/modules/General/PageLoadTimestamp.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import dateFns_format from 'date-fns/format'; 3 | import { common } from '../Common'; 4 | import { Settings } from '../../class/Settings'; 5 | import { DOM } from '../../class/DOM'; 6 | import { Shared } from '../../class/Shared'; 7 | 8 | class GeneralPageLoadTimestamp extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a timestamp indicating when the page was loaded to any page, in the preferred 16 | location. 17 |
  • 18 |
19 | ), 20 | id: 'plt', 21 | name: 'Page Load Timestamp', 22 | inputItems: [ 23 | { 24 | id: 'plt_format', 25 | prefix: `Timestamp format: `, 26 | tooltip: `ESGST uses date-fns v2.0.0-alpha.25, so check the accepted tokens here: https://date-fns.org/v2.0.0-alpha.25/docs/Getting-Started.`, 27 | }, 28 | ], 29 | options: { 30 | title: `Position:`, 31 | values: ['Sidebar', 'Footer'], 32 | }, 33 | sg: true, 34 | st: true, 35 | type: 'general', 36 | }; 37 | } 38 | 39 | init() { 40 | const timestamp = dateFns_format( 41 | Date.now(), 42 | Settings.get('plt_format') || `MMM dd, yyyy, HH:mm:ss` 43 | ); 44 | switch (Settings.get('plt_index')) { 45 | case 0: 46 | if (this.esgst.sidebar) { 47 | DOM.insert( 48 | this.esgst.sidebar, 49 | 'afterbegin', 50 | 51 |

Page Load Timestamp

52 |
{timestamp}
53 |
54 | ); 55 | break; 56 | } 57 | case 1: { 58 | if (!Shared.footer) { 59 | return; 60 | } 61 | 62 | const linkContainer = Shared.footer.addLinkContainer({ 63 | name: `Page loaded on ${timestamp}`, 64 | side: 'left', 65 | }); 66 | 67 | linkContainer.nodes.outer.classList.add('esgst-plt'); 68 | 69 | break; 70 | } 71 | } 72 | } 73 | } 74 | 75 | const generalPageLoadTimestamp = new GeneralPageLoadTimestamp(); 76 | 77 | export { generalPageLoadTimestamp }; 78 | -------------------------------------------------------------------------------- /src/modules/General/PaginationNavigationOnTop.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const getFeatureTooltip = common.getFeatureTooltip.bind(common); 7 | class GeneralPaginationNavigationOnTop extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Moves the pagination navigation of any page to the main page heading of the page.
  • 14 |
15 | ), 16 | features: { 17 | pnot_s: { 18 | name: `Enable simplified view (will show only the numbers and arrows).`, 19 | sg: true, 20 | st: true, 21 | }, 22 | }, 23 | id: 'pnot', 24 | name: 'Pagination Navigation On Top', 25 | sg: true, 26 | st: true, 27 | type: 'general', 28 | }; 29 | } 30 | 31 | init() { 32 | if (!this.esgst.paginationNavigation || !this.esgst.mainPageHeading) return; 33 | 34 | if (this.esgst.st) { 35 | this.esgst.paginationNavigation.classList.add('page_heading_btn'); 36 | } 37 | this.esgst.paginationNavigation.title = getFeatureTooltip('pnot'); 38 | this.pnot_simplify(); 39 | DOM.insert( 40 | this.esgst.mainPageHeading.querySelector( 41 | `.page__heading__breadcrumbs, .page_heading_breadcrumbs` 42 | ), 43 | 'afterend', 44 | this.esgst.paginationNavigation 45 | ); 46 | } 47 | 48 | pnot_simplify() { 49 | if (Settings.get('pnot') && Settings.get('pnot_s')) { 50 | const elements = this.esgst.paginationNavigation.querySelectorAll('span'); 51 | // @ts-ignore 52 | for (const element of elements) { 53 | if (element.textContent.match(/[A-Za-z]+/)) { 54 | element.textContent = element.textContent.replace(/[A-Za-z]+/g, ''); 55 | if (element.previousElementSibling) { 56 | element.appendChild(element.previousElementSibling); 57 | } 58 | if (element.nextElementSibling) { 59 | element.appendChild(element.nextElementSibling); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | const generalPaginationNavigationOnTop = new GeneralPaginationNavigationOnTop(); 68 | 69 | export { generalPaginationNavigationOnTop }; 70 | -------------------------------------------------------------------------------- /src/modules/General/SameTabOpener.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralSameTabOpener extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • Opens any link in the page in the same tab.
  • 11 |
12 | ), 13 | id: 'sto', 14 | name: 'Same Tab Opener', 15 | sg: true, 16 | st: true, 17 | type: 'general', 18 | featureMap: { 19 | endless: this.sto_setLinks.bind(this), 20 | }, 21 | }; 22 | } 23 | 24 | sto_setLinks(context, main, source, endless) { 25 | const elements = context.querySelectorAll( 26 | `${ 27 | endless 28 | ? `.esgst-es-page-${endless} [target="_blank"], .esgst-es-page-${endless}[target="_blank"]` 29 | : `[target="_blank"]` 30 | }` 31 | ); 32 | for (let i = 0, n = elements.length; i < n; ++i) { 33 | elements[i].removeAttribute('target'); 34 | } 35 | } 36 | } 37 | 38 | const generalSameTabOpener = new GeneralSameTabOpener(); 39 | 40 | export { generalSameTabOpener }; 41 | -------------------------------------------------------------------------------- /src/modules/General/ScrollToBottomButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { Shared } from '../../class/Shared'; 5 | import { DOM } from '../../class/DOM'; 6 | 7 | const animateScroll = common.animateScroll.bind(common), 8 | createElements = common.createElements.bind(common), 9 | createHeadingButton = common.createHeadingButton.bind(common), 10 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 11 | class GeneralScrollToBottomButton extends Module { 12 | constructor() { 13 | super(); 14 | this.info = { 15 | description: () => ( 16 |
    17 |
  • 18 | Adds a button () either to the bottom right 19 | corner, the main page heading or the footer (you can decide where) of any page that 20 | takes you to the bottom of the page. 21 |
  • 22 |
23 | ), 24 | id: 'stbb', 25 | name: 'Scroll To Bottom Button', 26 | options: { 27 | title: `Show in:`, 28 | values: ['Bottom Right Corner', 'Main Page Heading', 'Footer'], 29 | }, 30 | sg: true, 31 | st: true, 32 | type: 'general', 33 | }; 34 | } 35 | 36 | init() { 37 | let button; 38 | switch (Settings.get('stbb_index')) { 39 | case 0: 40 | button = createElements(document.body, 'beforeend', [ 41 | { 42 | attributes: { 43 | class: 'esgst-stbb-button esgst-stbb-button-fixed', 44 | title: `${getFeatureTooltip('stbb', 'Scroll to bottom')}`, 45 | }, 46 | type: 'div', 47 | children: [ 48 | { 49 | attributes: { 50 | class: 'fa fa-chevron-down', 51 | }, 52 | type: 'i', 53 | }, 54 | ], 55 | }, 56 | ]); 57 | window.addEventListener('scroll', () => { 58 | if (document.documentElement.offsetHeight - window.innerHeight >= window.scrollY + 100) { 59 | button.classList.remove('esgst-hidden'); 60 | } else { 61 | button.classList.add('esgst-hidden'); 62 | } 63 | }); 64 | break; 65 | case 1: 66 | button = createHeadingButton({ 67 | id: 'stbb', 68 | icons: ['fa-chevron-down'], 69 | title: 'Scroll to bottom', 70 | }); 71 | button.classList.add('esgst-stbb-button'); 72 | break; 73 | case 2: { 74 | const linkContainer = Shared.footer.addLinkContainer({ 75 | icon: 'fa fa-chevron-down', 76 | position: 'beforeend', 77 | side: 'right', 78 | }); 79 | 80 | linkContainer.nodes.outer.classList.add('esgst-stbb-button'); 81 | linkContainer.nodes.outer.title = getFeatureTooltip('stbb', 'Scroll to bottom'); 82 | 83 | button = linkContainer.nodes.outer; 84 | 85 | break; 86 | } 87 | } 88 | button.addEventListener('click', () => 89 | animateScroll(document.documentElement.offsetHeight, () => { 90 | if (Settings.get('es') && this.esgst.es.paginations) { 91 | this.esgst.modules.generalEndlessScrolling.es_changePagination( 92 | this.esgst.es, 93 | this.esgst.es.reverseScrolling ? 1 : this.esgst.es.paginations.length 94 | ); 95 | } 96 | }) 97 | ); 98 | } 99 | } 100 | 101 | const generalScrollToBottomButton = new GeneralScrollToBottomButton(); 102 | 103 | export { generalScrollToBottomButton }; 104 | -------------------------------------------------------------------------------- /src/modules/General/ScrollToTopButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { Shared } from '../../class/Shared'; 5 | import { DOM } from '../../class/DOM'; 6 | 7 | const animateScroll = common.animateScroll.bind(common), 8 | createElements = common.createElements.bind(common), 9 | createHeadingButton = common.createHeadingButton.bind(common), 10 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 11 | class GeneralScrollToTopButton extends Module { 12 | constructor() { 13 | super(); 14 | this.info = { 15 | description: () => ( 16 |
    17 |
  • 18 | Adds a button () either to the bottom right corner, 19 | the main page heading or the footer (you can decide where) of any page that takes you to 20 | the top of the page. 21 |
  • 22 |
23 | ), 24 | id: 'sttb', 25 | name: 'Scroll To Top Button', 26 | options: { 27 | title: `Show in:`, 28 | values: ['Bottom Right Corner', 'Main Page Heading', 'Footer'], 29 | }, 30 | sg: true, 31 | st: true, 32 | type: 'general', 33 | }; 34 | } 35 | 36 | init() { 37 | let button; 38 | switch (Settings.get('sttb_index')) { 39 | case 0: 40 | button = createElements(document.body, 'beforeend', [ 41 | { 42 | attributes: { 43 | class: 'esgst-sttb-button esgst-sttb-button-fixed', 44 | title: `${getFeatureTooltip('sttb', 'Scroll to top')}`, 45 | }, 46 | type: 'div', 47 | children: [ 48 | { 49 | attributes: { 50 | class: 'fa fa-chevron-up', 51 | }, 52 | type: 'i', 53 | }, 54 | ], 55 | }, 56 | ]); 57 | button.classList.add('esgst-hidden'); 58 | window.addEventListener('scroll', () => { 59 | if (window.scrollY > 100) { 60 | button.classList.remove('esgst-hidden'); 61 | } else { 62 | button.classList.add('esgst-hidden'); 63 | } 64 | }); 65 | break; 66 | case 1: 67 | button = createHeadingButton({ 68 | id: 'sttb', 69 | icons: ['fa-chevron-up'], 70 | title: 'Scroll to top', 71 | }); 72 | button.classList.add('esgst-sttb-button'); 73 | break; 74 | case 2: { 75 | const linkContainer = Shared.footer.addLinkContainer({ 76 | icon: 'fa fa-chevron-up', 77 | position: 'beforeend', 78 | side: 'right', 79 | }); 80 | 81 | linkContainer.nodes.outer.classList.add('esgst-sttb-button'); 82 | linkContainer.nodes.outer.title = getFeatureTooltip('sttb', 'Scroll to top'); 83 | 84 | button = linkContainer.nodes.outer; 85 | 86 | break; 87 | } 88 | } 89 | button.addEventListener( 90 | 'click', 91 | animateScroll.bind(common, 0, () => { 92 | if (Settings.get('es') && this.esgst.es.paginations) { 93 | this.esgst.modules.generalEndlessScrolling.es_changePagination( 94 | this.esgst.es, 95 | this.esgst.es.reverseScrolling ? this.esgst.es.paginations.length : 1 96 | ); 97 | } 98 | }) 99 | ); 100 | } 101 | } 102 | 103 | const generalScrollToTopButton = new GeneralScrollToTopButton(); 104 | 105 | export { generalScrollToTopButton }; 106 | -------------------------------------------------------------------------------- /src/modules/General/SearchClearButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralSearchClearButton extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • Adds a clear button to each search input in the page.
  • 12 |
13 | ), 14 | id: 'scb', 15 | name: 'Search Clear Button', 16 | sg: true, 17 | type: 'general', 18 | }; 19 | } 20 | 21 | init() { 22 | this.getInputs(document); 23 | } 24 | 25 | getInputs(context) { 26 | const inputs = context.querySelectorAll('.sidebar__search-input'); 27 | for (const input of inputs) { 28 | input.parentElement.classList.add('esgst-scb'); 29 | DOM.insert( 30 | input.parentElement, 31 | 'beforeend', 32 | { 36 | input.value = ''; 37 | input.dispatchEvent(new Event('change')); 38 | input.focus(); 39 | }} 40 | > 41 | ); 42 | } 43 | } 44 | } 45 | 46 | const generalSearchClearButton = new GeneralSearchClearButton(); 47 | 48 | export { generalSearchClearButton }; 49 | -------------------------------------------------------------------------------- /src/modules/General/SearchMagnifyingGlassButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralSearchMagnifyingGlassButton extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Turns the magnifying glass icon () in the search field 13 | of any page into a button that submits the search when you click on it. 14 |
  • 15 |
16 | ), 17 | id: 'smgb', 18 | name: 'Search Magnifying Glass Button', 19 | sg: true, 20 | type: 'general', 21 | }; 22 | } 23 | 24 | init() { 25 | let buttons, i; 26 | buttons = document.querySelectorAll( 27 | `.sidebar__search-container .fa-search, .esgst-qgs-container .fa-search` 28 | ); 29 | for (i = buttons.length - 1; i > -1; --i) { 30 | let button, input; 31 | button = buttons[i]; 32 | input = button.previousElementSibling; 33 | button.classList.add('esgst-clickable'); 34 | button.addEventListener('click', () => { 35 | let value = input.value.trim(); 36 | if (value) { 37 | if (Settings.get('as') && value.match(/"|id:/)) { 38 | this.esgst.modules.giveawaysArchiveSearcher.as_openPage(input); 39 | } else { 40 | window.location.href = `${this.esgst.searchUrl.replace(/page=/, '')}q=${value}`; 41 | } 42 | } 43 | }); 44 | } 45 | } 46 | } 47 | 48 | const generalSearchMagnifyingGlassButton = new GeneralSearchMagnifyingGlassButton(); 49 | 50 | export { generalSearchMagnifyingGlassButton }; 51 | -------------------------------------------------------------------------------- /src/modules/General/TimeToPointCapCalculator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { EventDispatcher } from '../../class/EventDispatcher'; 5 | import { Events } from '../../constants/Events'; 6 | import { Session } from '../../class/Session'; 7 | import { Shared } from '../../class/Shared'; 8 | import { DOM } from '../../class/DOM'; 9 | 10 | class GeneralTimeToPointCapCalculator extends Module { 11 | constructor() { 12 | super(); 13 | this.info = { 14 | description: () => ( 15 |
    16 |
  • 17 | If you have less than 400P and you hover over the number of points at the header of any 18 | page, it shows how much time you have to wait until you have 400P. 19 |
  • 20 |
21 | ), 22 | features: { 23 | ttpcc_a: { 24 | name: 'Show time alongside points.', 25 | sg: true, 26 | }, 27 | }, 28 | id: 'ttpcc', 29 | name: 'Time To Point Cap Calculator', 30 | sg: true, 31 | type: 'general', 32 | }; 33 | } 34 | 35 | init() { 36 | EventDispatcher.subscribe(Events.POINTS_UPDATED, this.update.bind(this)); 37 | 38 | this.update(null, Session.counters.points); 39 | } 40 | 41 | update(oldPoints, newPoints) { 42 | if (newPoints >= 400) { 43 | return; 44 | } 45 | 46 | let nextRefresh = 60 - new Date().getMinutes(); 47 | 48 | while (nextRefresh > 15) { 49 | nextRefresh -= 15; 50 | } 51 | 52 | const time = this.esgst.modules.giveawaysTimeToEnterCalculator.ttec_getTime( 53 | Math.round((nextRefresh + 15 * Math.floor((400 - newPoints) / 6)) * 100) / 100 54 | ); 55 | 56 | const pointsNode = Shared.header.buttonContainers['account'].nodes.points; 57 | pointsNode.textContent = `${newPoints.toLocaleString('en-US')}${ 58 | Settings.get('ttpcc_a') ? `P / ${time} to 400` : '' 59 | }`; 60 | pointsNode.title = common.getFeatureTooltip('ttpcc', `${time} to 400P`); 61 | } 62 | } 63 | 64 | const generalTimeToPointCapCalculator = new GeneralTimeToPointCapCalculator(); 65 | 66 | export { generalTimeToPointCapCalculator }; 67 | -------------------------------------------------------------------------------- /src/modules/General/URLRedirector.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GeneralURLRedirector extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Redirects broken URLs to the correct URLs. For example, "/giveaway/XXXXX" redirects to 12 | "/giveaway/XXXXX/". 13 |
  • 14 |
15 | ), 16 | id: 'urlr', 17 | name: 'URL Redirector', 18 | sg: true, 19 | st: true, 20 | type: 'general', 21 | }; 22 | } 23 | } 24 | 25 | const generalURLRedirector = new GeneralURLRedirector(); 26 | 27 | export { generalURLRedirector }; 28 | -------------------------------------------------------------------------------- /src/modules/General/VisibleAttachedImages.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GeneralVisibleAttachedImages extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | conflicts: ['ail'], 10 | description: () => ( 11 |
    12 |
  • 13 | Displays all of the attached images (in any page) by default so that you do not need to 14 | click on "View attached image" to view them. 15 |
  • 16 |
17 | ), 18 | features: { 19 | vai_gifv: { 20 | name: 'Rename .gifv images to .gif so that they are properly attached.', 21 | sg: true, 22 | st: true, 23 | }, 24 | }, 25 | id: 'vai', 26 | name: 'Visible Attached Images', 27 | sg: true, 28 | st: true, 29 | type: 'general', 30 | featureMap: { 31 | endless: this.vai_getImages.bind(this), 32 | }, 33 | }; 34 | } 35 | 36 | vai_getImages(context, main, source, endless) { 37 | let buttons = context.querySelectorAll( 38 | `${ 39 | endless 40 | ? `.esgst-es-page-${endless} .comment__toggle-attached, .esgst-es-page-${endless}.comment__toggle-attached` 41 | : '.comment__toggle-attached' 42 | }, ${ 43 | endless 44 | ? `.esgst-es-page-${endless} .view_attached, .esgst-es-page-${endless}.view_attached` 45 | : '.view_attached' 46 | }` 47 | ); 48 | for (let i = 0, n = buttons.length; i < n; i++) { 49 | let button = buttons[i]; 50 | let image = button.nextElementSibling.firstElementChild; 51 | let url = image.getAttribute('src'); 52 | if (url && Settings.get('vai_gifv')) { 53 | url = url.replace(/\.gifv/, '.gif'); 54 | image.setAttribute('src', url); 55 | } 56 | image.classList.remove('is_hidden', 'is-hidden'); 57 | } 58 | } 59 | } 60 | 61 | const generalVisibleAttachedImages = new GeneralVisibleAttachedImages(); 62 | 63 | export { generalVisibleAttachedImages }; 64 | -------------------------------------------------------------------------------- /src/modules/General/VisibleFullLevel.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { EventDispatcher } from '../../class/EventDispatcher'; 3 | import { Events } from '../../constants/Events'; 4 | import { Session } from '../../class/Session'; 5 | import { Shared } from '../../class/Shared'; 6 | import { DOM } from '../../class/DOM'; 7 | 8 | class GeneralVisibleFullLevel extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Displays the full level at the header, instead of only showing it when hovering over the 16 | level. For example, "Level 5" becomes "Lvl 5.25". 17 |
  • 18 |
19 | ), 20 | id: 'vfl', 21 | name: 'Visible Full Level', 22 | sg: true, 23 | type: 'general', 24 | }; 25 | } 26 | 27 | init() { 28 | EventDispatcher.subscribe(Events.LEVEL_UPDATED, this.update.bind(this)); 29 | 30 | this.update(null, Session.counters.level); 31 | } 32 | 33 | async update(oldLevel, newLevel) { 34 | const levelNode = Shared.header.buttonContainers['account'].nodes.level; 35 | levelNode.textContent = `Lvl ${newLevel.full}`; 36 | } 37 | } 38 | 39 | const generalVisibleFullLevel = new GeneralVisibleFullLevel(); 40 | 41 | export { generalVisibleFullLevel }; 42 | -------------------------------------------------------------------------------- /src/modules/Giveaways/CommunityWishlistSearchLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysCommunityWishlistSearchLink extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Turns the numbers in the "Giveaways" column of any{' '} 14 | community wishlist page into 15 | links that allow you to search for all of the active giveaways for the game (that are 16 | visible to you). 17 |
  • 18 |
19 | ), 20 | id: 'cwsl', 21 | name: 'Community Wishlist Search Link', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | init() { 28 | if (this.esgst.wishlistPath) { 29 | this.esgst.gameFeatures.push(this.cwsl_getGames.bind(this)); 30 | } 31 | } 32 | 33 | cwsl_getGames(games, main) { 34 | if (!main) { 35 | return; 36 | } 37 | for (const game of games.all) { 38 | let giveawayCount = game.heading.parentElement.nextElementSibling.nextElementSibling; 39 | createElements(giveawayCount, 'atinner', [ 40 | { 41 | attributes: { 42 | class: 'table__column__secondary-link', 43 | href: `/giveaways/search?${game.type.slice(0, -1)}=${game.id}`, 44 | }, 45 | type: 'a', 46 | children: [ 47 | ...Array.from(giveawayCount.childNodes).map((x) => { 48 | return { 49 | context: x, 50 | }; 51 | }), 52 | ], 53 | }, 54 | ]); 55 | } 56 | } 57 | } 58 | 59 | const giveawaysCommunityWishlistSearchLink = new GiveawaysCommunityWishlistSearchLink(); 60 | 61 | export { giveawaysCommunityWishlistSearchLink }; 62 | -------------------------------------------------------------------------------- /src/modules/Giveaways/CustomGiveawayBackground.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysCustomGiveawayBackground extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Allows you to color the background of giveaways based on their type (public, invite 13 | only, region restricted, group or whitelist) and level. 14 |
  • 15 |
16 | ), 17 | features: { 18 | cgb_b: { 19 | background: true, 20 | name: 'Color giveaways that cannot be entered because of blacklist reasons.', 21 | sg: true, 22 | }, 23 | cgb_p: { 24 | background: true, 25 | name: 'Color public giveaways.', 26 | sg: true, 27 | }, 28 | cgb_io: { 29 | background: true, 30 | name: 'Color invite only giveaways.', 31 | sg: true, 32 | }, 33 | cgb_rr: { 34 | background: true, 35 | name: 'Color region restricted giveaways.', 36 | sg: true, 37 | }, 38 | cgb_g: { 39 | background: true, 40 | name: 'Color group giveaways.', 41 | sg: true, 42 | }, 43 | cgb_w: { 44 | background: true, 45 | name: 'Color whitelist giveaways.', 46 | sg: true, 47 | }, 48 | cgb_sgt: { 49 | background: true, 50 | name: 'Color SGTools giveaways.', 51 | sg: true, 52 | }, 53 | }, 54 | featureMap: { 55 | giveaway: this.color.bind(this), 56 | }, 57 | id: 'cgb', 58 | name: 'Custom Giveaway Background', 59 | sg: true, 60 | type: 'giveaways', 61 | }; 62 | } 63 | 64 | color(giveaways) { 65 | for (const giveaway of giveaways) { 66 | if (Settings.get('cgb_b') && giveaway.outerWrap.getAttribute('data-blacklist')) { 67 | giveaway.outerWrap.setAttribute( 68 | 'style', 69 | `background-color: ${Settings.get('cgb_b_bgColor')} !important` 70 | ); 71 | } else if (Settings.get('cgb_sgt') && giveaway.sgTools) { 72 | giveaway.outerWrap.setAttribute( 73 | 'style', 74 | `background-color: ${Settings.get('cgb_sgt_bgColor')} !important` 75 | ); 76 | } 77 | const { color } = Settings.get('cgb_levelColors').filter( 78 | (colors) => 79 | giveaway.level >= parseInt(colors.lower) && giveaway.level <= parseInt(colors.upper) 80 | )[0] || { color: undefined }; 81 | if (color) { 82 | giveaway.outerWrap.setAttribute('style', `background-color: ${color} !important`); 83 | } else if (Settings.get('cgb_w') && giveaway.whitelist) { 84 | giveaway.outerWrap.setAttribute( 85 | 'style', 86 | `background-color: ${Settings.get('cgb_w_bgColor')} !important` 87 | ); 88 | } else if (Settings.get('cgb_g') && giveaway.group) { 89 | giveaway.outerWrap.setAttribute( 90 | 'style', 91 | `background-color: ${Settings.get('cgb_g_bgColor')} !important` 92 | ); 93 | } else if (Settings.get('cgb_rr') && giveaway.regionRestricted) { 94 | giveaway.outerWrap.setAttribute( 95 | 'style', 96 | `background-color: ${Settings.get('cgb_rr_bgColor')} !important` 97 | ); 98 | } else if (Settings.get('cgb_io') && giveaway.inviteOnly) { 99 | giveaway.outerWrap.setAttribute( 100 | 'style', 101 | `background-color: ${Settings.get('cgb_io_bgColor')} !important` 102 | ); 103 | } else if (Settings.get('cgb_p') && giveaway.public) { 104 | giveaway.outerWrap.setAttribute( 105 | 'style', 106 | `background-color: ${Settings.get('cgb_p_bgColor')} !important` 107 | ); 108 | } else { 109 | giveaway.outerWrap.style.backgroundColor = ''; 110 | } 111 | if (giveaway.outerWrap.style.backgroundColor) { 112 | giveaway.outerWrap.style.backgroundImage = 'none'; 113 | } 114 | } 115 | } 116 | } 117 | 118 | const giveawaysCustomGiveawayBackground = new GiveawaysCustomGiveawayBackground(); 119 | 120 | export { giveawaysCustomGiveawayBackground }; 121 | -------------------------------------------------------------------------------- /src/modules/Giveaways/DeleteKeyConfirmation.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { FetchRequest } from '../../class/FetchRequest'; 3 | import { Module } from '../../class/Module'; 4 | import { Session } from '../../class/Session'; 5 | import { common } from '../Common'; 6 | 7 | const createConfirmation = common.createConfirmation.bind(common), 8 | createElements = common.createElements.bind(common); 9 | class GiveawaysDeleteKeyConfirmation extends Module { 10 | constructor() { 11 | super(); 12 | this.info = { 13 | description: () => ( 14 |
    15 |
  • 16 | Shows a confirmation popup if you try to delete a giveaway's key(s) (in any{' '} 17 | winners{' '} 18 | page). 19 |
  • 20 |
21 | ), 22 | id: 'dkc', 23 | name: 'Delete Key Confirmation', 24 | sg: true, 25 | type: 'giveaways', 26 | }; 27 | } 28 | 29 | init() { 30 | if (!this.esgst.giveawayPath) return; 31 | this.esgst.endlessFeatures.push(this.dkc_getLinks.bind(this)); 32 | } 33 | 34 | dkc_getLinks(context, main, source, endless) { 35 | const elements = context.querySelectorAll( 36 | `${ 37 | endless 38 | ? `.esgst-es-page-${endless} .form__key-btn-delete, .esgst-es-page-${endless}.form__key-btn-delete` 39 | : '.form__key-btn-delete' 40 | }` 41 | ); 42 | for (let i = elements.length - 1; i > -1; --i) { 43 | const element = elements[i]; 44 | const newElement = createElements(element, 'afterend', [ 45 | { 46 | attributes: { 47 | class: 'table__column__secondary-link esgst-clickable', 48 | }, 49 | text: 'Delete', 50 | type: 'span', 51 | }, 52 | ]); 53 | element.remove(); 54 | newElement.addEventListener( 55 | 'click', 56 | createConfirmation.bind( 57 | common, 58 | 'Are you sure you want to delete this key?', 59 | this.dkc_deleteKey.bind(createConfirmation, newElement), 60 | null 61 | ) 62 | ); 63 | } 64 | } 65 | 66 | async dkc_deleteKey(link) { 67 | let row = link.closest('.table__row-inner-wrap'); 68 | row.getElementsByClassName('form__key-read')[0].classList.add('is-hidden'); 69 | row.getElementsByClassName('form__key-loading')[0].classList.remove('is-hidden'); 70 | row.querySelector(`[name="key_value"]`).value = ''; 71 | row.getElementsByClassName('form__key-value')[0].textContent = ''; 72 | await FetchRequest.post('/ajax.php', { 73 | data: `xsrf_token=${Session.xsrfToken}&do=set_gift_key&key_value=&winner_id=${ 74 | row.querySelector(`[name="winner_id"]`).value 75 | }`, 76 | }); 77 | row.getElementsByClassName('form__key-loading')[0].classList.add('is-hidden'); 78 | row.getElementsByClassName('form__key-insert')[0].classList.remove('is-hidden'); 79 | row.getElementsByClassName('js__sent-text')[0].textContent = 'Sent Gift'; 80 | row.getElementsByClassName('js__sent-text')[1].textContent = 'Sent Gift'; 81 | } 82 | } 83 | 84 | const giveawaysDeleteKeyConfirmation = new GiveawaysDeleteKeyConfirmation(); 85 | 86 | export { giveawaysDeleteKeyConfirmation }; 87 | -------------------------------------------------------------------------------- /src/modules/Giveaways/EnteredGiveawaysStats.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class GiveawaysEnteredGiveawaysStats extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Allows you to see stats for your entered giveaways in the sidebar of the entered page. 14 |
  • 15 |
16 | ), 17 | features: { 18 | egs_e: { 19 | name: 'Include ended giveaways in the stats.', 20 | sgPaths: 'My Giveaways - Entered', 21 | sg: true, 22 | }, 23 | }, 24 | id: 'egs', 25 | name: 'Entered Giveaways Stats', 26 | sg: true, 27 | sgPaths: 'My Giveaways - Entered', 28 | type: 'giveaways', 29 | }; 30 | } 31 | 32 | init() { 33 | if (!this.esgst.enteredPath) { 34 | return; 35 | } 36 | common.createSidebarNavigation(this.esgst.sidebar, 'beforeend', { 37 | name: 'Entered Giveaways Stats', 38 | items: [ 39 | { 40 | id: 'egs_chance', 41 | name: 'Average Chance', 42 | count: 0, 43 | }, 44 | { 45 | id: 'egs_level', 46 | name: 'Average Level', 47 | count: 0, 48 | }, 49 | { 50 | id: 'egs_entries', 51 | name: 'Average Entries', 52 | count: 0, 53 | }, 54 | { 55 | id: 'egs_points', 56 | name: 'Average Points Spent', 57 | count: 0, 58 | }, 59 | { 60 | id: 'egs_simple_points', 61 | name: 'Total Points Spent', 62 | count: 0, 63 | }, 64 | ], 65 | }); 66 | const obj = { 67 | counters: { 68 | chance: 0.0, 69 | level: 0.0, 70 | entries: 0.0, 71 | points: 0, 72 | }, 73 | simpleCounters: { 74 | points: 0, 75 | }, 76 | elements: {}, 77 | total: 0, 78 | }; 79 | for (const key in obj.counters) { 80 | obj.elements[key] = document 81 | .querySelector(`#egs_${key}`) 82 | .querySelector('.sidebar__navigation__item__count'); 83 | } 84 | for (const key in obj.simpleCounters) { 85 | obj.elements[`simple_${key}`] = document 86 | .querySelector(`#egs_simple_${key}`) 87 | .querySelector('.sidebar__navigation__item__count'); 88 | } 89 | this.esgst.giveawayFeatures.push((giveaways, main) => this.addStats(obj, giveaways, main)); 90 | } 91 | 92 | addStats(obj, giveaways, main) { 93 | if (!main) { 94 | return; 95 | } 96 | for (const giveaway of giveaways) { 97 | if (!giveaway.ended || Settings.get('egs_e')) { 98 | for (const key in obj.counters) { 99 | obj.counters[key] += giveaway[key]; 100 | } 101 | for (const key in obj.simpleCounters) { 102 | obj.simpleCounters[key] += giveaway[key]; 103 | } 104 | obj.total += 1; 105 | } 106 | } 107 | for (const key in obj.counters) { 108 | obj.elements[key].textContent = common.round(obj.counters[key] / obj.total); 109 | } 110 | for (const key in obj.simpleCounters) { 111 | obj.elements[`simple_${key}`].textContent = obj.simpleCounters[key]; 112 | } 113 | } 114 | } 115 | 116 | const giveawaysEnteredGiveawaysStats = new GiveawaysEnteredGiveawaysStats(); 117 | 118 | export { giveawaysEnteredGiveawaysStats }; 119 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayCopyHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayCopyHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Highlights the number of copies next a giveaway's game name (in any page) by coloring it 13 | as red and changing the font to bold. 14 |
  • 15 |
16 | ), 17 | featureMap: { 18 | giveaway: this.highlight.bind(this), 19 | }, 20 | id: 'gch', 21 | name: 'Giveaway Copy Highlighter', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | highlight(giveaways) { 28 | for (const giveaway of giveaways) { 29 | if (!giveaway.copiesContainer) { 30 | continue; 31 | } 32 | const { color, bgColor } = Settings.get('gch_colors').filter( 33 | (colors) => 34 | giveaway.copies >= parseInt(colors.lower) && giveaway.copies <= parseInt(colors.upper) 35 | )[0] || { color: undefined, bgColor: undefined }; 36 | giveaway.copiesContainer.classList.add('esgst-bold'); 37 | if (!color) { 38 | giveaway.copiesContainer.classList.add('esgst-red'); 39 | continue; 40 | } 41 | giveaway.copiesContainer.style.color = color; 42 | if (!bgColor) { 43 | continue; 44 | } 45 | giveaway.copiesContainer.classList.add('esgst-gch-highlight'); 46 | giveaway.copiesContainer.style.backgroundColor = bgColor; 47 | } 48 | } 49 | } 50 | 51 | const giveawaysGiveawayCopyHighlighter = new GiveawaysGiveawayCopyHighlighter(); 52 | 53 | export { giveawaysGiveawayCopyHighlighter }; 54 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayEndTimeHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayEndTimeHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Allows you to highlight the end time of a giveaway (in any page) by coloring it based on 13 | how many hours there are left. 14 |
  • 15 |
16 | ), 17 | id: 'geth', 18 | name: 'Giveaway End Time Highlighter', 19 | sg: true, 20 | type: 'giveaways', 21 | featureMap: { 22 | giveaway: this.geth_getGiveaways.bind(this), 23 | }, 24 | }; 25 | } 26 | 27 | geth_getGiveaways(giveaways) { 28 | if (!Settings.get('geth_colors').length) { 29 | return; 30 | } 31 | 32 | for (const giveaway of giveaways) { 33 | if (!giveaway.started) { 34 | continue; 35 | } 36 | 37 | const hoursLeft = (giveaway.endTime - Date.now()) / 3600000; 38 | for (let i = Settings.get('geth_colors').length - 1; i > -1; i--) { 39 | const colors = Settings.get('geth_colors')[i]; 40 | if (hoursLeft >= parseFloat(colors.lower) && hoursLeft <= parseFloat(colors.upper)) { 41 | (giveaway.endTimeColumn_gv || giveaway.endTimeColumn).style.color = colors.color; 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | const giveawaysGiveawayEndTimeHighlighter = new GiveawaysGiveawayEndTimeHighlighter(); 50 | 51 | export { giveawaysGiveawayEndTimeHighlighter }; 52 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayErrorSearchLinks.tsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const getFeatureTooltip = common.getFeatureTooltip.bind(common); 6 | class GiveawaysGiveawayErrorSearchLinks extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | // by Revadike 11 | // eslint-disable-next-line react/display-name 12 | description: () => ( 13 |
    14 |
  • 15 | If you cannot access a giveaway because of many different reasons, a "Search 16 | Links" row is added to the table of the{' '} 17 | error page containing 4 links 18 | that allow you to search for the game elsewhere: 19 |
  • 20 |
      21 |
    • 22 | A SteamGifts icon that allows you to search for open giveaways of the game on 23 | SteamGifts. 24 |
    • 25 |
    • 26 | allows you to search for the game on Steam. 27 |
    • 28 |
    • 29 | 30 | 31 | {' '} 32 | allows you to search for the game on SteamDB. 33 |
    • 34 |
    • 35 | 36 | 37 | {' '} 38 | allows you to search for the game on Barter.vg. 39 |
    • 40 |
    41 |
42 | ), 43 | id: 'gesl', 44 | name: 'Giveaway Error Search Links', 45 | sg: true, 46 | type: 'giveaways', 47 | }; 48 | } 49 | 50 | init = () => { 51 | const table = document.getElementsByClassName('table--summary')[0]; 52 | if (!this.esgst.giveawayPath || !table) return; 53 | const name = encodeURIComponent( 54 | table.getElementsByClassName('table__column__secondary-link')[0].textContent 55 | ); 56 | DOM.insert( 57 | table.getElementsByClassName('table__row-outer-wrap')[0], 58 | 'afterend', 59 |
60 |
61 |
62 | Search Links 63 |
64 | 104 |
105 |
106 | ); 107 | }; 108 | } 109 | 110 | const giveawaysGiveawayErrorSearchLinks = new GiveawaysGiveawayErrorSearchLinks(); 111 | 112 | export { giveawaysGiveawayErrorSearchLinks }; 113 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayLevelHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Settings } from '../../class/Settings'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysGiveawayLevelHighlighter extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Highlights the level of a giveaway (in any page) by coloring it with the specified 13 | colors. 14 |
  • 15 |
16 | ), 17 | featureMap: { 18 | giveaway: this.highlight.bind(this), 19 | }, 20 | id: 'glh', 21 | name: 'Giveaway Level Highlighter', 22 | sg: true, 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | highlight(giveaways) { 28 | for (const giveaway of giveaways) { 29 | if (!giveaway.levelColumn) { 30 | continue; 31 | } 32 | const { color, bgColor } = Settings.get('glh_colors').filter( 33 | (colors) => 34 | giveaway.level >= parseInt(colors.lower) && giveaway.level <= parseInt(colors.upper) 35 | )[0] || { color: undefined, bgColor: undefined }; 36 | if (!color || !bgColor) { 37 | continue; 38 | } 39 | giveaway.levelColumn.setAttribute( 40 | 'style', 41 | `${color ? `color: ${color} !important;` : ''}${ 42 | bgColor ? `background-color: ${bgColor};` : '' 43 | }` 44 | ); 45 | giveaway.levelColumn.classList.add('esgst-glh-highlight'); 46 | } 47 | } 48 | } 49 | 50 | const giveawaysGiveawayLevelHighlighter = new GiveawaysGiveawayLevelHighlighter(); 51 | 52 | export { giveawaysGiveawayLevelHighlighter }; 53 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayPopup.jsx: -------------------------------------------------------------------------------- 1 | import { DOM } from '../../class/DOM'; 2 | import { Module } from '../../class/Module'; 3 | import { Button } from '../../components/Button'; 4 | import { common } from '../Common'; 5 | 6 | const getFeatureTooltip = common.getFeatureTooltip.bind(common); 7 | class GiveawaysGiveawayPopup extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • 14 | Adds a button ( ) below a giveaway's start time 15 | (in any page) that allows you to read the description of the giveaway and/or add a 16 | comment to it without having to access it. 17 |
  • 18 |
  • You can move the button around by dragging and dropping it.
  • 19 |
20 | ), 21 | id: 'gp', 22 | name: 'Giveaway Popup', 23 | sg: true, 24 | type: 'giveaways', 25 | featureMap: { 26 | giveaway: this.gp_addButton.bind(this), 27 | }, 28 | }; 29 | } 30 | 31 | gp_addButton(giveaways, main, source) { 32 | giveaways.forEach((giveaway) => { 33 | if ( 34 | giveaway.sgTools || 35 | (main && 36 | (this.esgst.createdPath || 37 | this.esgst.enteredPath || 38 | this.esgst.wonPath || 39 | this.esgst.giveawayPath || 40 | this.esgst.newGiveawayPath)) 41 | ) 42 | return; 43 | if ( 44 | !giveaway.innerWrap.getElementsByClassName('esgst-gp-button')[0] && 45 | (!giveaway.inviteOnly || giveaway.url) 46 | ) { 47 | let button; 48 | const onClick = () => { 49 | return new Promise((resolve) => { 50 | // noinspection JSIgnoredPromiseFromCall 51 | this.esgst.modules.giveawaysEnterLeaveGiveawayButton.elgb_openPopup( 52 | giveaway, 53 | main, 54 | source, 55 | (error) => { 56 | if (error) { 57 | button.build(3); 58 | } else { 59 | button.build(1); 60 | } 61 | resolve(); 62 | } 63 | ); 64 | }); 65 | }; 66 | button = Button.create({ 67 | additionalContainerClass: 'esgst-gp-button', 68 | states: [ 69 | { 70 | color: 'white', 71 | tooltip: getFeatureTooltip('gp', 'View giveaway description / add a comment'), 72 | icons: ['fa-external-link'], 73 | name: '', 74 | switchTo: { onReturn: 1 }, 75 | onClick, 76 | }, 77 | { 78 | template: 'loading', 79 | isDisabled: true, 80 | name: '', 81 | }, 82 | { 83 | template: 'error', 84 | tooltip: getFeatureTooltip('gp', 'Could not access giveaway'), 85 | name: '', 86 | switchTo: { onReturn: 3 }, 87 | onClick, 88 | }, 89 | { 90 | template: 'loading', 91 | isDisabled: true, 92 | name: '', 93 | }, 94 | ], 95 | }).insert(giveaway.panel, 'beforeend'); 96 | button.nodes.outer.setAttribute('data-draggable-id', 'gp'); 97 | } 98 | }); 99 | } 100 | } 101 | 102 | const giveawaysGiveawayPopup = new GiveawaysGiveawayPopup(); 103 | 104 | export { giveawaysGiveawayPopup }; 105 | -------------------------------------------------------------------------------- /src/modules/Giveaways/GiveawayWinnersLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysGiveawayWinnersLink extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Adds a link next to an ended giveaway's "Entries" link (in any page) that shows how many 14 | winners the giveaway has and takes you to the giveaway's{' '} 15 | winners page. 16 |
  • 17 |
18 | ), 19 | id: 'gwl', 20 | name: 'Giveaway Winners Link', 21 | sg: true, 22 | type: 'giveaways', 23 | featureMap: { 24 | giveaway: this.gwl_addLinks.bind(this), 25 | }, 26 | }; 27 | } 28 | 29 | gwl_addLinks(giveaways, main) { 30 | if ( 31 | ((!this.esgst.createdPath && 32 | !this.esgst.enteredPath && 33 | !this.esgst.wonPath && 34 | !this.esgst.giveawayPath && 35 | !this.esgst.archivePath) || 36 | main) && 37 | (this.esgst.giveawayPath || 38 | this.esgst.createdPath || 39 | this.esgst.enteredPath || 40 | this.esgst.wonPath || 41 | this.esgst.archivePath) 42 | ) 43 | return; 44 | giveaways.forEach((giveaway) => { 45 | if (giveaway.innerWrap.getElementsByClassName('esgst-gwl')[0] || !giveaway.ended) return; 46 | const attributes = { 47 | class: 'esgst-gwl', 48 | ['data-draggable-id']: 'winners_count', 49 | }; 50 | if (giveaway.url) { 51 | attributes.href = `${giveaway.url}/winners`; 52 | } 53 | createElements(giveaway.entriesLink, 'afterend', [ 54 | { 55 | attributes, 56 | type: 'a', 57 | children: [ 58 | { 59 | attributes: { 60 | class: 'fa fa-trophy', 61 | }, 62 | type: 'i', 63 | }, 64 | { 65 | text: `${giveaway.numWinners} winners`, 66 | type: 'span', 67 | }, 68 | ], 69 | }, 70 | ]); 71 | }); 72 | } 73 | } 74 | 75 | const giveawaysGiveawayWinnersLink = new GiveawaysGiveawayWinnersLink(); 76 | 77 | export { giveawaysGiveawayWinnersLink }; 78 | -------------------------------------------------------------------------------- /src/modules/Giveaways/HiddenGamesEnterButtonDisabler.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysHiddenGamesEnterButtonDisabler extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Disables the enter button of any giveaway if you have hidden the game on SteamGifts so 14 | that you do not accidentally enter it. 15 |
  • 16 |
17 | ), 18 | id: 'hgebd', 19 | name: "Hidden Game's Enter Button Disabler", 20 | sg: true, 21 | sync: 'Hidden Games', 22 | syncKeys: ['HiddenGames'], 23 | type: 'giveaways', 24 | }; 25 | } 26 | 27 | init() { 28 | if (!this.esgst.giveawayPath || document.getElementsByClassName('table--summary')[0]) { 29 | return; 30 | } 31 | const hideButton = document.getElementsByClassName('featured__giveaway__hide')[0]; 32 | if ( 33 | (this.esgst.enterGiveawayButton || 34 | (this.esgst.giveawayErrorButton && 35 | !this.esgst.giveawayErrorButton.textContent.match(/Exists\sin\sAccount/))) && 36 | !hideButton 37 | ) { 38 | const parent = (this.esgst.enterGiveawayButton || this.esgst.giveawayErrorButton) 39 | .parentElement; 40 | if (this.esgst.enterGiveawayButton) { 41 | this.esgst.enterGiveawayButton.remove(); 42 | } 43 | if (this.esgst.giveawayErrorButton) { 44 | this.esgst.giveawayErrorButton.remove(); 45 | } 46 | createElements(parent, 'afterbegin', [ 47 | { 48 | attributes: { 49 | class: 'sidebar__error is-disabled', 50 | }, 51 | type: 'div', 52 | children: [ 53 | { 54 | attributes: { 55 | class: 'fa fa-exclamation-circle', 56 | }, 57 | type: 'i', 58 | }, 59 | { 60 | text: ' Hidden Game', 61 | type: 'node', 62 | }, 63 | ], 64 | }, 65 | ]); 66 | } 67 | } 68 | } 69 | 70 | const giveawaysHiddenGamesEnterButtonDisabler = new GiveawaysHiddenGamesEnterButtonDisabler(); 71 | 72 | export { giveawaysHiddenGamesEnterButtonDisabler }; 73 | -------------------------------------------------------------------------------- /src/modules/Giveaways/NewGiveawayDescriptionChecker.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GiveawaysNewGiveawayDescriptionChecker extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | When you click on "Review Giveaway" in the new giveaway page (also extends to the 13 | "Create Giveaway" button from and the "Add" 14 | button from ), this feature checks if there are 15 | possible Steam keys / Humble Bundle gift links in the description and warns you about 16 | it, in case you pasted them there by mistake. 17 |
  • 18 |
  • 19 | This feature replaces the native "Review Giveaway" button, so that ESGST can intercept 20 | the click. 21 |
  • 22 |
23 | ), 24 | id: 'ngdc', 25 | name: 'New Giveaway Description Checker', 26 | sg: true, 27 | type: 'giveaways', 28 | }; 29 | } 30 | 31 | init() { 32 | if (!Shared.esgst.newGiveawayPath) { 33 | return; 34 | } 35 | 36 | const reviewButton = document.querySelector('.js__submit-form'); 37 | const textArea = document.querySelector(`[name=description]`); 38 | 39 | if (!reviewButton || !textArea) { 40 | return; 41 | } 42 | 43 | reviewButton.classList.remove('js__submit-form'); 44 | 45 | const newReviewButton = reviewButton.cloneNode(true); 46 | reviewButton.parentElement.insertBefore(newReviewButton, reviewButton); 47 | reviewButton.remove(); 48 | newReviewButton.setAttribute('data-esgst', 'reviewButton'); 49 | 50 | newReviewButton.addEventListener('click', async () => { 51 | if (await this.check(textArea.value)) { 52 | return; 53 | } 54 | 55 | const form = newReviewButton.closest('form'); 56 | if (newReviewButton.classList.contains('js__edit-giveaway')) { 57 | form.querySelector(`[name=next_step]`).value = 1; 58 | } 59 | form.submit(); 60 | }); 61 | } 62 | 63 | check(value) { 64 | return new Promise(async (resolve) => { 65 | let message; 66 | 67 | if (value.match(/[\d\w]{5}(-[\d\w]{5}){2,}/)) { 68 | message = 'There appears to be a Steam key in the description of the giveaway.'; 69 | } else if (value.match(/https?:\/\/(www\.)?humblebundle\.com\/gift/)) { 70 | message = 71 | 'There appears to be a Humble Bundle gift link in the description of the giveaway.'; 72 | } 73 | 74 | if (message) { 75 | message = `${message} Are you sure you want to continue?`; 76 | await Shared.common.createConfirmation( 77 | message, 78 | () => resolve(false), 79 | () => resolve(true) 80 | ); 81 | } else { 82 | resolve(false); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | const giveawaysNewGiveawayDescriptionChecker = new GiveawaysNewGiveawayDescriptionChecker(); 89 | 90 | export { giveawaysNewGiveawayDescriptionChecker }; 91 | -------------------------------------------------------------------------------- /src/modules/Giveaways/PinnedGiveawaysButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common); 6 | class GiveawaysPinnedGiveawaysButton extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Modifies the arrow button in the pinned giveaways box of the main page so that you are 14 | able to collapse the box again after expanding it. 15 |
  • 16 |
17 | ), 18 | id: 'pgb', 19 | name: 'Pinned Giveaways Button', 20 | sg: true, 21 | type: 'giveaways', 22 | }; 23 | } 24 | 25 | init() { 26 | let button = document.getElementsByClassName('pinned-giveaways__button')[0]; 27 | if (!button) return; 28 | const container = button.previousElementSibling; 29 | container.classList.add('esgst-pgb-container'); 30 | button.remove(); 31 | button = createElements(container, 'afterend', [ 32 | { 33 | attributes: { 34 | class: 'esgst-pgb-button', 35 | }, 36 | type: 'div', 37 | children: [ 38 | { 39 | attributes: { 40 | class: 'esgst-pgb-icon fa fa-angle-down', 41 | }, 42 | type: 'i', 43 | }, 44 | ], 45 | }, 46 | ]); 47 | const icon = button.firstElementChild; 48 | button.addEventListener('click', this.pgb_toggle.bind(this, container, icon)); 49 | } 50 | 51 | pgb_toggle(container, icon) { 52 | container.classList.toggle('pinned-giveaways__inner-wrap--minimized'); 53 | icon.classList.toggle('fa-angle-down'); 54 | icon.classList.toggle('fa-angle-up'); 55 | } 56 | } 57 | 58 | const giveawaysPinnedGiveawaysButton = new GiveawaysPinnedGiveawaysButton(); 59 | 60 | export { giveawaysPinnedGiveawaysButton }; 61 | -------------------------------------------------------------------------------- /src/modules/Giveaways/QuickGiveawaySearch.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { Shared } from '../../class/Shared'; 5 | import { DOM } from '../../class/DOM'; 6 | 7 | const createElements = common.createElements.bind(common), 8 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 9 | class GiveawaysQuickGiveawaySearch extends Module { 10 | constructor() { 11 | super(); 12 | this.info = { 13 | description: () => ( 14 |
    15 |
  • 16 | Adds a search box before the "Giveaways" box at the header of any page that allows you 17 | to quickly search for giveaways from any page. 18 |
  • 19 |
  • 20 | Has built-in. 21 |
  • 22 |
23 | ), 24 | features: { 25 | qgs_h: { 26 | name: 'Hide the native search on the main page.', 27 | sg: true, 28 | }, 29 | }, 30 | id: 'qgs', 31 | name: 'Quick Giveaway Search', 32 | options: { 33 | title: `Position:`, 34 | values: ['Left', 'Right'], 35 | }, 36 | sg: true, 37 | type: 'giveaways', 38 | }; 39 | } 40 | 41 | init() { 42 | let container = createElements( 43 | Shared.header.nodes.leftNav, 44 | Settings.get('qgs_index') === 0 ? 'afterbegin' : 'beforeend', 45 | [ 46 | { 47 | attributes: { 48 | class: 'esgst-qgs-container', 49 | title: getFeatureTooltip('qgs'), 50 | }, 51 | type: 'div', 52 | children: [ 53 | { 54 | attributes: { 55 | class: 'esgst-qgs-input', 56 | placeholder: 'Search...', 57 | type: 'text', 58 | }, 59 | type: 'input', 60 | }, 61 | { 62 | attributes: { 63 | class: 'fa fa-search', 64 | }, 65 | type: 'i', 66 | }, 67 | ], 68 | }, 69 | ] 70 | ); 71 | container.addEventListener('mouseenter', this.qgs_expand.bind(this)); 72 | container.addEventListener('mouseleave', this.qgs_collapse.bind(this)); 73 | container.firstElementChild.addEventListener('keypress', this.qgs_trigger.bind(this)); 74 | if (Settings.get('qgs_h') && this.esgst.giveawaysPath) { 75 | document.getElementsByClassName('sidebar__search-container')[0].remove(); 76 | } 77 | } 78 | 79 | qgs_expand(event) { 80 | event.currentTarget.classList.add('esgst-qgs-container-expanded'); 81 | } 82 | 83 | qgs_collapse(event) { 84 | if (event.relatedTarget && event.relatedTarget.closest('.esgst-popout')) return; 85 | event.currentTarget.classList.remove('esgst-qgs-container-expanded'); 86 | } 87 | 88 | qgs_trigger(event) { 89 | if (event.key !== 'Enter') return; 90 | event.preventDefault(); 91 | window.location.href = `/giveaways/search?q=${encodeURIComponent(event.currentTarget.value)}`; 92 | } 93 | } 94 | 95 | const giveawaysQuickGiveawaySearch = new GiveawaysQuickGiveawaySearch(); 96 | 97 | export { giveawaysQuickGiveawaySearch }; 98 | -------------------------------------------------------------------------------- /src/modules/Giveaways/UnfadedEnteredGiveaway.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class GiveawaysUnfadedEnteredGiveaway extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • Removes SteamGifts' default fade for entered giveaways.
  • 11 |
12 | ), 13 | id: 'ueg', 14 | name: 'Unfaded Entered Giveaway', 15 | sg: true, 16 | type: 'giveaways', 17 | featureMap: { 18 | endless: this.ueg_remove.bind(this), 19 | }, 20 | }; 21 | } 22 | 23 | ueg_remove(context, main, source, endless) { 24 | const elements = context.querySelectorAll( 25 | `${ 26 | endless 27 | ? `.esgst-es-page-${endless} .giveaway__row-inner-wrap.is-faded, .esgst-es-page-${endless}.giveaway__row-inner-wrap.is-faded` 28 | : '.giveaway__row-inner-wrap.is-faded' 29 | }` 30 | ); 31 | for (let i = 0, n = elements.length; i < n; ++i) { 32 | elements[i].classList.add('esgst-ueg'); 33 | } 34 | } 35 | } 36 | 37 | const giveawaysUnfadedEnteredGiveaway = new GiveawaysUnfadedEnteredGiveaway(); 38 | 39 | export { giveawaysUnfadedEnteredGiveaway }; 40 | -------------------------------------------------------------------------------- /src/modules/Giveaways/UnhideGiveawayButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | const createElements = common.createElements.bind(common), 6 | getFeatureTooltip = common.getFeatureTooltip.bind(common), 7 | unhideGame = common.unhideGame.bind(common); 8 | class GiveawaysUnhideGiveawayButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button () next to a giveaway's game name (in any 16 | page), if you have hidden the game on SteamGifts, that allows you to unhide the game 17 | without having to access your{' '} 18 | 19 | giveaway filters 20 | {' '} 21 | page. 22 |
  • 23 |
24 | ), 25 | id: 'ugb', 26 | name: 'Unhide Giveaway Button', 27 | sg: true, 28 | type: 'giveaways', 29 | featureMap: { 30 | giveaway: this.ugb_add.bind(this), 31 | }, 32 | }; 33 | } 34 | 35 | ugb_add(giveaways, main) { 36 | giveaways.forEach((giveaway) => { 37 | let hideButton = giveaway.innerWrap.querySelector( 38 | `.giveaway__hide, .featured__giveaway__hide` 39 | ); 40 | if (!hideButton && (!main || this.esgst.giveawaysPath || this.esgst.giveawayPath)) { 41 | if (this.esgst.giveawayPath && main) { 42 | hideButton = createElements(giveaway.headingName, 'afterend', [ 43 | { 44 | type: 'a', 45 | children: [ 46 | { 47 | attributes: { 48 | class: 'fa fa-eye giveaway__hide', 49 | title: getFeatureTooltip('ugb', 'Unhide all giveaways for this game'), 50 | }, 51 | type: 'i', 52 | }, 53 | ], 54 | }, 55 | ]); 56 | } else { 57 | hideButton = createElements(giveaway.headingName, 'afterend', [ 58 | { 59 | attributes: { 60 | class: 'fa fa-eye giveaway__hide giveaway__icon', 61 | title: getFeatureTooltip('ugb', 'Unhide all giveaways for this game'), 62 | }, 63 | type: 'i', 64 | }, 65 | ]); 66 | } 67 | hideButton.addEventListener( 68 | 'click', 69 | unhideGame.bind( 70 | common, 71 | hideButton, 72 | giveaway.gameId, 73 | giveaway.name, 74 | giveaway.id, 75 | giveaway.type 76 | ) 77 | ); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | const giveawaysUnhideGiveawayButton = new GiveawaysUnhideGiveawayButton(); 84 | 85 | export { giveawaysUnhideGiveawayButton }; 86 | -------------------------------------------------------------------------------- /src/modules/Giveaways_addToStorage.js: -------------------------------------------------------------------------------- 1 | import { Module } from '../class/Module'; 2 | import { common } from './Common'; 3 | import { Settings } from '../class/Settings'; 4 | 5 | const addGiveawayToStorage = common.addGiveawayToStorage.bind(common); 6 | class Giveaways_addToStorage extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | endless: true, 11 | id: 'giveaways_addToStorage', 12 | }; 13 | } 14 | 15 | init() { 16 | if ( 17 | (Settings.get('lpv') || 18 | Settings.get('cewgd') || 19 | (Settings.get('gc') && Settings.get('gc_gi'))) && 20 | this.esgst.giveawayPath && 21 | document.referrer === `https://www.steamgifts.com/giveaways/new` 22 | ) { 23 | addGiveawayToStorage(); 24 | } 25 | } 26 | } 27 | 28 | const giveaways_addToStorage = new Giveaways_addToStorage(); 29 | 30 | export { giveaways_addToStorage }; 31 | -------------------------------------------------------------------------------- /src/modules/Groups.js: -------------------------------------------------------------------------------- 1 | import { Module } from '../class/Module'; 2 | import { Scope } from '../class/Scope'; 3 | import { Settings } from '../class/Settings'; 4 | 5 | class Groups extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | endless: true, 10 | id: 'groups', 11 | featureMap: { 12 | endless: this.groups_load.bind(this), 13 | }, 14 | }; 15 | } 16 | 17 | async groups_load(context, main, source, endless) { 18 | const elements = context.querySelectorAll( 19 | `${ 20 | endless 21 | ? `.esgst-es-page-${endless} a[href*="/group/"], .esgst-es-page-${endless}a[href*="/group/"]` 22 | : `a[href*="/group/"]` 23 | }, .form_list_item_summary_name` 24 | ); 25 | if (!elements.length) { 26 | return; 27 | } 28 | const groups = []; 29 | for (const element of elements) { 30 | if (!element.textContent || element.children.length || element.closest('.markdown')) { 31 | continue; 32 | } 33 | const group = { 34 | saved: null, 35 | url: element.getAttribute('href'), 36 | }; 37 | if (group.url) { 38 | const match = group.url.match(/\/group\/(.+?)\//); 39 | if (match) { 40 | group.id = match[1]; 41 | group.saved = this.esgst.groups.filter((x) => x.code === group.id)[0]; 42 | } 43 | } 44 | if (!group.id) { 45 | const avatarImage = element.parentElement.previousElementSibling; 46 | const avatar = avatarImage.style.backgroundImage; 47 | group.saved = this.esgst.groups.filter((x) => avatar.match(x.avatar))[0]; 48 | group.id = group.saved && group.saved.code; 49 | } 50 | if (!group.id) { 51 | continue; 52 | } 53 | group.code = group.id; 54 | if (!this.esgst.currentGroups[group.id]) { 55 | this.esgst.currentGroups[group.id] = { 56 | elements: [], 57 | savedGroup: group.saved, 58 | }; 59 | } 60 | if (this.esgst.currentGroups[group.id].elements.indexOf(element) > -1) { 61 | continue; 62 | } 63 | group.name = element.textContent.trim(); 64 | const container = element.parentElement; 65 | group.oldElement = element; 66 | if (this.esgst.groupPath && container.classList.contains('page__heading__breadcrumbs')) { 67 | group.element = document.getElementsByClassName('featured__heading__medium')[0]; 68 | group.container = group.element.parentElement; 69 | } else { 70 | group.element = element; 71 | group.container = container; 72 | } 73 | group.context = group.element; 74 | this.esgst.currentGroups[group.id].elements.push(group.element); 75 | group.innerWrap = element.closest('.table__row-inner-wrap') || group.container; 76 | group.outerWrap = element.closest('.table__row-outer-wrap') || group.container; 77 | const isHeading = group.context.classList.contains('featured__heading__medium'); 78 | if (isHeading) { 79 | group.tagContext = group.container; 80 | group.tagPosition = 'beforeend'; 81 | } else { 82 | group.tagContext = group.context; 83 | group.tagPosition = 'afterend'; 84 | } 85 | groups.push(group); 86 | } 87 | Scope.addData('current', 'groups', groups, endless); 88 | if ( 89 | main && 90 | this.esgst.gpf && 91 | this.esgst.gpf.filteredCount && 92 | Settings.get(`gpf_enable${this.esgst.gpf.type}`) 93 | ) { 94 | this.esgst.modules.groupsGroupFilters.filters_filter(this.esgst.gpf, false, endless); 95 | } 96 | for (const feature of this.esgst.groupFeatures) { 97 | await feature(groups, main, source, endless); 98 | } 99 | } 100 | } 101 | 102 | const groupsModule = new Groups(); 103 | 104 | export { groupsModule }; 105 | -------------------------------------------------------------------------------- /src/modules/Groups/GroupHighlighter.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const getValue = common.getValue.bind(common); 7 | class GroupsGroupHighlighter extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Adds a green background to a group that you are a member of (in any page).
  • 14 |
15 | ), 16 | id: 'gh', 17 | name: 'Group Highlighter', 18 | sg: true, 19 | sync: 'Steam Groups', 20 | syncKeys: ['Groups'], 21 | type: 'groups', 22 | }; 23 | } 24 | 25 | init() { 26 | if (Shared.common.isCurrentPath('Steam - Groups')) return; 27 | Shared.esgst.endlessFeatures.push(this.gh_highlightGroups.bind(this)); 28 | } 29 | 30 | async gh_highlightGroups(context, main, source, endless) { 31 | const elements = context.querySelectorAll( 32 | `${ 33 | endless 34 | ? `.esgst-es-page-${endless} .table__column__heading[href*="/group/"], .esgst-es-page-${endless}.table__column__heading[href*="/group/"]` 35 | : `.table__column__heading[href*="/group/"]` 36 | }` 37 | ); 38 | if (!elements.length) return; 39 | const savedGroups = JSON.parse(getValue('groups', '[]')); 40 | for (let i = 0, n = elements.length; i < n; ++i) { 41 | const element = elements[i], 42 | code = element.getAttribute('href').match(/\/group\/(.+?)\//)[1]; 43 | let j; 44 | for (j = savedGroups.length - 1; j >= 0 && savedGroups[j].code !== code; --j) {} 45 | if (j >= 0 && savedGroups[j].member) { 46 | element.closest('.table__row-outer-wrap').classList.add('esgst-gh-highlight'); 47 | } 48 | } 49 | } 50 | } 51 | 52 | const groupsGroupHighlighter = new GroupsGroupHighlighter(); 53 | 54 | export { groupsGroupHighlighter }; 55 | -------------------------------------------------------------------------------- /src/modules/Groups/GroupTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class GroupsGroupTags extends Tags { 6 | constructor() { 7 | super('gpt'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next to a group's name (in any page) that 13 | allows you to save tags for the group (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
28 | ), 29 | features: { 30 | gpt_s: { 31 | name: 'Show tag suggestions while typing.', 32 | sg: true, 33 | st: true, 34 | }, 35 | }, 36 | id: 'gpt', 37 | name: 'Group Tags', 38 | sg: true, 39 | type: 'groups', 40 | }; 41 | } 42 | 43 | init() { 44 | Shared.esgst.groupFeatures.push(this.tags_addButtons.bind(this)); 45 | // noinspection JSIgnoredPromiseFromCall 46 | this.tags_getTags(); 47 | } 48 | } 49 | 50 | const groupsGroupTags = new GroupsGroupTags(); 51 | 52 | export { groupsGroupTags }; 53 | -------------------------------------------------------------------------------- /src/modules/Trades/HeaderTradesButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class TradesHeaderTradesButton extends Module { 6 | constructor() { 7 | super(); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • Brings back the Trades button to the SteamGifts header.
  • 12 |
13 | ), 14 | id: 'htb', 15 | name: 'Header Trades Button', 16 | sg: true, 17 | type: 'trades', 18 | }; 19 | } 20 | 21 | init() { 22 | const tradesButton = Shared.header.addButtonContainer({ 23 | buttonName: 'Trades', 24 | position: 'beforeend', 25 | openInNewTab: true, 26 | side: 'left', 27 | url: 'https://www.steamtrades.com', 28 | }); 29 | Shared.header.nodes.leftNav.insertBefore( 30 | tradesButton.nodes.outer, 31 | Shared.header.buttonContainers.discussions.nodes.outer 32 | ); 33 | } 34 | } 35 | 36 | const tradesHeaderTradesButton = new TradesHeaderTradesButton(); 37 | 38 | export { tradesHeaderTradesButton }; 39 | -------------------------------------------------------------------------------- /src/modules/Users/LevelUpCalculator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { Settings } from '../../class/Settings'; 5 | import { DOM } from '../../class/DOM'; 6 | 7 | class UsersLevelUpCalculator extends Module { 8 | constructor() { 9 | super(); 10 | this.info = { 11 | description: () => ( 12 |
    13 |
  • Shows how much real CV a user needs to level up in their profile page.
  • 14 |
  • 15 | Uses the values mentioned on{' '} 16 | this discussion for the 17 | calculation. 18 |
  • 19 |
20 | ), 21 | features: { 22 | luc_c: { 23 | name: 'Display current user level.', 24 | sg: true, 25 | }, 26 | }, 27 | id: 'luc', 28 | name: 'Level Up Calculator', 29 | sg: true, 30 | type: 'users', 31 | featureMap: { 32 | profile: this.luc_calculate.bind(this), 33 | }, 34 | }; 35 | } 36 | 37 | luc_calculate(profile) { 38 | for (const [index, value] of Shared.esgst.cvLevels.entries()) { 39 | const cvRounded = Math.round(profile.realSentCV); 40 | if (cvRounded < value) { 41 | DOM.insert( 42 | profile.levelRowRight, 43 | 'beforeend', 44 | 45 | {`(${Settings.get('luc_c') ? `${profile.level} / ` : ''}~$${Shared.common.round( 46 | value - cvRounded 47 | )} real CV to level ${index})`} 48 | 49 | ); 50 | break; 51 | } 52 | } 53 | } 54 | } 55 | 56 | const usersLevelUpCalculator = new UsersLevelUpCalculator(); 57 | 58 | export { usersLevelUpCalculator }; 59 | -------------------------------------------------------------------------------- /src/modules/Users/RealWonSentCVLink.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const createElements = common.createElements.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class UsersRealWonSentCVLink extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Turns "Gifts Won" and "Gifts Sent" in a user's{' '} 16 | profile page into links that take you 17 | to their real won/sent CV pages on SGTools. 18 |
  • 19 |
20 | ), 21 | features: { 22 | rwscvl_r: { 23 | name: `Link SGTools' reverse pages (from newest to oldest).`, 24 | sg: true, 25 | }, 26 | }, 27 | id: 'rwscvl', 28 | name: 'Real Won/Sent CV Link', 29 | sg: true, 30 | type: 'users', 31 | featureMap: { 32 | profile: this.rwscvl_add.bind(this), 33 | }, 34 | }; 35 | } 36 | 37 | rwscvl_add(profile) { 38 | let sentUrl, wonUrl; 39 | wonUrl = `http://www.sgtools.info/won/${profile.username}`; 40 | sentUrl = `http://www.sgtools.info/sent/${profile.username}`; 41 | if (Settings.get('rwscvl_r')) { 42 | wonUrl += '/newestfirst'; 43 | sentUrl += '/newestfirst'; 44 | } 45 | createElements(profile.wonRowLeft, 'atinner', [ 46 | { 47 | attributes: { 48 | class: 'esgst-rwscvl-link', 49 | href: wonUrl, 50 | target: '_blank', 51 | title: getFeatureTooltip('rwscvl'), 52 | }, 53 | text: 'Gifts Won', 54 | type: 'a', 55 | }, 56 | ]); 57 | createElements(profile.sentRowLeft, 'atinner', [ 58 | { 59 | attributes: { 60 | class: 'esgst-rwscvl-link', 61 | href: sentUrl, 62 | target: '_blank', 63 | title: getFeatureTooltip('rwscvl'), 64 | }, 65 | text: 'Gifts Sent', 66 | type: 'a', 67 | }, 68 | ]); 69 | } 70 | } 71 | 72 | const usersRealWonSentCVLink = new UsersRealWonSentCVLink(); 73 | 74 | export { usersRealWonSentCVLink }; 75 | -------------------------------------------------------------------------------- /src/modules/Users/SteamFriendsIndicator.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { Shared } from '../../class/Shared'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersSteamFriendsIndicator extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Adds an icon () next to the a user's username (in any 14 | page) to indicate that they are on your Steam friends list. 15 |
  • 16 |
  • If you hover over the icon, it shows the date when you became friends.
  • 17 |
18 | ), 19 | id: 'sfi', 20 | inputItems: [ 21 | { 22 | id: 'sfi_icon', 23 | prefix: `Icon: `, 24 | }, 25 | ], 26 | name: 'Steam Friends Indicator', 27 | sg: true, 28 | st: true, 29 | sync: 'Steam Friends', 30 | syncKeys: ['SteamFriends'], 31 | type: 'users', 32 | featureMap: { 33 | user: this.addIcons.bind(this), 34 | }, 35 | }; 36 | } 37 | 38 | addIcons(users) { 39 | for (const user of users) { 40 | if ( 41 | user.saved && 42 | user.saved.steamFriend && 43 | !user.context.parentElement.querySelector('.esgst-sfi-icon') 44 | ) { 45 | DOM.insert( 46 | user.context, 47 | 'afterend', 48 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | 65 | const usersSteamFriendsIndicator = new UsersSteamFriendsIndicator(); 66 | 67 | export { usersSteamFriendsIndicator }; 68 | -------------------------------------------------------------------------------- /src/modules/Users/SteamGiftsProfileButton.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Shared } from '../../class/Shared'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | const createElements = common.createElements.bind(common), 7 | getFeatureTooltip = common.getFeatureTooltip.bind(common); 8 | class UsersSteamGiftsProfileButton extends Module { 9 | constructor() { 10 | super(); 11 | this.info = { 12 | description: () => ( 13 |
    14 |
  • 15 | Adds a button next to the "Visit Steam Profile" button of a user's{' '} 16 | profile page that 17 | allows you to go to their SteamGifts profile page. 18 |
  • 19 |
20 | ), 21 | id: 'sgpb', 22 | name: 'SteamGifts Profile Button', 23 | st: true, 24 | type: 'users', 25 | }; 26 | } 27 | 28 | init() { 29 | if (!Shared.esgst.userPath) return; 30 | Shared.esgst.profileFeatures.push(this.sgpb_add.bind(this)); 31 | } 32 | 33 | sgpb_add(profile) { 34 | let button; 35 | button = createElements(profile.steamButtonContainer, 'beforeend', [ 36 | { 37 | attributes: { 38 | class: 'esgst-sgpb-container', 39 | title: getFeatureTooltip('sgpb'), 40 | }, 41 | type: 'div', 42 | children: [ 43 | { 44 | attributes: { 45 | class: 'esgst-sgpb-button', 46 | href: `https://www.steamgifts.com/go/user/${profile.steamId}`, 47 | rel: 'nofollow', 48 | target: '_blank', 49 | }, 50 | type: 'a', 51 | children: [ 52 | { 53 | attributes: { 54 | class: 'fa', 55 | }, 56 | type: 'i', 57 | children: [ 58 | { 59 | attributes: { 60 | src: Shared.esgst.sgIcon, 61 | }, 62 | type: 'img', 63 | }, 64 | ], 65 | }, 66 | { 67 | text: 'Visit SteamGifts Profile', 68 | type: 'span', 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, 74 | ]); 75 | button.insertBefore(profile.steamButton, button.firstElementChild); 76 | } 77 | } 78 | 79 | const usersSteamGiftsProfileButton = new UsersSteamGiftsProfileButton(); 80 | 81 | export { usersSteamGiftsProfileButton }; 82 | -------------------------------------------------------------------------------- /src/modules/Users/UserLinks.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersUserLinks extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • Allows you to add custom links next to a user's username in their profile page.
  • 13 |
  • 14 | Can be used in other pages through . 15 |
  • 16 |
  • 17 | Comes by default with 5 links to BLAEO, Playing Appreciated, Touhou Giveaways, AStats 18 | and SteamRep. 19 |
  • 20 |
21 | ), 22 | id: 'ul', 23 | name: 'User Links', 24 | sg: true, 25 | type: 'users', 26 | featureMap: { 27 | profile: this.ul_add.bind(this), 28 | }, 29 | }; 30 | } 31 | 32 | ul_add(profile) { 33 | const items = []; 34 | const iconRegex = /^(fa-.+?)($|\s)/; 35 | const imageRegex = /^(https?:\/\/.+?)($|\s)/; 36 | const textRegex = /^(.+?)($|\s(fa-|https?:\/\/))/; 37 | for (const link of Settings.get('ul_links')) { 38 | const children = []; 39 | let label = link.label; 40 | while (label) { 41 | const icon = label.match(iconRegex); 42 | if (icon) { 43 | label = label.replace(iconRegex, ''); 44 | children.push(); 45 | continue; 46 | } 47 | const image = label.match(imageRegex); 48 | if (image) { 49 | label = label.replace(imageRegex, ''); 50 | children.push( 51 | 52 | ); 53 | continue; 54 | } 55 | const text = label.match(textRegex); 56 | if (text) { 57 | label = label.replace(textRegex, `$3`); 58 | children.push(text[1]); 59 | } 60 | } 61 | items.push( 62 | 68 | {children} 69 | 70 | ); 71 | } 72 | DOM.insert(profile.heading, 'beforeend', {items}); 73 | } 74 | } 75 | 76 | const usersUserLinks = new UsersUserLinks(); 77 | 78 | export { usersUserLinks }; 79 | -------------------------------------------------------------------------------- /src/modules/Users/UserTags.jsx: -------------------------------------------------------------------------------- 1 | import { Tags } from '../Tags'; 2 | import { Shared } from '../../class/Shared'; 3 | import { DOM } from '../../class/DOM'; 4 | 5 | class UsersUserTags extends Tags { 6 | constructor() { 7 | super('ut'); 8 | this.info = { 9 | description: () => ( 10 |
    11 |
  • 12 | Adds a button () next a user's username (in any page) that 13 | allows you to save tags for the user (only visible to you). 14 |
  • 15 |
  • You can press Enter to save the tags.
  • 16 |
  • Each tag can be colored individually.
  • 17 |
  • 18 | There is a button () in the tags popup that allows you to 19 | view a list with all of the tags that you have used ordered from most used to least 20 | used. 21 |
  • 22 |
  • 23 | Adds a button ( ) to the 24 | page heading of this menu that allows you to manage all of the tags that have been 25 | saved. 26 |
  • 27 |
  • 28 | This feature is recommended for cases where you want to associate a short text with a 29 | user, since the tags are displayed next to their username.For a long text, check 30 | . 31 |
  • 32 |
33 | ), 34 | features: { 35 | ut_s: { 36 | name: 'Show tag suggestions while typing.', 37 | sg: true, 38 | st: true, 39 | }, 40 | }, 41 | id: 'ut', 42 | name: 'User Tags', 43 | sg: true, 44 | st: true, 45 | type: 'users', 46 | }; 47 | } 48 | 49 | init() { 50 | Shared.esgst.userFeatures.push(this.tags_addButtons.bind(this)); 51 | // noinspection JSIgnoredPromiseFromCall 52 | this.tags_getTags(); 53 | } 54 | } 55 | 56 | const usersUserTags = new UsersUserTags(); 57 | 58 | export { usersUserTags }; 59 | -------------------------------------------------------------------------------- /src/modules/Users/VisibleGiftsBreakdown.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { common } from '../Common'; 3 | import { Settings } from '../../class/Settings'; 4 | import { DOM } from '../../class/DOM'; 5 | 6 | class UsersVisibleGiftsBreakdown extends Module { 7 | constructor() { 8 | super(); 9 | this.info = { 10 | description: () => ( 11 |
    12 |
  • 13 | Shows the gifts breakdown of a user in their profile page, with the following initials: 14 |
  • 15 |
      16 |
    • FCV - Full CV
    • 17 |
    • RCV - Reduced CV
    • 18 |
    • NCV - No CV
    • 19 |
    • A - Awaiting Feedback
    • 20 |
    • NR - Not Received
    • 21 |
    22 |
23 | ), 24 | id: 'vgb', 25 | inputItems: [ 26 | { 27 | id: 'vgb_wonFormat', 28 | prefix: `Won Format: `, 29 | tooltip: `[FCV], [RCV], [NCV] and [NR] will be replaced with their respective values.`, 30 | }, 31 | { 32 | id: 'vgb_sentFormat', 33 | prefix: `Sent Format: `, 34 | tooltip: `[FCV], [RCV], [NCV], [A] and [NR] will be replaced with their respective values.`, 35 | }, 36 | ], 37 | name: 'Visible Gifts Breakdown', 38 | options: { 39 | title: `Position: `, 40 | values: ['Left', 'Right'], 41 | }, 42 | sg: true, 43 | type: 'users', 44 | featureMap: { 45 | profile: this.vgb_add.bind(this), 46 | }, 47 | }; 48 | } 49 | 50 | vgb_add(profile) { 51 | const position = Settings.get('vgb_index') === 0 ? 'afterbegin' : 'beforeend'; 52 | DOM.insert( 53 | profile.wonRowRight.firstElementChild.firstElementChild, 54 | position, 55 | {` ${Settings.get('vgb_wonFormat') 56 | .replace(/\[FCV]/, profile.wonFull) 57 | .replace(/\[RCV]/, profile.wonReduced) 58 | .replace(/\[NCV]/, profile.wonZero) 59 | .replace(/\[NR]/, profile.wonNotReceived)} `} 60 | ); 61 | DOM.insert( 62 | profile.sentRowRight.firstElementChild.firstElementChild, 63 | position, 64 | {` ${Settings.get('vgb_sentFormat') 65 | .replace(/\[FCV]/, profile.sentFull) 66 | .replace(/\[RCV]/, profile.sentReduced) 67 | .replace(/\[NCV]/, profile.sentZero) 68 | .replace(/\[A]/, profile.sentAwaiting) 69 | .replace(/\[NR]/, profile.sentNotReceived)} `} 70 | ); 71 | } 72 | } 73 | 74 | const usersVisibleGiftsBreakdown = new UsersVisibleGiftsBreakdown(); 75 | 76 | export { usersVisibleGiftsBreakdown }; 77 | -------------------------------------------------------------------------------- /src/modules/Users/VisibleRealCV.jsx: -------------------------------------------------------------------------------- 1 | import { Module } from '../../class/Module'; 2 | import { DOM } from '../../class/DOM'; 3 | 4 | class UsersVisibleRealCV extends Module { 5 | constructor() { 6 | super(); 7 | this.info = { 8 | description: () => ( 9 |
    10 |
  • 11 | Displays the real sent/won CV next to the raw value in a user's{' '} 12 | profile page. 13 |
  • 14 |
  • 15 | This also extends to , if you have that feature 16 | enabled. 17 |
  • 18 |
  • 19 | With this feature disabled, you can still view the real CV, as provided by SteamGifts, 20 | by hovering over the raw value. 21 |
  • 22 |
23 | ), 24 | id: 'vrcv', 25 | name: 'Visible Real CV', 26 | sg: true, 27 | type: 'users', 28 | featureMap: { 29 | profile: this.vrcv_add.bind(this), 30 | }, 31 | }; 32 | } 33 | 34 | vrcv_add(profile) { 35 | /** 36 | * @property realSentCV.toLocaleString 37 | * @property realWonCV.toLocaleString 38 | */ 39 | profile.sentCvContainer.insertAdjacentText( 40 | 'beforeend', 41 | ` / $${profile.realSentCV.toLocaleString('en')}` 42 | ); 43 | profile.wonCvContainer.insertAdjacentText( 44 | 'beforeend', 45 | ` / $${profile.realWonCV.toLocaleString('en')}` 46 | ); 47 | } 48 | } 49 | 50 | const usersVisibleRealCV = new UsersVisibleRealCV(); 51 | 52 | export { usersVisibleRealCV }; 53 | -------------------------------------------------------------------------------- /src/permissions.tsx: -------------------------------------------------------------------------------- 1 | import { DOM } from './class/DOM'; 2 | import { permissions } from './class/Permissions'; 3 | import { Utils } from './lib/jsUtils'; 4 | 5 | const grantedPermissions = new Set(); 6 | const deniedPermissions = new Set(); 7 | let messageNode: HTMLElement | undefined; 8 | 9 | const loadPermissions = async (): Promise => { 10 | const params = Utils.getQueryParams(); 11 | const rows = []; 12 | const keys = params.keys ? params.keys.split(',') : Object.keys(permissions.permissions); 13 | for (const key of keys) { 14 | const permission = permissions.permissions[key]; 15 | if (!permission) { 16 | continue; 17 | } 18 | const permissionCell = permission.values.map((value) => [value,
]).flat(); 19 | const usageCell = Object.values(permission.messages) 20 | .map((value) => [value,
,
]) 21 | .flat(); 22 | let checkboxNode: HTMLInputElement | undefined; 23 | rows.push( 24 | 25 | {params.keys ? null : ( 26 | 27 | (checkboxNode = ref)} /> 28 | 29 | )} 30 | {permissionCell} 31 | {usageCell} 32 | 33 | ); 34 | if (params.keys) { 35 | grantedPermissions.add(key); 36 | } 37 | if (!checkboxNode) { 38 | continue; 39 | } 40 | checkboxNode.checked = await permissions.contains([[key]]); 41 | checkboxNode.addEventListener('change', () => { 42 | if (checkboxNode?.checked) { 43 | grantedPermissions.add(key); 44 | deniedPermissions.delete(key); 45 | } else { 46 | grantedPermissions.delete(key); 47 | deniedPermissions.add(key); 48 | } 49 | }); 50 | } 51 | DOM.insert( 52 | document.body, 53 | 'beforeend', 54 | 55 | 56 | 57 | {params.keys ? null : } 58 | 59 | 60 | 61 | {rows} 62 |
GrantedPermissionUsage
63 | 66 |
(messageNode = ref)}>
67 |
68 | ); 69 | }; 70 | 71 | const savePermissions = () => { 72 | permissions.request(Array.from(grantedPermissions), (granted: boolean) => { 73 | permissions.remove(Array.from(deniedPermissions), (denied: boolean) => { 74 | if (!messageNode) { 75 | return; 76 | } 77 | if (granted && denied) { 78 | messageNode.textContent = 'Permissions saved!'; 79 | } else { 80 | messageNode.textContent = 'Error saving permissions!'; 81 | } 82 | window.setTimeout(() => { 83 | if (messageNode) { 84 | messageNode.textContent = ''; 85 | } 86 | }, 2000); 87 | }); 88 | }); 89 | }; 90 | 91 | loadPermissions(); 92 | -------------------------------------------------------------------------------- /src/types/Footer.type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} IFooterNodes 3 | * @property {HTMLElement} inner 4 | * @property {HTMLElement} leftNav 5 | * @property {HTMLElement} nav 6 | * @property {HTMLElement} outer 7 | * @property {HTMLElement} rightNav 8 | */ 9 | 10 | /** 11 | * @typedef {Object} IFooterLinkContainer 12 | * @property {Object} nodes 13 | * @property {HTMLElement} [nodes.icon] 14 | * @property {HTMLElement} [nodes.link] 15 | * @property {HTMLElement} nodes.outer 16 | * @property {Object} data 17 | * @property {string} data.id 18 | * @property {string} [data.icon] 19 | * @property {string} data.name 20 | * @property {string} [data.url] 21 | */ 22 | 23 | /** 24 | * @typedef {Object} IFooterLinkContainerParams 25 | * @property {HTMLElement} [context] 26 | * @property {string} [icon] 27 | * @property {string} name 28 | * @property {string} [position] 29 | * @property {'left' | 'right'} [side] 30 | * @property {string} [url] 31 | */ 32 | -------------------------------------------------------------------------------- /src/types/Header.type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} IHeaderNodes 3 | * @property {HTMLElement} inner 4 | * @property {HTMLElement} leftNav 5 | * @property {HTMLElement} [logo] 6 | * @property {HTMLElement} nav 7 | * @property {HTMLElement} outer 8 | * @property {HTMLElement} rightNav 9 | */ 10 | 11 | /** 12 | * @typedef {Object} IHeaderButtonContainer 13 | * @property {Object} nodes 14 | * @property {HTMLElement} [nodes.absoluteDropdown] 15 | * @property {HTMLElement} [nodes.arrow] 16 | * @property {HTMLElement} [nodes.arrowIcon] 17 | * @property {HTMLElement} [nodes.button] 18 | * @property {HTMLElement} [nodes.buttonIcon] 19 | * @property {HTMLElement} [nodes.buttonImage] 20 | * @property {HTMLElement} [nodes.buttonName] 21 | * @property {HTMLElement} [nodes.counter] 22 | * @property {HTMLElement} [nodes.level] 23 | * @property {HTMLElement} nodes.outer 24 | * @property {HTMLElement} [nodes.points] 25 | * @property {HTMLElement} [nodes.relativeDropdown] 26 | * @property {HTMLElement} [nodes.reputation] 27 | * @property {Object} data 28 | * @property {string} data.id 29 | * @property {string} [data.buttonIcon] 30 | * @property {string} [data.buttonImage] 31 | * @property {string} data.buttonName 32 | * @property {number} data.counter 33 | * @property {boolean} [data.isActive] 34 | * @property {boolean} [data.isDropdown] 35 | * @property {boolean} data.isFlashing 36 | * @property {boolean} [data.isNotification] 37 | * @property {ILevel} data.level 38 | * @property {number} data.points 39 | * @property {IReputation} data.reputation 40 | * @property {string} [data.url] 41 | * @property {Object} [dropdownItems] 42 | * @property {import('../models/User').User} [user] 43 | */ 44 | 45 | /** 46 | * @typedef {Object} IHeaderButtonContainerParams 47 | * @property {string} [buttonIcon] 48 | * @property {string} [buttonImage] 49 | * @property {string} buttonName 50 | * @property {string} [context] 51 | * @property {number} [counter] 52 | * @property {boolean} [isActive] 53 | * @property {boolean} [isDropdown] 54 | * @property {boolean} [isFlashing] 55 | * @property {boolean} [isNotification] 56 | * @property {Function} [onClick] 57 | * @property {boolean} [openInNewTab] 58 | * @property {string} [position] 59 | * @property {'left' | 'right'} [side] 60 | * @property {string} [url] 61 | * @property {Object} [dropdownItems] 62 | */ 63 | 64 | /** 65 | * @typedef {Object} IHeaderDropdownItem 66 | * @property {Object} nodes 67 | * @property {HTMLElement} [nodes.description] 68 | * @property {HTMLElement} nodes.icon 69 | * @property {HTMLElement} nodes.name 70 | * @property {HTMLElement} nodes.outer 71 | * @property {HTMLElement} [nodes.summary] 72 | * @property {Object} data 73 | * @property {string} data.id 74 | * @property {string} [data.description] 75 | * @property {string} data.icon 76 | * @property {string} data.name 77 | * @property {string} data.url 78 | */ 79 | 80 | /** 81 | * @typedef {Object} IHeaderDropdownItemParams 82 | * @property {string} [buttonContainerId] 83 | * @property {string} [description] 84 | * @property {string} icon 85 | * @property {string} name 86 | * @property {Function} [onClick] 87 | * @property {boolean} [openInNewTab] 88 | * @property {string} url 89 | */ 90 | -------------------------------------------------------------------------------- /src/types/HeaderRefresher.type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} IHeaderRefresherCache 3 | * @property {number} created 4 | * @property {ILevel} level 5 | * @property {number} messages 6 | * @property {number} newWishlist 7 | * @property {number} points 8 | * @property {number} timestamp 9 | * @property {string} username 10 | * @property {number} wishlist 11 | * @property {number} won 12 | * @property {boolean} wonDelivered 13 | */ 14 | -------------------------------------------------------------------------------- /src/types/Session.type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ISessionCounters 3 | * @property {number} created 4 | * @property {ILevel} level 5 | * @property {number} messages 6 | * @property {number} points 7 | * @property {IReputation} reputation 8 | * @property {number} won 9 | * @property {boolean} wonDelivered 10 | */ 11 | -------------------------------------------------------------------------------- /src/types/common.type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ILevel 3 | * @property {number} base 4 | * @property {number} full 5 | */ 6 | 7 | /** 8 | * @typedef {Object} IReputation 9 | * @property {number} positive 10 | * @property {number} negative 11 | */ 12 | -------------------------------------------------------------------------------- /test-helpers/fixture-loader.ts: -------------------------------------------------------------------------------- 1 | const loadFixture = (fixture: string): Element => { 2 | document.body.insertAdjacentHTML('afterbegin', '
'); 3 | const fixtureEl = document.body.children[0]; 4 | fixtureEl.innerHTML = fixture; 5 | return fixtureEl; 6 | }; 7 | 8 | export { loadFixture }; 9 | -------------------------------------------------------------------------------- /test/fixtures/sg/notification-bar.html: -------------------------------------------------------------------------------- 1 |
Success. Synced with Steam.
2 | -------------------------------------------------------------------------------- /test/fixtures/st/notification-bar.html: -------------------------------------------------------------------------------- 1 |
Thanks for helping! It looks like you voted on 1 review.
2 | -------------------------------------------------------------------------------- /test/modules/General/FixedHeader.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Shared } from '../../../src/class/Shared'; 3 | import { Header, IHeader } from '../../../src/components/Header'; 4 | import { Namespaces } from '../../../src/constants/Namespaces'; 5 | import { generalFixedHeader } from '../../../src/modules/General/FixedHeader'; 6 | import { loadFixture } from '../../../test-helpers/fixture-loader'; 7 | import sgHeaderFixture from '../../fixtures/sg/header.html'; 8 | import stHeaderFixture from '../../fixtures/st/header.html'; 9 | 10 | let fixtureEl: Element; 11 | 12 | describe('Fixed Header', () => { 13 | describe('on SG', () => { 14 | describe('when there is a header', () => { 15 | before(() => { 16 | fixtureEl = loadFixture(sgHeaderFixture); 17 | Shared.header = Header(Namespaces.SG); 18 | Shared.header.parse(document.body); 19 | }); 20 | 21 | after(() => { 22 | fixtureEl.remove(); 23 | }); 24 | 25 | it('should load successfully', () => { 26 | expect(Shared.header).to.be.instanceOf(IHeader); 27 | generalFixedHeader.init(); 28 | expect(Array.from(Shared.header.nodes.outer.classList)).to.include('esgst-fh'); 29 | }); 30 | }); 31 | 32 | describe('when there is no header', () => { 33 | before(() => { 34 | Shared.header = Header(Namespaces.SG); 35 | }); 36 | 37 | after(() => { 38 | fixtureEl.remove(); 39 | }); 40 | 41 | it('should silently fail to load', () => { 42 | expect(Shared.header.nodes.outer).to.be.null; 43 | generalFixedHeader.init(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('on ST', () => { 49 | describe('when there is a header', () => { 50 | before(() => { 51 | fixtureEl = loadFixture(stHeaderFixture); 52 | Shared.header = Header(Namespaces.ST); 53 | Shared.header.parse(document.body); 54 | }); 55 | 56 | after(() => { 57 | fixtureEl.remove(); 58 | }); 59 | 60 | it('should load successfully', () => { 61 | expect(Shared.header).to.be.instanceOf(IHeader); 62 | generalFixedHeader.init(); 63 | expect(Array.from(Shared.header.nodes.outer.classList)).to.include('esgst-fh'); 64 | }); 65 | }); 66 | 67 | describe('when there is no header', () => { 68 | before(() => { 69 | Shared.header = Header(Namespaces.SG); 70 | }); 71 | 72 | after(() => { 73 | fixtureEl.remove(); 74 | }); 75 | 76 | it('should silently fail to load', () => { 77 | expect(Shared.header.nodes.outer).to.be.null; 78 | generalFixedHeader.init(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./node_modules", "./build", "./dist"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "jsx": "react", 7 | "jsxFactory": "DOM.element", 8 | "module": "CommonJS", 9 | "outDir": "./build", 10 | "sourceMap": true, 11 | "target": "ES2020", 12 | "strict": true, 13 | "typeRoots": ["./node_modules/@types", "./node_modules/web-ext-types"], 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------