├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── crowdin-push.yml │ └── crowdin-upload.yml ├── .gitignore ├── .stylelintrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── _locales ├── ar │ └── messages.json ├── bg │ └── messages.json ├── bn │ └── messages.json ├── ca │ └── messages.json ├── cs │ └── messages.json ├── da │ └── messages.json ├── de │ └── messages.json ├── el │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── es_419 │ └── messages.json ├── et │ └── messages.json ├── fa │ └── messages.json ├── fi │ └── messages.json ├── fil │ └── messages.json ├── fr │ └── messages.json ├── he │ └── messages.json ├── hi │ └── messages.json ├── hr │ └── messages.json ├── hu │ └── messages.json ├── id │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ka │ └── messages.json ├── ko │ └── messages.json ├── lt │ └── messages.json ├── lv │ └── messages.json ├── ms │ └── messages.json ├── nl │ └── messages.json ├── no │ └── messages.json ├── pl │ └── messages.json ├── pt_BR │ └── messages.json ├── pt_PT │ └── messages.json ├── ro │ └── messages.json ├── ru │ └── messages.json ├── sk │ └── messages.json ├── sl │ └── messages.json ├── sr │ └── messages.json ├── sv │ └── messages.json ├── th │ └── messages.json ├── tr │ └── messages.json ├── uk │ └── messages.json ├── vi │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── assets ├── 0_app.png ├── 1_sales.png ├── 2_settings.png ├── 3_settings.png ├── 4_inventory.png ├── 5_achievements.png ├── StoreReadme.txt └── steamdb-extension-promo.afphoto ├── build.js ├── crowdin-readme.yml ├── crowdin.yml ├── eslint.config.mjs ├── icons ├── 128.png ├── 16.png ├── 32.png ├── 48.png ├── 64.png ├── achievements_completed.svg ├── image.svg ├── pcgamingwiki.svg ├── steamhunters.svg └── white.svg ├── manifest.json ├── options ├── options.css ├── options.html ├── options.js ├── popup.css ├── popup.html └── popup.js ├── package-lock.json ├── package.json ├── scripts ├── appicon.js ├── background.js ├── common.d.ts ├── common.js ├── community │ ├── achievements.d.ts │ ├── achievements.js │ ├── achievements_cs2.js │ ├── achievements_global.js │ ├── achievements_profile.js │ ├── agecheck.js │ ├── agecheck_injected.js │ ├── filedetails.js │ ├── filedetails_award_injected.js │ ├── filedetails_guide.js │ ├── gamehub.js │ ├── inventory.js │ ├── market.js │ ├── market_injected.js │ ├── market_ssa.js │ ├── multibuy.js │ ├── multibuy_injected.js │ ├── profile.js │ ├── profile_award_injected.js │ ├── profile_badges.js │ ├── profile_gamecards.js │ ├── profile_inventory.js │ ├── profile_recommended.js │ ├── tradeoffer.js │ └── tradeoffer_injected.js ├── global.js ├── steamdb │ └── global.js ├── store │ ├── account_licenses.js │ ├── account_licenses_injected.js │ ├── agecheck.js │ ├── app.js │ ├── app_error.d.ts │ ├── app_error.js │ ├── app_images.js │ ├── app_news.js │ ├── bundle.js │ ├── explore.js │ ├── invalidate_cache.js │ ├── invalidate_cache_injected.js │ ├── registerkey.js │ ├── registerkey_injected.js │ ├── sub.js │ ├── subscriptions.js │ └── widget.js └── window.d.ts ├── styles ├── account_licenses.css ├── achievements.css ├── achievements_cs2.css ├── appicon.css ├── community.css ├── global.css ├── inventory-sidebar.css ├── inventory.css ├── market.css └── store.css ├── tsconfig.json └── version.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [*.yml] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Describe the bug 8 | description: A clear and concise description of what the bug is and what page it happens on. 9 | validations: 10 | required: true 11 | 12 | - type: input 13 | attributes: 14 | label: Browser name and version 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Discord 3 | about: Join Discord to chat 4 | url: https://steamdb.info/discord/ 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: [enhancement] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Is your feature request related to a problem? Please describe. 8 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like 15 | description: A clear and concise description of what you want to happen. 16 | 17 | - type: textarea 18 | attributes: 19 | label: Describe alternatives you've considered 20 | description: A clear and concise description of any alternative solutions or features you've considered. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | ignore: 8 | - dependency-name: "*" 9 | update-types: ["version-update:semver-minor", "version-update:semver-patch"] 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | name: CI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | 18 | - name: Install 19 | run: npm install 20 | 21 | - name: Test 22 | run: npm test 23 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-push.yml: -------------------------------------------------------------------------------- 1 | name: Push from Crowdin 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: '55 1 */7 * *' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | push: 13 | environment: dev-crowdin 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Download latest translations from Crowdin 20 | uses: crowdin/github-action@v2 21 | with: 22 | upload_sources: false 23 | download_sources: false 24 | download_translations: true 25 | skip_untranslated_strings: true 26 | push_translations: true 27 | create_pull_request: false 28 | crowdin_branch_name: master 29 | localization_branch_name: master 30 | config: 'crowdin.yml' 31 | project_id: ${{ secrets.CROWDIN_API_PROJECT_ID }} 32 | token: ${{ secrets.CROWDIN_API_TOKEN }} 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-upload.yml: -------------------------------------------------------------------------------- 1 | name: Upload to Crowdin 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | upload: 12 | environment: dev-crowdin 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Upload latest strings for translation on Crowdin 19 | uses: crowdin/github-action@v2 20 | with: 21 | crowdin_branch_name: master 22 | config: 'crowdin.yml' 23 | project_id: ${{ secrets.CROWDIN_API_PROJECT_ID }} 24 | token: ${{ secrets.CROWDIN_API_TOKEN }} 25 | 26 | - name: Upload latest strings for translation on Crowdin 27 | uses: crowdin/github-action@v2 28 | with: 29 | crowdin_branch_name: master 30 | config: 'crowdin-readme.yml' 31 | project_id: ${{ secrets.CROWDIN_API_PROJECT_ID }} 32 | token: ${{ secrets.CROWDIN_API_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | yarn.lock 5 | *.zip 6 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": 4 | { 5 | "media-feature-range-notation": "prefix", 6 | "selector-class-pattern": null, 7 | "selector-id-pattern": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "/_locales/[!e]*/messages.json": true, 4 | "/_locales/e[!n]*/messages.json": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-current, SteamDB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of SteamDB nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SteamDB Extension 2 | 3 | [![chrome users](https://img.shields.io/chrome-web-store/users/kdbmhfkmnlmbkgbabkdealhhbfhlmmon?label=chrome%20users&style=for-the-badge&logo=googlechrome)](https://chrome.google.com/webstore/detail/steam-database/kdbmhfkmnlmbkgbabkdealhhbfhlmmon) 4 | [![firefox users](https://img.shields.io/amo/users/steam-database?label=firefox%20users&color=4c1&style=for-the-badge&logo=firefoxbrowser)](https://addons.mozilla.org/firefox/addon/steam-database/) 5 | [![edge users](https://img.shields.io/badge/dynamic/json?label=edge%20users&query=%24.activeInstallCount&url=https://microsoftedge.microsoft.com/addons/getproductdetailsbycrxid/hjknpdomhlodgaebegjopkmfafjpbblg&style=for-the-badge&logo=microsoftedge)](https://microsoftedge.microsoft.com/addons/detail/steam-database/hjknpdomhlodgaebegjopkmfafjpbblg) 6 | 7 | Adds SteamDB links on various pages in the Steam Community and Store. 8 | Also highlights owned and wished products on steamdb.info. 9 | 10 | ![](https://steamdb.info/static/img/extension.png) 11 | 12 | ### Major features 13 | * Add SteamDB links across most Steam store and community pages 14 | * Highlight owned/wished/in cart games and packages on steamdb.info *(by fetching info from Steam store)* 15 | * Add new features on Steam sites (e.g. automatic age gate skip, quick sell in inventory, market prices in other inventories) 16 | * Fix stuff that Valve hasn't (e.g. properly center Steam store on big screens) 17 | * See [this link](https://steamdb.info/extension/) for a list of all options and features 18 | 19 | ### Links 20 | * Features: https://steamdb.info/extension/ 21 | * Privacy Policy: https://steamdb.info/extension/privacy/ 22 | * Chrome Web Store: https://chrome.google.com/webstore/detail/kdbmhfkmnlmbkgbabkdealhhbfhlmmon 23 | * Mozilla Addons: https://addons.mozilla.org/en-US/firefox/addon/steam-database/ 24 | * Microsoft Edge: https://microsoftedge.microsoft.com/addons/detail/steam-database/hjknpdomhlodgaebegjopkmfafjpbblg 25 | 26 | *There are no plans to support Safari at this time due to Apple's policies.* 27 | 28 | ### Contributing 29 | 30 | This extension does not have any build steps, and you can simply load the folder on the extensions page of your browser. 31 | 32 | When writing code, make sure to run our linter: 33 | 1. Run `npm install` to install eslint 34 | 2. Run `npm test` which should report warnings 35 | 3. Run `npm run fix` which should automatically fix most of the reported warnings 36 | 37 | #### Localization rules 38 | 39 | - Do not localize "SteamDB" 40 | - Keep the HTML codes intact 41 | - If there is whitespace in the strings, keep it 42 | - If there are words that are used by Steam itself (such as discovery queue), match them 43 | - Substitution tokens like `$1` and strings instead of `` tags should be kept 44 | 45 | [Translate on Crowdin](https://crowdin.com/project/steamdb-extension) 46 | [![Crowdin](https://badges.crowdin.net/steamdb-extension/localized.svg)](https://crowdin.com/project/steamdb-extension) 47 | 48 | To test a specific language in Chrome, see this link: 49 | https://developer.chrome.com/docs/extensions/reference/api/i18n#how-to-set-browsers-locale 50 | 51 | #### Making a release 52 | 53 | Run `npm run version 3.0.0` which updates `manifest.json`, creates a commit, creates a tag, 54 | and runs `npm run build` which creates a zip file with the release. 55 | 56 | ### Trade offers support for `for_item` and `my_item` 57 | 58 | This extension adds support for `for_item` and `my_item` parameters in `/tradeoffer/new` urls, 59 | these parameters will automatically add items to trade window upon page load. 60 | 61 | * `for_item` specifies an item in partner's inventory. 62 | * `my_item` specifies an item in your inventory. 63 | 64 | Multiple parameters can be specified, a single parameter takes format of `appid_contextid_assetid`. 65 | 66 | For example: 67 | ``` 68 | https://steamcommunity.com/tradeoffer/new?partner=[steamid]&for_item=753_6_1234 69 | https://steamcommunity.com/tradeoffer/new?partner=[steamid]&for_item=753_6_1234&for_item=753_6_5678 70 | https://steamcommunity.com/tradeoffer/new?partner=[steamid]&my_item=753_6_1234 71 | https://steamcommunity.com/tradeoffer/new?partner=[steamid]&my_item=753_6_1234&my_item=753_6_5678 72 | https://steamcommunity.com/tradeoffer/new?partner=[steamid]&for_item=753_6_1234&my_item=753_6_5678 73 | ``` 74 | 75 | ### Automatically open "grant an award" popup from a link using `award` 76 | 77 | This extension adds support for `award` parameter in profile, workshop, and other published files such as screenshots. 78 | 79 | Open the "grant an award" popup upon page load: `https://steamcommunity.com/id/xpaw?award` 80 | 81 | Open popup and pre-select a specific award: `https://steamcommunity.com/id/xpaw?award=17` 82 | In this case id 17 is "Take my points". 83 | 84 | It works the same way for shared files: `https://steamcommunity.com/sharedfiles/filedetails/?id=2935326022&award=17` 85 | 86 | ### License 87 | Code in this repository is governed by a BSD-style license that can be found in the [LICENSE](LICENSE) file. 88 | -------------------------------------------------------------------------------- /_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_description": { 3 | "message": "在 Steam 商店和社群中加入 SteamDB 連結和新功能。查看最低遊戲價格和統計資料。", 4 | "description": "This value can be up to 132 characters." 5 | }, 6 | "steamdb_options": { 7 | "message": "SteamDB 選項" 8 | }, 9 | "steamdb_calculator": { 10 | "message": "SteamDB 價值計算" 11 | }, 12 | "steamstatus": { 13 | "message": "前往 $domain$ 查看", 14 | "placeholders": { 15 | "domain": { 16 | "content": "steamstat.us", 17 | "example": "steamstat.us" 18 | } 19 | } 20 | }, 21 | "steamstatus_downtime": { 22 | "message": "Steam 似乎已離線。 " 23 | }, 24 | "view_on_steamdb": { 25 | "message": "在 SteamDB 上檢視" 26 | }, 27 | "view_on_steam_hunters": { 28 | "message": "在 Steam Hunters 上檢視", 29 | "description": "Steam Hunters is the name of https://steamhunters.com/ please do not translate it" 30 | }, 31 | "view_on_pcgamingwiki": { 32 | "message": "在 PCGamingWiki 上檢視" 33 | }, 34 | "view_your_achievements": { 35 | "message": "個人成就" 36 | }, 37 | "store_page": { 38 | "message": "商店頁面", 39 | "description": "This translation should match how Steam translates it on community hubs" 40 | }, 41 | "search": { 42 | "message": "搜尋" 43 | }, 44 | "app_steamdb_rating": { 45 | "message": "SteamDB 評分:" 46 | }, 47 | "app_steamdb_rating_responsive": { 48 | "message": " STEAMDB 評分", 49 | "description": "Visible on the mobile version" 50 | }, 51 | "app_steamdb_rating_tooltip": { 52 | "message": "$total$ 則評論中的 $positive$ 則為正面評價 (所有購買類型)", 53 | "placeholders": { 54 | "positive": { 55 | "content": "$1", 56 | "example": "123,456" 57 | }, 58 | "total": { 59 | "content": "$2", 60 | "example": "456,789" 61 | } 62 | } 63 | }, 64 | "app_depots_update": { 65 | "message": "最後更新:" 66 | }, 67 | "app_depots_updated_short": { 68 | "message": "上次更新", 69 | "description": "Visible on the mobile version" 70 | }, 71 | "app_stats_online": { 72 | "message": "在線統計" 73 | }, 74 | "app_stats_online_now": { 75 | "message": "在線玩家數:" 76 | }, 77 | "app_stats_peak_today": { 78 | "message": "24小時內最高峰:" 79 | }, 80 | "app_stats_alL_time_peak": { 81 | "message": "歷史峰值:" 82 | }, 83 | "app_stats_followers": { 84 | "message": "追蹤數:" 85 | }, 86 | "app_lowest_price": { 87 | "message": "SteamDB 歷史最低價格為 $price$", 88 | "placeholders": { 89 | "price": { 90 | "content": "$1", 91 | "example": "1,99€" 92 | } 93 | } 94 | }, 95 | "app_lowest_price_discount": { 96 | "message": "SteamDB 歷史最低價格為 $price$/-$discount$%", 97 | "placeholders": { 98 | "price": { 99 | "content": "$1", 100 | "example": "1,99€" 101 | }, 102 | "discount": { 103 | "content": "$2", 104 | "example": "80" 105 | } 106 | } 107 | }, 108 | "app_lowest_price_limited": { 109 | "message": "SteamDB 歷史最低價格為 $price$,近兩年最低價格為 $price_limited$", 110 | "placeholders": { 111 | "price": { 112 | "content": "$1", 113 | "example": "1,99€" 114 | }, 115 | "price_limited": { 116 | "content": "$2", 117 | "example": "5,99€" 118 | } 119 | } 120 | }, 121 | "app_lowest_date": { 122 | "message": "此價格於 $date$ 出現 ($relative$)", 123 | "placeholders": { 124 | "date": { 125 | "content": "$1", 126 | "example": "22 Aug 2018" 127 | }, 128 | "relative": { 129 | "content": "$2", 130 | "example": "71 months ago" 131 | } 132 | } 133 | }, 134 | "app_lowest_date_multiple": { 135 | "message": "價格已出現 $seen$ 次,上次出現在 $date$ ($relative$)", 136 | "description": "This string is only used if $seen$ is higher than one, so it can not be 0 or 1.", 137 | "placeholders": { 138 | "date": { 139 | "content": "$1", 140 | "example": "22 Aug 2018" 141 | }, 142 | "relative": { 143 | "content": "$2", 144 | "example": "71 months ago" 145 | }, 146 | "seen": { 147 | "content": "$3", 148 | "example": "15" 149 | } 150 | } 151 | }, 152 | "agecheck_option_hint": { 153 | "message": "SteamDB可自動略過這些年齡檢查頁面" 154 | }, 155 | "explore_auto_discover": { 156 | "message": "自動探索佇列" 157 | }, 158 | "explore_auto_discover_description": { 159 | "message": "自動探索佇列以取得活動卡片。" 160 | }, 161 | "explore_generating": { 162 | "message": "正在產生新的探索佇列,如果 Steam 處於高負載下可能會失敗..." 163 | }, 164 | "explore_exploring": { 165 | "message": "探索佇列中... 請求數 $current$/$total$", 166 | "placeholders": { 167 | "current": { 168 | "content": "$1", 169 | "example": "1" 170 | }, 171 | "total": { 172 | "content": "$2", 173 | "example": "12" 174 | } 175 | } 176 | }, 177 | "explore_failed_to_clear_too_many": { 178 | "message": "佇列清除失敗次數過多。您可以稍後再試。" 179 | }, 180 | "explore_finished": { 181 | "message": "佇列已探索完成。" 182 | }, 183 | "explore_saleitem_claim": { 184 | "message": "領取獎勵" 185 | }, 186 | "explore_saleitem_claim_description": { 187 | "message": "將取得可領取的免費每日獎勵。" 188 | }, 189 | "explore_saleitem_trying_to_claim": { 190 | "message": "每日獎勵領取中…" 191 | }, 192 | "explore_saleitem_cant_claim": { 193 | "message": "目前沒有可領取的每日獎勵。" 194 | }, 195 | "explore_saleitem_next_item_time": { 196 | "message": "下一個特賣物品可領取時間為:$datetime$", 197 | "placeholders": { 198 | "datetime": { 199 | "content": "$1", 200 | "example": "01.07.2024, 20:00:00", 201 | "description": "Date and time format depends on locale set in browser" 202 | } 203 | } 204 | }, 205 | "explore_saleitem_claim_failed": { 206 | "message": "每日獎勵領取失敗。" 207 | }, 208 | "explore_saleitem_success": { 209 | "message": "已領取新的每日獎勵:$item$。", 210 | "placeholders": { 211 | "item": { 212 | "content": "$1", 213 | "example": "Unicorn Sticker" 214 | } 215 | } 216 | }, 217 | "badges_idle_apps": { 218 | "message": "可掉落應用數: $amount$", 219 | "description": "Shown on your badges page, can also be translated as apps with drops or games with cards", 220 | "placeholders": { 221 | "amount": { 222 | "content": "$1", 223 | "example": "5" 224 | } 225 | } 226 | }, 227 | "badges_idle_apps_on_this_page": { 228 | "message": "此頁可掉落應用數: $amount$", 229 | "description": "Shown on your badges page, can also be translated as apps with drops or games with cards", 230 | "placeholders": { 231 | "amount": { 232 | "content": "$1", 233 | "example": "5" 234 | } 235 | } 236 | }, 237 | "badges_idle_drops": { 238 | "message": "剩餘掉落數:$amount$", 239 | "description": "Shown on your badges page, can also be translated as cards remaining", 240 | "placeholders": { 241 | "amount": { 242 | "content": "$1", 243 | "example": "5" 244 | } 245 | } 246 | }, 247 | "badges_idle_drops_on_this_page": { 248 | "message": "此頁剩餘掉落數:$amount$", 249 | "description": "Shown on your badges page, can also be translated as cards remaining", 250 | "placeholders": { 251 | "amount": { 252 | "content": "$1", 253 | "example": "5" 254 | } 255 | } 256 | }, 257 | "hidden_achievement": { 258 | "message": "隱藏成就: " 259 | }, 260 | "achievement_global_unlock": { 261 | "message": "$percent$ 的玩家擁有這項成就", 262 | "description": "Do not add percent symbol, it will be formatted automatically", 263 | "placeholders": { 264 | "percent": { 265 | "content": "$1", 266 | "example": "93.5%" 267 | } 268 | } 269 | }, 270 | "achievements_unlocked_count": { 271 | "message": "已取得成就:$count$", 272 | "placeholders": { 273 | "count": { 274 | "content": "$1", 275 | "example": "69" 276 | } 277 | } 278 | }, 279 | "achievements_locked_count": { 280 | "message": "未取得成就:$count$", 281 | "placeholders": { 282 | "count": { 283 | "content": "$1", 284 | "example": "69" 285 | } 286 | } 287 | }, 288 | "achievements_sort_by_time": { 289 | "message": "按時間排序" 290 | }, 291 | "achievements_groups_by_steamhunters": { 292 | "message": "成就分類由 Steam Hunters 透過 SteamDB 提供。", 293 | "description": "Steam Hunters is the name of https://steamhunters.com/ please do not translate it" 294 | }, 295 | "achievements_csrating_season": { 296 | "message": "第 $season$ 季", 297 | "placeholders": { 298 | "season": { 299 | "content": "$1", 300 | "example": "1" 301 | } 302 | } 303 | }, 304 | "achievements_csrating_date": { 305 | "message": "日期" 306 | }, 307 | "achievements_csrating_name": { 308 | "message": "CS 評級" 309 | }, 310 | "inventory_list_at": { 311 | "message": "以 $price$ 上架", 312 | "placeholders": { 313 | "price": { 314 | "content": "%price%", 315 | "example": "1,99€" 316 | } 317 | } 318 | }, 319 | "inventory_sell_at": { 320 | "message": "以 $price$ 出售", 321 | "placeholders": { 322 | "price": { 323 | "content": "%price%", 324 | "example": "1,99€" 325 | } 326 | } 327 | }, 328 | "inventory_list_at_title": { 329 | "message": "列出最低售價的商品\n顯示的價格是您收到的金額 (不含服務費)" 330 | }, 331 | "inventory_sell_at_title": { 332 | "message": "列出最高購買訂單價格的項目\n顯示的價格是您收到的金額 (不含服務費)" 333 | }, 334 | "inventory_badge_level": { 335 | "message": "您的徽章等級:$level$", 336 | "placeholders": { 337 | "level": { 338 | "content": "%level%", 339 | "example": "5" 340 | } 341 | } 342 | }, 343 | "inventory_badge_foil_level": { 344 | "message": "您的閃亮徽章等級:$level$", 345 | "placeholders": { 346 | "level": { 347 | "content": "%level%", 348 | "example": "1" 349 | } 350 | } 351 | }, 352 | "inventory_badge_none": { 353 | "message": "沒有已合成徽章" 354 | }, 355 | "spoilers_reveal": { 356 | "message": "顯示劇透內容", 357 | "description": "A button or checkbox that reveals spoilers when toggled" 358 | }, 359 | "popup_charts": { 360 | "message": "統計圖表" 361 | }, 362 | "popup_sales": { 363 | "message": "特賣" 364 | }, 365 | "popup_calendar": { 366 | "message": "發行日曆" 367 | }, 368 | "popup_patches": { 369 | "message": "更新日誌" 370 | }, 371 | "popup_calculator": { 372 | "message": "價值計算機" 373 | }, 374 | "options": { 375 | "message": "選項" 376 | }, 377 | "options_welcome_title": { 378 | "message": "👋 感謝您安裝 SteamDB 擴充功能!" 379 | }, 380 | "options_welcome_description": { 381 | "message": "查看下方的選項並隨您喜好開關。未來可藉由下列方式回來調整:" 382 | }, 383 | "options_welcome_option1": { 384 | "message": "SteamDB 上的設定頁面" 385 | }, 386 | "options_welcome_option2": { 387 | "message": "Steam 頁面上的帳戶下拉選單" 388 | }, 389 | "options_welcome_option3": { 390 | "message": "您的瀏覽器的擴充功能頁面" 391 | }, 392 | "options_review": { 393 | "message": "為這個擴充功能留下評論" 394 | }, 395 | "options_permissions_description": { 396 | "message": "此擴充功能需要額外的權限才能在 SteamDB、Steam 商店和 Steam 社群上正常運作。" 397 | }, 398 | "options_permissions_button": { 399 | "message": "⚠️授予權限" 400 | }, 401 | "options_footer1": { 402 | "message": "由 SteamDB 人員用 💙 發電。" 403 | }, 404 | "options_footer2": { 405 | "message": "這是開源軟體,感謝所有貢獻者!" 406 | }, 407 | "options_disabled_checkboxes": { 408 | "message": "* 停用的複選框表示是內建功能。" 409 | }, 410 | "options_header_website": { 411 | "message": "SteamDB 網站" 412 | }, 413 | "options_header_extra_data": { 414 | "message": "額外資料" 415 | }, 416 | "options_header_buttons_and_links": { 417 | "message": "按鈕和連結" 418 | }, 419 | "options_header_inventory": { 420 | "message": "物品庫" 421 | }, 422 | "options_header_trade_offers": { 423 | "message": "交易提案" 424 | }, 425 | "options_header_achievements": { 426 | "message": "成就" 427 | }, 428 | "options_header_enhancements": { 429 | "message": "增強功能" 430 | }, 431 | "options_website_explanation": { 432 | "message": "說明" 433 | }, 434 | "options_website_explanation1": { 435 | "message": "啟用此選項後,擴充功能將從 Steam 商店 請求一些使用著資料 (願望清單、已擁有、關注中、已忽略的產品、即在購物車中的產品),以便在網站上突出顯示。" 436 | }, 437 | "options_website_explanation2": { 438 | "message": "您必須在商店上登入才能使用。" 439 | }, 440 | "options_website_explanation3": { 441 | "message": "這些資料僅保存在您的瀏覽器中,並不會發送到 SteamDB 後端。" 442 | }, 443 | "options_website_explanation4": { 444 | "message": "為了防止對 Steam 商店發出過多請求,用戶資料會將快取放在瀏覽器的本地儲存。" 445 | }, 446 | "options_website_highlight": { 447 | "message": "在 $domain$ 上標記已擁有和在願望清單上的產品", 448 | "placeholders": { 449 | "domain": { 450 | "content": "steamdb.info", 451 | "example": "steamdb.info" 452 | } 453 | } 454 | }, 455 | "options_website_highlight1": { 456 | "message": "直接從 SteamDB 中新增願望清單、追蹤並忽略 Steam 商店中的產品" 457 | }, 458 | "options_website_highlight2": { 459 | "message": "啟用免費捆綁包腳本" 460 | }, 461 | "options_website_highlight3": { 462 | "message": "在販售頁面捆綁包頁面中啟用額外的過濾器" 463 | }, 464 | "options_website_family": { 465 | "message": "突出顯示親友成員擁有的應用數 (需要額外請求)" 466 | }, 467 | "options_extra_data": { 468 | "message": "從 $domain$ 取得的額外資料", 469 | "placeholders": { 470 | "domain": { 471 | "content": "steamdb.info", 472 | "example": "steamdb.info" 473 | } 474 | } 475 | }, 476 | "options_extra_data_players": { 477 | "message": "當啟用「$options_online_stats$」或「$options_steamdb_last_update$」選項時,擴充套件會向 $domain$ 傳送請求以取得在線玩家數和最近的更新日誌。", 478 | "placeholders": { 479 | "domain": { 480 | "content": "steamdb.info", 481 | "example": "steamdb.info" 482 | }, 483 | "options_online_stats": { 484 | "content": "$1", 485 | "description": "Uses translation for options_online_stats" 486 | }, 487 | "options_steamdb_last_update": { 488 | "content": "$2", 489 | "description": "Uses translation for options_steamdb_last_update" 490 | } 491 | } 492 | }, 493 | "options_extra_data_prices": { 494 | "message": "當 「$options_steamdb_lowest_price$」 選項啟用時,擴充功能將向 $domain$ 發出請求以取得特定遊戲在您使用的貨幣 (與 Steam 的設定相同) 的歷史最低價格。", 495 | "placeholders": { 496 | "domain": { 497 | "content": "steamdb.info", 498 | "example": "steamdb.info" 499 | }, 500 | "options_steamdb_lowest_price": { 501 | "content": "$1", 502 | "description": "Uses translation for options_steamdb_lowest_price" 503 | } 504 | } 505 | }, 506 | "options_extra_data_achievement_groups": { 507 | "message": "當啟用「$options_achievement_groups$」選項時,擴充功能將向 $domain$ 發出安全請求,從而使 Steam Hunters 提供的資料在個人成就頁面上進行成就分組。", 508 | "description": "Steam Hunters is the name of https://steamhunters.com/ please do not translate it", 509 | "placeholders": { 510 | "domain": { 511 | "content": "steamdb.info", 512 | "example": "steamdb.info" 513 | }, 514 | "options_achievement_groups": { 515 | "content": "$1", 516 | "description": "Uses translation for options_achievement_groups" 517 | } 518 | } 519 | }, 520 | "options_steamdb_lowest_price": { 521 | "message": "在產品頁上顯示歷史最低價" 522 | }, 523 | "options_online_stats": { 524 | "message": "在產品頁上顯示線上玩家數及最高紀錄" 525 | }, 526 | "options_steamdb_last_update": { 527 | "message": "在產品頁顯示最近的更新" 528 | }, 529 | "options_button_app": { 530 | "message": "在產品頁面顯示按鈕" 531 | }, 532 | "options_button_pcgw": { 533 | "message": "在產品頁面上顯示 PCGamingWiki 按鈕" 534 | }, 535 | "options_button_sub": { 536 | "message": "在捆綁包頁面上顯示按鈕" 537 | }, 538 | "options_button_gamehub": { 539 | "message": "在社群顯示按鈕" 540 | }, 541 | "options_button_gamecards": { 542 | "message": "在遊戲主頁顯示 SteamDB 和商店頁面按鈕" 543 | }, 544 | "options_link_subid": { 545 | "message": "在每個購買選項底下顯示 SubID" 546 | }, 547 | "options_link_subid_widget": { 548 | "message": "在商店 Widget 中顯示 SubID", 549 | "description": "Reference: https://partner.steamgames.com/doc/marketing/widget" 550 | }, 551 | "options_link_accountpage": { 552 | "message": "在您的授權頁面替每個套件增加連結" 553 | }, 554 | "options_profile_calculator": { 555 | "message": "在社群的個人資料上增加價值計算連結" 556 | }, 557 | "options_enhancement_inventory_sidebar": { 558 | "message": "將庫存頁面上的遊戲選擇移至側邊" 559 | }, 560 | "options_link_inventory": { 561 | "message": "於庫存頁面的每個禮物和優惠券顯示 SteamDB 連結" 562 | }, 563 | "options_link_inventory_gift_subid": { 564 | "message": "於庫存頁面顯示禮物 SubID" 565 | }, 566 | "options_enhancement_inventory_no_sell_reload": { 567 | "message": "出售物品時不要刷新庫存頁面" 568 | }, 569 | "options_enhancement_inventory_quick_sell": { 570 | "message": "加入快速出售按鈕" 571 | }, 572 | "options_enhancement_inventory_quick_sell_auto": { 573 | "message": "自動接受快速出售" 574 | }, 575 | "options_enhancement_inventory_badge_info": { 576 | "message": "在交換卡片和擴充包上顯示徽章等級:" 577 | }, 578 | "options_display_market_price_others": { 579 | "message": "在其他人的物品庫中顯示物品市集價格" 580 | }, 581 | "options_enhancement_tradeoffer_url_items": { 582 | "message": "支援 $for_item$ 和 $my_item$ 參數", 583 | "placeholders": { 584 | "for_item": { 585 | "content": "for_item" 586 | }, 587 | "my_item": { 588 | "content": "my_item" 589 | } 590 | } 591 | }, 592 | "options_github_explanation": { 593 | "message": "參閱 GitHub 上的說明" 594 | }, 595 | "options_enhancement_tradeoffer_no_gift_confirm": { 596 | "message": "禁止交易提案的「此交易看起來很可疑」和「您確定這是一份禮物嗎?」的警告" 597 | }, 598 | "options_enhancement_tradeoffer_better_errors": { 599 | "message": "顯示更好的錯誤訊息,而不是 Valve 神秘的錯誤號碼" 600 | }, 601 | "options_enhancement_skip_agecheck": { 602 | "message": "跳過年齡檢查" 603 | }, 604 | "options_enhancement_skip_agecheck_description": { 605 | "message": "將年齡和生日 cookies 設定為始終通過" 606 | }, 607 | "options_enhancement_no_linkfilter": { 608 | "message": "關閉在前往外部網站時顯示的「您正要離開 Steam」警示", 609 | "description:": "\"You are leaving Steam\" should match what the button on Steam itself says: https://steamcommunity.com/linkfilter/?url=https://steamdb.info" 610 | }, 611 | "options_enhancement_market_ssa": { 612 | "message": "自動勾選市集、庫存與產品啟動頁面的「同意 Steam 訂戶協議」選項", 613 | "description": "Steam Subscriber Agreement (SSA): https://store.steampowered.com/subscriber_agreement/" 614 | }, 615 | "options_enhancement_hide_install_button": { 616 | "message": "隱藏頂部的「安裝 Steam」按鈕", 617 | "description:": "\"Install Steam\" should match what the button on Steam itself in the header says: https://store.steampowered.com/" 618 | }, 619 | "options_enhancement_hide_mobile_app_button": { 620 | "message": "隱藏「在 Steam 中開啟」橫幅" 621 | }, 622 | "options_steamdb_rating": { 623 | "message": "在產品頁面上顯示 SteamDB 評分" 624 | }, 625 | "options_enhancement_appicon": { 626 | "message": "在商店及社群頁面上顯示產品圖示", 627 | "description": "The icon is displayed to the left of the game name" 628 | }, 629 | "options_improve_achievements": { 630 | "message": "改進個人成就頁面" 631 | }, 632 | "options_achievements_global_unlock": { 633 | "message": "顯示全球成就取得比率" 634 | }, 635 | "options_modernized_achievements": { 636 | "message": "啟用簡約風格" 637 | }, 638 | "options_achievement_groups": { 639 | "message": "透過 Steam Hunters 提供資料進行成就分組", 640 | "description": "Steam Hunters is the name of https://steamhunters.com/ please do not translate it" 641 | }, 642 | "options_spoiler_achievements": { 643 | "message": "將隱藏成就放在劇透元素後" 644 | }, 645 | "options_prevent_store_images": { 646 | "message": "在產品頁面上停止讀取圖片" 647 | }, 648 | "options_prevent_store_images_description": { 649 | "message": "有些開發人員會在產品敘述內放上大容量圖片導致讀取時間變長,點擊以讀取圖片" 650 | }, 651 | "options_enhancement_award_popup_url": { 652 | "message": "支援個人資料和共享文件連結上的 $award$ 參數", 653 | "placeholders": { 654 | "award": { 655 | "content": "award" 656 | } 657 | } 658 | }, 659 | "options_builtin_collapse_library": { 660 | "message": "添加按鈕以摺疊「已在收藏庫中」的區塊" 661 | }, 662 | "options_builtin_card_drops": { 663 | "message": "在徽章頁面上顯示剩餘的可掉落卡片總數" 664 | }, 665 | "options_builtin_queue_cheat": { 666 | "message": "在特賣時加入自動探索佇列的按鈕" 667 | }, 668 | "options_builtin_multibuy_redirect": { 669 | "message": "購買缺少卡片後將重新導向回遊戲卡片頁面" 670 | }, 671 | "options_builtin_achievements_global": { 672 | "message": "改進全球成就頁面" 673 | }, 674 | "options_builtin_achievements_link": { 675 | "message": "連結至個人成就頁面" 676 | }, 677 | "options_builtin_achievements_locked": { 678 | "message": "為未取得成就顯示鎖定圖示" 679 | }, 680 | "options_builtin_achievements_csrating": { 681 | "message": "在CS2成就頁面顯示CS評級歷史" 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /assets/0_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/0_app.png -------------------------------------------------------------------------------- /assets/1_sales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/1_sales.png -------------------------------------------------------------------------------- /assets/2_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/2_settings.png -------------------------------------------------------------------------------- /assets/3_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/3_settings.png -------------------------------------------------------------------------------- /assets/4_inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/4_inventory.png -------------------------------------------------------------------------------- /assets/5_achievements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/5_achievements.png -------------------------------------------------------------------------------- /assets/StoreReadme.txt: -------------------------------------------------------------------------------- 1 | - Display lowest recorded price on game pages (all currencies supported, price history) 2 | - Display concurrent players and peaks on game pages 3 | - Display latest game update on game page 4 | - Add links to SteamDB across Steam sites 5 | - Add a link to PCGamingWiki 6 | - Modernize personal achievements page, group achievements by update and DLC 7 | - Quick sell buttons in inventory 8 | - Skip age check page on the store 9 | - Accept Subscriber Agreement on market, inventory, and activate key pages automatically 10 | - Remove "You are leaving Steam" link filter from external sites 11 | - Highlight your owned games, DLCs and packages on steamdb.info 12 | - Allows wishlisting, following and ignoring games directly from steamdb.info 13 | - A lot of other smaller tweaks and features 14 | - Simpler and faster alternative to Enhanced Steam and Augmented Steam 15 | 16 | More information: https://steamdb.info/extension/ 17 | GitHub: https://github.com/SteamDatabase/BrowserExtension 18 | -------------------------------------------------------------------------------- /assets/steamdb-extension-promo.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/assets/steamdb-extension-promo.afphoto -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require( 'node:fs' ); 4 | const path = require( 'node:path' ); 5 | const archiver = require( 'archiver' ); 6 | const manifest = require( './manifest.json' ); 7 | const version = manifest.version.replace( /\./g, '_' ); 8 | 9 | delete manifest.$schema; 10 | 11 | ( async() => 12 | { 13 | await ArchiveChromium(); 14 | await ArchiveFirefox(); 15 | } )(); 16 | 17 | function ArchiveChromium() 18 | { 19 | const zipPath = path.join( __dirname, `steamdb_ext_${version}.zip` ); 20 | const archive = PrepareArchive( zipPath ); 21 | 22 | const chromeManifest = structuredClone( manifest ); 23 | delete chromeManifest.background.scripts; 24 | delete chromeManifest.browser_specific_settings; 25 | 26 | const json = JSON.stringify( chromeManifest, null, '\t' ); 27 | archive.append( json, { name: 'manifest.json' } ); 28 | 29 | return archive.finalize(); 30 | } 31 | 32 | function ArchiveFirefox() 33 | { 34 | const zipPath = path.join( __dirname, `steamdb_ext_${version}_firefox.zip` ); 35 | const archive = PrepareArchive( zipPath ); 36 | 37 | const firefoxManifest = structuredClone( manifest ); 38 | delete firefoxManifest.background.service_worker; 39 | 40 | const json = JSON.stringify( firefoxManifest, null, '\t' ); 41 | archive.append( json, { name: 'manifest.json' } ); 42 | 43 | return archive.finalize(); 44 | } 45 | 46 | /** 47 | * @param {string} zipPath 48 | */ 49 | function PrepareArchive( zipPath ) 50 | { 51 | console.log( `Packaging to ${zipPath}` ); 52 | 53 | const output = fs.createWriteStream( zipPath ); 54 | const archive = archiver( 'zip' ); 55 | 56 | output.on( 'close', () => 57 | { 58 | console.log( `Written ${archive.pointer()} total bytes to ${zipPath}` ); 59 | } ); 60 | 61 | archive.on( 'warning', err => 62 | { 63 | throw err; 64 | } ); 65 | 66 | archive.on( 'error', err => 67 | { 68 | throw err; 69 | } ); 70 | 71 | archive.pipe( output ); 72 | 73 | archive.file( path.join( __dirname, 'LICENSE' ), { name: 'LICENSE' } ); 74 | archive.directory( path.join( __dirname, 'icons/' ), 'icons' ); 75 | archive.directory( path.join( __dirname, 'options/' ), 'options' ); 76 | archive.directory( path.join( __dirname, 'scripts/' ), 'scripts' ); 77 | archive.directory( path.join( __dirname, 'styles/' ), 'styles' ); 78 | archive.directory( path.join( __dirname, '_locales/' ), '_locales' ); 79 | 80 | return archive; 81 | } 82 | -------------------------------------------------------------------------------- /crowdin-readme.yml: -------------------------------------------------------------------------------- 1 | preserve_hierarchy: true 2 | files: 3 | - source: /assets/StoreReadme.txt 4 | translation: /store_readme/%language% (%two_letters_code%).txt 5 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | preserve_hierarchy: true 2 | files: 3 | - source: /_locales/en/messages.json 4 | translation: /_locales/%two_letters_code%/messages.json 5 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import { fileURLToPath } from "node:url"; 3 | import globals from "globals"; 4 | import js from "@eslint/js"; 5 | import jsdoc from 'eslint-plugin-jsdoc'; 6 | import path from "node:path"; 7 | 8 | const __filename = fileURLToPath( import.meta.url ); 9 | const __dirname = path.dirname( __filename ); 10 | const compat = new FlatCompat( { 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all 14 | } ); 15 | 16 | export default[ 17 | ...compat.extends( "eslint:all" ), 18 | ...compat.extends( "eslint:recommended" ), 19 | jsdoc.configs[ 'flat/recommended-typescript-flavor' ], 20 | { 21 | ignores: [ "node_modules/*" ], 22 | languageOptions: { 23 | globals: { 24 | ...globals.browser, 25 | ...globals.webextensions, 26 | 27 | _t: "readonly", 28 | ExtensionApi: "readonly", 29 | GetAppIDFromUrl: "readonly", 30 | GetCurrentAppID: "readonly", 31 | GetHomepage: "readonly", 32 | GetLanguage: "readonly", 33 | GetLocalResource: "readonly", 34 | GetOption: "readonly", 35 | SendMessageToBackgroundScript: "readonly", 36 | SetOption: "readonly", 37 | WriteLog: "readonly", 38 | }, 39 | sourceType: "commonjs", 40 | }, 41 | 42 | plugins: { 43 | jsdoc, 44 | }, 45 | 46 | rules: { 47 | "jsdoc/require-description": "off", 48 | "jsdoc/require-jsdoc": "off", 49 | "jsdoc/require-param-description": "off", 50 | "jsdoc/require-property-description": "off", 51 | "jsdoc/require-returns-description": "off", 52 | "jsdoc/require-returns": "off", 53 | "array-bracket-spacing": [ "error", "always" ], 54 | "brace-style": [ "error", "allman" ], 55 | "camelcase": "off", 56 | "capitalized-comments": "off", 57 | "class-methods-use-this": "off", 58 | "complexity": "off", 59 | "computed-property-spacing": [ "error", "always" ], 60 | "consistent-return": "off", 61 | "consistent-this": "off", 62 | "default-case": "off", 63 | "func-name-matching": "off", 64 | "func-names": "off", 65 | "func-style": "off", 66 | "guard-for-in": "off", 67 | "id-length": "off", 68 | "indent": [ "error", "tab", { SwitchCase: 1 } ], 69 | "init-declarations": "off", 70 | "line-comment-position": "off", 71 | "max-classes-per-file": "off", 72 | "max-depth": "off", 73 | "max-lines-per-function": "off", 74 | "max-lines": "off", 75 | "max-params": "off", 76 | "max-statements": "off", 77 | "multiline-comment-style": "off", 78 | "new-cap": "off", 79 | "no-alert": "off", 80 | "no-bitwise": "off", 81 | "no-console": "off", 82 | "no-continue": "off", 83 | "no-implicit-coercion": "off", 84 | "no-inline-comments": "off", 85 | "no-invalid-this": "off", 86 | "no-magic-numbers": "off", 87 | "no-multi-assign": "off", 88 | "no-negated-condition": "off", 89 | "no-nested-ternary": "off", 90 | "no-param-reassign": "off", 91 | "no-plusplus": "off", 92 | "no-shadow": "off", 93 | "no-tabs": "off", 94 | "no-ternary": "off", 95 | "no-trailing-spaces": [ "error" ], 96 | "no-undefined": "off", 97 | "no-underscore-dangle": "off", 98 | "no-unused-vars": "off", 99 | "no-use-before-define": "off", 100 | "no-useless-assignment": "off", 101 | "no-var": [ "error" ], 102 | "no-warning-comments": "off", 103 | "object-curly-spacing": [ "error", "always" ], 104 | "object-shorthand": "off", 105 | "one-var": "off", 106 | "prefer-arrow-callback": "off", 107 | "prefer-const": [ "error" ], 108 | "prefer-destructuring": "off", 109 | "prefer-named-capture-group": "off", 110 | "prefer-rest-params": "off", 111 | "prefer-template": "off", 112 | "require-atomic-updates": "off", 113 | "require-unicode-regexp": "off", 114 | "semi": [ "error", "always" ], 115 | "sort-keys": "off", 116 | "space-before-function-paren": [ "error", "never" ], 117 | "space-in-parens": [ "error", "always" ], 118 | "strict": [ "error", "global" ], 119 | "keyword-spacing": [ "error", { 120 | before: false, 121 | after: false, 122 | 123 | overrides: { 124 | async: { 125 | after: true, 126 | }, 127 | 128 | await: { 129 | after: true, 130 | }, 131 | 132 | case: { 133 | after: true, 134 | }, 135 | 136 | return: { 137 | after: true, 138 | }, 139 | 140 | const: { 141 | after: true, 142 | }, 143 | 144 | let: { 145 | after: true, 146 | }, 147 | 148 | of: { 149 | before: true, 150 | after: true, 151 | }, 152 | 153 | from: { 154 | before: true, 155 | after: true, 156 | }, 157 | 158 | static: { 159 | after: true, 160 | }, 161 | 162 | import: { 163 | after: true, 164 | }, 165 | }, 166 | } ], 167 | }, 168 | }, 169 | { 170 | files: [ "*.mjs" ], 171 | 172 | languageOptions: { 173 | sourceType: "module", 174 | }, 175 | }, 176 | { 177 | files: [ "build.js", "version.js" ], 178 | 179 | languageOptions: { 180 | globals: { 181 | ...globals.node 182 | }, 183 | } 184 | } 185 | ]; 186 | -------------------------------------------------------------------------------- /icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/icons/128.png -------------------------------------------------------------------------------- /icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/icons/16.png -------------------------------------------------------------------------------- /icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/icons/32.png -------------------------------------------------------------------------------- /icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/icons/48.png -------------------------------------------------------------------------------- /icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamDatabase/BrowserExtension/2d1bd5dae17fbae61448959647b61371f58c12d8/icons/64.png -------------------------------------------------------------------------------- /icons/achievements_completed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/pcgamingwiki.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/steamhunters.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/chrome-manifest", 3 | "manifest_version": 3, 4 | "version": "4.16", 5 | "author": "SteamDB", 6 | "name": "SteamDB", 7 | "short_name": "SteamDB", 8 | "description": "__MSG_extension_description__", 9 | "default_locale": "en", 10 | "homepage_url": "https://steamdb.info/", 11 | "options_ui": 12 | { 13 | "open_in_tab": true, 14 | "page": "options/options.html" 15 | }, 16 | "icons": 17 | { 18 | "16": "icons/16.png", 19 | "32": "icons/32.png", 20 | "48": "icons/48.png", 21 | "64": "icons/64.png", 22 | "128": "icons/128.png" 23 | }, 24 | "permissions": 25 | [ 26 | "storage" 27 | ], 28 | "host_permissions": 29 | [ 30 | "https://steamdb.info/*", 31 | "https://steamcommunity.com/*", 32 | "https://*.steampowered.com/*" 33 | ], 34 | "action": 35 | { 36 | "default_popup": "options/popup.html" 37 | }, 38 | "background": 39 | { 40 | "scripts": 41 | [ 42 | "scripts/background.js" 43 | ], 44 | "service_worker": "scripts/background.js" 45 | }, 46 | "browser_specific_settings": 47 | { 48 | "gecko": 49 | { 50 | "id": "firefox-extension@steamdb.info", 51 | "strict_min_version": "109.0" 52 | }, 53 | "gecko_android": 54 | { 55 | "strict_min_version": "120.0" 56 | } 57 | }, 58 | "web_accessible_resources": 59 | [ 60 | { 61 | "resources": 62 | [ 63 | "options/options.html", 64 | 65 | "icons/white.svg", 66 | "icons/pcgamingwiki.svg", 67 | "icons/steamhunters.svg", 68 | "icons/image.svg", 69 | "icons/achievements_completed.svg", 70 | 71 | "styles/appicon.css", 72 | "styles/inventory-sidebar.css", 73 | 74 | "scripts/community/inventory.js", 75 | "scripts/community/agecheck_injected.js", 76 | "scripts/community/filedetails_award_injected.js", 77 | "scripts/community/market_injected.js", 78 | "scripts/community/multibuy_injected.js", 79 | "scripts/community/profile_award_injected.js", 80 | "scripts/community/tradeoffer_injected.js", 81 | "scripts/store/account_licenses_injected.js", 82 | "scripts/store/invalidate_cache_injected.js", 83 | "scripts/store/registerkey_injected.js", 84 | "scripts/store/subscriptions.js" 85 | ], 86 | "matches": 87 | [ 88 | "https://store.steampowered.com/*", 89 | "https://steamcommunity.com/*" 90 | ] 91 | }, 92 | { 93 | "resources": 94 | [ 95 | "options/options.html" 96 | ], 97 | "matches": 98 | [ 99 | "https://steamdb.info/*" 100 | ] 101 | } 102 | ], 103 | "content_scripts": 104 | [ 105 | { 106 | "run_at": "document_start", 107 | "matches": 108 | [ 109 | "https://steamdb.info/*" 110 | ], 111 | "exclude_matches": 112 | [ 113 | "https://steamdb.info/api/*", 114 | "https://steamdb.info/static/*", 115 | "https://steamdb.info/embed/*" 116 | ], 117 | "js": 118 | [ 119 | "scripts/common.js", 120 | "scripts/steamdb/global.js" 121 | ] 122 | }, 123 | { 124 | "run_at": "document_start", 125 | "all_frames": true, 126 | "matches": 127 | [ 128 | "https://store.steampowered.com/*", 129 | "https://steamcommunity.com/*" 130 | ], 131 | "js": 132 | [ 133 | "scripts/common.js" 134 | ] 135 | }, 136 | 137 | 138 | 139 | { 140 | "run_at": "document_end", 141 | "matches": 142 | [ 143 | "https://store.steampowered.com/*" 144 | ], 145 | "exclude_matches": 146 | [ 147 | "https://store.steampowered.com/account/ackgift/*", 148 | "https://store.steampowered.com/account/redeemwalletcode/*", 149 | "https://store.steampowered.com/actions/*", 150 | "https://store.steampowered.com/api/*", 151 | "https://store.steampowered.com/broadcast/*", 152 | "https://store.steampowered.com/buyitem/*", 153 | "https://store.steampowered.com/dynamicstore/*", 154 | "https://store.steampowered.com/join/*", 155 | "https://store.steampowered.com/login/*", 156 | "https://store.steampowered.com/public/*", 157 | "https://store.steampowered.com/saleaction/*", 158 | "https://store.steampowered.com/supportmessages/*", 159 | "https://store.steampowered.com/videos/*", 160 | "https://store.steampowered.com/vtt/*", 161 | "https://store.steampowered.com/widget/*" 162 | ], 163 | "js": 164 | [ 165 | "scripts/global.js", 166 | "scripts/store/invalidate_cache.js" 167 | ], 168 | "css": 169 | [ 170 | "styles/global.css", 171 | "styles/store.css" 172 | ] 173 | }, 174 | { 175 | "run_at": "document_end", 176 | "matches": 177 | [ 178 | "https://store.steampowered.com/app/*" 179 | ], 180 | "js": 181 | [ 182 | "scripts/store/app_error.js", 183 | "scripts/store/app.js" 184 | ], 185 | "css": 186 | [ 187 | "styles/store.css" 188 | ] 189 | }, 190 | { 191 | "matches": 192 | [ 193 | "https://store.steampowered.com/news/app/*" 194 | ], 195 | "js": 196 | [ 197 | "scripts/store/app_error.js", 198 | "scripts/store/app_news.js" 199 | ], 200 | "css": 201 | [ 202 | "styles/store.css" 203 | ] 204 | }, 205 | { 206 | "run_at": "document_end", 207 | "matches": 208 | [ 209 | "https://store.steampowered.com/app/*" 210 | ], 211 | "js": 212 | [ 213 | "scripts/store/app_images.js" 214 | ] 215 | }, 216 | { 217 | "run_at": "document_start", 218 | "matches": 219 | [ 220 | "https://store.steampowered.com/account/licenses*" 221 | ], 222 | "js": 223 | [ 224 | "scripts/store/account_licenses.js" 225 | ], 226 | "css": 227 | [ 228 | "styles/account_licenses.css" 229 | ] 230 | }, 231 | { 232 | "run_at": "document_end", 233 | "matches": 234 | [ 235 | "https://store.steampowered.com/account/registerkey*" 236 | ], 237 | "js": 238 | [ 239 | "scripts/store/registerkey.js" 240 | ], 241 | "css": 242 | [ 243 | "styles/store.css" 244 | ] 245 | }, 246 | { 247 | "matches": 248 | [ 249 | "https://store.steampowered.com/sub/*" 250 | ], 251 | "js": 252 | [ 253 | "scripts/store/sub.js" 254 | ], 255 | "css": 256 | [ 257 | "styles/store.css" 258 | ] 259 | }, 260 | { 261 | "matches": 262 | [ 263 | "https://store.steampowered.com/bundle/*" 264 | ], 265 | "js": 266 | [ 267 | "scripts/store/bundle.js" 268 | ] 269 | }, 270 | { 271 | "all_frames": true, 272 | "matches": 273 | [ 274 | "https://store.steampowered.com/widget/*" 275 | ], 276 | "js": 277 | [ 278 | "scripts/store/widget.js" 279 | ], 280 | "css": 281 | [ 282 | "styles/store.css" 283 | ] 284 | }, 285 | { 286 | "run_at": "document_end", 287 | "matches": 288 | [ 289 | "https://store.steampowered.com/app/*/agecheck", 290 | "https://store.steampowered.com/agecheck/*" 291 | ], 292 | "js": 293 | [ 294 | "scripts/store/app_error.js", 295 | "scripts/store/agecheck.js" 296 | ] 297 | }, 298 | { 299 | "matches": 300 | [ 301 | "https://store.steampowered.com/explore*" 302 | ], 303 | "js": 304 | [ 305 | "scripts/store/explore.js" 306 | ] 307 | }, 308 | 309 | 310 | 311 | { 312 | "run_at": "document_start", 313 | "matches": 314 | [ 315 | "https://store.steampowered.com/app/*", 316 | "https://steamcommunity.com/app/*", 317 | "https://steamcommunity.com/sharedfiles/filedetails*", 318 | "https://steamcommunity.com/workshop/filedetails*", 319 | "https://steamcommunity.com/workshop/browse*", 320 | "https://steamcommunity.com/workshop/discussions*" 321 | ], 322 | "js": 323 | [ 324 | "scripts/appicon.js" 325 | ] 326 | }, 327 | 328 | 329 | { 330 | "run_at": "document_end", 331 | "matches": 332 | [ 333 | "https://steamcommunity.com/*" 334 | ], 335 | "exclude_matches": 336 | [ 337 | "https://steamcommunity.com/actions/*", 338 | "https://steamcommunity.com/chat/*", 339 | "https://steamcommunity.com/login/*", 340 | "https://steamcommunity.com/miniprofile/*", 341 | "https://steamcommunity.com/public/*", 342 | "https://steamcommunity.com/openid/*" 343 | ], 344 | "js": 345 | [ 346 | "scripts/global.js" 347 | ], 348 | "css": 349 | [ 350 | "styles/global.css" 351 | ] 352 | }, 353 | { 354 | "matches": 355 | [ 356 | "https://steamcommunity.com/id/*", 357 | "https://steamcommunity.com/profiles/*" 358 | ], 359 | "js": 360 | [ 361 | "scripts/community/profile.js" 362 | ], 363 | "css": 364 | [ 365 | "styles/community.css" 366 | ] 367 | }, 368 | { 369 | "run_at": "document_end", 370 | "matches": 371 | [ 372 | "https://steamcommunity.com/id/*/inventory*", 373 | "https://steamcommunity.com/profiles/*/inventory*" 374 | ], 375 | "js": 376 | [ 377 | "scripts/community/profile_inventory.js" 378 | ], 379 | "css": 380 | [ 381 | "styles/inventory.css" 382 | ] 383 | }, 384 | { 385 | "run_at": "document_end", 386 | "matches": 387 | [ 388 | "https://steamcommunity.com/id/*/stats*", 389 | "https://steamcommunity.com/profiles/*/stats*" 390 | ], 391 | "js": 392 | [ 393 | "scripts/community/achievements.js", 394 | "scripts/community/achievements_profile.js" 395 | ], 396 | "css": 397 | [ 398 | "styles/achievements.css" 399 | ] 400 | }, 401 | { 402 | "run_at": "document_end", 403 | "matches": 404 | [ 405 | "https://steamcommunity.com/id/*/stats/CSGO*", 406 | "https://steamcommunity.com/profiles/*/stats/CSGO*" 407 | ], 408 | "js": 409 | [ 410 | "scripts/community/achievements_cs2.js" 411 | ], 412 | "css": 413 | [ 414 | "styles/achievements_cs2.css" 415 | ] 416 | }, 417 | { 418 | "matches": 419 | [ 420 | "https://steamcommunity.com/stats/*/achievements*" 421 | ], 422 | "js": 423 | [ 424 | "scripts/community/achievements.js", 425 | "scripts/community/achievements_global.js" 426 | ], 427 | "css": 428 | [ 429 | "styles/community.css", 430 | "styles/achievements.css" 431 | ] 432 | }, 433 | { 434 | "matches": 435 | [ 436 | "https://steamcommunity.com/tradeoffer/*" 437 | ], 438 | "exclude_matches": 439 | [ 440 | "https://steamcommunity.com/tradeoffer/*/confirm*" 441 | ], 442 | "js": 443 | [ 444 | "scripts/community/tradeoffer.js" 445 | ] 446 | }, 447 | { 448 | "matches": 449 | [ 450 | "https://steamcommunity.com/id/*/recommended/*", 451 | "https://steamcommunity.com/profiles/*/recommended/*" 452 | ], 453 | "js": 454 | [ 455 | "scripts/community/profile_recommended.js" 456 | ], 457 | "css": 458 | [ 459 | "styles/community.css" 460 | ] 461 | }, 462 | { 463 | "matches": 464 | [ 465 | "https://steamcommunity.com/id/*/badges*", 466 | "https://steamcommunity.com/profiles/*/badges*" 467 | ], 468 | "js": 469 | [ 470 | "scripts/community/profile_badges.js" 471 | ], 472 | "css": 473 | [ 474 | "styles/community.css" 475 | ] 476 | }, 477 | { 478 | "matches": 479 | [ 480 | "https://steamcommunity.com/id/*/gamecards/*", 481 | "https://steamcommunity.com/profiles/*/gamecards/*" 482 | ], 483 | "js": 484 | [ 485 | "scripts/community/profile_gamecards.js" 486 | ], 487 | "css": 488 | [ 489 | "styles/community.css" 490 | ] 491 | }, 492 | { 493 | "matches": 494 | [ 495 | "https://steamcommunity.com/app/*", 496 | "https://steamcommunity.com/sharedfiles/filedetails*", 497 | "https://steamcommunity.com/workshop/filedetails*", 498 | "https://steamcommunity.com/workshop/browse*", 499 | "https://steamcommunity.com/workshop/discussions*" 500 | ], 501 | "js": 502 | [ 503 | "scripts/community/gamehub.js" 504 | ], 505 | "css": 506 | [ 507 | "styles/community.css" 508 | ] 509 | }, 510 | { 511 | "matches": 512 | [ 513 | "https://steamcommunity.com/sharedfiles/filedetails*", 514 | "https://steamcommunity.com/workshop/filedetails*" 515 | ], 516 | "js": 517 | [ 518 | "scripts/community/filedetails.js" 519 | ] 520 | }, 521 | { 522 | "all_frames": true, 523 | "matches": 524 | [ 525 | "https://steamcommunity.com/sharedfiles/filedetails*", 526 | "https://steamcommunity.com/workshop/filedetails*" 527 | ], 528 | "js": 529 | [ 530 | "scripts/community/filedetails_guide.js" 531 | ] 532 | }, 533 | { 534 | "run_at": "document_end", 535 | "matches": 536 | [ 537 | "https://steamcommunity.com/market/multibuy*" 538 | ], 539 | "js": 540 | [ 541 | "scripts/community/multibuy.js" 542 | ] 543 | }, 544 | { 545 | "run_at": "document_start", 546 | "matches": 547 | [ 548 | "https://steamcommunity.com/market/*" 549 | ], 550 | "js": 551 | [ 552 | "scripts/community/market.js" 553 | ], 554 | "css": 555 | [ 556 | "styles/market.css" 557 | ] 558 | }, 559 | { 560 | "run_at": "document_end", 561 | "matches": 562 | [ 563 | "https://steamcommunity.com/app/*", 564 | "https://steamcommunity.com/games/*", 565 | "https://steamcommunity.com/sharedfiles/*", 566 | "https://steamcommunity.com/workshop/*" 567 | ], 568 | "js": 569 | [ 570 | "scripts/community/agecheck.js" 571 | ] 572 | }, 573 | { 574 | "all_frames": true, 575 | "matches": 576 | [ 577 | "https://steamcommunity.com/market/*", 578 | "https://steamcommunity.com/id/*/inventory*", 579 | "https://steamcommunity.com/profiles/*/inventory*" 580 | ], 581 | "js": 582 | [ 583 | "scripts/community/market_ssa.js" 584 | ] 585 | } 586 | ] 587 | } 588 | -------------------------------------------------------------------------------- /options/options.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | [hidden] { 8 | display: none !important; 9 | } 10 | 11 | html { 12 | color-scheme: dark; 13 | font-size: 16px; 14 | background: #1b1b24; 15 | accent-color: #00aff4; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | font: 21 | 1rem/1.5 -apple-system, 22 | BlinkMacSystemFont, 23 | "Segoe UI", 24 | Roboto, 25 | Helvetica, 26 | Arial, 27 | sans-serif; 28 | color: #c6d4df; 29 | } 30 | 31 | a { 32 | color: inherit; 33 | } 34 | 35 | a:hover { 36 | color: #fff; 37 | } 38 | 39 | :target { 40 | outline: 3px solid #00aff4; 41 | outline-offset: 5px; 42 | } 43 | 44 | button { 45 | cursor: pointer; 46 | } 47 | 48 | h1, 49 | h2, 50 | h3, 51 | h4 { 52 | margin: 0 0 0.5rem; 53 | font-weight: normal; 54 | line-height: 1; 55 | } 56 | 57 | h1 { 58 | font-size: 3rem; 59 | } 60 | 61 | h2 { 62 | font-size: 2.5rem; 63 | } 64 | 65 | h3 { 66 | margin-top: 3rem; 67 | font-size: 1.75rem; 68 | font-weight: 600; 69 | font-style: italic; 70 | color: #fff; 71 | text-transform: uppercase; 72 | } 73 | 74 | h4 { 75 | font-size: 1.25rem; 76 | } 77 | 78 | h3:first-child { 79 | margin-top: 0; 80 | } 81 | 82 | p { 83 | margin: 0 0 1rem; 84 | } 85 | 86 | summary { 87 | cursor: pointer; 88 | } 89 | 90 | summary:hover { 91 | color: #fff; 92 | text-decoration: underline; 93 | } 94 | 95 | .muted { 96 | font-size: 80%; 97 | color: #999; 98 | } 99 | 100 | .container { 101 | margin: 0 auto; 102 | padding: 1rem 2rem; 103 | width: 100%; 104 | max-width: 34rem; 105 | } 106 | 107 | .built-in-checkboxes { 108 | margin-top: 3rem; 109 | color: #888; 110 | } 111 | 112 | header { 113 | padding-top: 0.5rem; 114 | padding-bottom: 0.5rem; 115 | border-bottom: 1px solid #0197cf; 116 | background-color: #000; 117 | color: #fff; 118 | } 119 | 120 | header .logo { 121 | font-size: 20px; 122 | } 123 | 124 | header svg { 125 | vertical-align: middle; 126 | } 127 | 128 | #localization { 129 | background-color: #16161d; 130 | color: #888; 131 | direction: ltr; 132 | } 133 | 134 | #localization .container { 135 | padding-top: 0.3rem; 136 | padding-bottom: 0.3rem; 137 | } 138 | 139 | footer { 140 | margin-top: 3rem; 141 | padding-top: 3rem; 142 | padding-bottom: 3rem; 143 | border-top: 1px solid #0197cf; 144 | background-color: #000; 145 | color: #34c8fe; 146 | text-align: center; 147 | } 148 | 149 | footer svg { 150 | margin-bottom: 1rem; 151 | } 152 | 153 | footer p { 154 | margin-bottom: 0.5rem; 155 | } 156 | 157 | .checkbox { 158 | display: flex; 159 | gap: 0.4rem; 160 | margin-top: 0.4rem; 161 | } 162 | 163 | :focus, 164 | .checkbox:focus { 165 | outline: solid; 166 | outline-color: #00aff4; 167 | outline-offset: 4px; 168 | outline-width: 2px; 169 | } 170 | 171 | .checkbox > input { 172 | width: 1rem; 173 | height: 1rem; 174 | margin: 0; 175 | margin-top: 0.3rem; 176 | flex-shrink: 0; 177 | } 178 | 179 | #welcome { 180 | padding-bottom: 1rem; 181 | margin-top: 1rem; 182 | color: #34c8fe; 183 | text-wrap: balance; 184 | text-wrap: pretty; 185 | } 186 | 187 | #welcome h2 { 188 | font-size: 1.3rem; 189 | font-weight: bold; 190 | line-height: 1.4; 191 | margin-bottom: 1rem; 192 | } 193 | 194 | #permissions { 195 | background: #000; 196 | color: #fff; 197 | border-bottom: 0.4rem solid red; 198 | margin-top: 1rem; 199 | padding: 1rem; 200 | border-top-left-radius: 1rem; 201 | border-top-right-radius: 1rem; 202 | } 203 | 204 | #star { 205 | background: #000; 206 | color: #fff; 207 | margin-top: 1rem; 208 | padding: 1rem; 209 | border-radius: 1rem; 210 | font-weight: bold; 211 | } 212 | 213 | #js-get-permissions { 214 | appearance: none; 215 | border: 0; 216 | border-radius: 0.5rem; 217 | background: #0064ff; 218 | color: #fff; 219 | font: inherit; 220 | font-weight: bold; 221 | line-height: 1; 222 | padding: 0.8rem 1rem; 223 | width: 100%; 224 | } 225 | 226 | #js-get-permissions:focus, 227 | #js-get-permissions:hover { 228 | background: #00f; 229 | color: #fff; 230 | } 231 | 232 | @media (max-width: 34rem) { 233 | .container { 234 | padding: 1rem; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Options · SteamDB 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | See an issue with localization? Translate on Crowdin 29 |
30 |
31 | 32 |
33 | 42 | 43 |
44 |

45 | 46 |
47 | 48 | 49 | 50 |

51 | 52 |
53 | 54 |

55 |

56 |

57 |

58 |
59 | 60 |
61 | 62 |
63 | 64 | 68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | 83 |

84 | 85 |
86 | 87 |

88 |

89 |

90 |
91 | 92 | 96 | 97 | 101 | 102 | 106 | 107 | 111 | 112 |

113 | 114 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 146 | 150 | 151 |

152 | 153 | 157 |
158 | 159 |
160 | 161 | 165 |
166 |
167 | 171 |
172 | 173 |
174 | 175 | 179 |
180 |
181 | 185 |
186 | 187 |
188 |
189 | 190 |

191 | 192 | 199 | 203 |
204 | 205 |
206 |
207 | 208 |

209 | 210 |
211 | 212 |
213 | 214 | 218 | 222 | 226 |
227 | 228 |
229 |
230 |
231 | 232 |
233 |
234 |
235 |
236 |
237 | 238 |
239 |
240 |
241 | 242 |
243 |
244 |
245 | 246 |
247 |
248 |
249 |
250 | 251 |

252 | 253 | 260 | 264 | 268 | 272 | 276 | 280 | 284 | 291 | 298 |
299 | 300 |
301 |
302 |
303 | 304 |
305 |
306 |
307 | 308 |
309 |
310 |
311 | 312 |
313 |
314 |
315 | 316 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | document.body.dir = _t( '@@bidi_dir' ); 6 | 7 | /** @type {NodeListOf} */ 8 | const localizable = document.querySelectorAll( '[data-msg]' ); 9 | 10 | for( const element of localizable ) 11 | { 12 | const token = element.dataset.msg; 13 | let msg = null; 14 | 15 | if( token === 'options_extra_data_players' ) 16 | { 17 | msg = _t( token, [ _t( 'options_online_stats' ), _t( 'options_steamdb_last_update' ) ] ); 18 | } 19 | else if( token === 'options_extra_data_prices' ) 20 | { 21 | msg = _t( token, [ _t( 'options_steamdb_lowest_price' ) ] ); 22 | } 23 | else if( token === 'options_extra_data_achievement_groups' ) 24 | { 25 | msg = _t( token, [ _t( 'options_achievement_groups' ) ] ); 26 | } 27 | else 28 | { 29 | msg = _t( token ); 30 | } 31 | 32 | if( !msg ) 33 | { 34 | console.error( 'Missing localization', element, token ); 35 | } 36 | 37 | if( element.tagName === 'TITLE' ) 38 | { 39 | msg += ' · SteamDB'; 40 | } 41 | 42 | element.innerHTML = msg; 43 | } 44 | 45 | if( location.search.includes( 'welcome=1' ) ) 46 | { 47 | document.getElementById( 'welcome' ).hidden = false; 48 | } 49 | 50 | let starDismissed = false; 51 | 52 | /** @type {NodeListOf} */ 53 | const checkboxes = document.querySelectorAll( '.option-check:not(:disabled)' ); 54 | 55 | /** @type {Record} */ 56 | const options = 57 | { 58 | 'clicked-star': null, 59 | }; 60 | 61 | /** 62 | * @this {HTMLInputElement} 63 | * @param {Event} e 64 | */ 65 | const CheckboxChange = function( e ) 66 | { 67 | if( !e.isTrusted ) 68 | { 69 | return; 70 | } 71 | 72 | this.disabled = true; 73 | 74 | SetOption( this.dataset.option, this.checked ); 75 | }; 76 | 77 | for( let i = 0; i < checkboxes.length; i++ ) 78 | { 79 | const element = checkboxes[ i ]; 80 | const item = element.dataset.option; 81 | 82 | if( !options[ item ] ) 83 | { 84 | options[ item ] = [ element ]; 85 | } 86 | else 87 | { 88 | options[ item ].push( element ); 89 | } 90 | 91 | element.addEventListener( 'change', CheckboxChange ); 92 | } 93 | 94 | GetOption( Object.keys( options ), ( items ) => 95 | { 96 | for( const item in items ) 97 | { 98 | if( item === 'clicked-star' ) 99 | { 100 | starDismissed = true; 101 | document.getElementById( 'star' ).hidden = true; 102 | continue; 103 | } 104 | 105 | for( const element of options[ item ] ) 106 | { 107 | element.checked = items[ item ]; 108 | } 109 | } 110 | } ); 111 | 112 | ExtensionApi.storage.sync.onChanged.addListener( ( changes ) => 113 | { 114 | const changedItems = Object.keys( changes ); 115 | 116 | for( const item of changedItems ) 117 | { 118 | if( options[ item ] ) 119 | { 120 | for( const element of options[ item ] ) 121 | { 122 | element.checked = !!changes[ item ].newValue; 123 | element.disabled = false; 124 | } 125 | } 126 | } 127 | } ); 128 | 129 | // Must be synced with host_permissions in manifest.json 130 | const permissions = { 131 | origins: [ 132 | 'https://steamdb.info/*', 133 | 'https://steamcommunity.com/*', 134 | 'https://*.steampowered.com/*', 135 | ], 136 | }; 137 | 138 | document.getElementById( 'js-get-permissions' ).addEventListener( 'click', ( e ) => 139 | { 140 | e.preventDefault(); 141 | 142 | try 143 | { 144 | ExtensionApi.permissions.request( permissions ).catch( e => 145 | { 146 | alert( `Failed to request permissions: ${e.message}` ); 147 | } ); 148 | } 149 | catch( e ) 150 | { 151 | alert( `Failed to request permissions: ${e}` ); 152 | } 153 | } ); 154 | 155 | ExtensionApi.permissions.onAdded.addListener( HideButtonIfAllPermissionsGranted ); 156 | ExtensionApi.permissions.onRemoved.addListener( HideButtonIfAllPermissionsGranted ); 157 | 158 | HideButtonIfAllPermissionsGranted(); 159 | 160 | function HideButtonIfAllPermissionsGranted() 161 | { 162 | ExtensionApi.permissions.contains( permissions, ( result ) => 163 | { 164 | document.getElementById( 'permissions' ).hidden = result; 165 | document.getElementById( 'star' ).hidden = starDismissed || !result; 166 | } ); 167 | } 168 | 169 | let storeHref = null; 170 | 171 | if( ExtensionApi.runtime.id === 'kdbmhfkmnlmbkgbabkdealhhbfhlmmon' ) 172 | { 173 | storeHref = 'https://chromewebstore.google.com/detail/steamdb/kdbmhfkmnlmbkgbabkdealhhbfhlmmon?utm_source=Options'; 174 | } 175 | else if( ExtensionApi.runtime.id === 'hjknpdomhlodgaebegjopkmfafjpbblg' ) 176 | { 177 | storeHref = 'https://microsoftedge.microsoft.com/addons/detail/steamdb/hjknpdomhlodgaebegjopkmfafjpbblg?utm_source=Options'; 178 | } 179 | else if( ExtensionApi.runtime.id === 'firefox-extension@steamdb.info' ) 180 | { 181 | storeHref = 'https://addons.mozilla.org/firefox/addon/steam-database/?utm_source=Options'; 182 | } 183 | 184 | /** @type {HTMLAnchorElement} */ 185 | const storeUrl = document.querySelector( '#star a' ); 186 | storeUrl.addEventListener( 'click', () => 187 | { 188 | starDismissed = true; 189 | SetOption( 'clicked-star', true ); 190 | document.getElementById( 'star' ).hidden = true; 191 | } ); 192 | 193 | if( storeHref !== null ) 194 | { 195 | storeUrl.href = storeHref; 196 | } 197 | } )() ); 198 | -------------------------------------------------------------------------------- /options/popup.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | --bg-color: #000; 9 | --link-color: #fff; 10 | --link-color-hover: #34c8fe; 11 | } 12 | 13 | body { 14 | min-width: min-content; 15 | margin: 0; 16 | padding: 0; 17 | background: var(--bg-color); 18 | color: var(--link-color); 19 | font: 20 | 1rem/1.5 -apple-system, 21 | BlinkMacSystemFont, 22 | "Segoe UI", 23 | Roboto, 24 | Helvetica, 25 | Arial, 26 | sans-serif; 27 | } 28 | 29 | .links { 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .link { 35 | display: flex; 36 | align-items: center; 37 | padding: 0.5em 1em; 38 | gap: 0.7em; 39 | text-decoration: none; 40 | color: inherit; 41 | 42 | > svg { 43 | width: 1em; 44 | height: 1em; 45 | flex-shrink: 0; 46 | } 47 | 48 | &:hover { 49 | color: var(--link-color-hover); 50 | background-color: rgba(0 100 255 / 20%); 51 | 52 | > b, 53 | > span { 54 | text-decoration: underline; 55 | } 56 | } 57 | 58 | &.options-link { 59 | color: var(--link-color-hover); 60 | } 61 | } 62 | 63 | .separator { 64 | height: 1px; 65 | background: rgba(255 255 255 / 30%); 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | --bg-color: #fff; 71 | --link-color: #000; 72 | --link-color-hover: #00f; 73 | } 74 | 75 | .separator { 76 | background: rgba(0 0 0 / 30%); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /options/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SteamDB 6 | 7 | 8 | 9 | 10 | 11 | 12 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /options/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( () => 4 | { 5 | const browserApi = typeof browser !== 'undefined' ? browser : chrome; 6 | 7 | document.body.dir = browserApi.i18n.getMessage( '@@bidi_dir' ); 8 | 9 | /** @type {NodeListOf} */ 10 | const localizable = document.querySelectorAll( '[data-msg]' ); 11 | 12 | for( const element of localizable ) 13 | { 14 | element.textContent = browserApi.i18n.getMessage( element.dataset.msg ); 15 | } 16 | 17 | document.getElementById( 'options-link' ).addEventListener( 'click', function( ev ) 18 | { 19 | ev.preventDefault(); 20 | 21 | browserApi.runtime.openOptionsPage(); 22 | } ); 23 | } )(); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "steamdb-browser-extension", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "start:chrome": "web-ext run --target chromium --source-dir ./", 8 | "start:firefox": "web-ext run --firefox --source-dir ./", 9 | "start:firefoxdev": "web-ext run --firefox=firefoxdeveloperedition --source-dir ./", 10 | "build": "node build.js", 11 | "version": "node version.js", 12 | "test": "eslint . && stylelint \"**/*.css\" && prettier \"**/*.css\" --check && tsc", 13 | "test:ext": "web-ext lint", 14 | "test:ts": "tsc", 15 | "fix": "eslint . --fix && stylelint \"**/*.css\" --fix && prettier \"**/*.css\" --write" 16 | }, 17 | "devDependencies": { 18 | "@types/chrome": "^0.0.309", 19 | "@types/firefox-webext-browser": "^120.0.3", 20 | "archiver": "^7.0.1", 21 | "eslint": "^9.10.0", 22 | "eslint-plugin-jsdoc": "^50.6.3", 23 | "prettier": "^3.3.2", 24 | "stylelint": "^16.5.0", 25 | "stylelint-config-standard": "^37.0.0", 26 | "typescript": "^5.7.3", 27 | "web-ext": "^8.0.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/SteamDatabase/BrowserExtension.git" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/appicon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 4 | 'enhancement-appicon': true, 5 | }, ( items ) => 6 | { 7 | if( items[ 'enhancement-appicon' ] ) 8 | { 9 | let styleAdded = false; 10 | const style = document.createElement( 'link' ); 11 | style.id = 'steamdb_appicon'; 12 | style.type = 'text/css'; 13 | style.rel = 'stylesheet'; 14 | style.href = GetLocalResource( 'styles/appicon.css' ); 15 | 16 | if( document.head ) 17 | { 18 | styleAdded = true; 19 | document.head.appendChild( style ); 20 | } 21 | 22 | window.addEventListener( 'DOMContentLoaded', () => 23 | { 24 | if( !styleAdded ) 25 | { 26 | document.head.appendChild( style ); 27 | } 28 | 29 | /** @type {HTMLImageElement} */ 30 | const icon = document.querySelector( '.apphub_AppIcon > img' ); 31 | 32 | if( !icon ) 33 | { 34 | return; 35 | } 36 | 37 | const src = icon.getAttribute( 'src' ); 38 | 39 | if( !src.includes( '%CDN_HOST_MEDIA_SSL%' ) ) 40 | { 41 | return; 42 | } 43 | 44 | const applicationConfigElement = document.getElementById( 'application_config' ); 45 | 46 | if( !applicationConfigElement ) 47 | { 48 | return; 49 | } 50 | 51 | const applicationConfig = JSON.parse( applicationConfigElement.dataset.config ); 52 | 53 | icon.src = src.replace( 'https://%CDN_HOST_MEDIA_SSL%/', applicationConfig.MEDIA_CDN_URL ); 54 | } ); 55 | } 56 | } ); 57 | -------------------------------------------------------------------------------- /scripts/common.d.ts: -------------------------------------------------------------------------------- 1 | declare var ExtensionApi: typeof browser | typeof chrome; 2 | 3 | declare var CurrentAppID: number; 4 | 5 | declare function GetCurrentAppID(): number; 6 | 7 | declare function GetHomepage(): string; 8 | 9 | declare function _t(message: string, substitutions?: string[]): string; 10 | 11 | declare function GetLanguage(): string; 12 | 13 | declare type GetOptionCallback = (items: { [key: string]: any }) => void; 14 | 15 | declare function GetOption(items: { [key: string]: any }, callback: GetOptionCallback): void; 16 | 17 | declare function SetOption(option: string, value: any): void; 18 | 19 | declare function GetLocalResource(res: string): string; 20 | 21 | declare type SendMessageToBackgroundScriptResponse = { 22 | success: boolean; 23 | error?: string; 24 | data?: any; 25 | }; 26 | 27 | declare type SendMessageToBackgroundScriptCallback = (data: SendMessageToBackgroundScriptResponse | undefined) => void; 28 | 29 | declare function SendMessageToBackgroundScript( 30 | message: { contentScriptQuery: string, [key: string]: any }, 31 | callback: SendMessageToBackgroundScriptCallback 32 | ): void; 33 | 34 | declare function WriteLog(...args: any[]): void; 35 | -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | /* exported _t, ExtensionApi, GetCurrentAppID, GetHomepage, GetOption, GetLanguage, GetLocalResource, SendMessageToBackgroundScript, SetOption, WriteLog */ 2 | 3 | 'use strict'; 4 | 5 | /** @type {chrome|browser} ExtensionApi */ 6 | // eslint-disable-next-line no-var 7 | var ExtensionApi = ( () => 8 | { 9 | if( typeof browser !== 'undefined' && typeof browser.storage !== 'undefined' ) 10 | { 11 | return browser; 12 | } 13 | else if( typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' ) 14 | { 15 | return chrome; 16 | } 17 | 18 | throw new Error( 'Did not find appropriate web extensions api' ); 19 | } )(); 20 | 21 | // exported variable needs to be `var` 22 | /** @type {number|undefined} */ 23 | // eslint-disable-next-line no-var 24 | var CurrentAppID; 25 | 26 | /** 27 | * @param {string} url 28 | * @returns {number} 29 | */ 30 | function GetAppIDFromUrl( url ) 31 | { 32 | const appid = url.match( /\/(?:app|sub|bundle|friendsthatplay|gamecards|recommended|widget)\/(?[0-9]+)/ ); 33 | 34 | return appid ? Number.parseInt( appid.groups.id, 10 ) : -1; 35 | } 36 | 37 | function GetCurrentAppID() 38 | { 39 | if( !CurrentAppID ) 40 | { 41 | CurrentAppID = GetAppIDFromUrl( location.pathname ); 42 | } 43 | 44 | return CurrentAppID; 45 | } 46 | 47 | function GetHomepage() 48 | { 49 | return 'https://steamdb.info/'; 50 | } 51 | 52 | /** 53 | * @param {string} message 54 | * @param {string[]} substitutions 55 | * @returns {string} 56 | */ 57 | function _t( message, substitutions = [] ) 58 | { 59 | return ExtensionApi.i18n.getMessage( message, substitutions ); 60 | } 61 | 62 | function GetLanguage() 63 | { 64 | return ExtensionApi.i18n.getUILanguage(); 65 | } 66 | 67 | /** 68 | * @callback GetOptionCallback 69 | * @param {{[key: string]: any}} items 70 | */ 71 | 72 | /** 73 | * @param {{[key: string]: any}} items 74 | * @param {GetOptionCallback} callback 75 | */ 76 | function GetOption( items, callback ) 77 | { 78 | ExtensionApi.storage.sync.get( items ).then( callback ); 79 | } 80 | 81 | /** 82 | * @param {string} option 83 | * @param {any} value 84 | */ 85 | function SetOption( option, value ) 86 | { 87 | /** @type {{ [key: string]: any }} */ 88 | const obj = {}; 89 | obj[ option ] = value; 90 | 91 | ExtensionApi.storage.sync.set( obj ); 92 | } 93 | 94 | /** 95 | * @param {string} res 96 | */ 97 | function GetLocalResource( res ) 98 | { 99 | return ExtensionApi.runtime.getURL( res ); 100 | } 101 | 102 | /** 103 | * @callback SendMessageToBackgroundScriptCallback 104 | * @param {{success: boolean, error?: string, data?: any}?} data 105 | */ 106 | 107 | /** 108 | * @param {{contentScriptQuery: string, [key: string]: any}} message 109 | * @param {SendMessageToBackgroundScriptCallback} callback 110 | */ 111 | function SendMessageToBackgroundScript( message, callback ) 112 | { 113 | /** @param {Error} error */ 114 | const errorCallback = ( error ) => callback( { success: false, error: error.message } ); 115 | 116 | try 117 | { 118 | ExtensionApi.runtime 119 | // @ts-ignore - janky type definitions 120 | .sendMessage( message ) 121 | .then( callback ) 122 | .catch( errorCallback ); 123 | } 124 | catch( error ) 125 | { 126 | if( error instanceof Error ) 127 | { 128 | errorCallback( error ); 129 | } 130 | else 131 | { 132 | errorCallback( new Error( String( error ) ) ); 133 | } 134 | } 135 | } 136 | 137 | function WriteLog( ) 138 | { 139 | console.log( '%c[SteamDB]%c', 'color:#2196F3; font-weight:bold;', '', ...arguments ); 140 | } 141 | -------------------------------------------------------------------------------- /scripts/community/achievements.d.ts: -------------------------------------------------------------------------------- 1 | declare function DoAchievements(isPersonal: boolean): void; 2 | declare function StartViewTransition(callback: ViewTransitionUpdateCallback): void; 3 | -------------------------------------------------------------------------------- /scripts/community/achievements_cs2.js: -------------------------------------------------------------------------------- 1 | /* global StartViewTransition */ 2 | 3 | 'use strict'; 4 | 5 | const tierColors = 6 | [ 7 | '#b0c3d9', 8 | '#8cc6ff', 9 | '#6a7dff', 10 | '#c166ff', 11 | '#f03cff', 12 | '#eb4b4b', 13 | '#ffd700', 14 | ]; 15 | 16 | /** 17 | * @typedef {object[]} CSRArray 18 | * @property {number} csr 19 | * @property {string} datetime 20 | * @property {string} season 21 | * @property {number} [delta] 22 | */ 23 | 24 | /** 25 | * @param {Element} container 26 | * @param {CSRArray} initialData 27 | */ 28 | const InitChart = ( container, initialData ) => 29 | { 30 | let maxLength = 200; 31 | 32 | const canvas = document.createElement( 'canvas' ); 33 | canvas.className = 'steamdb_achievements_csrating_graph'; 34 | container.append( canvas ); 35 | 36 | const ctx = canvas.getContext( '2d' ); 37 | ctx.font = '16px "Motiva Sans", sans-serif'; 38 | 39 | const tooltip = document.createElement( 'div' ); 40 | tooltip.className = 'community_tooltip steamdb_achievements_csrating_graph_tooltip'; 41 | document.body.append( tooltip ); 42 | 43 | canvas.addEventListener( 'mousemove', ( event ) => 44 | { 45 | const gap = canvas.offsetWidth / ( Math.min( initialData.length, maxLength ) - 1 ); 46 | const x = event.offsetX - ( gap / 2 ); 47 | const index = Math.ceil( x / gap ); 48 | DrawChart( initialData, index, canvas, tooltip, maxLength ); 49 | tooltip.style.display = 'block'; 50 | 51 | const tooltipWidth = tooltip.clientWidth; 52 | const shiftTooltip = event.pageX + tooltipWidth - document.body.clientWidth; 53 | tooltip.style.left = shiftTooltip > 0 54 | ? event.pageX - shiftTooltip + 'px' 55 | : event.pageX + 'px'; 56 | tooltip.style.top = event.pageY + 30 + 'px'; 57 | } ); 58 | 59 | const resetCanvas = () => 60 | { 61 | DrawChart( initialData, -1, canvas, tooltip, maxLength ); 62 | tooltip.style.display = 'none'; 63 | }; 64 | canvas.addEventListener( 'mouseleave', resetCanvas ); 65 | window.addEventListener( 'resize', resetCanvas ); 66 | 67 | maxLength = Math.min( maxLength, initialData.length ); 68 | 69 | /** @type {HTMLInputElement} */ 70 | const maxLengthInput = document.createElement( 'input' ); 71 | maxLengthInput.className = 'steamdb_achievements_csrating_graph_slider'; 72 | maxLengthInput.type = 'range'; 73 | maxLengthInput.min = '2'; 74 | maxLengthInput.max = initialData.length.toString(); 75 | maxLengthInput.value = maxLength.toString(); 76 | maxLengthInput.addEventListener( 'input', () => 77 | { 78 | maxLength = Number.parseInt( maxLengthInput.value, 10 ); 79 | DrawChart( initialData, -1, canvas, tooltip, maxLength ); 80 | } ); 81 | canvas.insertAdjacentElement( 'afterend', maxLengthInput ); 82 | 83 | return { canvas, tooltip, maxLength }; 84 | }; 85 | 86 | /** 87 | * @param {CSRArray} initialData 88 | * @param {number} hoveredIndex 89 | * @param {HTMLCanvasElement} canvas 90 | * @param {HTMLDivElement} tooltip 91 | * @param {number} maxLength 92 | */ 93 | const DrawChart = ( initialData, hoveredIndex, canvas, tooltip, maxLength ) => 94 | { 95 | const data = initialData.slice( 0, maxLength ).reverse(); 96 | const maxCSR = data.reduce( ( a, b ) => a.csr > b.csr ? a : b ).csr; 97 | 98 | const rect = canvas.getBoundingClientRect(); 99 | const width = rect.width * devicePixelRatio; 100 | const height = rect.height * devicePixelRatio; 101 | 102 | const ctx = canvas.getContext( '2d' ); 103 | 104 | // Setting size clears the canvas 105 | canvas.width = width; 106 | canvas.height = height; 107 | 108 | // Draw gradient 109 | let i = 0; 110 | let lastTier = -1; 111 | let lastSeason = data[ 0 ].season; 112 | const paddedHeight = height * 0.95; 113 | const halfHeight = height / 2; 114 | const gap = width / ( data.length - 1 ); 115 | 116 | 117 | /** @type {{season: string, x: number}[]} */ 118 | const seasonChanges = []; 119 | 120 | ctx.beginPath(); 121 | ctx.moveTo( 0, height ); 122 | 123 | for( const point of data ) 124 | { 125 | const val = 2 * ( point.csr / maxCSR - 0.5 ); 126 | const x = i * gap; 127 | const y = ( -val * paddedHeight ) / 2 + halfHeight; 128 | 129 | const tier = Math.min( Math.floor( point.csr / 5000 ), tierColors.length - 1 ); 130 | 131 | if( lastTier !== tier ) 132 | { 133 | if( i > 0 ) 134 | { 135 | ctx.lineTo( x, y ); 136 | ctx.lineTo( x, height ); 137 | ctx.fill(); 138 | ctx.beginPath(); 139 | ctx.moveTo( x, height ); 140 | } 141 | 142 | const grd = ctx.createLinearGradient( 0, 0, 0, height ); 143 | grd.addColorStop( 0, tierColors[ tier ] + '22' ); 144 | grd.addColorStop( 1, 'transparent' ); 145 | ctx.fillStyle = grd; 146 | 147 | ctx.lineTo( x, y ); 148 | 149 | lastTier = tier; 150 | } 151 | else 152 | { 153 | ctx.lineTo( x, y ); 154 | } 155 | 156 | if( lastSeason !== point.season ) 157 | { 158 | lastSeason = point.season; 159 | 160 | seasonChanges.push( { 161 | x, 162 | season: lastSeason, 163 | } ); 164 | } 165 | 166 | i += 1; 167 | } 168 | 169 | ctx.lineTo( width, height ); 170 | ctx.fill(); 171 | 172 | // Max tier dashed line 173 | ctx.strokeStyle = '#424857'; 174 | ctx.lineWidth = 1 * devicePixelRatio; 175 | ctx.setLineDash( [ 7 * devicePixelRatio, 4 * devicePixelRatio ] ); 176 | ctx.fillStyle = '#999'; 177 | i = 0; 178 | 179 | for( let maxCSRClean = maxCSR - ( maxCSR % 5000 ); maxCSRClean >= 5000 && i < 2; maxCSRClean -= 5000, i++ ) 180 | { 181 | const maxCSRTier = 2 * ( maxCSRClean / maxCSR - 0.5 ); 182 | const maxCSRTierY = ( -maxCSRTier * paddedHeight ) / 2 + halfHeight; 183 | 184 | ctx.beginPath(); 185 | ctx.moveTo( 0, maxCSRTierY ); 186 | ctx.lineTo( width, maxCSRTierY ); 187 | ctx.stroke(); 188 | 189 | ctx.fillText( `${( maxCSRClean / 1000 ).toFixed( 0 )}k`, 0, maxCSRTierY < 12 ? maxCSRTierY + 12 : maxCSRTierY - 4 ); 190 | } 191 | 192 | ctx.setLineDash( [] ); 193 | 194 | // Draw season changes 195 | for( const season of seasonChanges ) 196 | { 197 | ctx.beginPath(); 198 | ctx.moveTo( season.x, 0 ); 199 | ctx.lineTo( season.x, height ); 200 | ctx.stroke(); 201 | 202 | ctx.fillText( `Season ${season.season}`, season.x + 5, height - 5 ); 203 | } 204 | 205 | // Draw line 206 | ctx.beginPath(); 207 | ctx.lineWidth = 2 * devicePixelRatio; 208 | 209 | let circleX = null; 210 | let circleY = null; 211 | let highlightedCSR = 0; 212 | let highlightedDate = ''; 213 | i = 0; 214 | lastTier = -1; 215 | 216 | for( const point of data ) 217 | { 218 | const val = 2 * ( point.csr / maxCSR - 0.5 ); 219 | const x = i * gap; 220 | const y = ( -val * paddedHeight ) / 2 + halfHeight; 221 | const tier = Math.min( Math.floor( point.csr / 5000 ), tierColors.length - 1 ); 222 | 223 | if( lastTier !== tier ) 224 | { 225 | if( i > 0 ) 226 | { 227 | ctx.lineTo( x, y ); 228 | ctx.stroke(); 229 | ctx.beginPath(); 230 | } 231 | 232 | ctx.strokeStyle = tierColors[ tier ]; 233 | ctx.moveTo( x, y ); 234 | 235 | lastTier = tier; 236 | } 237 | else 238 | { 239 | ctx.lineTo( x, y ); 240 | } 241 | 242 | if( hoveredIndex === i ) 243 | { 244 | circleX = x; 245 | circleY = y; 246 | highlightedCSR = point.csr; 247 | highlightedDate = point.datetime; 248 | } 249 | 250 | i += 1; 251 | } 252 | 253 | ctx.stroke(); 254 | 255 | if( circleX !== null && circleY !== null ) 256 | { 257 | ctx.beginPath(); 258 | ctx.fillStyle = '#fff'; 259 | ctx.arc( circleX, circleY, 3 * devicePixelRatio, 0, Math.PI * 2 ); 260 | ctx.fill(); 261 | tooltip.textContent = `${highlightedCSR.toLocaleString()}\n${highlightedDate}`; 262 | } 263 | }; 264 | 265 | /** 266 | * @param {Element} container 267 | * @param {CSRArray} rows 268 | */ 269 | const CreateCSRatingTable = ( container, rows ) => 270 | { 271 | const table = document.createElement( 'table' ); 272 | table.className = 'steamdb_achievements_csrating'; 273 | container.append( table ); 274 | 275 | const CreateHeader = () => 276 | { 277 | const header = document.createElement( 'tr' ); 278 | 279 | const headerTdDatetime = document.createElement( 'th' ); 280 | headerTdDatetime.textContent = _t( 'achievements_csrating_date' ); 281 | const headerTdCSR = document.createElement( 'th' ); 282 | headerTdCSR.textContent = _t( 'achievements_csrating_name' ); 283 | const headerTdCSRdelta = document.createElement( 'th' ); 284 | headerTdCSRdelta.textContent = 'Δ'; 285 | header.append( headerTdDatetime, headerTdCSR, headerTdCSRdelta ); 286 | 287 | return header; 288 | }; 289 | 290 | let prevScore = 0; 291 | for( let i = rows.length - 1; i >= 0; i-- ) 292 | { 293 | if( prevScore !== 0 ) 294 | { 295 | rows[ i ].delta = rows[ i ].csr - prevScore; 296 | } 297 | 298 | prevScore = rows[ i ].csr; 299 | } 300 | 301 | const tbody = document.createElement( 'tbody' ); 302 | table.append( tbody ); 303 | 304 | let season; 305 | 306 | for( const row of rows ) 307 | { 308 | if( season !== row.season ) 309 | { 310 | season = row.season; 311 | const tr = document.createElement( 'tr' ); 312 | tbody.append( tr ); 313 | 314 | const th = document.createElement( 'th' ); 315 | th.textContent = _t( 'achievements_csrating_season', [ season ] ); 316 | th.colSpan = 3; 317 | th.className = 'steamdb_achievements_csrating_season'; 318 | tr.append( th ); 319 | 320 | tbody.append( CreateHeader() ); 321 | } 322 | 323 | const tr = document.createElement( 'tr' ); 324 | tbody.append( tr ); 325 | 326 | const datetime = document.createElement( 'td' ); 327 | datetime.textContent = row.datetime; 328 | tr.append( datetime ); 329 | 330 | const csr = document.createElement( 'td' ); 331 | csr.textContent = row.csr.toLocaleString(); 332 | const tier = Math.min( Math.floor( row.csr / 5000 ), tierColors.length - 1 ); 333 | csr.className = 'steamdb_achievements_csrating-value'; 334 | csr.style.color = tierColors[ tier ]; 335 | tr.append( csr ); 336 | 337 | const delta = document.createElement( 'td' ); 338 | 339 | if( row.delta ) 340 | { 341 | delta.textContent = ( row.delta > 0 ? '+' : '' ) + row.delta; 342 | } 343 | 344 | delta.className = row.delta > 0 345 | ? 'steamdb_achievements_csrating_positive' 346 | : 'steamdb_achievements_csrating_negative'; 347 | 348 | if( row.delta < -199 || row.delta > 199 ) 349 | { 350 | delta.classList.add( 'steamdb_achievements_csrating_significant' ); 351 | } 352 | 353 | tr.append( delta ); 354 | } 355 | }; 356 | 357 | /** 358 | * @param {string} profileUrl 359 | */ 360 | const FetchCSRating = async( profileUrl ) => 361 | { 362 | const res = await fetch( `https://steamcommunity.com${profileUrl}/gcpd/730?tab=majors&ajax=1` ); 363 | const json = await res.json(); 364 | 365 | const parser = new DOMParser(); 366 | const dom = parser.parseFromString( json.html, 'text/html' ); 367 | 368 | const rows = [ ...dom.querySelectorAll( 'tr' ) ] 369 | .filter( tr => tr.querySelector( 'td' )?.textContent.startsWith( 'premier' ) ); 370 | 371 | const dateFormatter = new Intl.DateTimeFormat( GetLanguage(), { 372 | dateStyle: 'medium', 373 | timeStyle: 'short', 374 | } ); 375 | 376 | /** @type {CSRArray} */ 377 | const premierRows = []; 378 | 379 | for( const row of rows ) 380 | { 381 | premierRows.push( { 382 | season: row.querySelector( 'td' ).textContent.replace( 'premier_season', '' ), 383 | datetime: dateFormatter.format( 384 | new Date( row.querySelector( 'td:nth-child(2)' ).textContent ).getTime(), 385 | ), 386 | csr: Number( row.querySelector( 'td:nth-child(3)' ).textContent ) >> 15, 387 | } ); 388 | } 389 | 390 | if( premierRows.length < 1 ) 391 | { 392 | return; 393 | } 394 | 395 | const summary = document.createElement( 'details' ); 396 | summary.open = true; 397 | 398 | const summaryName = document.createElement( 'summary' ); 399 | summaryName.className = 'steamdb_achievements_game_name steamdb_achievements_csrating_fold'; 400 | summaryName.textContent = _t( 'achievements_csrating_name' ); 401 | summary.append( summaryName ); 402 | 403 | let chart = null; 404 | 405 | if( premierRows.length > 1 ) 406 | { 407 | chart = InitChart( summary, premierRows ); 408 | } 409 | 410 | CreateCSRatingTable( summary, premierRows ); 411 | 412 | StartViewTransition( () => 413 | { 414 | document.querySelector( '#mainContents' ).append( summary ); 415 | 416 | if( chart !== null ) 417 | { 418 | DrawChart( premierRows, -1, chart.canvas, chart.tooltip, chart.maxLength ); 419 | } 420 | } ); 421 | }; 422 | 423 | /** 424 | * @param {string} str 425 | */ 426 | const removeTrailingSlash = ( str ) => str.endsWith( '/' ) ? str.slice( 0, -1 ) : str; 427 | 428 | const viewingProfile = removeTrailingSlash( /** @type {HTMLAnchorElement} */ ( document.querySelector( '.pagecontent .persona_name_text_content' ) )?.pathname ?? '' ); 429 | const myProfile = removeTrailingSlash( /** @type {HTMLAnchorElement} */ ( document.querySelector( '#global_actions .user_avatar' ) )?.pathname ?? '' ); 430 | 431 | if( viewingProfile === myProfile ) 432 | { 433 | FetchCSRating( myProfile ); 434 | } 435 | -------------------------------------------------------------------------------- /scripts/community/achievements_global.js: -------------------------------------------------------------------------------- 1 | /* global DoAchievements */ 2 | 3 | 'use strict'; 4 | 5 | DoAchievements( false ); 6 | 7 | { 8 | /** @type {HTMLAnchorElement} */ 9 | const currentUser = document.querySelector( '#global_actions .user_avatar' ); 10 | const currentUserPath = location.pathname.split( '/' ); 11 | 12 | if( currentUser && currentUserPath[ 1 ] === 'stats' ) 13 | { 14 | const currentUserUrl = currentUser.href.replace( /\/$/, '' ); 15 | 16 | const tab = document.createElement( 'div' ); 17 | tab.className = 'tab steamdb_stats_tab'; 18 | 19 | const link = document.createElement( 'a' ); 20 | link.className = 'tabOff'; 21 | link.href = `${currentUserUrl}/stats/${currentUserPath[ 2 ]}?tab=achievements`; 22 | link.textContent = _t( 'view_your_achievements' ); 23 | 24 | tab.appendChild( link ); 25 | document.querySelector( '#tabs' )?.appendChild( tab ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/community/achievements_profile.js: -------------------------------------------------------------------------------- 1 | /* global DoAchievements */ 2 | 3 | 'use strict'; 4 | 5 | DoAchievements( true ); 6 | -------------------------------------------------------------------------------- /scripts/community/agecheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'enhancement-skip-agecheck': false }, ( items ) => 4 | { 5 | if( items[ 'enhancement-skip-agecheck' ] ) 6 | { 7 | const element = document.createElement( 'script' ); 8 | element.id = 'steamdb_skip_agecheck'; 9 | element.type = 'text/javascript'; 10 | element.src = GetLocalResource( 'scripts/community/agecheck_injected.js' ); 11 | 12 | if( document.head ) 13 | { 14 | document.head.insertBefore( element, document.head.firstChild ); 15 | } 16 | else 17 | { 18 | document.documentElement.appendChild( element ); 19 | } 20 | } 21 | } ); 22 | -------------------------------------------------------------------------------- /scripts/community/agecheck_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( 'AcceptAppHub' in window && 'Proceed' in window ) 6 | { 7 | window.Proceed(); 8 | } 9 | } )() ); 10 | -------------------------------------------------------------------------------- /scripts/community/filedetails.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 4 | 'enhancement-award-popup-url': true, 5 | }, ( items ) => 6 | { 7 | if( items[ 'enhancement-award-popup-url' ] && window.location.search.includes( 'award' ) ) 8 | { 9 | const script = document.createElement( 'script' ); 10 | script.id = 'steamdb_filedetails_award'; 11 | script.type = 'text/javascript'; 12 | script.src = GetLocalResource( 'scripts/community/filedetails_award_injected.js' ); 13 | document.head.appendChild( script ); 14 | } 15 | } ); 16 | -------------------------------------------------------------------------------- /scripts/community/filedetails_award_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( !( 'PublishedFileAward' in window ) ) 6 | { 7 | return; 8 | } 9 | 10 | const params = new URLSearchParams( window.location.search ); 11 | const awardId = params.get( 'award' ); 12 | 13 | if( awardId === null ) 14 | { 15 | return; 16 | } 17 | 18 | const button = document.querySelector( '.general_btn[onClick^="PublishedFileAward"]' ); 19 | 20 | if( !button ) 21 | { 22 | console.log( '[SteamDB] Failed to find PublishedFileAward button' ); 23 | return; 24 | } 25 | 26 | const data = button.getAttribute( 'onClick' ).match( /PublishedFileAward\(\s*'(?[0-9]+)',\s*(?[0-9]+)\s*\)/ ); 27 | 28 | if( !data ) 29 | { 30 | console.log( '[SteamDB] Failed to extract data from PublishedFileAward button' ); 31 | return; 32 | } 33 | 34 | window.PublishedFileAward( 35 | data.groups.id, 36 | Number.parseInt( data.groups.fileType, 10 ), 37 | Number.parseInt( awardId, 10 ), 38 | ); 39 | } )() ); 40 | -------------------------------------------------------------------------------- /scripts/community/filedetails_guide.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if( document.querySelector( '.guideTopContent' ) ) 4 | { 5 | const guide = document.querySelector( '.guide' ); 6 | 7 | if( guide.querySelector( '.bb_spoiler' ) ) 8 | { 9 | /** 10 | * @param {ViewTransitionUpdateCallback} callback 11 | */ 12 | const StartViewTransition = ( callback ) => 13 | { 14 | if( document.startViewTransition ) 15 | { 16 | document.startViewTransition( () => 17 | { 18 | try 19 | { 20 | callback(); 21 | } 22 | catch( e ) 23 | { 24 | console.error( e ); 25 | } 26 | } ); 27 | } 28 | else 29 | { 30 | callback(); 31 | } 32 | }; 33 | 34 | const controls = document.querySelector( '#ItemControls' ); 35 | 36 | const divider = document.createElement( 'div' ); 37 | divider.className = 'vertical_divider'; 38 | controls.append( divider ); 39 | 40 | const checkboxWrapper = document.createElement( 'label' ); 41 | checkboxWrapper.textContent = _t( 'spoilers_reveal' ); 42 | checkboxWrapper.className = 'workshopItemControlCtn general_btn steamdb_reveal_spoilers_button'; 43 | 44 | const checkbox = document.createElement( 'input' ); 45 | checkbox.type = 'checkbox'; 46 | checkboxWrapper.prepend( checkbox ); 47 | controls.append( checkboxWrapper ); 48 | 49 | checkbox.addEventListener( 'change', () => 50 | { 51 | const spoilers = guide.querySelectorAll( '.bb_spoiler' ); 52 | const reveal = checkbox.checked; 53 | 54 | StartViewTransition( () => 55 | { 56 | for( const spoiler of spoilers ) 57 | { 58 | spoiler.classList.toggle( 'steamdb_spoiler_revealed', reveal ); 59 | } 60 | } ); 61 | } ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scripts/community/gamehub.js: -------------------------------------------------------------------------------- 1 | /* global CurrentAppID: true */ 2 | 'use strict'; 3 | 4 | GetOption( { 5 | 'button-gamehub': true, 6 | 'button-pcgw': true, 7 | }, ( items ) => 8 | { 9 | const container = document.querySelector( '.apphub_OtherSiteInfo' ); 10 | 11 | if( container ) 12 | { 13 | // Are we in a hacky game group with a custom url? 14 | if( GetCurrentAppID() === -1 ) 15 | { 16 | /** @type {HTMLAnchorElement} */ 17 | const sectionTab = document.querySelector( '.apphub_sectionTab' ); 18 | 19 | const match = sectionTab.href.match( /\/([0-9]+)\/?/ ); 20 | CurrentAppID = CurrentAppID ? Number.parseInt( match[ 1 ], 10 ) : -1; 21 | } 22 | 23 | if( GetCurrentAppID() < 1 ) 24 | { 25 | return; 26 | } 27 | 28 | // Make in-game number clickable 29 | const numInApp = document.querySelector( '.apphub_NumInApp' ); 30 | 31 | if( numInApp ) 32 | { 33 | const link = document.createElement( 'a' ); 34 | link.className = 'apphub_NumInApp'; 35 | link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/charts/'; 36 | link.title = _t( 'view_on_steamdb' ); 37 | link.textContent = numInApp.textContent; 38 | 39 | numInApp.parentNode.replaceChild( link, numInApp ); 40 | } 41 | 42 | if( items[ 'button-gamehub' ] ) 43 | { 44 | const link = document.createElement( 'a' ); 45 | link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb'; 46 | link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/'; 47 | 48 | const element = document.createElement( 'span' ); 49 | element.dataset.tooltipText = _t( 'view_on_steamdb' ); 50 | link.appendChild( element ); 51 | 52 | const image = document.createElement( 'img' ); 53 | image.className = 'ico16'; 54 | image.src = GetLocalResource( 'icons/white.svg' ); 55 | 56 | element.appendChild( image ); 57 | 58 | container.insertBefore( link, container.firstChild ); 59 | 60 | const responsiveMenu = document.querySelector( '.apphub_ResponsiveMenuCtn' ); 61 | 62 | if( responsiveMenu ) 63 | { 64 | responsiveMenu.append( link.cloneNode( true ) ); 65 | } 66 | } 67 | 68 | if( items[ 'button-pcgw' ] ) 69 | { 70 | const link = document.createElement( 'a' ); 71 | link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb'; 72 | link.href = 'https://pcgamingwiki.com/api/appid.php?appid=' + GetCurrentAppID() + '&utm_source=SteamDB'; 73 | 74 | const element = document.createElement( 'span' ); 75 | element.dataset.tooltipText = _t( 'view_on_pcgamingwiki' ); 76 | link.appendChild( element ); 77 | 78 | const image = document.createElement( 'img' ); 79 | image.className = 'ico16'; 80 | image.src = GetLocalResource( 'icons/pcgamingwiki.svg' ); 81 | 82 | element.appendChild( image ); 83 | 84 | container.insertBefore( link, container.firstChild ); 85 | 86 | container.insertBefore( document.createTextNode( ' ' ), link.nextSibling ); 87 | 88 | const responsiveMenu = document.querySelector( '.apphub_ResponsiveMenuCtn' ); 89 | 90 | if( responsiveMenu ) 91 | { 92 | responsiveMenu.append( document.createTextNode( ' ' ) ); 93 | responsiveMenu.append( link.cloneNode( true ) ); 94 | } 95 | } 96 | } 97 | } ); 98 | -------------------------------------------------------------------------------- /scripts/community/market.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const marketScript = document.createElement( 'script' ); 4 | marketScript.id = 'steamdb_market_script'; 5 | marketScript.type = 'text/javascript'; 6 | marketScript.src = GetLocalResource( 'scripts/community/market_injected.js' ); 7 | document.documentElement.append( marketScript ); 8 | -------------------------------------------------------------------------------- /scripts/community/market_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // If prototype.js already loaded, use its event 4 | if( 'observe' in Event ) 5 | { 6 | // @ts-ignore 7 | Event.observe( document, 'dom:loaded', OnLoaded ); 8 | } 9 | else 10 | { 11 | document.addEventListener( 'DOMContentLoaded', OnLoaded ); 12 | } 13 | 14 | function OnLoaded() 15 | { 16 | if( window.CAjaxPagingControls ) 17 | { 18 | const originalGoToPage = window.CAjaxPagingControls.prototype.GoToPage; 19 | const originalOnAJAXComplete = window.CAjaxPagingControls.prototype.OnAJAXComplete; 20 | const originalOnResponseRenderResults = window.CAjaxPagingControls.prototype.OnResponseRenderResults; 21 | 22 | const loader = document.createElement( 'div' ); 23 | loader.className = 'steamdb_market_loader'; 24 | loader.hidden = true; 25 | 26 | const summary = document.getElementById( 'searchResultsTable' ); 27 | 28 | if( summary ) 29 | { 30 | summary.append( loader ); 31 | } 32 | 33 | /** 34 | * @param {any} transport 35 | */ 36 | window.CAjaxPagingControls.prototype.OnResponseRenderResults = function SteamDB_OnResponseRenderResults( transport ) 37 | { 38 | const response = transport.responseJSON; 39 | 40 | if( !response ) 41 | { 42 | // Call original but it does nothing for no success 43 | originalOnResponseRenderResults.apply( this, arguments ); 44 | return; 45 | } 46 | 47 | const responseStart = response.start; 48 | let fixedBug = false; 49 | 50 | if( response.success && responseStart > 0 && response.total_count < 1 ) 51 | { 52 | fixedBug = true; 53 | response.start = 0; 54 | 55 | console.log( '[SteamDB] Steam returned 0 results, but user was trying to load a page, fixing this' ); 56 | } 57 | 58 | originalOnResponseRenderResults.apply( this, arguments ); 59 | 60 | if( fixedBug ) 61 | { 62 | // If user tries to fetch some page, but Steam says there are no results and returns an error html, 63 | // it normally screws the state of the pagination 64 | this.m_iCurrentPage = Math.floor( responseStart / this.m_cPageSize ); 65 | this.m_cMaxPages = this.m_iCurrentPage + 1; 66 | } 67 | }; 68 | 69 | /** 70 | * @param {number} iPage 71 | */ 72 | window.CAjaxPagingControls.prototype.GoToPage = function SteamDB_GoToPage( iPage ) 73 | { 74 | if( this.m_strElementPrefix !== 'searchResults' ) 75 | { 76 | originalGoToPage.apply( this, arguments ); 77 | return; 78 | } 79 | 80 | // If initial page load has no count, but somehow is trying to go to a page, 81 | // force the page check to pass otherwise it will not try to load anything 82 | if( window.g_oSearchData && window.g_oSearchData.total_count < 1 && this.m_cMaxPages < 1 ) 83 | { 84 | this.m_cMaxPages = iPage + 1; 85 | 86 | console.log( '[SteamDB] Page loaded with 0 results, fixing this' ); 87 | } 88 | 89 | originalGoToPage.apply( this, arguments ); 90 | 91 | if( this.m_bLoading ) 92 | { 93 | loader.hidden = false; 94 | } 95 | }; 96 | 97 | /** 98 | * @param {any} transport 99 | */ 100 | window.CAjaxPagingControls.prototype.OnAJAXComplete = function( transport ) 101 | { 102 | originalOnAJAXComplete.apply( this, arguments ); 103 | 104 | if( this.m_strElementPrefix !== 'searchResults' ) 105 | { 106 | return; 107 | } 108 | 109 | loader.hidden = true; 110 | 111 | AddRetryMarketButton( this ); 112 | 113 | // If the request fail, cache bust future requests, otherwise retrying will just hit browser cache 114 | if( !transport.responseJSON || !transport.responseJSON.success || transport.responseJSON.total_count < 1 ) 115 | { 116 | if( this.m_rgStaticParams === null ) 117 | { 118 | this.m_rgStaticParams = {}; 119 | } 120 | 121 | this.m_rgStaticParams.steamdb_cache = Date.now().toString(); 122 | } 123 | }; 124 | } 125 | 126 | /** 127 | * @param {any} context 128 | */ 129 | function AddRetryMarketButton( context ) 130 | { 131 | const message = document.querySelector( '#searchResultsTable .market_listing_table_message' ); 132 | 133 | if( !message ) 134 | { 135 | return; 136 | } 137 | 138 | const div = document.createElement( 'div' ); 139 | div.className = 'steamdb_market_retry_button'; 140 | 141 | const btn = document.createElement( 'button' ); 142 | btn.className = 'btnv6_green_white_innerfade btn_medium'; 143 | 144 | const span = document.createElement( 'span' ); 145 | span.textContent = 'Try again'; 146 | btn.append( span ); 147 | 148 | btn.addEventListener( 'click', () => 149 | { 150 | btn.remove(); 151 | context.GoToPage( context.m_iCurrentPage, true ); 152 | } ); 153 | 154 | div.append( btn ); 155 | message.append( div ); 156 | } 157 | 158 | setTimeout( () => 159 | { 160 | if( window.g_oSearchResults ) 161 | { 162 | AddRetryMarketButton( window.g_oSearchResults ); 163 | } 164 | }, 100 ); 165 | } 166 | -------------------------------------------------------------------------------- /scripts/community/market_ssa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'enhancement-market-ssa': false }, ( items ) => 4 | { 5 | if( items[ 'enhancement-market-ssa' ] ) 6 | { 7 | /** @type {HTMLInputElement} */ 8 | let element = document.querySelector( '#market_buynow_dialog_accept_ssa' ); 9 | 10 | if( element ) 11 | { 12 | element.checked = true; 13 | } 14 | 15 | element = document.querySelector( '#market_buyorder_dialog_accept_ssa' ); 16 | 17 | if( element ) 18 | { 19 | element.checked = true; 20 | } 21 | 22 | element = document.querySelector( '#market_sell_dialog_accept_ssa' ); 23 | 24 | if( element ) 25 | { 26 | element.checked = true; 27 | } 28 | } 29 | } ); 30 | -------------------------------------------------------------------------------- /scripts/community/multibuy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const multibuyScript = document.createElement( 'script' ); 4 | multibuyScript.id = 'steamdb_multibuy'; 5 | multibuyScript.type = 'text/javascript'; 6 | multibuyScript.src = GetLocalResource( 'scripts/community/multibuy_injected.js' ); 7 | document.head.append( multibuyScript ); 8 | -------------------------------------------------------------------------------- /scripts/community/multibuy_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | const originalOrderPollingComplete = window.OrderPollingComplete; 6 | 7 | window.OrderPollingComplete = function SteamDB_OrderPollingComplete() 8 | { 9 | originalOrderPollingComplete.apply( this, arguments ); 10 | 11 | // Verify that all purchases succeeded 12 | for( let iOrder = 0; iOrder < window.g_rgItemNameIds.length; iOrder++ ) 13 | { 14 | const order = window.g_rgOrders[ iOrder ]; 15 | 16 | if( order.m_nQuantity < 1 ) 17 | { 18 | continue; 19 | } 20 | 21 | if( !order.m_bOrderSuccess ) 22 | { 23 | return; 24 | } 25 | 26 | const success = document.getElementById( `buy_${order.m_llNameId}_success` ); 27 | 28 | // If the success checkmark is not visible, something went wrong 29 | if( !success || !success.checkVisibility() ) 30 | { 31 | return; 32 | } 33 | } 34 | 35 | const params = new URLSearchParams( window.location.search ); 36 | const returnTo = params.get( 'steamdb_return_to' ); 37 | 38 | if( returnTo === null ) 39 | { 40 | return; 41 | } 42 | 43 | const returnToUrl = new URL( returnTo ); 44 | 45 | // Verify that we're returning to the same origin 46 | if( returnToUrl.origin !== window.location.origin ) 47 | { 48 | return; 49 | } 50 | 51 | window.location.href = returnToUrl.toString(); 52 | }; 53 | } )() ); 54 | -------------------------------------------------------------------------------- /scripts/community/profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 4 | 'profile-calculator': true, 5 | 'enhancement-award-popup-url': true, 6 | }, ( items ) => 7 | { 8 | if( items[ 'enhancement-award-popup-url' ] && window.location.search.includes( 'award' ) ) 9 | { 10 | const script = document.createElement( 'script' ); 11 | script.id = 'steamdb_profile_award'; 12 | script.type = 'text/javascript'; 13 | script.src = GetLocalResource( 'scripts/community/profile_award_injected.js' ); 14 | document.head.appendChild( script ); 15 | } 16 | 17 | if( !items[ 'profile-calculator' ] ) 18 | { 19 | return; 20 | } 21 | 22 | // Can't access g_rgProfileData inside sandbox :( 23 | 24 | let steamID = ''; 25 | let isCommunityID = false; 26 | 27 | // If we can, use abuseID 28 | /** @type {HTMLInputElement} */ 29 | const abuseIDInput = document.querySelector( '#abuseForm > input[name=abuseID]' ); 30 | 31 | if( abuseIDInput ) 32 | { 33 | steamID = abuseIDInput.value; 34 | 35 | isCommunityID = true; 36 | } 37 | else 38 | { 39 | // Fallback to url if we can't 40 | steamID = location.pathname.match( /^\/(?:id|profiles)\/([^\s/]+)\/?/ )[ 1 ]; 41 | 42 | isCommunityID = /^\/profiles/.test( location.pathname ); 43 | } 44 | 45 | let container = document.querySelector( '#profile_action_dropdown .popup_body' ); 46 | let url = GetHomepage() + 'calculator/'; 47 | 48 | if( isCommunityID ) 49 | { 50 | url += `${steamID}/`; 51 | } 52 | else 53 | { 54 | url += `?player=${steamID}`; 55 | } 56 | 57 | if( container ) 58 | { 59 | const image = document.createElement( 'img' ); 60 | image.className = 'steamdb_popup_icon'; 61 | image.src = GetLocalResource( 'icons/white.svg' ); 62 | 63 | const element = document.createElement( 'a' ); 64 | element.href = url; 65 | element.className = 'popup_menu_item'; 66 | element.appendChild( image ); 67 | element.appendChild( document.createTextNode( '\u00a0 ' + _t( 'steamdb_calculator' ) ) ); 68 | 69 | container.insertBefore( element, null ); 70 | } 71 | else 72 | { 73 | container = document.querySelector( '.profile_header_actions' ); 74 | 75 | if( container ) 76 | { 77 | const image = document.createElement( 'img' ); 78 | image.src = GetLocalResource( 'icons/white.svg' ); 79 | image.className = 'steamdb_self_profile'; 80 | 81 | const text = document.createElement( 'span' ); 82 | text.dataset.tooltipText = _t( 'steamdb_calculator' ); 83 | text.appendChild( image ); 84 | 85 | const element = document.createElement( 'a' ); 86 | element.className = 'btn_profile_action btn_medium'; 87 | element.href = url; 88 | element.appendChild( text ); 89 | 90 | container.appendChild( element ); 91 | } 92 | } 93 | } ); 94 | -------------------------------------------------------------------------------- /scripts/community/profile_award_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( !( 'g_rgProfileData' in window ) || !( 'fnLoyalty_ShowAwardModal' in window ) ) 6 | { 7 | return; 8 | } 9 | 10 | const params = new URLSearchParams( window.location.search ); 11 | const awardId = params.get( 'award' ); 12 | 13 | if( awardId === null ) 14 | { 15 | return; 16 | } 17 | 18 | window.fnLoyalty_ShowAwardModal( 19 | window.g_rgProfileData.steamid, 20 | 3, // profile 21 | () => 22 | { 23 | // do nothing 24 | }, 25 | undefined, // ugcType 26 | Number.parseInt( awardId, 10 ), 27 | ); 28 | } )() ); 29 | -------------------------------------------------------------------------------- /scripts/community/profile_badges.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const progressInfo = document.querySelectorAll( '.badge_title_stats_drops .progress_info_bold' ); 4 | 5 | if( progressInfo.length > 0 ) 6 | { 7 | let apps = 0; 8 | let drops = 0; 9 | let match; 10 | 11 | for( let i = 0; i < progressInfo.length; i++ ) 12 | { 13 | match = progressInfo[ i ].textContent.match( /(?[0-9]+)/ ); 14 | 15 | if( match ) 16 | { 17 | match = Number.parseInt( match.groups.number, 10 ) || 0; 18 | 19 | if( match > 0 ) 20 | { 21 | apps++; 22 | drops += match; 23 | } 24 | } 25 | } 26 | 27 | if( apps > 0 ) 28 | { 29 | const container = document.querySelector( '.badge_details_set_favorite' ); 30 | 31 | if( container ) 32 | { 33 | const hasPages = document.querySelector( '.pageLinks' ); 34 | 35 | let text = document.createElement( 'span' ); 36 | text.className = 'steamdb_drops_remaining'; 37 | text.appendChild( document.createTextNode( _t( hasPages ? 'badges_idle_apps_on_this_page' : 'badges_idle_apps', [ apps.toString() ] ) ) ); 38 | container.prepend( text ); 39 | container.prepend( document.createTextNode( ' ' ) ); 40 | 41 | text = document.createElement( 'span' ); 42 | text.className = 'steamdb_drops_remaining'; 43 | text.appendChild( document.createTextNode( _t( hasPages ? 'badges_idle_drops_on_this_page' : 'badges_idle_drops', [ drops.toString() ] ) ) ); 44 | container.prepend( text ); 45 | } 46 | } 47 | } 48 | else 49 | { 50 | GetOption( { 'button-gamecards': true }, ( items ) => 51 | { 52 | if( !items[ 'button-gamecards' ] ) 53 | { 54 | return; 55 | } 56 | 57 | const profileTexture = document.querySelector( '.profile_small_header_texture' ); 58 | 59 | if( !profileTexture ) 60 | { 61 | return; 62 | } 63 | 64 | const badgeUrl = location.pathname.match( /\/badges\/([0-9]+)/ ); 65 | 66 | if( !badgeUrl ) 67 | { 68 | return; 69 | } 70 | 71 | const badgeid = Number.parseInt( badgeUrl[ 1 ], 10 ); 72 | 73 | const container = document.createElement( 'div' ); 74 | container.className = 'profile_small_header_additional steamdb'; 75 | 76 | const image = document.createElement( 'img' ); 77 | image.className = 'ico16'; 78 | image.src = GetLocalResource( 'icons/white.svg' ); 79 | 80 | const span = document.createElement( 'span' ); 81 | span.dataset.tooltipText = _t( 'view_on_steamdb' ); 82 | span.appendChild( image ); 83 | 84 | const link = document.createElement( 'a' ); 85 | link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb'; 86 | link.href = GetHomepage() + 'badge/' + badgeid + '/'; 87 | link.appendChild( span ); 88 | 89 | container.insertBefore( link, container.firstChild ); 90 | 91 | profileTexture.appendChild( container ); 92 | } ); 93 | } 94 | -------------------------------------------------------------------------------- /scripts/community/profile_gamecards.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | MoveMultiBuyButton(); 4 | 5 | GetOption( { 'button-gamecards': true }, ( items ) => 6 | { 7 | if( !items[ 'button-gamecards' ] ) 8 | { 9 | return; 10 | } 11 | 12 | const profileTexture = document.querySelector( '.profile_small_header_texture' ); 13 | 14 | if( !profileTexture ) 15 | { 16 | return; 17 | } 18 | 19 | // Container 20 | const container = document.createElement( 'div' ); 21 | container.className = 'profile_small_header_additional steamdb'; 22 | 23 | // Store button 24 | let span = document.createElement( 'span' ); 25 | span.appendChild( document.createTextNode( _t( 'store_page' ) ) ); 26 | 27 | let link = document.createElement( 'a' ); 28 | link.className = 'btnv6_blue_hoverfade btn_medium'; 29 | link.href = 'https://store.steampowered.com/app/' + GetCurrentAppID() + '/'; 30 | link.appendChild( span ); 31 | 32 | container.insertBefore( link, container.firstChild ); 33 | 34 | // SteamDB button 35 | const image = document.createElement( 'img' ); 36 | image.className = 'ico16'; 37 | image.src = GetLocalResource( 'icons/white.svg' ); 38 | 39 | span = document.createElement( 'span' ); 40 | span.dataset.tooltipText = _t( 'view_on_steamdb' ); 41 | span.appendChild( image ); 42 | 43 | link = document.createElement( 'a' ); 44 | link.className = 'btnv6_blue_hoverfade btn_medium btn_steamdb'; 45 | link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/communityitems/'; 46 | link.appendChild( span ); 47 | 48 | container.insertBefore( link, container.firstChild ); 49 | container.insertBefore( document.createTextNode( ' ' ), link.nextSibling ); 50 | 51 | // Add to the page 52 | profileTexture.appendChild( container ); 53 | } ); 54 | 55 | function MoveMultiBuyButton() 56 | { 57 | /** @type {NodeListOf} */ 58 | const links = document.querySelectorAll( '.gamecards_inventorylink a' ); 59 | 60 | for( const element of links ) 61 | { 62 | const link = new URL( element.href ); 63 | 64 | // Fix Valve incorrectly using CDN in the link 65 | if( link.host.endsWith( '.steamstatic.com' ) ) 66 | { 67 | link.host = window.location.host; 68 | element.href = link.toString(); 69 | } 70 | 71 | if( link.pathname === '/market/multibuy' ) 72 | { 73 | // Add return to link to automatically return to the badge page after multi buying the cards 74 | const params = new URLSearchParams( link.search ); 75 | params.set( 'steamdb_return_to', window.location.href ); 76 | 77 | link.search = params.toString(); 78 | element.href = link.toString(); 79 | 80 | // Move the buy button up top 81 | const topLinks = document.querySelector( '.badge_detail_tasks .gamecards_inventorylink' ); 82 | 83 | if( topLinks ) 84 | { 85 | topLinks.append( element ); 86 | topLinks.append( document.createTextNode( ' ' ) ); 87 | 88 | // Some languages will overflow the buttons so we have to correct the spacing 89 | topLinks.classList.add( 'steamdb_gamecards_inventorylink' ); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/community/profile_inventory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if( document.getElementById( 'inventory_link_753' ) ) 4 | { 5 | GetOption( { 6 | 'link-inventory': true, 7 | 'link-inventory-gift-subid': true, 8 | 'enhancement-inventory-sidebar': true, 9 | 'enhancement-inventory-quick-sell': true, 10 | 'enhancement-inventory-quick-sell-auto': false, 11 | 'enhancement-inventory-no-sell-reload': true, 12 | 'enhancement-inventory-badge-info': true, 13 | }, ( items ) => 14 | { 15 | if( items[ 'enhancement-inventory-sidebar' ] ) 16 | { 17 | const style = document.createElement( 'link' ); 18 | style.id = 'steamdb_inventory_sidebar'; 19 | style.type = 'text/css'; 20 | style.rel = 'stylesheet'; 21 | style.href = GetLocalResource( 'styles/inventory-sidebar.css' ); 22 | 23 | document.head.appendChild( style ); 24 | } 25 | 26 | const element = document.createElement( 'script' ); 27 | element.id = 'steamdb_inventory_hook'; 28 | element.type = 'text/javascript'; 29 | element.src = GetLocalResource( 'scripts/community/inventory.js' ); 30 | element.dataset.homepage = GetHomepage(); 31 | element.dataset.options = JSON.stringify( items ); 32 | element.dataset.i18n = JSON.stringify( { 33 | view_on_steamdb: _t( 'view_on_steamdb' ), 34 | inventory_list_at: _t( 'inventory_list_at' ), 35 | inventory_sell_at: _t( 'inventory_sell_at' ), 36 | inventory_list_at_title: _t( 'inventory_list_at_title' ), 37 | inventory_sell_at_title: _t( 'inventory_sell_at_title' ), 38 | inventory_badge_level: _t( 'inventory_badge_level' ), 39 | inventory_badge_foil_level: _t( 'inventory_badge_foil_level' ), 40 | inventory_badge_none: _t( 'inventory_badge_none' ), 41 | } ); 42 | 43 | document.head.appendChild( element ); 44 | } ); 45 | } 46 | -------------------------------------------------------------------------------- /scripts/community/profile_recommended.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'button-gamehub': true }, ( items ) => 4 | { 5 | if( !items[ 'button-gamehub' ] ) 6 | { 7 | return; 8 | } 9 | 10 | const container = document.querySelector( '.review_app_actions' ); 11 | 12 | if( !container ) 13 | { 14 | return; 15 | } 16 | 17 | // image 18 | const image = document.createElement( 'img' ); 19 | image.className = 'toolsIcon steamdb_ogg_icon'; 20 | image.src = GetLocalResource( 'icons/white.svg' ); 21 | 22 | // link 23 | const link = document.createElement( 'a' ); 24 | link.className = 'general_btn panel_btn'; 25 | link.href = GetHomepage() + 'app/' + GetCurrentAppID() + '/'; 26 | link.appendChild( image ); 27 | link.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) ); 28 | 29 | container.insertBefore( link, null ); 30 | } ); 31 | -------------------------------------------------------------------------------- /scripts/community/tradeoffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 4 | 'enhancement-tradeoffer-url-items': true, 5 | 'enhancement-tradeoffer-no-gift-confirm': null, 6 | }, ( items ) => 7 | { 8 | const element = document.createElement( 'script' ); 9 | 10 | if( items[ 'enhancement-tradeoffer-no-gift-confirm' ] ) 11 | { 12 | element.dataset.noGiftConfirm = 'true'; 13 | } 14 | 15 | if( items[ 'enhancement-tradeoffer-url-items' ] ) 16 | { 17 | element.dataset.urlItemSupport = 'true'; 18 | } 19 | 20 | element.id = 'steamdb_tradeoffer'; 21 | element.type = 'text/javascript'; 22 | element.src = GetLocalResource( 'scripts/community/tradeoffer_injected.js' ); 23 | 24 | document.head.appendChild( element ); 25 | } ); 26 | -------------------------------------------------------------------------------- /scripts/community/tradeoffer_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( !window.CTradeOfferStateManager ) 6 | { 7 | return; 8 | } 9 | 10 | const script = document.getElementById( 'steamdb_tradeoffer' ); 11 | 12 | // for_item support 13 | if( script.dataset.urlItemSupport === 'true' && window.g_rgCurrentTradeStatus && window.location.pathname.startsWith( '/tradeoffer/new' ) ) 14 | { 15 | const params = new URLSearchParams( window.location.search ); 16 | const theirItems = params.getAll( 'for_item' ); 17 | const myItems = params.getAll( 'my_item' ); 18 | let redrawTrade = false; 19 | 20 | if( theirItems.length > 0 ) 21 | { 22 | window.g_rgCurrentTradeStatus.them.ready = false; 23 | window.g_rgCurrentTradeStatus.them.assets = []; 24 | 25 | for( const item of theirItems ) 26 | { 27 | const parsed = item.match( /(?[0-9]+)_(?[0-9]+)_(?[0-9]+)/ ); 28 | 29 | if( parsed === null ) 30 | { 31 | continue; 32 | } 33 | 34 | window.g_rgCurrentTradeStatus.them.assets.push( { 35 | appid: parsed.groups.appid, 36 | contextid: parsed.groups.contextid, 37 | assetid: parsed.groups.assetid, 38 | amount: 1, 39 | } ); 40 | 41 | redrawTrade = true; 42 | } 43 | } 44 | 45 | if( myItems.length > 0 ) 46 | { 47 | window.g_rgCurrentTradeStatus.me.ready = false; 48 | window.g_rgCurrentTradeStatus.me.assets = []; 49 | 50 | for( const item of myItems ) 51 | { 52 | const parsed = item.match( /(?[0-9]+)_(?[0-9]+)_(?[0-9]+)/ ); 53 | 54 | if( parsed === null ) 55 | { 56 | continue; 57 | } 58 | 59 | window.g_rgCurrentTradeStatus.me.assets.push( { 60 | appid: parsed.groups.appid, 61 | contextid: parsed.groups.contextid, 62 | assetid: parsed.groups.assetid, 63 | amount: 1, 64 | } ); 65 | 66 | redrawTrade = true; 67 | } 68 | } 69 | 70 | if( redrawTrade ) 71 | { 72 | window.RedrawCurrentTradeStatus(); 73 | } 74 | } 75 | 76 | // no gift confirmation 77 | if( script.dataset.noGiftConfirm === 'true' ) 78 | { 79 | const originalToggleReady = window.ToggleReady; 80 | 81 | /** 82 | * @param {any} ready 83 | */ 84 | window.ToggleReady = function( ready ) 85 | { 86 | window.g_rgCurrentTradeStatus.me.ready = ready; 87 | window.g_cTheirItemsInTrade = 1; 88 | window.g_bWarnOnReady = false; 89 | 90 | originalToggleReady.apply( this, arguments ); 91 | }; 92 | } 93 | 94 | // better error messages 95 | const originalShowAlertDialog = window.ShowAlertDialog; 96 | const originalSetAssetOrCurrencyInTrade = window.CTradeOfferStateManager.SetAssetOrCurrencyInTrade; 97 | 98 | /** 99 | * @param {Record} item 100 | */ 101 | window.CTradeOfferStateManager.SetAssetOrCurrencyInTrade = function SteamDB_SetAssetOrCurrencyInTrade( item ) 102 | { 103 | try 104 | { 105 | // Make sure this item can actually be traded 106 | const appName = window.g_rgPartnerAppContextData[ item.appid ].name; 107 | const errorTitle = 'Cannot Add "' + item.name + '" to Trade'; 108 | 109 | switch( window.g_rgPartnerAppContextData[ item.appid ].trade_permissions ) 110 | { 111 | case 'NONE': 112 | originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot trade items in ' + appName + '.' ); 113 | return; 114 | 115 | case 'SENDONLY': 116 | case 'SENDONLY_FULLINVENTORY': 117 | if( !item.is_their_item ) 118 | { 119 | originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot receive items in ' + appName + ( window.g_rgPartnerAppContextData[ item.appid ].trade_permissions === 'SENDONLY_FULLINVENTORY' ? ' because their inventory is full' : '' ) + '.' ); 120 | return; 121 | } 122 | 123 | break; 124 | 125 | case 'RECEIVEONLY': 126 | if( item.is_their_item ) 127 | { 128 | originalShowAlertDialog( errorTitle, window.g_strTradePartnerPersonaName + ' cannot send items in ' + appName + '.' ); 129 | return; 130 | } 131 | 132 | break; 133 | } 134 | } 135 | catch( ex ) 136 | { 137 | // don't care! 138 | } 139 | 140 | originalSetAssetOrCurrencyInTrade.apply( this, arguments ); 141 | }; 142 | 143 | /** 144 | * @param {string} strTitle 145 | * @param {string} strDescription 146 | */ 147 | window.ShowAlertDialog = function SteamDB_ShowAlertDialog( strTitle, strDescription ) 148 | { 149 | const eresult = strDescription.match( /\(([0-9]+)\)$/ ); 150 | if( eresult !== null ) 151 | { 152 | let explanation; 153 | 154 | switch( +eresult[ 1 ] ) 155 | { 156 | case 2: explanation = 'There was an internal error when sending your trade offer.'; break; 157 | case 11: explanation = 'This trade offer is not currently active. It may have been previously accepted or canceled.'; break; 158 | case 16: explanation = 'The Steam Community servers did not get a timely reply from the economy server. Your offer may or may not have been sent.

Please check your sent trade offers.'; break; 159 | case 20: explanation = 'The trade offer server is temporarily unavailable.'; break; 160 | case 25: explanation = 'You cannot send this trade offer because you have exceeded your active offer limit.

You are limited to 5 outstanding trade offers to a single user, and 30 outstanding trade offers in total.'; break; 161 | case 26: explanation = 'One or more of the items in this trade offer is no longer present in the inventory from which it is being requested.

Please check all items to ensure that they still exist and are tradable.'; break; 162 | } 163 | 164 | if( explanation ) 165 | { 166 | arguments[ 0 ] += ' Failed'; 167 | arguments[ 1 ] += '

' + explanation + '

'; 168 | arguments[ 1 ] += '(explained by SteamDB)'; 169 | } 170 | } 171 | 172 | return originalShowAlertDialog.apply( this, arguments ); 173 | }; 174 | } )() ); 175 | -------------------------------------------------------------------------------- /scripts/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // There's no easier way to check if we're on error page :( 4 | if( document.title === 'Sorry!' || 5 | document.title === 'Error' || 6 | document.title === '502 Bad Gateway' || 7 | document.title === 'We Broke It' ) 8 | { 9 | const link = document.createElement( 'a' ); 10 | link.href = 'https://steamstat.us'; 11 | link.appendChild( document.createTextNode( _t( 'steamstatus' ) ) ); 12 | 13 | const container = document.createElement( 'div' ); 14 | container.className = 'steamdb_downtime'; 15 | container.appendChild( document.createTextNode( _t( 'steamstatus_downtime' ) ) ); 16 | container.appendChild( link ); 17 | 18 | document.body.insertBefore( container, document.body.firstChild ); 19 | 20 | document.body.style.margin = '0'; 21 | } 22 | else 23 | { 24 | GetOption( { 'enhancement-hide-install-button': true, 'enhancement-no-linkfilter': false }, ( items ) => 25 | { 26 | if( items[ 'enhancement-hide-install-button' ] ) 27 | { 28 | /** @type {HTMLElement} */ 29 | const button = document.querySelector( '.header_installsteam_btn' ); 30 | 31 | if( button ) 32 | { 33 | button.setAttribute( 'hidden', 'true' ); 34 | button.style.display = 'none'; 35 | } 36 | } 37 | 38 | if( items[ 'enhancement-no-linkfilter' ] ) 39 | { 40 | /** @type {NodeListOf} */ 41 | const links = document.querySelectorAll( 'a[href^="https://steamcommunity.com/linkfilter/"]' ); 42 | 43 | for( const link of links ) 44 | { 45 | if( !link.search ) 46 | { 47 | continue; 48 | } 49 | 50 | const params = new URLSearchParams( link.search ); 51 | 52 | if( params.has( 'u' ) ) 53 | { 54 | link.href = params.get( 'u' ); 55 | } 56 | else if( params.has( 'url' ) ) 57 | { 58 | link.href = params.get( 'url' ); 59 | } 60 | } 61 | } 62 | } ); 63 | 64 | const popup = document.querySelector( '#account_dropdown .popup_body' ); 65 | 66 | if( popup ) 67 | { 68 | const optionsLink = document.createElement( 'a' ); 69 | optionsLink.target = '_blank'; 70 | optionsLink.className = 'popup_menu_item steamdb_options_link'; 71 | optionsLink.textContent = ' ' + _t( 'steamdb_options' ); 72 | optionsLink.href = GetLocalResource( 'options/options.html' ); 73 | 74 | const image = document.createElement( 'img' ); 75 | image.className = 'ico16'; 76 | image.src = GetLocalResource( 'icons/white.svg' ); 77 | optionsLink.prepend( image ); 78 | 79 | popup.appendChild( optionsLink ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/steamdb/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EXTENSION_INTEROP_VERSION = 2; 4 | 5 | const OnPageLoadedInit = () => 6 | { 7 | window.postMessage( { 8 | version: EXTENSION_INTEROP_VERSION, 9 | type: 'steamdb:extension-init', 10 | data: { 11 | options_url: GetLocalResource( 'options/options.html' ), 12 | }, 13 | }, GetHomepage() ); 14 | }; 15 | 16 | if( document.readyState === 'loading' ) 17 | { 18 | document.addEventListener( 'readystatechange', OnPageLoadedInit, { once: true } ); 19 | } 20 | else 21 | { 22 | OnPageLoadedInit(); 23 | } 24 | 25 | window.addEventListener( 'message', ( request ) => 26 | { 27 | if( !request || !request.data || request.origin !== window.location.origin ) 28 | { 29 | return; 30 | } 31 | 32 | switch( request.data.type ) 33 | { 34 | case 'steamdb:extension-query': 35 | { 36 | if( request.data.contentScriptQuery ) 37 | { 38 | SendMessageToBackgroundScript( request.data, ( response ) => 39 | { 40 | window.postMessage( { 41 | version: EXTENSION_INTEROP_VERSION, 42 | type: 'steamdb:extension-response', 43 | request: request.data, 44 | response, 45 | }, GetHomepage() ); 46 | } ); 47 | } 48 | break; 49 | } 50 | case 'steamdb:extension-invalidate-cache': 51 | { 52 | WriteLog( 'Invalidating userdata cache' ); 53 | SendMessageToBackgroundScript( { 54 | contentScriptQuery: 'InvalidateCache', 55 | }, () => 56 | { 57 | // noop 58 | } ); 59 | break; 60 | } 61 | } 62 | } ); 63 | 64 | GetOption( { 'steamdb-highlight': true, 'steamdb-highlight-family': true }, async( items ) => 65 | { 66 | if( !items[ 'steamdb-highlight' ] ) 67 | { 68 | return; 69 | } 70 | 71 | /** @type {Promise<{data?: Record, error?: string}>} */ 72 | const userDataPromise = new Promise( ( resolve ) => 73 | { 74 | SendMessageToBackgroundScript( { 75 | contentScriptQuery: 'FetchSteamUserData', 76 | }, resolve ); 77 | } ); 78 | 79 | /** @type {Promise<{data?: Record, error?: string}>} */ 80 | const familyDataPromise = new Promise( ( resolve ) => 81 | { 82 | if( !items[ 'steamdb-highlight-family' ] ) 83 | { 84 | resolve( {} ); 85 | return; 86 | } 87 | 88 | SendMessageToBackgroundScript( { 89 | contentScriptQuery: 'FetchSteamUserFamilyData', 90 | }, resolve ); 91 | } ); 92 | 93 | /** @type {Promise<{data?: undefined, error?: string}>} */ 94 | const familyDataTimeoutPromise = new Promise( ( resolve ) => 95 | { 96 | setTimeout( () => 97 | { 98 | resolve( { error: 'Family data timed out' } ); 99 | }, 10000 ); // 10 seconds 100 | } ); 101 | 102 | const userData = await userDataPromise; 103 | 104 | // If family data does not load fast enough, assume it failed 105 | const familyData = await Promise.race( [ 106 | familyDataPromise, 107 | familyDataTimeoutPromise, 108 | ] ); 109 | 110 | if( userData.error ) 111 | { 112 | WriteLog( 'Failed to load userdata', userData.error ); 113 | 114 | window.postMessage( { 115 | version: EXTENSION_INTEROP_VERSION, 116 | type: 'steamdb:extension-error', 117 | error: `Failed to load your games. ${userData.error}`, 118 | }, GetHomepage() ); 119 | } 120 | 121 | if( familyData.error ) 122 | { 123 | WriteLog( 'Failed to load family userdata', familyData.error ); 124 | } 125 | 126 | /** @type {Record} */ 127 | let response = null; 128 | 129 | if( userData.data ) 130 | { 131 | response = userData.data; 132 | 133 | if( familyData.data ) 134 | { 135 | response.rgFamilySharedApps = familyData.data.rgFamilySharedApps; 136 | 137 | if( familyData.data.rgOwnedApps ) 138 | { 139 | // Merge owned apps from the shared library because it returns extra apps 140 | // that are not returned by the dynamicstore such as tools 141 | response.rgOwnedApps = Array.from( new Set( [ 142 | ...response.rgOwnedApps, 143 | ...familyData.data.rgOwnedApps 144 | ] ) ); 145 | } 146 | } 147 | } 148 | 149 | const OnPageLoaded = () => 150 | { 151 | if( response ) 152 | { 153 | window.postMessage( { 154 | version: EXTENSION_INTEROP_VERSION, 155 | type: 'steamdb:extension-loaded', 156 | data: response, 157 | }, GetHomepage() ); 158 | 159 | WriteLog( 160 | 'Userdata loaded', 161 | 'Packages', 162 | response.rgOwnedPackages?.length || 0, 163 | 'Family Apps', 164 | response.rgFamilySharedApps?.length || 0, 165 | ); 166 | } 167 | }; 168 | 169 | const IsSiteReady = () => document.readyState === 'complete' || !!document.getElementById( 'main' ); 170 | 171 | if( IsSiteReady() ) 172 | { 173 | OnPageLoaded(); 174 | return; 175 | } 176 | 177 | // As we wait for promises to complete first, chances are very high that the main element should be ready by now, 178 | // but to avoid any possible issues we still have a fallback to the mutation observer. 179 | // 180 | // The website has code to process the extension messages loaded in a script before the #main element, 181 | // and this script is not deferred. But to apply the highlights to all the elements correctly, 182 | // the site will have to wait for the DOM to complete loading before applying the highlights. 183 | // 184 | // We avoid waiting for DOMContentLoaded event and instead wait for the receiving script to be ready, 185 | // because postMessage() itself may take time. 186 | 187 | WriteLog( 'Data loaded too fast, site is not yet ready.' ); 188 | 189 | const observer = new MutationObserver( () => 190 | { 191 | if( IsSiteReady() ) 192 | { 193 | observer.disconnect(); 194 | OnPageLoaded(); 195 | } 196 | } ); 197 | 198 | observer.observe( document.documentElement, { 199 | childList: true, 200 | subtree: true 201 | } ); 202 | } ); 203 | -------------------------------------------------------------------------------- /scripts/store/account_licenses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const script = document.createElement( 'script' ); 4 | script.id = 'steamdb_disable_tooltips'; 5 | script.type = 'text/javascript'; 6 | script.src = GetLocalResource( 'scripts/store/account_licenses_injected.js' ); 7 | document.documentElement.append( script ); 8 | 9 | GetOption( { 'link-accountpage': true }, ( items ) => 10 | { 11 | const addLinks = items[ 'link-accountpage' ]; 12 | 13 | if( document.readyState === 'loading' ) 14 | { 15 | document.addEventListener( 'DOMContentLoaded', OnContentLoaded ); 16 | } 17 | else 18 | { 19 | OnContentLoaded(); 20 | } 21 | 22 | function OnContentLoaded() 23 | { 24 | const table = document.querySelector( '.account_table' ); 25 | 26 | if( !addLinks || !table ) 27 | { 28 | document.body.classList.add( 'steamdb_account_table_loaded' ); 29 | return; 30 | } 31 | 32 | const licenses = table.querySelectorAll( 'tr' ); 33 | 34 | if( licenses ) 35 | { 36 | const params = new URLSearchParams(); 37 | params.set( 'a', 'sub' ); 38 | 39 | for( const tr of licenses ) 40 | { 41 | const nameCell = tr.cells[ 1 ]; 42 | 43 | if( nameCell.tagName === 'TH' ) 44 | { 45 | const newTd = document.createElement( 'th' ); 46 | newTd.className = 'steamdb_license_id_col'; 47 | newTd.textContent = 'SteamDB'; 48 | nameCell.after( newTd ); 49 | 50 | continue; 51 | } 52 | 53 | /** @type {HTMLAnchorElement} */ 54 | const link = document.createElement( 'a' ); 55 | 56 | /** @type {HTMLAnchorElement} */ 57 | const removeElement = nameCell.querySelector( '.free_license_remove_link a' ); 58 | 59 | if( removeElement ) 60 | { 61 | const subidMatch = removeElement.href.match( /RemoveFreeLicense\( ?(?[0-9]+)/ ); 62 | 63 | if( !subidMatch ) 64 | { 65 | continue; 66 | } 67 | 68 | const subid = subidMatch.groups.subid; 69 | 70 | link.href = `${GetHomepage()}sub/${subid}/`; 71 | link.textContent = subid; 72 | } 73 | else 74 | { 75 | params.set( 'q', nameCell.textContent.trim() ); 76 | 77 | link.href = `${GetHomepage()}search/?${params.toString()}`; 78 | link.textContent = _t( 'Search' ); 79 | } 80 | 81 | const newTd = document.createElement( 'td' ); 82 | newTd.className = 'steamdb_license_id_col'; 83 | newTd.append( link ); 84 | nameCell.after( newTd ); 85 | } 86 | } 87 | 88 | document.body.classList.add( 'steamdb_account_table_loaded' ); 89 | } 90 | } ); 91 | -------------------------------------------------------------------------------- /scripts/store/account_licenses_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( document.body ) 6 | { 7 | PerformHook(); 8 | } 9 | else 10 | { 11 | // If the script was injected too early, wait for element to be created 12 | const observer = new MutationObserver( () => 13 | { 14 | if( document.body ) 15 | { 16 | PerformHook(); 17 | 18 | observer.disconnect(); 19 | } 20 | } ); 21 | 22 | observer.observe( document, { 23 | childList: true, 24 | subtree: true, 25 | } ); 26 | } 27 | 28 | function PerformHook() 29 | { 30 | const noop = () => 31 | { 32 | // noop 33 | }; 34 | 35 | window.InstrumentLinks = noop; 36 | window.BindTooltips = noop; 37 | 38 | if( window.GDynamicStore ) 39 | { 40 | window.GDynamicStore.Init = noop; 41 | } 42 | 43 | // As Valve's own comment says this function is for "perf sensitive pages" 44 | if( window.DisableTooltipMutationObserver ) 45 | { 46 | window.DisableTooltipMutationObserver(); 47 | } 48 | } 49 | } )() ); 50 | -------------------------------------------------------------------------------- /scripts/store/agecheck.js: -------------------------------------------------------------------------------- 1 | /* global AddLinksInErrorBox */ 2 | 3 | 'use strict'; 4 | 5 | if( GetCurrentAppID() > 0 ) 6 | { 7 | const elementIdsToTry = 8 | [ 9 | 'error_box', 10 | 'app_agegate', 11 | 'agegate_box', 12 | ]; 13 | 14 | for( const id of elementIdsToTry ) 15 | { 16 | const container = document.getElementById( id ); 17 | 18 | if( container ) 19 | { 20 | AddLinksInErrorBox( container ); 21 | break; 22 | } 23 | } 24 | } 25 | 26 | GetOption( { 'enhancement-skip-agecheck': false }, ( items ) => 27 | { 28 | if( items[ 'enhancement-skip-agecheck' ] ) 29 | { 30 | const dateFuture = new Date(); 31 | dateFuture.setFullYear( dateFuture.getFullYear() + 1 ); 32 | const date = dateFuture.toUTCString(); 33 | 34 | document.cookie = 'wants_mature_content=1; expires=' + date + '; path=/; Secure; SameSite=Lax;'; 35 | document.cookie = 'mature_content=1; expires=' + date + '; path=/; Secure; SameSite=Lax;'; 36 | document.cookie = 'lastagecheckage=1-January-1900; expires=' + date + '; path=/; Secure; SameSite=Lax;'; 37 | document.cookie = 'birthtime=-' + ( 30 ** 6 ) + '; expires=' + date + '; path=/; Secure; SameSite=Lax;'; 38 | 39 | // Make sure we know how to bypass this agegate before redirecting 40 | // App 526520 causes inifite redirects due to an error message on agecheck url 41 | if( document.querySelector( '#agecheck_form, #app_agegate' ) ) 42 | { 43 | document.location.href = document.location.href.replace( /\/agecheck/, '' ); 44 | } 45 | } 46 | else 47 | { 48 | const container = document.getElementById( 'app_agegate' ); 49 | 50 | if( !container ) 51 | { 52 | return; 53 | } 54 | 55 | const optionsLink = document.createElement( 'a' ); 56 | optionsLink.target = '_blank'; 57 | optionsLink.textContent = _t( 'agecheck_option_hint' ); 58 | optionsLink.href = GetLocalResource( 'options/options.html' ) + '#skip_agecheck'; 59 | 60 | const linkContainer = document.createElement( 'div' ); 61 | linkContainer.className = 'steamdb_error_link steamdb_agecheck_hint'; 62 | linkContainer.append( optionsLink ); 63 | 64 | container.append( linkContainer ); 65 | } 66 | } ); 67 | -------------------------------------------------------------------------------- /scripts/store/app_error.d.ts: -------------------------------------------------------------------------------- 1 | declare function AddLinksInErrorBox(container: Element): void; 2 | -------------------------------------------------------------------------------- /scripts/store/app_error.js: -------------------------------------------------------------------------------- 1 | /* exported AddLinksInErrorBox */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * @param {Element} container 7 | */ 8 | function AddLinksInErrorBox( container ) 9 | { 10 | const pageTypeMatch = location.pathname.match( /^\/(?:[a-z]+\/)?(?app|bundle|sub)\// ); 11 | 12 | if( !pageTypeMatch ) 13 | { 14 | return; 15 | } 16 | 17 | const pageType = pageTypeMatch.groups.type; 18 | 19 | const linkContainer = document.createElement( 'div' ); 20 | linkContainer.className = 'steamdb_error_link'; 21 | 22 | // SteamDB 23 | let link = document.createElement( 'a' ); 24 | link.className = 'btnv6_blue_hoverfade btn_medium'; 25 | link.href = `${GetHomepage()}${pageType}/${GetCurrentAppID()}/`; 26 | 27 | let text = document.createElement( 'span' ); 28 | text.append( document.createTextNode( _t( 'view_on_steamdb' ) ) ); 29 | link.append( text ); 30 | 31 | linkContainer.append( link ); 32 | 33 | // PCGW 34 | if( pageType === 'app' ) 35 | { 36 | linkContainer.append( document.createTextNode( ' ' ) ); 37 | 38 | link = document.createElement( 'a' ); 39 | link.className = 'btnv6_blue_hoverfade btn_medium'; 40 | link.href = 'https://pcgamingwiki.com/api/appid.php?appid=' + GetCurrentAppID() + '&utm_source=SteamDB'; 41 | 42 | text = document.createElement( 'span' ); 43 | text.append( document.createTextNode( _t( 'view_on_pcgamingwiki' ) ) ); 44 | link.append( text ); 45 | 46 | linkContainer.append( link ); 47 | } 48 | 49 | container.append( linkContainer ); 50 | } 51 | -------------------------------------------------------------------------------- /scripts/store/app_images.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 4 | 'prevent-store-images': false, 5 | }, ( items ) => 6 | { 7 | if( !items[ 'prevent-store-images' ] ) 8 | { 9 | return; 10 | } 11 | 12 | /** @type {HTMLImageElement[]} */ 13 | const images = Array.from( document.querySelectorAll( '.game_area_description img, .early_access_announcements img' ) ); 14 | 15 | if( !images.length ) 16 | { 17 | return; 18 | } 19 | 20 | for( const image of images ) 21 | { 22 | if( image.complete ) 23 | { 24 | continue; 25 | } 26 | 27 | image.style.width = ( image.width || 100 ) + 'px'; 28 | image.style.height = ( image.height || 100 ) + 'px'; 29 | image.dataset.src = image.src; 30 | image.src = GetLocalResource( 'icons/image.svg' ); 31 | image.addEventListener( 'click', ImageClick, { once: true } ); 32 | } 33 | 34 | /** 35 | * @param {MouseEvent} e 36 | * @this {HTMLImageElement} 37 | */ 38 | function ImageClick( e ) 39 | { 40 | this.addEventListener( 'load', ImageLoad, { once: true } ); 41 | this.src = this.dataset.src; 42 | e.preventDefault(); 43 | } 44 | 45 | /** 46 | * @this {HTMLImageElement} 47 | */ 48 | function ImageLoad() 49 | { 50 | this.style.width = ''; 51 | this.style.height = ''; 52 | } 53 | } ); 54 | -------------------------------------------------------------------------------- /scripts/store/app_news.js: -------------------------------------------------------------------------------- 1 | /* global AddLinksInErrorBox */ 2 | 3 | 'use strict'; 4 | 5 | const container = document.getElementById( 'error_box' ); 6 | 7 | if( container && GetCurrentAppID() > 0 ) 8 | { 9 | AddLinksInErrorBox( container ); 10 | } 11 | -------------------------------------------------------------------------------- /scripts/store/bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'button-sub': true }, ( items ) => 4 | { 5 | if( !items[ 'button-sub' ] ) 6 | { 7 | return; 8 | } 9 | 10 | const container = document.querySelector( '.game_meta_data' ); 11 | 12 | if( container ) 13 | { 14 | let element = document.createElement( 'span' ); 15 | element.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) ); 16 | 17 | const link = document.createElement( 'a' ); 18 | link.className = 'action_btn'; 19 | link.href = GetHomepage() + 'bundle/' + GetCurrentAppID() + '/'; 20 | link.appendChild( element ); 21 | 22 | element = document.createElement( 'div' ); 23 | element.className = 'block'; 24 | element.appendChild( link ); 25 | 26 | container.insertBefore( element, container.firstChild ); 27 | } 28 | } ); 29 | -------------------------------------------------------------------------------- /scripts/store/explore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Fix Valve's bug where empty queue banner has wrong height and it hides text 4 | /** @type {HTMLElement} */ 5 | const emptyQueue = document.querySelector( '.discover_queue_empty' ); 6 | 7 | if( emptyQueue ) 8 | { 9 | emptyQueue.style.height = 'auto'; 10 | } 11 | 12 | const applicationConfigElement = document.getElementById( 'application_config' ); 13 | 14 | if( !applicationConfigElement ) 15 | { 16 | throw new Error( 'Failed to find application_config' ); 17 | } 18 | 19 | const storeUserConfigJSON = applicationConfigElement.dataset.store_user_config; 20 | const applicationConfig = JSON.parse( applicationConfigElement.dataset.config ); 21 | const accessToken = storeUserConfigJSON && JSON.parse( storeUserConfigJSON ).webapi_token; 22 | 23 | if( !accessToken || !applicationConfig ) 24 | { 25 | throw new Error( 'Failed to get application_config' ); 26 | } 27 | 28 | /** @type {HTMLElement} */ 29 | let exploreButton; 30 | /** @type {HTMLElement} */ 31 | let exploreStatus; 32 | /** @type {HTMLElement} */ 33 | let itemButton; 34 | /** @type {HTMLElement} */ 35 | let itemStatus; 36 | /** @type {HTMLImageElement} */ 37 | let itemImage; 38 | 39 | CreateSaleItemContainer(); 40 | CreateExploreContainer(); 41 | 42 | function CreateExploreContainer() 43 | { 44 | const buttonContainer = document.createElement( 'div' ); 45 | buttonContainer.className = 'steamdb_cheat_queue discovery_queue_customize_ctn'; 46 | 47 | exploreButton = document.createElement( 'div' ); 48 | exploreButton.className = 'btnv6_blue_hoverfade btn_medium'; 49 | const span = document.createElement( 'span' ); 50 | span.textContent = _t( 'explore_auto_discover' ); 51 | exploreButton.append( span ); 52 | buttonContainer.append( exploreButton ); 53 | 54 | exploreStatus = document.createElement( 'div' ); 55 | exploreStatus.className = 'steamdb_cheat_queue_text'; 56 | exploreStatus.textContent = _t( 'explore_auto_discover_description' ); 57 | buttonContainer.append( exploreStatus ); 58 | 59 | const image = document.createElement( 'img' ); 60 | image.src = GetLocalResource( 'icons/white.svg' ); 61 | image.width = 32; 62 | image.height = 32; 63 | buttonContainer.append( image ); 64 | 65 | const container = document.querySelector( '.discovery_queue_customize_ctn' ); 66 | container.parentNode.insertBefore( buttonContainer, container ); 67 | 68 | exploreButton.addEventListener( 'click', ( ) => 69 | { 70 | if( exploreButton.classList.contains( 'btn_disabled' ) ) 71 | { 72 | return; 73 | } 74 | 75 | StartViewTransition( () => 76 | { 77 | exploreStatus.textContent = _t( 'explore_generating' ); 78 | exploreButton.classList.add( 'btn_disabled' ); 79 | GenerateQueue(); 80 | } ); 81 | }, false ); 82 | } 83 | 84 | function CreateSaleItemContainer() 85 | { 86 | const buttonContainer = document.createElement( 'div' ); 87 | buttonContainer.className = 'steamdb_saleitem_claim discovery_queue_customize_ctn'; 88 | 89 | itemButton = document.createElement( 'div' ); 90 | itemButton.className = 'btnv6_blue_hoverfade btn_medium btn_disabled'; 91 | const span = document.createElement( 'span' ); 92 | span.textContent = _t( 'explore_saleitem_claim' ); 93 | itemButton.append( span ); 94 | buttonContainer.append( itemButton ); 95 | 96 | itemStatus = document.createElement( 'div' ); 97 | itemStatus.className = 'steamdb_cheat_queue_text'; 98 | itemStatus.textContent = _t( 'explore_saleitem_cant_claim' ); 99 | buttonContainer.append( itemStatus ); 100 | 101 | itemImage = document.createElement( 'img' ); 102 | itemImage.src = GetLocalResource( 'icons/white.svg' ); 103 | itemImage.width = 32; 104 | itemImage.height = 32; 105 | itemImage.style.opacity = '0'; 106 | buttonContainer.append( itemImage ); 107 | 108 | const container = document.querySelector( '.discovery_queue_customize_ctn' ); 109 | container.parentNode.insertBefore( buttonContainer, container ); 110 | 111 | itemButton.addEventListener( 'click', ( ) => 112 | { 113 | if( itemButton.classList.contains( 'btn_disabled' ) ) 114 | { 115 | return; 116 | } 117 | 118 | StartViewTransition( () => 119 | { 120 | itemButton.classList.add( 'btn_disabled' ); 121 | ClaimSaleItem(); 122 | } ); 123 | }, false ); 124 | 125 | CheckClaimSaleItem(); 126 | } 127 | 128 | function GenerateQueue( generateFails = 0 ) 129 | { 130 | const valveQueueEl = document.getElementById( 'discovery_queue_ctn' ); 131 | 132 | if( valveQueueEl ) 133 | { 134 | valveQueueEl.style.display = 'none'; 135 | } 136 | 137 | if( emptyQueue ) 138 | { 139 | emptyQueue.style.display = 'block'; 140 | } 141 | 142 | const params = new URLSearchParams(); 143 | params.set( 'origin', location.origin ); 144 | params.set( 'access_token', accessToken ); 145 | params.set( 'country_code', applicationConfig.COUNTRY || 'US' ); 146 | params.set( 'rebuild_queue', '1' ); 147 | params.set( 'queue_type', '0' ); // k_EStoreDiscoveryQueueTypeNew 148 | params.set( 'ignore_user_preferences', '1' ); 149 | 150 | fetch( 151 | `${applicationConfig.WEBAPI_BASE_URL}IStoreService/GetDiscoveryQueue/v1/?${params.toString()}`, 152 | ) 153 | .then( ( response ) => 154 | { 155 | if( !response.ok ) 156 | { 157 | throw new Error( `HTTP ${response.status}` ); 158 | } 159 | 160 | return response.json(); 161 | } ) 162 | .then( ( data ) => 163 | { 164 | if( !data.response || !data.response.appids ) 165 | { 166 | throw new Error( 'Unexpected response' ); 167 | } 168 | 169 | const appids = data.response.appids; 170 | let done = 0; 171 | let fails = 0; 172 | 173 | /** 174 | * @param {Response} response 175 | */ 176 | const requestDone = ( response ) => 177 | { 178 | if( response.status !== 200 ) 179 | { 180 | requestFail( new Error( `HTTP ${response.status}` ) ); 181 | return; 182 | } 183 | 184 | fails--; 185 | 186 | if( ++done === appids.length ) 187 | { 188 | exploreButton.classList.remove( 'btn_disabled' ); 189 | exploreStatus.textContent = _t( 'explore_finished' ); 190 | } 191 | else 192 | { 193 | exploreStatus.textContent = _t( 'explore_exploring', [ done, appids.length ] ); 194 | 195 | requestNextInQueue( done ); 196 | } 197 | }; 198 | 199 | /** 200 | * @param {any} error 201 | */ 202 | const requestFail = ( error ) => 203 | { 204 | WriteLog( 'Failed to clear queue item', error ); 205 | 206 | if( ++fails >= 10 ) 207 | { 208 | exploreButton.classList.remove( 'btn_disabled' ); 209 | exploreStatus.textContent = _t( 'explore_failed_to_clear_too_many' ); 210 | return; 211 | } 212 | 213 | setTimeout( () => 214 | { 215 | requestNextInQueue( done ); 216 | }, RandomInt( 5000, 10000 ) ); 217 | }; 218 | 219 | /** 220 | * @param {number} index 221 | */ 222 | const requestNextInQueue = ( index ) => 223 | { 224 | const skipParams = new URLSearchParams(); 225 | skipParams.set( 'origin', location.origin ); 226 | skipParams.set( 'access_token', accessToken ); 227 | skipParams.set( 'appid', appids[ index ] ); 228 | 229 | fetch( 230 | `${applicationConfig.WEBAPI_BASE_URL}IStoreService/SkipDiscoveryQueueItem/v1/?${skipParams.toString()}`, 231 | { 232 | method: 'POST', 233 | }, 234 | ) 235 | .then( requestDone ) 236 | .catch( requestFail ); 237 | }; 238 | 239 | requestNextInQueue( 0 ); 240 | } ) 241 | .catch( ( error ) => 242 | { 243 | WriteLog( 'Failed to get discovery queue', error ); 244 | 245 | if( ++generateFails >= 20 ) 246 | { 247 | exploreButton.classList.remove( 'btn_disabled' ); 248 | exploreStatus.textContent = _t( 'explore_failed_to_clear_too_many' ); 249 | return; 250 | } 251 | 252 | exploreStatus.textContent = `${_t( 'explore_generating' )} (#${generateFails})`; 253 | 254 | setTimeout( () => 255 | { 256 | GenerateQueue( generateFails ); 257 | }, RandomInt( 5000, 10000 * generateFails ) ); 258 | } ); 259 | } 260 | 261 | /** 262 | * @param {Record} response 263 | */ 264 | function HandleSaleItemResponse( response ) 265 | { 266 | if( response.next_claim_time ) 267 | { 268 | const dateFormatter = new Intl.DateTimeFormat( GetLanguage(), { 269 | dateStyle: 'medium', 270 | timeStyle: 'short', 271 | } ); 272 | const nextClaimTime = dateFormatter.format( response.next_claim_time * 1000 ); 273 | 274 | itemStatus.textContent += ' ' + _t( 'explore_saleitem_next_item_time', [ nextClaimTime.toLocaleString() ] ); 275 | 276 | const timer = ( response.next_claim_time * 1000 ) - Date.now(); 277 | 278 | setTimeout( () => 279 | { 280 | itemStatus.textContent = _t( 'explore_saleitem_claim_description' ); 281 | itemButton.classList.remove( 'btn_disabled' ); 282 | }, timer ); 283 | } 284 | 285 | if( response.reward_item?.community_item_data ) 286 | { 287 | const item = response.reward_item.community_item_data; 288 | const file = item.item_image_small || item.item_image_large; 289 | itemImage.src = `${applicationConfig.MEDIA_CDN_COMMUNITY_URL}images/items/${response.reward_item.appid}/${file}`; 290 | itemImage.title = item.item_title; 291 | itemImage.style.opacity = '1'; 292 | } 293 | } 294 | 295 | function CheckClaimSaleItem( fails = 0 ) 296 | { 297 | const params = new URLSearchParams(); 298 | params.set( 'origin', location.origin ); 299 | params.set( 'access_token', accessToken ); 300 | params.set( 'language', applicationConfig.LANGUAGE ); 301 | 302 | fetch( `${applicationConfig.WEBAPI_BASE_URL}ISaleItemRewardsService/CanClaimItem/v1/?${params.toString()}` ) 303 | .then( ( response ) => 304 | { 305 | if( !response.ok ) 306 | { 307 | throw new Error( `HTTP ${response.status}` ); 308 | } 309 | 310 | return response.json(); 311 | } ) 312 | .then( ( data ) => 313 | { 314 | const response = data.response; 315 | 316 | StartViewTransition( () => 317 | { 318 | if( response.can_claim ) 319 | { 320 | itemStatus.textContent = _t( 'explore_saleitem_claim_description' ); 321 | itemButton.classList.remove( 'btn_disabled' ); 322 | return; 323 | } 324 | 325 | itemStatus.textContent = _t( 'explore_saleitem_cant_claim' ); 326 | 327 | HandleSaleItemResponse( response ); 328 | } ); 329 | } ) 330 | .catch( ( error ) => 331 | { 332 | WriteLog( 'Failed to find out if a sale item can be claimed', error ); 333 | 334 | if( ++fails >= 5 ) 335 | { 336 | itemStatus.textContent = _t( 'explore_saleitem_cant_claim' ); 337 | return; 338 | } 339 | 340 | setTimeout( () => 341 | { 342 | CheckClaimSaleItem( fails ); 343 | }, RandomInt( 5000, 10000 ) ); 344 | } ); 345 | } 346 | 347 | function ClaimSaleItem( fails = 0 ) 348 | { 349 | itemStatus.textContent = _t( 'explore_saleitem_trying_to_claim' ); 350 | 351 | const params = new URLSearchParams(); 352 | params.set( 'origin', location.origin ); 353 | params.set( 'access_token', accessToken ); 354 | params.set( 'language', applicationConfig.LANGUAGE ); 355 | 356 | fetch( 357 | `${applicationConfig.WEBAPI_BASE_URL}ISaleItemRewardsService/ClaimItem/v1/?${params.toString()}`, 358 | { 359 | method: 'POST', 360 | }, 361 | ) 362 | .then( ( response ) => 363 | { 364 | if( !response.ok ) 365 | { 366 | throw new Error( `HTTP ${response.status}` ); 367 | } 368 | 369 | return response.json(); 370 | } ) 371 | .then( ( data ) => 372 | { 373 | const response = data.response; 374 | 375 | if( !response || !response.communityitemid ) 376 | { 377 | fails = 10; // If there is no item to claim the response is just empty 378 | throw new Error( 'Unexpected response' ); 379 | } 380 | 381 | StartViewTransition( () => 382 | { 383 | const itemTitle = response.reward_item?.community_item_data?.item_title; 384 | itemStatus.textContent = _t( 'explore_saleitem_success', [ itemTitle || `ID #${response.communityitemid}` ] ); 385 | 386 | HandleSaleItemResponse( response ); 387 | } ); 388 | } ) 389 | .catch( ( error ) => 390 | { 391 | WriteLog( 'Failed to get a sale item', error ); 392 | 393 | if( ++fails >= 5 ) 394 | { 395 | itemButton.classList.remove( 'btn_disabled' ); 396 | itemStatus.textContent = _t( 'explore_saleitem_claim_failed' ); 397 | return; 398 | } 399 | 400 | setTimeout( () => 401 | { 402 | ClaimSaleItem( fails ); 403 | }, RandomInt( 5000, 10000 ) ); 404 | } ); 405 | } 406 | 407 | /** 408 | * @param {number} min 409 | * @param {number} max 410 | */ 411 | function RandomInt( min, max ) 412 | { 413 | return Math.floor( Math.random() * ( max - min + 1 ) + min ); 414 | } 415 | 416 | /** 417 | * @param {ViewTransitionUpdateCallback} callback 418 | */ 419 | function StartViewTransition( callback ) 420 | { 421 | if( document.startViewTransition ) 422 | { 423 | document.startViewTransition( () => 424 | { 425 | try 426 | { 427 | callback(); 428 | } 429 | catch( e ) 430 | { 431 | console.error( e ); 432 | } 433 | } ); 434 | } 435 | else 436 | { 437 | callback(); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /scripts/store/invalidate_cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'steamdb-highlight': true }, ( items ) => 4 | { 5 | if( !items[ 'steamdb-highlight' ] ) 6 | { 7 | return; 8 | } 9 | 10 | const element = document.createElement( 'script' ); 11 | element.id = 'steamdb_invalidate_cache'; 12 | element.type = 'text/javascript'; 13 | element.src = GetLocalResource( 'scripts/store/invalidate_cache_injected.js' ); 14 | 15 | document.head.appendChild( element ); 16 | 17 | window.addEventListener( 'message', ( request ) => 18 | { 19 | if( request?.data && request.data.type === 'steamdb:extension-invalidate-cache' ) 20 | { 21 | WriteLog( 'Invalidating userdata cache' ); 22 | SendMessageToBackgroundScript( { 23 | contentScriptQuery: 'InvalidateCache', 24 | }, () => 25 | { 26 | // noop 27 | } ); 28 | } 29 | } ); 30 | } ); 31 | -------------------------------------------------------------------------------- /scripts/store/invalidate_cache_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | if( !window.GDynamicStore || !window.GDynamicStore.InvalidateCache ) 6 | { 7 | return; 8 | } 9 | 10 | const originalGDynamicStoreInvalidateCache = window.GDynamicStore.InvalidateCache; 11 | 12 | window.GDynamicStore.InvalidateCache = function SteamDB_GDynamicStore_InvalidateCache() 13 | { 14 | originalGDynamicStoreInvalidateCache.apply( this, arguments ); 15 | 16 | window.postMessage( { 17 | type: 'steamdb:extension-invalidate-cache', 18 | }, window.location.origin ); 19 | }; 20 | } )() ); 21 | -------------------------------------------------------------------------------- /scripts/store/registerkey.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'enhancement-market-ssa': true }, ( items ) => 4 | { 5 | if( items[ 'enhancement-market-ssa' ] ) 6 | { 7 | /** @type {HTMLInputElement} */ 8 | const element = document.querySelector( '#accept_ssa' ); 9 | 10 | if( element ) 11 | { 12 | element.checked = true; 13 | } 14 | } 15 | } ); 16 | 17 | const element = document.createElement( 'script' ); 18 | element.id = 'steamdb_registerkey_hook'; 19 | element.type = 'text/javascript'; 20 | element.src = GetLocalResource( 'scripts/store/registerkey_injected.js' ); 21 | element.dataset.icon = GetLocalResource( 'icons/white.svg' ); 22 | element.dataset.homepage = GetHomepage(); 23 | 24 | document.head.appendChild( element ); 25 | -------------------------------------------------------------------------------- /scripts/store/registerkey_injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | const script = document.getElementById( 'steamdb_registerkey_hook' ); 6 | const originalOnRegisterProductKeyFailure = window.OnRegisterProductKeyFailure; 7 | 8 | /** 9 | * @param {any} ePurchaseResult 10 | * @param {any} receipt 11 | */ 12 | window.OnRegisterProductKeyFailure = function SteamDB_OnRegisterProductKeyFailure( ePurchaseResult, receipt ) 13 | { 14 | originalOnRegisterProductKeyFailure.apply( this, arguments ); 15 | 16 | if( receipt?.line_items && receipt.line_items.length > 0 ) 17 | { 18 | document.getElementById( 'error_display' ).appendChild( FormatLineItems( receipt.line_items ) ); 19 | } 20 | }; 21 | 22 | /** 23 | * @param {any} result 24 | */ 25 | window.UpdateReceiptForm = function SteamDB_UpdateReceiptForm( result ) 26 | { 27 | const list = document.getElementById( 'registerkey_productlist' ); 28 | 29 | while( list.firstChild ) 30 | { 31 | list.firstChild.remove(); 32 | } 33 | 34 | list.appendChild( FormatLineItems( result.purchase_receipt_info.line_items ) ); 35 | }; 36 | 37 | /** 38 | * @param {Record[]} line_items 39 | */ 40 | function FormatLineItems( line_items ) 41 | { 42 | const fragment = document.createElement( 'div' ); 43 | fragment.className = 'steamdb_registerkey_lineitem'; 44 | 45 | const image = document.createElement( 'img' ); 46 | image.src = script.dataset.icon; 47 | 48 | for( const item of line_items ) 49 | { 50 | const lineitem = document.createElement( 'div' ); 51 | lineitem.className = 'registerkey_lineitem'; 52 | lineitem.append( image ); 53 | 54 | let link = document.createElement( 'a' ); 55 | link.href = script.dataset.homepage + 'sub/' + item.packageid + '/'; 56 | link.rel = 'noreferrer'; 57 | link.target = '_blank'; 58 | link.appendChild( document.createTextNode( 'SteamDB' ) ); 59 | 60 | lineitem.append( link ); 61 | lineitem.appendChild( document.createTextNode( ' - ' ) ); 62 | 63 | link = document.createElement( 'a' ); 64 | link.href = 'steam://subscriptioninstall/' + item.packageid; 65 | link.appendChild( document.createTextNode( 'Install' ) ); 66 | 67 | lineitem.append( link ); 68 | lineitem.appendChild( document.createTextNode( ' - ' + item.line_item_description ) ); 69 | 70 | fragment.appendChild( lineitem ); 71 | } 72 | 73 | return fragment; 74 | } 75 | } )() ); 76 | -------------------------------------------------------------------------------- /scripts/store/sub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'button-sub': true }, ( items ) => 4 | { 5 | if( !items[ 'button-sub' ] ) 6 | { 7 | return; 8 | } 9 | 10 | const container = document.querySelector( '.game_meta_data' ); 11 | 12 | if( container ) 13 | { 14 | let element = document.createElement( 'span' ); 15 | element.appendChild( document.createTextNode( _t( 'view_on_steamdb' ) ) ); 16 | 17 | const link = document.createElement( 'a' ); 18 | link.className = 'action_btn'; 19 | link.href = GetHomepage() + 'sub/' + GetCurrentAppID() + '/'; 20 | link.appendChild( element ); 21 | 22 | element = document.createElement( 'div' ); 23 | element.className = 'block'; 24 | element.appendChild( link ); 25 | 26 | container.insertBefore( element, container.firstChild ); 27 | } 28 | } ); 29 | -------------------------------------------------------------------------------- /scripts/store/subscriptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | ( ( () => 4 | { 5 | const scriptHook = document.getElementById( 'steamdb_subscriptions_hook' ); 6 | const homepage = scriptHook.dataset.homepage; 7 | const originalDropdownSelectOption = window.GamePurchaseDropdownSelectOption; 8 | 9 | /** 10 | * @param {string} dropdownName 11 | * @param {number} subId 12 | */ 13 | window.GamePurchaseDropdownSelectOption = function SteamDB_GamePurchaseDropdownSelectOption( dropdownName, subId ) 14 | { 15 | originalDropdownSelectOption.apply( this, arguments ); 16 | 17 | const cartForm = document.getElementById( `add_to_cart_${dropdownName}` ); 18 | 19 | if( !cartForm ) 20 | { 21 | return; 22 | } 23 | 24 | /** @type {HTMLAnchorElement} */ 25 | const link = cartForm.parentNode.querySelector( '.steamdb_link' ); 26 | link.hidden = false; 27 | link.href = `${homepage}sub/${subId}/`; 28 | link.querySelector( '.steamdb_link_id' ).textContent = subId.toString(); 29 | }; 30 | } )() ); 31 | -------------------------------------------------------------------------------- /scripts/store/widget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | GetOption( { 'link-subid-widget': true }, ( items ) => 4 | { 5 | if( !items[ 'link-subid-widget' ] ) 6 | { 7 | return; 8 | } 9 | 10 | const link = document.createElement( 'a' ); 11 | link.className = 'btn_black btn_tiny steamdb_link'; 12 | link.target = '_blank'; 13 | 14 | const text = document.createElement( 'span' ); 15 | text.className = 'steamdb_link_id'; 16 | 17 | /** @type {HTMLInputElement} */ 18 | const subid = document.querySelector( 'input[name="subid"]' ); 19 | 20 | if( subid ) 21 | { 22 | link.href = GetHomepage() + 'sub/' + subid.value + '/'; 23 | text.textContent = subid.value.toString(); 24 | } 25 | else 26 | { 27 | const appid = GetCurrentAppID(); 28 | 29 | link.href = GetHomepage() + 'app/' + appid + '/'; 30 | text.textContent = appid.toString(); 31 | } 32 | 33 | const span = document.createElement( 'span' ); 34 | span.dataset.tooltipText = _t( 'view_on_steamdb' ); 35 | 36 | const hash = document.createElement( 'span' ); 37 | hash.style.fontWeight = 'bold'; 38 | hash.textContent = '# '; 39 | span.append( hash ); 40 | 41 | span.append( text ); 42 | link.append( span ); 43 | 44 | let platforms = document.querySelector( '.game_area_purchase_platform' ); 45 | 46 | if( !platforms ) 47 | { 48 | platforms = document.createElement( 'div' ); 49 | platforms.className = 'game_area_purchase_platform'; 50 | 51 | const widget = document.getElementById( 'widget' ); 52 | 53 | if( !widget ) 54 | { 55 | return; 56 | } 57 | 58 | widget.append( platforms ); 59 | } 60 | 61 | platforms.append( link ); 62 | } ); 63 | -------------------------------------------------------------------------------- /scripts/window.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | [propName: string]: any; 3 | } 4 | 5 | declare namespace browser { 6 | namespace runtime { 7 | const OnInstalledReason: typeof chrome.runtime.OnInstalledReason; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /styles/account_licenses.css: -------------------------------------------------------------------------------- 1 | /* These styles are copied from Steam's account.css for mobile view */ 2 | table.account_table { 3 | table-layout: fixed; 4 | } 5 | 6 | table.account_table td { 7 | word-wrap: break-word; 8 | } 9 | 10 | table.account_table th.license_date_col, 11 | table.account_table td.license_date_col { 12 | width: 7em; 13 | } 14 | 15 | table.account_table th.license_acquisition_col, 16 | table.account_table td.license_acquisition_col { 17 | width: 8em; 18 | } 19 | 20 | /* - */ 21 | 22 | body:not(.steamdb_account_table_loaded) .account_table_ctn::after { 23 | display: block; 24 | padding: 16px; 25 | font-family: "Motiva Sans", Arial, Helvetica, sans-serif; 26 | font-weight: bold; 27 | font-size: 16px; 28 | font-style: italic; 29 | text-align: center; 30 | content: "Page is loading…"; 31 | } 32 | 33 | body:not(.steamdb_account_table_loaded) table.account_table { 34 | display: none; 35 | } 36 | 37 | table.account_table th.steamdb_license_id_col, 38 | table.account_table td.steamdb_license_id_col { 39 | width: 5em; 40 | } 41 | 42 | table.account_table td.steamdb_license_id_col a { 43 | color: inherit; 44 | } 45 | -------------------------------------------------------------------------------- /styles/achievements.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Make the scrollbar stable so the page doesn't shift when collapsing all elements 3 | */ 4 | html { 5 | scrollbar-gutter: stable; 6 | } 7 | 8 | .achieveRow .compareImg { 9 | position: relative; 10 | font-size: 0; /* fix incorrect extra padding */ 11 | } 12 | 13 | .achieveRow:not(.unlocked) .compareImg::before { 14 | content: ""; 15 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cpath fill='%2366C0F4' fill-rule='evenodd' d='M27 26v-2h-1.75l-1.5 2H27zm0 1a3.002 3.002 0 0 1-3.009 3H9.01A3.008 3.008 0 0 1 6 27h21zM6 26v-2h3.25l-1.5 2H6zm2.75 0l1.5-2h2l-1.5 2h-2zm3 0l1.5-2h2l-1.5 2h-2zm3 0l1.5-2h2l-1.5 2h-2zm3 0l1.5-2h2l-1.5 2h-2zm3 0l1.5-2h2l-1.5 2h-2zM27 23v-5.99c0-1.66-1.34-3.005-3-3.01v-3.5a7.5 7.5 0 1 0-15 0V14c-1.658.005-3 1.34-3 3.01V23h21zm-15-9v-3.5C12 8.01 14.015 6 16.5 6c2.48 0 4.5 2.015 4.5 4.5V14h-9z'/%3E%3C/svg%3E%0A"); 16 | background-size: 24px 24px; 17 | width: 24px; 18 | height: 24px; 19 | display: block; 20 | position: absolute; 21 | left: 5px; 22 | top: 22px; 23 | } 24 | 25 | #topSummaryAchievements .compareVal { 26 | margin-top: 10px; 27 | } 28 | 29 | .steamdb_global_achievements_page { 30 | padding: 0 !important; 31 | } 32 | 33 | .steamdb_global_achievements_page #compareAvatar { 34 | display: none; 35 | } 36 | 37 | .steamdb_global_achievements_page #headerContent { 38 | margin-left: 0; 39 | } 40 | 41 | .steamdb_achievement_spoiler { 42 | filter: brightness(3) blur(6px); 43 | } 44 | 45 | .steamdb_achievement_image.steamdb_achievement_spoiler { 46 | filter: grayscale(1) blur(6px); 47 | } 48 | 49 | .steamdb_achievement:hover .steamdb_achievement_spoiler { 50 | filter: unset; 51 | } 52 | 53 | .steamdb_stats_extra_tabs { 54 | float: right; 55 | display: flex; 56 | align-items: center; 57 | gap: 12px; 58 | } 59 | 60 | .steamdb_stats_extra_tabs_mobile { 61 | float: unset; 62 | margin-bottom: 20px; 63 | } 64 | 65 | .steamdb_stats_extra_tabs > a { 66 | display: flex; 67 | } 68 | 69 | .steamdb_stats_tab_icon { 70 | width: 22px; 71 | height: 22px; 72 | } 73 | 74 | .steamdb_achievements_group { 75 | margin-bottom: 40px; 76 | } 77 | 78 | .steamdb_achievements_group > summary { 79 | display: flex; 80 | } 81 | 82 | .steamdb_achievements_title { 83 | display: flex; 84 | align-items: center; 85 | gap: 15px; 86 | color: #fff; 87 | margin-bottom: 15px; 88 | overflow-wrap: anywhere; 89 | } 90 | 91 | .steamdb_achievements_group .steamdb_achievements_game_logo_contain { 92 | position: relative; 93 | display: flex; 94 | } 95 | 96 | .steamdb_achievements_group .steamdb_achievements_game_logo { 97 | width: 184px; 98 | height: 69px; 99 | } 100 | 101 | .steamdb_achievements_game_name { 102 | font-weight: bold; 103 | font-size: 21px; 104 | } 105 | 106 | .steamdb_achievements_update_name { 107 | color: #83868a; 108 | font-size: 16px; 109 | } 110 | 111 | .steamdb_completed_achievements_icon { 112 | position: absolute; 113 | left: -12px; 114 | bottom: -12px; 115 | width: 40px; 116 | height: 40px; 117 | } 118 | 119 | .steamdb_fold_button, 120 | .steamdb_done_button { 121 | margin-left: auto; 122 | background: none; 123 | border: none; 124 | color: #83868a; 125 | display: flex; 126 | place-content: center; 127 | cursor: pointer; 128 | padding: 0; 129 | } 130 | 131 | .steamdb_hide_earned_achievements .steamdb_done_button { 132 | color: #5aa9d6; 133 | } 134 | 135 | .steamdb_fold_button:hover, 136 | .steamdb_fold_button:focus, 137 | .steamdb_done_button:hover, 138 | .steamdb_done_button:focus { 139 | color: #1a9fff; 140 | } 141 | 142 | .steamdb_stats_extra_tabs .steamdb_fold_button, 143 | .steamdb_stats_extra_tabs .steamdb_done_button { 144 | margin-left: 30px; 145 | } 146 | 147 | .steamdb_stats_extra_tabs_mobile .steamdb_fold_button, 148 | .steamdb_stats_extra_tabs_mobile .steamdb_done_button { 149 | margin-left: auto; 150 | } 151 | 152 | .steamdb_stats_extra_tabs .steamdb_done_button + .steamdb_fold_button { 153 | margin-left: 0; 154 | } 155 | 156 | .steamdb_achievements_list { 157 | font-weight: 700; 158 | color: #83868a; 159 | } 160 | 161 | .steamdb_achievements_list + .steamdb_achievements_list { 162 | margin-top: 10px; 163 | } 164 | 165 | .steamdb_achievements_list > summary { 166 | cursor: pointer; 167 | margin-bottom: 4px; 168 | } 169 | 170 | .steamdb_achievements_list > summary:hover { 171 | color: #fff; 172 | } 173 | 174 | .steamdb_achievements_sort_button { 175 | float: right; 176 | } 177 | 178 | .steamdb_achievement { 179 | display: flex; 180 | align-items: center; 181 | gap: 8px; 182 | padding: 4px; 183 | padding-right: 8px; 184 | margin-bottom: 8px; 185 | background-color: #23262e; 186 | position: relative; 187 | } 188 | 189 | .steamdb_achievement[hidden] { 190 | display: none; 191 | } 192 | 193 | .steamdb_achievement_global_progress_overlay { 194 | background: linear-gradient( 195 | to right, 196 | #23262e 40px, 197 | #31343e 40px, 198 | #31343e var(--steamdb-progress), 199 | #23262e var(--steamdb-progress) 200 | ); 201 | } 202 | 203 | .steamdb_achievement_image { 204 | width: 58px; 205 | height: 58px; 206 | z-index: 1; 207 | } 208 | 209 | .steamdb_achievement_image_glow { 210 | box-shadow: 211 | 0 0 2px 1px rgb(255 184 78 / 60%), 212 | 0 0 16px 1px rgb(255 184 78 / 40%); 213 | } 214 | 215 | .steamdb_achievement h3 { 216 | font-size: 16px; 217 | font-weight: 500; 218 | color: #dcdedf; 219 | overflow-wrap: anywhere; 220 | z-index: 1; 221 | } 222 | 223 | .steamdb_achievement h5 { 224 | font-size: 12px; 225 | font-weight: 400; 226 | color: #b8bcbf; 227 | overflow-wrap: anywhere; 228 | z-index: 1; 229 | } 230 | 231 | .steamdb_achievement h6 { 232 | font-size: 12px; 233 | font-weight: 400; 234 | color: #8b929a; 235 | } 236 | 237 | .steamdb_hide_earned_achievements .steamdb_earned_achievement { 238 | display: none; 239 | } 240 | 241 | .steamdb_achievement_checkmark { 242 | display: flex; 243 | align-items: center; 244 | justify-content: center; 245 | width: 20px; 246 | padding: 6px; 247 | flex-shrink: 0; 248 | } 249 | 250 | .steamdb_achievement_checkmark > svg { 251 | width: 20px; 252 | fill: #fff; 253 | } 254 | 255 | .steamdb_achievement_status { 256 | display: flex; 257 | flex-direction: column; 258 | gap: 5px; 259 | margin-left: auto; 260 | text-align: right; 261 | font-size: 12px; 262 | font-weight: 400; 263 | color: #8b929a; 264 | } 265 | 266 | .steamdb_achievement_status_row { 267 | display: flex; 268 | align-items: center; 269 | justify-content: end; 270 | gap: 8px; 271 | } 272 | 273 | .steamdb_achievement_status_avatar { 274 | height: 24px; 275 | } 276 | 277 | .steamdb_achievement_status_locked .steamdb_achievement_status_avatar { 278 | opacity: 0.2; 279 | } 280 | 281 | .steamdb_achievement_status_row_compare { 282 | color: #ffcc6a; 283 | } 284 | 285 | .steamdb_achievement_unlock_global { 286 | margin-left: auto; 287 | text-align: right; 288 | font-size: 14px; 289 | font-weight: 700; 290 | color: #fff; 291 | z-index: 1; 292 | } 293 | 294 | .steamdb_achievement_progress { 295 | display: flex; 296 | gap: 10px; 297 | margin-left: auto; 298 | font-size: 12px; 299 | font-weight: 400; 300 | color: #b8bcbf; 301 | text-align: right; 302 | white-space: nowrap; 303 | } 304 | 305 | .steamdb_achievement_progress_info { 306 | width: 100%; 307 | } 308 | 309 | .steamdb_achievement_progress_compare { 310 | color: #ffcc6a; 311 | } 312 | 313 | .steamdb_achievements_name_container .steamdb_achievement_progress_compare { 314 | margin-top: 10px; 315 | } 316 | 317 | .steamdb_achievements_title .steamdb_achievement_progress { 318 | text-align: left; 319 | } 320 | 321 | .steamdb_achievement_progress_avatar { 322 | height: 26px; 323 | align-self: center; 324 | } 325 | 326 | .steamdb_achievement_progressbar { 327 | border-radius: 10px; 328 | height: 8px; 329 | width: 200px; 330 | margin-top: 4px; 331 | background: #3d4450; 332 | overflow: hidden; 333 | } 334 | 335 | .achieveBar.steamdb_achievement_progressbar { 336 | width: 100%; 337 | padding: 0; 338 | border: 0; 339 | } 340 | 341 | .achieveBar.steamdb_achievement_progressbar .achieveBarProgress, 342 | .steamdb_achievement_progressbar_inner { 343 | height: 100%; 344 | border-top-right-radius: 10px; 345 | border-bottom-right-radius: 10px; 346 | background: #1a9fff; 347 | width: 0%; 348 | } 349 | 350 | .steamdb_achievement_progressbar.steamdb_achievement_progress_compare 351 | .achieveBarProgress, 352 | .steamdb_achievement_progress_compare .steamdb_achievement_progressbar_inner { 353 | background: #ffcc6a; 354 | } 355 | 356 | .steamdb_achievement_groups_disclaimer { 357 | display: flex; 358 | gap: 8px; 359 | align-items: center; 360 | justify-content: center; 361 | margin-left: 16px; 362 | margin-top: 100px; 363 | } 364 | 365 | .steamdb_achievement_groups_disclaimer > img { 366 | width: 32px; 367 | height: 32px; 368 | } 369 | 370 | .steamdb_achievement_groups_disclaimer:not(:hover) img { 371 | filter: grayscale(1) brightness(0.5); 372 | } 373 | 374 | .steamdb_achievement_groups_disclaimer > a { 375 | font-style: italic; 376 | color: inherit; 377 | } 378 | 379 | .steamdb_achievement_groups_disclaimer > a:hover { 380 | text-decoration: underline; 381 | } 382 | 383 | .steamdb_achievement_search { 384 | margin-bottom: 16px; 385 | } 386 | 387 | .steamdb_achievement_search > input { 388 | background: #1b2838; 389 | color: #fff; 390 | border: 1px solid #000; 391 | border-radius: 6px; 392 | width: 100%; 393 | font: inherit; 394 | box-sizing: border-box; 395 | padding: 7px 10px 7px 36px; 396 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E"); 397 | background-size: 16px; 398 | background-position: 10px center; 399 | background-repeat: no-repeat; 400 | } 401 | 402 | .steamdb_achievement_search > input:focus { 403 | border-color: #417b9c; 404 | outline: none; 405 | } 406 | 407 | .steamdb_achievement_search > input::-webkit-search-cancel-button { 408 | opacity: 1; 409 | } 410 | 411 | @media (max-width: 600px) { 412 | .steamdb_achievements_title { 413 | flex-flow: row wrap; 414 | } 415 | 416 | .steamdb_achievements_name_container { 417 | order: 2; 418 | flex-basis: 100%; 419 | } 420 | 421 | .steamdb_achievement_progressbar { 422 | max-width: 200px; 423 | width: 100%; 424 | } 425 | 426 | .steamdb_achievement_image_compare { 427 | display: none; 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /styles/achievements_cs2.css: -------------------------------------------------------------------------------- 1 | .steamdb_achievements_csrating { 2 | width: 100%; 3 | border-spacing: 0; 4 | border: solid 1px #2c405b; 5 | color: #c6d4df; 6 | background-color: #1b2838; 7 | font-variant-numeric: tabular-nums; 8 | } 9 | 10 | .steamdb_achievements_csrating_fold { 11 | color: #fff; 12 | cursor: pointer; 13 | width: fit-content; 14 | } 15 | 16 | .steamdb_achievements_csrating-value { 17 | font-weight: bold; 18 | } 19 | 20 | .steamdb_achievements_csrating tr:nth-child(odd) { 21 | background-color: rgb(0 0 0 / 20%); 22 | } 23 | 24 | .steamdb_achievements_csrating tr:hover { 25 | background-color: rgb(255 255 255 / 20%); 26 | } 27 | 28 | .steamdb_achievements_csrating th { 29 | background-color: #0197cf; 30 | padding-left: 10px; 31 | } 32 | 33 | .steamdb_achievements_csrating .steamdb_achievements_csrating_season { 34 | background-color: #417a9b; 35 | } 36 | 37 | .steamdb_achievements_csrating td { 38 | padding: 10px; 39 | } 40 | 41 | .steamdb_achievements_csrating_negative { 42 | color: #f44336; 43 | } 44 | 45 | .steamdb_achievements_csrating_positive { 46 | color: #0b8; 47 | } 48 | 49 | .steamdb_achievements_csrating_significant { 50 | font-weight: bold; 51 | } 52 | 53 | .steamdb_achievements_csrating_graph { 54 | display: block; 55 | height: 200px; 56 | width: 100%; 57 | } 58 | 59 | .steamdb_achievements_csrating_graph_slider { 60 | color-scheme: dark; 61 | direction: rtl; 62 | width: 100%; 63 | margin: 2px 0; 64 | } 65 | 66 | .community_tooltip.steamdb_achievements_csrating_graph_tooltip { 67 | display: none; 68 | position: absolute; 69 | white-space: pre; 70 | font-size: 14px; 71 | font-weight: 500; 72 | z-index: 9999; 73 | } 74 | -------------------------------------------------------------------------------- /styles/appicon.css: -------------------------------------------------------------------------------- 1 | .apphub_AppIcon { 2 | display: block !important; 3 | position: relative; 4 | height: 32px; 5 | width: 32px; 6 | float: left; 7 | margin-top: 4px; 8 | margin-left: 12px; 9 | margin-right: 10px; 10 | } 11 | 12 | .apphub_AppIcon > img { 13 | height: 32px; 14 | width: 32px; 15 | border-radius: 3px; /* hide the terrible jpeg rounded corner */ 16 | } 17 | 18 | .game_page_background .apphub_AppIcon { 19 | margin-left: 0; 20 | } 21 | -------------------------------------------------------------------------------- /styles/community.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Valve has these styles inlined, instead of having them in their .css 3 | */ 4 | .apphub_OtherSiteInfo .btn_medium { 5 | position: relative; 6 | z-index: 1; 7 | } 8 | 9 | /** 10 | * Fix game group logo overflowing on old group page 11 | */ 12 | #oggLogo img { 13 | height: 200px; 14 | } 15 | 16 | /** 17 | * We make it clickable 18 | */ 19 | a.apphub_NumInApp { 20 | text-decoration: underline; 21 | } 22 | 23 | a.apphub_NumInApp:hover { 24 | text-decoration: none; 25 | color: #00e676; 26 | } 27 | 28 | .btn_steamdb.btn_medium .ico16 { 29 | background: none; 30 | vertical-align: -10px; /* ugh */ 31 | } 32 | 33 | .steamdb_ogg_icon { 34 | width: 16px; 35 | height: 16px; 36 | } 37 | 38 | .steamdb_popup_icon { 39 | width: 18px; 40 | height: 18px; 41 | } 42 | 43 | .steamdb_self_profile { 44 | width: 16px; 45 | height: 16px; 46 | margin: 7px 0; 47 | vertical-align: top; 48 | } 49 | 50 | .steamdb_drops_remaining { 51 | border-radius: 2px; 52 | display: inline-block; 53 | background: rgb(0 0 0 / 30%); 54 | padding: 1px 5px; 55 | font-size: 12px; 56 | line-height: 20px; 57 | } 58 | 59 | .profile_small_header_additional.steamdb { 60 | margin-top: 40px; 61 | } 62 | 63 | .steamdb_gamecards_inventorylink { 64 | display: flex; 65 | flex-wrap: wrap; 66 | gap: 4px; 67 | align-items: center; 68 | } 69 | 70 | .steamdb_reveal_spoilers_button { 71 | cursor: pointer; 72 | align-items: center; 73 | gap: 5px; 74 | } 75 | 76 | .steamdb_reveal_spoilers_button:has(input:checked) { 77 | background: #2d6bcd; 78 | color: #fff; 79 | } 80 | 81 | .steamdb_reveal_spoilers_button > input { 82 | margin: 0; 83 | color-scheme: dark; 84 | cursor: pointer; 85 | } 86 | 87 | span.bb_spoiler.steamdb_spoiler_revealed { 88 | background-color: rgb(0 0 0 / 50%); 89 | color: #fff; 90 | } 91 | 92 | span.bb_spoiler.steamdb_spoiler_revealed > span { 93 | visibility: visible; 94 | } 95 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | .steamdb_options_link .ico16 { 2 | background: none; 3 | } 4 | 5 | .steamdb_options_link:hover .ico16 { 6 | filter: invert(1); 7 | } 8 | 9 | .steamdb_downtime { 10 | color: #fff; 11 | background-color: #c0392b; 12 | line-height: 24px; 13 | padding: 10px; 14 | font-family: 15 | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, 16 | sans-serif; 17 | font-size: 20px; 18 | text-align: center; 19 | } 20 | 21 | .steamdb_downtime a { 22 | text-decoration: underline; 23 | color: #ffee58; 24 | } 25 | 26 | .steamdb_downtime a:focus, 27 | .steamdb_downtime a:hover { 28 | color: #99fff5; 29 | } 30 | 31 | .steamdb_trade_error { 32 | margin-top: 2em; 33 | color: #ffee58; 34 | } 35 | 36 | .steamdb_trade_error_explained { 37 | font-style: italic; 38 | font-size: 90%; 39 | color: #ccc; 40 | } 41 | 42 | .steamdb_trade_error_explained:hover { 43 | text-decoration: underline; 44 | } 45 | -------------------------------------------------------------------------------- /styles/inventory-sidebar.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1450px) { 2 | /** 3 | * Make the scrollbar stable so the page doesn't shift when selecting items 4 | */ 5 | html { 6 | scrollbar-gutter: stable; 7 | } 8 | 9 | .tabitems_ctn { 10 | float: left; 11 | width: 242px; 12 | margin-left: -259px; 13 | } 14 | 15 | .games_list_separator { 16 | border: 1px solid #2c3235; 17 | margin-top: 0; 18 | padding: 8px 12px; 19 | } 20 | 21 | .games_list_separator:not(.actionable) { 22 | display: none; /* Hide "Active inventories" header */ 23 | } 24 | 25 | .games_list_tabs_ctn { 26 | border: 1px solid #2c3235; 27 | padding: 0; 28 | border-radius: 0; 29 | } 30 | 31 | .games_list_tabs { 32 | padding: 0; 33 | border-radius: 0; 34 | overflow-y: auto; 35 | max-height: min(100vh, 600px); 36 | } 37 | 38 | #tabcontent_inventory { 39 | float: right; 40 | width: 924px; 41 | margin-top: 0; 42 | } 43 | 44 | .games_list_tab, 45 | a.games_list_tab { 46 | width: 100%; 47 | float: none; 48 | display: flex; 49 | gap: 8px; 50 | align-items: center; 51 | background: unset; 52 | border: 0 !important; 53 | border-radius: 0 !important; 54 | } 55 | 56 | .games_list_tab > span, 57 | a.games_list_tab > span { 58 | padding: 0; 59 | line-height: 1; 60 | } 61 | 62 | .games_list_tab_icon { 63 | margin: 8px 0; 64 | } 65 | 66 | .games_list_tab_number { 67 | margin-left: auto; 68 | } 69 | 70 | .games_list_tab:first-child { 71 | border-top: 0; 72 | } 73 | 74 | .games_list_tab_separator, 75 | .games_list_tab_row_separator { 76 | display: none; 77 | } 78 | 79 | .games_list_tab:hover, 80 | a.games_list_tab:hover { 81 | background: #4e7297; 82 | } 83 | 84 | .games_list_tab:hover .games_list_tab_name, 85 | a.games_list_tab:hover .games_list_tab_name { 86 | text-decoration: underline; 87 | } 88 | 89 | .games_list_tab.active { 90 | box-shadow: unset; 91 | background: #2c4056; 92 | position: sticky; 93 | top: 0; 94 | bottom: 0; 95 | } 96 | 97 | #mainContents::after { 98 | content: ""; 99 | display: table; 100 | clear: both; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /styles/inventory.css: -------------------------------------------------------------------------------- 1 | .steamdb_sold > img { 2 | filter: grayscale(1) opacity(0.5); 3 | } 4 | 5 | .steamdb_confirm_listed::before, 6 | .steamdb_confirm_mobile::before, 7 | .steamdb_confirm_email::before { 8 | display: block; 9 | padding: 4px 0; 10 | background: #000; 11 | bottom: calc(50% - 18px); 12 | position: absolute; 13 | content: "CONFIRM\aON MOBILE"; 14 | color: #fff; 15 | text-align: center; 16 | font-weight: 400; 17 | font-size: 14px; 18 | line-height: 1; 19 | font-family: "Motiva Sans", Arial, Helvetica, sans-serif; 20 | z-index: 1; 21 | white-space: pre; 22 | width: 100%; 23 | } 24 | 25 | .steamdb_confirm_email::before { 26 | content: "CONFIRM\aBY EMAIL"; 27 | } 28 | 29 | .steamdb_confirm_listed::before { 30 | content: "LISTED\aON MARKET"; 31 | } 32 | 33 | .steamdb_quick_sell { 34 | display: flex; 35 | flex-wrap: wrap; 36 | gap: 10px; 37 | padding: 10px; 38 | padding-bottom: 8px; 39 | background: #161920; 40 | } 41 | 42 | .steamdb_badge_info { 43 | margin-bottom: 11px; 44 | } 45 | 46 | .steamdb_badge_info a { 47 | margin-bottom: 5px; 48 | margin-right: 5px; 49 | } 50 | 51 | /** 52 | * Fix Valve's responsive style removing width/height which breaks gems display 53 | */ 54 | .trade_item_box .item { 55 | aspect-ratio: 1 / 1; 56 | } 57 | -------------------------------------------------------------------------------- /styles/market.css: -------------------------------------------------------------------------------- 1 | .steamdb_market_retry_button { 2 | margin-top: 15px; 3 | } 4 | 5 | .steamdb_market_loader[hidden] { 6 | display: block; 7 | visibility: hidden; 8 | } 9 | 10 | .steamdb_market_loader { 11 | margin: 0 auto; 12 | width: 90px; 13 | height: 14px; 14 | 15 | --c: #fff 92%, #0000; 16 | 17 | background: 18 | radial-gradient(circle closest-side, var(--c)) calc(100% / -4) 0, 19 | radial-gradient(circle closest-side, var(--c)) calc(100% / 4) 0; 20 | background-size: calc(100% / 2) 100%; 21 | animation: steamdb-market-loader 1.5s infinite; 22 | } 23 | 24 | @keyframes steamdb-market-loader { 25 | 0% { 26 | background-position: 27 | calc(100% / -4) 0, 28 | calc(100% / 4) 0; 29 | } 30 | 31 | 50% { 32 | background-position: 33 | calc(100% / -4) -14px, 34 | calc(100% / 4) 14px; 35 | } 36 | 37 | 100% { 38 | background-position: 39 | calc(100% / 4) -14px, 40 | calc(3 * 100% / 4) 14px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /styles/store.css: -------------------------------------------------------------------------------- 1 | a.steamdb_link:hover { 2 | text-decoration: underline; 3 | } 4 | 5 | .game_area_purchase_game a.steamdb_link, 6 | html.responsive .game_area_purchase_game a.steamdb_link { 7 | color: #ebebeb !important; 8 | } 9 | 10 | .steamdb_error_link { 11 | margin-top: 25px; 12 | text-align: center; 13 | } 14 | 15 | .steamdb_agecheck_hint { 16 | margin-top: 25px; 17 | text-align: center; 18 | } 19 | 20 | .steamdb_agecheck_hint a { 21 | text-decoration: underline; 22 | } 23 | 24 | .game_area_purchase_game .steamdb_link { 25 | position: absolute; 26 | left: 0; 27 | top: 10px; 28 | } 29 | 30 | .game_area_purchase_game .steamdb_link[hidden] { 31 | visibility: hidden; 32 | pointer-events: none; 33 | } 34 | 35 | .game_purchase_sub_dropdown .steamdb_link { 36 | position: absolute; 37 | top: unset; 38 | bottom: -9px; 39 | left: 16px; 40 | } 41 | 42 | .linkbar_steamdb { 43 | display: none !important; 44 | } 45 | 46 | @media (max-width: 500px) { 47 | .game_purchase_sub_dropdown .steamdb_link { 48 | position: relative; 49 | top: unset; 50 | bottom: unset; 51 | left: unset; 52 | margin-bottom: 20px; 53 | } 54 | 55 | html.responsive .game_purchase_action .steamdb_link { 56 | position: relative; 57 | align-self: start; 58 | margin-right: auto; 59 | } 60 | 61 | html.responsive .linkbar_steamdb { 62 | display: block !important; 63 | } 64 | } 65 | 66 | #widget .steamdb_link { 67 | vertical-align: 6px; 68 | margin-left: 10px; 69 | } 70 | 71 | .btn_steamdb .ico16 { 72 | background: none; 73 | } 74 | 75 | .date:not(:hover) .steamdb_last_update_old { 76 | color: #f44336; 77 | } 78 | 79 | .steamdb_stats { 80 | font-family: "Motiva Sans", Arial, Helvetica, sans-serif; 81 | } 82 | 83 | .steamdb_stats .block_content_inner { 84 | display: flex; 85 | align-items: center; 86 | justify-content: space-between; 87 | gap: 5px; 88 | } 89 | 90 | .steamdb_stats_logo { 91 | width: 54px; 92 | height: 54px; 93 | opacity: 0.5; 94 | transition: opacity 0.1s ease-in-out; 95 | } 96 | 97 | .steamdb_stats:hover .steamdb_stats_logo { 98 | opacity: 1; 99 | } 100 | 101 | .steamdb_stats_grid { 102 | display: grid; 103 | grid-template-columns: min-content 1fr; 104 | gap: 2px 10px; 105 | white-space: nowrap; 106 | font-size: 12px; 107 | color: #8f98a0; 108 | } 109 | 110 | .steamdb_stats_number { 111 | font-weight: bold; 112 | color: #67c1f5; 113 | } 114 | 115 | .steamdb_stats p:last-child { 116 | margin-bottom: 0; 117 | } 118 | 119 | .steamdb_last_update { 120 | padding-bottom: 13px; 121 | } 122 | 123 | .steamdb_last_update .date { 124 | unicode-bidi: plaintext; 125 | } 126 | 127 | .glance_ctn .release_date + .steamdb_last_update { 128 | margin-top: -13px; 129 | } 130 | 131 | .steamdb_last_update a { 132 | display: block; 133 | } 134 | 135 | #error_display .steamdb_registerkey_lineitem { 136 | margin-top: 20px; 137 | } 138 | 139 | .steamdb_registerkey_lineitem img { 140 | width: 16px; 141 | height: 16px; 142 | margin-right: 5px; 143 | vertical-align: text-top; 144 | } 145 | 146 | .steamdb_prices { 147 | background-color: rgb(0 0 0 / 20%); 148 | border-radius: 4px; 149 | margin-bottom: 10px; 150 | padding: 8px 16px; 151 | font-family: "Motiva Sans", Arial, Helvetica, sans-serif; 152 | color: #c6d4df; 153 | display: flex; 154 | gap: 10px; 155 | } 156 | 157 | .steamdb_prices_bottom { 158 | font-style: italic; 159 | font-size: 12px; 160 | color: #8f98a0; 161 | } 162 | 163 | .steamdb_prices img { 164 | align-self: center; 165 | width: 32px; 166 | height: 32px; 167 | opacity: 0.5; 168 | transition: opacity 0.1s ease-in-out; 169 | } 170 | 171 | .steamdb_prices:hover img { 172 | opacity: 1; 173 | } 174 | 175 | .steamdb_prices:hover .steamdb_prices_top { 176 | text-decoration: underline; 177 | } 178 | 179 | .steamdb_prices:hover .steamdb_prices_bottom { 180 | color: #c6d4df; 181 | } 182 | 183 | .steamdb_prices_top b { 184 | font-weight: bold; 185 | color: #a3cf06; 186 | unicode-bidi: plaintext; 187 | } 188 | 189 | a.steamdb_rating.steamdb_rating_good { 190 | color: #6c3 !important; 191 | } 192 | 193 | a.steamdb_rating.steamdb_rating_average { 194 | color: #fc3 !important; 195 | } 196 | 197 | a.steamdb_rating.steamdb_rating_poor { 198 | color: #e60 !important; 199 | } 200 | 201 | a.steamdb_rating.steamdb_rating_white { 202 | color: #aaa !important; 203 | } 204 | 205 | a.steamdb_rating:hover { 206 | color: #fff; 207 | } 208 | 209 | .steamdb_already_in_library_link { 210 | color: inherit; 211 | } 212 | 213 | .steamdb_already_in_library_link:hover { 214 | color: #fff; 215 | text-decoration: underline; 216 | } 217 | 218 | .steamdb_already_in_library_collapse { 219 | display: flex; 220 | align-items: center; 221 | justify-content: center; 222 | position: absolute; 223 | right: 0; 224 | top: 0; 225 | height: 100%; 226 | padding: 0 10px; 227 | border: 0; 228 | background: transparent; 229 | cursor: pointer; 230 | } 231 | 232 | .steamdb_already_in_library_collapse:hover, 233 | .steamdb_already_in_library_collapse:focus { 234 | background: hsl(73deg 94% 42% / 50%); 235 | } 236 | 237 | .steamdb_already_in_library_collapse > .graph_toggle_icon { 238 | margin: 0; 239 | } 240 | 241 | .steamdb_saleitem_claim, 242 | .steamdb_cheat_queue { 243 | display: flex; 244 | align-items: center; 245 | gap: 8px 15px; 246 | } 247 | 248 | .steamdb_cheat_queue.discovery_queue_customize_ctn { 249 | margin-bottom: 0; 250 | border-bottom: 0; 251 | } 252 | 253 | .steamdb_saleitem_claim .btn_medium, 254 | .steamdb_cheat_queue .btn_medium { 255 | margin: 0; 256 | } 257 | 258 | .steamdb_cheat_queue_text { 259 | margin-right: auto; 260 | } 261 | 262 | /** 263 | * Fix for very long game names not overflowing correctly 264 | */ 265 | .apphub_AppName { 266 | word-wrap: break-word; 267 | } 268 | 269 | /** 270 | * Fix date not fitting after we insert subid links 271 | */ 272 | .account_table th { 273 | min-width: 100px; 274 | } 275 | 276 | @media (max-width: 500px) { 277 | html.responsive .steamdb_stats .block_content_inner { 278 | margin-top: 0; 279 | } 280 | 281 | .steamdb_saleitem_claim, 282 | .steamdb_cheat_queue { 283 | flex-wrap: wrap; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": false, 8 | "noEmit": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": false, 12 | "moduleResolution": "node", 13 | "moduleDetection": "force", 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "types": [ 18 | "chrome", 19 | "firefox-webext-browser" 20 | ] 21 | }, 22 | "include": [ 23 | "options/**/*", 24 | "scripts/**/*" 25 | ] 26 | } -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require( 'node:fs' ); 4 | const path = require( 'node:path' ); 5 | const child_process = require( 'node:child_process' ); 6 | const manifestPath = path.join( __dirname, 'manifest.json' ); 7 | const version = process.argv[ 2 ]; 8 | 9 | if( !/^[0-9]+\.[0-9]+$/.test( version ) ) 10 | { 11 | console.error( 'Provide version as the first argument. Example: 3.0' ); 12 | process.exit( 1 ); 13 | } 14 | 15 | fs.writeFileSync( 16 | manifestPath, 17 | fs.readFileSync( manifestPath ).toString().replace( /"version": "[0-9.]+",$/m, `"version": "${version}",` ), 18 | ); 19 | 20 | child_process.execSync( `git add manifest.json && git commit -m "Increase version to ${version}"`, { stdio: 'inherit' } ); 21 | child_process.execSync( `git tag "v${version}" -m "v${version}"`, { stdio: 'inherit' } ); 22 | child_process.execSync( 'npm run build', { stdio: 'inherit' } ); 23 | --------------------------------------------------------------------------------