Last active
March 15, 2024 11:46
-
-
Save DerekZiemba/41c7049b721fe3c12e8d5144d346e343 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name tradeogre-tampermonkey-script | |
// @author Derek Ziemba | |
// @version 2024.03.18 | |
// @match https://tradeogre.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=tradeogre.com | |
// @updateUrl https://gist.github.com/DerekZiemba/41c7049b721fe3c12e8d5144d346e343.js | |
// @downloadUrl https://gist.github.com/DerekZiemba/41c7049b721fe3c12e8d5144d346e343.js | |
//// @require file:///C:/Dropbox/Scripts/.js/ZZ5/websites/tradeogre/tradeogre.js | |
// @run-at document-start | |
// @grant GM_addStyle | |
// @grant GM_addElement | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant unsafeWindow | |
// ==/UserScript== | |
/** | |
* @typedef {HTMLElementTagNameMap & SVGElementTagNameMap} ElementTagMap | |
*/ | |
/** | |
* @typedef {`${keyof ElementTagMap}${' ' | '#' | '.' | '['}${string}` | `${string} ${keyof ElementTagMap}${' ' | '#' | '.' | '['}${string}` | keyof ElementTagMap} CssSelector | |
*/ | |
/** | |
* @interface ParentNode | |
*/ | |
/** | |
* @method ParentNode.querySelector | |
* @param {CssSelector} selectors | |
* @returns {ElementTagMap[keyof ElementTagMap]} | |
*/ | |
/** | |
* @method ParentNode.querySelectorAll | |
* @param {CssSelector} selectors | |
* @returns {NodeListOf<ElementTagMap[keyof ElementTagMap]>} | |
*/ | |
(()=>{ | |
/**@template T */ | |
extendType(/**@type {Array<T>}*/(Array.prototype), { | |
/**@typedef {(this: Array<T>, ...items: T[])=>boolean} pushUnique*/ | |
/**@type {pushUnique} */ | |
pushUnique(...items) { | |
var result = false; | |
for (var len = items.length, i = 0; i < len; i++) { | |
if (!this.includes(items[i])) { | |
this.push(items[i]); | |
result = true; | |
} | |
} | |
return result; | |
}, | |
/**@typedef {(this: Array<T>, item: T)=>boolean} removeItem */ | |
/**@type {removeItem} */ | |
removeItem(item) { | |
var idx = this.indexOf(item); | |
if (idx >= 0) { | |
this.splice(idx, 1); | |
while ((idx = this.indexOf(item))>=0) { this.splice(idx, 1); } | |
return true; | |
} | |
return false; | |
}, | |
/**@typedef {(this: Array<T>)=>Array<T>} distinct */ | |
/**@type {distinct} */ | |
distinct() { | |
const result = []; | |
for (let len = this.length, i = 0; i < len; ++i) { | |
const val = this[i]; | |
if (!result.includes(val)) { result.push(val); } | |
} | |
return result; | |
}, | |
}); | |
/**@template {Element} T */ | |
extendType(/**@type {NodeListOf<T>}*/(NodeList.prototype), { | |
toArray() { return Array.from(this); }, | |
filter: Array.prototype.filter, | |
map: Array.prototype.map, | |
at: Array.prototype.at, | |
}); | |
/**@template {Element} T */ | |
extendType(/**@type {HTMLCollectionOf<T>}*/(HTMLCollection.prototype), { | |
toArray() { return Array.from(this); }, | |
forEach: Array.prototype.forEach, | |
at: Array.prototype.at, | |
get values() { return this[Symbol.iterator]; } | |
}); | |
/**@type {<T extends object, S extends object>(target: T, source: S)=>asserts target is T&S} */ | |
function extendType(target, source) { | |
var entries = Array.isArray(source) ? source : Object.entries(Object.getOwnPropertyDescriptors(source)); | |
for(var [name, desc] of entries) { | |
if (!(name in target)) { | |
desc.configurable = true; | |
desc.enumerable = false; | |
if (desc.writable === true && typeof desc.value === 'function') { | |
desc.writable = false; | |
} | |
Object.defineProperty(target, name, desc); | |
} | |
} | |
}; | |
})(); | |
var { html, css } = (()=>{ return { html, css }; | |
function html(strings, ...values) { | |
const html = zipStringsAndValues(strings, values).join(''); | |
const template = document.createElement('template'); | |
template.innerHTML = html; | |
const fragment = template.content; | |
return /**@type {HTMLElement&DocumentFragment}*/(fragment.childElementCount === 1 ? fragment.firstElementChild : fragment); | |
} | |
function css(strings, ...values) { | |
const html = zipStringsAndValues(strings, values).join(''); | |
const styleElement = document.createElement('style'); | |
styleElement.innerHTML = html; | |
return styleElement; | |
} | |
function zipStringsAndValues(strings, values) { | |
const destination = []; | |
let lenS=strings.length, iS=0; | |
for(let lenV = values.length, iV=0; iS<lenS && iV<lenV; iV++, iS++) { | |
destination.push(strings[iS]); | |
Array.isArray(values[iV]) ? destination.push(...values[iV]) : destination.push(values[iV]); | |
} | |
for(; iS<lenS; iS++) { destination.push(strings[iS]); } | |
return destination; | |
} | |
})(); | |
var { deferredPromise } = (()=>{ | |
/** @template T @typedef {Promise<T>&{ resolve(x: T): void; reject(x: any): void; }} DeferredPromise */ | |
var savedArgs = null; | |
var steal = (...args)=>{ savedArgs = args; } | |
/**@type {<T>(x?: Pick<Promise<T>,'then'>)=>DeferredPromise<T>} */ | |
function deferredPromise(x) { | |
const prom = /**@type {DeferredPromise<any>} */(new Promise(steal)); | |
[prom.resolve, prom.reject] = savedArgs; savedArgs = null; | |
if (x && typeof x.then === 'function') { | |
x.then(prom.resolve, prom.reject); | |
} | |
return prom; | |
} | |
return { deferredPromise }; | |
})(); | |
var { documentInteractive, documentLoaded, documentComplete } = (()=>{ | |
var documentComplete = deferredPromise(); | |
var documentLoaded = deferredPromise(documentComplete); | |
var documentInteractive = deferredPromise(documentLoaded); | |
var checkReadyStateChange = (ev)=> { | |
var readyState = document.readyState; | |
if (readyState === 'complete') { | |
documentComplete.resolve(document); | |
} else if (readyState === 'interactive') { | |
if (ev && ev.type === 'DOMContentLoaded') { | |
documentLoaded.resolve(document); | |
} else { | |
documentInteractive.resolve(document); | |
} | |
} | |
}; | |
document.addEventListener('DOMContentLoaded', checkReadyStateChange); | |
document.addEventListener('readystatechange', checkReadyStateChange); | |
documentComplete.then((doc)=>{ | |
doc.removeEventListener('DOMContentLoaded', checkReadyStateChange); | |
doc.removeEventListener('readystatechange', checkReadyStateChange); | |
}); | |
checkReadyStateChange(); | |
return { documentInteractive, documentLoaded, documentComplete }; | |
})(); | |
const storage = (()=>{ | |
/**@type {<K extends string, T>(key: K, defaultV: T)=>((newValue?: T)=>Promise<T>)} */ | |
function createAccessor(key, defaultValue) { | |
return async function accessor(value) { | |
if (value!==undefined) { | |
return GM.setValue(key, value); | |
} else { | |
return (await GM.getValue(key)) ?? defaultValue; | |
} | |
}; | |
} | |
return { | |
shortcuts: createAccessor('navbar-crypto-shortcuts', { valid: /**@type {string[]} */([]), invalid: /**@type {string[]} */([]) }) | |
}; | |
})(); | |
GM.addStyle(css` | |
#main > #main2 { | |
padding: 0; | |
> #chartc { | |
height: 55vh !important; | |
#marketinfo, #chart, & > * { | |
height: 100%; | |
} | |
#marketinfo { | |
min-width: unset; | |
} | |
} | |
> div { | |
> div[class*=container] { | |
> .bordered { | |
.ps:is(.ordersbody,.historybody)[style*="height:"] { | |
height: 29vh !important; | |
} | |
} | |
} | |
} | |
} | |
#main > #main2 > .exchange { | |
padding: 0 4px; | |
margin-top: -8px; | |
max-width: unset; | |
height: 100vh; | |
padding-bottom: 35px; | |
overflow: hidden; | |
> .orderbook { | |
.orderbookorder { | |
.orderbookitem { | |
width: auto; | |
} | |
} | |
} | |
> .userorders { | |
.userorderrow { | |
border-bottom: 1px ridge #7f7f7f7f; | |
padding: 2px 10px; | |
} | |
} | |
> .chart { | |
#klinechartcontainer { | |
padding-left: 10px; | |
> div { | |
> div:first-child { | |
> div:first-child { | |
} | |
} | |
} | |
} | |
.chartbuttonscontainer { | |
gap: 3px; | |
> .chartbutton { | |
margin: 0px 1px; | |
padding: 1px 6px; | |
} | |
} | |
} | |
> .marketinfo { | |
position: absolute; | |
width: calc(50% + 420px); | |
right: 0; | |
padding-top: 5px; | |
} | |
@media (orientation: landscape) and (width > 1440px) { | |
grid-template-columns: minmax(300px,1fr) minmax(285px,1fr) minmax(auto,3fr) minmax(auto,3fr) minmax(300px,3fr); | |
grid-template-rows: 0 minmax(90px, 4fr) minmax(50px, 3fr) minmax(225px, 2fr) 20px; | |
> .orderbook { | |
grid-column: 1/2; | |
grid-row: 3/6; | |
margin-top: -8px; | |
} | |
> .marketinfo { | |
grid-column: 3/5; | |
grid-row: 1/1; | |
} | |
> .chart { | |
grid-column: 2/6; | |
grid-row: 2/4; | |
} | |
> .order { | |
grid-column: 2/3; | |
grid-row: 4/5; | |
} | |
> .markets { | |
grid-column: 5/6; | |
grid-row: 4/6; | |
} | |
> .trades, #market-history { | |
grid-column: 1/2; | |
grid-row: 2/3; | |
} | |
> .usertrades { | |
grid-column: 4/5; | |
grid-row: 4/6; | |
} | |
> .userorders { | |
grid-column: 3/4; | |
grid-row: 4/6; | |
} | |
> .chart { | |
> #klinechartcontainer > div > div:first-child > div:first-child { | |
/* top: calc(7vh + 30px) !important; */ | |
} | |
> .chartbuttonscontainer { | |
flex-direction: row; | |
/* margin-top: 0; */ | |
padding-left: 10%; | |
margin-left: -250px; | |
padding-left: 12%; | |
} | |
} | |
} | |
@media ((orientation: portrait) and (width >= 980px) ) or ((width <= 1440px) and (height <=1400px)){ | |
grid-template-columns: minmax(290px,3fr) minmax(310px,3fr) minmax(auto,6fr); | |
grid-template-rows: 5px minmax(50px, 6fr) minmax(240px, 1fr) minmax(100px, 1fr) minmax(100px, 1fr) 20px; | |
> .orderbook { | |
grid-column: 1/2; | |
grid-row: 3/6; | |
} | |
> .marketinfo { | |
grid-column: 1/3; | |
grid-row: 1/1; | |
} | |
> .chart { | |
grid-column: 1/4; | |
grid-row: 2/3; | |
} | |
> .order { | |
grid-column: 2/3; | |
grid-row: 3/4; | |
} | |
> .markets { | |
grid-column: 3/4; | |
grid-row: 5/6; | |
} | |
> .trades { | |
grid-column: 2/3; | |
grid-row: 4/6; | |
} | |
> .usertrades { | |
grid-column: 3/4; | |
grid-row: 4/5; | |
} | |
> .userorders { | |
grid-column: 3/4; | |
grid-row: 3/4; | |
} | |
> .chart { | |
> #klinechartcontainer > div > div:first-child > div:first-child { | |
padding-left: 20px !important; | |
} | |
> .chartbuttonscontainer { | |
} | |
} | |
} | |
@media (min-width: 761px) and (max-width: 980px), (min-width: 590px) and (max-width: 761px) { | |
grid-template-rows: minmax(50px, 1fr) minmax(250px, 9fr) minmax(225px, 2fr) minmax(240px, 3fr) auto minmax(100px, 2fr) minmax(100px, 2fr) minmax(125px, 1fr) !important; | |
padding-bottom: 60px; | |
> .trades { | |
display: none | |
} | |
> .chart { | |
> #klinechartcontainer > div > div:first-child > div:first-child { | |
padding-left: 20px !important; | |
} | |
> .chartbuttonscontainer { | |
} | |
} | |
} | |
} | |
`.innerHTML).catch(console.error); | |
documentInteractive.then(async ()=>{ | |
const rnav = html` | |
<ul id="rnav"> | |
<style> | |
#navbar { | |
ul#lnav { | |
li { | |
&:has(a[href='/markets']) { | |
display: none; | |
} | |
} | |
} | |
ul#rnav { | |
width: calc(100% - 40px); | |
display: flex; | |
justify-content: space-between; | |
[name=left] ul { | |
@media (max-width: 980px) { | |
display: none; | |
} | |
} | |
[name=right] ul { | |
@media (max-width: 592px) { | |
display: none; | |
} | |
} | |
} | |
> div { | |
position:unset !important; | |
margin-left:0px !important; | |
} | |
} | |
</style> | |
<li name='left'> | |
<ul> | |
<!-- | |
Markets/Cryptots the user has visited are added here as navbar shortcuts | |
after any discovered USDT markets from the /account/balances table. | |
Sorted by users USD balance of that Crypto in descending order. | |
+ | |
--> | |
</ul> | |
</li> | |
<li name='right'> | |
<ul> | |
<li> | |
<a class="" href="/account/orders"> | |
<span class="fa fa-list-alt fa-lg fa-fw"></span> | |
<span style="">Orders</span> | |
</a> | |
</li> | |
<li> | |
<a class="" href="/account/history/deposits"> | |
<span class="fa fa-cloud-download fa-lg fa-fw"></span> | |
<span style="">Deposits</span> | |
</a> | |
</li> | |
<li> | |
<a class="" href="/account/balances"> | |
<span class="fa fa-money fa-lg fa-fw"></span> | |
<span style="">Balances</span> | |
</a> | |
</li> | |
</ul> | |
</li> | |
</ul>`; | |
document.getElementById('lnav').insertAdjacentElement('afterend', rnav); | |
const shortcuts = await storage.shortcuts(); | |
let spaceAvailable = (rnav.clientWidth*0.8) - rnav.children.right.clientWidth - rnav.children.left.clientWidth; | |
for(let sym of shortcuts.valid) { | |
if (spaceAvailable < 0) { break; } | |
let li = html`<li><a href="/exchange/${sym}">${sym.split('-').shift()}</a></li>`; | |
rnav.children.left.firstElementChild.append(li); | |
spaceAvailable -= li.clientWidth; | |
} | |
}).catch(console.error); | |
if (location.pathname.startsWith('/account/balances')) { | |
documentInteractive.then(()=>{ | |
const table = /**@type {HTMLTableElement} */(document.querySelector('table#coins')); | |
if (!table) { return; } | |
betterTable(); | |
new MutationObserver(betterTable).observe(table, { childList: true, subtree: true }); | |
async function betterTable() { | |
const shortcuts = await storage.shortcuts(); | |
addUSDTMarketLinksToTable(table, shortcuts); | |
updateNavBarShortcuts(table, shortcuts); | |
} | |
function addUSDTMarketLinksToTable(/**@type {HTMLTableElement} */table, /**@type {Awaited<ReturnType<typeof storage['shortcuts']>>} */shortcuts) { | |
const symbolIndex = Array.from(table.querySelectorAll('thead > tr > th')).findIndex(x=>x.textContent.includes('Symbol')); | |
const symbolColumns = table.querySelectorAll(`tbody > tr > td:nth-child(${symbolIndex+1}):not(:has(a))`); | |
for (let td of symbolColumns) { | |
let sym = td.textContent; | |
let exch = sym + '-USDT'; | |
if (!shortcuts.invalid.includes(exch)) { | |
td.classList.add('usdt_link'); | |
td.innerHTML = `<a name="${exch}" href="/exchange/${exch}">${sym}</a>`; | |
} | |
} | |
} | |
function updateNavBarShortcuts(/**@type {HTMLTableElement} */table, /**@type {Awaited<ReturnType<typeof storage['shortcuts']>>} */shortcuts) { | |
const isSortedByUSD = table?.querySelector('thead > tr > th.sorting_desc[aria-label^="Est. USD"]'); | |
if (!isSortedByUSD) { return; } | |
const isFullTable = table.rows.length > 15; | |
const isPageOne = document.querySelector('#coins_wrapper #coins_paginate ul.pagination .page-item.active:has(a[data-dt-idx="1"])'); | |
for(let rows = table.rows, len = rows.length, i = 1, count = 0; i < len; i++) { | |
let tr = rows[i]; | |
let currency = tr.querySelector('td.usdt_link a[name]')?.getAttribute('name'); | |
if (currency && !shortcuts.invalid.includes(currency)) { | |
if (isFullTable && isPageOne) { | |
shortcuts.valid.splice(count, 0, currency); | |
count++; | |
} else { | |
shortcuts.valid.pushUnique(currency); | |
} | |
} | |
} | |
shortcuts.valid = shortcuts.valid.distinct().slice(0, 15); | |
storage.shortcuts(shortcuts); | |
} | |
}); | |
} | |
if (location.pathname.startsWith('/exchange/')) { | |
documentComplete.then(()=>{ | |
var h1h2 = document.querySelectorAll('#main > #main2 > *:is(h1, h2)').map(x=>x.textContent.trim()).join('|'); | |
if (h1h2 == 'Error|Exchange not found') { | |
// Potential USDT markets are automatically added from the /account/balances table | |
// If we now find a USDT market doesn't exist for the crypto, remove & mark it as invalid | |
return storage.shortcuts().then((shortcuts)=>{ | |
const coin = location.pathname.split('/').pop(); | |
if (shortcuts.valid.removeItem(coin)) { | |
shortcuts.invalid.pushUnique(coin); | |
return storage.shortcuts(shortcuts); | |
} | |
}); | |
} else { | |
// if visiting a valid market, add it to the shortcuts | |
return storage.shortcuts().then((shortcuts)=>{ | |
const coin = location.pathname.split('/').pop(); | |
shortcuts.invalid.removeItem(coin); | |
shortcuts.valid.pushUnique(coin); | |
return storage.shortcuts(shortcuts); | |
}); | |
} | |
}).catch(console.error); | |
} | |
if (location.pathname.startsWith('/exchange/')) { | |
documentComplete.then(()=>{ | |
document.querySelector('.chart .chartbuttonscontainer .chartbutton.exbutton[data-interval="1h"]')?.click(); | |
}).catch(console.error); | |
} | |
Object.assign(GM, { html, css, deferredPromise, documentInteractive, documentLoaded, storage }); | |
unsafeWindow.GM = GM; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment