Repository: Mardaneus86/futwebapp-tampermonkey Branch: master Commit: e3b7beab64ec Files: 56 Total size: 113.3 KB Directory structure: gitextract_atpnmaik/ ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── analytics/ │ ├── LICENSE │ ├── config.js │ └── index.js ├── app/ │ ├── core/ │ │ ├── analytics.js │ │ ├── base-script.js │ │ ├── browser.js │ │ ├── db.js │ │ ├── index.js │ │ ├── queue.js │ │ ├── settings-entry.js │ │ └── settings.js │ ├── futbin/ │ │ ├── futbin-player-links.js │ │ ├── futbin-prices.js │ │ ├── index.js │ │ ├── settings-entry.js │ │ └── style/ │ │ └── futbin-prices.scss │ ├── index.js │ ├── index.scss │ ├── instant-bin-confirm/ │ │ ├── index.js │ │ ├── instant-bin-confirm.js │ │ └── settings-entry.js │ ├── settings/ │ │ ├── html/ │ │ │ └── index/ │ │ │ └── settings.html │ │ ├── index.js │ │ └── index.scss │ └── transferlist/ │ ├── card-info.js │ ├── index.js │ ├── list-size.js │ ├── min-bin.js │ ├── refresh-list.js │ ├── style/ │ │ ├── card-info.scss │ │ ├── refresh-list.scss │ │ └── transfer-totals.scss │ └── transfer-totals.js ├── fut/ │ ├── club.js │ ├── errors/ │ │ └── index.js │ ├── index.js │ ├── logger.js │ ├── pinEvent.js │ ├── priceTiers.js │ ├── store.js │ ├── transferMarket.js │ └── utils.js ├── package.json ├── tampermonkey-headers.js ├── webpack.config.js └── webpack.config.prd.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true [*.js] indent_style = space indent_size = 2 ================================================ FILE: .eslintignore ================================================ dist/ tampermonkey-headers.js webpack.config.js webpack.config.prd.js ================================================ FILE: .eslintrc ================================================ { "extends": "airbnb", "parser": "babel-eslint", "rules": { "func-names": ["error", "never"], "import/prefer-default-export": "off", "no-underscore-dangle": "off" }, "globals": { "UA_TOKEN": false, "getAppMain": false, "GM_notification": false }, "env": { "greasemonkey": true } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: Mardaneus86 patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ["https://www.paypal.me/timklingeleers"] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- READ BEFORE YOU ADD AN ISSUE: Do not request autobuyer features, because they are considered as cheating. Issues regarding autobuyers will be closed immediately. ### Expected behavior: Give a detailed explanation of the expected behavior. ### Current behavior: Give a detailed explanation of the current behavior. ### Metadata: - **Script version:** - **Browser:** - **OS:** ### To Reproduce Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ### Screenshots If applicable, add screenshots to help explain your problem. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- READ BEFORE YOU ADD A FEATURE REQUEST: Do not request autobuyer features, because they are considered as cheating. Issues regarding autobuyers will be closed immediately. ### Describe the solution you'd like A clear and concise description of what you want to happen. ### Suggestions for implementation Add any other context, mockups or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ dist/ node_modules/ .vscode/ ================================================ FILE: .travis.yml ================================================ language: node_js cache: directories: - ~/.npm - ~/node_modules notifications: email: false node_js: - '8' before_script: - npm prune - git fetch --tags - export TM_VERSION=$(npm run semantic-release:dry | grep "The next release version is " | sed 's/[^0-9.]*\([0-9.]*\).*/\1/') script: - npm run build:production env: - NPM_TOKEN="00000000-0000-0000-0000-000000000000" branches: except: - /^v\d+\.\d+\.\d+$/ after_success: # CREATE GIT TAG - git config --global user.email "builds@travis-ci.com" - git config --global user.name "Travis CI" - '[[ $TRAVIS_BRANCH == "master" ]] && git tag $TM_VERSION -a -m "Generated tag from TravisCI build $TRAVIS_BUILD_NUMBER"' - '[[ $TRAVIS_BRANCH == "master" ]] && git push --quiet https://$GITHUBKEY@github.com/Mardaneus86/futwebapp-tampermonkey $GIT_TAG > /dev/null 2>&1' deploy: provider: releases api_key: secure: "itypG5lXUZkA647w7CUiagtzgr617UA640j17OFgzsChADpmilsdsEHV8afcuzts/CO+nzxbCO42X6jeBjYKEMvSSe1DInXCy9p0OcryUvTYsTM2zYvqTRn7syF5cN+2B3BTIQERk8nct+mao0p9iKDRvna9l6OI2MGLHm6nEsP/b4VmYMJwemleLY+dHeBLF1eSlEvg6sOHf8MZp6+OU78tUUUfb6+EW0+EpOomf4FG5D+XJn0Q7naMc6L4ehI8lvxcbuM7ECVgXBcw0ixnC/OS/JcpFYlNFHjTUqK/WDXWvoMkJPWff/+2SPYk0bnFJRI7LGCqF1DP0Yzn8Sz+TsNwkJ/plWNgZmq1GC1rEl3fVxHNRwQXb0Qu66BeK/5qJqsvZ1DW+96OwGPEYnUZBAqCVLK/4IHg1yDFTWsSs+sB+ECJNmRsXVF9dLluA3qtKlkujkTcrl5iNXptBx0czRwEvZmzZfEL8OS6naEIc396wEs4YT02vRsg8wp4psMW7YnetDcnVbqcNT9I0leFCSd7MKVFQLVfs5ybhXpb40Eo/+luq6N45GFMS+QazMnoyggAqtZ/g8HoCE0K+pK6YzbrboXeMui50yzMwwRWojM+XFSIKzcSOYP7AWWof66RKfOHqlBVYw8nhFsJ3xRQxzfp6528hZeJpWvRBFzHjbU=" file: - dist/fut-enhancer.user.js skip_cleanup: true ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Tim Klingeleers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # FUT Web App - TamperMonkey scripts [![Join the chat at https://gitter.im/futwebapp-tampermonkey/Lobby](https://badges.gitter.im/futwebapp-tampermonkey/Lobby.svg)](https://gitter.im/futwebapp-tampermonkey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## :warning: Discontinued **Due to personal time constraints I can no longer keep this script working at all times, as you have all noted over the past few months. So I've taken the decision to stop working on this project and create some clarity for the community.** **The code will remain available in the Github repository and on OpenUserJS. Maybe someone is willing to continue the project in a fork. If there is a fork with enough trustworthiness, I'm happy to route everyone there. Send me a message in case you want me to link to your fork.** **This project has been a great journey for me, and I'm very thankful for all the support of the community over the years.** FIFA 21's companion app for FIFA Ultimate Team, the FUT 21 Web App, is a website that let's you trade and manage your team on the go. This TamperMonkey script is meant to enhance the FUT 21 Web App experience. You can install the script following the instructions below. Afterwards you will get a settings button on the bottom right of the web app, where you can enable every feature by itself. The script provides a certain degree of customization possibilities. :warning: Using this script is at your own risk. EA might (temp-)ban you for altering parts of their Web App. :bangbang: Do not request autobuyer features. Because they are considered to be cheating, it will not be added. I started this project to learn about reverse engineering big Javascript codebases. If you benefit from this project, you can buy me a beer :beers: :+1: [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/timklingeleers) ## Features - [x] Futbin integration - [x] Show Futbin prices on all player cards throughout the app - [x] Show link to player on Futbin - [x] Mark bargains (BIN price lower then Futbin value) - [x] Find minimum BIN value of players - [x] Refresh transfer list - [x] Increase transfer list size - [x] Extra card information (contracts) - [x] Total coin value for cards on the transfer list ## Installation Make sure you have user scripts enabled in your browser (these instructions refer to the latest versions of the browser): * Firefox - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=firefox). :warning: Has issues loading properly (see issue #115) * Chrome - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=chrome). * Opera - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=opera). * Safari - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=safari). * Dolphin - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=dolphin). * UC Browser - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=ucweb). ### Install scripts Install the scripts via [OpenUserJS][install-script]. Or find the latest version and release notes at the [releases page](https://github.com/Mardaneus86/futwebapp-tampermonkey/releases). ## Feature requests If you feel there are missing features, feel free to add a request to the [issue list][issue-list]. Make sure to provide the necessary details, or even a mockup of what the feature would look like. ## Issues File a bug report in the [issue list][issue-list]. ## Developing Clone this repository and execute: ``` npm install ``` To start the bundling process and linting process, execute: ``` npm start ``` Make sure to enable `Allow access to file URLs` in `chrome://extensions/` for Tampermonkey, and add the following script snippet: ``` // ==UserScript== // @name FUT Enhancer dev // @version 0.1 // @description // @license MIT // @author Tim Klingeleers // @match https://www.easports.com/fifa/ultimate-team/web-app/* // @match https://www.easports.com/*/fifa/ultimate-team/web-app/* // @match https://www.ea.com/fifa/ultimate-team/web-app/* // @match https://www.ea.com/*/fifa/ultimate-team/web-app/* // @namespace https://github.com/Mardaneus86 // @supportURL https://github.com/Mardaneus86/futwebapp-tampermonkey/issues // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant window.focus // @require file:////dist/fut-enhancer.user.js // @connect ea.com // @connect futbin.com // ==/UserScript== ``` Remember to change the path after `@require` to the folder where you cloned the repository. It should point to the generated `fut-enhancer.user.js` in the `dist` folder. ## Contribute Add a feature request or bug to the [issue list][issue-list] before doing a PR in order to discuss it before implementing a fix. Issues that are marked with the `help wanted` have priority if you want to help. [issue-list]: https://github.com/Mardaneus86/futwebapp-tampermonkey/issues [install-script]: https://openuserjs.org/install/Mardaneus86/FUT_Enhancer.user.js ================================================ FILE: analytics/LICENSE ================================================ (The MIT License) Copyright (c) 2017 Peaks & Pies GmbH ; Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: analytics/config.js ================================================ /* eslint-disable */ module.exports = { protocolVersion: "1", hostname: "https://www.google-analytics.com", path: "/collect", batchPath: "/batch", batching: true, batchSize: 10, acceptedParameters: [ // General "v", "tid", "aip", "ds", "qt", "z", // User "cid", "uid", // Session "sc", "uip", "ua", "geoid", // Traffic Sources "dr", "cn", "cs", "cm", "ck", "cc", "ci", "gclid", "dclid", // System Info "sr", "vp", "de", "sd", "ul", "je", "fl", // Hit "t", "ni", // Content Information "dl", "dh", "dp", "dt", "cd", "linkid", // App Tracking "an", "aid", "av", "aiid", // Event Tracking "ec", "ea", "el", "ev", // E-commerce (transaction data: simple and enhanced) "ti", "ta", "tr", "ts", "tt", // E-commerce (item data: simple) "in", "ip", "iq", "ic", "iv", // E-commerce (currency: simple and enhanced) "cu", // Enhanced E-Commerce (see also: regex below) "pa", "tcc", "pal", "cos", "col", "promoa", // Social Interactions "sn", "sa", "st", // Timing "utc", "utv", "utt", "utl", "plt", "dns", "pdt", "rrt", "tcp", "srt", "dit", "clt", // Exceptions "exd", "exf", // Content Experiments "xid", "xvar"], acceptedParametersRegex: [ /^cm[0-9]+$/, /^cd[0-9]+$/, /^cg(10|[0-9])$/, /pr[0-9]{1,3}id/, /pr[0-9]{1,3}nm/, /pr[0-9]{1,3}br/, /pr[0-9]{1,3}ca/, /pr[0-9]{1,3}va/, /pr[0-9]{1,3}pr/, /pr[0-9]{1,3}qt/, /pr[0-9]{1,3}cc/, /pr[0-9]{1,3}ps/, /pr[0-9]{1,3}cd[0-9]{1,3}/, /pr[0-9]{1,3}cm[0-9]{1,3}/, /il[0-9]{1,3}nm/, /il[0-9]{1,3}pi[0-9]{1,3}id/, /il[0-9]{1,3}pi[0-9]{1,3}nm/, /il[0-9]{1,3}pi[0-9]{1,3}br/, /il[0-9]{1,3}pi[0-9]{1,3}ca/, /il[0-9]{1,3}pi[0-9]{1,3}va/, /il[0-9]{1,3}pi[0-9]{1,3}ps/, /il[0-9]{1,3}pi[0-9]{1,3}pr/, /il[0-9]{1,3}pi[0-9]{1,3}cd[0-9]{1,3}/, /il[0-9]{1,3}pi[0-9]{1,3}cm[0-9]{1,3}/, /promo[0-9]{1,3}id/, /promo[0-9]{1,3}nm/, /promo[0-9]{1,3}cr/, /promo[0-9]{1,3}ps/ ], parametersMap: { "protocolVersion": "v", "trackingId": "tid", "webPropertyId": "tid", "anonymizeIp": "aip", "dataSource": "ds", "queueTime": "qt", "cacheBuster": "z", "clientId": "cid", "userId": "uid", "sessionControl": "sc", "ipOverride": "uip", "userAgentOverride": "ua", "documentReferrer": "dr", "campaignName": "cn", "campaignSource": "cs", "campaignMedium": "cm", "campaignKeyword": "ck", "campaignContent": "cc", "campaignId": "ci", "googleAdwordsId": "gclid", "googleDisplayAdsId": "dclid", "screenResolution": "sr", "viewportSize": "vp", "documentEncoding": "de", "screenColors": "sd", "userLanguage": "ul", "javaEnabled": "je", "flashVersion": "fl", "hitType": "t", "non-interactionHit": "ni", "documentLocationUrl": "dl", "documentHostName": "dh", "documentPath": "dp", "documentTitle": "dt", "screenName": "cd", "linkId": "linkid", "applicationName": "an", "applicationId": "aid", "applicationVersion": "av", "applicationInstallerId": "aiid", "eventCategory": "ec", "eventAction": "ea", "eventLabel": "el", "eventValue": "ev", "transactionId": "ti", "transactionAffiliation": "ta", "transactionRevenue": "tr", "transactionShipping": "ts", "transactionTax": "tt", "itemName": "in", "itemPrice": "ip", "itemQuantity": "iq", "itemCode": "ic", "itemCategory": "iv", "currencyCode": "cu", "socialNetwork": "sn", "socialAction": "sa", "socialActionTarget": "st", "userTimingCategory": "utc", "userTimingVariableName": "utv", "userTimingTime": "utt", "userTimingLabel": "utl", "pageLoadTime": "plt", "dnsTime": "dns", "pageDownloadTime": "pdt", "redirectResponseTime": "rrt", "tcpConnectTime": "tcp", "serverResponseTime": "srt", "domInteractiveTime": "dit", "contentLoadTime": "clt", "exceptionDescription": "exd", "isExceptionFatal": "exf", "isExceptionFatal?": "exf", "experimentId": "xid", "experimentVariant": "xvar" } }; ================================================ FILE: analytics/index.js ================================================ /* eslint-disable */ import querystring from 'querystring'; import config from './config'; module.exports = init; function init (tid, cid, options) { return new Visitor(tid, cid, options); } var Visitor = module.exports.Visitor = function (tid, cid, options) { this._queue = []; this.options = options || {}; if(this.options.hostname) { config.hostname = this.options.hostname; } if(this.options.path) { config.path = this.options.path; } if(this.options.enableBatching !== undefined) { config.batching = options.enableBatching; } if(this.options.batchSize) { config.batchSize = this.options.batchSize; } this._context = {}; this._persistentParams = {}; this.tid = this.options.tid; this.cid = this.options.cid; if(this.options.uid) { this.uid = this.options.uid; } } Visitor.prototype = { reset: function () { this._context = null; return this; }, set: function (key, value) { this._persistentParams = this._persistentParams || {}; this._persistentParams[key] = value; }, pageview: function (path, hostname, title, params, fn) { if (typeof path === 'object' && path != null) { params = path; if (typeof hostname === 'function') { fn = hostname } path = hostname = title = null; } else if (typeof hostname === 'function') { fn = hostname hostname = title = null; } else if (typeof title === 'function') { fn = title; title = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.dp = path || params.dp || this._context.dp; params.dh = hostname || params.dh || this._context.dh; params.dt = title || params.dt || this._context.dt; this._tidyParameters(params); if (!params.dp && !params.dl) { return this._handleError('Please provide either a page path (dp) or a document location (dl)', fn); } return this._withContext(params)._enqueue('pageview', params, fn); }, screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) { if (typeof screenName === 'object' && screenName != null) { params = screenName; if (typeof appName === 'function') { fn = appName } screenName = appName = appVersion = appId = appInstallerId = null; } else if (typeof appName === 'function') { fn = appName appName = appVersion = appId = appInstallerId = null; } else if (typeof appVersion === 'function') { fn = appVersion; appVersion = appId = appInstallerId = null; } else if (typeof appId === 'function') { fn = appId; appId = appInstallerId = null; } else if (typeof appInstallerId === 'function') { fn = appInstallerId; appInstallerId = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.cd = screenName || params.cd || this._context.cd; params.an = appName || params.an || this._context.an; params.av = appVersion || params.av || this._context.av; params.aid = appId || params.aid || this._context.aid; params.aiid = appInstallerId || params.aiid || this._context.aiid; this._tidyParameters(params); if (!params.cd || !params.an) { return this._handleError('Please provide at least a screen name (cd) and an app name (an)', fn); } return this._withContext(params)._enqueue('screenview', params, fn); }, event: function (category, action, label, value, params, fn) { if (typeof category === 'object' && category != null) { params = category; if (typeof action === 'function') { fn = action } category = action = label = value = null; } else if (typeof label === 'function') { fn = label; label = value = null; } else if (typeof value === 'function') { fn = value; value = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.ec = category || params.ec || this._context.ec; params.ea = action || params.ea || this._context.ea; params.el = label || params.el || this._context.el; params.ev = value || params.ev || this._context.ev; params.p = params.p || params.dp || this._context.p || this._context.dp; delete params.dp; this._tidyParameters(params); if (!params.ec || !params.ea) { return this._handleError('Please provide at least an event category (ec) and an event action (ea)', fn); } return this._withContext(params)._enqueue('event', params, fn); }, transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) { if (typeof transaction === 'object') { params = transaction; if (typeof revenue === 'function') { fn = revenue } transaction = revenue = shipping = tax = affiliation = null; } else if (typeof revenue === 'function') { fn = revenue; revenue = shipping = tax = affiliation = null; } else if (typeof shipping === 'function') { fn = shipping; shipping = tax = affiliation = null; } else if (typeof tax === 'function') { fn = tax; tax = affiliation = null; } else if (typeof affiliation === 'function') { fn = affiliation; affiliation = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.ti = transaction || params.ti || this._context.ti; params.tr = revenue || params.tr || this._context.tr; params.ts = shipping || params.ts || this._context.ts; params.tt = tax || params.tt || this._context.tt; params.ta = affiliation || params.ta || this._context.ta; params.p = params.p || this._context.p || this._context.dp; this._tidyParameters(params); if (!params.ti) { return this._handleError('Please provide at least a transaction ID (ti)', fn); } return this._withContext(params)._enqueue('transaction', params, fn); }, item: function (price, quantity, sku, name, variation, params, fn) { if (typeof price === 'object') { params = price; if (typeof quantity === 'function') { fn = quantity } price = quantity = sku = name = variation = null; } else if (typeof quantity === 'function') { fn = quantity; quantity = sku = name = variation = null; } else if (typeof sku === 'function') { fn = sku; sku = name = variation = null; } else if (typeof name === 'function') { fn = name; name = variation = null; } else if (typeof variation === 'function') { fn = variation; variation = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.ip = price || params.ip || this._context.ip; params.iq = quantity || params.iq || this._context.iq; params.ic = sku || params.ic || this._context.ic; params.in = name || params.in || this._context.in; params.iv = variation || params.iv || this._context.iv; params.p = params.p || this._context.p || this._context.dp; params.ti = params.ti || this._context.ti; this._tidyParameters(params); if (!params.ti) { return this._handleError('Please provide at least an item transaction ID (ti)', fn); } return this._withContext(params)._enqueue('item', params, fn); }, exception: function (description, fatal, params, fn) { if (typeof description === 'object') { params = description; if (typeof fatal === 'function') { fn = fatal; } description = fatal = null; } else if (typeof fatal === 'function') { fn = fatal; fatal = 0; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.exd = description || params.exd || this._context.exd; params.exf = +!!(fatal || params.exf || this._context.exf); if (params.exf === 0) { delete params.exf; } this._tidyParameters(params); return this._withContext(params)._enqueue('exception', params, fn); }, timing: function (category, variable, time, label, params, fn) { if (typeof category === 'object') { params = category; if (typeof variable === 'function') { fn = variable; } category = variable = time = label = null; } else if (typeof variable === 'function') { fn = variable; variable = time = label = null; } else if (typeof time === 'function') { fn = time; time = label = null; } else if (typeof label === 'function') { fn = label; label = null; } else if (typeof params === 'function') { fn = params; params = null; } params = this._translateParams(params); params = Object.assign({}, this._persistentParams || {}, params); params.utc = category || params.utc || this._context.utc; params.utv = variable || params.utv || this._context.utv; params.utt = time || params.utt || this._context.utt; params.utl = label || params.utl || this._context.utl; this._tidyParameters(params); return this._withContext(params)._enqueue('timing', params, fn); }, send: function (fn) { var self = this; var count = 1; var fn = fn || function () {}; var getBody = function(params) { return params.map(function(x) { return querystring.stringify(x); }).join('\n'); } var onFinish = function (err) { fn.call(self, err || null, count - 1); } var iterator = function () { if (!self._queue.length) { return onFinish(null); } var params = []; if(config.batching) { params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize)); } else { params.push(self._queue.shift()); } var useBatchPath = params.length > 1; var path = config.hostname + (useBatchPath ? config.batchPath :config.path); var options = Object.assign({}, self.options.requestOptions, { body: getBody(params), headers: self.options.headers || {} }); GM_xmlhttpRequest({ method: 'POST', url: path, headers: options.headers, data: options.body, onload: function () { nextIteration() }, onerror: function(res) { nextIteration(res.status); } }); } function nextIteration(err) { if (err) return onFinish(err); iterator(); } iterator(); }, _enqueue: function (type, params, fn) { if (typeof params === 'function') { fn = params; params = {}; } params = this._translateParams(params) || {}; Object.assign(params, { v: config.protocolVersion, tid: this.tid, cid: this.cid, t: type }); if(this.uid) { params.uid = this.uid; } this._queue.push(params); if (fn) { this.send(fn); } return this; }, _handleError: function (message, fn) { fn && fn.call(this, new Error(message)) return this; }, _translateParams: function (params) { var translated = {}; for (var key in params) { if (config.parametersMap.hasOwnProperty(key)) { translated[config.parametersMap[key]] = params[key]; } else { translated[key] = params[key]; } } return translated; }, _tidyParameters: function (params) { for (var param in params) { if (params[param] === null || params[param] === undefined) { delete params[param]; } } return params; }, _withContext: function (context) { var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams); visitor._queue = this._queue; return visitor; } } Visitor.prototype.pv = Visitor.prototype.pageview Visitor.prototype.e = Visitor.prototype.event Visitor.prototype.t = Visitor.prototype.transaction Visitor.prototype.i = Visitor.prototype.item ================================================ FILE: app/core/analytics.js ================================================ import ua from '../../analytics'; import { Database } from './db'; class Analytics { constructor() { if (this.ua === undefined) { let id = Database.get('uuid', ''); if (id === '') { id = this._uuidv4(); Database.set('uuid', id); } this.ua = ua(null, null, { tid: UA_TOKEN, cid: id, uid: id, }); } } /* eslint-disable */ _uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /* eslint-enable */ trackPage(pageId) { return new Promise((resolve, reject) => { this.ua.pageview(pageId, (err) => { if (err) { reject(err); } else { resolve(); } }); }); } trackEvent(category, action, label = null, value = null) { return new Promise((resolve, reject) => { this.ua.event(category, action, label, value, (err) => { if (err) { reject(err); } else { resolve(); } }); }); } } export default new Analytics(); ================================================ FILE: app/core/base-script.js ================================================ /* global window */ /* eslint class-methods-use-this: "off" */ import { Settings } from './settings'; import { Database } from './db'; export class BaseScript { constructor(id) { this._id = id; Settings.getInstance().on('entry-enabled', (entry) => { if (entry.id === id) { this.screenRequestObserver = window.onPageNavigation.observe( this, function (obs, event) { setTimeout(() => { this.onScreenRequest(event); }, 1000); }, ); this.activate({ screenId: window.currentPage, }); } }); Settings.getInstance().on('entry-disabled', (entry) => { if (entry.id === id) { this.screenRequestObserver.unobserve(this); this.deactivate({ screenId: window.currentPage, }); } }); } activate() { // override in subclasses } deactivate() { // override in subclasses } onScreenRequest() { // override in subclasses } getSettings() { return Database.getJson(`settings:${this._id}`, {}); } } ================================================ FILE: app/core/browser.js ================================================ /* globals window document Blob */ export default { downloadFile(filename, data) { const blob = new Blob([data], { type: 'text/csv' }); if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(blob, filename); } else { const elem = window.document.createElement('a'); elem.href = window.URL.createObjectURL(blob); elem.download = filename; document.body.appendChild(elem); elem.click(); document.body.removeChild(elem); } }, }; ================================================ FILE: app/core/db.js ================================================ /* eslint valid-typeof: "error" */ export class Database { constructor() { this.set('database-version', '1'); } static set(key, value) { GM_setValue(key, value); } static setJson(key, value) { this.set(key, JSON.stringify(value)); } static get(key, defaultValue) { let value = defaultValue; if (typeof value === 'object') { value = JSON.stringify(value); } return GM_getValue(key, value); } static getJson(key, defaultValue) { return JSON.parse(this.get(key, defaultValue)); } } ================================================ FILE: app/core/index.js ================================================ import { Settings } from './settings'; import { SettingsEntry } from './settings-entry'; import { BaseScript } from './base-script'; import { Database } from './db'; import { Queue } from './queue'; import browser from './browser'; import analytics from './analytics'; export { BaseScript, Database, Queue, Settings, SettingsEntry, browser, analytics, }; ================================================ FILE: app/core/queue.js ================================================ import { utils } from '../../fut'; export class Queue { constructor() { this._queue = []; } static getInstance() { if (this._instance == null) { this._instance = new Queue(); } return this._instance; } add(identifier, cb) { this._queue.push({ identifier, cb, }); } async start() { this._running = true; /* eslint-disable no-await-in-loop */ while (this._running) { if (this._queue.length > 0) { const scriptToRun = this._queue.shift(); if (scriptToRun) { await scriptToRun.cb(); } } else { await utils.sleep(1000); } } /* eslint-enable no-await-in-loop */ } stop() { this._running = false; } } ================================================ FILE: app/core/settings-entry.js ================================================ import { Database } from './db'; export class SettingsEntry { constructor(id, name) { const settings = Database.getJson(`settings:${id}`, {}); this.id = id; this.name = name; this.isActive = settings.isActive ? settings.isActive : false; this.settings = []; } toggle() { this.isActive = !this.isActive; const settings = Database.getJson(`settings:${this.id}`, {}); settings.isActive = this.isActive; Database.setJson(`settings:${this.id}`, settings); } addSetting(label, key, defaultValue, type, cb) { const settings = Database.getJson(`settings:${this.id}`, {}); settings[key] = key in settings ? settings[key] : defaultValue; Database.setJson(`settings:${this.id}`, settings); this.settings.push({ label, key, type, value: key in settings ? settings[key] : defaultValue, callback: cb, subsettings: [], }); } addSettingUnder(underKey, label, key, defaultValue, type, cb) { const settings = Database.getJson(`settings:${this.id}`, {}); settings[key] = key in settings ? settings[key] : defaultValue; Database.setJson(`settings:${this.id}`, settings); const setting = this.settings.find(s => s.key === underKey); setting.subsettings.push({ label, key, type, value: key in settings ? settings[key] : defaultValue, callback: cb, }); } changeValue(key, value) { const settings = Database.getJson(`settings:${this.id}`, {}); settings[key] = value; Database.setJson(`settings:${this.id}`, settings); } } ================================================ FILE: app/core/settings.js ================================================ import EventEmitter from 'event-emitter-es6'; import analytics from './analytics'; export class Settings extends EventEmitter { constructor() { super(); this._entries = []; } static getInstance() { if (this._instance == null) { this._instance = new Settings(); } return this._instance; } /** * * @param {SettingsEntry} entry The entry for the settings */ registerEntry(entry) { this._entries.push(entry); if (entry.isActive) { this._emitEvent(entry); } } getEntries() { return this._entries; } toggleEntry(id) { const entries = this._entries.filter(e => e.id === id); if (!entries || entries.length === 0) { return; } entries[0].toggle(); analytics.trackEvent('Settings', `Toggle setting ${id}`, entries[0].isActive); this._emitEvent(entries[0]); } _emitEvent(entry) { if (entry.isActive) { this.emit('entry-enabled', entry); } else { this.emit('entry-disabled', entry); } } } ================================================ FILE: app/futbin/futbin-player-links.js ================================================ /* globals window $ document */ import { analytics, BaseScript, Database } from '../core'; import { FutbinSettings } from './settings-entry'; export class FutbinPlayerLinks extends BaseScript { constructor() { super(FutbinSettings.id); const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; this._observer = new MutationObserver(this._mutationHandler.bind(this)); this._playerPrices = []; } activate(state) { super.activate(state); const obsConfig = { childList: true, characterData: true, attributes: false, subtree: true, }; setTimeout(() => { this._observer.observe($(document)[0], obsConfig); }, 0); } deactivate(state) { super.deactivate(state); $('#futbinPlayerLink').remove(); this._observer.disconnect(); } _mutationHandler(mutationRecords) { mutationRecords.forEach(function (mutation) { if ($(mutation.target).hasClass('DetailView') && $(mutation.target) .find('.DetailPanel') && mutation.addedNodes.length > 0) { if (this.getSettings()['show-link-to-player'].toString() !== 'true') { return; } let selectedItem = this._getSelectedItem(); if (selectedItem == null || selectedItem.resourceId === 0) { return; } const futbinPlayerLink = $(mutation.target).find('#futbinPlayerLink'); futbinPlayerLink.remove(); $(mutation.target).find('.DetailPanel > .ut-button-group').prepend(``); $('#futbinPlayerLink').bind('click', async () => { let btn = $('#futbinPlayerLink'); btn.find('.btn-text').html('Searching on Futbin ...'); const futbinLink = await FutbinPlayerLinks._getFutbinPlayerUrl(selectedItem); selectedItem = this._getSelectedItem(); btn = $('#futbinPlayerLink'); if (btn.data('resource-id') === selectedItem.resourceId) { if (futbinLink) { btn.find('.btn-text').html('View on Futbin'); analytics.trackEvent('Futbin', 'Show player on Futbin', btn.data('resource-id')); window.open(futbinLink); } else { btn.find('.btn-text').html('No exact Futbin player found'); } } }); } }, this); } static _getFutbinPlayerUrl(item) { return new Promise((resolve) => { if (!item._staticData) { return resolve(null); } let futbinPlayerIds = Database.getJson('futbin-player-ids', []); const futbinPlayer = futbinPlayerIds.find(i => i.id === item.resourceId); if (futbinPlayer != null) { return resolve(`https://www.futbin.com/21/player/${futbinPlayer.futbinId}`); } const name = `${item._staticData.firstName} ${item._staticData.lastName}`.replace(' ', '+'); const url = `https://www.futbin.com/search?year=21&term=${name}`; return GM_xmlhttpRequest({ method: 'GET', url, onload: (res) => { if (res.status !== 200) { return resolve(null); } const players = JSON.parse(res.response); let exactPlayers = players.filter(p => parseInt(p.rating, 10) === parseInt(item.rating, 10)); if (exactPlayers.length > 1) { exactPlayers = exactPlayers.filter(p => p.rare_type === item.rareflag.toString() && p.club_image.endsWith(`/${item.teamId}.png`)); } if (exactPlayers.length === 1) { futbinPlayerIds = Database.getJson('futbin-player-ids', []); if (futbinPlayerIds.find(i => i.id === item.resourceId) == null) { futbinPlayerIds.push({ id: item.resourceId, futbinId: exactPlayers[0].id, }); } Database.setJson('futbin-player-ids', futbinPlayerIds); return resolve(`https://www.futbin.com/21/player/${exactPlayers[0].id}`); } else if (exactPlayers.length > 1) { // Take first one, several players are returned more than once return resolve(`https://www.futbin.com/21/player/${exactPlayers[0].id}`); } return resolve(null); // TODO: what should we do if we find more than one? }, }); }); } /* eslint-disable class-methods-use-this */ _getSelectedItem() { const listController = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController()._listController; if (listController) { return listController.getIterator().current(); } const currentController = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController()._rightController._currentController; if (currentController && currentController._viewmodel) { const current = currentController._viewmodel.current(); return current._item ? current._item : current; } return null; } /* eslint-enable class-methods-use-this */ } new FutbinPlayerLinks(); // eslint-disable-line no-new ================================================ FILE: app/futbin/futbin-prices.js ================================================ /* globals $ window */ import { utils } from '../../fut'; import { BaseScript, Database } from '../core'; import { FutbinSettings } from './settings-entry'; export class FutbinPrices extends BaseScript { constructor() { super(FutbinSettings.id); this._squadObserver = null; } activate(state) { super.activate(state); this._show(state.screenId); } onScreenRequest(screenId) { super.onScreenRequest(screenId); const controllerName = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController().className; if (screenId === 'SBCSquadSplitViewController' || screenId === 'SquadSplitViewController' || screenId === 'UTSquadSplitViewController' || screenId === 'UTSBCSquadSplitViewController') { if (this.getSettings()['show-sbc-squad'].toString() !== 'true') { return; } this._squadObserver = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController()._leftController._squad.onDataUpdated .observe(this, () => { $('.squadSlotPedestal.futbin').remove(); // forces update this._show('SBCSquadSplitViewController', true); }); if ($('.ut-squad-summary-info--right.ut-squad-summary-info').find('.futbin').length === 0) { $('.ut-squad-summary-info--right.ut-squad-summary-info').append(`
Total BIN value
---
`); $('.refresh-squad-button').click(() => { Database.set('lastFutbinFetchFail', 0); this.onScreenRequest(screenId); }); } } else if (this._squadObserver !== null && controllerName !== 'SBCSquadSplitViewController' && controllerName !== 'SquadSplitViewController' && controllerName !== 'UTSquadSplitViewController' && controllerName !== 'UTSBCSquadSplitViewController') { this._squadObserver.unobserve(this); } this._show(screenId); } deactivate(state) { super.deactivate(state); $('.futbin').remove(); if (this._squadObserver !== null) { this._squadObserver.unobserve(this); } if (this._intervalRunning) { clearInterval(this._intervalRunning); } } _show(screen, force = false) { const showFutbinPricePages = [ // Players 'UTTransferListSplitViewController', // transfer list 'UTWatchListSplitViewController', // transfer targets 'UTUnassignedItemsSplitViewController', // pack buy 'ClubSearchResultsSplitViewController', // club 'UTMarketSearchResultsSplitViewController', // market search 'UTPlayerPicksViewController', // Squad 'SBCSquadSplitViewController', 'SquadSplitViewController', 'UTSquadSplitViewController', 'UTSBCSquadSplitViewController', 'UTTOTWSquadSplitViewController', ]; if (showFutbinPricePages.indexOf(screen) !== -1) { if (this._intervalRunning) { clearInterval(this._intervalRunning); } if (screen === 'SBCSquadSplitViewController' || screen === 'SquadSplitViewController' || screen === 'UTSquadSplitViewController' || screen === 'UTSquadsHubViewController' || screen === 'UTSBCSquadSplitViewController' || screen === 'UTTOTWSquadSplitViewController') { this.loadFutbinPrices(showFutbinPricePages, screen, force); } else { this._intervalRunning = setInterval(() => { this.loadFutbinPrices(showFutbinPricePages, screen, force); }, 1000); } } else { // no need to search prices on other pages // reset page if (this._intervalRunning) { clearInterval(this._intervalRunning); } this._intervalRunning = null; } } loadFutbinPrices(showFutbinPricePages, screen, force) { const lastFutbinFetchFail = Database.get('lastFutbinFetchFail', 0); if (lastFutbinFetchFail + (5 * 60000) > Date.now()) { console.log(`Futbin fetching has been paused for 5 minutes because of failed requests earlier (retrying after ${new Date(lastFutbinFetchFail + (5 * 60000)).toLocaleTimeString()}). Check on Github for known issues.`); // eslint-disable-line no-console if (this._intervalRunning) { clearInterval(this._intervalRunning); } return; } if (showFutbinPricePages.indexOf(window.currentPage) === -1 && !force) { if (this._intervalRunning) { clearInterval(this._intervalRunning); } return; } const controller = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController(); let uiItems = null; if (screen === 'SBCSquadSplitViewController' || screen === 'SquadSplitViewController' || screen === 'UTSquadSplitViewController' || screen === 'UTSquadsHubViewController' || screen === 'UTSBCSquadSplitViewController' || screen === 'UTTOTWSquadSplitViewController') { uiItems = $(controller._view.__root).find('.squadSlot'); if (this.getSettings()['show-sbc-squad'].toString() !== 'true') { return; } } else { uiItems = $(getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() ._view.__root).find('.listFUTItem'); } if ($(uiItems[0]).find('.futbin').length > 0) { return; } let listController = null; if (screen === 'SBCSquadSplitViewController' || screen === 'SquadSplitViewController' || screen === 'UTSquadSplitViewController' || screen === 'UTSBCSquadSplitViewController' || screen === 'UTTOTWSquadSplitViewController') { // not needed } else if (screen === 'UTPlayerPicksViewController') { if (!controller.getPresentedViewController()) { return; } if ($(controller.getPresentedViewController()._view.__root).find('.futbin').length > 0) { // Futbin prices already shown return; } listController = controller.getPresentedViewController(); } else if (screen === 'UTUnassignedItemsSplitViewController' || screen === 'UTWatchListSplitViewController') { if (!controller || !controller._leftController || !controller._leftController._view) { return; } listController = controller._leftController; } else { if (!controller || !controller._listController || !controller._listController._view) { return; // only run if data is available } listController = controller._listController; } let listrows = null; if (screen === 'SBCSquadSplitViewController' || screen === 'SquadSplitViewController' || screen === 'UTSquadSplitViewController' || screen === 'UTSBCSquadSplitViewController' || screen === 'UTTOTWSquadSplitViewController') { listrows = controller._squad._players.slice(0, 11).map((p, index) => ( { data: p._item, target: controller._view._lView._slotViews[index].__root, })); } else if (listController._picks && screen === 'UTPlayerPicksViewController') { listrows = listController._picks.map((pick, index) => ( { data: pick, target: listController._view._playerPickViews[index].__root, })); } else if (listController._view._list && listController._view._list.listRows && listController._view._list.listRows.length > 0) { listrows = listController._view._list.listRows; // for transfer market and club search } else if (listController._view._sections && listController._view._sections.length > 0) { // for transfer list & trade pile listController._view._sections.forEach((row) => { if (row.listRows.length > 0) { if (listrows == null) { listrows = row.listRows; } else { listrows = listrows.concat(row.listRows); } } }); } if (listrows === null) { return; } const showBargains = (this.getSettings()['show-bargains'].toString() === 'true'); const resourceIdMapping = []; listrows .filter(row => row.data.type === 'player' && row.data.resourceId !== 0) .forEach((row, index) => { $(row.__auction).show(); resourceIdMapping.push({ target: uiItems[index] || row.target, playerId: row.data.resourceId, item: row.data, }); }); let fetchedPlayers = 0; const fetchAtOnce = 30; const futbinlist = []; while (resourceIdMapping.length > 0 && fetchedPlayers < resourceIdMapping.length && Database.get('lastFutbinFetchFail', 0) + (5 * 60000) < Date.now()) { const futbinUrl = `https://www.futbin.com/21/playerPrices?player=&rids=${ resourceIdMapping.slice(fetchedPlayers, fetchedPlayers + fetchAtOnce) .map(i => i.playerId) .filter((current, next) => current !== next && current !== 0) .join(',') }`; fetchedPlayers += fetchAtOnce; /* eslint-disable no-loop-func */ GM_xmlhttpRequest({ method: 'GET', url: futbinUrl, onload: (res) => { if (res.status !== 200) { Database.set('lastFutbinFetchFail', Date.now()); GM_notification(`Could not load Futbin prices (code ${res.status}), pausing fetches for 5 minutes. Disable Futbin integration if the problem persists.`, 'Futbin fetch failed'); return; } const futbinData = JSON.parse(res.response); resourceIdMapping.forEach((item) => { FutbinPrices._showFutbinPrice(screen, item, futbinData, showBargains); futbinlist.push(futbinData[item.playerId]); }); const platform = utils.getPlatform(); if (screen === 'SBCSquadSplitViewController' || screen === 'SquadSplitViewController' || screen === 'UTSquadSplitViewController' || screen === 'UTSBCSquadSplitViewController') { const futbinTotal = futbinlist.reduce( (sum, item) => sum + parseInt( item.prices[platform].LCPrice.toString().replace(/[,.]/g, ''), 10, ) || 0 , 0, ); $('.ut-squad-summary-value.coins.value').html(`${futbinTotal.toLocaleString()}`); } }, }); } } static async _showFutbinPrice(screen, item, futbinData, showBargain) { if (!futbinData) { return; } const target = $(item.target); const { playerId } = item; if (target.find('.player').length === 0) { // not a player return; } const platform = utils.getPlatform(); if (!futbinData[playerId]) { return; // futbin data might not be available for this player } let targetForButton = null; if (target.find('.futbin').length > 0) { return; // futbin price already added to the row } const futbinText = 'Futbin BIN'; switch (screen) { case 'SBCSquadSplitViewController': case 'SquadSplitViewController': case 'UTSquadSplitViewController': case 'UTSBCSquadSplitViewController': case 'UTTOTWSquadSplitViewController': target.prepend(`
${futbinData[playerId].prices[platform].LCPrice || '---'}
`); break; case 'UTPlayerPicksViewController': target.prepend(`
${futbinText} ${futbinData[playerId].prices[platform].LCPrice || '---'}
`); break; case 'UTTransferListSplitViewController': case 'UTWatchListSplitViewController': case 'UTUnassignedItemsSplitViewController': case 'ClubSearchResultsSplitViewController': case 'UTMarketSearchResultsSplitViewController': $('.secondary.player-stats-data-component').css('float', 'left'); target.find('.auction').prepend(`
${futbinText} ${futbinData[playerId].prices[platform].LCPrice || '---'}
`); break; case 'SearchResults': targetForButton = target.find('.auctionValue').parent(); targetForButton.prepend(`
${futbinText} ${futbinData[playerId].prices[platform].LCPrice || '---'}
`); break; default: // no need to do anything } if (showBargain) { if (item.item._auction && item.item._auction.buyNowPrice < futbinData[playerId].prices[platform].LCPrice.toString().replace(/[,.]/g, '')) { target.addClass('futbin-bargain'); } } } } ================================================ FILE: app/futbin/index.js ================================================ import './style/futbin-prices.scss'; import { FutbinPrices } from './futbin-prices'; import { FutbinPlayerLinks } from './futbin-player-links'; import { FutbinSettings } from './settings-entry'; export { FutbinSettings, }; new FutbinPrices(); // eslint-disable-line no-new new FutbinPlayerLinks(); // eslint-disable-line no-new ================================================ FILE: app/futbin/settings-entry.js ================================================ import { SettingsEntry } from '../core'; export class FutbinSettings extends SettingsEntry { static id = 'futbin'; constructor() { super('futbin', 'FutBIN integration'); this.addSetting('Show link to player page', 'show-link-to-player', false, 'checkbox'); this.addSetting('Show prices on SBC and Squad', 'show-sbc-squad', false, 'checkbox'); this.addSetting('Mark bargains', 'show-bargains', false, 'checkbox'); } } ================================================ FILE: app/futbin/style/futbin-prices.scss ================================================ #TradePile .player-stats-data-component, #Unassigned .player-stats-data-component { width: 12em; } #TradePile .listFUTItem .entityContainer, #Unassigned .listFUTItem .entityContainer { width: 45%; } #Unassigned .listFUTItem .auction .auctionValue, #Unassigned .listFUTItem .auction .auction-state { display: none; } #Unassigned .listFUTItem .auction .auctionValue.futbin { display: block; float: right; } .MyClubResults .listFUTItem .auction { display: block; position: absolute; right: 0; } .MyClubResults .listFUTItem .auction .auctionValue, .MyClubResults .listFUTItem .auction .auction-state { width: 24%; float: right; padding-right: 1%; display: none; } .MyClubResults .listFUTItem .auction .auctionValue.futbin { display: block; } .listFUTItem .auction>.auction-state, .listFUTItem .auction>.auctionStartPrice, .listFUTItem .auction>.auctionValue { flex: 1 1 20%; overflow: hidden; } .listFUTItem .auction { top: 30%; max-width: none; width: 50%; .futbin .coins.value .time { display: inline; font-size: 1em; } } @media (max-width: 1130px) { .listFUTItem .auction { width: auto; } html[dir=ltr] .listFUTItem .auction { left: auto; } } .ut-navigation-container-view.ui-layout-right .listFUTItem .auction { top: 30%; } .futbinupdate { font-size: 14px; clear: both; display: block; } .coins.value.futbin { -webkit-filter: hue-rotate(165deg); filter: hue-rotate(165deg); } .listFUTItem.has-auction-data.futbin-bargain .rowContent { background-color: #7ffe9445; } .listFUTItem.has-auction-data.selected.futbin-bargain .rowContent, .listFUTItem.has-auction-data.selected.futbin-bargain .rowContent.active { background-color: #7ffe94; color: #434853; } .ut-club-search-results-view { .listFUTItem .auction { width: 10%; } .auction-state, .auctionValue { display: none; &.futbin { display: block; } } } .player-picks-modal .time { display: block; } .ut-squad-slot-pedestal-view.futbin { min-width: 58px; flex: none; width: auto; bottom: -2.6em; white-space: nowrap; .coins.value { text-align: center; margin: 0 8px; } } .ut-squad-overview .ut-squad-summary { width: 70%; } .refresh-squad-button { margin: 17px 5px; color:#e2dde2; &:before { font-family: UltimateTeam-Icons,sans-serif; content: '\E051'; } } ================================================ FILE: app/index.js ================================================ /* globals onVisibilityChanged services UTGameFlowNavigationController UTViewController EAObservable window document $ */ import 'babel-polyfill'; import './index.scss'; import initSettingsScreen from './settings'; import { analytics, Settings, Queue } from './core'; import { Logger } from '../fut'; /* RemoveSoldAuctionsSettings, RelistAuctionsSettings, */ import { RefreshListSettings, CardInfoSettings, ListSizeSettings, MinBinSettings, TransferTotalsSettings, } from './transferlist'; import { FutbinSettings, } from './futbin'; import { InstantBinConfirmSettings, } from './instant-bin-confirm'; /* import { ClubInfoSettings, } from './club'; */ window.onPageNavigation = new EAObservable(); window.currentPage = ''; UTGameFlowNavigationController.prototype.didPush = (t) => { if (t) { analytics.trackPage(t.className); window.onPageNavigation.notify(t.className); window.currentPage = t.className; } }; UTViewController.prototype.didPresent = (t) => { if (t) { analytics.trackPage(t.className); window.onPageNavigation.notify(t.className); window.currentPage = t.className; } }; setTimeout(() => { services.Authentication.oAuthentication.observe( this, () => { // reset the logs at startup new Logger().reset(); // force full web app layout in any case $('body').removeClass('phone').addClass('landscape'); Queue.getInstance().start(); // get rid of pinEvents when switching tabs document.removeEventListener('visibilitychange', onVisibilityChanged); const settings = Settings.getInstance(); settings.registerEntry(new RefreshListSettings()); settings.registerEntry(new MinBinSettings()); settings.registerEntry(new CardInfoSettings()); settings.registerEntry(new ListSizeSettings()); settings.registerEntry(new TransferTotalsSettings()); settings.registerEntry(new FutbinSettings()); settings.registerEntry(new InstantBinConfirmSettings()); initSettingsScreen(settings); }, ); }, 1000); ================================================ FILE: app/index.scss ================================================ .ut-content-container { padding: 0; .ut-content { border: 0; &.ut-content--split-view-extend { max-height: 100%; } } } .listFUTItem .entityContainer .name.untradeable { display: block; &::before { position: relative; padding-right: 10px; } } .ut-transfer-list-view .listFUTItem .entityContainer, .ut-club-search-results-view.ui-layout-left .listFUTItem .entityContainer, .ut-unassigned-view.ui-layout-left .listFUTItem .entityContainer { width: 45%; } @media (min-width: 1281px) { .ut-content-container .ut-content { max-width: 100%; max-height: 100%; } .ut-split-view .ut-content { max-width: 100%; max-height: 100%; } } ================================================ FILE: app/instant-bin-confirm/index.js ================================================ import { InstantBinConfirmSettings } from './settings-entry'; import { InstantBinConfirm } from './instant-bin-confirm'; export { InstantBinConfirmSettings, }; new InstantBinConfirm(); // eslint-disable-line no-new ================================================ FILE: app/instant-bin-confirm/instant-bin-confirm.js ================================================ /* global gPopupClickShield enums EADialogViewController services utils */ import { BaseScript } from '../core'; import { InstantBinConfirmSettings } from './settings-entry'; export class InstantBinConfirm extends BaseScript { unmodifiedConfirmation = utils.PopupManager.ShowConfirmation; constructor() { super(InstantBinConfirmSettings.id); } activate(state) { super.activate(state); } onScreenRequest(screenId) { super.onScreenRequest(screenId); const settings = this.getSettings(); utils.PopupManager.ShowConfirmation = (dialog, amount, proceed, s) => { let cancel = s; if (!utils.JS.isFunction(s)) { cancel = function () { }; } if (settings.isActive && dialog.title === utils.PopupManager.Confirmations.CONFIRM_BUY_NOW.title) { proceed(); return; } const n = new EADialogViewController({ dialogOptions: [dialog.buttonLabels[0], dialog.buttonLabels[1]], message: services.Localization.localize(dialog.message, amount), title: services.Localization.localize(dialog.title), }); n.init(); gPopupClickShield.setActivePopup(n); n.onExit.observe(this, (e, t) => { if (t !== enums.UIDialogOptions.CANCEL && t !== enums.UIDialogOptions.NO) { if (proceed) { proceed(); } else if (cancel) { cancel(); } } else { cancel(); } }); }; } deactivate(state) { super.deactivate(state); utils.PopupManager.ShowConfirmation = this.unmodifiedConfirmation; } } ================================================ FILE: app/instant-bin-confirm/settings-entry.js ================================================ import { SettingsEntry } from '../core'; export class InstantBinConfirmSettings extends SettingsEntry { static id = 'instant-bin-confirm'; constructor() { super('instant-bin-confirm', 'Instantly confirm Buy It Now dialog'); } } ================================================ FILE: app/settings/html/index/settings.html ================================================

FUT Tampermonkey settings

Need help?

Talk to us in the Gitter channel or report errors in the Github repository.


Enjoying this plugin?

Consider a donation so this plugin can keep being improved.

================================================ FILE: app/settings/index.js ================================================ /* globals $ */ /* eslint-disable no-restricted-syntax */ import './index.scss'; import { analytics } from '../core'; import settingsPage from './html/index/settings.html'; const handleFieldChange = (entry, setting, e) => { if (setting.subsettings && setting.subsettings.length > 0) { entry.changeValue(setting.key, e.target.checked); } else if (setting.type === 'checkbox') { entry.changeValue(setting.key, e.target.checked); } else { entry.changeValue(setting.key, e.target.value); } if (setting.callback) { setting.callback(e.target.value); } if (setting.subsettings && setting.subsettings.length > 0) { $(`[data-parent-feature-setting-id='${entry.id}:${setting.key}']`).toggle(); } }; const renderSettingsEntry = (setting, entry) => { const inputId = `${entry.id}:${setting.key}`; return `
${setting.type !== 'checkbox' ? `` : ''} ${setting.type === 'checkbox' ? `` : ''}
`; }; export default (settings) => { const html = settingsPage; $('body').prepend(html); const settingsPanel = $('.futsettings #settingspanel'); for (const entry of settings.getEntries()) { const checked = entry.isActive ? 'checked="checked"' : ''; settingsPanel.append(`

`); let settingsFields = ''; if (entry.settings && entry.settings.length > 0) { for (const setting of entry.settings) { if (setting.subsettings.length > 0) { settingsFields += renderSettingsEntry(setting, entry); const settingActive = setting.value ? 'block' : 'none'; settingsFields += `
`; for (const subsetting of setting.subsettings) { settingsFields += renderSettingsEntry(subsetting, entry); } settingsFields += '
'; } else { settingsFields += renderSettingsEntry(setting, entry); } } const featureActive = entry.isActive ? 'block' : 'none'; settingsPanel.append(`
${settingsFields}
`); for (const setting of entry.settings) { $(`[data-feature-setting-id='${entry.id}:${setting.key}']`).on('change', (e) => { handleFieldChange(entry, setting, e); }); for (const subsetting of setting.subsettings) { $(`[data-feature-setting-id='${entry.id}:${subsetting.key}']`).on('change', (e) => { handleFieldChange(entry, subsetting, e); }); } } } else { settingsPanel.append('
'); } $(`[data-feature-id='${entry.id}']`).on('click', () => { settings.toggleEntry(entry.id); $(`[data-feature-settings='${entry.id}']`).toggle(); }); } $('.futsettings-toggle').click(() => { analytics.trackEvent('Settings', 'Toggle settings', $('.futsettings').is(':visible')); $('.futsettings').toggle(); }); }; ================================================ FILE: app/settings/index.scss ================================================ .futsettings-toggle { position: absolute !important; bottom: 20px; right: 20px; z-index: 999; ::before { font-family: UltimateTeam-Icons,sans-serif; content: "\E056"; font-size: 2rem; color: gray; } } .futsettings { position: absolute; top: 112px; bottom: 0; left: 105px; right: 0; background-color: #fff; overflow-y: auto; display: none; z-index: 998; padding: 15px; label { color: black; } &, *, *:before, *:after { box-sizing: border-box; } footer { text-align: center; padding: 15px; color: black; hr { border: none; border-bottom: 1px solid #ddd; } p, li { font-size: smaller; margin: 10px; } } .settings-title { color: #183f94; font-size: 2.5em; font-weight: 400; font-family: UltimateTeamCondensed,sans-serif; line-height: 1em; margin-bottom: 0.5rem; text-transform: uppercase; width: 100%; } .main-setting { label { display: inline-block; padding-bottom: 15px; padding-top: 15px; } } .feature-settings-empty { display: none; } .feature-settings { background-color: #f5f5f5; margin-bottom: 25px; padding: 10px; position: relative; .setting { padding: 10px; input[type=number], input[type=text] { background-color: #fff; border: 1px #33314e solid; clear: both; color: #33314e; display: block; font-size: 14px; height: 3.5em; padding: 10px; text-align: left; width: 100%; } } } input[type=checkbox] { display: none; + label { cursor: pointer; position: relative; padding-left: 50px; &:before { background-color: #ccc; border: 1px solid #999; border-radius: 8px; content: ''; height: 16px; left: 0; position: absolute; transition: background-color 300ms ease, border-color 300ms ease;; width: 40px; } &:after { background-color: #999; border: 1px solid #999; border-radius: 50%; content: ''; height: 22px; left: 0; margin-top: -3px; position: absolute; transform: translateX(0); transition: background-color 300ms ease, border-color 300ms ease, transform 300ms ease; width: 22px; } } &:checked + label { &:before { background-color: #fc87ac; border-color: #f93b78; } &:after { background-color: #f93b78; border-color: #f93b78; transform: translateX(20px); } } } } ================================================ FILE: app/transferlist/card-info.js ================================================ /* globals window $ document */ import { BaseScript, SettingsEntry } from '../core'; import './style/card-info.scss'; export class CardInfoSettings extends SettingsEntry { static id = 'card-info'; constructor() { super('card-info', 'Extra card information', null); this.addSetting('Show contracts', 'show-contracts', true, 'checkbox'); } } class CardInfo extends BaseScript { constructor() { super(CardInfoSettings.id); const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; this._observer = new MutationObserver(this._mutationHandler.bind(this)); } activate(state) { super.activate(state); const obsConfig = { childList: true, characterData: true, attributes: false, subtree: true, }; setTimeout(() => { this._observer.observe($(document)[0], obsConfig); }, 0); } deactivate(state) { super.deactivate(state); this._observer.disconnect(); } _mutationHandler(mutationRecords) { const settings = this.getSettings(); mutationRecords.forEach((mutation) => { if ($(mutation.target).find('.listFUTItem').length > 0) { const controller = getAppMain().getRootViewController() .getPresentedViewController().getCurrentViewController() .getCurrentController(); if (!controller || !controller._listController) { return; } let items = []; if (controller._listController._view._list) { items = controller._listController._view._list._listRows; } else { items = controller._listController._viewmodel._collection.map(item => ( { data: item } )); } const rows = $('.listFUTItem'); rows.each((index, row) => { if ($(row).find('.infoTab-extra').length > 0) { return; // already added } let info = ''; if (settings['show-contracts'].toString() === 'true') { info += `
C:${items[index].data.contract}
`; } $(row).find('.small.player').prepend(`
${info}
`); }); } }); } } new CardInfo(); // eslint-disable-line no-new ================================================ FILE: app/transferlist/index.js ================================================ import { RefreshListSettings } from './refresh-list'; import { MinBinSettings } from './min-bin'; import { CardInfoSettings } from './card-info'; import { ListSizeSettings } from './list-size'; import { TransferTotalsSettings } from './transfer-totals'; export { CardInfoSettings, RefreshListSettings, MinBinSettings, ListSizeSettings, TransferTotalsSettings, }; ================================================ FILE: app/transferlist/list-size.js ================================================ /* globals gConfigurationModel models */ import { BaseScript, SettingsEntry } from '../core'; export class ListSizeSettings extends SettingsEntry { static id = 'list-size'; constructor() { super('list-size', 'Increase transfer list size', null); this.addSetting('Items per page on transfer market (max 30)', 'items-per-page-transfermarket', 30, 'number'); this.addSetting('Items per page on club (max 90)', 'items-per-page-club', 90, 'number'); } } class ListSize extends BaseScript { constructor() { super(ListSizeSettings.id); } activate(state) { super.activate(state); this._start(); } onScreenRequest(screenId) { super.onScreenRequest(screenId); if (this._running) { this._start(); } } deactivate(state) { super.deactivate(state); this._stop(); } _start() { this._running = true; const itemsOnMarket = parseInt(this.getSettings()['items-per-page-transfermarket'], 10); const itemsOnClub = parseInt(this.getSettings()['items-per-page-club'], 10); const configObj = gConfigurationModel .getConfigObject(models.ConfigurationModel.KEY_ITEMS_PER_PAGE); configObj[models.ConfigurationModel.ITEMS_PER_PAGE.TRANSFER_MARKET] = itemsOnMarket; configObj[models.ConfigurationModel.ITEMS_PER_PAGE.CLUB] = itemsOnClub; } _stop() { this._running = false; const configObj = gConfigurationModel .getConfigObject(models.ConfigurationModel.KEY_ITEMS_PER_PAGE); configObj[models.ConfigurationModel.ITEMS_PER_PAGE.TRANSFER_MARKET] = 15; configObj[models.ConfigurationModel.ITEMS_PER_PAGE.CLUB] = 45; } } new ListSize(); // eslint-disable-line no-new ================================================ FILE: app/transferlist/min-bin.js ================================================ /* globals window $ document */ import { analytics, BaseScript, SettingsEntry } from '../core'; import { TransferMarket, priceTiers } from '../../fut'; export class MinBinSettings extends SettingsEntry { static id = 'min-bin'; constructor() { super('min-bin', 'Search minimum BIN'); this.addSetting('Amount of lowest BINs to determine minimum on', 'mean-count', 3, 'number'); this.addSetting('Adjust quicklist panel price automatically based on minimum BIN', 'adjust-list-price', true, 'checkbox'); this.addSettingUnder('adjust-list-price', 'Start price percentage (0 to 100%)', 'start-price-percentage', 90, 'number'); this.addSettingUnder('adjust-list-price', 'Buy now price percentage (0 to 100%)', 'buy-now-price-percentage', 110, 'number'); } } class MinBin extends BaseScript { constructor() { super(MinBinSettings.id); const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; this._observer = new MutationObserver(this._mutationHandler.bind(this)); this._playerPrices = []; } activate(state) { super.activate(state); const obsConfig = { childList: true, characterData: true, attributes: false, subtree: true, }; setTimeout(() => { this._observer.observe($(document)[0], obsConfig); }, 0); } deactivate(state) { super.deactivate(state); this._observer.disconnect(); } _mutationHandler(mutationRecords) { mutationRecords.forEach(function (mutation) { if ($(mutation.target).hasClass('DetailView') && $(mutation.target) .find('.DetailPanel') && mutation.addedNodes.length > 0) { const searchMinBin = $(mutation.target).find('#searchMinBin'); searchMinBin.remove(); let selectedItem = this._getSelectedItem(); if (selectedItem == null || selectedItem.resourceId === 0) { return; } const knownPlayerPrice = this._playerPrices .find(p => p.resourceId === selectedItem.resourceId); let price = ''; if (knownPlayerPrice != null) { price = `(${knownPlayerPrice.minimumBin})`; this._updateListPrice(knownPlayerPrice.minimumBin); } $(mutation.target).find('.DetailPanel > .ut-button-group').prepend(``); $('#searchMinBin').bind('click', async () => { const btn = $('#searchMinBin'); btn.find('.btn-text').html('Searching minimum BIN...'); analytics.trackEvent('Min BIN', 'Search Min BIN', btn.data('resource-id')); const settings = this.getSettings(); const minimumBin = await new TransferMarket().searchMinBuy(selectedItem, parseInt(settings['mean-count'], 10)); const playerPrice = this._playerPrices.find(p => p.resourceId === btn.data('resource-id')); if (playerPrice != null) { this._playerPrices.splice(this._playerPrices.indexOf(playerPrice), 1); } this._playerPrices.push({ resourceId: btn.data('resource-id'), minimumBin, }); selectedItem = this._getSelectedItem(); let notificationText = `Minimum BIN found for ${selectedItem._staticData.name} is ${minimumBin}`; if (btn.data('resource-id') === selectedItem.resourceId) { if (minimumBin === 0) { btn.find('.btn-text').html('Search minimum BIN (extinct)'); notificationText = `Minimum BIN not found for ${selectedItem._staticData.name}, card may be extinct`; } else { btn.find('.btn-text').html(`Search minimum BIN (${minimumBin})`); this._updateListPrice(minimumBin); } } GM_notification({ text: notificationText, title: 'FUT 21 Web App', timeout: 5000, onclick: () => window.focus(), }); }); } }, this); } _updateListPrice(minimumBin) { const settings = this.getSettings(); const quicklistPanel = getAppMain().getRootViewController() .getPresentedViewController() .getCurrentViewController() .getCurrentController() ._rightController._currentController._quickListPanel; if (settings['adjust-list-price'] && quicklistPanel) { const quicklistpanelView = quicklistPanel._view; const listPrice = priceTiers.determineListPrice( minimumBin * (settings['start-price-percentage'] / 100), minimumBin * (settings['buy-now-price-percentage'] / 100), ); if (quicklistPanel._item) { // sets the values when the quicklistpanel hasn't been initialized const auction = quicklistPanel._item._auction; if (auction.tradeState === 'closed') { // item is sold return; } if (auction.tradeState !== 'active') { auction.startingBid = listPrice.start; auction.buyNowPrice = listPrice.buyNow; quicklistPanel._item.setAuctionData(auction); } } const bidSpinner = quicklistpanelView._bidNumericStepper; const buySpinner = quicklistpanelView._buyNowNumericStepper; bidSpinner.setValue(listPrice.start); buySpinner.setValue(listPrice.buyNow); } } /* eslint-disable class-methods-use-this */ _getSelectedItem() { const listController = getAppMain().getRootViewController() .getPresentedViewController() .getCurrentViewController() .getCurrentController()._listController; if (listController) { return listController.getIterator().current(); } const detailController = getAppMain().getRootViewController() .getPresentedViewController() .getCurrentViewController() .getCurrentController()._rightController; if (detailController && detailController._currentController._viewmodel) { const current = detailController ._currentController._viewmodel.current(); return current._item ? current._item : current; } return null; } /* eslint-enable class-methods-use-this */ } new MinBin(); // eslint-disable-line no-new ================================================ FILE: app/transferlist/refresh-list.js ================================================ /* globals $ */ import { BaseScript, SettingsEntry } from '../core'; import './style/refresh-list.scss'; export class RefreshListSettings extends SettingsEntry { static id = 'refresh-transferlist'; constructor() { super('refresh-transferlist', 'Refresh transferlist', null); } } class RefreshTransferList extends BaseScript { constructor() { super(RefreshListSettings.id); } activate(state) { super.activate(state); this._show(state.screenId); } onScreenRequest(screenId) { super.onScreenRequest(screenId); this._show(screenId); } deactivate(state) { super.deactivate(state); $('#header').find('.subTitle').find('.refresh').remove(); } /* eslint-disable class-methods-use-this */ _show(event) { switch (event) { case 'UTMarketSearchResultsSplitViewController': // market search setTimeout(() => { if ($('.pagingContainer').find('.refresh').length === 0) { $('.pagingContainer').append(''); $('.refresh').click(() => { const listController = getAppMain().getRootViewController() .getPresentedViewController() .getCurrentViewController() .getCurrentController() ._listController; const currentPage = listController._paginationViewModel._pageIndex; listController._requestItems(currentPage); }); } }, 1000); break; default: // no need to show anything on other screens } } /* eslint-enable class-methods-use-this */ } new RefreshTransferList(); // eslint-disable-line no-new ================================================ FILE: app/transferlist/style/card-info.scss ================================================ .item.player.small.TOTW .infoTab-extra, .item.player.small.OTW .infoTab-extra, .item.player.small.TOTS .infoTab-extra, .item.player.small.TOTY .infoTab-extra, .item.player.small.legend .infoTab-extra { color: white; } .item.player.small .infoTab-extra { width: 100%; height: 100%; position: absolute; } ================================================ FILE: app/transferlist/style/refresh-list.scss ================================================ button.flat.pagination.refresh{ &:before { font-family: UltimateTeam-Icons,sans-serif; content: '\E051'; } } ================================================ FILE: app/transferlist/style/transfer-totals.scss ================================================ .transfer-totals { background-color: #183f94; color: #fff; .auction { float: right; margin: 1em 3em 1em 0; text-align: right; width: 45%; .auctionStartPrice { display: none; @media (min-width: 1281px) { display: block; } } .auctionValue { float: left; padding-right: 1%; width: 24%; } .label { color: #b5b7bb; display: block; font-size: .75rem; text-transform: uppercase; } .value { font-size: 1.125em; font-weight: 400; font-family: UltimateTeamCondensed,sans-serif; display: block; } @media (max-width: 1130px) { align-items: flex-start; box-sizing: border-box; display: flex; float: none; margin: 0; padding: 0.5em 1.2rem 0.5em 113px; text-align: left; width: 100%; } } &:after { content: ''; display: table; width: 100%; } } ================================================ FILE: app/transferlist/transfer-totals.js ================================================ /* globals window $ document */ import { BaseScript, SettingsEntry } from '../core'; import './style/transfer-totals.scss'; export class TransferTotalsSettings extends SettingsEntry { static id = 'transfer-totals'; constructor() { super('transfer-totals', 'Transfer list totals', null); this.addSetting('Show transfer list totals', 'show-transfer-totals', true, 'checkbox'); } } class TransferTotals extends BaseScript { constructor() { super(TransferTotalsSettings.id); const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; this._observer = new MutationObserver(this._mutationHandler.bind(this)); } activate(state) { super.activate(state); const obsConfig = { childList: true, characterData: true, attributes: false, subtree: true, }; setTimeout(() => { this._observer.observe($(document)[0], obsConfig); }, 0); } deactivate(state) { super.deactivate(state); this._observer.disconnect(); } _mutationHandler(mutationRecords) { const settings = this.getSettings(); mutationRecords.forEach((mutation) => { if ( $(mutation.target).find('.listFUTItem').length > 0 || $(mutation.target).find('.futbin').length > 0 ) { const controller = getAppMain() .getRootViewController() .getPresentedViewController() .getCurrentViewController() .getCurrentController(); if (!controller || !controller._listController) { return; } if (window.currentPage !== 'UTTransferListSplitViewController') { return; } if (!settings.isActive || settings['show-transfer-totals'].toString() !== 'true') { return; } const lists = $('.ut-transfer-list-view .itemList'); const items = controller._listController._viewmodel._collection; const listRows = $('.ut-transfer-list-view .listFUTItem'); lists.each((index, list) => { const totals = { futbin: 0, bid: 0, bin: 0, }; const listEl = $(list); if (!listEl.find('.listFUTItem').length) { return; } const firstIndex = $(list).find('.listFUTItem:first').index('.ut-transfer-list-view .listFUTItem'); const lastIndex = $(list).find('.listFUTItem:last').index('.ut-transfer-list-view .listFUTItem'); totals.futbin = items.slice(firstIndex, lastIndex + 1).reduce((sum, item, i) => { const futbin = parseInt( listRows.eq(i + firstIndex) .find('.auctionValue.futbin .coins.value') .text() .replace(/[,.]/g, ''), 10, ) || 0; return sum + futbin; }, 0); totals.bid = items.slice(firstIndex, lastIndex + 1) .reduce((sum, item) => { const { currentBid, startingBid } = item._auction; const actualBid = currentBid > 0 ? currentBid : startingBid; return sum + actualBid; }, 0); totals.bin = items.slice(firstIndex, lastIndex + 1) .reduce((sum, item) => sum + item._auction.buyNowPrice, 0); const totalsItem = listEl.prev('.transfer-totals'); if (!totalsItem.length) { $(`
Futbin BIN 0
Bid Total 0
BIN Total 0
`).insertBefore(listEl); } if (totals.futbin > 0) { totalsItem.find('.total-futbin').text(totals.futbin); totalsItem.find('.futbin').show(); } else { totalsItem.find('.futbin').hide(); } totalsItem.find('.total-bin').text(totals.bin); totalsItem.find('.total-bid').text(totals.bid); }); } }); } } new TransferTotals(); // eslint-disable-line no-new ================================================ FILE: fut/club.js ================================================ /* globals transferobjects enums communication factories */ export class Club { async getPlayers(start, count) { return new Promise((resolve, reject) => { const t = new transferobjects.SearchCriteria(); t.type = enums.SearchType.PLAYER; const o = new communication.ClubSearchDelegate(t, start, count); o._useClickShield = false; o.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { sender.clearListenersByScope(this); const players = Array.isArray(response.itemData) ? factories.Item.generateItemsFromItemData(response.itemData) : []; const isLastPage = players.length <= count - 1; resolve({ isLastPage, getNextPage: isLastPage ? null : () => this.getPlayers(start + count, count), players, }); }); o.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { sender.clearListenersByScope(this); reject(response); }); o.send(); }); } } ================================================ FILE: fut/errors/index.js ================================================ export class ListPlayerError extends Error {} ================================================ FILE: fut/index.js ================================================ import { Logger } from './logger'; import { PinEvent } from './pinEvent'; import { Store } from './store'; import { TransferMarket } from './transferMarket'; import { Club } from './club'; import utils from './utils'; import priceTiers from './priceTiers'; export { Club, Logger, PinEvent, Store, TransferMarket, utils, priceTiers, }; ================================================ FILE: fut/logger.js ================================================ export class Logger { constructor() { this._storeName = 'logger'; } log(message, category = 'FUT') { /* eslint-disable no-console */ console.log(`${category}: ${message}`); /* eslint-enable no-console */ const log = JSON.parse(GM_getValue(this._storeName, '[]')); log.push(`${category}: ${message}`); GM_setValue(this._storeName, JSON.stringify(log)); } reset() { GM_setValue(this._storeName, '[]'); } } ================================================ FILE: fut/pinEvent.js ================================================ /* globals PIN_PAGEVIEW_EVT_TYPE services PINEventType */ export class PinEvent { static sendPageView(pageId, delay = 2000) { return new Promise(resolve => setTimeout(() => { services.PIN.sendData(PINEventType.PAGE_VIEW, { type: PIN_PAGEVIEW_EVT_TYPE, pgid: pageId, }); resolve(); }, delay)); } } ================================================ FILE: fut/priceTiers.js ================================================ /* global utils UTCurrencyInputControl */ export default { roundValueToNearestPriceTiers(value) { const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => value > i.min); const diff = value % tier.inc; if (diff === 0) { return value; } else if (diff < tier.inc / 2) { return value - diff; } return value + (tier.inc - diff); }, roundDownToNearestPriceTiers(value) { const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => value > i.min); const diff = value % tier.inc; if (diff === 0) { return value - tier.inc; } return value - diff; }, determineListPrice(start, buyNow) { const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => buyNow > i.min); const startPrice = this.roundValueToNearestPriceTiers(start); let buyNowPrice = this.roundValueToNearestPriceTiers(buyNow); if (startPrice === buyNowPrice) { buyNowPrice += tier.inc; } return { start: startPrice, buyNow: buyNowPrice, }; }, }; ================================================ FILE: fut/store.js ================================================ /* global communication repositories enums services */ export class Store { getUnassignedItems() { return new Promise((resolve) => { repositories.Item.reset(enums.FUTItemPile.PURCHASED); repositories.Item.getUnassignedItems().observe(this, function (o, list) { o.unobserve(this); resolve(list.items); }); }); } getTradePile() { return new Promise((resolve, reject) => { repositories.Item.getTransferItems().observe(this, (obs, data) => { obs.unobserve(this); if (data.error) { reject(new Error(data.erorr)); } else { resolve(data.items); } }); }); } async getTradePileUnsold() { const tradepile = await this.getTradePile(); return tradepile.filter(d => d.state === enums.ItemState.FREE && d._auction.buyNowPrice > 0); } redeemItem(item) { return new Promise((resolve, reject) => { const redeem = new communication.ConsumeUnlockableDelegate(item.id); redeem.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { sender.clearListenersByScope(this); resolve(response); }); redeem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { sender.clearListenersByScope(this); reject(response); }); redeem.send(); }); } quickSell(items) { return new Promise((resolve) => { services.Item.discard(items).observe(this, (obs, res) => { obs.unobserve(this); resolve(res); }); }); } sendToClub(items) { return new Promise((resolve, reject) => { const moveItem = new communication.MoveItemDelegate(items, enums.FUTItemPile.CLUB); moveItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { sender.clearListenersByScope(this); resolve(response); }); moveItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { sender.clearListenersByScope(this); reject(response); }); moveItem.send(); }); } removeSoldAuctions() { return new Promise((resolve, reject) => { services.Item.clearSoldItems().observe(this, (observer, data) => { observer.unobserve(this); if (data.error) { reject(new Error(data.erorr)); } else { resolve(data.items); } }); }); } } ================================================ FILE: fut/transferMarket.js ================================================ /* globals enums factories communication gUserModel models repositories services */ import { mean } from 'math-statistics'; import utils from './utils'; import priceTiers from './priceTiers'; import { Logger } from './logger'; import { PinEvent } from './pinEvent'; import { ListItemError } from './errors'; export class TransferMarket { _logger = new Logger(); /* eslint-disable class-methods-use-this */ async navigateToTransferHub() { await PinEvent.sendPageView('Hub - Transfers'); } async navigateToTransferList() { await this.navigateToTransferHub(); await PinEvent.sendPageView('Transfer List - List View'); } /* eslint-enable class-methods-use-this */ async searchMinBuy(item, itemsForMean = 3, lowUp = false) { services.Item.clearTransferMarketCache(); this._logger.log(`Searching min buy for ${item.type} ${item._staticData.name} from low upward first ${lowUp}`, 'Core - Transfermarket'); let minBuy = 0; if (lowUp) { minBuy = await this._findLowUp(item, itemsForMean); this._logger.log(`Low up search yielded ${minBuy} as a result`, 'Core - Transfermarket'); } if (minBuy === 0) { this._logger.log('Searching low down...', 'Core - Transfermarket'); minBuy = await this._findLowDown(item, itemsForMean); } if (minBuy === 0) { this._logger.log('No players found... it might be extinct', 'Core - Transfermarket'); } else { this._logger.log(`Min buy for ${item.type} ${item._staticData.name} is ${minBuy}`, 'Core - Transfermarket'); } return minBuy; } /** * List item on transfermarket * * @param {FUTItem} item * @param {number} start start price * @param {number} buyNow buy now price * @param {number} duration time to list in seconds (1, 3, 6, 12, 24 or 72 hours) */ async listItem(item, start, buyNow, duration = 3600) { return new Promise(async (resolve, reject) => { if (gUserModel.getTradeAccess() !== models.UserModel.TRADE_ACCESS.WHITELIST) { reject(new Error('You are not authorized for trading')); return; } const prices = priceTiers.determineListPrice(start, buyNow); await this.sendToTradePile(item); await utils.sleep(1000); const listItem = new communication.ListItemDelegate({ itemId: item.id, startingBid: prices.start, buyNowPrice: prices.buyNow, duration, }); listItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { sender.clearListenersByScope(this); resolve({ startingBid: prices.start, buyNowPrice: prices.buyNow, }); }); listItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { sender.clearListenersByScope(this); reject(new ListItemError(response)); }); listItem.send(); }); } sendToTradePile(item) { return new Promise((resolve, reject) => { const moveItem = new communication.MoveItemDelegate([item], enums.FUTItemPile.TRANSFER); moveItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { sender.clearListenersByScope(this); resolve(); }); moveItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { sender.clearListenersByScope(this); reject(new Error(response)); }); moveItem.send(); }); } relistAllItems() { return new Promise((resolve, reject) => { if (gUserModel.getTradeAccess() !== models.UserModel.TRADE_ACCESS.WHITELIST) { reject(new Error('You are not authorized for trading')); return; } const relistExpired = new communication.AuctionRelistDelegate(); relistExpired.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { sender.clearListenersByScope(this); repositories.Item.setDirty(enums.FUTItemPile.TRANSFER); resolve(); }); relistExpired.addListener(communication.BaseDelegate.FAIL, this, (sender, error) => { sender.clearListenersByScope(this); reject(new Error(error)); }); relistExpired.execute(); }); } async _findLowUp(item, itemsForMean) { const searchCriteria = this._defineSearchCriteria(item, 200); await PinEvent.sendPageView('Transfer Market Search'); await utils.sleep(3000); await PinEvent.sendPageView('Transfer Market Results - List View', 0); await PinEvent.sendPageView('Item - Detail View', 0); const items = await this._find(searchCriteria); if (items.length > itemsForMean) { // we find more than X listed at this price, so it must be low value return 200; } return 0; // trigger searching low down } async _findLowDown(item, itemsForMean) { let minBuy = 99999999; const searchCriteria = this._defineSearchCriteria(item); let valuesFound = []; for (let minBuyFound = false; minBuyFound === false;) { /* eslint-disable no-await-in-loop */ await PinEvent.sendPageView('Transfer Market Search'); await utils.sleep(800); await PinEvent.sendPageView('Transfer Market Results - List View', 0); await PinEvent.sendPageView('Item - Detail View', 0); const items = await this._find(searchCriteria); /* eslint-enable no-await-in-loop */ if (items.length > 0) { valuesFound = valuesFound.concat(items.map(i => i._auction.buyNowPrice)); const minBuyOnPage = Math.min(...items.map(i => i._auction.buyNowPrice)); if (minBuyOnPage < minBuy) { minBuy = minBuyOnPage; if (items.length < searchCriteria.count) { minBuyFound = true; break; } searchCriteria.maxBuy = priceTiers.roundDownToNearestPriceTiers(minBuy); if (searchCriteria.maxBuy < 200) { searchCriteria.maxBuy = 200; } } else if (items.length === searchCriteria.count) { if (searchCriteria.maxBuy === 0) { searchCriteria.maxBuy = minBuy; } else { searchCriteria.maxBuy = priceTiers.roundDownToNearestPriceTiers(searchCriteria.maxBuy); } if (searchCriteria.maxBuy < 200) { searchCriteria.maxBuy = 200; minBuy = 200; minBuyFound = true; } } else { minBuy = Math.min(...items.map(i => i._auction.buyNowPrice)); minBuyFound = true; } } else { minBuyFound = true; } } valuesFound = valuesFound.sort((a, b) => a - b).slice(0, itemsForMean); if (valuesFound.length > 0) { return priceTiers.roundValueToNearestPriceTiers(mean(valuesFound)); } return 0; // player extinct } /* eslint-disable class-methods-use-this */ _defineSearchCriteria(item, maxBuy = -1) { // TODO: check if this can handle other items as well // eslint-disable-next-line no-undef const searchCriteria = new UTSearchCriteriaDTO(); searchCriteria.count = 30; searchCriteria.maskedDefId = item.getMaskedResourceId(); searchCriteria.type = item.type; if (item.rareflag === 47) { // 47 = Champions // if it is a Champions card, this is seen as a gold card // Can only search for "Gold" in this case searchCriteria.level = factories.DataProvider.getItemLevelDP(true) .filter(d => d.id === 2)[0].value; } else if (item.rareflag >= 3) { // 3 = TOTW // if it is TOTW or other special, set it to TOTW. See enums.ItemRareType. // Can only search for "Specials", not more specific on Rare Type searchCriteria.level = factories.DataProvider.getItemLevelDP(true) .filter(d => d.id === 3)[0].value; } searchCriteria.category = enums.SearchCategory.ANY; searchCriteria.position = enums.SearchType.ANY; if (maxBuy !== -1) { searchCriteria.maxBuy = maxBuy; } return searchCriteria; } /* eslint-enable class-methods-use-this */ _find(searchCriteria) { return new Promise((resolve, reject) => { services.Item.searchTransferMarket(searchCriteria, 1).observe( this, function (obs, res) { if (!res.success) { obs.unobserve(this); reject(res.status); } else { resolve(res.data.items); } }, ); }); } } ================================================ FILE: fut/utils.js ================================================ /* globals services */ export default { /** * Sleep for a while * * @param {number} min minimum sleep time in ms * @param {number} variance maximum variation to add to the minimum in ms */ sleep(min, variance = 1000) { const delay = min + Math.floor(Math.random() * variance); // new Logger().log(`Delay for ${delay} (requested: ${min}+${variance})`, 'Core'); return new Promise(resolve => setTimeout(resolve, delay)); }, getPlatform() { if (services.User.getUser().getSelectedPersona().isPlaystation) { return 'ps'; } if (services.User.getUser().getSelectedPersona().isPC) { return 'pc'; } if (services.User.getUser().getSelectedPersona().isXbox) { return 'xbox'; } throw new Error('unknown platform'); }, }; ================================================ FILE: package.json ================================================ { "name": "futwebapp-single", "version": "0.0.0-development", "description": "", "main": "index.js", "scripts": { "clean": "rm -rf dist", "test": "echo \"Error: no test specified\" && exit 1", "start": "npm run standards:watch & webpack --watch", "build": "npm run standards && webpack", "build:production": "npm run standards && npm run semantic-release:dry && webpack --config webpack.config.prd.js", "standards": "eslint .", "standards:watch": "esw --watch", "semantic-release:dry": "semantic-release --dry-run", "semantic-release": "semantic-release" }, "eslintIgnore": [ "dist", "webpack.config.js", "webpack.config.prd.js", "tampermonkey-headers.js" ], "author": "", "license": "ISC", "devDependencies": { "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-eslint": "^8.0.1", "babel-loader": "^7.1.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-env": "^1.7.0", "css-loader": "^0.28.7", "eslint": "^4.10.0", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.8.0", "eslint-plugin-jsx-a11y": "^6.0.2", "eslint-plugin-react": "^7.4.0", "eslint-watch": "^3.1.3", "html-loader": "^0.5.1", "last-release-git": "0.0.3", "node-libs-browser": "webpack/node-libs-browser", "node-sass": "^4.14.1", "sass-loader": "^6.0.6", "semantic-release": "^9.0.0", "style-loader": "^0.19.0", "webpack": "^3.8.1", "webpack-cli": "^3.3.12", "webpack-obfuscator": "^2.4.3" }, "dependencies": { "babel-polyfill": "^6.26.0", "c3": "^0.4.18", "d3": "^4.11.0", "event-emitter-es6": "^1.1.5", "math-statistics": "^1.2.0", "moment": "^2.22.2", "moment-duration-format": "^1.3.0" }, "repository": { "type": "git", "url": "https://github.com/Mardaneus86/futwebapp-tampermonkey.git" }, "release": { "getLastRelease": "last-release-git", "branch": "master" } } ================================================ FILE: tampermonkey-headers.js ================================================ // ==UserScript== // @name FUT Enhancer // @version VERSION // @description Enhances the FIFA Ultimate Team 21 Web app. Includes Futbin integration and other useful tools // @license MIT // @author Tim Klingeleers // @match https://www.ea.com/fifa/ultimate-team/web-app/* // @match https://www.easports.com/*/fifa/ultimate-team/web-app/* // @match https://www.ea.com/*/fifa/ultimate-team/web-app/* // @namespace https://github.com/Mardaneus86 // @supportURL https://github.com/Mardaneus86/futwebapp-tampermonkey/issues // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant window.focus // @connect ea.com // @connect futbin.com // @connect google-analytics.com // @updateURL https://github.com/Mardaneus86/futwebapp-tampermonkey-web/raw/master/downloads/FUT_Enhancer.meta.js // @downloadURL https://github.com/Mardaneus86/futwebapp-tampermonkey-web/raw/master/downloads/FUT_Enhancer.user.js // ==/UserScript== ================================================ FILE: webpack.config.js ================================================ const webpack = require('webpack'); module.exports = { entry: './app/index.js', output: { filename: './dist/fut-enhancer.user.js', }, node: { fs: 'empty', tls: 'empty', net: 'empty' }, devtool: 'eval-source-map', plugins: [ new webpack.DefinePlugin({ 'UA_TOKEN': JSON.stringify('UA-126264296-1') }) ], module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['env'], plugins: [ 'transform-class-properties', ], }, }, }, { test: /\.scss$|\.css$/, use: [{ loader: 'style-loader', // creates style nodes from JS strings }, { loader: 'css-loader', // translates CSS into CommonJS }, { loader: 'sass-loader', // compiles Sass to CSS }], }, { test: /\.html$/, use: [{ loader: 'html-loader', options: { minimize: true, removeComments: false, collapseWhitespace: false, }, }], }, ], }, }; ================================================ FILE: webpack.config.prd.js ================================================ var fs = require("fs"); const webpack = require('webpack'); const config = require('./webpack.config'); module.exports = env => { let header = fs.readFileSync('./tampermonkey-headers.js', 'utf8'); header = header.replace('VERSION', process.env.TM_VERSION); // set by the build process on Travis console.log('Changed Tampermonkey header version to ' + process.env.TM_VERSION); config.devtool = 'none'; config.plugins = [ new webpack.DefinePlugin({ 'UA_TOKEN': JSON.stringify('UA-126264296-2') }), new webpack.BannerPlugin({ banner: header, raw: true, entryOnly: true }), ]; return config; };