electron/mainRenderer.js
console.time('init')
const request = require("request")
const crypto = require("crypto")
const ReconnectingWebSocket = require('./vendor/rws.js')
const fs = require('fs')
const jwt = require('jsonwebtoken')
const Timer = require('easytimer.js');
const keytar = require('keytar')
const hideWindowManager = require("./hide-manager")
const { remote, ipcRenderer, shell, clipboard } = require('electron')
const { Menu, MenuItem } = remote
let browserWindow = remote.getCurrentWindow()
const activeWin = require("active-win")
const mousetrap = require('mousetrap')
function contextData(data) {
// find out what context we are in.
// return data for that context as a single string value.
if (data.showDraftStats) {
return appData.draftStats.map(card=>card.pretty_name).join('\n')
} else if (data.list_selected) {
return data.selected_list.map(
c=>(c.count_in_deck + ' ' + c.pretty_name + ' (' + c.set + ') ' + c.set_number)
).join('\n')
} else if (data.show_available_decklists) {
return data.player_decks.map( d=>d.pool_name ).join('\n')
} else if (data.draw_stats.length) {
// FIXME: Figure out where the set and card numbers are stored.
return data.draw_stats.map(
c=>(c.count_in_deck + ' ' + c.card)
).join('\n')
} else {
console.log('Contextual copy found no data.')
}
}
function copyData() {
// This should copy the data on the current screen onto the clipboard.
// The format should be context specific, comma or tab delimited seems
// like the best choice for most stuff.
// TODO: deck lists.
console.log('Copying relevant data to clipboard, if any.')
let dataString = contextData(appData)
if (dataString) {
clipboard.writeText(dataString)
}
}
function copyEventHandler(e) {
copyData()
}
var { rendererPreload } = require('electron-routes');
rendererPreload();
let addClickHandler = (selector,handler) => {
let new_handler = (e) => {
if (!$.contains($('.menu-div').get(0),event.target) && !$('#main-menu').hasClass('hide-me')){
e.preventDefault()
e.stopPropagation()
toggleMenu()
} else {
if ( handler != null){
handler(e)
}
}
}
$(selector).each( (i,elem) => {
$(elem).off().on('click',new_handler)
})
}
// poll for active window semi-regularly; if it's not MTGA or MTGATracker, minimize / unset alwaysontop
setInterval(() => {
if (appData.mtgaOverlayOnly) {
activeWin().then(win => {
if (win.owner.name == "MTGA.exe" || win.owner.name == "MTGATracker.exe" || win.title == "MTGA Tracker") {
if(!browserWindow.isAlwaysOnTop()) browserWindow.setAlwaysOnTop(true)
} else {
if(browserWindow.isAlwaysOnTop()) browserWindow.setAlwaysOnTop(false)
}
})
} else {
console.log("skipping overlay check and turning on always on top")
if(!browserWindow.isAlwaysOnTop()) browserWindow.setAlwaysOnTop(true)
}
}, 200)
window.addEventListener('beforeunload', function() {
ws.send("die")
})
let rightClickPosition = null
const contextMenu = new Menu()
const contextMenuItem = new MenuItem({
label: 'Inspect Element',
click: () => {
remote.getCurrentWindow().inspectElement(rightClickPosition.x, rightClickPosition.y)
}
})
contextMenu.append(contextMenuItem)
const API_URL = remote.getGlobal('API_URL');
var lastUsedUsername = null;
var debug = remote.getGlobal('debug');
var useFrame = remote.getGlobal('useFrame');
var staticMode = remote.getGlobal('staticMode');
var showIIDs = remote.getGlobal('showIIDs');
var showErrors = remote.getGlobal('showErrors');
var appVersionStr = remote.getGlobal('version');
var runFromSource = remote.getGlobal('runFromSource');
var showTotalWinLossCounter = remote.getGlobal('showTotalWinLossCounter');
var showDeckWinLossCounter = remote.getGlobal('showDeckWinLossCounter');
var showDailyTotalWinLossCounter = remote.getGlobal('showDailyTotalWinLossCounter');
var showDailyDeckWinLossCounter = remote.getGlobal('showDailyDeckWinLossCounter');
var showVaultProgress = remote.getGlobal('showVaultProgress');
var lastCollection = remote.getGlobal('lastCollection');
var lastVaultProgress = remote.getGlobal('lastVaultProgress');
var minVaultProgress = remote.getGlobal('minVaultProgress');
var sortMethod = remote.getGlobal('sortMethod');
var showChessTimers = remote.getGlobal('showChessTimers');
var hideDelay = remote.getGlobal('hideDelay');
var invertHideMode = remote.getGlobal('invertHideMode');
var rollupMode = remote.getGlobal('rollupMode');
var minToTray = remote.getGlobal('minToTray');
var recentCardsQuantityToShow = remote.getGlobal('recentCardsQuantityToShow');
var showGameTimer = remote.getGlobal('showGameTimer');
var zoom = remote.getGlobal('zoom');
var recentCards = remote.getGlobal('recentCards');
var port = remote.getGlobal('port');
var timerRunning = false;
var uploadDelay = 0;
var hideModeManager;
var showUIButtons = remote.getGlobal('showUIButtons')
var showHideButton = remote.getGlobal('showHideButton')
var showMenu = remote.getGlobal('showMenu')
var useMinimal = remote.getGlobal('useMinimal')
setInterval(() => {
uploadDelay -= 1
if (uploadDelay < 0) uploadDelay = 0;
}, 200)
var lastUseTheme = remote.getGlobal('useTheme')
var lastThemeFile = remote.getGlobal('themeFile')
var token = null;
keytar.getPassword("mtgatracker", "tracker-id-token").then(savedToken => {
token = savedToken;
})
if (debug) {
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
rightClickPosition = {x: e.x, y: e.y}
contextMenu.popup(remote.getCurrentWindow())
}, false)
}
var ws = new ReconnectingWebSocket(`ws://127.0.0.1:${port}/`, null, {constructor: WebSocket})
var gameLookup = {}
var lastGameState = null;
var resizing = false;
var overallTimer = new Timer();
var heroTimer = new Timer();
var opponentTimer = new Timer();
window.overallTimer = overallTimer
window.heroTimer = heroTimer
window.opponentTimer = opponentTimer
var winLossCounterInitial = remote.getGlobal("winLossCounter")
let getMainWindowDisplay = () => {
let {x, y} = browserWindow.getBounds()
let display = remote.screen.getDisplayNearestPoint({x: x, y: y})
return display;
}
let calcMainMaxHeight = () => {
let displayBounds = getMainWindowDisplay().bounds
let displayY= displayBounds.y
let displayHeight = displayBounds.height
let windowBounds = browserWindow.getBounds()
let windowY = windowBounds.y
let maxHeight = displayHeight - (windowY - displayY)
return maxHeight + 10; // add some buffer; 10px won't hide anything
}
var appData = {
deck_name: "loading...",
cant_connect: false,
showErrors: showErrors,
last_error: "",
error_count: 0,
debug: debug,
show_iids: showIIDs,
last_connect: 0,
last_connect_as_seconds: 0,
lastCollection: lastCollection,
lastVaultProgress: lastVaultProgress,
minVaultProgress: minVaultProgress,
game_in_progress: false,
showDraftStats: false,
draftStats: [],
game_complete: false,
game_dismissed: false,
show_available_decklists: true,
no_decks: false,
no_list_selected: true,
list_selected: false,
selected_list_size: "0",
selected_list: [],
selected_list_name: "",
player_decks: [],
total_cards_in_deck: "0",
draw_stats: [],
opponent_hand: [],
messages: [],
version: appVersionStr,
showTotalWinLossCounter: showTotalWinLossCounter,
showDeckWinLossCounter: showDeckWinLossCounter,
showDailyTotalWinLossCounter: showDailyTotalWinLossCounter,
showDailyDeckWinLossCounter: showDailyDeckWinLossCounter,
showDeckCounters: false,
winLossObj: winLossCounterInitial,
activeDeck: 'total',
totalWinCounter: winLossCounterInitial.alltime.total.win,
totalLossCounter: winLossCounterInitial.alltime.total.loss,
deckWinCounter: 0,
deckLossCounter: 0,
dailyTotalWinCounter: winLossCounterInitial.daily.total.win,
dailyTotalLossCounter: winLossCounterInitial.daily.total.win,
dailyDeckWinCounter: 0,
dailyDeckLossCounter: 0,
showVaultProgress: showVaultProgress,
showGameTimer: showGameTimer,
showChessTimers: showChessTimers,
hideDelay: hideDelay,
invertHideMode: invertHideMode,
rollupMode: rollupMode,
recentCardsQuantityToShow: recentCardsQuantityToShow,
recentCards: recentCards,
minToTray: minToTray,
showUIButtons: showUIButtons,
showHideButton: showHideButton,
showMenu: showMenu
}
var parseVersionString = (versionStr) => {
version = {}
version_parts = versionStr.split("-")
if (version_parts.length > 1)
version.suffix = version_parts[1]
version_bits = version_parts[0].split(".")
version.major = version_bits[0]
version.medium = version_bits[1]
version.minor = version_bits[2]
return version;
}
var addMessage = (message, link, mustfollow, messageID) => {
if (!link) link = "#"
let existingMessage = appData.messages.filter(x => x["text"] == message && x["show"])
if (existingMessage.length) {
// we can just bump the count on this one
existingMessage[0]["count"] += 1
} else {
// we need a new one
let newMessage = {text: message, count: 1, show: true}
if (messageID) {
newMessage["messageID"] = messageID
}
if (mustfollow) {
newMessage["mustfollow"] = link
} else {
newMessage["mayfollow"] = link
}
appData.messages.push(newMessage)
}
resizeWindow()
}
var dismissMessage = (element) => {
let $element = $(element)
if ($element.hasClass('no-dismiss')){
return
}
if (!$element.hasClass('message-container')){
element = $(element).parents('.message-container').get(0)
}
let elementIdx = element.attributes.index.value
let messageID = false
if (element.attributes.messageID) {
messageID = element.attributes.messageID.value
}
if (messageID) {
ipcRenderer.send('messageAcknowledged', messageID)
}
appData.messages[elementIdx]["show"] = false;
resizeWindow()
}
request.get({
url: `${API_URL}/api/tracker-notifications`,
json: true,
headers: {'User-Agent': 'MTGATracker-App'}
}, (err, res, data) => {
if (data.notifications) {
data.notifications.forEach(message => {
let link = "#"
let mustfollow = false;
if (message.mayfollow) {
link = message.mayfollow
} else if (message.mustfollow) {
link = message.mustfollow
mustfollow = true
}
addMessage(message.text, link, mustfollow, message.messageID)
})
}
})
let deckFrequencySubLists = function (decklist) {
let card_count = -1;
let sublists = [];
let current_sublist = null;
let reverse_sort = function(a, b){return b.count_in_deck - a.count_in_deck}
// Don't mutate input list
sorted_deck = [...decklist].sort(reverse_sort)
for ( card of sorted_deck ) {
if (card.count_in_deck != card_count){
if ( current_sublist != null ){
sublists.push(current_sublist);
}
current_sublist = [];
card_count = card.count_in_deck;
}
current_sublist.push(card);
}
sublists.push(current_sublist);
return sublists;
};
let emeraldSort = function (decklist) {
return decklist.sort(
function (a, b) {
// Sort by cardtype first
return cardtypeCompare(a.card_type, b.card_type)
// Then sort by mana cost
|| manaCostCompare(a, b)
// Then sort by name
|| nameCompare(a, b);
}
);
};
let drawSort = function (decklist,subsort) {
let sublists = deckFrequencySubLists(decklist);
let sorted = [];
for (sublist of sublists){
if (subsort === 'name') {
sorted.push(sublist.sort( (a,b) => { return nameCompare(a,b); } ));
} else if ( subsort === 'emerald' ) {
sorted.push(emeraldSort(sublist));
} else if ( subsort === 'color' ) {
sorted.push(colorSort(sublist));
}
}
//flatten
return [].concat.apply([], sorted);
}
let colorSort = function (decklist) {
return decklist.sort(
function (a, b) {
// Sort by cmc first
return manaCostCompare(a, b)
// then sort by color
|| colorCompare(a,b)
// Then sort by name
|| nameCompare(a, b);
}
);
}
let cardtypeCompare = function (a, b) {
// Creatures -> Planeswalkers -> Enchantments -> Artifacts -> Sorceries -> Instants -> Non-Basic Lands -> Basic Lands
if (a.includes("Creature")) {
if (!b.includes("Creature")) {
return -1;
}
return 0;
}
if (b.includes("Creature")) {
return 1;
}
if (a.includes("Planeswalker")) {
if (!b.includes("Planeswalker")) {
return -1;
}
return 0;
}
if (b.includes("Planeswalker")) {
return 1;
}
if (a.includes("Enchantment")) {
if (!b.includes("Enchantment")) {
return -1;
}
return 0;
}
if (b.includes("Enchantment")) {
return 1;
}
if (a.includes("Artifact")) {
if (!b.includes("Artifact")) {
return -1;
}
return 0;
}
if (b.includes("Artifact")) {
return 1;
}
if (a.includes("Sorcery")) {
if (!b.includes("Sorcery")) {
return -1;
}
return 0;
}
if (b.includes("Sorcery")) {
return 1;
}
if (a.includes("Instant")) {
if (!b.includes("Instant")) {
return -1;
}
return 0;
}
if (b.includes("Instant")) {
return 1;
}
if (a.includes("Basic")) {
if (!b.includes("Basic")) {
return 1;
}
return 0;
}
if (b.includes("Basic")) {
return -1;
}
return 0;
};
let cmcCompute = function (card) {
//put lands at bottom
if ( card.card_type === 'Land'){
return 1000;
}
let total = 0;
for (let manaSymbol of card.cost) {
// Put X spells at the end
if (manaSymbol === "X") {
total += 100;
} else {
// Generic mana amount
let intValue = parseInt(manaSymbol);
if (!isNaN(intValue)) {
total += intValue;
} else {
// Colored mana
total += 1;
}
}
}
return total;
};
let manaCostCompare = function (a, b) {
let cmcA = cmcCompute(a);
let cmcB = cmcCompute(b);
if (cmcA < cmcB) {
return -1;
}
if (cmcB < cmcA) {
return 1;
}
return 0;
};
let nameCompare = function (a, b) {
let field = null
if (a.pretty_name != undefined){
field = 'pretty_name'
} else if (a.card != undefined){
field = 'card'
}
if (a[field] < b[field]) {
return -1;
}
if (b[field] < a[field]) {
return 1;
}
return 0;
};
/**
* Rank color combos.
*
* Each color is assigned value
* W=1 U=2 B=4 R=8 G=16 C=32 L=+32
*
* Array holds total color values in sorted order.
* To find sort position of particular card,
* get index of color combo value
*/
let color_ranks = [
1,2,4,8,16, // W U B G R
3,5,9,17, // WU WB WR WG
6,10,18, // UB UR UG
12,20, // BR BG
24, // RG
7,11,19, // WUB WUR WUG
13,21, // WBR WBG
25, // WRG
14,22,26, // UBR UBG URG
28, // BRG
15,23,27,29,30, // WUBR WUBG WURG WBRG UBRG
31, // WUBRG
32, // C
33,34,36,40,48, // L W U B G R
35,37,41,49, // L WU WB WR WG
38,42,50, // L UB UR UG
44,52, // L BR BG
56, // L RG
39,43,51, // L WUB WUR WUG
45,53, // L WBR WBG
57, // L WRG
46,54,58, // L UBR UBG URG
60, // L BRG
47,55,59,61,62, // L WUBR WUBG WURG WBRG UBRG
63, // L WUBRG
64 // L C
]
let colorCompare = function (a,b) {
let a_index = color_ranks.indexOf(getColorValue(a));
let b_index = color_ranks.indexOf(getColorValue(b));
if ( a_index < b_index ) {
return -1;
} else if ( a_index === b_index ) {
return 0;
} else {
return 1;
}
}
let getColorValue = function(card) {
let value = card.cardtype === 'Land' ? 32 : 0
for ( color of card.colors) {
if ( color === 'White' ){
value += 1
} else if ( color === 'Blue' ) {
value += 2
} else if ( color === 'Black' ) {
value += 4
} else if ( color === 'Red') {
value += 8
} else if ( color === 'Green' ){
value += 16
} else if ( color === 'Colorless' ){
value += 32
}
}
return value;
}
let mergeDuplicates = function(decklist) {
let field = 'pretty_name'
if (decklist[0].card != undefined){ field = 'card'}
let mergedDecklist = new Map();
decklist.forEach((card) => {
if (mergedDecklist.get(card[field])) {
mergedDecklist.get(card[field]).count_in_deck += card.count_in_deck;
}
else {
mergedDecklist.set(card[field], Object.assign({}, card));
}
});
return Array.from(mergedDecklist.values());
};
rivets.formatters.decklistSort = function(decklist) {
if (decklist.length === 0) {
return decklist;
}
let sorted_decklist = mergeDuplicates(decklist)
if (sortMethod.startsWith("draw")) {
let subsort = sortMethod.split('-')[1];
if ( subsort === undefined ){
subsort = 'name';
}
return drawSort(sorted_decklist,subsort);
} else if (sortMethod == "emerald") {
return emeraldSort(sorted_decklist);
} else if (sortMethod == "color") {
return colorSort(sorted_decklist);
}
};
rivets.binders.showmessage = function(el, value) {
if (value && remote.getGlobal('messagesAcknowledged').includes(value)) {
el.style.display = "none"
} else {
el.style.display = "block"
}
}
rivets.binders.showvault = function(el, value) {
if (value > appData.minVaultProgress && appData.showVaultProgress) {
el.style.display = "block"
} else {
el.style.display = "none";
}
}
rivets.binders.manasymbols = (el,cost) => {
let $el = $(el);
$el.children().remove()
for (let symbol of cost){
let $span = $('<span class="mana-symbol"></span>')
let colors = []
if (symbol.indexOf('/') == -1) {
colors.push(symbol.toLowerCase())
} else {
colors = symbol.split('/').map(x => x.replace('(','').replace(')','').toLowerCase())
$span.addClass('mi-split').addClass('mi-split-color')
}
for (let color of colors) {
let $i = ($('<i class="mi mi-shadow mi-' + color + '"></i>'))
if (colors.length == 1){
$i.addClass('mi-mana')
}
$span.append($i)
}
$(el).append($span)
}
}
rivets.binders.card_color = function(el, value) {
el.classList.remove("card-b")
el.classList.remove("card-g")
el.classList.remove("card-r")
el.classList.remove("card-u")
el.classList.remove("card-w")
el.classList.remove("card-c") // colorless
el.classList.remove("card-m") // multicolor, not mountain
let atLeastOneColor = false;
if (value.length > 1) {
// card-m sets the fade color
el.classList.add("card-m")
}
if (value.length > 2) {
// card-m-back sets the background image to generic 3-color background
el.classList.add("card-m-back")
} else {
if (value.includes("Black")) {
el.classList.add("card-b")
atLeastOneColor = true
}
if (value.includes("White")) {
el.classList.add("card-w")
atLeastOneColor = true
}
if (value.includes("Blue")) {
el.classList.add("card-u")
atLeastOneColor = true
}
if (value.includes("Green")) {
el.classList.add("card-g")
atLeastOneColor = true
}
if (value.includes("Red")) {
el.classList.add("card-r")
atLeastOneColor = true
}
if (value.includes("Colorless") || !atLeastOneColor) {
el.classList.add("card-c")
}
}
}
rivets.formatters.as_seconds = function(value) {
return value / 100;
}
rivets.formatters.more_than_one = function(value) {
return value > 1;
}
rivets.bind(document.getElementById('container'), appData)
var hideBackButton = function(hidden) {
if(hidden) {
$(".hide-on-rollup").addClass("rollup-modifier");
} else {
$(".hide-on-rollup").removeClass("rollup-modifier");
}
}
function hideCallback(hidden) {
console.log("hidden callback called")
hideBackButton(hidden)
resizeWindow()
}
ws.addEventListener('open', () => {
ws.send('hello!');
console.log("sent hello")
});
function resizeWindow() {
let total = 0;
$.each($(".card"), function(i, c) {
total += c.offsetHeight;
})
container = document.getElementById("container")
let totalHeight = 0;
totalHeight += $('#tracker-header').outerHeight(true);
$("#tracker-body").children().each(function(c, e) {
if(e.style.display != "none" && !e.classList.contains("no-height-contribution"))
totalHeight += $(e).outerHeight(true);
});
bounds = browserWindow.getBounds()
container.style.height = "" + parseInt(totalHeight) + "px"
if (zoom > 0.85) {
// TODO: resize with math that is less "throwing shit at the wall"
// tbh this is stupid but it works for now
// (and is probably better than setting max width and height and hoping transparent window works)
let heightPower = 5;
if (remote.getGlobal("useFlat")) {
heightPower = 3;
}
totalHeight += parseInt(100 * (zoom ** heightPower))
bounds.width = parseInt(354 * (zoom ** 2))
if (0.85 < zoom && zoom < 0.95) {
// dirty, dirty hack, but 0.9 is broken so.
bounds.width += 30;
}
} else {
bounds.width = parseInt(354 * zoom);
}
let menuHeight = 0
let selectors = ['#tracker-header h1', '#main-menu ul', '#main-menu ul li ul']
for (selector of selectors) {
menuHeight += $(selector).outerHeight(true)
}
if (menuHeight > totalHeight) {
totalHeight = menuHeight
}
bounds.height = Math.min(parseInt(totalHeight), calcMainMaxHeight());
if (!(debug || useFrame)) {
browserWindow.setBounds(bounds)
}
setClickHandlers();
}
function populateDeck(elem) {
deckID = elem.getAttribute('data-deckid');
appData.activeDeck = deckID;
deck = getDeckById(deckID);
const types = ['daily','alltime'];
$.each(types, (i, type) => {
if (appData.winLossObj[type][deckID] === undefined) {
appData.winLossObj[type][deckID] = {win: 0, loss: 0, name: deck.pool_name}
}
const counter_prefix = type === 'daily' ? 'dailyDeck' : 'deck';
appData[counter_prefix + 'WinCounter'] = appData.winLossObj[type][deckID].win;
appData[counter_prefix + 'LossCounter'] = appData.winLossObj[type][deckID].loss;
});
if (deck != null){
appData.selected_list = deck.cards;
appData.selected_list_name = deck.pool_name;
appData.list_selected = true;
appData.no_list_selected = false;
appData.showDeckCounters = true;
}
resizeWindow()
}
function exitDraft() {
appData.game_in_progress = false;
appData.show_available_decklists = true;
appData.showDraftStats = false;
resizeWindow()
}
function unpopulateDecklist() {
appData.list_selected = false;
appData.no_list_selected = true;
appData.activeDeck = 'total';
appData.deckWinCounter = 0;
appData.deckLossCounter = 0;
appData.dailyDeckWinCounter = 0;
appData.dailyDeckLossCounter = 0;
appData.showDeckCounters = false;
appData.game_in_progress = false;
appData.show_available_decklists = true;
appData.showDraftStats = false;
resizeWindow()
}
function passThrough(endpoint, passData, playerID) {
passData.playerId = playerID;
return new Promise((resolve, reject) => {
console.log(`posting ${endpoint} request...`)
let fetchURL = `insp://${endpoint}`
fetch(fetchURL, {method: "POST", body: JSON.stringify(passData)})
.then(resp => resp.json())
.then(data => {
if (data.error) throw new Error(data.error)
console.log(`finished posting ${endpoint} request...`)
resolve({success: true})
}).catch(err => {
addMessage(`WARNING! Could not record ${endpoint}! Please contact us in Discord for support! (${err})`)
console.log(err)
reject({on: `post_${endpoint}`, error: err || res})
})
})
}
function cleanError(error, depth, maxDepth) {
if (depth === undefined) depth = 0;
if (maxDepth === undefined) maxDepth = 3;
if (depth >= maxDepth) return;
if (error && typeof error === 'object') {
for (let key in error) {
if (key == "token") {
delete error[key]
} else {
cleanError(error[key], depth+1, maxDepth)
}
}
}
}
function cleanErrors(errors) {
errors.forEach(error => cleanError(error, 0))
}
function getDeckById(deckID){
return appData.player_decks.find(x => x.deck_id == deckID) || false
}
function uploadGame(gameData) {
const victory = gameData.players[0].name === gameData.winner;
//only update per-deck win/loss for decks we know about
const deckID = gameData.players[0].deck.deckID;
if (appData.player_decks.map(deck=>deck.deck_id).includes(deckID)) {
const types = ['daily','alltime'];
$.each(types, (i, type) => {
if(appData.winLossObj[type][deckID]) {
if (victory) {
appData.winLossObj[type][deckID].win++;
} else {
appData.winLossObj[type][deckID].loss++;
}
} else {
if (victory) {
appData.winLossObj[type][deckID] = {win: 1, loss: 0, name: gameData.players[0].deck.poolName};
} else {
appData.winLossObj[type][deckID] = {win: 0, loss: 1, name: gameData.players[0].deck.poolName};
}
}
});
ipcRenderer.send('updateWinLossCounters', {
key: deckID,
value: {alltime: appData.winLossObj['alltime'][deckID],daily:appData.winLossObj['daily'][deckID]}
})
}
if (victory) {
appData.winLossObj.alltime.total.win++;
appData.winLossObj.daily.total.win++;
} else {
appData.winLossObj.alltime.total.loss++;
appData.winLossObj.daily.total.loss++;
}
ipcRenderer.send('updateWinLossCounters', {
key: "total",
value: {alltime:appData.winLossObj.alltime.total,daily:appData.winLossObj.daily.total}
});
let fetchURL = `insp://insert-game`
fetch(fetchURL, {method: "POST", body: JSON.stringify(gameData)})
.then(resp => resp.json())
.then(data => {
if (data.error) {
throw new Error(data.error)
}
console.log(`got ${data} from insert-game`)
console.log(data)
if (remote.getGlobal("showInspector")) {
// TODO: open inspector window instead of linking to https://inspector...
addMessage("Game saved in Inspector!", "javascript:openInspector();")
}
})
.catch(err => {
console.log(err)
addMessage(`WARNING! Could not save game result! Please contact us in Discord for support! (${err})`)
})
return new Promise((resolve, reject) => {
let anonGameData = {
anonymousUserID: crypto.createHash('md5').update(gameData.players[0].name).digest("hex"),
gameID: crypto.createHash('md5').update(gameData.gameID).digest("hex"),
client_version: appData.version,
}
console.log("posting game request...")
console.log(anonGameData)
postGameUrl = `${API_URL}/api/game`
request.post({
url: postGameUrl,
json: true,
body: anonGameData,
headers: {'User-Agent': 'MTGATracker-App', token: token}
}, (err, res, data) => {
console.log("finished posting game request...")
console.log(res)
console.log(err)
if (err || res.statusCode != 201) {
console.log({on: "post_game", error: err || res})
resolve({attempt: attempt, errors: errors})
} else {
resolve({success: true})
}
})
})
}
let gameAlreadyUploaded = (gameID) => {
return Object.keys(gameLookup).includes(gameID)
}
let onMessage = (data) => {
data = JSON.parse(event.data)
if (data.data_type == "game_state") {
if (data.match_complete) {
timerRunning = false;
$("#opponent-timer").removeClass("active")
$("#hero-timer").removeClass("active")
overallTimer.pause()
heroTimer.pause()
opponentTimer.pause()
console.log("match over")
if (data.game && gameAlreadyUploaded(data.game.gameID)) {
console.log(`Backend sent match_complete for ${data.game.gameID}, but already know that game`)
} else if (data.game) {
appData.game_complete = true;
$(".cardsleft").addClass("gamecomplete")
gameLookup[data.game.gameID] = {count: 0, uploaded: true}
uploadGame(data.game)
} else if (data.gameID) {
console.log(`match_complete and gameID ${data.gameID} but no game data`)
if (gameAlreadyUploaded(data.gameID)) {
if (gameLookup[data.gameID].count++ > 5) {
if (!gameLookup[data.gameID].uploaded) {
gameLookup[data.gameID].uploaded = true
if (lastGameState) {
uploadGame(lastGameState)
}
}
}
} else { // gameLookup doesn't know this game yet
console.log(`haven't seen ${data.gameID} before, adding now'`)
gameLookup[data.gameID] = {count: 0, uploaded: false}
}
}
} else if (!gameAlreadyUploaded(data.game_id)) {
lastGameState = data
if (!timerRunning) {
timerRunning = true;
console.log("TIMER: resetcss")
// this is transition into a game. reset all the timers
overallTimer.reset()
opponentTimer.reset()
heroTimer.reset()
overallTimer.start()
// pause each player's timer. we'll unpause them soon, with a decisionPlayerChange event.
opponentTimer.pause()
heroTimer.pause()
//set the stats to report for this deck, if known
//otherwise, use total stats
if (appData.player_decks.map(deck=>deck.deck_id).includes(data.deck_id)){
const deck = getDeckById(data.deck_id);
appData.activeDeck = data.deck_id;
const types = ['daily','alltime'];
$.each(types, (i, type) => {
if (!(data.deck_id in appData.winLossObj[type])){
appData.winLossObj[type][appData.activeDeck] = {win: 0, loss: 0, name: deck.pool_name};
}
});
appData.deckWinCounter = appData.winLossObj['alltime'][data.deck_id].win;
appData.deckLossCounter = appData.winLossObj['alltime'][data.deck_id].loss;
appData.dailyDeckWinCounter = appData.winLossObj['daily'][data.deck_id].win;
appData.dailyDeckLossCounter = appData.winLossObj['daily'][data.deck_id].loss;
appData.showDeckCounters = true;
} else {
appData.activeDeck = 'total';
appData.deckWinCounter = 0;
appData.deckLossCounter = 0;
appData.dailyDeckWinCounter = 0;
appData.dailyDeckLossCounter = 0;
appData.showDeckCounters = false;
}
}
appData.game_in_progress = true;
appData.show_available_decklists = false;
appData.showDraftStats = false;
appData.game_complete = false;
$(".cardsleft").removeClass("gamecomplete")
appData.deck_name = data.draw_odds.deck_name;
appData.opponent_hand = data.opponent_hand;
if (staticMode) {
appData.draw_stats = data.draw_odds.original_deck_stats;
appData.total_cards_in_deck = data.draw_odds.original_decklist_total;
} else {
appData.draw_stats = data.draw_odds.stats;
appData.total_cards_in_deck = data.draw_odds.total_cards_in_deck;
}
}
} else if (data.game_history_event) {
ipcRenderer.send('gameHistoryEvent', data.game_history_event)
} else if (data.data_type == "error") {
if (data.count) {
appData.error_count = data.count;
}
appData.last_error = data.msg;
} else if (data.data_type == "message") {
if (data.right_click && !debug) {
hideWindow()
} else if (data.draft_collection_count) {
console.log("handle draft stuff")
console.log(data.draft_collection_count)
appData.game_in_progress = false;
appData.show_available_decklists = false;
appData.showDraftStats = true;
appData.draftStats = data.draft_collection_count
} else if (data.rank_change) {
passThrough("rankChange", data.rank_change, data.player_key).catch(e => {
console.log("error uploading rank data: ")
console.log(e)
})
} else if (data.inventory_update) {
// passThrough("tracker-api/inventory-update", data.inventory_update, data.player_key).catch(e => {
// // TODO: check for wildcard redemptions? or should we do that in the API?
// console.log("error uploading inventory-update data: ")
// console.log(e)
// })
} else if (data.inventory) {
if (data.inventory.vaultProgress) {
appData.lastVaultProgress = data.inventory.vaultProgress;
ipcRenderer.send("lastVaultProgressChanged",appData.lastVaultProgress)
}
//console.log(data.inventory)
ipcRenderer.send('inventoryChanged', data.inventory)
// passThrough("tracker-api/inventory", data.inventory, data.player_key).catch(e => {
// console.log("error uploading inventory data: ")
// console.log(e)
// })
} else if (data.collection) {
var cardQuantity;
if (data.collection) {
if(appData.lastCollection && (Object.keys(appData.lastCollection).length != 0)) {
var objectToPush = {time:(new Date(Date.now())).toLocaleString(), cardsObtained:{}};
for(var cardID in data.collection) {
if(data.collection.hasOwnProperty(cardID)) {
if(/^\d+$/.test(cardID)) {
cardQuantity = data.collection[cardID] - appData.lastCollection[cardID];
if(isNaN(cardQuantity)) {
cardQuantity = data.collection[cardID];
}
if(cardQuantity > 0) {
objectToPush.cardsObtained[cardID] = cardQuantity;
}
}
}
}
if(Object.keys(objectToPush.cardsObtained).length > 0) {
appData.recentCards.unshift(objectToPush);
console.log(appData.recentCards);
ipcRenderer.send('recentCardsChanged', objectToPush)
}
}
appData.lastCollection = data.collection
ipcRenderer.send('settingsChanged', {
key: "lastCollection",
value: appData.lastCollection
})
// passThrough("tracker-api/collection", data.collection, data.player_key).catch(e => {
// console.log("error uploading collections data: ")
// console.log(e)
// })
}
} else if (data.draftPick) {
passThrough("draft-pick", data.draftPick, data.player_key).catch(e => {
console.log("error uploading draftPick data: ")
console.log(e)
})
} else if (data.decisionPlayerChange) {
if (data.heroIsDeciding) {
opponentTimer.start()
heroTimer.pause()
$("#opponent-timer").removeClass("active")
$("#hero-timer").addClass("active")
} else {
opponentTimer.pause()
heroTimer.start()
$("#opponent-timer").addClass("active")
$("#hero-timer").removeClass("active")
}
} else if (data.authenticateResponse) {
console.log("handle authenticateResponse")
// TODO save user?
}
} else if (data.data_type=="decklist_change") {
if (data.decks.no_decks_defined) {
appData.no_decks = true;
} else {
new_decks = []
$.each(data.decks, (key, value) => {
new_decks.push(value)
})
appData.player_decks = new_decks.sort((a,b) => {
if (a.pool_name < b.pool_name ){
return -1;
} else if ( a.pool_name === b.pool_name ){
return 0;
} else {
return 1;
}
});
appData.no_decks = false;
}
}
resizeWindow()
}
let stepZoom = (zoomIn=true) => {
zoom += zoomIn ? 0.1 : -0.1;
if ( zoom < 0.2 ) {
zoom = 0.2;
}
applyZoom()
}
let resetZoom = () => {
zoom = 1
applyZoom()
}
let applyZoom = () => {
browserWindow.webContents.setZoomFactor(zoom)
ipcRenderer.send('settingsChanged', {key: "zoom", value: zoom})
}
let zoomIn = () => {
stepZoom()
}
let zoomOut = () => {
stepZoom(false)
}
//function to close because of coming feature: close to tray
let close = () => {
browserWindow.close()
}
let openInspector = () => { ipcRenderer.send('openInspector', null); }
let openHistory = () => { ipcRenderer.send('openHistory', null); }
let openSettings = () => { ipcRenderer.send('openSettings', null); }
let openCollection = () => { ipcRenderer.send('openCollection', null); }
let menu_items = [
{
label: 'History',
action: openHistory,
keybind: 'Ctrl+H'
},
{
label: 'Inspector',
action: openInspector,
keybind: 'Ctrl+I'
},
{
label: 'Settings',
action: openSettings,
keybind: 'Ctrl+;',
alt_keybind: 'Ctrl+S'
},
{
label: 'Vault',
action: openCollection,
keybind: "Ctrl+V"
},
{
label: 'Copy',
action: copyData,
keybind: 'Ctrl+C',
separator: 'top'
},
{
label: 'Zoom',
submenu: [
{
label: 'Zoom in',
action: zoomIn,
keep_open: true,
keybind: 'Ctrl+\+',
alt_keybind: 'Ctrl+='
},
{
label: 'Zoom out',
action: zoomOut,
keep_open: true,
keybind: 'Ctrl+-',
alt_keybind: 'Ctrl+Shift+-'
},
{
label: 'Reset zoom',
action: resetZoom,
keep_open: true,
keybind: 'Ctrl+0'
},
],
separator: 'bottom'
},
{
label: 'Quit',
action: close,
keybind: 'Ctrl+Q'
},
];
let buildMenu = () => {
let menu = $('<ul></ul>')
for (let menu_item of menu_items) {
menu.append(buildMenuItem(menu_item))
}
$('#main-menu').append(menu)
};
let buildMenuItem = (menu_item) => {
let li = $('<li></li>')
let item = $('<a></a>')
item.text(menu_item.label)
if (menu_item.action != undefined) {
let action = null
if ( menu_item.keep_open ) {
action = menu_item.action
} else {
action = () => { menu_item.action.call(); toggleMenu()}
}
item.click(action)
}
if (menu_item.keybind != undefined){
let span = $('<span class="keybind">' + menu_item.keybind + '</span>')
item.append(span)
mousetrap.bind(menu_item.keybind.toLowerCase(),menu_item.action)
if (menu_item.alt_keybind != undefined){
mousetrap.bind(menu_item.alt_keybind.toLowerCase(),menu_item.action)
}
}
li.append(item)
if (menu_item.separator != undefined) {
if (menu_item.separator == 'both' || menu_item.separator == 'top'){
li.addClass('separator-top')
}
if (menu_item.separator == 'both' || menu_item.separator == 'bottom'){
li.addClass('separator-bottom')
}
}
if (menu_item.submenu != undefined) {
let submenu = $('<ul></ul>')
for (let submenu_item of menu_item.submenu){
submenu.append(buildMenuItem(submenu_item))
}
li.append(submenu)
}
return li
}
let toggleMenu = () => {
$('#main-menu').toggleClass('hide-me');
$('body').toggleClass('no-drag');
resizeWindow()
}
let updateTitleWidth = () => {
let width = useMinimal ? 278 : 320
if (appData.showHideButton) {
width -= 26
}
if (appData.showUIButtons){
width -= 50
}
if (appData.showMenu) {
width -= 24
}
if (!(appData.showHideButton && appData.showMenu && appData.showUIButtons)){
width -= 8
}
$('#tracker-header h1').css('width',width)
}
/**
* These click handlers need to be reset on all resizeWindow calls because of DOM changes
*/
let setClickHandlers = () => {
addClickHandler('.message-container',(e) => {dismissMessage(e.target)})
addClickHandler('.back-draft',(e) => {exitDraft()})
addClickHandler('.back-to-decklist',(e) => {unpopulateDecklist()})
addClickHandler('.deck-container',(e) => {populateDeck($(e.target).parent().get(0))})
}
let hideWindow = () => {
hideModeManager.toggleHidden()
ipcRenderer.send('hideRequest')
}
document.addEventListener("DOMContentLoaded", function(event) {
hideModeManager = hideWindowManager({
useRollupMode: function() {return remote.getGlobal('rollupMode')},
getHideDelay: function() {return remote.getGlobal('hideDelay')},
getInverted: function() {return remote.getGlobal('invertHideMode')},
windowName: "mainRenderer",
bodyID: "#tracker-body",
headerID: "#tracker-header",
containerID: "#container",
hideCallback: hideCallback,
})
if (invertHideMode){
hideWindow()
}
setInterval(() => {
$('#overall-timer').html(overallTimer.getTimeValues().toString());
$('#hero-timer').html(opponentTimer.getTimeValues().toString());
$('#opponent-timer').html(heroTimer.getTimeValues().toString());
}, 1000)
if (debug || useFrame) {
$("#container").addClass("container-framed")
$("body").css("background-color", "green")
} else {
$("#container").addClass("container-normal")
}
/**
* These click handlers only need to be set once, so called here.
*/
buildMenu()
updateTitleWidth()
addClickHandler('#menu-icon',toggleMenu)
addClickHandler('#floating-eye',hideWindow)
addClickHandler('#minimize-icon',() => {browserWindow.minimize()})
addClickHandler('#close-icon',close)
addClickHandler('body',null)
//open links externally by default
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault();
shell.openExternal(this.href);
});
// load theme on first launch without settings change
if (lastThemeFile && lastUseTheme) {
let currentThemeLink = $("#theme")
if (currentThemeLink) {
currentThemeLink.remove()
}
if (lastUseTheme) {
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.id = 'theme';
link.rel = 'stylesheet';
link.type = 'text/css';
let themePath = runFromSource ? "themes/" : "../../../themes/";
link.href = themePath + lastThemeFile;
head.appendChild(link)
}
}
ws.onmessage = onMessage
});
ipcRenderer.on('stdout', (event, data) => {
console.log(data.text)
})
ipcRenderer.on('updateReadyToInstall', (messageInfo) => {
addMessage("A new tracker update will be applied on next launch!", "https://github.com/shawkinsl/mtga-tracker/releases/latest")
})
ipcRenderer.on('settingsChanged', () => {
debug = remote.getGlobal('debug');
appData.debug = debug
mtgaOverlayOnly = remote.getGlobal('mtgaOverlayOnly');
appData.mtgaOverlayOnly = mtgaOverlayOnly
sortMethod = remote.getGlobal('sortMethod');
useFrame = remote.getGlobal('useFrame');
appData.useFrame = useFrame
staticMode = remote.getGlobal('staticMode');
appData.staticMode = staticMode
showIIDs = remote.getGlobal('showIIDs');
appData.showIIDs = showIIDs
showErrors = remote.getGlobal('showErrors');
appData.showErrors = showErrors
appVersionStr = remote.getGlobal('version');
appData.appVersionStr = appVersionStr
showTotalWinLossCounter = remote.getGlobal('showTotalWinLossCounter');
appData.showTotalWinLossCounter = showTotalWinLossCounter
showDeckWinLossCounter = remote.getGlobal('showDeckWinLossCounter');
appData.showDeckWinLossCounter = showDeckWinLossCounter
showDailyTotalWinLossCounter = remote.getGlobal('showDailyTotalWinLossCounter');
appData.showDailyTotalWinLossCounter = showDailyTotalWinLossCounter
showDailyDeckWinLossCounter = remote.getGlobal('showDailyDeckWinLossCounter');
appData.showDailyDeckWinLossCounter = showDailyDeckWinLossCounter
showVaultProgress = remote.getGlobal('showVaultProgress');
appData.showVaultProgress = showVaultProgress
showGameTimer = remote.getGlobal('showGameTimer');
appData.showGameTimer = showGameTimer
showChessTimers = remote.getGlobal('showChessTimers');
appData.showChessTimers = showChessTimers
hideDelay = remote.getGlobal('hideDelay');
appData.hideDelay = hideDelay
invertHideMode = remote.getGlobal('invertHideMode');
appData.invertHideMode = invertHideMode
rollupMode = remote.getGlobal('rollupMode');
appData.rollupMode = rollupMode
recentCards = remote.getGlobal('recentCards');
appData.recentCards = recentCards
minToTray = remote.getGlobal('minToTray');
appData.minToTray = minToTray
let useTheme = remote.getGlobal("useTheme")
let themeFile = remote.getGlobal("themeFile")
let useFlat = remote.getGlobal("useFlat")
let currentFlatLink = $("#flat")
if (useFlat) {
if(!currentFlatLink.length) {
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.id = 'flat';
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'flat.css';
head.appendChild(link)
} else {
console.log(currentFlatLink)
}
} else if (currentFlatLink) {
currentFlatLink.remove()
}
useMinimal = remote.getGlobal("useMinimal")
let currentMinimalLink = $("#minimal")
if (useMinimal) {
if (!currentMinimalLink.length) {
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.id = 'minimal';
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'minimal.css';
head.appendChild(link)
updateTitleWidth()
}
} else if (currentMinimalLink) {
currentMinimalLink.remove()
updateTitleWidth()
}
if ((themeFile && (themeFile != lastThemeFile)) || useTheme != lastUseTheme) {
lastThemeFile = themeFile
lastUseTheme = useTheme
let currentThemeLink = $("#theme")
if (currentThemeLink) {
currentThemeLink.remove()
}
if (useTheme) {
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.id = 'theme';
link.rel = 'stylesheet';
link.type = 'text/css';
let themePath = runFromSource ? "themes/" : "../../../themes/";
link.href = themePath + lastThemeFile;
head.appendChild(link)
}
}
let buttonsChanged = false
let fields = ['showUIButtons','showMenu','showHideButton']
for (let field of fields) {
let newVal = remote.getGlobal(field)
if (newVal != appData[field]){
appData[field] = newVal
buttonsChanged = true
}
}
if (buttonsChanged) {
updateTitleWidth()
}
resizeWindow()
})
ipcRenderer.on('counterChanged', (e,new_wlc) => {
appData.winLossObj = new_wlc;
if (appData.activeDeck === 'total') {
appData.deckWinCounter = 0;
appData.deckLossCounter = 0;
appData.dailyDeckWinCounter = 0;
appData.dailyDeckLossCounter = 0;
appData.showDeckCounters = false;
} else {
appData.deckWinCounter = appData.winLossObj.alltime[appData.activeDeck].win;
appData.deckLossCounter = appData.winLossObj.alltime[appData.activeDeck].loss;
if (appData.winLossObj.daily[appData.activeDeck] === undefined){
appData.winLossObj.daily[appData.activeDeck] = {win:0,loss:0,name:getDeckById(appData.activeDeck).pool_name};
}
appData.dailyDeckWinCounter = appData.winLossObj.daily[appData.activeDeck].win;
appData.dailyDeckLossCounter = appData.winLossObj.daily[appData.activeDeck].loss;
appData.showDeckCounters = true;
}
appData.totalWinCounter = appData.winLossObj.alltime.total.win;
appData.totalLossCounter = appData.winLossObj.alltime.total.loss;
appData.dailyTotalWinCounter = appData.winLossObj.daily.total.win;
appData.dailyTotalLossCounter = appData.winLossObj.daily.total.loss;
});
console.timeEnd('init')