├── .gitignore ├── fut ├── errors │ └── index.js ├── index.js ├── pinEvent.js ├── logger.js ├── utils.js ├── club.js ├── priceTiers.js ├── store.js └── transferMarket.js ├── .eslintignore ├── images ├── settings-screen.jpg ├── sbc-futbin-value.jpg ├── club-search-player-screen.jpg └── transfer-search-player-screen.jpg ├── app ├── transferlist │ ├── style │ │ ├── refresh-list.scss │ │ ├── card-info.scss │ │ └── transfer-totals.scss │ ├── index.js │ ├── list-size.js │ ├── refresh-list.js │ ├── card-info.js │ ├── transfer-totals.js │ └── min-bin.js ├── instant-bin-confirm │ ├── index.js │ ├── settings-entry.js │ └── instant-bin-confirm.js ├── futbin │ ├── index.js │ ├── settings-entry.js │ ├── style │ │ └── futbin-prices.scss │ ├── futbin-player-links.js │ └── futbin-prices.js ├── core │ ├── index.js │ ├── browser.js │ ├── db.js │ ├── queue.js │ ├── settings.js │ ├── base-script.js │ ├── analytics.js │ └── settings-entry.js ├── index.scss ├── index.js └── settings │ ├── index.scss │ ├── index.js │ └── html │ └── index │ └── settings.html ├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── FUNDING.yml ├── webpack.config.prd.js ├── tampermonkey-headers.js ├── LICENSE ├── analytics ├── LICENSE ├── config.js └── index.js ├── webpack.config.js ├── .travis.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /fut/errors/index.js: -------------------------------------------------------------------------------- 1 | export class ListPlayerError extends Error {} 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | tampermonkey-headers.js 3 | webpack.config.js 4 | webpack.config.prd.js 5 | -------------------------------------------------------------------------------- /images/settings-screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mardaneus86/futwebapp-tampermonkey/HEAD/images/settings-screen.jpg -------------------------------------------------------------------------------- /images/sbc-futbin-value.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mardaneus86/futwebapp-tampermonkey/HEAD/images/sbc-futbin-value.jpg -------------------------------------------------------------------------------- /images/club-search-player-screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mardaneus86/futwebapp-tampermonkey/HEAD/images/club-search-player-screen.jpg -------------------------------------------------------------------------------- /images/transfer-search-player-screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mardaneus86/futwebapp-tampermonkey/HEAD/images/transfer-search-player-screen.jpg -------------------------------------------------------------------------------- /app/transferlist/style/refresh-list.scss: -------------------------------------------------------------------------------- 1 | button.flat.pagination.refresh{ 2 | &:before { 3 | font-family: UltimateTeam-Icons,sans-serif; 4 | content: '\E051'; 5 | } 6 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.js] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /app/instant-bin-confirm/index.js: -------------------------------------------------------------------------------- 1 | import { InstantBinConfirmSettings } from './settings-entry'; 2 | import { InstantBinConfirm } from './instant-bin-confirm'; 3 | 4 | export { 5 | InstantBinConfirmSettings, 6 | }; 7 | 8 | new InstantBinConfirm(); // eslint-disable-line no-new 9 | -------------------------------------------------------------------------------- /app/instant-bin-confirm/settings-entry.js: -------------------------------------------------------------------------------- 1 | import { SettingsEntry } from '../core'; 2 | 3 | export class InstantBinConfirmSettings extends SettingsEntry { 4 | static id = 'instant-bin-confirm'; 5 | constructor() { 6 | super('instant-bin-confirm', 'Instantly confirm Buy It Now dialog'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/futbin/index.js: -------------------------------------------------------------------------------- 1 | import './style/futbin-prices.scss'; 2 | 3 | import { FutbinPrices } from './futbin-prices'; 4 | import { FutbinPlayerLinks } from './futbin-player-links'; 5 | import { FutbinSettings } from './settings-entry'; 6 | 7 | export { 8 | FutbinSettings, 9 | }; 10 | 11 | new FutbinPrices(); // eslint-disable-line no-new 12 | new FutbinPlayerLinks(); // eslint-disable-line no-new 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "func-names": ["error", "never"], 6 | "import/prefer-default-export": "off", 7 | "no-underscore-dangle": "off" 8 | }, 9 | "globals": { 10 | "UA_TOKEN": false, 11 | "getAppMain": false, 12 | "GM_notification": false 13 | }, 14 | "env": { 15 | "greasemonkey": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/transferlist/style/card-info.scss: -------------------------------------------------------------------------------- 1 | .item.player.small.TOTW .infoTab-extra, 2 | .item.player.small.OTW .infoTab-extra, 3 | .item.player.small.TOTS .infoTab-extra, 4 | .item.player.small.TOTY .infoTab-extra, 5 | .item.player.small.legend .infoTab-extra { 6 | color: white; 7 | } 8 | 9 | .item.player.small .infoTab-extra { 10 | width: 100%; 11 | height: 100%; 12 | position: absolute; 13 | } 14 | -------------------------------------------------------------------------------- /fut/index.js: -------------------------------------------------------------------------------- 1 | import { Logger } from './logger'; 2 | import { PinEvent } from './pinEvent'; 3 | import { Store } from './store'; 4 | import { TransferMarket } from './transferMarket'; 5 | import { Club } from './club'; 6 | import utils from './utils'; 7 | import priceTiers from './priceTiers'; 8 | 9 | export { 10 | Club, 11 | Logger, 12 | PinEvent, 13 | Store, 14 | TransferMarket, 15 | utils, 16 | priceTiers, 17 | }; 18 | -------------------------------------------------------------------------------- /fut/pinEvent.js: -------------------------------------------------------------------------------- 1 | /* globals PIN_PAGEVIEW_EVT_TYPE services PINEventType */ 2 | 3 | export class PinEvent { 4 | static sendPageView(pageId, delay = 2000) { 5 | return new Promise(resolve => 6 | setTimeout(() => { 7 | services.PIN.sendData(PINEventType.PAGE_VIEW, { 8 | type: PIN_PAGEVIEW_EVT_TYPE, 9 | pgid: pageId, 10 | }); 11 | resolve(); 12 | }, delay)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/transferlist/index.js: -------------------------------------------------------------------------------- 1 | import { RefreshListSettings } from './refresh-list'; 2 | import { MinBinSettings } from './min-bin'; 3 | import { CardInfoSettings } from './card-info'; 4 | import { ListSizeSettings } from './list-size'; 5 | import { TransferTotalsSettings } from './transfer-totals'; 6 | 7 | export { 8 | CardInfoSettings, 9 | RefreshListSettings, 10 | MinBinSettings, 11 | ListSizeSettings, 12 | TransferTotalsSettings, 13 | }; 14 | -------------------------------------------------------------------------------- /app/core/index.js: -------------------------------------------------------------------------------- 1 | import { Settings } from './settings'; 2 | import { SettingsEntry } from './settings-entry'; 3 | import { BaseScript } from './base-script'; 4 | import { Database } from './db'; 5 | import { Queue } from './queue'; 6 | import browser from './browser'; 7 | import analytics from './analytics'; 8 | 9 | export { 10 | BaseScript, 11 | Database, 12 | Queue, 13 | Settings, 14 | SettingsEntry, 15 | browser, 16 | analytics, 17 | }; 18 | -------------------------------------------------------------------------------- /app/futbin/settings-entry.js: -------------------------------------------------------------------------------- 1 | import { SettingsEntry } from '../core'; 2 | 3 | export class FutbinSettings extends SettingsEntry { 4 | static id = 'futbin'; 5 | constructor() { 6 | super('futbin', 'FutBIN integration'); 7 | 8 | this.addSetting('Show link to player page', 'show-link-to-player', false, 'checkbox'); 9 | this.addSetting('Show prices on SBC and Squad', 'show-sbc-squad', false, 'checkbox'); 10 | this.addSetting('Mark bargains', 'show-bargains', false, 'checkbox'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fut/logger.js: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | constructor() { 3 | this._storeName = 'logger'; 4 | } 5 | 6 | log(message, category = 'FUT') { 7 | /* eslint-disable no-console */ 8 | console.log(`${category}: ${message}`); 9 | /* eslint-enable no-console */ 10 | const log = JSON.parse(GM_getValue(this._storeName, '[]')); 11 | log.push(`${category}: ${message}`); 12 | GM_setValue(this._storeName, JSON.stringify(log)); 13 | } 14 | 15 | reset() { 16 | GM_setValue(this._storeName, '[]'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/core/browser.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | window document Blob 3 | */ 4 | 5 | export default { 6 | downloadFile(filename, data) { 7 | const blob = new Blob([data], { type: 'text/csv' }); 8 | if (window.navigator.msSaveOrOpenBlob) { 9 | window.navigator.msSaveBlob(blob, filename); 10 | } else { 11 | const elem = window.document.createElement('a'); 12 | elem.href = window.URL.createObjectURL(blob); 13 | elem.download = filename; 14 | document.body.appendChild(elem); 15 | elem.click(); 16 | document.body.removeChild(elem); 17 | } 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 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. 11 | 12 | ### Describe the solution you'd like 13 | A clear and concise description of what you want to happen. 14 | 15 | ### Suggestions for implementation 16 | Add any other context, mockups or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /app/core/db.js: -------------------------------------------------------------------------------- 1 | /* eslint valid-typeof: "error" */ 2 | 3 | export class Database { 4 | constructor() { 5 | this.set('database-version', '1'); 6 | } 7 | 8 | static set(key, value) { 9 | GM_setValue(key, value); 10 | } 11 | 12 | static setJson(key, value) { 13 | this.set(key, JSON.stringify(value)); 14 | } 15 | 16 | static get(key, defaultValue) { 17 | let value = defaultValue; 18 | if (typeof value === 'object') { 19 | value = JSON.stringify(value); 20 | } 21 | return GM_getValue(key, value); 22 | } 23 | 24 | static getJson(key, defaultValue) { 25 | return JSON.parse(this.get(key, defaultValue)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Mardaneus86 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.paypal.me/timklingeleers"] 13 | -------------------------------------------------------------------------------- /webpack.config.prd.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | const webpack = require('webpack'); 4 | const config = require('./webpack.config'); 5 | 6 | module.exports = env => { 7 | let header = fs.readFileSync('./tampermonkey-headers.js', 'utf8'); 8 | header = header.replace('VERSION', process.env.TM_VERSION); // set by the build process on Travis 9 | 10 | console.log('Changed Tampermonkey header version to ' + process.env.TM_VERSION); 11 | 12 | config.devtool = 'none'; 13 | config.plugins = [ 14 | new webpack.DefinePlugin({ 15 | 'UA_TOKEN': JSON.stringify('UA-126264296-2') 16 | }), 17 | new webpack.BannerPlugin({ 18 | banner: header, 19 | raw: true, 20 | entryOnly: true 21 | }), 22 | ]; 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | READ BEFORE YOU ADD AN ISSUE: Do not request autobuyer features, because they are considered as cheating. Issues regarding autobuyers will be closed immediately. 11 | 12 | ### Expected behavior: 13 | Give a detailed explanation of the expected behavior. 14 | 15 | ### Current behavior: 16 | Give a detailed explanation of the current behavior. 17 | 18 | ### Metadata: 19 | - **Script version:** 20 | - **Browser:** 21 | - **OS:** 22 | 23 | ### To Reproduce 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | ### Screenshots 31 | If applicable, add screenshots to help explain your problem. 32 | -------------------------------------------------------------------------------- /app/index.scss: -------------------------------------------------------------------------------- 1 | .ut-content-container { 2 | padding: 0; 3 | 4 | .ut-content { 5 | border: 0; 6 | 7 | &.ut-content--split-view-extend { 8 | max-height: 100%; 9 | } 10 | } 11 | } 12 | 13 | .listFUTItem .entityContainer .name.untradeable { 14 | display: block; 15 | 16 | &::before { 17 | position: relative; 18 | padding-right: 10px; 19 | } 20 | } 21 | 22 | .ut-transfer-list-view .listFUTItem .entityContainer, 23 | .ut-club-search-results-view.ui-layout-left .listFUTItem .entityContainer, 24 | .ut-unassigned-view.ui-layout-left .listFUTItem .entityContainer { 25 | width: 45%; 26 | } 27 | 28 | @media (min-width: 1281px) { 29 | .ut-content-container .ut-content { 30 | max-width: 100%; 31 | max-height: 100%; 32 | } 33 | 34 | .ut-split-view .ut-content { 35 | max-width: 100%; 36 | max-height: 100%; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fut/utils.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | services 3 | */ 4 | 5 | export default { 6 | /** 7 | * Sleep for a while 8 | * 9 | * @param {number} min minimum sleep time in ms 10 | * @param {number} variance maximum variation to add to the minimum in ms 11 | */ 12 | sleep(min, variance = 1000) { 13 | const delay = min + Math.floor(Math.random() * variance); 14 | // new Logger().log(`Delay for ${delay} (requested: ${min}+${variance})`, 'Core'); 15 | return new Promise(resolve => setTimeout(resolve, delay)); 16 | }, 17 | 18 | getPlatform() { 19 | if (services.User.getUser().getSelectedPersona().isPlaystation) { 20 | return 'ps'; 21 | } 22 | if (services.User.getUser().getSelectedPersona().isPC) { 23 | return 'pc'; 24 | } 25 | if (services.User.getUser().getSelectedPersona().isXbox) { 26 | return 'xbox'; 27 | } 28 | 29 | throw new Error('unknown platform'); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /app/core/queue.js: -------------------------------------------------------------------------------- 1 | import { utils } from '../../fut'; 2 | 3 | export class Queue { 4 | constructor() { 5 | this._queue = []; 6 | } 7 | 8 | static getInstance() { 9 | if (this._instance == null) { 10 | this._instance = new Queue(); 11 | } 12 | 13 | return this._instance; 14 | } 15 | 16 | add(identifier, cb) { 17 | this._queue.push({ 18 | identifier, 19 | cb, 20 | }); 21 | } 22 | 23 | async start() { 24 | this._running = true; 25 | /* eslint-disable no-await-in-loop */ 26 | while (this._running) { 27 | if (this._queue.length > 0) { 28 | const scriptToRun = this._queue.shift(); 29 | if (scriptToRun) { 30 | await scriptToRun.cb(); 31 | } 32 | } else { 33 | await utils.sleep(1000); 34 | } 35 | } 36 | /* eslint-enable no-await-in-loop */ 37 | } 38 | 39 | stop() { 40 | this._running = false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tampermonkey-headers.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name FUT Enhancer 3 | // @version VERSION 4 | // @description Enhances the FIFA Ultimate Team 21 Web app. Includes Futbin integration and other useful tools 5 | // @license MIT 6 | // @author Tim Klingeleers 7 | // @match https://www.ea.com/fifa/ultimate-team/web-app/* 8 | // @match https://www.easports.com/*/fifa/ultimate-team/web-app/* 9 | // @match https://www.ea.com/*/fifa/ultimate-team/web-app/* 10 | // @namespace https://github.com/Mardaneus86 11 | // @supportURL https://github.com/Mardaneus86/futwebapp-tampermonkey/issues 12 | // @grant GM_notification 13 | // @grant GM_xmlhttpRequest 14 | // @grant GM_getValue 15 | // @grant GM_setValue 16 | // @grant window.focus 17 | // @connect ea.com 18 | // @connect futbin.com 19 | // @connect google-analytics.com 20 | // @updateURL https://github.com/Mardaneus86/futwebapp-tampermonkey-web/raw/master/downloads/FUT_Enhancer.meta.js 21 | // @downloadURL https://github.com/Mardaneus86/futwebapp-tampermonkey-web/raw/master/downloads/FUT_Enhancer.user.js 22 | // ==/UserScript== 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tim Klingeleers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analytics/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Peaks & Pies GmbH ; 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /fut/club.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | transferobjects enums communication factories 3 | */ 4 | 5 | export class Club { 6 | async getPlayers(start, count) { 7 | return new Promise((resolve, reject) => { 8 | const t = new transferobjects.SearchCriteria(); 9 | t.type = enums.SearchType.PLAYER; 10 | 11 | const o = new communication.ClubSearchDelegate(t, start, count); 12 | o._useClickShield = false; 13 | 14 | o.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { 15 | sender.clearListenersByScope(this); 16 | 17 | const players = Array.isArray(response.itemData) ? 18 | factories.Item.generateItemsFromItemData(response.itemData) : []; 19 | const isLastPage = players.length <= count - 1; 20 | resolve({ 21 | isLastPage, 22 | getNextPage: isLastPage ? null : () => this.getPlayers(start + count, count), 23 | players, 24 | }); 25 | }); 26 | 27 | o.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { 28 | sender.clearListenersByScope(this); 29 | reject(response); 30 | }); 31 | 32 | o.send(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fut/priceTiers.js: -------------------------------------------------------------------------------- 1 | /* global utils UTCurrencyInputControl */ 2 | export default { 3 | roundValueToNearestPriceTiers(value) { 4 | const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => value > i.min); 5 | 6 | const diff = value % tier.inc; 7 | 8 | if (diff === 0) { 9 | return value; 10 | } else if (diff < tier.inc / 2) { 11 | return value - diff; 12 | } 13 | return value + (tier.inc - diff); 14 | }, 15 | 16 | roundDownToNearestPriceTiers(value) { 17 | const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => value > i.min); 18 | 19 | const diff = value % tier.inc; 20 | 21 | if (diff === 0) { 22 | return value - tier.inc; 23 | } 24 | return value - diff; 25 | }, 26 | 27 | determineListPrice(start, buyNow) { 28 | const tier = utils.JS.find(UTCurrencyInputControl.PRICE_TIERS, i => buyNow > i.min); 29 | 30 | const startPrice = this.roundValueToNearestPriceTiers(start); 31 | let buyNowPrice = this.roundValueToNearestPriceTiers(buyNow); 32 | 33 | if (startPrice === buyNowPrice) { 34 | buyNowPrice += tier.inc; 35 | } 36 | 37 | return { 38 | start: startPrice, 39 | buyNow: buyNowPrice, 40 | }; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /app/core/settings.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'event-emitter-es6'; 2 | 3 | import analytics from './analytics'; 4 | 5 | export class Settings extends EventEmitter { 6 | constructor() { 7 | super(); 8 | this._entries = []; 9 | } 10 | 11 | static getInstance() { 12 | if (this._instance == null) { 13 | this._instance = new Settings(); 14 | } 15 | 16 | return this._instance; 17 | } 18 | 19 | /** 20 | * 21 | * @param {SettingsEntry} entry The entry for the settings 22 | */ 23 | registerEntry(entry) { 24 | this._entries.push(entry); 25 | 26 | if (entry.isActive) { 27 | this._emitEvent(entry); 28 | } 29 | } 30 | 31 | getEntries() { 32 | return this._entries; 33 | } 34 | 35 | toggleEntry(id) { 36 | const entries = this._entries.filter(e => e.id === id); 37 | if (!entries || entries.length === 0) { 38 | return; 39 | } 40 | 41 | entries[0].toggle(); 42 | 43 | analytics.trackEvent('Settings', `Toggle setting ${id}`, entries[0].isActive); 44 | this._emitEvent(entries[0]); 45 | } 46 | 47 | _emitEvent(entry) { 48 | if (entry.isActive) { 49 | this.emit('entry-enabled', entry); 50 | } else { 51 | this.emit('entry-disabled', entry); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/core/base-script.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | /* eslint class-methods-use-this: "off" */ 3 | import { Settings } from './settings'; 4 | import { Database } from './db'; 5 | 6 | export class BaseScript { 7 | constructor(id) { 8 | this._id = id; 9 | 10 | Settings.getInstance().on('entry-enabled', (entry) => { 11 | if (entry.id === id) { 12 | this.screenRequestObserver = window.onPageNavigation.observe( 13 | this, 14 | function (obs, event) { 15 | setTimeout(() => { 16 | this.onScreenRequest(event); 17 | }, 1000); 18 | }, 19 | ); 20 | 21 | this.activate({ 22 | screenId: window.currentPage, 23 | }); 24 | } 25 | }); 26 | 27 | Settings.getInstance().on('entry-disabled', (entry) => { 28 | if (entry.id === id) { 29 | this.screenRequestObserver.unobserve(this); 30 | 31 | this.deactivate({ 32 | screenId: window.currentPage, 33 | }); 34 | } 35 | }); 36 | } 37 | 38 | activate() { 39 | // override in subclasses 40 | } 41 | 42 | deactivate() { 43 | // override in subclasses 44 | } 45 | 46 | onScreenRequest() { 47 | // override in subclasses 48 | } 49 | 50 | getSettings() { 51 | return Database.getJson(`settings:${this._id}`, {}); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/transferlist/style/transfer-totals.scss: -------------------------------------------------------------------------------- 1 | .transfer-totals { 2 | background-color: #183f94; 3 | color: #fff; 4 | .auction { 5 | float: right; 6 | margin: 1em 3em 1em 0; 7 | text-align: right; 8 | width: 45%; 9 | .auctionStartPrice { 10 | display: none; 11 | @media (min-width: 1281px) { 12 | display: block; 13 | } 14 | } 15 | .auctionValue { 16 | float: left; 17 | padding-right: 1%; 18 | width: 24%; 19 | } 20 | .label { 21 | color: #b5b7bb; 22 | display: block; 23 | font-size: .75rem; 24 | text-transform: uppercase; 25 | } 26 | .value { 27 | font-size: 1.125em; 28 | font-weight: 400; 29 | font-family: UltimateTeamCondensed,sans-serif; 30 | display: block; 31 | } 32 | @media (max-width: 1130px) { 33 | align-items: flex-start; 34 | box-sizing: border-box; 35 | display: flex; 36 | float: none; 37 | margin: 0; 38 | padding: 0.5em 1.2rem 0.5em 113px; 39 | text-align: left; 40 | width: 100%; 41 | } 42 | } 43 | &:after { 44 | content: ''; 45 | display: table; 46 | width: 100%; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/core/analytics.js: -------------------------------------------------------------------------------- 1 | import ua from '../../analytics'; 2 | 3 | import { Database } from './db'; 4 | 5 | class Analytics { 6 | constructor() { 7 | if (this.ua === undefined) { 8 | let id = Database.get('uuid', ''); 9 | if (id === '') { 10 | id = this._uuidv4(); 11 | Database.set('uuid', id); 12 | } 13 | 14 | this.ua = ua(null, null, { 15 | tid: UA_TOKEN, 16 | cid: id, 17 | uid: id, 18 | }); 19 | } 20 | } 21 | 22 | /* eslint-disable */ 23 | _uuidv4() { 24 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 25 | var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); 26 | return v.toString(16); 27 | }); 28 | } 29 | /* eslint-enable */ 30 | 31 | trackPage(pageId) { 32 | return new Promise((resolve, reject) => { 33 | this.ua.pageview(pageId, (err) => { 34 | if (err) { 35 | reject(err); 36 | } else { 37 | resolve(); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | trackEvent(category, action, label = null, value = null) { 44 | return new Promise((resolve, reject) => { 45 | this.ua.event(category, action, label, value, (err) => { 46 | if (err) { 47 | reject(err); 48 | } else { 49 | resolve(); 50 | } 51 | }); 52 | }); 53 | } 54 | } 55 | 56 | export default new Analytics(); 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './app/index.js', 5 | output: { 6 | filename: './dist/fut-enhancer.user.js', 7 | }, 8 | node: { 9 | fs: 'empty', 10 | tls: 'empty', 11 | net: 'empty' 12 | }, 13 | devtool: 'eval-source-map', 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'UA_TOKEN': JSON.stringify('UA-126264296-1') 17 | }) 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /(node_modules|bower_components)/, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['env'], 28 | plugins: [ 29 | 'transform-class-properties', 30 | ], 31 | }, 32 | }, 33 | }, 34 | { 35 | test: /\.scss$|\.css$/, 36 | use: [{ 37 | loader: 'style-loader', // creates style nodes from JS strings 38 | }, { 39 | loader: 'css-loader', // translates CSS into CommonJS 40 | }, { 41 | loader: 'sass-loader', // compiles Sass to CSS 42 | }], 43 | }, 44 | { 45 | test: /\.html$/, 46 | use: [{ 47 | loader: 'html-loader', 48 | options: { 49 | minimize: true, 50 | removeComments: false, 51 | collapseWhitespace: false, 52 | }, 53 | }], 54 | }, 55 | ], 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | - ~/node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '8' 10 | before_script: 11 | - npm prune 12 | - git fetch --tags 13 | - export TM_VERSION=$(npm run semantic-release:dry | grep "The next release version is " | sed 's/[^0-9.]*\([0-9.]*\).*/\1/') 14 | script: 15 | - npm run build:production 16 | env: 17 | - NPM_TOKEN="00000000-0000-0000-0000-000000000000" 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | after_success: 22 | # CREATE GIT TAG 23 | - git config --global user.email "builds@travis-ci.com" 24 | - git config --global user.name "Travis CI" 25 | - '[[ $TRAVIS_BRANCH == "master" ]] && git tag $TM_VERSION -a -m "Generated tag from TravisCI build $TRAVIS_BUILD_NUMBER"' 26 | - '[[ $TRAVIS_BRANCH == "master" ]] && git push --quiet https://$GITHUBKEY@github.com/Mardaneus86/futwebapp-tampermonkey $GIT_TAG > /dev/null 2>&1' 27 | deploy: 28 | provider: releases 29 | api_key: 30 | 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=" 31 | file: 32 | - dist/fut-enhancer.user.js 33 | skip_cleanup: true 34 | -------------------------------------------------------------------------------- /app/core/settings-entry.js: -------------------------------------------------------------------------------- 1 | import { Database } from './db'; 2 | 3 | export class SettingsEntry { 4 | constructor(id, name) { 5 | const settings = Database.getJson(`settings:${id}`, {}); 6 | 7 | this.id = id; 8 | this.name = name; 9 | this.isActive = settings.isActive ? settings.isActive : false; 10 | this.settings = []; 11 | } 12 | 13 | toggle() { 14 | this.isActive = !this.isActive; 15 | 16 | const settings = Database.getJson(`settings:${this.id}`, {}); 17 | settings.isActive = this.isActive; 18 | Database.setJson(`settings:${this.id}`, settings); 19 | } 20 | 21 | addSetting(label, key, defaultValue, type, cb) { 22 | const settings = Database.getJson(`settings:${this.id}`, {}); 23 | 24 | settings[key] = key in settings ? settings[key] : defaultValue; 25 | Database.setJson(`settings:${this.id}`, settings); 26 | 27 | this.settings.push({ 28 | label, 29 | key, 30 | type, 31 | value: key in settings ? settings[key] : defaultValue, 32 | callback: cb, 33 | subsettings: [], 34 | }); 35 | } 36 | 37 | addSettingUnder(underKey, label, key, defaultValue, type, cb) { 38 | const settings = Database.getJson(`settings:${this.id}`, {}); 39 | settings[key] = key in settings ? settings[key] : defaultValue; 40 | Database.setJson(`settings:${this.id}`, settings); 41 | 42 | const setting = this.settings.find(s => s.key === underKey); 43 | setting.subsettings.push({ 44 | label, 45 | key, 46 | type, 47 | value: key in settings ? settings[key] : defaultValue, 48 | callback: cb, 49 | }); 50 | } 51 | 52 | changeValue(key, value) { 53 | const settings = Database.getJson(`settings:${this.id}`, {}); 54 | 55 | settings[key] = value; 56 | 57 | Database.setJson(`settings:${this.id}`, settings); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/instant-bin-confirm/instant-bin-confirm.js: -------------------------------------------------------------------------------- 1 | /* global 2 | gPopupClickShield 3 | enums 4 | EADialogViewController 5 | services 6 | utils 7 | */ 8 | 9 | import { BaseScript } from '../core'; 10 | import { InstantBinConfirmSettings } from './settings-entry'; 11 | 12 | export class InstantBinConfirm extends BaseScript { 13 | unmodifiedConfirmation = utils.PopupManager.ShowConfirmation; 14 | 15 | constructor() { 16 | super(InstantBinConfirmSettings.id); 17 | } 18 | 19 | activate(state) { 20 | super.activate(state); 21 | } 22 | 23 | onScreenRequest(screenId) { 24 | super.onScreenRequest(screenId); 25 | const settings = this.getSettings(); 26 | 27 | utils.PopupManager.ShowConfirmation = (dialog, amount, proceed, s) => { 28 | let cancel = s; 29 | if (!utils.JS.isFunction(s)) { 30 | cancel = function () { }; 31 | } 32 | 33 | if (settings.isActive && dialog.title === 34 | utils.PopupManager.Confirmations.CONFIRM_BUY_NOW.title) { 35 | proceed(); 36 | return; 37 | } 38 | 39 | const n = new EADialogViewController({ 40 | dialogOptions: [dialog.buttonLabels[0], 41 | dialog.buttonLabels[1]], 42 | message: services.Localization.localize(dialog.message, amount), 43 | title: services.Localization.localize(dialog.title), 44 | }); 45 | 46 | n.init(); 47 | gPopupClickShield.setActivePopup(n); 48 | n.onExit.observe(this, (e, t) => { 49 | if (t !== enums.UIDialogOptions.CANCEL && t !== enums.UIDialogOptions.NO) { 50 | if (proceed) { 51 | proceed(); 52 | } else if (cancel) { 53 | cancel(); 54 | } 55 | } else { 56 | cancel(); 57 | } 58 | }); 59 | }; 60 | } 61 | 62 | deactivate(state) { 63 | super.deactivate(state); 64 | utils.PopupManager.ShowConfirmation = this.unmodifiedConfirmation; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/transferlist/list-size.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | gConfigurationModel models 3 | */ 4 | 5 | import { BaseScript, SettingsEntry } from '../core'; 6 | 7 | export class ListSizeSettings extends SettingsEntry { 8 | static id = 'list-size'; 9 | constructor() { 10 | super('list-size', 'Increase transfer list size', null); 11 | this.addSetting('Items per page on transfer market (max 30)', 'items-per-page-transfermarket', 30, 'number'); 12 | this.addSetting('Items per page on club (max 90)', 'items-per-page-club', 90, 'number'); 13 | } 14 | } 15 | 16 | class ListSize extends BaseScript { 17 | constructor() { 18 | super(ListSizeSettings.id); 19 | } 20 | 21 | activate(state) { 22 | super.activate(state); 23 | 24 | this._start(); 25 | } 26 | 27 | onScreenRequest(screenId) { 28 | super.onScreenRequest(screenId); 29 | 30 | if (this._running) { 31 | this._start(); 32 | } 33 | } 34 | 35 | deactivate(state) { 36 | super.deactivate(state); 37 | 38 | this._stop(); 39 | } 40 | 41 | _start() { 42 | this._running = true; 43 | 44 | const itemsOnMarket = parseInt(this.getSettings()['items-per-page-transfermarket'], 10); 45 | const itemsOnClub = parseInt(this.getSettings()['items-per-page-club'], 10); 46 | const configObj = gConfigurationModel 47 | .getConfigObject(models.ConfigurationModel.KEY_ITEMS_PER_PAGE); 48 | configObj[models.ConfigurationModel.ITEMS_PER_PAGE.TRANSFER_MARKET] = itemsOnMarket; 49 | configObj[models.ConfigurationModel.ITEMS_PER_PAGE.CLUB] = itemsOnClub; 50 | } 51 | 52 | _stop() { 53 | this._running = false; 54 | 55 | const configObj = gConfigurationModel 56 | .getConfigObject(models.ConfigurationModel.KEY_ITEMS_PER_PAGE); 57 | configObj[models.ConfigurationModel.ITEMS_PER_PAGE.TRANSFER_MARKET] = 15; 58 | configObj[models.ConfigurationModel.ITEMS_PER_PAGE.CLUB] = 45; 59 | } 60 | } 61 | 62 | new ListSize(); // eslint-disable-line no-new 63 | -------------------------------------------------------------------------------- /app/transferlist/refresh-list.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | 3 | import { BaseScript, SettingsEntry } from '../core'; 4 | import './style/refresh-list.scss'; 5 | 6 | export class RefreshListSettings extends SettingsEntry { 7 | static id = 'refresh-transferlist'; 8 | constructor() { 9 | super('refresh-transferlist', 'Refresh transferlist', null); 10 | } 11 | } 12 | 13 | class RefreshTransferList extends BaseScript { 14 | constructor() { 15 | super(RefreshListSettings.id); 16 | } 17 | 18 | activate(state) { 19 | super.activate(state); 20 | this._show(state.screenId); 21 | } 22 | 23 | onScreenRequest(screenId) { 24 | super.onScreenRequest(screenId); 25 | this._show(screenId); 26 | } 27 | 28 | deactivate(state) { 29 | super.deactivate(state); 30 | $('#header').find('.subTitle').find('.refresh').remove(); 31 | } 32 | 33 | /* eslint-disable class-methods-use-this */ 34 | _show(event) { 35 | switch (event) { 36 | case 'UTMarketSearchResultsSplitViewController': // market search 37 | setTimeout(() => { 38 | if ($('.pagingContainer').find('.refresh').length === 0) { 39 | $('.pagingContainer').append(''); 40 | $('.refresh').click(() => { 41 | const listController = getAppMain().getRootViewController() 42 | .getPresentedViewController() 43 | .getCurrentViewController() 44 | .getCurrentController() 45 | ._listController; 46 | 47 | const currentPage = listController._paginationViewModel._pageIndex; 48 | 49 | listController._requestItems(currentPage); 50 | }); 51 | } 52 | }, 1000); 53 | break; 54 | default: 55 | // no need to show anything on other screens 56 | } 57 | } 58 | /* eslint-enable class-methods-use-this */ 59 | } 60 | 61 | new RefreshTransferList(); // eslint-disable-line no-new 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futwebapp-single", 3 | "version": "0.0.0-development", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf dist", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "npm run standards:watch & webpack --watch", 10 | "build": "npm run standards && webpack", 11 | "build:production": "npm run standards && npm run semantic-release:dry && webpack --config webpack.config.prd.js", 12 | "standards": "eslint .", 13 | "standards:watch": "esw --watch", 14 | "semantic-release:dry": "semantic-release --dry-run", 15 | "semantic-release": "semantic-release" 16 | }, 17 | "eslintIgnore": [ 18 | "dist", 19 | "webpack.config.js", 20 | "webpack.config.prd.js", 21 | "tampermonkey-headers.js" 22 | ], 23 | "author": "", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "babel-cli": "^6.26.0", 27 | "babel-core": "^6.26.3", 28 | "babel-eslint": "^8.0.1", 29 | "babel-loader": "^7.1.2", 30 | "babel-plugin-transform-class-properties": "^6.24.1", 31 | "babel-preset-env": "^1.7.0", 32 | "css-loader": "^0.28.7", 33 | "eslint": "^4.10.0", 34 | "eslint-config-airbnb": "^16.1.0", 35 | "eslint-plugin-import": "^2.8.0", 36 | "eslint-plugin-jsx-a11y": "^6.0.2", 37 | "eslint-plugin-react": "^7.4.0", 38 | "eslint-watch": "^3.1.3", 39 | "html-loader": "^0.5.1", 40 | "last-release-git": "0.0.3", 41 | "node-libs-browser": "webpack/node-libs-browser", 42 | "node-sass": "^4.14.1", 43 | "sass-loader": "^6.0.6", 44 | "semantic-release": "^9.0.0", 45 | "style-loader": "^0.19.0", 46 | "webpack": "^3.8.1", 47 | "webpack-cli": "^3.3.12", 48 | "webpack-obfuscator": "^2.4.3" 49 | }, 50 | "dependencies": { 51 | "babel-polyfill": "^6.26.0", 52 | "c3": "^0.4.18", 53 | "d3": "^4.11.0", 54 | "event-emitter-es6": "^1.1.5", 55 | "math-statistics": "^1.2.0", 56 | "moment": "^2.22.2", 57 | "moment-duration-format": "^1.3.0" 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "https://github.com/Mardaneus86/futwebapp-tampermonkey.git" 62 | }, 63 | "release": { 64 | "getLastRelease": "last-release-git", 65 | "branch": "master" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /* globals onVisibilityChanged services UTGameFlowNavigationController 2 | UTViewController EAObservable window document $ */ 3 | import 'babel-polyfill'; 4 | import './index.scss'; 5 | import initSettingsScreen from './settings'; 6 | 7 | import { analytics, Settings, Queue } from './core'; 8 | 9 | import { Logger } from '../fut'; 10 | /* 11 | RemoveSoldAuctionsSettings, 12 | RelistAuctionsSettings, 13 | */ 14 | import { 15 | RefreshListSettings, 16 | CardInfoSettings, 17 | ListSizeSettings, 18 | MinBinSettings, 19 | TransferTotalsSettings, 20 | } from './transferlist'; 21 | 22 | import { 23 | FutbinSettings, 24 | } from './futbin'; 25 | 26 | import { 27 | InstantBinConfirmSettings, 28 | } from './instant-bin-confirm'; 29 | /* 30 | import { 31 | ClubInfoSettings, 32 | } from './club'; 33 | */ 34 | 35 | window.onPageNavigation = new EAObservable(); 36 | 37 | window.currentPage = ''; 38 | 39 | UTGameFlowNavigationController.prototype.didPush = (t) => { 40 | if (t) { 41 | analytics.trackPage(t.className); 42 | window.onPageNavigation.notify(t.className); 43 | window.currentPage = t.className; 44 | } 45 | }; 46 | 47 | UTViewController.prototype.didPresent = (t) => { 48 | if (t) { 49 | analytics.trackPage(t.className); 50 | window.onPageNavigation.notify(t.className); 51 | window.currentPage = t.className; 52 | } 53 | }; 54 | 55 | setTimeout(() => { 56 | services.Authentication.oAuthentication.observe( 57 | this, 58 | () => { 59 | // reset the logs at startup 60 | new Logger().reset(); 61 | 62 | // force full web app layout in any case 63 | $('body').removeClass('phone').addClass('landscape'); 64 | 65 | Queue.getInstance().start(); 66 | 67 | // get rid of pinEvents when switching tabs 68 | document.removeEventListener('visibilitychange', onVisibilityChanged); 69 | 70 | const settings = Settings.getInstance(); 71 | settings.registerEntry(new RefreshListSettings()); 72 | settings.registerEntry(new MinBinSettings()); 73 | settings.registerEntry(new CardInfoSettings()); 74 | settings.registerEntry(new ListSizeSettings()); 75 | settings.registerEntry(new TransferTotalsSettings()); 76 | 77 | settings.registerEntry(new FutbinSettings()); 78 | settings.registerEntry(new InstantBinConfirmSettings()); 79 | 80 | initSettingsScreen(settings); 81 | }, 82 | ); 83 | }, 1000); 84 | -------------------------------------------------------------------------------- /app/transferlist/card-info.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | window $ document */ 3 | 4 | import { BaseScript, SettingsEntry } from '../core'; 5 | 6 | import './style/card-info.scss'; 7 | 8 | export class CardInfoSettings extends SettingsEntry { 9 | static id = 'card-info'; 10 | constructor() { 11 | super('card-info', 'Extra card information', null); 12 | 13 | this.addSetting('Show contracts', 'show-contracts', true, 'checkbox'); 14 | } 15 | } 16 | 17 | class CardInfo extends BaseScript { 18 | constructor() { 19 | super(CardInfoSettings.id); 20 | 21 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 22 | this._observer = new MutationObserver(this._mutationHandler.bind(this)); 23 | } 24 | 25 | activate(state) { 26 | super.activate(state); 27 | 28 | const obsConfig = { 29 | childList: true, 30 | characterData: true, 31 | attributes: false, 32 | subtree: true, 33 | }; 34 | 35 | setTimeout(() => { 36 | this._observer.observe($(document)[0], obsConfig); 37 | }, 0); 38 | } 39 | 40 | deactivate(state) { 41 | super.deactivate(state); 42 | this._observer.disconnect(); 43 | } 44 | 45 | _mutationHandler(mutationRecords) { 46 | const settings = this.getSettings(); 47 | mutationRecords.forEach((mutation) => { 48 | if ($(mutation.target).find('.listFUTItem').length > 0) { 49 | const controller = getAppMain().getRootViewController() 50 | .getPresentedViewController().getCurrentViewController() 51 | .getCurrentController(); 52 | if (!controller || !controller._listController) { 53 | return; 54 | } 55 | 56 | let items = []; 57 | if (controller._listController._view._list) { 58 | items = controller._listController._view._list._listRows; 59 | } else { 60 | items = controller._listController._viewmodel._collection.map(item => ( 61 | { data: item } 62 | )); 63 | } 64 | const rows = $('.listFUTItem'); 65 | 66 | rows.each((index, row) => { 67 | if ($(row).find('.infoTab-extra').length > 0) { 68 | return; // already added 69 | } 70 | 71 | let info = ''; 72 | if (settings['show-contracts'].toString() === 'true') { 73 | info += `
74 | C:${items[index].data.contract} 75 |
`; 76 | } 77 | 78 | $(row).find('.small.player').prepend(`
${info}
`); 79 | }); 80 | } 81 | }); 82 | } 83 | } 84 | 85 | new CardInfo(); // eslint-disable-line no-new 86 | -------------------------------------------------------------------------------- /fut/store.js: -------------------------------------------------------------------------------- 1 | /* global communication repositories enums services */ 2 | 3 | export class Store { 4 | getUnassignedItems() { 5 | return new Promise((resolve) => { 6 | repositories.Item.reset(enums.FUTItemPile.PURCHASED); 7 | repositories.Item.getUnassignedItems().observe(this, function (o, list) { 8 | o.unobserve(this); 9 | resolve(list.items); 10 | }); 11 | }); 12 | } 13 | 14 | getTradePile() { 15 | return new Promise((resolve, reject) => { 16 | repositories.Item.getTransferItems().observe(this, (obs, data) => { 17 | obs.unobserve(this); 18 | 19 | if (data.error) { 20 | reject(new Error(data.erorr)); 21 | } else { 22 | resolve(data.items); 23 | } 24 | }); 25 | }); 26 | } 27 | 28 | async getTradePileUnsold() { 29 | const tradepile = await this.getTradePile(); 30 | 31 | return tradepile.filter(d => d.state === enums.ItemState.FREE && d._auction.buyNowPrice > 0); 32 | } 33 | 34 | redeemItem(item) { 35 | return new Promise((resolve, reject) => { 36 | const redeem = new communication.ConsumeUnlockableDelegate(item.id); 37 | redeem.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { 38 | sender.clearListenersByScope(this); 39 | resolve(response); 40 | }); 41 | redeem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { 42 | sender.clearListenersByScope(this); 43 | reject(response); 44 | }); 45 | redeem.send(); 46 | }); 47 | } 48 | 49 | quickSell(items) { 50 | return new Promise((resolve) => { 51 | services.Item.discard(items).observe(this, (obs, res) => { 52 | obs.unobserve(this); 53 | resolve(res); 54 | }); 55 | }); 56 | } 57 | 58 | sendToClub(items) { 59 | return new Promise((resolve, reject) => { 60 | const moveItem = new communication.MoveItemDelegate(items, enums.FUTItemPile.CLUB); 61 | moveItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender, response) => { 62 | sender.clearListenersByScope(this); 63 | resolve(response); 64 | }); 65 | moveItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { 66 | sender.clearListenersByScope(this); 67 | reject(response); 68 | }); 69 | moveItem.send(); 70 | }); 71 | } 72 | 73 | removeSoldAuctions() { 74 | return new Promise((resolve, reject) => { 75 | services.Item.clearSoldItems().observe(this, (observer, data) => { 76 | observer.unobserve(this); 77 | 78 | if (data.error) { 79 | reject(new Error(data.erorr)); 80 | } else { 81 | resolve(data.items); 82 | } 83 | }); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/futbin/style/futbin-prices.scss: -------------------------------------------------------------------------------- 1 | #TradePile .player-stats-data-component, #Unassigned .player-stats-data-component { 2 | width: 12em; 3 | } 4 | #TradePile .listFUTItem .entityContainer, #Unassigned .listFUTItem .entityContainer { 5 | width: 45%; 6 | } 7 | #Unassigned .listFUTItem .auction .auctionValue, #Unassigned .listFUTItem .auction .auction-state { 8 | display: none; 9 | } 10 | #Unassigned .listFUTItem .auction .auctionValue.futbin { 11 | display: block; 12 | float: right; 13 | } 14 | .MyClubResults .listFUTItem .auction { 15 | display: block; 16 | position: absolute; 17 | right: 0; 18 | } 19 | .MyClubResults .listFUTItem .auction .auctionValue, .MyClubResults .listFUTItem .auction .auction-state { 20 | width: 24%; 21 | float: right; 22 | padding-right: 1%; 23 | display: none; 24 | } 25 | .MyClubResults .listFUTItem .auction .auctionValue.futbin { 26 | display: block; 27 | } 28 | 29 | .listFUTItem .auction>.auction-state, .listFUTItem .auction>.auctionStartPrice, .listFUTItem .auction>.auctionValue { 30 | flex: 1 1 20%; 31 | overflow: hidden; 32 | } 33 | 34 | .listFUTItem .auction { 35 | top: 30%; 36 | max-width: none; 37 | width: 50%; 38 | 39 | .futbin .coins.value .time { 40 | display: inline; 41 | font-size: 1em; 42 | } 43 | } 44 | 45 | @media (max-width: 1130px) { 46 | .listFUTItem .auction { 47 | width: auto; 48 | } 49 | 50 | html[dir=ltr] .listFUTItem .auction { 51 | left: auto; 52 | } 53 | } 54 | .ut-navigation-container-view.ui-layout-right .listFUTItem .auction { 55 | top: 30%; 56 | } 57 | 58 | .futbinupdate { 59 | font-size: 14px; 60 | clear: both; 61 | display: block; 62 | } 63 | .coins.value.futbin { 64 | -webkit-filter: hue-rotate(165deg); 65 | filter: hue-rotate(165deg); 66 | } 67 | .listFUTItem.has-auction-data.futbin-bargain .rowContent { 68 | background-color: #7ffe9445; 69 | } 70 | .listFUTItem.has-auction-data.selected.futbin-bargain .rowContent, .listFUTItem.has-auction-data.selected.futbin-bargain .rowContent.active { 71 | background-color: #7ffe94; 72 | color: #434853; 73 | } 74 | .ut-club-search-results-view { 75 | .listFUTItem .auction { 76 | width: 10%; 77 | } 78 | 79 | .auction-state, .auctionValue { 80 | display: none; 81 | 82 | &.futbin { 83 | display: block; 84 | } 85 | } 86 | } 87 | .player-picks-modal .time { 88 | display: block; 89 | } 90 | .ut-squad-slot-pedestal-view.futbin { 91 | min-width: 58px; 92 | flex: none; 93 | width: auto; 94 | bottom: -2.6em; 95 | white-space: nowrap; 96 | 97 | .coins.value { 98 | text-align: center; 99 | margin: 0 8px; 100 | } 101 | } 102 | 103 | .ut-squad-overview .ut-squad-summary { 104 | width: 70%; 105 | } 106 | 107 | .refresh-squad-button { 108 | margin: 17px 5px; 109 | color:#e2dde2; 110 | &:before { 111 | font-family: UltimateTeam-Icons,sans-serif; 112 | content: '\E051'; 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /app/settings/index.scss: -------------------------------------------------------------------------------- 1 | .futsettings-toggle { 2 | position: absolute !important; 3 | bottom: 20px; 4 | right: 20px; 5 | z-index: 999; 6 | 7 | ::before { 8 | font-family: UltimateTeam-Icons,sans-serif; 9 | content: "\E056"; 10 | font-size: 2rem; 11 | color: gray; 12 | } 13 | } 14 | 15 | .futsettings { 16 | position: absolute; 17 | top: 112px; 18 | bottom: 0; 19 | left: 105px; 20 | right: 0; 21 | background-color: #fff; 22 | overflow-y: auto; 23 | display: none; 24 | z-index: 998; 25 | padding: 15px; 26 | 27 | label { 28 | color: black; 29 | } 30 | 31 | &, *, *:before, *:after { 32 | box-sizing: border-box; 33 | } 34 | 35 | footer { 36 | text-align: center; 37 | padding: 15px; 38 | color: black; 39 | 40 | hr { 41 | border: none; 42 | border-bottom: 1px solid #ddd; 43 | } 44 | 45 | p, li { 46 | font-size: smaller; 47 | margin: 10px; 48 | } 49 | } 50 | 51 | .settings-title { 52 | color: #183f94; 53 | font-size: 2.5em; 54 | font-weight: 400; 55 | font-family: UltimateTeamCondensed,sans-serif; 56 | line-height: 1em; 57 | margin-bottom: 0.5rem; 58 | text-transform: uppercase; 59 | width: 100%; 60 | } 61 | 62 | .main-setting { 63 | label { 64 | display: inline-block; 65 | padding-bottom: 15px; 66 | padding-top: 15px; 67 | } 68 | } 69 | 70 | .feature-settings-empty { 71 | display: none; 72 | } 73 | 74 | .feature-settings { 75 | background-color: #f5f5f5; 76 | margin-bottom: 25px; 77 | padding: 10px; 78 | position: relative; 79 | 80 | .setting { 81 | padding: 10px; 82 | 83 | input[type=number], 84 | input[type=text] { 85 | background-color: #fff; 86 | border: 1px #33314e solid; 87 | clear: both; 88 | color: #33314e; 89 | display: block; 90 | font-size: 14px; 91 | height: 3.5em; 92 | padding: 10px; 93 | text-align: left; 94 | width: 100%; 95 | } 96 | } 97 | } 98 | 99 | input[type=checkbox] { 100 | display: none; 101 | + label { 102 | cursor: pointer; 103 | position: relative; 104 | padding-left: 50px; 105 | 106 | &:before { 107 | background-color: #ccc; 108 | border: 1px solid #999; 109 | border-radius: 8px; 110 | content: ''; 111 | height: 16px; 112 | left: 0; 113 | position: absolute; 114 | transition: background-color 300ms ease, border-color 300ms ease;; 115 | width: 40px; 116 | } 117 | &:after { 118 | background-color: #999; 119 | border: 1px solid #999; 120 | border-radius: 50%; 121 | content: ''; 122 | height: 22px; 123 | left: 0; 124 | margin-top: -3px; 125 | position: absolute; 126 | transform: translateX(0); 127 | transition: background-color 300ms ease, border-color 300ms ease, transform 300ms ease; 128 | width: 22px; 129 | } 130 | } 131 | &:checked + label { 132 | &:before { 133 | background-color: #fc87ac; 134 | border-color: #f93b78; 135 | } 136 | &:after { 137 | background-color: #f93b78; 138 | border-color: #f93b78; 139 | transform: translateX(20px); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/settings/index.js: -------------------------------------------------------------------------------- 1 | /* globals $ */ 2 | /* eslint-disable no-restricted-syntax */ 3 | 4 | import './index.scss'; 5 | import { analytics } from '../core'; 6 | import settingsPage from './html/index/settings.html'; 7 | 8 | const handleFieldChange = (entry, setting, e) => { 9 | if (setting.subsettings && setting.subsettings.length > 0) { 10 | entry.changeValue(setting.key, e.target.checked); 11 | } else if (setting.type === 'checkbox') { 12 | entry.changeValue(setting.key, e.target.checked); 13 | } else { 14 | entry.changeValue(setting.key, e.target.value); 15 | } 16 | 17 | if (setting.callback) { 18 | setting.callback(e.target.value); 19 | } 20 | if (setting.subsettings && setting.subsettings.length > 0) { 21 | $(`[data-parent-feature-setting-id='${entry.id}:${setting.key}']`).toggle(); 22 | } 23 | }; 24 | 25 | const renderSettingsEntry = (setting, entry) => { 26 | const inputId = `${entry.id}:${setting.key}`; 27 | return `
28 | ${setting.type !== 'checkbox' ? `` : ''} 29 | 36 | ${setting.type === 'checkbox' ? `` : ''} 37 |
`; 38 | }; 39 | 40 | export default (settings) => { 41 | const html = settingsPage; 42 | 43 | $('body').prepend(html); 44 | 45 | const settingsPanel = $('.futsettings #settingspanel'); 46 | 47 | for (const entry of settings.getEntries()) { 48 | const checked = entry.isActive ? 'checked="checked"' : ''; 49 | settingsPanel.append(`

50 | 51 | 52 |

`); 53 | let settingsFields = ''; 54 | if (entry.settings && entry.settings.length > 0) { 55 | for (const setting of entry.settings) { 56 | if (setting.subsettings.length > 0) { 57 | settingsFields += renderSettingsEntry(setting, entry); 58 | 59 | const settingActive = setting.value ? 'block' : 'none'; 60 | settingsFields += `
`; 61 | for (const subsetting of setting.subsettings) { 62 | settingsFields += renderSettingsEntry(subsetting, entry); 63 | } 64 | settingsFields += '
'; 65 | } else { 66 | settingsFields += renderSettingsEntry(setting, entry); 67 | } 68 | } 69 | const featureActive = entry.isActive ? 'block' : 'none'; 70 | settingsPanel.append(`
${settingsFields}
`); 71 | for (const setting of entry.settings) { 72 | $(`[data-feature-setting-id='${entry.id}:${setting.key}']`).on('change', (e) => { 73 | handleFieldChange(entry, setting, e); 74 | }); 75 | for (const subsetting of setting.subsettings) { 76 | $(`[data-feature-setting-id='${entry.id}:${subsetting.key}']`).on('change', (e) => { 77 | handleFieldChange(entry, subsetting, e); 78 | }); 79 | } 80 | } 81 | } else { 82 | settingsPanel.append('
'); 83 | } 84 | 85 | $(`[data-feature-id='${entry.id}']`).on('click', () => { 86 | settings.toggleEntry(entry.id); 87 | $(`[data-feature-settings='${entry.id}']`).toggle(); 88 | }); 89 | } 90 | 91 | $('.futsettings-toggle').click(() => { 92 | analytics.trackEvent('Settings', 'Toggle settings', $('.futsettings').is(':visible')); 93 | $('.futsettings').toggle(); 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /analytics/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | protocolVersion: "1", 4 | hostname: "https://www.google-analytics.com", 5 | path: "/collect", 6 | batchPath: "/batch", 7 | batching: true, 8 | batchSize: 10, 9 | acceptedParameters: [ 10 | 11 | // General 12 | "v", "tid", "aip", "ds", "qt", "z", 13 | 14 | // User 15 | "cid", "uid", 16 | 17 | // Session 18 | "sc", "uip", "ua", "geoid", 19 | 20 | // Traffic Sources 21 | "dr", "cn", "cs", "cm", "ck", "cc", "ci", "gclid", "dclid", 22 | 23 | // System Info 24 | "sr", "vp", "de", "sd", "ul", "je", "fl", 25 | 26 | // Hit 27 | "t", "ni", 28 | 29 | // Content Information 30 | "dl", "dh", "dp", "dt", "cd", "linkid", 31 | 32 | // App Tracking 33 | "an", "aid", "av", "aiid", 34 | 35 | // Event Tracking 36 | "ec", "ea", "el", "ev", 37 | 38 | // E-commerce (transaction data: simple and enhanced) 39 | "ti", "ta", "tr", "ts", "tt", 40 | 41 | // E-commerce (item data: simple) 42 | "in", "ip", "iq", "ic", "iv", 43 | 44 | // E-commerce (currency: simple and enhanced) 45 | "cu", 46 | 47 | // Enhanced E-Commerce (see also: regex below) 48 | "pa", "tcc", "pal", "cos", "col", "promoa", 49 | 50 | // Social Interactions 51 | "sn", "sa", "st", 52 | 53 | // Timing 54 | "utc", "utv", "utt", "utl", "plt", "dns", "pdt", "rrt", "tcp", "srt", "dit", "clt", 55 | 56 | // Exceptions 57 | "exd", "exf", 58 | 59 | // Content Experiments 60 | "xid", "xvar"], 61 | 62 | acceptedParametersRegex: [ 63 | /^cm[0-9]+$/, 64 | /^cd[0-9]+$/, 65 | /^cg(10|[0-9])$/, 66 | 67 | /pr[0-9]{1,3}id/, 68 | /pr[0-9]{1,3}nm/, 69 | /pr[0-9]{1,3}br/, 70 | /pr[0-9]{1,3}ca/, 71 | /pr[0-9]{1,3}va/, 72 | /pr[0-9]{1,3}pr/, 73 | /pr[0-9]{1,3}qt/, 74 | /pr[0-9]{1,3}cc/, 75 | /pr[0-9]{1,3}ps/, 76 | /pr[0-9]{1,3}cd[0-9]{1,3}/, 77 | /pr[0-9]{1,3}cm[0-9]{1,3}/, 78 | 79 | /il[0-9]{1,3}nm/, 80 | /il[0-9]{1,3}pi[0-9]{1,3}id/, 81 | /il[0-9]{1,3}pi[0-9]{1,3}nm/, 82 | /il[0-9]{1,3}pi[0-9]{1,3}br/, 83 | /il[0-9]{1,3}pi[0-9]{1,3}ca/, 84 | /il[0-9]{1,3}pi[0-9]{1,3}va/, 85 | /il[0-9]{1,3}pi[0-9]{1,3}ps/, 86 | /il[0-9]{1,3}pi[0-9]{1,3}pr/, 87 | /il[0-9]{1,3}pi[0-9]{1,3}cd[0-9]{1,3}/, 88 | /il[0-9]{1,3}pi[0-9]{1,3}cm[0-9]{1,3}/, 89 | 90 | /promo[0-9]{1,3}id/, 91 | /promo[0-9]{1,3}nm/, 92 | /promo[0-9]{1,3}cr/, 93 | /promo[0-9]{1,3}ps/ 94 | ], 95 | parametersMap: { 96 | "protocolVersion": "v", 97 | "trackingId": "tid", 98 | "webPropertyId": "tid", 99 | "anonymizeIp": "aip", 100 | "dataSource": "ds", 101 | "queueTime": "qt", 102 | "cacheBuster": "z", 103 | "clientId": "cid", 104 | "userId": "uid", 105 | "sessionControl": "sc", 106 | "ipOverride": "uip", 107 | "userAgentOverride": "ua", 108 | "documentReferrer": "dr", 109 | "campaignName": "cn", 110 | "campaignSource": "cs", 111 | "campaignMedium": "cm", 112 | "campaignKeyword": "ck", 113 | "campaignContent": "cc", 114 | "campaignId": "ci", 115 | "googleAdwordsId": "gclid", 116 | "googleDisplayAdsId": "dclid", 117 | "screenResolution": "sr", 118 | "viewportSize": "vp", 119 | "documentEncoding": "de", 120 | "screenColors": "sd", 121 | "userLanguage": "ul", 122 | "javaEnabled": "je", 123 | "flashVersion": "fl", 124 | "hitType": "t", 125 | "non-interactionHit": "ni", 126 | "documentLocationUrl": "dl", 127 | "documentHostName": "dh", 128 | "documentPath": "dp", 129 | "documentTitle": "dt", 130 | "screenName": "cd", 131 | "linkId": "linkid", 132 | "applicationName": "an", 133 | "applicationId": "aid", 134 | "applicationVersion": "av", 135 | "applicationInstallerId": "aiid", 136 | "eventCategory": "ec", 137 | "eventAction": "ea", 138 | "eventLabel": "el", 139 | "eventValue": "ev", 140 | "transactionId": "ti", 141 | "transactionAffiliation": "ta", 142 | "transactionRevenue": "tr", 143 | "transactionShipping": "ts", 144 | "transactionTax": "tt", 145 | "itemName": "in", 146 | "itemPrice": "ip", 147 | "itemQuantity": "iq", 148 | "itemCode": "ic", 149 | "itemCategory": "iv", 150 | "currencyCode": "cu", 151 | "socialNetwork": "sn", 152 | "socialAction": "sa", 153 | "socialActionTarget": "st", 154 | "userTimingCategory": "utc", 155 | "userTimingVariableName": "utv", 156 | "userTimingTime": "utt", 157 | "userTimingLabel": "utl", 158 | "pageLoadTime": "plt", 159 | "dnsTime": "dns", 160 | "pageDownloadTime": "pdt", 161 | "redirectResponseTime": "rrt", 162 | "tcpConnectTime": "tcp", 163 | "serverResponseTime": "srt", 164 | "domInteractiveTime": "dit", 165 | "contentLoadTime": "clt", 166 | "exceptionDescription": "exd", 167 | "isExceptionFatal": "exf", 168 | "isExceptionFatal?": "exf", 169 | "experimentId": "xid", 170 | "experimentVariant": "xvar" 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /app/transferlist/transfer-totals.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | window $ document */ 3 | 4 | import { BaseScript, SettingsEntry } from '../core'; 5 | 6 | import './style/transfer-totals.scss'; 7 | 8 | export class TransferTotalsSettings extends SettingsEntry { 9 | static id = 'transfer-totals'; 10 | constructor() { 11 | super('transfer-totals', 'Transfer list totals', null); 12 | 13 | this.addSetting('Show transfer list totals', 'show-transfer-totals', true, 'checkbox'); 14 | } 15 | } 16 | 17 | class TransferTotals extends BaseScript { 18 | constructor() { 19 | super(TransferTotalsSettings.id); 20 | 21 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 22 | this._observer = new MutationObserver(this._mutationHandler.bind(this)); 23 | } 24 | 25 | activate(state) { 26 | super.activate(state); 27 | 28 | const obsConfig = { 29 | childList: true, 30 | characterData: true, 31 | attributes: false, 32 | subtree: true, 33 | }; 34 | 35 | setTimeout(() => { 36 | this._observer.observe($(document)[0], obsConfig); 37 | }, 0); 38 | } 39 | 40 | deactivate(state) { 41 | super.deactivate(state); 42 | this._observer.disconnect(); 43 | } 44 | 45 | _mutationHandler(mutationRecords) { 46 | const settings = this.getSettings(); 47 | mutationRecords.forEach((mutation) => { 48 | if ( 49 | $(mutation.target).find('.listFUTItem').length > 0 || 50 | $(mutation.target).find('.futbin').length > 0 51 | ) { 52 | const controller = getAppMain() 53 | .getRootViewController() 54 | .getPresentedViewController() 55 | .getCurrentViewController() 56 | .getCurrentController(); 57 | if (!controller || !controller._listController) { 58 | return; 59 | } 60 | 61 | if (window.currentPage !== 'UTTransferListSplitViewController') { 62 | return; 63 | } 64 | 65 | if (!settings.isActive || settings['show-transfer-totals'].toString() !== 'true') { 66 | return; 67 | } 68 | 69 | const lists = $('.ut-transfer-list-view .itemList'); 70 | const items = controller._listController._viewmodel._collection; 71 | const listRows = $('.ut-transfer-list-view .listFUTItem'); 72 | 73 | lists.each((index, list) => { 74 | const totals = { 75 | futbin: 0, 76 | bid: 0, 77 | bin: 0, 78 | }; 79 | const listEl = $(list); 80 | 81 | if (!listEl.find('.listFUTItem').length) { 82 | return; 83 | } 84 | 85 | const firstIndex = $(list).find('.listFUTItem:first').index('.ut-transfer-list-view .listFUTItem'); 86 | const lastIndex = $(list).find('.listFUTItem:last').index('.ut-transfer-list-view .listFUTItem'); 87 | 88 | totals.futbin = items.slice(firstIndex, lastIndex + 1).reduce((sum, item, i) => { 89 | const futbin = parseInt( 90 | listRows.eq(i + firstIndex) 91 | .find('.auctionValue.futbin .coins.value') 92 | .text() 93 | .replace(/[,.]/g, ''), 94 | 10, 95 | ) || 0; 96 | return sum + futbin; 97 | }, 0); 98 | totals.bid = items.slice(firstIndex, lastIndex + 1) 99 | .reduce((sum, item) => { 100 | const { currentBid, startingBid } = item._auction; 101 | const actualBid = currentBid > 0 ? currentBid : startingBid; 102 | return sum + actualBid; 103 | }, 0); 104 | totals.bin = items.slice(firstIndex, lastIndex + 1) 105 | .reduce((sum, item) => sum + item._auction.buyNowPrice, 0); 106 | 107 | const totalsItem = listEl.prev('.transfer-totals'); 108 | 109 | if (!totalsItem.length) { 110 | $(`
111 |
112 |
113 | Futbin BIN 114 | 0 115 |
116 |
117 | Bid Total 118 | 0 119 |
120 |
121 | BIN Total 122 | 0 123 |
124 |
125 |
`).insertBefore(listEl); 126 | } 127 | 128 | if (totals.futbin > 0) { 129 | totalsItem.find('.total-futbin').text(totals.futbin); 130 | totalsItem.find('.futbin').show(); 131 | } else { 132 | totalsItem.find('.futbin').hide(); 133 | } 134 | totalsItem.find('.total-bin').text(totals.bin); 135 | totalsItem.find('.total-bid').text(totals.bid); 136 | }); 137 | } 138 | }); 139 | } 140 | } 141 | 142 | new TransferTotals(); // eslint-disable-line no-new 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FUT Web App - TamperMonkey scripts 2 | 3 | [![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) 4 | 5 | ## :warning: Discontinued 6 | **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.** 7 | 8 | **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.** 9 | 10 | **This project has been a great journey for me, and I'm very thankful for all the support of the community over the years.** 11 | 12 | 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. 13 | 14 | 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. 15 | 16 | :warning: Using this script is at your own risk. EA might (temp-)ban you for altering parts of their Web App. 17 | 18 | :bangbang: Do not request autobuyer features. Because they are considered to be cheating, it will not be added. 19 | 20 | I started this project to learn about reverse engineering big Javascript codebases. 21 | 22 | If you benefit from this project, you can buy me a beer :beers: :+1: 23 | 24 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/timklingeleers) 25 | 26 | ## Features 27 | - [x] Futbin integration 28 | - [x] Show Futbin prices on all player cards throughout the app 29 | - [x] Show link to player on Futbin 30 | - [x] Mark bargains (BIN price lower then Futbin value) 31 | - [x] Find minimum BIN value of players 32 | - [x] Refresh transfer list 33 | - [x] Increase transfer list size 34 | - [x] Extra card information (contracts) 35 | - [x] Total coin value for cards on the transfer list 36 | 37 | ## Installation 38 | Make sure you have user scripts enabled in your browser (these instructions refer to the latest versions of the browser): 39 | 40 | * Firefox - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=firefox). :warning: Has issues loading properly (see issue #115) 41 | * Chrome - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=chrome). 42 | * Opera - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=opera). 43 | * Safari - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=safari). 44 | * Dolphin - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=dolphin). 45 | * UC Browser - install [Tampermonkey](https://tampermonkey.net/?ext=dhdg&browser=ucweb). 46 | 47 | ### Install scripts 48 | 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). 49 | 50 | ## Feature requests 51 | 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. 52 | 53 | ## Issues 54 | File a bug report in the [issue list][issue-list]. 55 | 56 | ## Developing 57 | Clone this repository and execute: 58 | ``` 59 | npm install 60 | ``` 61 | 62 | To start the bundling process and linting process, execute: 63 | ``` 64 | npm start 65 | ``` 66 | 67 | Make sure to enable `Allow access to file URLs` in `chrome://extensions/` for Tampermonkey, and add the following script snippet: 68 | ``` 69 | // ==UserScript== 70 | // @name FUT Enhancer dev 71 | // @version 0.1 72 | // @description 73 | // @license MIT 74 | // @author Tim Klingeleers 75 | // @match https://www.easports.com/fifa/ultimate-team/web-app/* 76 | // @match https://www.easports.com/*/fifa/ultimate-team/web-app/* 77 | // @match https://www.ea.com/fifa/ultimate-team/web-app/* 78 | // @match https://www.ea.com/*/fifa/ultimate-team/web-app/* 79 | // @namespace https://github.com/Mardaneus86 80 | // @supportURL https://github.com/Mardaneus86/futwebapp-tampermonkey/issues 81 | // @grant GM_notification 82 | // @grant GM_xmlhttpRequest 83 | // @grant GM_getValue 84 | // @grant GM_setValue 85 | // @grant window.focus 86 | // @require file:////dist/fut-enhancer.user.js 87 | // @connect ea.com 88 | // @connect futbin.com 89 | // ==/UserScript== 90 | ``` 91 | 92 | 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. 93 | 94 | ## Contribute 95 | 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. 96 | 97 | [issue-list]: https://github.com/Mardaneus86/futwebapp-tampermonkey/issues 98 | [install-script]: https://openuserjs.org/install/Mardaneus86/FUT_Enhancer.user.js 99 | -------------------------------------------------------------------------------- /app/futbin/futbin-player-links.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | window $ document 3 | */ 4 | import { analytics, BaseScript, Database } from '../core'; 5 | import { FutbinSettings } from './settings-entry'; 6 | 7 | export class FutbinPlayerLinks extends BaseScript { 8 | constructor() { 9 | super(FutbinSettings.id); 10 | 11 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 12 | this._observer = new MutationObserver(this._mutationHandler.bind(this)); 13 | 14 | this._playerPrices = []; 15 | } 16 | 17 | activate(state) { 18 | super.activate(state); 19 | 20 | const obsConfig = { 21 | childList: true, 22 | characterData: true, 23 | attributes: false, 24 | subtree: true, 25 | }; 26 | 27 | setTimeout(() => { 28 | this._observer.observe($(document)[0], obsConfig); 29 | }, 0); 30 | } 31 | 32 | deactivate(state) { 33 | super.deactivate(state); 34 | 35 | $('#futbinPlayerLink').remove(); 36 | 37 | this._observer.disconnect(); 38 | } 39 | 40 | _mutationHandler(mutationRecords) { 41 | mutationRecords.forEach(function (mutation) { 42 | if ($(mutation.target).hasClass('DetailView') && $(mutation.target) 43 | .find('.DetailPanel') && mutation.addedNodes.length > 0) { 44 | if (this.getSettings()['show-link-to-player'].toString() !== 'true') { 45 | return; 46 | } 47 | 48 | let selectedItem = this._getSelectedItem(); 49 | if (selectedItem == null || selectedItem.resourceId === 0) { 50 | return; 51 | } 52 | 53 | const futbinPlayerLink = $(mutation.target).find('#futbinPlayerLink'); 54 | futbinPlayerLink.remove(); 55 | 56 | $(mutation.target).find('.DetailPanel > .ut-button-group').prepend(``); 57 | 58 | $('#futbinPlayerLink').bind('click', async () => { 59 | let btn = $('#futbinPlayerLink'); 60 | btn.find('.btn-text').html('Searching on Futbin ...'); 61 | const futbinLink = await FutbinPlayerLinks._getFutbinPlayerUrl(selectedItem); 62 | 63 | selectedItem = this._getSelectedItem(); 64 | btn = $('#futbinPlayerLink'); 65 | if (btn.data('resource-id') === selectedItem.resourceId) { 66 | if (futbinLink) { 67 | btn.find('.btn-text').html('View on Futbin'); 68 | analytics.trackEvent('Futbin', 'Show player on Futbin', btn.data('resource-id')); 69 | window.open(futbinLink); 70 | } else { 71 | btn.find('.btn-text').html('No exact Futbin player found'); 72 | } 73 | } 74 | }); 75 | } 76 | }, this); 77 | } 78 | 79 | static _getFutbinPlayerUrl(item) { 80 | return new Promise((resolve) => { 81 | if (!item._staticData) { 82 | return resolve(null); 83 | } 84 | 85 | let futbinPlayerIds = Database.getJson('futbin-player-ids', []); 86 | const futbinPlayer = futbinPlayerIds.find(i => i.id === item.resourceId); 87 | if (futbinPlayer != null) { 88 | return resolve(`https://www.futbin.com/21/player/${futbinPlayer.futbinId}`); 89 | } 90 | 91 | const name = `${item._staticData.firstName} ${item._staticData.lastName}`.replace(' ', '+'); 92 | const url = `https://www.futbin.com/search?year=21&term=${name}`; 93 | return GM_xmlhttpRequest({ 94 | method: 'GET', 95 | url, 96 | onload: (res) => { 97 | if (res.status !== 200) { 98 | return resolve(null); 99 | } 100 | const players = JSON.parse(res.response); 101 | let exactPlayers = players.filter(p => 102 | parseInt(p.rating, 10) === parseInt(item.rating, 10)); 103 | if (exactPlayers.length > 1) { 104 | exactPlayers = exactPlayers.filter(p => 105 | p.rare_type === item.rareflag.toString() && 106 | p.club_image.endsWith(`/${item.teamId}.png`)); 107 | } 108 | if (exactPlayers.length === 1) { 109 | futbinPlayerIds = Database.getJson('futbin-player-ids', []); 110 | if (futbinPlayerIds.find(i => i.id === item.resourceId) == null) { 111 | futbinPlayerIds.push({ 112 | id: item.resourceId, 113 | futbinId: exactPlayers[0].id, 114 | }); 115 | } 116 | Database.setJson('futbin-player-ids', futbinPlayerIds); 117 | return resolve(`https://www.futbin.com/21/player/${exactPlayers[0].id}`); 118 | } else if (exactPlayers.length > 1) { 119 | // Take first one, several players are returned more than once 120 | return resolve(`https://www.futbin.com/21/player/${exactPlayers[0].id}`); 121 | } 122 | 123 | return resolve(null); // TODO: what should we do if we find more than one? 124 | }, 125 | }); 126 | }); 127 | } 128 | 129 | /* eslint-disable class-methods-use-this */ 130 | _getSelectedItem() { 131 | const listController = getAppMain().getRootViewController() 132 | .getPresentedViewController().getCurrentViewController() 133 | .getCurrentController()._listController; 134 | if (listController) { 135 | return listController.getIterator().current(); 136 | } 137 | 138 | const currentController = getAppMain().getRootViewController() 139 | .getPresentedViewController().getCurrentViewController() 140 | .getCurrentController()._rightController._currentController; 141 | if (currentController && currentController._viewmodel) { 142 | const current = currentController._viewmodel.current(); 143 | 144 | return current._item ? current._item : current; 145 | } 146 | 147 | return null; 148 | } 149 | /* eslint-enable class-methods-use-this */ 150 | } 151 | 152 | new FutbinPlayerLinks(); // eslint-disable-line no-new 153 | -------------------------------------------------------------------------------- /app/transferlist/min-bin.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | window $ document 3 | */ 4 | import { analytics, BaseScript, SettingsEntry } from '../core'; 5 | import { TransferMarket, priceTiers } from '../../fut'; 6 | 7 | export class MinBinSettings extends SettingsEntry { 8 | static id = 'min-bin'; 9 | constructor() { 10 | super('min-bin', 'Search minimum BIN'); 11 | 12 | this.addSetting('Amount of lowest BINs to determine minimum on', 'mean-count', 3, 'number'); 13 | this.addSetting('Adjust quicklist panel price automatically based on minimum BIN', 'adjust-list-price', true, 'checkbox'); 14 | this.addSettingUnder('adjust-list-price', 'Start price percentage (0 to 100%)', 'start-price-percentage', 90, 'number'); 15 | this.addSettingUnder('adjust-list-price', 'Buy now price percentage (0 to 100%)', 'buy-now-price-percentage', 110, 'number'); 16 | } 17 | } 18 | 19 | class MinBin extends BaseScript { 20 | constructor() { 21 | super(MinBinSettings.id); 22 | 23 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 24 | this._observer = new MutationObserver(this._mutationHandler.bind(this)); 25 | 26 | this._playerPrices = []; 27 | } 28 | 29 | activate(state) { 30 | super.activate(state); 31 | 32 | const obsConfig = { 33 | childList: true, 34 | characterData: true, 35 | attributes: false, 36 | subtree: true, 37 | }; 38 | 39 | setTimeout(() => { 40 | this._observer.observe($(document)[0], obsConfig); 41 | }, 0); 42 | } 43 | 44 | deactivate(state) { 45 | super.deactivate(state); 46 | 47 | this._observer.disconnect(); 48 | } 49 | 50 | _mutationHandler(mutationRecords) { 51 | mutationRecords.forEach(function (mutation) { 52 | if ($(mutation.target).hasClass('DetailView') && $(mutation.target) 53 | .find('.DetailPanel') && mutation.addedNodes.length > 0) { 54 | const searchMinBin = $(mutation.target).find('#searchMinBin'); 55 | searchMinBin.remove(); 56 | 57 | let selectedItem = this._getSelectedItem(); 58 | 59 | if (selectedItem == null || selectedItem.resourceId === 0) { 60 | return; 61 | } 62 | const knownPlayerPrice = this._playerPrices 63 | .find(p => p.resourceId === selectedItem.resourceId); 64 | let price = ''; 65 | if (knownPlayerPrice != null) { 66 | price = `(${knownPlayerPrice.minimumBin})`; 67 | 68 | this._updateListPrice(knownPlayerPrice.minimumBin); 69 | } 70 | $(mutation.target).find('.DetailPanel > .ut-button-group').prepend(``); 71 | 72 | $('#searchMinBin').bind('click', async () => { 73 | const btn = $('#searchMinBin'); 74 | btn.find('.btn-text').html('Searching minimum BIN...'); 75 | analytics.trackEvent('Min BIN', 'Search Min BIN', btn.data('resource-id')); 76 | const settings = this.getSettings(); 77 | const minimumBin = await new TransferMarket().searchMinBuy(selectedItem, parseInt(settings['mean-count'], 10)); 78 | const playerPrice = this._playerPrices.find(p => p.resourceId === btn.data('resource-id')); 79 | if (playerPrice != null) { 80 | this._playerPrices.splice(this._playerPrices.indexOf(playerPrice), 1); 81 | } 82 | this._playerPrices.push({ 83 | resourceId: btn.data('resource-id'), 84 | minimumBin, 85 | }); 86 | 87 | selectedItem = this._getSelectedItem(); 88 | 89 | let notificationText = `Minimum BIN found for ${selectedItem._staticData.name} is ${minimumBin}`; 90 | if (btn.data('resource-id') === selectedItem.resourceId) { 91 | if (minimumBin === 0) { 92 | btn.find('.btn-text').html('Search minimum BIN (extinct)'); 93 | notificationText = `Minimum BIN not found for ${selectedItem._staticData.name}, card may be extinct`; 94 | } else { 95 | btn.find('.btn-text').html(`Search minimum BIN (${minimumBin})`); 96 | 97 | this._updateListPrice(minimumBin); 98 | } 99 | } 100 | 101 | GM_notification({ 102 | text: notificationText, 103 | title: 'FUT 21 Web App', 104 | timeout: 5000, 105 | onclick: () => window.focus(), 106 | }); 107 | }); 108 | } 109 | }, this); 110 | } 111 | 112 | _updateListPrice(minimumBin) { 113 | const settings = this.getSettings(); 114 | const quicklistPanel = getAppMain().getRootViewController() 115 | .getPresentedViewController() 116 | .getCurrentViewController() 117 | .getCurrentController() 118 | ._rightController._currentController._quickListPanel; 119 | 120 | if (settings['adjust-list-price'] && quicklistPanel) { 121 | const quicklistpanelView = quicklistPanel._view; 122 | 123 | const listPrice = priceTiers.determineListPrice( 124 | minimumBin * (settings['start-price-percentage'] / 100), 125 | minimumBin * (settings['buy-now-price-percentage'] / 100), 126 | ); 127 | 128 | if (quicklistPanel._item) { 129 | // sets the values when the quicklistpanel hasn't been initialized 130 | const auction = quicklistPanel._item._auction; 131 | if (auction.tradeState === 'closed') { 132 | // item is sold 133 | return; 134 | } 135 | if (auction.tradeState !== 'active') { 136 | auction.startingBid = listPrice.start; 137 | auction.buyNowPrice = listPrice.buyNow; 138 | quicklistPanel._item.setAuctionData(auction); 139 | } 140 | } 141 | 142 | const bidSpinner = quicklistpanelView._bidNumericStepper; 143 | const buySpinner = quicklistpanelView._buyNowNumericStepper; 144 | bidSpinner.setValue(listPrice.start); 145 | buySpinner.setValue(listPrice.buyNow); 146 | } 147 | } 148 | 149 | /* eslint-disable class-methods-use-this */ 150 | _getSelectedItem() { 151 | const listController = getAppMain().getRootViewController() 152 | .getPresentedViewController() 153 | .getCurrentViewController() 154 | .getCurrentController()._listController; 155 | if (listController) { 156 | return listController.getIterator().current(); 157 | } 158 | 159 | const detailController = getAppMain().getRootViewController() 160 | .getPresentedViewController() 161 | .getCurrentViewController() 162 | .getCurrentController()._rightController; 163 | if (detailController && detailController._currentController._viewmodel) { 164 | const current = detailController 165 | ._currentController._viewmodel.current(); 166 | 167 | return current._item ? current._item : current; 168 | } 169 | 170 | return null; 171 | } 172 | /* eslint-enable class-methods-use-this */ 173 | } 174 | 175 | new MinBin(); // eslint-disable-line no-new 176 | -------------------------------------------------------------------------------- /app/settings/html/index/settings.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

FUT Tampermonkey settings 7 | 8 |
9 | 11 | 12 | 13 |
14 |

15 | 16 | 17 |
18 | 19 |
20 |

Need help?

21 |

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

26 |

27 |
28 |

Enjoying this plugin?

29 |

Consider a donation so this plugin can keep being improved.

30 |

31 | 33 | 34 | 35 |

36 |
37 |
38 | -------------------------------------------------------------------------------- /fut/transferMarket.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | enums factories communication gUserModel models repositories services 3 | */ 4 | import { mean } from 'math-statistics'; 5 | 6 | import utils from './utils'; 7 | import priceTiers from './priceTiers'; 8 | import { Logger } from './logger'; 9 | import { PinEvent } from './pinEvent'; 10 | import { ListItemError } from './errors'; 11 | 12 | export class TransferMarket { 13 | _logger = new Logger(); 14 | 15 | /* eslint-disable class-methods-use-this */ 16 | async navigateToTransferHub() { 17 | await PinEvent.sendPageView('Hub - Transfers'); 18 | } 19 | 20 | async navigateToTransferList() { 21 | await this.navigateToTransferHub(); 22 | await PinEvent.sendPageView('Transfer List - List View'); 23 | } 24 | /* eslint-enable class-methods-use-this */ 25 | 26 | async searchMinBuy(item, itemsForMean = 3, lowUp = false) { 27 | services.Item.clearTransferMarketCache(); 28 | 29 | this._logger.log(`Searching min buy for ${item.type} ${item._staticData.name} from low upward first ${lowUp}`, 'Core - Transfermarket'); 30 | let minBuy = 0; 31 | 32 | if (lowUp) { 33 | minBuy = await this._findLowUp(item, itemsForMean); 34 | this._logger.log(`Low up search yielded ${minBuy} as a result`, 'Core - Transfermarket'); 35 | } 36 | 37 | if (minBuy === 0) { 38 | this._logger.log('Searching low down...', 'Core - Transfermarket'); 39 | minBuy = await this._findLowDown(item, itemsForMean); 40 | } 41 | 42 | if (minBuy === 0) { 43 | this._logger.log('No players found... it might be extinct', 'Core - Transfermarket'); 44 | } else { 45 | this._logger.log(`Min buy for ${item.type} ${item._staticData.name} is ${minBuy}`, 'Core - Transfermarket'); 46 | } 47 | return minBuy; 48 | } 49 | 50 | /** 51 | * List item on transfermarket 52 | * 53 | * @param {FUTItem} item 54 | * @param {number} start start price 55 | * @param {number} buyNow buy now price 56 | * @param {number} duration time to list in seconds (1, 3, 6, 12, 24 or 72 hours) 57 | */ 58 | async listItem(item, start, buyNow, duration = 3600) { 59 | return new Promise(async (resolve, reject) => { 60 | if (gUserModel.getTradeAccess() !== models.UserModel.TRADE_ACCESS.WHITELIST) { 61 | reject(new Error('You are not authorized for trading')); 62 | return; 63 | } 64 | 65 | const prices = priceTiers.determineListPrice(start, buyNow); 66 | 67 | await this.sendToTradePile(item); 68 | await utils.sleep(1000); 69 | 70 | const listItem = new communication.ListItemDelegate({ 71 | itemId: item.id, 72 | startingBid: prices.start, 73 | buyNowPrice: prices.buyNow, 74 | duration, 75 | }); 76 | listItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { 77 | sender.clearListenersByScope(this); 78 | resolve({ 79 | startingBid: prices.start, 80 | buyNowPrice: prices.buyNow, 81 | }); 82 | }); 83 | listItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { 84 | sender.clearListenersByScope(this); 85 | reject(new ListItemError(response)); 86 | }); 87 | listItem.send(); 88 | }); 89 | } 90 | 91 | sendToTradePile(item) { 92 | return new Promise((resolve, reject) => { 93 | const moveItem = new communication.MoveItemDelegate([item], enums.FUTItemPile.TRANSFER); 94 | moveItem.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { 95 | sender.clearListenersByScope(this); 96 | resolve(); 97 | }); 98 | moveItem.addListener(communication.BaseDelegate.FAIL, this, (sender, response) => { 99 | sender.clearListenersByScope(this); 100 | reject(new Error(response)); 101 | }); 102 | moveItem.send(); 103 | }); 104 | } 105 | 106 | relistAllItems() { 107 | return new Promise((resolve, reject) => { 108 | if (gUserModel.getTradeAccess() !== models.UserModel.TRADE_ACCESS.WHITELIST) { 109 | reject(new Error('You are not authorized for trading')); 110 | return; 111 | } 112 | 113 | const relistExpired = new communication.AuctionRelistDelegate(); 114 | 115 | relistExpired.addListener(communication.BaseDelegate.SUCCESS, this, (sender) => { 116 | sender.clearListenersByScope(this); 117 | repositories.Item.setDirty(enums.FUTItemPile.TRANSFER); 118 | resolve(); 119 | }); 120 | 121 | relistExpired.addListener(communication.BaseDelegate.FAIL, this, (sender, error) => { 122 | sender.clearListenersByScope(this); 123 | reject(new Error(error)); 124 | }); 125 | relistExpired.execute(); 126 | }); 127 | } 128 | 129 | async _findLowUp(item, itemsForMean) { 130 | const searchCriteria = this._defineSearchCriteria(item, 200); 131 | await PinEvent.sendPageView('Transfer Market Search'); 132 | await utils.sleep(3000); 133 | await PinEvent.sendPageView('Transfer Market Results - List View', 0); 134 | await PinEvent.sendPageView('Item - Detail View', 0); 135 | const items = await this._find(searchCriteria); 136 | if (items.length > itemsForMean) { 137 | // we find more than X listed at this price, so it must be low value 138 | return 200; 139 | } 140 | 141 | return 0; // trigger searching low down 142 | } 143 | 144 | async _findLowDown(item, itemsForMean) { 145 | let minBuy = 99999999; 146 | const searchCriteria = this._defineSearchCriteria(item); 147 | 148 | let valuesFound = []; 149 | for (let minBuyFound = false; minBuyFound === false;) { 150 | /* eslint-disable no-await-in-loop */ 151 | await PinEvent.sendPageView('Transfer Market Search'); 152 | await utils.sleep(800); 153 | await PinEvent.sendPageView('Transfer Market Results - List View', 0); 154 | await PinEvent.sendPageView('Item - Detail View', 0); 155 | const items = await this._find(searchCriteria); 156 | /* eslint-enable no-await-in-loop */ 157 | if (items.length > 0) { 158 | valuesFound = valuesFound.concat(items.map(i => i._auction.buyNowPrice)); 159 | 160 | const minBuyOnPage = Math.min(...items.map(i => i._auction.buyNowPrice)); 161 | if (minBuyOnPage < minBuy) { 162 | minBuy = minBuyOnPage; 163 | if (items.length < searchCriteria.count) { 164 | minBuyFound = true; 165 | break; 166 | } 167 | searchCriteria.maxBuy = priceTiers.roundDownToNearestPriceTiers(minBuy); 168 | if (searchCriteria.maxBuy < 200) { 169 | searchCriteria.maxBuy = 200; 170 | } 171 | } else if (items.length === searchCriteria.count) { 172 | if (searchCriteria.maxBuy === 0) { 173 | searchCriteria.maxBuy = minBuy; 174 | } else { 175 | searchCriteria.maxBuy = priceTiers.roundDownToNearestPriceTiers(searchCriteria.maxBuy); 176 | } 177 | if (searchCriteria.maxBuy < 200) { 178 | searchCriteria.maxBuy = 200; 179 | minBuy = 200; 180 | minBuyFound = true; 181 | } 182 | } else { 183 | minBuy = Math.min(...items.map(i => i._auction.buyNowPrice)); 184 | minBuyFound = true; 185 | } 186 | } else { 187 | minBuyFound = true; 188 | } 189 | } 190 | 191 | valuesFound = valuesFound.sort((a, b) => a - b).slice(0, itemsForMean); 192 | 193 | if (valuesFound.length > 0) { 194 | return priceTiers.roundValueToNearestPriceTiers(mean(valuesFound)); 195 | } 196 | 197 | return 0; // player extinct 198 | } 199 | 200 | /* eslint-disable class-methods-use-this */ 201 | _defineSearchCriteria(item, maxBuy = -1) { 202 | // TODO: check if this can handle other items as well 203 | // eslint-disable-next-line no-undef 204 | const searchCriteria = new UTSearchCriteriaDTO(); 205 | 206 | searchCriteria.count = 30; 207 | searchCriteria.maskedDefId = item.getMaskedResourceId(); 208 | searchCriteria.type = item.type; 209 | 210 | if (item.rareflag === 47) { // 47 = Champions 211 | // if it is a Champions card, this is seen as a gold card 212 | // Can only search for "Gold" in this case 213 | searchCriteria.level = factories.DataProvider.getItemLevelDP(true) 214 | .filter(d => d.id === 2)[0].value; 215 | } else if (item.rareflag >= 3) { // 3 = TOTW 216 | // if it is TOTW or other special, set it to TOTW. See enums.ItemRareType. 217 | // Can only search for "Specials", not more specific on Rare Type 218 | searchCriteria.level = factories.DataProvider.getItemLevelDP(true) 219 | .filter(d => d.id === 3)[0].value; 220 | } 221 | 222 | searchCriteria.category = enums.SearchCategory.ANY; 223 | searchCriteria.position = enums.SearchType.ANY; 224 | if (maxBuy !== -1) { 225 | searchCriteria.maxBuy = maxBuy; 226 | } 227 | 228 | return searchCriteria; 229 | } 230 | /* eslint-enable class-methods-use-this */ 231 | 232 | _find(searchCriteria) { 233 | return new Promise((resolve, reject) => { 234 | services.Item.searchTransferMarket(searchCriteria, 1).observe( 235 | this, 236 | function (obs, res) { 237 | if (!res.success) { 238 | obs.unobserve(this); 239 | reject(res.status); 240 | } else { 241 | resolve(res.data.items); 242 | } 243 | }, 244 | ); 245 | }); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /analytics/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import querystring from 'querystring'; 3 | 4 | import config from './config'; 5 | 6 | module.exports = init; 7 | 8 | function init (tid, cid, options) { 9 | return new Visitor(tid, cid, options); 10 | } 11 | 12 | var Visitor = module.exports.Visitor = function (tid, cid, options) { 13 | 14 | this._queue = []; 15 | 16 | this.options = options || {}; 17 | 18 | if(this.options.hostname) { 19 | config.hostname = this.options.hostname; 20 | } 21 | if(this.options.path) { 22 | config.path = this.options.path; 23 | } 24 | 25 | if(this.options.enableBatching !== undefined) { 26 | config.batching = options.enableBatching; 27 | } 28 | 29 | if(this.options.batchSize) { 30 | config.batchSize = this.options.batchSize; 31 | } 32 | 33 | this._context = {}; 34 | this._persistentParams = {}; 35 | 36 | this.tid = this.options.tid; 37 | this.cid = this.options.cid; 38 | if(this.options.uid) { 39 | this.uid = this.options.uid; 40 | } 41 | } 42 | 43 | Visitor.prototype = { 44 | reset: function () { 45 | this._context = null; 46 | return this; 47 | }, 48 | 49 | set: function (key, value) { 50 | this._persistentParams = this._persistentParams || {}; 51 | this._persistentParams[key] = value; 52 | }, 53 | 54 | pageview: function (path, hostname, title, params, fn) { 55 | 56 | if (typeof path === 'object' && path != null) { 57 | params = path; 58 | if (typeof hostname === 'function') { 59 | fn = hostname 60 | } 61 | path = hostname = title = null; 62 | } else if (typeof hostname === 'function') { 63 | fn = hostname 64 | hostname = title = null; 65 | } else if (typeof title === 'function') { 66 | fn = title; 67 | title = null; 68 | } else if (typeof params === 'function') { 69 | fn = params; 70 | params = null; 71 | } 72 | 73 | params = this._translateParams(params); 74 | 75 | params = Object.assign({}, this._persistentParams || {}, params); 76 | 77 | params.dp = path || params.dp || this._context.dp; 78 | params.dh = hostname || params.dh || this._context.dh; 79 | params.dt = title || params.dt || this._context.dt; 80 | 81 | this._tidyParameters(params); 82 | 83 | if (!params.dp && !params.dl) { 84 | return this._handleError('Please provide either a page path (dp) or a document location (dl)', fn); 85 | } 86 | 87 | return this._withContext(params)._enqueue('pageview', params, fn); 88 | }, 89 | 90 | 91 | screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) { 92 | 93 | if (typeof screenName === 'object' && screenName != null) { 94 | params = screenName; 95 | if (typeof appName === 'function') { 96 | fn = appName 97 | } 98 | screenName = appName = appVersion = appId = appInstallerId = null; 99 | } else if (typeof appName === 'function') { 100 | fn = appName 101 | appName = appVersion = appId = appInstallerId = null; 102 | } else if (typeof appVersion === 'function') { 103 | fn = appVersion; 104 | appVersion = appId = appInstallerId = null; 105 | } else if (typeof appId === 'function') { 106 | fn = appId; 107 | appId = appInstallerId = null; 108 | } else if (typeof appInstallerId === 'function') { 109 | fn = appInstallerId; 110 | appInstallerId = null; 111 | } else if (typeof params === 'function') { 112 | fn = params; 113 | params = null; 114 | } 115 | 116 | params = this._translateParams(params); 117 | 118 | params = Object.assign({}, this._persistentParams || {}, params); 119 | 120 | params.cd = screenName || params.cd || this._context.cd; 121 | params.an = appName || params.an || this._context.an; 122 | params.av = appVersion || params.av || this._context.av; 123 | params.aid = appId || params.aid || this._context.aid; 124 | params.aiid = appInstallerId || params.aiid || this._context.aiid; 125 | 126 | this._tidyParameters(params); 127 | 128 | if (!params.cd || !params.an) { 129 | return this._handleError('Please provide at least a screen name (cd) and an app name (an)', fn); 130 | } 131 | 132 | return this._withContext(params)._enqueue('screenview', params, fn); 133 | }, 134 | 135 | 136 | event: function (category, action, label, value, params, fn) { 137 | 138 | if (typeof category === 'object' && category != null) { 139 | params = category; 140 | if (typeof action === 'function') { 141 | fn = action 142 | } 143 | category = action = label = value = null; 144 | } else if (typeof label === 'function') { 145 | fn = label; 146 | label = value = null; 147 | } else if (typeof value === 'function') { 148 | fn = value; 149 | value = null; 150 | } else if (typeof params === 'function') { 151 | fn = params; 152 | params = null; 153 | } 154 | 155 | params = this._translateParams(params); 156 | 157 | params = Object.assign({}, this._persistentParams || {}, params); 158 | 159 | params.ec = category || params.ec || this._context.ec; 160 | params.ea = action || params.ea || this._context.ea; 161 | params.el = label || params.el || this._context.el; 162 | params.ev = value || params.ev || this._context.ev; 163 | params.p = params.p || params.dp || this._context.p || this._context.dp; 164 | 165 | delete params.dp; 166 | this._tidyParameters(params); 167 | 168 | if (!params.ec || !params.ea) { 169 | return this._handleError('Please provide at least an event category (ec) and an event action (ea)', fn); 170 | } 171 | 172 | return this._withContext(params)._enqueue('event', params, fn); 173 | }, 174 | 175 | 176 | transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) { 177 | if (typeof transaction === 'object') { 178 | params = transaction; 179 | if (typeof revenue === 'function') { 180 | fn = revenue 181 | } 182 | transaction = revenue = shipping = tax = affiliation = null; 183 | } else if (typeof revenue === 'function') { 184 | fn = revenue; 185 | revenue = shipping = tax = affiliation = null; 186 | } else if (typeof shipping === 'function') { 187 | fn = shipping; 188 | shipping = tax = affiliation = null; 189 | } else if (typeof tax === 'function') { 190 | fn = tax; 191 | tax = affiliation = null; 192 | } else if (typeof affiliation === 'function') { 193 | fn = affiliation; 194 | affiliation = null; 195 | } else if (typeof params === 'function') { 196 | fn = params; 197 | params = null; 198 | } 199 | 200 | params = this._translateParams(params); 201 | 202 | params = Object.assign({}, this._persistentParams || {}, params); 203 | 204 | params.ti = transaction || params.ti || this._context.ti; 205 | params.tr = revenue || params.tr || this._context.tr; 206 | params.ts = shipping || params.ts || this._context.ts; 207 | params.tt = tax || params.tt || this._context.tt; 208 | params.ta = affiliation || params.ta || this._context.ta; 209 | params.p = params.p || this._context.p || this._context.dp; 210 | 211 | this._tidyParameters(params); 212 | 213 | if (!params.ti) { 214 | return this._handleError('Please provide at least a transaction ID (ti)', fn); 215 | } 216 | 217 | return this._withContext(params)._enqueue('transaction', params, fn); 218 | }, 219 | 220 | 221 | item: function (price, quantity, sku, name, variation, params, fn) { 222 | if (typeof price === 'object') { 223 | params = price; 224 | if (typeof quantity === 'function') { 225 | fn = quantity 226 | } 227 | price = quantity = sku = name = variation = null; 228 | } else if (typeof quantity === 'function') { 229 | fn = quantity; 230 | quantity = sku = name = variation = null; 231 | } else if (typeof sku === 'function') { 232 | fn = sku; 233 | sku = name = variation = null; 234 | } else if (typeof name === 'function') { 235 | fn = name; 236 | name = variation = null; 237 | } else if (typeof variation === 'function') { 238 | fn = variation; 239 | variation = null; 240 | } else if (typeof params === 'function') { 241 | fn = params; 242 | params = null; 243 | } 244 | 245 | params = this._translateParams(params); 246 | 247 | params = Object.assign({}, this._persistentParams || {}, params); 248 | 249 | params.ip = price || params.ip || this._context.ip; 250 | params.iq = quantity || params.iq || this._context.iq; 251 | params.ic = sku || params.ic || this._context.ic; 252 | params.in = name || params.in || this._context.in; 253 | params.iv = variation || params.iv || this._context.iv; 254 | params.p = params.p || this._context.p || this._context.dp; 255 | params.ti = params.ti || this._context.ti; 256 | 257 | this._tidyParameters(params); 258 | 259 | if (!params.ti) { 260 | return this._handleError('Please provide at least an item transaction ID (ti)', fn); 261 | } 262 | 263 | return this._withContext(params)._enqueue('item', params, fn); 264 | 265 | }, 266 | 267 | exception: function (description, fatal, params, fn) { 268 | 269 | if (typeof description === 'object') { 270 | params = description; 271 | if (typeof fatal === 'function') { 272 | fn = fatal; 273 | } 274 | description = fatal = null; 275 | } else if (typeof fatal === 'function') { 276 | fn = fatal; 277 | fatal = 0; 278 | } else if (typeof params === 'function') { 279 | fn = params; 280 | params = null; 281 | } 282 | 283 | params = this._translateParams(params); 284 | 285 | params = Object.assign({}, this._persistentParams || {}, params); 286 | 287 | params.exd = description || params.exd || this._context.exd; 288 | params.exf = +!!(fatal || params.exf || this._context.exf); 289 | 290 | if (params.exf === 0) { 291 | delete params.exf; 292 | } 293 | 294 | this._tidyParameters(params); 295 | 296 | return this._withContext(params)._enqueue('exception', params, fn); 297 | }, 298 | 299 | timing: function (category, variable, time, label, params, fn) { 300 | 301 | if (typeof category === 'object') { 302 | params = category; 303 | if (typeof variable === 'function') { 304 | fn = variable; 305 | } 306 | category = variable = time = label = null; 307 | } else if (typeof variable === 'function') { 308 | fn = variable; 309 | variable = time = label = null; 310 | } else if (typeof time === 'function') { 311 | fn = time; 312 | time = label = null; 313 | } else if (typeof label === 'function') { 314 | fn = label; 315 | label = null; 316 | } else if (typeof params === 'function') { 317 | fn = params; 318 | params = null; 319 | } 320 | 321 | params = this._translateParams(params); 322 | 323 | params = Object.assign({}, this._persistentParams || {}, params); 324 | 325 | params.utc = category || params.utc || this._context.utc; 326 | params.utv = variable || params.utv || this._context.utv; 327 | params.utt = time || params.utt || this._context.utt; 328 | params.utl = label || params.utl || this._context.utl; 329 | 330 | this._tidyParameters(params); 331 | 332 | return this._withContext(params)._enqueue('timing', params, fn); 333 | }, 334 | 335 | 336 | send: function (fn) { 337 | var self = this; 338 | var count = 1; 339 | var fn = fn || function () {}; 340 | 341 | var getBody = function(params) { 342 | return params.map(function(x) { return querystring.stringify(x); }).join('\n'); 343 | } 344 | 345 | var onFinish = function (err) { 346 | fn.call(self, err || null, count - 1); 347 | } 348 | 349 | var iterator = function () { 350 | if (!self._queue.length) { 351 | return onFinish(null); 352 | } 353 | var params = []; 354 | 355 | if(config.batching) { 356 | params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize)); 357 | } else { 358 | params.push(self._queue.shift()); 359 | } 360 | 361 | var useBatchPath = params.length > 1; 362 | 363 | var path = config.hostname + (useBatchPath ? config.batchPath :config.path); 364 | 365 | var options = Object.assign({}, self.options.requestOptions, { 366 | body: getBody(params), 367 | headers: self.options.headers || {} 368 | }); 369 | 370 | GM_xmlhttpRequest({ 371 | method: 'POST', 372 | url: path, 373 | headers: options.headers, 374 | data: options.body, 375 | onload: function () { 376 | nextIteration() 377 | }, 378 | onerror: function(res) { 379 | nextIteration(res.status); 380 | } 381 | }); 382 | } 383 | 384 | function nextIteration(err) { 385 | if (err) return onFinish(err); 386 | iterator(); 387 | } 388 | 389 | iterator(); 390 | 391 | }, 392 | 393 | _enqueue: function (type, params, fn) { 394 | 395 | if (typeof params === 'function') { 396 | fn = params; 397 | params = {}; 398 | } 399 | 400 | params = this._translateParams(params) || {}; 401 | 402 | Object.assign(params, { 403 | v: config.protocolVersion, 404 | tid: this.tid, 405 | cid: this.cid, 406 | t: type 407 | }); 408 | if(this.uid) { 409 | params.uid = this.uid; 410 | } 411 | 412 | this._queue.push(params); 413 | 414 | if (fn) { 415 | this.send(fn); 416 | } 417 | 418 | return this; 419 | }, 420 | 421 | 422 | _handleError: function (message, fn) { 423 | fn && fn.call(this, new Error(message)) 424 | return this; 425 | }, 426 | 427 | _translateParams: function (params) { 428 | var translated = {}; 429 | for (var key in params) { 430 | if (config.parametersMap.hasOwnProperty(key)) { 431 | translated[config.parametersMap[key]] = params[key]; 432 | } else { 433 | translated[key] = params[key]; 434 | } 435 | } 436 | return translated; 437 | }, 438 | 439 | _tidyParameters: function (params) { 440 | for (var param in params) { 441 | if (params[param] === null || params[param] === undefined) { 442 | delete params[param]; 443 | } 444 | } 445 | return params; 446 | }, 447 | 448 | _withContext: function (context) { 449 | var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams); 450 | visitor._queue = this._queue; 451 | return visitor; 452 | } 453 | 454 | 455 | } 456 | 457 | Visitor.prototype.pv = Visitor.prototype.pageview 458 | Visitor.prototype.e = Visitor.prototype.event 459 | Visitor.prototype.t = Visitor.prototype.transaction 460 | Visitor.prototype.i = Visitor.prototype.item 461 | -------------------------------------------------------------------------------- /app/futbin/futbin-prices.js: -------------------------------------------------------------------------------- 1 | /* globals 2 | $ 3 | window 4 | */ 5 | 6 | import { utils } from '../../fut'; 7 | import { BaseScript, Database } from '../core'; 8 | import { FutbinSettings } from './settings-entry'; 9 | 10 | export class FutbinPrices extends BaseScript { 11 | constructor() { 12 | super(FutbinSettings.id); 13 | this._squadObserver = null; 14 | } 15 | 16 | activate(state) { 17 | super.activate(state); 18 | 19 | this._show(state.screenId); 20 | } 21 | 22 | onScreenRequest(screenId) { 23 | super.onScreenRequest(screenId); 24 | 25 | const controllerName = getAppMain().getRootViewController() 26 | .getPresentedViewController().getCurrentViewController() 27 | .getCurrentController().className; 28 | 29 | if (screenId === 'SBCSquadSplitViewController' || 30 | screenId === 'SquadSplitViewController' || 31 | screenId === 'UTSquadSplitViewController' || 32 | screenId === 'UTSBCSquadSplitViewController') { 33 | if (this.getSettings()['show-sbc-squad'].toString() !== 'true') { 34 | return; 35 | } 36 | 37 | this._squadObserver = getAppMain().getRootViewController() 38 | .getPresentedViewController().getCurrentViewController() 39 | .getCurrentController()._leftController._squad.onDataUpdated 40 | .observe(this, () => { 41 | $('.squadSlotPedestal.futbin').remove(); // forces update 42 | this._show('SBCSquadSplitViewController', true); 43 | }); 44 | if ($('.ut-squad-summary-info--right.ut-squad-summary-info').find('.futbin').length === 0) { 45 | $('.ut-squad-summary-info--right.ut-squad-summary-info').append(` 46 |
47 | Total BIN value 48 |
49 | --- 50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 | `); 58 | 59 | $('.refresh-squad-button').click(() => { 60 | Database.set('lastFutbinFetchFail', 0); 61 | this.onScreenRequest(screenId); 62 | }); 63 | } 64 | } else if (this._squadObserver !== null && 65 | controllerName !== 'SBCSquadSplitViewController' && 66 | controllerName !== 'SquadSplitViewController' && 67 | controllerName !== 'UTSquadSplitViewController' && 68 | controllerName !== 'UTSBCSquadSplitViewController') { 69 | this._squadObserver.unobserve(this); 70 | } 71 | 72 | this._show(screenId); 73 | } 74 | 75 | deactivate(state) { 76 | super.deactivate(state); 77 | 78 | $('.futbin').remove(); 79 | 80 | if (this._squadObserver !== null) { 81 | this._squadObserver.unobserve(this); 82 | } 83 | 84 | if (this._intervalRunning) { 85 | clearInterval(this._intervalRunning); 86 | } 87 | } 88 | 89 | _show(screen, force = false) { 90 | const showFutbinPricePages = [ 91 | // Players 92 | 'UTTransferListSplitViewController', // transfer list 93 | 'UTWatchListSplitViewController', // transfer targets 94 | 'UTUnassignedItemsSplitViewController', // pack buy 95 | 'ClubSearchResultsSplitViewController', // club 96 | 'UTMarketSearchResultsSplitViewController', // market search 97 | 'UTPlayerPicksViewController', 98 | // Squad 99 | 'SBCSquadSplitViewController', 100 | 'SquadSplitViewController', 101 | 'UTSquadSplitViewController', 102 | 'UTSBCSquadSplitViewController', 103 | 'UTTOTWSquadSplitViewController', 104 | ]; 105 | 106 | if (showFutbinPricePages.indexOf(screen) !== -1) { 107 | if (this._intervalRunning) { 108 | clearInterval(this._intervalRunning); 109 | } 110 | if (screen === 'SBCSquadSplitViewController' || 111 | screen === 'SquadSplitViewController' || 112 | screen === 'UTSquadSplitViewController' || 113 | screen === 'UTSquadsHubViewController' || 114 | screen === 'UTSBCSquadSplitViewController' || 115 | screen === 'UTTOTWSquadSplitViewController') { 116 | this.loadFutbinPrices(showFutbinPricePages, screen, force); 117 | } else { 118 | this._intervalRunning = setInterval(() => { 119 | this.loadFutbinPrices(showFutbinPricePages, screen, force); 120 | }, 1000); 121 | } 122 | } else { 123 | // no need to search prices on other pages 124 | // reset page 125 | if (this._intervalRunning) { 126 | clearInterval(this._intervalRunning); 127 | } 128 | this._intervalRunning = null; 129 | } 130 | } 131 | 132 | loadFutbinPrices(showFutbinPricePages, screen, force) { 133 | const lastFutbinFetchFail = Database.get('lastFutbinFetchFail', 0); 134 | if (lastFutbinFetchFail + (5 * 60000) > Date.now()) { 135 | 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 136 | if (this._intervalRunning) { 137 | clearInterval(this._intervalRunning); 138 | } 139 | return; 140 | } 141 | if (showFutbinPricePages.indexOf(window.currentPage) === -1 && !force) { 142 | if (this._intervalRunning) { 143 | clearInterval(this._intervalRunning); 144 | } 145 | return; 146 | } 147 | const controller = getAppMain().getRootViewController() 148 | .getPresentedViewController().getCurrentViewController() 149 | .getCurrentController(); 150 | 151 | let uiItems = null; 152 | if (screen === 'SBCSquadSplitViewController' || 153 | screen === 'SquadSplitViewController' || 154 | screen === 'UTSquadSplitViewController' || 155 | screen === 'UTSquadsHubViewController' || 156 | screen === 'UTSBCSquadSplitViewController' || 157 | screen === 'UTTOTWSquadSplitViewController') { 158 | uiItems = $(controller._view.__root).find('.squadSlot'); 159 | 160 | if (this.getSettings()['show-sbc-squad'].toString() !== 'true') { 161 | return; 162 | } 163 | } else { 164 | uiItems = $(getAppMain().getRootViewController() 165 | .getPresentedViewController().getCurrentViewController() 166 | ._view.__root).find('.listFUTItem'); 167 | } 168 | 169 | if ($(uiItems[0]).find('.futbin').length > 0) { 170 | return; 171 | } 172 | 173 | let listController = null; 174 | if (screen === 'SBCSquadSplitViewController' || 175 | screen === 'SquadSplitViewController' || 176 | screen === 'UTSquadSplitViewController' || 177 | screen === 'UTSBCSquadSplitViewController' || 178 | screen === 'UTTOTWSquadSplitViewController') { 179 | // not needed 180 | } else if (screen === 'UTPlayerPicksViewController') { 181 | if (!controller.getPresentedViewController()) { 182 | return; 183 | } 184 | if ($(controller.getPresentedViewController()._view.__root).find('.futbin').length > 0) { 185 | // Futbin prices already shown 186 | return; 187 | } 188 | listController = controller.getPresentedViewController(); 189 | } else if (screen === 'UTUnassignedItemsSplitViewController' || screen === 'UTWatchListSplitViewController') { 190 | if (!controller || 191 | !controller._leftController || 192 | !controller._leftController._view) { 193 | return; 194 | } 195 | listController = controller._leftController; 196 | } else { 197 | if (!controller || 198 | !controller._listController || 199 | !controller._listController._view) { 200 | return; // only run if data is available 201 | } 202 | listController = controller._listController; 203 | } 204 | 205 | let listrows = null; 206 | if (screen === 'SBCSquadSplitViewController' || 207 | screen === 'SquadSplitViewController' || 208 | screen === 'UTSquadSplitViewController' || 209 | screen === 'UTSBCSquadSplitViewController' || 210 | screen === 'UTTOTWSquadSplitViewController') { 211 | listrows = controller._squad._players.slice(0, 11).map((p, index) => ( 212 | { 213 | data: p._item, 214 | target: controller._view._lView._slotViews[index].__root, 215 | })); 216 | } else if (listController._picks && screen === 'UTPlayerPicksViewController') { 217 | listrows = listController._picks.map((pick, index) => ( 218 | { 219 | data: pick, 220 | target: listController._view._playerPickViews[index].__root, 221 | })); 222 | } else if (listController._view._list && 223 | listController._view._list.listRows && 224 | listController._view._list.listRows.length > 0) { 225 | listrows = listController._view._list.listRows; // for transfer market and club search 226 | } else if (listController._view._sections && 227 | listController._view._sections.length > 0) { // for transfer list & trade pile 228 | listController._view._sections.forEach((row) => { 229 | if (row.listRows.length > 0) { 230 | if (listrows == null) { 231 | listrows = row.listRows; 232 | } else { 233 | listrows = listrows.concat(row.listRows); 234 | } 235 | } 236 | }); 237 | } 238 | 239 | if (listrows === null) { 240 | return; 241 | } 242 | 243 | const showBargains = (this.getSettings()['show-bargains'].toString() === 'true'); 244 | 245 | const resourceIdMapping = []; 246 | 247 | listrows 248 | .filter(row => row.data.type === 'player' && row.data.resourceId !== 0) 249 | .forEach((row, index) => { 250 | $(row.__auction).show(); 251 | resourceIdMapping.push({ 252 | target: uiItems[index] || row.target, 253 | playerId: row.data.resourceId, 254 | item: row.data, 255 | }); 256 | }); 257 | 258 | let fetchedPlayers = 0; 259 | const fetchAtOnce = 30; 260 | const futbinlist = []; 261 | while (resourceIdMapping.length > 0 && fetchedPlayers < resourceIdMapping.length && Database.get('lastFutbinFetchFail', 0) + (5 * 60000) < Date.now()) { 262 | const futbinUrl = `https://www.futbin.com/21/playerPrices?player=&rids=${ 263 | resourceIdMapping.slice(fetchedPlayers, fetchedPlayers + fetchAtOnce) 264 | .map(i => i.playerId) 265 | .filter((current, next) => current !== next && current !== 0) 266 | .join(',') 267 | }`; 268 | fetchedPlayers += fetchAtOnce; 269 | /* eslint-disable no-loop-func */ 270 | GM_xmlhttpRequest({ 271 | method: 'GET', 272 | url: futbinUrl, 273 | onload: (res) => { 274 | if (res.status !== 200) { 275 | Database.set('lastFutbinFetchFail', Date.now()); 276 | 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'); 277 | return; 278 | } 279 | 280 | const futbinData = JSON.parse(res.response); 281 | resourceIdMapping.forEach((item) => { 282 | FutbinPrices._showFutbinPrice(screen, item, futbinData, showBargains); 283 | futbinlist.push(futbinData[item.playerId]); 284 | }); 285 | const platform = utils.getPlatform(); 286 | if (screen === 'SBCSquadSplitViewController' || 287 | screen === 'SquadSplitViewController' || 288 | screen === 'UTSquadSplitViewController' || 289 | screen === 'UTSBCSquadSplitViewController') { 290 | const futbinTotal = futbinlist.reduce( 291 | (sum, item) => 292 | sum + parseInt( 293 | item.prices[platform].LCPrice.toString().replace(/[,.]/g, ''), 294 | 10, 295 | ) || 0 296 | , 0, 297 | ); 298 | $('.ut-squad-summary-value.coins.value').html(`${futbinTotal.toLocaleString()}`); 299 | } 300 | }, 301 | }); 302 | } 303 | } 304 | static async _showFutbinPrice(screen, item, futbinData, showBargain) { 305 | if (!futbinData) { 306 | return; 307 | } 308 | const target = $(item.target); 309 | const { playerId } = item; 310 | 311 | if (target.find('.player').length === 0) { 312 | // not a player 313 | return; 314 | } 315 | 316 | const platform = utils.getPlatform(); 317 | 318 | if (!futbinData[playerId]) { 319 | return; // futbin data might not be available for this player 320 | } 321 | 322 | let targetForButton = null; 323 | 324 | if (target.find('.futbin').length > 0) { 325 | return; // futbin price already added to the row 326 | } 327 | 328 | const futbinText = 'Futbin BIN'; 329 | 330 | switch (screen) { 331 | case 'SBCSquadSplitViewController': 332 | case 'SquadSplitViewController': 333 | case 'UTSquadSplitViewController': 334 | case 'UTSBCSquadSplitViewController': 335 | case 'UTTOTWSquadSplitViewController': 336 | target.prepend(` 337 |
338 | ${futbinData[playerId].prices[platform].LCPrice || '---'} 339 |
`); 340 | break; 341 | case 'UTPlayerPicksViewController': 342 | target.prepend(` 343 |
344 | ${futbinText} 345 | ${futbinData[playerId].prices[platform].LCPrice || '---'} 346 |
`); 347 | break; 348 | case 'UTTransferListSplitViewController': 349 | case 'UTWatchListSplitViewController': 350 | case 'UTUnassignedItemsSplitViewController': 351 | case 'ClubSearchResultsSplitViewController': 352 | case 'UTMarketSearchResultsSplitViewController': 353 | $('.secondary.player-stats-data-component').css('float', 'left'); 354 | target.find('.auction').prepend(` 355 |
356 | ${futbinText} 357 | ${futbinData[playerId].prices[platform].LCPrice || '---'} 358 |
`); 359 | break; 360 | case 'SearchResults': 361 | targetForButton = target.find('.auctionValue').parent(); 362 | targetForButton.prepend(` 363 |
364 | ${futbinText} 365 | ${futbinData[playerId].prices[platform].LCPrice || '---'} 366 |
`); 367 | break; 368 | default: 369 | // no need to do anything 370 | } 371 | 372 | if (showBargain) { 373 | if (item.item._auction && 374 | item.item._auction.buyNowPrice < futbinData[playerId].prices[platform].LCPrice.toString().replace(/[,.]/g, '')) { 375 | target.addClass('futbin-bargain'); 376 | } 377 | } 378 | } 379 | } 380 | --------------------------------------------------------------------------------