/*jshint multistr: true */
/*global URL, indexedDB, unsafeWindow, GM_addStyle, GM_xmlhttpRequest, GM_getResourceText, GM_info, exportFunction */
// Userscript with no name - A TF2r enhancement userscript
// Copyright (C) 2016 James Lyne <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// ==UserScript==
// @name Userscript with no name
// @namespace NiGHTS
// @author James Lyne <[email protected]> [U:1:34673527]
// @description Overhauls the new raffle page and enhances a few others
// @include http://tf2r.com/*
// @version 1.5.8
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getResourceText
// @resource css https://gist.githubusercontent.com/JLyne/c02c409932a14c1734c5/raw/d8be43a17b8c22bf002e6fbc32326b00201127b6/noname-style.css
// @require http://code.jquery.com/jquery-1.12.0.min.js
// @require https://greasyforks.org/scripts/18834-userscript-with-no-name-skin-dictionary/code/Userscript%20with%20no%20name%20-%20Skin%20dictionary.js?version=164800
// @require https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.0.4/jscolor.js
// @run-at document-start
// @license GPLv3 - http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright Copyright (C) 2016, by James Lyne <[email protected]>
// @supportURL https://greasyforks.org/en/scripts/18644-userscript-with-no-name/feedback
// @connect steamcommunity.com
// @noframes
// ==/UserScript==
//Tampermonkey on firefox doesn't have some of these defined for some reason
var console = window.console || {};
console.log = console.log || function() {};
console.info = console.info || console.log;
console.warn = console.warn || console.log;
console.error = console.error || console.log;
console.debug = console.debug || console.log;
console.time = console.time || console.log;
console.timeEnd = console.timeEnd || console.log;
console.trace = console.trace || console.log;
console.group = console.group || console.log;
console.groupEnd = console.groupEnd || console.log;
window.NoName = {
page: 0,
pages: {
'news': {
id: 'home',
},
'donate': {
id: 'donate',
},
'info': {
id: 'info',
},
'chat': {
id: 'chat-page',
},
'newraf': {
init: 'NewRaffle',
},
'settings': {
init: 'Settings',
},
'user': {
init: 'Profile',
},
'raffles': {
init: 'RaffleList',
},
'ilinks': {
id: 'raffle-invites',
},
},
/**
*
*/
init: function() {
'use strict';
console.log('[NoName::init] Init');
this.Steam.init();
this.UI.init();
this.ScrapTF.init();
this.determinePage();
},
/**
*
*/
determinePage: function() {
'use strict';
var uri = window.location.pathname.replace('.html', '').split('/');
for(var pageID in this.pages) {
if(!this.pages.hasOwnProperty(pageID)) {
continue;
}
var page = this.pages[pageID];
if(uri[1] !== pageID) {
continue;
}
document.body.id = page.id || 'page';
this.page = pageID;
if(page.init) {
try {
window.NoName[page.init].init();
} catch(ignored) {
console.error('[NoName::determinePage] Invalid callback for page: ' + pageID);
}
}
break;
}
if(!this.page && $('.participants').length) {
this.page = 'raffle';
this.Raffle.init();
}
},
/**
* Export override functions to unsafe window
* This needs to be run both as early as possible and after page load
* We can't know when our script runs relative to the scripts on the page so we need to cover both eventualities
*/
exportOverrides: function() {
'use strict';
console.log('[NoName::exportOverrides] Exporting overrides');
try {
this.UI.exportOverrides();
this.Raffle.exportOverrides();
this.RaffleList.exportOverrides();
} catch(e) {
console.error(e);
}
},
/**
* Generic ajax function
* TODO: Replace alert()s with something less shit
* @param data
* @returns promise
* @constructor
*/
AJAX: function(data) {
'use strict';
var deferred = jQuery.Deferred();
//noinspection JSUnresolvedFunction
$.ajax(
{
url: 'http://tf2r.com/job.php',
type: 'POST',
dataType: 'JSON',
data: data,
}
).done(
function(data, textStatus, jqXHR) {
if(data.status !== 'ok') {
alert(data.message);
deferred.reject(jqXHR);
} else {
deferred.resolve(data, jqXHR);
}
}
).fail(
function(jqXHR) {
deferred.reject(jqXHR);
}
);
return deferred.promise();
},
};
//Generic ui changes
window.NoName.UI = {
/**
*
*/
init: function() {
'use strict';
console.time("NoName:UI");
this.initUI();
if(NoName.Storage.get('other:snow', true)) {
NoName.Snow.enable();
}
NoName.Storage.listen(
'general:transitions', function(oldValue, newValue) {
if(newValue) {
$(document.body).addClass('transitions');
} else {
$(document.body).removeClass('transitions');
}
}
);
//Apply UI colour changes
NoName.Storage.listen(
'general:colour general:customcolour general:linksusecolour', function() {
NoName.UI.updateAccentColor();
}
);
console.timeEnd("NoName:UI");
NoName.Storage.listen('other:snow', function(oldValue, newValue) {
if(newValue) {
NoName.Snow.enable();
} else {
NoName.Snow.disable();
}
});
},
/**
* Export override functions to unsafe window
* This needs to be run both as early as possible and after page load
* We can't know when our script runs relative to the scripts on the page so we need to cover both eventualities
*/
exportOverrides: function() {
'use strict';
var that = this;
//Override raffle list getItems() function to handle items that don't display correctly
unsafeWindow.getItem = exportFunction(
function(item) {
return that.getItem(item);
}, unsafeWindow
);
//Remove slDown, message transitions are done in css now
unsafeWindow.slDown = exportFunction(
function() {
}, unsafeWindow
);
},
/**
*
*/
initUI: function() {
'use strict';
console.time('NoName:UI:initUI');
//Unbind hover event handler added by the existing js
unsafeWindow.$('.item').unbind('mouseenter mouseleave');
if(NoName.Storage.get('general:transitions', true)) {
$(document.body).addClass('transitions');
}
$('.infitem').replaceWith(
$('<div></div>').addClass('infitem').append(
[
$('<strong></strong>').addClass('infname'),
$('<ul></ul>').addClass('infdesc'),
]
)
);
$('table table.raffle_infomation').each(
function() {
$(this).removeClass('raffle_infomation');
$(this).parent().addClass('raffle_infomation');
}
);
$('#content').children().last().prepend(
[
'Extended item information provided by Userscript with No Name, accuracy not guaranteed.<br />',
'Unusual effect images by backpack.tf.<br />',
]
);
console.timeEnd('NoName:UI:initUI');
},
/**
* Updates colour of the active states of various elements on the page
* If no colors are given, it'll use the currently selected custom colour or the default ones
* @param background
* @param foreground
*/
updateAccentColor: function(background, foreground) {
'use strict';
var backgroundColour,
foregroundColour;
//Remove existing styles
$('#noname-colour').remove();
//Fallback to saved values if needed
if(!background || !foreground) {
if(NoName.Storage.get('general:customcolour', false)) { //Custom colour
var colors = NoName.Storage.get('general:colour', '#CF6A32,#222222').split(',');
backgroundColour = background || colors[0];
foregroundColour = foreground || colors[1];
} else { //Default colour
backgroundColour = background || '#CF6A32';
foregroundColour = foreground || '#222222';
}
}
var $css = $('<style></style>').prop('id', 'noname-colour'),
css =
'input:focus,\
textarea:focus,\
select:focus,\
button:focus {\
border: 1px solid ' + backgroundColour + ';\
}\
input[type=submit]:hover,\
input[type=submit]:focus,\
input[type=submit]:active,\
input[type=button]:hover,\
input[type=button]:focus,\
input[type=button]:active,\
button:hover,\
button:focus,\
button:active {\
background-color: ' + backgroundColour + ';\
border-color: ' + backgroundColour + ';\
color: ' + foregroundColour + ';\
}\
.switch-field input:checked + label {\
background-color: ' + backgroundColour + ';\
color: ' + foregroundColour + ';\
}\
#settings input[type=checkbox] + label:before,\
#settings input[type=checkbox] + span:before {\
border-color: ' + backgroundColour + ';\
background-color: ' + backgroundColour + ';\
color: ' + foregroundColour + ';\
}\
a:hover,\
a:focus,\
a:active,\
.nav_font a:hover,\
.nav_font a:focus,\
.nav_font a:active {\
';
//Handle links text-shadow/colour setting
if(NoName.Storage.get('general:linksusecolour', false)) {
css += 'color: ' + backgroundColour + ' !important;\
text-shadow: 2px 2px 1px #000000 !important;\
transition-property: color !important;';
} else {
css += 'text-shadow: 2px 2px 1px ' + backgroundColour + ' !important;';
}
css += '}';
$css.text(css);
$(document.head).append($css);
console.info('[UI::updateAccentColor] Accent colour set to ' + backgroundColour);
},
/**
*
* @param item
* @returns {*}
*/
getItem: function(item) {
'use strict';
var element = document.createElement('div');
if(typeof item !== 'object') {
return null;
}
if(!(item instanceof NoName.Item)) {
var data = {
name: item.name,
quality: item.q.substring(1),
thumbnail: item.image,
level: item.level,
};
item = new NoName.Item(data, true);
}
element.style.backgroundImage = item.getBackgroundImages();
element.className = item.getCSSClasses();
element.item = item;
return element;
},
/**
* Creates a toggle switch that can replace radio buttons
* @param label
* @param name
* @param options
* @returns {*|jQuery}
*/
createSwitch: function(label, name, options) {
'use strict';
var $container = $('<div></div>').addClass('switch-field'),
children = [];
children.push($('<label></label>').addClass('switch-title').text(label));
options.forEach(
function(option, index) {
option.id = option.id || name.replace(' ', '-').toLowerCase() + '-' + index;
option.value = (typeof option.value !== 'undefined') ? option.value : '';
children.push(
$('<input />')
.prop(
{
type: 'radio',
name: name,
id: option.id,
checked: !!option.checked,
}
).val(option.value)
);
children.push(
$('<label></label>')
.prop('for', option.id)
.text(option.label)
);
}
);
$container.append(children);
return $container;
},
/**
* Item details tooltip
* Used everywhere items are shown
* Except the new raffle page which uses its own implementation due to different data
* @param element
*/
showItemInfo: function(element) {
'use strict';
var $element = $(element),
pos = $element.offset(),
height = $element.height(),
width = $element.width(),
item = element.item;
//Need all item classes other than .item so this'll do
$('.infname').addClass(item.getCSSClasses()).removeClass('item').html(item.getName());
//$('.infdesc').html('<li>Level: ' + item.getLevel() + ((item.getSeries()) ? '<br />#' + item.getSeries() : '') + '</li>');
$('.infdesc').html(item.getDescriptionList());
$('.infitem').show().css(
{
left: pos.left + (width / 2) - ($('.infdesc').outerWidth() / 2) + 'px',
top: (pos.top + height + 4) + 'px'
}
);
},
/**
*
*/
hideItemInfo: function() {
'use strict';
$('.infitem').hide();
$('.infname').removeClass().addClass('infname');
},
/**
*
*/
addStyles: function() {
'use strict';
GM_addStyle(GM_getResourceText('css'));
NoName.UI.updateAccentColor();
},
};
window.NoName.Snow = {
enabled: false,
enable: function() {
//TODO: Snowfall
$(document.body).addClass('snow');
this.enabled = true;
},
disable: function() {
$(document.body).removeClass('snow');
this.enabled = false;
}
};
//Profile pages
window.NoName.Profile = {
commentBlock: false,
steamID: null,
/**
*
*/
init: function() {
'use strict';
document.body.id = 'profile';
this.getProfileSteamID();
this.initUI();
},
/**
*
*/
getProfileSteamID: function() {
'use strict';
try {
var result = window.location.href.match(/https?:\/\/tf2r.com\/user\/(\d+)\.html/);
this.steamID = result[1];
} catch(ignore) {
console.warn('[Steam::getSteamID] Unable to determine steamID');
}
},
/**
*
*/
initUI: function() {
'use strict';
var progress,
$name;
//Optimisation to avoid excessive style calculations in firefox
//Detach containing element before making UI changes
this.$UI = $('#content').detach();
progress = this.calculateProgression(this.$UI.find('.upvb'));
//Add ids to things for css styling
$name = this.$UI.find('td:nth-child(2) > div > a').prop('id', 'name');
this.$UI.find('.indent > table tr:nth-child(3) > td:nth-child(2)').prop('id', 'rep');
this.$UI.find('.indent > table > tbody > tr:nth-child(2) > td').prop('id', 'rank');
this.$UI.find('.indent > table tr:nth-child(2) > td > table tr:nth-child(2) > td:nth-child(2)').prop(
'id', 'progress'
);
//Fix progress bar value and update colour to match username colour
this.$UI.find('#progress div > div').first().css(
{
'background-color': $name.css('color'),
'font-size': 0,
width: Math.min(progress, 100) + '%',
'border-radius': (progress >= 100) ? '3px' : '',
}
).next().text(progress.toFixed() + '%');
this.initFeedbackForm();
this.checkMoreUserInfo();
//Reattach updated UI
this.$UI.insertAfter('#nav_holder');
},
/**
*
*/
initFeedbackForm: function() {
'use strict';
var that = this,
$form = this.$UI.find('.newfeed').empty(),
$typeSwitch = NoName.UI.createSwitch(
'Type:', 'type', [
{
id: 'type1',
label: 'Positive',
value: '1',
},
{
id: 'type2',
label: 'Negative',
value: '2',
},
{
id: 'type0',
label: 'Neutral',
value: '0',
checked: true,
},
]
),
$elements = [
$typeSwitch, //Type Switch
$('<label></label>').text('Message:'), //Message Label
$('<textarea></textarea>').prop('id', 'feedtext').addClass('full-width'), //Message textarea
$('<button></button>').prop(
{ //Submit button
type: 'button',
id: 'sendfeed',
}
).text('Post'),
];
$form.append($elements);
$form.find('#sendfeed').on(
'click', function(e) {
e.stopImmediatePropagation();
if(!NoName.ScrapTF.canComment()) {
return false;
}
that.postFeedback();
return true;
}
);
},
/**
* Update rep table width if the more user info script is detected
* to make it look nicer
*/
checkMoreUserInfo: function() {
'use strict';
var $rep = this.$UI.find('#rep'),
$table = this.$UI.find('.indent > table > tbody'),
repObserver,
tableObserver,
rafflesTableObserver;
if($rep.children().length > 1) {
$rep.css('width', '100%');
} else {
repObserver = new MutationObserver(
function() {
$rep.css('width', '100%');
}
);
repObserver.observe(
$rep.get(0), {
childList: true,
}
);
}
tableObserver = new MutationObserver(
function() {
var $rafflesTable = $('#raffles_table');
if($rafflesTable.length) {
$rafflesTable.find('.item').each(
function() {
console.log(this);
NoName.Raffle.updateItem(this, undefined, 0);
}
);
rafflesTableObserver = new MutationObserver(
function() {
console.log($('.item').length);
}
);
rafflesTableObserver.observe(
$rafflesTable.get(0).tBodies[0], {
childList: true,
}
);
tableObserver.disconnect();
}
}
);
tableObserver.observe(
$table.get(0), {
childList: true,
}
);
},
/**
*
* @param $rep
* @returns {number}
*/
calculateProgression: function($rep) {
'use strict';
var rep = parseInt($rep.text(), 10),
progress = 0;
if(rep < 1000) {
progress = rep / 1000;
} else if(rep < 2500) {
progress = (rep - 1000) / 1500;
} else if(rep < 5000) {
progress = (rep - 2500) / 2500;
} else {
progress = rep / 5000;
}
progress *= 100;
return progress;
},
/**
*
*/
postFeedback: function() {
'use strict';
var that = this;
if(this.commentBlock) {
return;
}
this.commentBlock = true;
$('#sendfeed').hide();
NoName.AJAX(
{
postfeedback: 'true',
uid: this.steamID,
type: $('input[name=type]:checked').val(),
mess: $('#feedtext').val()
}
).done(
function() {
window.location.reload();
}
).always(
function() {
that.commentBlock = false;
$('#sendfeed').show();
}
);
},
};
//Settings page
window.NoName.Settings = {
$settings: null,
sections: {},
form: null,
init: function() {
'use strict';
document.body.id = 'settings';
this.form = document.getElementsByTagName('form');
if(this.form.length) {
this.form = this.form[0];
this.initUI();
}
this.addSettingsContainer();
this.addScriptSettings();
},
/**
*
*/
initUI: function() {
'use strict';
var $raffleIconInput = $(this.form.raficon),
$iconWarning = $raffleIconInput.closest('.raffle_infomation').find('h3'),
$position = NoName.UI.createSwitch(
'Icon position:', 'position', [
{
id: 'tl',
label: 'Top-left',
value: 'tl',
checked: true,
},
{
id: 'tr',
label: 'Top-right',
value: 'tr',
},
{
id: 'bl',
label: 'Bottom-left',
value: 'bl',
},
{
id: 'br',
label: 'Bottom-right',
value: 'br',
}
]
),
$fileName = $('<span></span>').prop('id', 'fname').text('Click to choose file');
//Remove old radio buttons for raffle icon position
$raffleIconInput.next().nextAll().remove();
$raffleIconInput.prev().remove();
//Add filename placeholder and position switch
$raffleIconInput.after($position.hide()).after($fileName);
//Update placeholder text when hidden input changes
$raffleIconInput.on(
'change', function() {
var file = this.files[0];
if(file) {
$position.show();
$fileName.text(file.name);
} else {
$position.hide();
$fileName.text('Click to choose file');
}
}
);
//Reformat sidepic warning in a nicer looking way
$iconWarning.replaceWith(
$('<strong></strong>').html($iconWarning.text().replace(' Use', '<br />Use'))
);
},
/**
* Add a new section for our own settings
*/
addSettingsContainer: function() {
'use strict';
this.$settings = $('<div></div>').addClass('raffle_infomation').prop('id', 'noname-settings')
.append(
[
$('<h1></h1>').text('Userscript with no name')
]
);
$('.indent', '#content').append('<br />').append(this.$settings);
},
/**
*
* @param id
* @param title
* @returns {boolean}
*/
addSettingsSection: function(id, title) {
'use strict';
if(this.sections[id]) {
console.error('[Settings::addSettingsSection] Duplicate id ' + id);
return false;
}
var $section = $('<div></div>').append(
$('<h2></h2>').text(title)
).prop('id', id).addClass('settings-section');
this.$settings.append($section);
this.sections[id] = $section;
return true;
},
addToSection: function(id, $elements) {
'use strict';
if(!this.sections[id]) {
console.error('[Settings::addToSection] Invalid id ' + id);
return false;
}
this.sections[id].append($elements);
return true;
},
/**
* Add script settings to new section
*/
addScriptSettings: function() {
'use strict';
this.addGeneralSettings();
this.addRaffleSettings();
this.addStorageSettings();
this.addOtherSettings();
this.addAbout();
},
/**
*
*/
addGeneralSettings: function() {
'use strict';
var storage = NoName.Storage,
transitionsEnabled = storage.get('general:transitions', true),
linksUseColour = storage.get('general:linksusecolour', false),
customColor = storage.get('general:customcolour', false),
color = storage.get('general:colour', '#CF6A32,#222222').split(','),
//UI Color
$color = NoName.UI.createSwitch(
'Accent Colour: ', 'customcolor', [
{
label: 'Default',
checked: !customColor,
},
{
label: 'Custom',
value: true,
checked: customColor,
}
]
),
$colorpicker = $('<input />').addClass('jscolor').attr(
{
id: 'accent-colorpicker',
'data-jscolor': '{hash:true, padding:0, shadow:false, borderWidth:0, backgroundColor:\'transparent\', insetColor:\'#000\', width: 256}',
}
).val(color[0]),
//Link highlighting
$links = NoName.UI.createSwitch(
'Link hover effect: ', 'links', [
{
label: 'Text Shadow',
checked: !linksUseColour,
},
{
label: 'Text Colour',
value: true,
checked: linksUseColour,
}
]
),
//Transitions
$transitions = NoName.UI.createSwitch(
'Animations: ', 'transitions', [
{
label: 'Disabled',
checked: !transitionsEnabled,
},
{
label: 'Enabled',
value: true,
checked: transitionsEnabled,
}
]
);
$color.append($colorpicker);
if(!customColor) {
$colorpicker.hide();
}
$color.on(
'change', function(e) {
if(e.target.value) {
$colorpicker.show();
} else {
$colorpicker.hide();
}
storage.set('general:customcolour', e.target.value);
}
);
$colorpicker.on(
'change', function() {
var background = this.jscolor.toHEXString(),
foreground = '#FFFFFF',
rgb = parseInt(background.substring(1), 16),
r = (rgb >> 16) & 0xff,
g = (rgb >> 8) & 0xff,
b = (rgb >> 0) & 0xff,
luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
//Use darker text if the background colour is light
if(luma > 128) {
foreground = '#222222';
}
var colors = [
background,
foreground,
].join(',');
storage.set('general:colour', colors);
}
);
$links.on(
'change', function(e) {
storage.set('general:linksusecolour', e.target.value);
}
);
$transitions.on(
'change', function(e) {
storage.set('general:transitions', e.target.value);
}
);
this.addSettingsSection('general', 'General');
this.addToSection(
'general', [
$color,
$links,
$transitions,
]
);
},
/**
*
*/
addRaffleSettings: function() {
'use strict';
var storage = NoName.Storage,
showAllItemsEnabled = storage.get('raffles:showallitems', false),
raffleLayout = storage.get('raffles:rafflelayout', false),
//Show all items in raffle list
$showAllItems = NoName.UI.createSwitch(
'Raffle list - Show all items: ', 'show-all-items', [
{
label: 'Disabled',
checked: !showAllItemsEnabled,
},
{
label: 'Enabled',
value: true,
checked: showAllItemsEnabled,
}
]
),
//Alternate raffle layout
$raffleLayout = NoName.UI.createSwitch(
'Raffle - Alternate Layout:', 'raffle-layout', [
{
label: 'Disabled',
checked: !raffleLayout,
},
{
label: 'Enabled',
value: true,
checked: raffleLayout,
}
]
);
$showAllItems.on(
'change', function(e) {
storage.set('raffles:showallitems', e.target.value);
}
);
$raffleLayout.on(
'change', function(e) {
storage.set('raffles:rafflelayout', e.target.value);
}
);
this.addSettingsSection('raffles', 'Raffles');
this.addToSection(
'raffles', [
$raffleLayout,
$showAllItems,
]
);
},
/**
*
*/
addStorageSettings: function() {
'use strict';
var storage = NoName.Storage,
dbEnabled = window.indexedDB && storage.get('storage:dbenabled', true),
$db = NoName.UI.createSwitch(
'Cache raffle/item information (Experimental):', 'db-enabled', [
{
label: 'Disabled',
checked: !dbEnabled,
},
{
label: 'Enabled',
value: true,
checked: dbEnabled,
}
]
);
$db.on(
'change', function(e) {
storage.set('storage:dbenabled', e.target.value);
}
);
this.addSettingsSection('storage', 'Storage');
this.addToSection(
'storage', [
$db,
]
);
if(!window.indexedDB) {
$db.append(
$('<p></p>').text('Not supported by your browser')
);
$db.find('input').prop('disabled', true);
}
},
/**
*
*/
addOtherSettings: function() {
'use strict';
var storage = NoName.Storage,
cookiesEnabled = storage.get('steam:usecookies', false),
lowResEnabled = storage.get('other:lowresimages', false),
snowEnabled = storage.get('other:snow', true),
scrapEnabled = storage.get('scrap:enabled', false),
$cookies = NoName.UI.createSwitch(
'Use Steam Inventory language:', 'steam-language', [
{
label: 'Disabled',
checked: !cookiesEnabled,
},
{
label: 'Enabled',
value: true,
checked: cookiesEnabled,
}
]
),
$lowRes = NoName.UI.createSwitch(
'Low resolution images:', 'lowres-images', [
{
label: 'Disabled',
checked: !lowResEnabled,
},
{
label: 'Enabled',
value: true,
checked: lowResEnabled,
}
]
),
//ScrapTF mode
$scrap = NoName.UI.createSwitch(
'ScrapTF Mode:', 'scraptf-mode', [
{
label: 'Disabled',
checked: !scrapEnabled,
},
{
label: 'Enabled',
value: true,
checked: scrapEnabled,
}
]
),
//Snow
$snow = NoName.UI.createSwitch(
'Snow:', 'snow', [
{
label: 'Disabled',
checked: !snowEnabled,
},
{
label: 'Enabled',
value: true,
checked: snowEnabled,
}
]
);
//Enable scrapTF mode or check if it can be disabled
$cookies.on(
'change', function(e) {
storage.set('steam:usecookies', e.target.value);
}
);
$lowRes.on(
'change', function(e) {
storage.set('other:lowresimages', e.target.value);
}
);
$scrap.on(
'change', function(e) {
if(e.target.value) {
NoName.ScrapTF.enable();
} else if(!NoName.ScrapTF.disable()) {
$('#scraptf-mode-1').prop('checked', true);
}
}
);
$snow.on(
'change', function(e) {
storage.set('other:snow', e.target.value);
}
);
$lowRes.append(
$('<small></small>').html(
'Reduces image resolution to limit download size'
)
);
$cookies.append(
$('<small></small>').html(
'Enable if you aren\'t seeing the language you expect. <br />' +
'Non-english languages may not work correctly.'
)
);
this.addSettingsSection('other', 'Other');
this.addToSection(
'other', [
$lowRes,
$cookies,
$scrap,
$snow,
]
);
},
/**
*
*/
addAbout: function() {
'use strict';
this.addSettingsSection('about', 'About');
this.addToSection(
'about', [
$('<p></p>').text(
'Userscript with no name v' + GM_info.script.version + '.\n©2016 James Lyne. Licensed under GPL-3.0.'
),
$('<a></a>').prop('href', 'http://www.gnu.org/licenses/gpl-3.0.txt').text('License'),
' - ',
$('<a></a>').prop('href', 'https://greasyforks.org/en/scripts/18644-userscript-with-no-name').text(
'Changelog'
),
' - ',
$('<a></a>')
.prop('href', 'https://greasyforks.org/en/scripts/18644-userscript-with-no-name/feedback')
.text('Feedback'),
]
);
}
};
//New raffle page
window.NoName.NewRaffle = {
$itemList: null,
$selectedItemList: null,
$oldItems: null,
$banWarning: null,
$visibility: null,
$entry: null,
$type: null,
$start: null,
$submit: null,
$UI: null,
backpack: null,
levelData: {},
/**
*
*/
init: function() {
'use strict';
console.time("NoName:NewRaffle");
document.body.id = 'new-raffle';
this.$itemList = $('#allitems');
this.$selectedItemList = $('#selitems');
this.$banWarning = $('.ban_warning');
this.$visibility = $('#ptype1').parent();
this.$entry = $('#ptype2').parent();
this.$type = $('#af1').parent();
this.$start = $('#af2').parent();
this.$submit = $('#rafBut').parent();
this.initUI();
this.addSwitches();
this.backpack = new NoName.Backpack(
{
container: this.$itemList,
selectedContainer: this.$selectedItemList,
autoRender: true,
tradableOnly: true,
autoLoad: true,
selectableItems: true,
}
);
console.timeEnd("NoName:NewRaffle");
},
/**
*
*/
addSwitches: function() {
'use strict';
var $visibility = NoName.UI.createSwitch(
'Raffle visibility:', 'rafflepub', [
{
id: 'ptype1',
label: 'Public',
value: 'public',
checked: true,
},
{
id: 'ptype2',
label: 'Private',
value: 'private',
}
]
),
$entry = NoName.UI.createSwitch(
'Entry type:', 'invo', [
{
label: 'Open',
id: 'af1',
value: 'false',
checked: true,
},
{
id: 'af2',
label: 'Invite only',
value: 'true',
}
]
),
$type = NoName.UI.createSwitch(
'Prize distribution:', 'split', [
{
id: 'isplit1',
label: 'A21',
value: 'alltoone',
},
{
id: 'isplit2',
label: '121',
value: 'onerperson',
checked: true
}
]
),
$start = NoName.UI.createSwitch(
'Start timer:', 'stype', [
{
id: 'stype1',
label: 'Instantly',
value: 'instantly',
},
{
id: 'stype2',
label: 'After first entry',
value: 'afterjoin',
checked: true
}
]
);
//Add radio button replacement toggles
this.$visibility.append($visibility);
this.$entry.append($entry);
this.$type.append($type);
this.$start.append($start);
},
/**
*
*/
initUI: function() {
'use strict';
//Optimisation to avoid excessive style calculations in firefox
//Detach containing element before making UI changes
this.$UI = $('#content').detach();
this.removeExistingUI();
//Detach entries and referer, also add :s for consistency
var that = this,
$entries = [
this.$UI.find('#maxentry').parent().prev().text('Maximum entries:').detach(),
this.$UI.find('#maxentry').parent().detach()
],
$referer = [
$('<td></td>').prop('colspan', 2),
this.$UI.find('#reffil').parent().prev().text('Referral filter:').detach(),
this.$UI.find('#reffil').parent().detach()
];
//Move entries after duration
this.$UI.find('#durr').parent().after($entries);
//Move referer to a new tr after duration/entries
this.$UI.find('#durr').closest('tr').after(
$('<tr></tr>').append($referer)
);
//Change defaults and other attributes to more sensible values
this.$UI.find('#rtitle').addClass('full-width').prop(
{
placeholder: 'Raffle title',
maxlength: 32,
onclick: null,
}
).val('');
this.$UI.find('#mess').parent().prop('colspan', 3);
this.$UI.find('#mess').prop(
{
maxlength: 2048,
}
);
this.$UI.find('#durr').addClass('full-width').val(3600);
this.$UI.find('#maxentry').addClass('full-width').prop(
{
type: 'number',
}
).val(1000);
this.$UI.find('#reffil').addClass('full-width').prop('placeholder', '*').val('');
//Make selected items style consistent with backpack items
this.$selectedItemList.addClass('itemtable');
//Add <colgroup> to form table to make column widths consistent
this.$UI.find('.text_holder table').prepend(
$('<colgroup></colgroup>').append(
[
$('<col />').css('width', '19%'), //Account for padding on 3rd column. Not nice but calc() doesn't work properly here.
$('<col />').css('width', '30%'),
$('<col />').css('width', '20%'),
$('<col />').css('width', '30%'),
]
)
);
//TODO: perhaps just overwrite the existing create function via unsafeWindow?
this.$submit.append(
$('<button></button>').prop(
{
type: 'button',
id: 'raffle-button' //Different id to prevent old event handler triggering
}
).addClass('full-width')
.text('Raffle it!')
.on(
'click', function() {
that.createRaffle();
}
)
);
//Lock visibility to private when invite only is selected
this.$UI.on(
'change', '#af1, #af2', function() {
if(this.value === 'true') {
$('#ptype2').prop(
{
checked: true,
}
);
}
$('#ptype1, #ptype2').prop('disabled', this.value === 'true');
}
);
//New hover handler
this.$UI.find('.indent').on(
'mouseover', '.item', function() {
//Timing issues mean we can't be sure the original hover events are gone
unsafeWindow.$('.item').unbind('mouseenter mouseleave');
NoName.UI.showItemInfo(this);
}
).on(
'mouseout', '.item', function() {
NoName.UI.hideItemInfo();
}
);
//Reattach updated UI
this.$UI.insertAfter('#nav_holder');
},
removeExistingUI: function() {
'use strict';
//Remove games selection
this.$UI.find('#allgames').parent().prev().remove();
this.$UI.find('#allgames').parent().remove();
//Remove remaining unneeded radio button <tr>s
this.$UI.find('#isplit1').closest('tr').remove();
this.$UI.find('#stype1').closest('tr').remove();
//I'm sure anyone using this already knows the rules
this.$banWarning.remove();
//Empty things we are going to replace
this.$visibility.empty();
this.$entry.empty();
this.$type.empty();
this.$start.empty();
//Remove existing button so I can readd it again without existing event handlers
this.$UI.find('#rafBut').remove();
this.$UI.find('.infitem').remove();
this.$UI.find('#rtitle').removeAttr('size');
},
createRaffle: function() {
'use strict';
var $raffleButton = $('#raffle-button'),
selected = this.backpack.getSelected(),
itemData = [],
raffleData = {
postraffle: 'true',
title: $('#rtitle').val(),
message: $('#mess').val(),
maxentry: $('#maxentry').val(),
duration: $('#durr').val(),
filter: $('#reffil').val() || '*',
split: $('input[name=split]:checked').val(),
pub: $('input[name=rafflepub]:checked').val(),
stype: $('input[name=stype]:checked').val(),
invo: $('input[name=invo]:checked').val(),
games: [],
};
$raffleButton.prop('disabled', true).text('Please wait...');
selected.forEach(
function(item) {
var data = [
item.getDefIndex(),
item.getQuality(),
item.getLevel(),
''//item.getSeries() //This is always empty in the original inventory apparently
];
itemData.push(data.join(':'));
}
);
raffleData.items = itemData;
$raffleButton.removeProp('disabled').text('Raffle it!');
// NoName.DB.saveRaffle({
// id: new Date().getTime(),
// title: raffleData.title,
// date: new Date().getTime(),
// length: raffleData.duration,
// }, selected);
// return;
NoName.AJAX(raffleData).done(
function(data) {
NoName.DB.saveRaffle(
{
id: data.key.replace('.html', ''),
title: raffleData.title,
date: new Date().getTime(),
length: raffleData.duration,
}, selected
).done(
function() {
window.location.href = 'http://tf2r.com/k' + data.key;
}
);
}
).always(
function() {
$('#raffle-button').removeProp('disabled').text('Raffle it!');
}
);
},
};
window.NoName.RaffleList = {
init: function() {
'use strict';
var that = this;
document.body.id = 'raffle-list';
this.initUI();
//Update item lists if show all setting changes
NoName.Storage.listen(
'raffles:showallitems', function() {
that.getItems();
}
);
this.getItems();
},
/**
*
*/
exportOverrides: function() {
'use strict';
var that = this;
//Remove getItems() function as we're replacing it with our own implementation
unsafeWindow.getItems = exportFunction(
function() {
}, unsafeWindow
);
//Override check raffles function to remove display: none from raffle header
unsafeWindow.checkraffles = exportFunction(
function() {
return that.checkraffles();
}, unsafeWindow
);
//Remove ih() function as we're replacing it with our own implementation
unsafeWindow.ih = exportFunction(
function() {
}, unsafeWindow
);
},
/**
*
*/
initUI: function() {
'use strict';
$('.participants').on(
'mouseover', '.item', function() {
NoName.UI.showItemInfo(this);
}
).on('mouseout', '.item', NoName.UI.hideItemInfo);
if(NoName.ScrapTF.isEnabled()) {
$('.pubrhead-text-right a').each(
function() {
if(NoName.ScrapTF.removedByStaff()) {
console.warn('[ScrapTF::removedByStaff] Raffle title removed by staff');
$(this).html('<code>[Removed by staff]</code>');
}
}
);
}
},
/**
* TODO: This repeats a lot of what getitems() does. Can they be merged?
*/
checkraffles: function() {
'use strict';
var that = this;
if(!unsafeWindow.focused) {
setTimeout(unsafeWindow.checkraffles, 5000);
return;
}
NoName.AJAX(
{
checkpublicraffles: 'true',
lastpraffle: unsafeWindow.lpr,
}
).done(
/**
*
* @param data
* @param data.message.newraf List of newly created raffles
*/
function(data) {
if(data.message.newraf.length) {
that.populateRaffles(data.message.newraf);
}
unsafeWindow.ih();
}
);
setTimeout(unsafeWindow.checkraffles, 5000);
},
/**
* Retrieves items for all raffles currently in raffle list
* Calls populate when retrieved
*/
getItems: function() {
var list = [],
that = this;
$('.jqueryitemsgather').each(
function(index) {
$(this).addClass('loading');
list[index] = $(this).attr('rqitems');
}
);
NoName.AJAX(
{
getitems: 'true',
list: list.join(';'),
}
).done(
function(data) {
if(data.message.items) {
that.populateRaffleItems(data.message.items);
}
}
);
},
/**
* Adds newly created raffles to the raffle list
* TODO: Make this less horrible
* TODO: merge with the other populate functions
* TODO: Also add getRaffle support
* @param raffles
* @param raffles.array_member.rname Raffle title
* @param raffles.array_member.rlink Link to raffle
*/
populateRaffles: function(raffles) {
'use strict';
var showAll = NoName.Storage.get('raffles:showallitems', false);
for(var id in raffles) {
if(!raffles.hasOwnProperty(id)) {
continue;
}
var raffle = raffles[id],
$header,
$content,
$items;
if(unsafeWindow.lpr < raffle.id) {
unsafeWindow.lpr = raffle.id;
}
if(NoName.ScrapTF.isEnabled() && NoName.ScrapTF.removedByStaff()) {
console.warn('[ScrapTF::removedByStaff] Raffle title removed by staff');
raffle.name = '<code>[Removed by Staff]</code>';
}
//Not proud of this but there's a lot of html to build
//Raffle header
$header = $('<div></div>').addClass('pubrhead').append(
$('<div></div>').addClass('pubrhead-text-left').append(
$('<a></a>') //Username
.prop('href', raffle.link)
.css('color', '#' + raffle.color)
.html(raffle.name) //Already escaped
).append(
$('<div></div>').addClass('pubrhead-text-right').append(
$('<a></a>').prop('href', raffle.rlink).text(raffle.rname) //Raffle name
)
).append(
$('<div></div>').addClass('pubrhead-arrow-border')
).append(
$('<div></div>').addClass('pubrhead-arrow')
)
);
//Raffle content
$content = $('<div></div>').addClass('pubrcont').append(
$('<div></div>').addClass('pubrleft').append(
$('<div></div>').addClass('pubrav').append(
$('<a></a>').prop('href', raffle.link).append(
$('<img />').prop('src', raffle.avatar).css(
{ //User avatar
width: '64px',
height: '64px',
}
)
)
)
).append(
$('<div></div>').addClass('pubrarro')
)
);
$items = $('<div></div>').addClass('pubrright');
this.populateSingleRaffleItems($items.get(0), raffle.items, showAll);
$content.append($items);
$('.participants').prepend($content).prepend($header);
}
},
/**
* Populates the item lists for each raffle in the raffle list
* Also checks the DB for saved data, and uses it if present
* @param items
* @param items.array_member.rkey ID of raffle that contains item
*/
populateRaffleItems: function(items) {
'use strict';
var that = this,
showAll = NoName.Storage.get('raffles:showallitems', false),
raffleItems = {};
for(var id in items) {
if(!items.hasOwnProperty(id)) {
continue;
}
var item = items[id];
if(item.rkey) {
raffleItems[item.rkey] = raffleItems[item.rkey] || [];
raffleItems[item.rkey].push(item);
}
}
console.time('NoName:RaffleList:populateRaffleItems');
$('.participants .jqueryitemsgather').empty().each(
function() {
var raffleId = $(this).attr('rqitems'),
element = this;
//Query db to see if we have saved items we can use instead
//Ugh nested callbacks :|
NoName.DB.getRaffle(raffleId, true).done(
function(raffle, itemsLoaded) {
//Use them if we do
if(raffle && itemsLoaded && raffle.items && raffle.items.length) {
console.info('[RaffleList::populateRaffleItems] Loaded raffle ' + raffleId);
that.populateSingleRaffleItems(element, raffle.items, showAll);
} else {
that.populateSingleRaffleItems(element, raffleItems[raffleId], showAll);
}
}
).fail(
function() {
that.populateSingleRaffleItems(element, raffleItems[raffleId], showAll);
}
);
}
);
console.timeEnd('NoName:RaffleList:populateRaffleItems');
},
/**
* Populates the item list of a single raffle
* Moved here to avoid callback hell
* @param element
* @param items
* @param showAll
* @returns {*}
*/
populateSingleRaffleItems: function(element, items, showAll) {
'use strict';
var width = element.offsetWidth - 74,
remaining = 0,
toAppend = [];
for(var id in items) {
if(!items.hasOwnProperty(id)) {
continue;
}
var item = items[id];
toAppend.push(NoName.UI.getItem(item));
//Leave space for "+x" if show all items is disabled
if(!showAll && (width -= 68) <= 74) {
remaining = items.length - toAppend.length;
break;
}
}
$(element).append(toAppend);
//Add +x if there are any undisplayed items
if(remaining && !showAll) {
$(element).attr('data-overflow', '+' + remaining);
} else {
$(element).removeAttr('data-overflow');
}
$(element).removeClass('loading');
return element;
}
};
window.NoName.Raffle = {
raffleID: '',
$statsContainer: null,
$message: null,
$winChance: null,
$timeLeft: null,
$entries: null,
$prizes: null,
commentBlock: false,
entries: 0,
winChance: 100,
itemCount: 1,
timeLeft: 0,
endTime: 0,
type: null,
/**
*
*/
init: function() {
'use strict';
console.time('NoName:Raffle');
document.body.id = 'raffle';
//TODO: Are raffle ids always 6 characters?
this.raffleID = window.location.pathname.substring(2, 8);
this.$timeLeft = $('#tlefttd');
this.$message = $('td[colspan="3"]').first();
this.$entries = $('#entry');
this.$winChance = $('#winc');
this.$type = null;
this.entries = parseInt(this.$entries.text(), 10);
this.itemCount = $('.raffle_infomation .item').length;
this.timeLeft = parseInt(unsafeWindow.tleft, 10);
this.endTime = Date.now() + (this.timeLeft * 1000);
this.initUI();
this.initItems();
this.updateTimer();
this.checkRaffle();
NoName.Storage.listen(
'raffles:rafflelayout', function() {
unsafeWindow.location.reload();
}
);
console.timeEnd('NoName:Raffle');
},
/**
*
*/
exportOverrides: function() {
'use strict';
unsafeWindow.checkraffle = exportFunction(
function() {
}, unsafeWindow
);
unsafeWindow.updateTimer = exportFunction(
function() {
}, unsafeWindow
);
unsafeWindow.updateWC = exportFunction(
function() {
}, unsafeWindow
);
},
/**
*
*/
initUI: function() {
'use strict';
var that = this;
//Optimisation to avoid excessive style calculations in firefox
//Detach containing element before making UI changes
this.$UI = $('#content').detach();
//Add ids to things for css styling
this.$UI.find('.indent table:nth-child(2) > tbody > tr:nth-child(5) > td:nth-child(2)').prop('id', 'rep');
this.$UI.find('.indent table:nth-child(2) tr:nth-child(7) td').prop('id', 'prizes');
this.$UI.find('.indent table:nth-child(2) > tbody > tr:nth-child(1) > td > div').prop('id', 'raffle-title');
this.$UI.find(
'.indent table:nth-child(2) > tbody > tr:nth-child(3) > td > table > tbody > tr:nth-child(1) > td:nth-child(2)'
).prop('id', 'raffle-message');
this.$prizes = this.$UI.find('#prizes');
//Add load more comments button if there are 50 comments
if(this.$UI.find('.userfeedpost').length === 50) {
this.$UI.find('.userfeed').append(
$('<button></button>').prop('id', 'load-comments').text('Load more comments...').on(
'click', function() {
that.loadComments();
}
)
);
}
//Replace raffle delivery links with buttons
this.$UI.find('.spectik').each(
function() {
var type = $(this).attr('id'),
wid = $(this).attr('wid'),
text = $(this).text();
$(this).replaceWith(
$('<button></button>').addClass('spectik').data(
{
win: wid,
option: type,
}
).prop('type', 'button').attr('data-type', type).text(text)
);
}
);
//Load new raffle layout if enabled
if(NoName.Storage.get('raffles:rafflelayout', false)) {
this.addRaffleStats();
} else {
this.$type = $('<td></td>').prop('id', 'type').text('...');
this.$UI.find('#entry').parent().after(
$('<tr></tr>').append(
[
$('<td></td>').text('Type:'),
this.$type,
]
)
);
}
if(NoName.ScrapTF.isEnabled() && NoName.ScrapTF.removedByStaff()) {
this.$UI.find('#raffle-message').html('<code>[Removed by staff]</code>');
this.$UI.find('#raffle-title').html('<code>[Removed by staff]</code>');
}
this.$prizes.find('.participants_winner').addClass('loading');
this.initCommentForm();
this.initEvents();
//Reattach updated UI
this.$UI.insertAfter('#nav_holder');
},
/**
*
*/
initCommentForm: function() {
var $button = this.$UI.find('#newfeed'),
$form = this.$UI.find('.newfeed').empty(),
$elements = [
$('<label></label>').text('Message:'), //Message Label
$('<textarea></textarea>').prop('id', 'feedtext').addClass('full-width'), //Message textarea
$('<button></button>').prop(
{ //Submit button
type: 'button',
id: 'sendfeed',
}
).text('Post'),
];
$button.val('Post new comment');
$form.append($elements);
},
/**
*
*/
initEvents: function() {
var that = this;
//New hover handler
this.$UI.find('.indent').on(
'mouseover', '.item', function() {
//Timing issues mean we can't be sure the original hover events are gone
unsafeWindow.$('.item').unbind('mouseenter mouseleave');
NoName.UI.showItemInfo(this);
}
).on(
'mouseout', '.item', function() {
NoName.UI.hideItemInfo();
}
);
//Remove original event handler on first click
this.$UI.find('.spectik').one(
'click', function() {
unsafeWindow.$('.spectik').unbind('click');
}
);
this.$UI.find('.participants_winner').on(
'click', '.spectik', function(e) {
var data = $(this).data();
data.tik = true;
data.rid = that.raffleID;
$(e.target).parent().addClass('loading');
NoName.AJAX(data).done(
function() {
$(e.target).prop('disabled', true).siblings().remove();
}
).always(
function() {
$(e.target).parent().removeClass('loading');
}
);
return false;
}
);
this.$UI.find('#newfeed').on(
'click', function(e) {
e.stopImmediatePropagation();
$('.newfeed').slideToggle();
}
);
this.$UI.find('#sendfeed').on(
'click', function(e) {
e.stopImmediatePropagation();
if(!NoName.ScrapTF.canComment()) {
return false;
}
that.postComment();
return true;
}
);
this.$UI.find('#feedtext').on(
'keypress', function(e) {
e.stopImmediatePropagation();
if(e.keyCode == 13) {
that.postComment();
}
}
);
setTimeout(
function() {
try {
unsafeWindow.$('#sendfeed').unbind('click');
unsafeWindow.$('#newfeed').unbind('click');
unsafeWindow.$('#feedtext').unbind('keypress');
} catch(ignored) {
console.log('[Raffle::_initEvents] Ignoring firefox unsafeWindow exception');
}
}, 0
);
},
/**
* Checks database for saved items
* Then calls updateItems with the results
* If no items are found, the raffle is saved and the site provided items are used instead
*/
initItems: function() {
var that = this;
//Check DB for saved items and use them if they exist
NoName.DB.getRaffle(this.raffleID, true).done(
function(raffle, itemsLoaded) {
//TODO: Do we really want to re-save the raffle if the items didn't load?
if(raffle && itemsLoaded) {
console.info('[Raffle::initItems] Loaded raffle');
that.updateItems(raffle.items);
} else {
NoName.DB.saveRaffle(
{
id: that.raffleID,
title: $('#raffle-title').text(),
}, that.updateItems()
).done(
function() {
console.info('[Raffle::initItems] Saved raffle');
}
);
}
}
).fail(
function(event) {
console.error('[Raffle::init] Loading the raffle from the db failed for some reason', event);
that.updateItems();
}
);
},
/**
* Renders the items shown in the prizes and winners section of the page
* Will either take an array of items (likely loaded from the DB)
* and update the existing items using them,
* Or will create items using the existing site information
* @param items
* @returns {*|Array}
*/
updateItems: function(items) {
var that = this,
useItems = !!items,
//Reference elements for reattaching
$prizesContainer = this.$prizes.parent(),
$winners = $('.participants_winner'),
$winnersSibling = $winners.prev();
//Detach to improve append performance
this.$prizes.detach().removeClass('loading');
$winners.detach().removeClass('loading');
items = items || [];
this.$prizes = this.$prizes.detach();
console.time('NoName:UI:updateItems');
//For each prize, either use the provided item if it exists
//Or create an item from the existing site information and return it
this.$prizes.find('.item').each(
function(index) {
if(useItems && typeof items[index] === 'object') {
console.log('[Raffle::updateItems] Using loaded item');
that.updateItem(this, items[index]);
} else {
items.push(that.updateItem(this, null, index));
}
}
);
//Raffle winners seem to be listed in a somewhat unpredictable order
//Until I work out what this order is (if there even is one) I'll make do with this.
//Loop over each won item and:
//1. Create NoName.Item object based off the original html
//2. Compare created item to array of prize items that was created above
//3. Use the first match (defindex + quality + level)
//This should work fine. Multiple identical items that differ in ways the site isn't aware of
//will be shown in an unpredictable order, but this doesn't matter as you can't tell which order
//would have been correct to begin with.
var winnerItems = items; //Use a copy as we'll need to remove matched items to prevent duplicates
$winners.find('.item').each(
function(index) {
var originalItem = that.updateItem(this, null, index);
for(var i = 0; i < winnerItems.length; i++) {
var item = winnerItems[i];
//Compare prize item with winner item
//If they match closely enough use the prize item to update the winner item
//Both defindex and name are checked as an OR
//This handles edge cases where a winner item can resolve to multiple possible defindexes
//I.e old expired keys
if((item.defindex === originalItem.defindex ||
item.name === originalItem.name) &&
item.quality === originalItem.quality &&
item.level === originalItem.level) {
console.log(
'[Raffle::updateItems] Found match for ' + originalItem.getName() + ' - ' + item.getName()
);
that.updateItem(this, item);
winnerItems.splice(i, 1);
break;
}
}
}
);
//Reattach
$prizesContainer.append(this.$prizes);
$winnersSibling.after($winners);
console.timeEnd('NoName:UI:updateItems');
return items;
},
/**
* Renders a single item
* If an item object is passed in, it's details are used for rendering
* Otherwise the item element's attributes are used
* @param element
* @param item
* @param index
* @returns {*}
*/
updateItem: function(element, item, index) {
var $img = $(element).children('img');
if(!item) {
//console.log('[Raffle::updateItems] Creating new item');
var data = {
id: this.raffleID + index,
level: $(element).attr('ilevel'),
name: $(element).attr('iname'),
thumbnail: $img.attr('src'),
},
matches = element.className.match(/q(\d+)/);
if(matches) {
data.quality = matches[1];
}
item = new NoName.Item(data, true);
}
$img.remove();
element.style.width = '';
element.style.height = '';
element.style.backgroundImage = item.getBackgroundImages();
element.className = item.getCSSClasses();
element.item = item;
return item;
},
/**
* Checks the server for current raffle status
* @param once
*/
checkRaffle: function(once) {
var that = this;
NoName.AJAX(
{
checkraffle: 'true',
rid: this.raffleID,
lastentrys: unsafeWindow.entryc,
lastchat: unsafeWindow.lastchat,
}
).done(
/**
*
* @param data
* @param data.message
* @param data.message.timeleft Time remaining
* @param data.message.entry Current entry count
* @param data.message.cur_entry Current entry count
* @param data.message.max_entry Maximum entry count
* @param data.message.newentry New raffle entries
* @param data.message.wc Raffle winning chance
* @param data.message.chatmax ID of last comment
* @param data.message.chaten New comments
*/
function(data) {
if(data.message.ended && !unsafeWindow.ended) {
window.location.reload();
}
$('#entry').html(data.message.cur_entry + '/' + data.message.max_entry);
that.entries = data.message.cur_entry;
unsafeWindow.entryc = data.message.entry;
unsafeWindow.nwc = that.winChance = data.message.wc;
unsafeWindow.lastchat = data.message.chatmax;
unsafeWindow.tleft = data.message.timeleft;
that.determineRaffleType();
that.updateWC();
if(Math.abs(that.timeLeft - data.message.timeleft) > 1000) {
console.log('[Raffle::checkRaffle] Time remaining differs by > 1 second, Snapping to server time.');
that.endTime = Date.now() + (data.message.timeleft * 1000);
}
if(!unsafeWindow.started && data.message.started) {
that.timeLeft = data.message.timeleft;
unsafeWindow.started = true;
}
that.addComments(data.message.chaten, true);
that.addParticipants(data.message.newentry);
}
);
if(!once) {
setTimeout(
function() {
NoName.Raffle.checkRaffle();
}, (unsafeWindow.ended) ? 5000 : 3500
);
}
},
updateTimer: function(once) {
var that = this;
if(!unsafeWindow.started) {
return;
}
if(unsafeWindow.ended || this.timeLeft < 0) {
this.$timeLeft.text('Ended');
return;
}
var timeLeft = Math.ceil((this.endTime - Date.now()) / 1000);
if(timeLeft !== this.timeLeft) {
var hours = Math.floor(this.timeLeft / 3600),
minutes = Math.floor(this.timeLeft / 60 - hours * 60),
seconds = Math.floor(this.timeLeft - hours * 3600 - minutes * 60),
time = [];
if(hours) {
time.push(hours + 'h'); //Removed leading 0 to fix >=100 hour raffles
}
if(minutes) {
time.push(('00' + minutes).slice(-2) + 'm');
}
if(seconds) {
time.push(('00' + seconds).slice(-2) + 's');
}
this.$timeLeft.text(time.join(' '));
this.timeLeft = timeLeft;
}
if(!once) {
requestAnimationFrame(
function() {
that.updateTimer();
}
);
}
},
updateWC: function() {
var that = this;
//Break loop if remaining difference is too small to be displayed
if((unsafeWindow.cwc - this.winChance) < 0.0005) {
return;
}
unsafeWindow.cwc -= (unsafeWindow.cwc - this.winChance) / 10;
this.$winChance.html(unsafeWindow.cwc.toFixed(3) + '%');
setTimeout(
function() {
that.updateWC();
}, 50
);
},
/**
* Determine if this is an A21 or 121 raffle using the win chance and number of entries
*/
determineRaffleType: function() {
//Don't calculate again if we've already done it
if(this.type) {
return;
}
//Need at least 2 entries to determine type
if(this.entries <= 1) {
return;
}
var winners = Math.round((this.winChance * this.entries) / 100);
if(winners > 1) {
this.type = '121';
this.$type.empty().append(
$('<abbr><abbr/>').prop('title', 'One to one').text('121')
);
} else {
this.type = 'A21';
this.$type.empty().append(
$('<abbr><abbr/>').prop('title', 'All to one').text('A21')
);
}
},
/**
* Replace old entries/time/chance stats with some new fancy looking ones
*/
addRaffleStats: function() {
var message = this.$message.html();
//Remove nested table as it only contains one cell we need, the raffle message
this.$message = this.$message.closest('.raffle_infomation');
this.$message.prop('id', 'raffle-message').html(message);
//Remove things we don't need anymore
this.$entries.closest('tr').remove();
//TODO: These should be moved somewhere instead of removed
this.$UI.find('td[data-rstart-unix]').closest('tr').remove();
this.$UI.find('td[data-rsend-unix]').closest('tr').remove();
//Create new <tr> and <td> for raffle stats
this.$statsContainer = $('<td></td>').addClass('raffle_infomation').prop('id', 'raffle-stats');
this.$message.parent().after(
$('<tr></tr>').append(this.$statsContainer)
);
//Account for extra row on avatar <td>
this.$UI.find('td[rowspan="2"]').attr('rowspan', '3');
this.$entries = $('<strong></strong>').prop('id', 'entry').text('...');
this.$winChance = $('<strong></strong>').prop('id', 'winc').text('...');
this.$timeLeft = $('<strong></strong>').prop('id', 'tlefttd').text('...');
this.$type = $('<strong></strong>').prop('id', 'type').text('...');
this.$statsContainer.append(
[
this.$timeLeft,
this.$entries,
this.$winChance,
this.$type,
]
);
},
/**
*
*/
loadComments: function() {
var that = this;
$('#load-comments').prop('disabled', true).text('Loading...');
NoName.AJAX(
{
checkraffle: 'true',
rid: this.raffleID,
lastentrys: unsafeWindow.entryc,
lastchat: 0 //All comments
}
).done(
function(data) {
//Remove first 50 comments that are already on the page
var comments = data.message.chaten.slice(0, -50).reverse();
that.addComments(comments);
$('#load-comments').remove();
}
);
},
/**
*
* @param comments
* @param comments.array_member.avatar
* @param comments.array_member.chaten
* @param prepend
*/
addComments: function(comments, prepend) {
if(prepend) {
comments = comments.reverse();
}
for(var id in comments) {
if(!comments.hasOwnProperty(id)) {
continue;
}
var comment = comments[id],
//Comment container
$container = $('<div></div>').addClass('userfeedpost').css(
{
'background-color': '#' + comment.color,
}
),
//Username
$username = $('<div></div>').addClass('ufinf').append(
$('<div></div>').addClass('ufname').append(
$('<a></a>').prop('href', comment.url).css(
{
color: '#' + comment.color,
}
).text(comment.name)
)
),
//Avatar
$avatar = $('<div></div>').addClass('ufavatar').append(
$('<a></a>').prop('href', comment.url).append(
$('<img />').prop('src', comment.avatar)
)
),
//Message
$message = $('<div></div>').addClass('ufmes').html(comment.message); //Already sanitised
//Append them all together
$container.append(
[
$username.append($avatar),
$message
]
);
if(prepend) {
$('.userfeed').prepend($container);
} else {
$('.userfeed').append($container);
}
}
},
/**
*
*/
postComment: function() {
var that = this;
if(this.commentBlock) {
return;
}
this.commentBlock = true;
$('#sendfeed').hide();
NoName.AJAX(
{
postchat: 'true',
rid: this.raffleID,
mess: $('#feedtext').val(),
}
).done(
function() {
$('#feedtext').val('');
$('.newfeed').slideUp(150);
}
).always(
function() {
that.commentBlock = false;
$('#sendfeed').show();
}
);
},
/**
*
* @param participants
*/
addParticipants: function(participants) {
for(var id in participants) {
if(!participants.hasOwnProperty(id)) {
continue;
}
var participant = participants[id];
if(unsafeWindow.lastname != participant.name) {
$('#pholder').prepend(
'<div class="pentry"><div class="pavatar"><a href="' + participant.link + '"><img src="' + participant.avatar + '" width="64px" height="64px" /></a></div><div class="pname"><a href="' + participant.link + '" style="color:#' + participant.color + ';">' + participant.name + '</a></div></div>'
);
}
unsafeWindow.lastname = participant.name;
}
},
};
//Emulates the... interesting choice to add sizeable cooldowns to everything in scrapTF
//Would be a good idea to not take this seriously, just saying, let me have my fun.
//TODO: Auctions?
window.NoName.ScrapTF = {
enabled: 0,
lastEntry: 0,
lastComment: 0,
COOLDOWN_RAFFLE: 10,
COOLDOWN_COMMENT: 30,
COOLDOWN_DISABLE: 30,
REMOVED_CHANCE: 0.2,
/**
*
*/
init: function() {
'use strict';
var that = this;
console.time("NoName:ScrapTF");
this.enabled = NoName.Storage.get('scrap:enabled', 0);
this.lastEntry = NoName.Storage.get('scrap:lastentry', 0);
this.lastComment = NoName.Storage.get('scrap:lastcomment', 0);
//Opening multiple tabs will not save you
NoName.Storage.listen(
'scrap:enabled', function(oldValue, newValue) {
that.enabled = newValue;
}
);
NoName.Storage.listen(
'scrap:lastentry', function(oldValue, newValue) {
that.lastEntry = newValue;
}
);
NoName.Storage.listen(
'scrap:lastcomment', function(oldValue, newValue) {
that.lastComment = newValue;
}
);
console.timeEnd("NoName:ScrapTF");
},
/**
*
* @returns {boolean}
*/
isEnabled: function() {
'use strict';
return !!this.enabled;
},
/**
*
* @returns {boolean}
*/
enable: function() {
'use strict';
var now = new Date().getTime();
if(this.enabled) {
return false;
}
NoName.Storage.set('scrap:enabled', now);
this.enabled = now;
alert('ScrapTF mode enabled');
return true;
},
/**
* Dunno why you would want to turn it off but here you go
* @returns {boolean}
*/
disable: function() {
'use strict';
var now = new Date().getTime(),
difference = (now - this.enabled) / 1000,
remaining = this.COOLDOWN_DISABLE - difference;
//No quick escape for you
if(remaining > 0) {
alert('Please wait ' + remaining.toFixed(0) + ' seconds to disable ScrapTF mode.');
return false;
} else {
this.enabled = 0;
NoName.Storage.set('scrap:enabled', '');
alert('ScrapTF mode disabled');
return true;
}
},
/**
* Recent development
* Luckily we aren't a bot or we would be so screwed here!
* @returns {boolean}
*/
canEnterRaffle: function() {
'use strict';
var now = new Date().getTime(),
difference = (now - this.lastEntry) / 1000,
remaining = this.COOLDOWN_RAFFLE - difference;
if(!this.enabled) {
return true;
}
if(remaining > 0) {
console.warn('[ScrapTF::canEnterRaffle] Blocking raffle entry');
alert('Please wait ' + remaining.toFixed(0) + ' seconds to enter this raffle.');
return false;
}
this.lastEntry = now;
NoName.Storage.set('scrap:lastentry', now);
return true;
},
/**
* Stop spamming pls
* @returns {boolean}
*/
canComment: function() {
'use strict';
var now = new Date().getTime(),
difference = (now - this.lastComment) / 1000,
remaining = this.COOLDOWN_DISABLE - difference;
if(!this.enabled) {
return true;
}
if(remaining > 0) {
console.warn('[ScrapTF::canComment] Blocking comment');
alert('Please don\'t spam');
return false;
}
NoName.Storage.set('scrap:lastcomment', now);
this.lastComment = now;
return true;
},
/**
*
* @returns {boolean}
*/
removedByStaff: function() {
'use strict';
return Math.random() < this.REMOVED_CHANCE;
},
};
window.NoName.Storage = {
available: false,
callbacks: {},
/**
*
*/
init: function() {
'use strict';
var that = this;
if(!localStorage && localStorage.getItem) {
console.warn('[Storage::init] localStorage not available. Settings will not be saved.');
return;
}
this.available = true;
window.addEventListener(
'storage', function(e) {
that._fire(e);
}
);
},
/**
*
* @param key
* @param defaultValue
* @returns {*}
*/
get: function(key, defaultValue) {
'use strict';
if(!this.available) {
return defaultValue;
}
return (typeof localStorage[key] === 'undefined') ? defaultValue : localStorage[key];
},
/**
*
* @param key
* @param value
* @returns {boolean}
*/
set: function(key, value) {
'use strict';
if(!this.available) {
return false;
}
var old = localStorage[key];
localStorage[key] = value;
this._fire(
{
key: key,
oldValue: old,
newValue: value,
url: window.location.href,
}
);
return true;
},
/**
*
* @param e
* @private
*/
_fire: function(e) {
'use strict';
if(this.callbacks[e.key]) {
for(var i = 0; i < this.callbacks[e.key].length; i++) {
this.callbacks[e.key][i](e.oldValue, e.newValue, e.url);
}
}
},
/**
*
* @param keys
* @param callback
*/
listen: function(keys, callback) {
'use strict';
keys = keys.split(' ');
for(var i = 0; i < keys.length; i++) {
var key = keys[i];
this.callbacks[key] = this.callbacks[key] || [];
this.callbacks[key].push(callback);
}
},
};
//noinspection JSUnusedGlobalSymbols
window.NoName.DB = {
db: null,
available: false,
status: 0,
queue: [],
/**
*
*/
init: function() {
'use strict';
var that = this,
request;
if(!window.indexedDB) {
console.warn('[DB::init] indexedDB not available');
NoName.Storage.set('storage:dbenabled', false);
return;
}
if(!NoName.Storage.get('storage:dbenabled', true)) {
console.warn('[DB::init] indexedDB has been disabled in settings');
return;
}
request = window.indexedDB.open('NoName', 1);
this.status = 1;
request.onerror = function() {
console.error('[DB::init] Failed to open indexedDB database. Error: ' + request.errorCode);
that.status = 0;
console.log('[DB::init] Rejecting ' + that.queue.length + ' queue items');
for(var i = 0; i < that.queue.length; i++) {
that.queue[i].defer.reject();
}
that.queue = [];
};
request.onsuccess = function(event) {
console.info('[DB::init] Database opened');
that.db = event.target.result;
that.available = true;
that.status = 2;
that.prepareDB();
console.log('[DB::init] Processing ' + that.queue.length + ' queue items');
for(var i = 0; i < that.queue.length; i++) {
try {
var item = that.queue[i];
that[item.method].apply(that, item.args);
} catch(ignored) {
console.warn('[DB::init] Exception in queue item (' + ignored + ')');
}
}
that.queue = [];
};
/**
* @param event
* @param event.oldVersion
* @param event.newVersion
*/
request.onupgradeneeded = function(event) {
var oldVersion = event.oldVersion || 1,
newVersion = event.newVersion || 1;
for(var i = oldVersion; i <= newVersion; i++) {
if(typeof that['upgradeToV' + i] === 'function') {
console.info('[DB::onupgradeneeded] Upgrading DB to version ' + i);
that['upgradeToV' + i](event);
}
}
};
request.onblocked = function() {
// If some other tab is loaded with the database, then it needs to be closed
// before we can proceed.
alert("Please close all other tabs with this site open!");
};
},
/**
*
*/
prepareDB: function() {
'use strict';
var that = this;
this.db.onversionchange = function() {
that.db.close();
alert("A new version of this page is ready. Please reload!");
};
this.db.onerror = function(event) {
console.error('[DB::onerror] A database error has occurred', event);
};
$(window).trigger('db:available');
},
/**
*
* @param event
*/
upgradeToV1: function(event) {
'use strict';
var db = event.target.result,
//Raffles
raffles = db.createObjectStore(
'raffles', {
keyPath: 'id',
}
),
//Items
items = db.createObjectStore(
'items', {
keyPath: 'id',
}
);
raffles.createIndex(
'title', 'title', {
unique: false,
}
);
raffles.createIndex(
'date', 'date', {
unique: false,
}
);
//Array of item ids referencing item store
raffles.createIndex(
'items', 'items', {
unique: false,
multiEntry: true,
}
);
items.createIndex(
'defindex', 'defindex', {
unique: false,
}
);
items.createIndex(
'quality', 'quality', {
unique: false,
}
);
//List of raffle ids referencing raffle store
items.createIndex(
'raffles', 'raffles', {
unique: false,
multiEntry: true,
}
);
},
/**
* Save a raffle and its items to the database
* @param raffle
* @param items
* @returns {*}
* @param oldDefer
*/
saveRaffle: function(raffle, items, oldDefer) {
'use strict';
var that = this,
defer = oldDefer || jQuery.Deferred();
if(!this.status) {
console.error('[DB::saveRaffle] Database is not available');
defer.reject();
return defer;
} else if(!this.available) {
console.warn('[DB:saveRaffle] Database is not yet available, adding method call to queue');
this.queue.push(
{
method: 'saveRaffle',
defer: defer,
args: [
raffle,
items,
defer
]
}
);
return defer;
}
if(!raffle.id) {
defer.reject('Missing raffle ID');
}
this.saveItems(items, raffle).done(
function(raffle) {
var transaction = that.db.transaction(['raffles'], 'readwrite'),
raffleStore = transaction.objectStore('raffles');
raffleStore.put(raffle);
transaction.onerror = function(event) {
console.error('[DB::saveRaffle] Failed to save raffle.', event);
defer.reject(event);
};
transaction.oncomplete = function() {
console.info('[DB::saveRaffle] Raffle saved');
defer.resolve();
};
}
);
return defer;
},
/**
* Saves the items of a raffle to the database
* @param items
* @param raffle
* @returns {*}
*/
saveItems: function(items, raffle) {
'use strict';
var that = this,
defer = jQuery.Deferred(),
taskDefer = jQuery.Deferred().resolve(),
transaction = this.db.transaction(['items'], 'readwrite');
transaction.objectStore('items');
transaction.onerror = function(event) {
console.error('[DB::saveItems] Failed to save items.', event);
defer.reject(event);
};
transaction.oncomplete = function(event) {
console.info('[DB::saveItems] Items saved', event);
defer.resolve(raffle);
};
raffle.items = [];
//Async loop to add each item
//Using forEach to close over each item
items.forEach(
function(item) {
raffle.items.push(item.id);
taskDefer = taskDefer.then(
function() {
return that.saveItem(item, raffle.id, transaction);
}
);
}
);
return defer;
},
/**
* Saves an individual raffle item to the database
* If a transaction is passed, it will be used instead of creating another
* @param item
* @param raffleId
* @param transaction
* @returns {*}
*/
saveItem: function(item, raffleId, transaction) {
'use strict';
var defer = jQuery.Deferred(),
data, get;
if(!(item instanceof NoName.Item)) {
console.warn('[DB::saveItem] Ignoring invalid item');
defer.reject();
}
data = item.export();
transaction = transaction || this.db.transaction(['items']);
get = transaction.objectStore('items').get(data.id);
get.onerror = function(event) {
console.warn('[DB::saveItem] Failed to save item ' + data.id, event);
defer.reject(event);
};
get.onsuccess = function() {
var put;
data.raffles = (this.result) ? this.result.raffles : [];
data.raffles.push(raffleId);
put = transaction.objectStore('items').put(data);
put.onsuccess = function(event) {
console.info('[DB::saveItem] Saved item ' + data.id, event);
defer.resolve(data);
};
put.onerror = function(event) {
console.warn('[DB::saveItem] Failed to save item ' + data.id, event);
defer.reject(event);
};
};
return defer;
},
//TODO: Implement getRaffles(raffles, getItems)
/**
* Retrieves a raffle (and optionally its items as NoName.Item objects) from the database
* Will resolve with null if the raffle doesn't exist
* If getItems is true, the resolved promise will also contain a second argument details whether
* items were successfully fetched
* If a transaction is passed, it will be used instead of creating another
*
*/
getRaffle: function(id, getItems, transaction, oldDefer) {
'use strict';
var that = this,
defer = oldDefer || jQuery.Deferred(),
request;
if(!this.status) {
console.error('[DB::getRaffle] Database is not available');
defer.reject();
return defer;
} else if(!this.available) {
console.warn('[DB:getRaffle] Database is not yet available, adding method call to queue');
this.queue.push(
{
method: 'getRaffle',
defer: defer,
args: [
id,
getItems,
transaction,
defer
],
}
);
return defer;
}
if(!id) {
defer.reject('Missing raffle ID');
}
console.time('NoName:DB:GetRaffle:' + id);
transaction = transaction || this.db.transaction(['raffles']);
request = transaction.objectStore('raffles').get(id);
request.onsuccess = function() {
//Firefox throws "Not allowed to define cross-origin object as property"
//if I try to modify the result without cloning it
//Thanks Firefox
var raffle = (this.result) ? JSON.parse(JSON.stringify(this.result)) : null;
if(!raffle) {
defer.resolve(null);
console.timeEnd('NoName:DB:GetRaffle:' + id);
return;
}
if(!getItems) {
defer.resolve(raffle, false);
console.timeEnd('NoName:DB:GetRaffle:' + id);
return;
}
that.getRaffleItems(id).done(
function(items) {
for(var i = 0; i < items.length; i++) {
var item = items[i],
index = raffle.items.indexOf(item.id);
if(index > -1) {
raffle.items[index] = item;
} else {
raffle.items.push(item);
}
}
defer.resolve(raffle, true);
console.timeEnd('NoName:DB:GetRaffle:' + id);
}
).fail(
function(event) {
console.error('[DB::getRaffle] Failed to load items for raffle ' + id, event);
defer.resolve(raffle, false, event);
console.timeEnd('NoName:DB:GetRaffle:' + id);
}
);
};
request.onerror = function(event) {
console.error('[DB::getRaffle] Failed to load raffle ' + id, event);
defer.reject(event);
};
return defer;
},
/**
* Retrieves a raffle's items from the database as NoName.Item objects
* If a transaction is passed, it will be used instead of creating another
* @param raffleId
* @param transaction
* @returns {*}
* @param oldDefer
*/
getRaffleItems: function(raffleId, transaction, oldDefer) {
'use strict';
var defer = oldDefer || jQuery.Deferred(),
index,
itemsCursor,
items = [];
if(!this.status) {
console.error('[DB::getRaffleItems] Database is not available');
defer.reject();
return defer;
} else if(!this.available) {
console.warn('[DB:getRaffleItems] Database is not yet available, adding method call to queue');
this.queue.push({
method: 'saveRaffle',
defer: defer,
args: [raffleId, transaction, defer]
});
return defer;
}
if(!raffleId) {
defer.reject('Missing raffle ID');
}
console.time('NoName:DB:getRaffleItems:' + raffleId);
//noinspection AssignmentToFunctionParameterJS
transaction = transaction || this.db.transaction(['items']);
index = transaction.objectStore('items').index('raffles');
itemsCursor = index.openCursor(IDBKeyRange.only(raffleId));
itemsCursor.onsuccess = function() {
var cursor = this.result;
if(!cursor) {
defer.resolve(items);
console.timeEnd('NoName:DB:getRaffleItems:' + raffleId);
return;
}
items.push(new window.NoName.Item(cursor.value, true));
cursor.continue();
};
itemsCursor.onerror = function(event) {
console.error('[DB::getRaffleItems] Failed to load items for raffle' + id, event);
console.timeEnd('NoName:DB:getRaffleItems:' + raffleId);
defer.reject(event);
};
return defer;
},
};
window.NoName.Steam = {
steamID: '',
JSON_URL: '',
/**
*
*/
init: function() {
'use strict';
console.time("NoName:Steam");
this.getSteamID();
this.JSON_URL = 'http://steamcommunity.com/profiles/' + this.steamID + '/inventory/json/440/2/';
console.timeEnd("NoName:Steam");
},
/**
*
*/
getSteamID: function() {
'use strict';
try {
var result = $('#avatar').children('a').first().prop('href').match(/https?:\/\/tf2r.com\/user\/(\d+)\.html/);
this.steamID = result[1];
} catch(ignored) {
console.warn('[Steam::getSteamID] Unable to determine steamID');
}
},
/**
*
* @returns {*}
*/
fetchInventoryJSON: function() {
'use strict';
var defer = jQuery.Deferred(),
anonymous = !NoName.Storage.get('steam:usecookies', false);
if(!this.steamID) {
defer.reject();
return defer;
}
GM_xmlhttpRequest(
{
method: 'GET',
url: this.JSON_URL,
anonymous: anonymous,
onload: function(response) {
defer.resolve(response.responseText);
},
onerror: function(response) {
console.error('[Steam::fetchBackpack] Failed to retrieve inventory JSON: ' + response.textStatus);
defer.reject();
},
onprogress: function(response) {
}
}
);
return defer;
}
};
//noinspection FunctionTooLongJS
/**
* Object that handles parsing and displaying of user's backpack
* @param options
* @constructor
*/
window.NoName.Backpack = function(options) {
var that = this;
console.time("NoName:Backpack");
this.selectableItems = !!options.selectableItems;
this.autoRender = !!options.autoRender;
this.autoLoad = !!options.autoLoad;
this.tradableOnly = !!options.tradableOnly;
this.loaded = 0;
this.items = [];
this.selectedItems = [];
this.badItems = []; //Selected items that will not be displayed correctly in raffles
this.levelData = {};
this.timeout = null;
this.filters = {}; //Current search filters
this.oldFilters = {}; //Previous search filters
this.searchResults = null; //Item indexes that match current filters
this.$container = options.container;
this.$selectedContainer = options.selectedContainer;
this.$displayWarning = null;
this.$search = null;
if(!this.$selectedContainer || !this.$selectedContainer.length) {
console.error('[Backpack] $container does not exist');
return;
}
/**
*
* @returns {boolean}
* @private
*/
function _initElements() {
that.$container.empty().append($('<ol></ol>'));
if(that.selectableItems) {
if(!that.$selectedContainer || !that.$selectedContainer.length) {
console.error('[Backpack] $selectedContainer must exist for items to be selectable');
return false;
}
that.$selectedContainer.empty().append($('<ol></ol>'));
that.$container.on(
'click', 'li', function() {
if(that.isSelected(this.item)) {
that.deselect(this);
} else {
that.select(this);
}
}
);
that.$selectedContainer.on(
'click', 'li', function() {
that.deselect(this);
}
);
that.$displayWarning = $('<div></div>').addClass('notif lev1').text(
'Some selected items will not display correctly in your raffle\nConsider listing them in the description.'
);
that.$selectedContainer.prepend(that.$displayWarning);
}
return true;
}
/**
*
* @private
*/
function _initFilters() {
var $qualities = $('<div></div>').addClass('qualities');
that.$search = $('<input />').prop(
{
'placeholder': 'Search',
}
).addClass('search');
for(var quality in dictionary.qualities) {
if(!dictionary.qualities.hasOwnProperty(quality)) {
continue;
}
var $quality = $('<input />').prop(
{
type: 'checkbox',
value: quality,
autocomplete: 'off',
id: 'q' + quality,
}
),
$label = $('<label></label>').prop(
{
title: dictionary.qualities[quality],
htmlFor: 'q' + quality,
}
).addClass('q' + quality);
$qualities.append(
[
$quality,
$label,
]
);
}
that.$container.prepend($qualities).prepend(that.$search);
}
/**
*
* @private
*/
function _initEvents() {
that.$container.on(
'ei:backpackfailed', function() {
that.$container.addClass('error');
that.$container.append(
$('<a></a>').text('Failed to load backpack. Click to retry.')
.click(
function() {
that.load();
}
)
);
}
);
that.$container.on(
'click', 'ol', function(event) {
if(event.target == this) {
that.loadMore();
}
}
);
that.$container.on(
'change', '.qualities input', function() {
var qualities = that.$container.find('.qualities input:checked').map(
function() {
return parseInt(this.value, 10);
}
).get();
if(!qualities.length) {
delete that.filters.quality;
} else {
that.filters.quality = qualities;
}
_queueSearch(50);
}
);
that.$search.on(
'change input', function() {
if(!this.value) {
delete that.filters.text;
} else {
that.filters.text = this.value;
}
_queueSearch(150);
}
);
}
/**
*
* @private
*/
function _initDragDrop() {
var source;
that.$selectedContainer.on(
'dragstart', '.item', function(e) {
source = e.target;
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setDragImage(this, 0, 0);
e.originalEvent.dataTransfer.setData('text/plain', this.item.name); //Needed for DnD to work at all in firefox;
$(this).addClass('dragging');
}
);
that.$selectedContainer.on(
'dragenter', '.item', function(e) {
var position = source.compareDocumentPosition(this);
//noinspection JSBitwiseOperatorUsage
if(position & Node.DOCUMENT_POSITION_DISCONNECTED) {
return false;
} else { //noinspection JSBitwiseOperatorUsage
if(position & Node.DOCUMENT_POSITION_PRECEDING) {
e.target.parentNode.insertBefore(source, e.target);
} else {
e.target.parentNode.insertBefore(source, e.target.nextSibling);
}
}
return true;
}
);
that.$selectedContainer.on(
'dragover', '.item', function(e) {
e.preventDefault();
return false;
}
);
that.$selectedContainer.on(
'dragend drop', '.item', function(e) {
var oldIndex = that.selectedItems.indexOf(this.item);
//Otherwise firefox will happily try to visit "http://Strange Australium Black Box/"
e.preventDefault();
if(oldIndex < 0) {
console.warn('[Backpack::_initDragDrop] Ignoring unselected item drop');
return true;
}
var newIndex = that.$selectedContainer.find('.item').index(this);
if(newIndex < 0) {
console.warn('[Backpack::_initDragDrop] Ignoring weird situation');
return true;
}
that.selectedItems.splice(oldIndex, 1);
that.selectedItems.splice(newIndex, 0, this.item);
$(this).removeClass('dragging');
console.debug(
'[Backpack::_initDragDrop] Moved item ' + this.item.name + ' from ' + oldIndex + ' to ' + newIndex
);
return false;
}
);
}
/**
*
* @param delay
* @private
*/
function _queueSearch(delay) {
if(that.timeout) {
clearTimeout(that.timeout);
}
that.timeout = setTimeout(
function() {
_search();
}, delay
);
}
/**
* TODO: Should probably be a public function and accept filters as an argument
* @private
*/
function _search() {
if(!Object.keys(that.filters).length) {
that.searchResults = null;
that.render(true, 0, 50);
return;
}
console.time('NoName:Backpack:_search');
//Compute any possible optimisations
var optimisation = (that.searchResults !== null) ? _computeSearchOptimisation(
that.filters, that.oldFilters
) : false,
//Create temporary copy of current filters so we can convert the text filter to a regex
//without breaking future searches that try to compare their text filter with this one
filters = Object.assign({}, that.filters);
//Store current filters for later comparison with future searches
that.oldFilters = Object.assign({}, that.filters);
//Convert text filter to case insensitive regex
if(filters.text) {
filters.text = new RegExp(filters.text, 'i');
}
//Handle cases that can be optimised
switch(optimisation) {
//Optimisation for AND filters where the new filters are a superset of the old ones
//Also for OR filters where the new filters are a subset of the old ones
//In these cases the new results will always be a subset of the old ones, so only the old results need to be checked
case 'narrowing' :
var oldResults = that.searchResults,
newResults = [];
console.info('[Backpack::_search] Using narrowing optimisation');
for(var result in oldResults) {
if(!oldResults.hasOwnProperty(result)) {
continue;
}
//noinspection JSDuplicatedDeclaration
var index = parseInt(oldResults[result], 10),
item = that.items[oldResults[result]];
if(item.matchesFilters(filters)) {
newResults.push(index);
}
}
that.searchResults = newResults;
break;
//Optimisation for OR filters where the new filters are a superset of the old ones
//Also for AND filters where the new filters are a subset of the old ones
//In these cases the new results will always be a superset of the old results, so no need to check the old results again
case 'widening' :
var results = that.searchResults;
console.info('[Backpack::_search] Using widening optimisation');
//noinspection JSDuplicatedDeclaration
for(var item in that.items) {
if(!that.items.hasOwnProperty(item)) {
continue;
}
item = parseInt(item, 10);
if(results.indexOf(item) > -1) {
continue;
}
if(that.items[item].matchesFilters(filters)) {
results.push(item);
}
}
that.searchResults = results.sort(
function(a, b) {
return (a - b);
}
);
break;
//No optimisations
default :
that.searchResults = [];
//noinspection JSDuplicatedDeclaration
for(var item in that.items) {
if(!that.items.hasOwnProperty(item)) {
continue;
}
if(that.items[item].matchesFilters(filters)) {
that.searchResults.push(parseInt(item, 10));
}
}
break;
}
that.render(true, 0, 50);
console.timeEnd('NoName:Backpack:_search');
}
/**
* Determine if the change between old and current search filters can be optimised
* TODO: handle multiple filter types and filters other than text properly
* @param newFilters
* @param oldFilters
* @returns {boolean}
* @private
*/
function _computeSearchOptimisation(newFilters, oldFilters) {
var optimisation = false;
//Text in old filter + no text in new filter = widening
//Text in both filters + new text is equal to old text = nothing
//Text in both filters + new text is superset of old text = narrowing
//Text in both filters + new text is subset of old text = widening
//Text in new filter + no text in old filter = narrowing
if(oldFilters.text) {
if(!newFilters.text) {
optimisation = 'widening';
} else if(oldFilters.text === newFilters.text) {
optimisation = false;
} else if(newFilters.text.indexOf(oldFilters.text) === 0) {
optimisation = 'narrowing';
} else if(oldFilters.text.indexOf(newFilters.text) === 0) {
optimisation = 'widening';
}
} else if(newFilters.text) {
optimisation = 'narrowing';
}
if(oldFilters.quality != newFilters.quality) {
return false;
}
return optimisation;
}
/**
* Parse default item list to get levels of items that do not have a level in the steam inventory json
* @private
*/
function _getLevelData() {
console.time('NoName:Backpack:_getLevelData');
//Optimisation
//Exclude decorated weapons as they never have levels
that.$container.find('.item:not(.q15)').each(
function() {
//Optimisation
//Using vanilla javascript here makes this about 10 times faster
var defindex = this.getAttribute('iid'),
level = this.getAttribute('ilevel'),
quality = this.getAttribute('iqual');
if(!level) {
return;
}
that.levelData[defindex] = that.levelData[defindex] || {};
that.levelData[defindex][quality] = that.levelData[defindex][quality] || [];
that.levelData[defindex][quality].push(level);
}
);
console.timeEnd('NoName:Backpack:_getLevelData');
}
/**
* Populate items array with item objects created from parsed data
* @param items
* @private
*/
function _populateItems(items) {
for(var item in items) {
if(!items.hasOwnProperty(item)) {
continue;
}
item = items[item];
that.items.push(new NoName.Item(item));
}
}
/**
* Use a webworker to parse the json and clean up data
* Doing this on the main thread causes noticeable lag
* @param json
* @returns {*}
*/
this.parseJSON = function(json) {
console.time('NoName:Backpack:parseJSON');
var that = this,
defer = jQuery.Deferred(),
//Create a blob from the below workerParse function to allow its use in the worker
work = URL.createObjectURL(
new Blob(
[
'(',
this.workerParse.toString(),
')()'
], {
type: 'application/javascript'
}
)
),
//Create worker
worker = new Worker(work);
//Listen for worker response
worker.addEventListener(
'message', function(event) {
if(event.data.success) {
console.info('[Backpack::parseJSON] Backpack parsed');
//Using JSON to avoid " Not allowed to define cross-origin object" error in firefox
try {
_populateItems(JSON.parse(event.data.items));
} catch(e) {
console.error('[Backpack::parseJSON] Failed to parse worker JSON response: ' + e);
console.timeEnd('NoName:Backpack:parseJSON');
defer.reject();
return;
}
console.timeEnd('NoName:Backpack:parseJSON');
defer.resolve();
} else {
console.error('[Backpack::parseJSON] Failed to parse backpack: ' + event.data.error);
console.timeEnd('NoName:Backpack:parseJSON');
defer.reject();
}
}, false
);
//Send the worker the json to parse
worker.postMessage(
{
json: json,
levelData: that.levelData,
dictionary: window.dictionary,
}
);
URL.revokeObjectURL(work);
return defer;
};
/**
* Used by web worker to parse the json and then restructure the parsed data
*/
this.workerParse = function() {
var levelData, //Backpack item level data scraped from new raffle page html
dictionary, //Dictionary to map description strings to attribute ids
//RegExps used to parse descriptions.
//Precompiled here as they are used 1000s of times.
regexes = {
australium: /^[^'].* (Australium) /,
level: /.*Level (\d+).*/,
series: /.*Series #(\d+).*/,
uncraftable: /^\( Not (or )?Usable in Crafting \)$/,
specKS: /^Sheen: (.+)/,
profKS: /^Killstreaker: (.+)/,
ksKitTarget: /^This Killstreak Kit can be applied to a (.*)$/,
gift: /^\nGift from: (.+)/,
paint: /^Paint Color: (.+)/,
crafter: /^Crafted by (.+)/,
unusual: /^★ Unusual Effect: (.+)/,
spell: /^Halloween: (.+) \(spell only active during event\)/,
nameDesc: /^''(.*)''$/,
part: /^\((.*): \d+\)$/,
grade: /^(\w+) Grade (.*)$/,
};
self.addEventListener(
'message', function(event) {
try {
levelData = event.data.levelData;
dictionary = event.data.dictionary;
var data = JSON.parse(event.data.json);
console.time('workerParse::parseItems');
var items = parseItems(data);
console.timeEnd('workerParse::parseItems');
//Using JSON to avoid " Not allowed to define cross-origin object" error in firefox
if(items) {
self.postMessage(
{
success: true,
items: JSON.stringify(items)
}
);
} else {
throw new Error('Item parsing failed');
}
} catch(e) {
console.error('[Steam::workerParse] Error while parsing JSON : ' + e);
self.postMessage(
{
success: false,
error: e.toString()
}
);
}
}
);
/**
* Checks the parsed json is a valid response
* Merges item and description arrays into a single item array
* Removes unneeded item data
* @param data
* @param data.success
* @param data.rgDescriptions
* @param data.rgInventory
* @param data.rgInventory.array_member.classid
* @param data.rgInventory.array_member.instanceid
* @returns {*}
*/
function parseItems(data) {
var items = data.rgInventory,
descriptions = data.rgDescriptions,
parsedItems = [];
if(!data.success) {
console.error('[Backpack::workerParse] Success property is false');
return false;
}
if(!items) {
console.error('[Backpack::workerParse] Inventory array missing');
return false;
}
if(!descriptions) {
console.error('[Backpack::workerParse] Descriptions array missing');
return false;
}
for(var item in items) {
if(!items.hasOwnProperty(item)) {
continue;
}
item = items[item];
var classInstanceId = item.classid + '_' + item.instanceid,
description = descriptions[classInstanceId];
parsedItems.push(parseItem(item, description));
}
parsedItems.sort(
function(item1, item2) {
return item1.position - item2.position;
}
);
return parsedItems;
}
/**
* Parses a single item
* Extracts level and series data where possible
* Loops over descriptions to determine which ones to item.descriptions
* Moved to worker as it is very slow
* @param item
* @param description
* @param description.name Item name
* @param description.type Item type (Level x y)
* @param description.icon_url Item thumbnail
* @param description.tradable Whether item is tradable
* @param description.app_data.def_index Item defindex
* @param description.app_data.quality Item quality
* @param description.market_hash_name Item name used on community market
* @param description.descriptions Item description strings
* @param description.tags Item tags
* @returns {{id: Number, position: *, defindex: *, quality: (*|string), name, type, level: (Array|{index: number, input: string}), series: (Array|{index: number, input: string}), untradable: boolean, descriptions: Array, thumbnail: *}}
*/
function parseItem(item, description) {
var level = description.type.match(regexes.level),
series = description.name.match(regexes.series),
matches;
level = (level) ? parseInt(level[1], 10) : 0;
series = (series) ? parseInt(series[1], 10) : 0;
var parsedItem = {
id: parseInt(item.id, 10),
position: item.pos,
defindex: description.app_data.def_index,
quality: description.app_data.quality,
name: description.name,
type: description.type,
level: level,
series: series,
untradable: !description.tradable,
descriptions: [],
thumbnail: description.icon_url, //Reduce image size
};
//Fallback to level data found in the default item list if the json api didn't give us one in the item description
//Using this data is a guess, but it will usually be correct unless the user has multiple copies of the same strange
//which differ in a noticeable way such as parts
if(!level) {
if(levelData[parsedItem.defindex] && levelData[parsedItem.defindex][parsedItem.quality]) {
parsedItem.level = parseInt(levelData[parsedItem.defindex][parsedItem.quality], 10);
} else {
parsedItem.level = 1; //The site uses 1 for items that don't have a level
}
}
if((matches = description.name.match(regexes.nameDesc))) {
parsedItem.customName = matches[1];
}
if(description.market_hash_name.match(regexes.australium)) {
parsedItem.australium = true;
}
parseDescriptions(parsedItem, description.descriptions || []);
parseTags(parsedItem, description.tags || []);
return parsedItem;
}
//Loops over the description strings for an item and returns the ones we care about
//Moved to the worker as it is slow enough to cause noticeable ui lag
//Now even slower since I need to parse the attribute values out of the strings so they can be saved
function parseDescriptions(item, descriptions) {
descriptions.forEach(
function(description) {
var matches;
//Uncraftable
if(regexes.uncraftable.test(description.value)) {
item.descriptions.push(description);
item.uncraftable = true;
return;
}
//Basic killstreak
if(description.value === 'Killstreaks Active') {
item.descriptions.push(description);
item.killstreak = (item.killstreak) ? Math.max(item.killstreak, 1) : 1;
return;
}
//Specialized killstreak
if((matches = description.value.match(regexes.specKS))) {
item.descriptions.push(description);
item.killstreak = (item.killstreak) ? Math.max(item.killstreak, 2) : 2;
item.ksSheen = dictionary.killstreakSheens.indexOf(matches[1]);
return;
}
//Professional killstreak
if((matches = description.value.match(regexes.profKS))) {
item.descriptions.push(description);
item.killstreak = (item.killstreak) ? Math.max(item.killstreak, 3) : 3;
item.ksEffect = dictionary.killstreakEffects.indexOf(matches[1]);
return;
}
if(item.killstreak) {
//Killstreak kit target item
if((matches = description.value.match(regexes.ksKitTarget))) {
item.descriptions.push(description);
item.referencedItem = matches[1];
return;
}
}
//Gifts
if((matches = description.value.match(regexes.gift))) {
item.descriptions.push(description);
item.gifter = matches[1];
return;
}
//Paint
if((matches = description.value.match(regexes.paint))) {
item.descriptions.push(description);
item.paint = dictionary.paintColours.indexOf(matches[1]);
return;
}
//Crafted
if((matches = description.value.match(regexes.crafter))) {
item.descriptions.push(description);
item.crafter = matches[1];
return;
}
//Unusual effects
if((matches = description.value.match(regexes.unusual))) {
item.descriptions.push(description);
item.unusualEffect = dictionary.unusualEffects.indexOf(matches[1]);
return;
}
//Festivized
if(!description.value.indexOf('Festivized')) {
item.descriptions.push(description);
item.festive = 2; //2 = Festivised
return;
}
//Stat clocks
if(!description.value.indexOf('Strange Stat Clock Attached')) {
item.descriptions.push(description);
item.statClock = true;
return;
}
//Spells
if((matches = description.value.match(regexes.spell))) {
description.value = description.value.replace('(spell only active during event)', '');
item.descriptions.push(description);
item.spells = item.spells || [];
item.spells.push(dictionary.halloweenSpells.indexOf(matches[1]));
return;
}
//Custom description
if((matches = description.value.match(regexes.nameDesc))) {
item.descriptions.push(description);
item.customDesc = matches[1];
return;
}
//Strange parts
if((matches = description.value.match(regexes.part)) && description.color === '756b5e') {
var part = dictionary.strangeParts.indexOf(matches[1]);
item.descriptions.push(description);
item.parts = item.parts || [];
item.parts.push(part);
if(part === -1) {
console.warn('Unknown Strange part: ' + matches[1]);
}
return;
}
//Collection grades
if(regexes.grade.test(description.value) && description.color) {
item.descriptions.push(description);
}
}
);
return item;
}
/**
* Parse item tags that we care about
* @todo Not yet implemented
* @param item
* @param {Object[]} tags
* @param tags[].category
* @returns {*}
*/
function parseTags(item, tags) {
tags.forEach(
function(tag) {
switch(tag.category) {
case 'Rarity':
item.grade = dictionary.grades.indexOf(tag.name);
return;
case 'Exterior':
item.wear = dictionary.wears.indexOf(tag.name);
return;
}
}
);
return item;
}
};
_getLevelData();
_initElements();
_initFilters();
_initEvents();
_initDragDrop();
if(this.autoLoad) {
this.load();
}
console.timeEnd("NoName:Backpack");
};
window.NoName.Backpack.prototype = {
loadMore: function() {
this.render(false, this.loaded, this.loaded + 300);
},
/**
* Renders backpack items, within an optional range and optionally emptying the parent element
* Renders search results if there are any, otherwise all items
* @param empty
* @param fromPos
* @param toPos
*/
render: function(empty, fromPos, toPos) {
console.time('NoName:Backpack:render');
var that = this,
items = document.createDocumentFragment();
if(empty) {
this.$container.children('ol').empty();
}
if(this.searchResults !== null) {
if(!toPos || toPos > this.searchResults.length) {
this.$container.removeClass('minimised');
toPos = this.items.length;
} else {
this.$container.addClass('minimised');
}
this.searchResults.slice(fromPos, toPos).forEach(
function(item) {
item = that.items[item];
items.appendChild(that.renderItem(item));
}
);
} else {
if(!toPos || toPos > this.items.length) {
this.$container.removeClass('minimised');
toPos = this.items.length;
} else {
this.$container.addClass('minimised');
}
this.items.slice(fromPos, toPos).forEach(
function(item) {
items.appendChild(that.renderItem(item));
}
);
}
this.$container.children('ol').append(items);
this.loaded = toPos;
console.timeEnd('NoName:Backpack:render');
},
/**
* Renders a single backpack item, greying it out if it is selected
* Using standard javascript, need all the performance I can get here
* @param item
* @returns {Element}
*/
renderItem: function(item) {
var element = document.createElement('li');
element.className = item.getCSSClasses();
if(this.selectedItems.indexOf(item) > -1) {
element.className += ' selected';
}
element.item = item;
element.style.backgroundImage = item.getBackgroundImages();
return element;
},
/**
* Load the user's backpack
*/
load: function() {
var that = this,
jsonLoad = NoName.Steam.fetchInventoryJSON();
this.$container.trigger('ei:backpackloading');
this.$container.addClass('loading');
jsonLoad.done(
function(json) {
that.parseJSON(json).done(
function() {
that.$container.trigger('ei:backpackloaded');
if(that.tradableOnly) {
that.items = that.items.filter(
function(item) {
return item.isTradable();
}
);
}
if(that.autoRender) {
that.render(true, 0, 50);
}
}
).fail(
function() {
that.$container.trigger('ei:backpackfailed');
}
).always(
function() {
that.$container.removeClass('loading');
}
);
}
).fail(
function() {
console.error('[Backpack::load] Failed to load backpack');
that.$container.trigger('ei:backpackfailed');
that.$container.removeClass('loading');
}
);
},
/**
* Select an item, adding it to the selected list
* @param element
* @returns {boolean}
*/
select: function(element) {
var item = element.item;
if($(element).parents().index(this.$container) === -1) {
console.error('[Backpack::select] item is not a descendant of item container');
return false;
}
if(this.items.indexOf(item) < 0) {
console.error('[Backpack::select] Item does not exist in backpack');
return false;
}
//Clone item element and add to selected item list
var clone = element.cloneNode(false);
clone.item = item;
clone.draggable = true;
this.selectedItems.push(item);
this.$selectedContainer.children('ol').append(clone);
//Update existing element to show item has been selected
$(element).addClass('selected');
//Show warning if this item won't be displayed correctly in the raffle
if(this.selectableItems && !item.willDisplayCorrectly()) {
console.warn('[Backpack::select] Selected item will not be displayed correctly');
this.badItems.push(item);
this.$displayWarning.show();
}
console.debug('[Backpack::select] Selected item ' + item.name);
console.log(item.exportText());
return true;
},
/**
* Deselect an item, removing it from the list of selected items and allowing it to be selected again
* @param element
* @returns {boolean}
*/
deselect: function(element) {
var item = element.item,
index = this.selectedItems.indexOf(item);
if(index < 0) {
console.error('[Backpack::deselect] Item does not exist in backpack');
return false;
}
//Remove from selected array
this.selectedItems.splice(index, 1);
//If the passed element is in the selected list remove it
//Otherwise find it and then remove it
//Also restore the appearance of the element in the main list
if($(element).parents().index(this.$selectedContainer) > -1) {
//Remove element from selected list
$(element).remove();
//Get index of current item in item list or search results (if search results are currently being shown)
index = this.items.indexOf(item);
if(this.searchResults) {
index = this.searchResults.indexOf(index);
}
//Use above index to restore item in the list of selectable items
this.$container.children('ol').children().eq(index).removeClass('selected');
} else {
//Use above index to remove element from selected list
this.$selectedContainer.children('ol').children().eq(index).remove();
//Restore appearance of main element
$(element).removeClass('selected');
}
//Remove from bad items list if it exists
if(this.selectableItems && this.badItems.indexOf(item) > -1) {
this.badItems.splice(this.badItems.indexOf(item), 1);
}
//If bad item list is empty remove warning
if(this.selectableItems && !this.badItems.length) {
this.$displayWarning.hide();
}
console.debug('[Backpack::deselect] Deselected item ' + item.name);
return true;
},
isSelected: function(item) {
return this.selectedItems.indexOf(item) > -1;
},
getSelected: function() {
return this.selectedItems;
}
};
//The schema is complex so cant fix warnings here
//noinspection FunctionWithMoreThanThreeNegationsJS,OverlyComplexFunctionJS,FunctionTooLongJS
/**
* Object that represents a single item
* @param data
* @param useSchema
* @constructor
*/
window.NoName.Item = function(data, useSchema) {
//General stuff
this.id = data.id;
this.defindex = (!isNaN(data.defindex) && data.defindex) ? parseInt(data.defindex, 10) : null;
this.name = data.name || '';
this.quality = (!isNaN(data.quality) && data.quality) ? parseInt(data.quality, 10) : 0;
this.subQuality = (!isNaN(data.subQuality) && data.subQuality) ? parseInt(data.subQuality, 10) : null;
this.untradable = !!data.untradable;
this.uncraftable = !!data.uncraftable;
this.type = data.type || '';
this.level = (!isNaN(data.level) && data.level) ? parseInt(data.level, 10) : 1;
this.series = (!isNaN(data.series) && data.series) ? parseInt(data.series, 10) : 0;
//Tags
this.customName = data.customName || null;
this.customDesc = data.customDesc || null;
//Stranges
this.statClock = data.statClock || false;
this.parts = data.parts || [];
//Gifting
this.gifter = data.gifter || null;
//Crafting
this.crafter = data.crafter || null;
this.craftNumber = data.craftNumber || null;
//Killstreaks
this.killstreak = data.killstreak || null;
this.ksSheen = data.ksSheen || null;
this.ksEffect = data.ksEffect || null;
//Skins/Collections
this.grade = data.grade || null;
this.wear = data.wear || null;
this.paint = data.paint || null;
this.australium = data.australium || false;
this.festive = data.festive || false;
this.unusualEffect = data.unusualEffect || null;
this.spells = data.spells || [];
this.position = data.position || null;
this.descriptions = data.descriptions || [];
//Add item type/level as a description if there are no other descriptions
//Saves some annoying checking in getDescriptions(), as items could have a descriptions array
//that doesn't include the item type
if(this.descriptions.length && (this.type || this.level)) {
this.descriptions.splice(
0, 0, {
value: this.type || 'Level ' + this.level,
color: 'ffffff',
}
);
}
if(NoName.Storage.get('other:lowresimages', false)) {
this.thumbnail = (data.thumbnail) ? this.IMAGE_URL + data.thumbnail + '/64fx64f' : null;
} else {
this.thumbnail = (data.thumbnail) ? this.IMAGE_URL + data.thumbnail + '/128fx128f' : null;
}
//If set, retrieve name, images and other details from the schema
//Otherwise populate from provided data if any
if(useSchema) {
this.definition = NoName.Item.getDefinition(this);
if(this.definition) {
delete this.name;
delete this.thumbnail;
this.defindex = this.definition.defindex || null;
this.grade = this.definition.grade || null;
this.name = this.getName();
this.thumbnail = this.getThumbnail();
} else {
this.name = 'Unknown Item';
this.thumbnail = this.MISSING_IMAGE;
}
}
};
//noinspection SpellCheckingInspection
window.NoName.Item.prototype = {
IMAGE_URL: 'https://steamcommunity-a.akamaihd.net/economy/image/',
SCHEMA_IMAGE_URL: 'http://media.steampowered.com/apps/440/icons/',
UNUSUAL_IMAGE_URL: 'http://tf2.hades-underworld.com/particles/',
MISSING_IMAGE: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAMFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaPxwLAAAAEHRSTlMAzF8Ovno2lrEpG6NtUohEM5nYxgAAAURJREFUSMdjGIHALElJSS0Bp/SkEkEwaL2MVZrrhCAchGGTDxREApUY8sxgeQRQQFfwRRAVSBugyrMKooPNhBRIoznREUPFAVQVihgKpFAVcGAoEEVVwCaIAdD88RAi2q6kDnPOB1QFC0Fi4reALCZHrGHFBLJ2AZg5Eas3mBsFxRegBEoAWmB/FLkAczD26GDfyYBfAZcBmoIJOFMWL1heAlUQM1SFcCuApB1nnPKsGG5ECxBIoIvjNGA2xIBmXPKckJgQwenJKRADPHG6oBHiAgMCXjiA04mJYHk3FDHMhCW9AKc8M4FAZOAHK0jArYAHJC/DgBswJxWCbMALkkoOMBAABgwDC2yfv8Urz9kIKnzwABPMwgdLQXMBjwJwcnmARwE4rgrwKHCE5Hvc4CCB2GawhhVv+LKVAt6Q4lK6zDAcAQAdIEKHGzsRJwAAAABJRU5ErkJggg==',
EXPORT_DEFINDEX: 0,
EXPORT_CRAFTER: 1,
EXPORT_GRADE: 2,
EXPORT_WEAR: 3,
EXPORT_FESTIVE: 4,
EXPORT_UNUSUAL: 5,
EXPORT_KS: 6,
EXPORT_STRANGE_PARTS: 7,
EXPORT_GIFTER: 8,
EXPORT_PAINT: 9,
EXPORT_AUSTRALIUM: 10,
EXPORT_NAME: 11,
EXPORT_DESC: 12,
EXPORT_CRAFT_NUMBER: 13,
EXPORT_SPELLS: 14,
/**
* List of known defindexes that the site cannot display correctly
* - Newer taunts
* - Festivizers
* - Smissmass gifts
*/
badIndexes: [
30671,
30618,
5838,
5839,
1162
],
/**
* Gets the item's defindex
* @returns {null|*}
*/
getDefIndex: function() {
return this.defindex;
},
/**
* Retrieve item name or generate it from the attributes and schema if not set
* TODO: Strange filter prefixes?
* @returns {*}
*/
getName: function() {
var name = [];
if(this.customName) {
return this.customName;
}
if(this.name) {
return this.name;
}
this.definition = this.definition || NoName.Item.getDefinition(this);
if(!this.definition) {
if(this.defindex) {
return 'Unknown Item ' + this.defindex;
}
return 'Unknown Item';
}
//Add "The" prefix to relevant unique items
if(this.definition.the && this.quality == 6 && !this.killstreak) {
name.push('The');
}
//Subquality name (i.e strange collector's items)
if(this.subQuality && dictionary.qualities[this.subQuality]) {
name.push(dictionary.qualities[this.subQuality]);
}
//Quality name for non-decorated/unique items
if(this.quality != 15 && this.quality != 6 && dictionary.qualities[this.quality]) {
name.push(dictionary.qualities[this.quality]);
}
//Festives and festivised skins
if(this.festive) {
name.push('Festive');
}
if(this.australium) {
name.push('Australium');
}
//Killstreaks
switch(this.killstreak) {
case 1 :
name.push('Killstreak');
break;
case 2:
name.push('Specialized Killstreak');
break;
case 3:
name.push('Professional Killstreak');
break;
}
//Actual name
name.push(this.definition.name);
if(this.wear && dictionary.wears[this.wear]) {
name.push('(' + dictionary.wears[this.wear] + ')');
} else if(this.definition.hasWear) {
name.push('(Unknown Wear)');
}
this.name = name.join(' ');
return this.name;
},
/**
* Gets the item's thumbnail url
* Url is cached after first call
* @returns {*}
*/
getThumbnail: function() {
var size = '';
if(this.thumbnail) {
return this.thumbnail;
}
this.definition = this.definition || NoName.Item.getDefinition(this);
if(!this.definition) {
return this.MISSING_IMAGE;
}
if(NoName.Storage.get('other:lowresimages', false)) {
size = '/64fx64f';
} else {
size = '/128fx128f';
}
//Define these arrays to reduce boilerplate below
this.definition.images = this.definition.images || [];
this.definition.festives = this.definition.festives || [];
//Items with a single image
if(!this.definition.hasWear) {
//Australium items
if(this.australium && this.definition.australium) {
this.thumbnail = this.IMAGE_URL + this.definition.australium + size;
} else if(this.definition.image) {
this.thumbnail = this.SCHEMA_IMAGE_URL + this.definition.image;
}
return this.thumbnail || this.MISSING_IMAGE;
}
//Items with multiple images but unknown wear (will use factory new)
if(!this.wear) {
//Festivised items
if(this.festive === 2 && this.definition.festives.length) {
this.thumbnail = this.IMAGE_URL + this.definition.festives[0] + size;
} else if(this.definition.images.length) {
//Normal items, or festivised items if there is no festive image
this.thumbnail = this.IMAGE_URL + this.definition.images[0] + size;
}
return this.thumbnail || this.MISSING_IMAGE;
}
//Items with multiple images and known wear
//Festivised items
if(this.festive === 2 && this.definition.festives[this.wear - 1]) {
this.thumbnail = this.IMAGE_URL + this.definition.festives[this.wear - 1] + size;
} else if(this.definition.images[this.wear - 1]) {
//Normal items, or festivised items if there is no festive image
this.thumbnail = this.IMAGE_URL + this.definition.images[this.wear - 1] + size;
}
return this.thumbnail || this.MISSING_IMAGE;
},
/**
* Gets an array of the item's description entries
* Array is cached after first call
* @returns {*}
*/
getDescriptions: function() {
var descriptions = this.descriptions || [];
if(this.descriptions && this.descriptions.length) {
return this.descriptions;
}
if(this.type || this.level) {
descriptions.push(
{
value: this.type || 'Level ' + this.level,
color: 'ffffff',
}
);
}
if(this.grade) {
var grade = dictionary.grades[this.grade] || 'Unknown';
descriptions.push(
{
value: grade + ' Grade',
color: 'ffffff',
}
);
}
if(this.statClock) {
descriptions.push(
{
value: 'Strange Stat Clock Attached',
color: 'cf6a32',
}
);
}
if(this.parts) {
for(var i = 0; i < this.parts.length; i++) {
var parts = this.parts[i],
partName = dictionary.strangeParts[parts] || 'Unknown strange part';
descriptions.push(
{
value: '(' + partName + ')',
color: '756b5e',
}
);
}
}
if(this.paint) {
var color = dictionary.paintColours[this.paint] || 'Unknown';
descriptions.push(
{
value: 'Paint Color: ' + color,
color: '756b5e',
}
);
}
if(this.unusualEffect) {
var effect = dictionary.unusualEffects[this.unusualEffect] || 'Unknown';
descriptions.push(
{
value: '★ Unusual Effect: ' + effect,
color: 'ffd700',
}
);
}
if(this.spells) {
for(var j = 0; j < this.spells.length; j++) {
var spell = this.spells[i],
spellName = dictionary.halloweenSpells[spell] || 'Unknown spell';
descriptions.push(
{
value: 'Halloween: ' + spellName,
color: '7ea9d1',
}
);
}
}
if(this.killstreak === 3) {
var killstreaker = dictionary.killstreakEffects[this.ksEffect] || 'Unknown';
descriptions.push(
{
value: 'Killstreaker: ' + killstreaker,
color: '7ea9d1',
}
);
}
if(this.killstreak >= 2) {
var sheen = dictionary.killstreakSheens[this.ksSheen] || 'Unknown';
descriptions.push(
{
value: 'Sheen: ' + sheen,
color: '7ea9d1',
}
);
}
if(this.killstreak) {
descriptions.push(
{
value: 'Killstreaks Active',
color: '7ea9d1',
}
);
}
if(this.gifter) {
descriptions.push(
{
value: 'Gift from: ' + this.gifter,
color: '7ea9d1',
}
);
}
if(this.customDesc) {
descriptions.push(
{
value: this.customDesc,
color: 'ffffff',
}
);
}
this.descriptions = descriptions;
return descriptions;
},
/**
* Gets a list of the item's description entries
* @returns {Array}
*/
getDescriptionList: function() {
var $list = [],
descriptions = this.getDescriptions();
//Other description strings
descriptions.forEach(
function(description) {
$list.push(
$('<li></li>').text(description.value).css(
{
color: (description.color) ? '#' + description.color : '#ffffff',
}
)
);
}
);
return $list;
},
/**
* Gets the css classes required to style the item correctly
* @returns {string}
*/
getCSSClasses: function() {
var classes = ['item'];
classes.push('q' + this.quality);
if(this.grade) {
classes.push('hasgrade');
classes.push('g' + this.grade);
}
if(this.uncraftable) {
classes.push('uncraftable');
}
return classes.join(' ');
},
/**
* Gets the images to display behind the item image.
* Currently used for unusual effects
* @returns {string}
*/
getBackgroundImages: function() {
var images = [];
images.push('url(' + this.getThumbnail() + ')');
if(this.unusualEffect && dictionary.unusualEffectImages[this.unusualEffect]) {
images.push('url(' + this.UNUSUAL_IMAGE_URL + dictionary.unusualEffectImages[this.unusualEffect] + ')');
}
return images.join(',');
},
/**
* Gets the item's quality
* @returns {*}
*/
getQuality: function() {
return this.quality;
},
/**
* Gets the item's grade, if it has one (items in collections)
* @returns {null|*}
*/
getGrade: function() {
return this.grade;
},
/**
* Gets the item's level, if it has one (newer items don't)
* @returns {*|number}
*/
getLevel: function() {
return this.level;
},
/**
* Gets the item's series, if I has one (crates etc)
* @returns {*}
*/
getSeries: function() {
return this.series;
},
/**
* Gets the item's backpack position
* @returns {*|string|position|string|null}
*/
getPosition: function() {
return this.position;
},
/**
* Is item tradable
* @returns {boolean}
*/
isTradable: function() {
return !this.untradable;
},
/**
* Checks whether the current item is likely to be displayed correctly
* Many newer items don't appear correctly due to missing schema information:
* Appearing as stock weapons, having unlocalised names, or having no image and name at all
* Returning true is not a guarantee of correct display, but should be a good enough guess for most items
* @returns {boolean}
*/
willDisplayCorrectly: function() {
//Skins show as stock or nothing at all
if(this.defindex >= 15000 && this.defindex < 16000) {
return false;
}
//Tough break cosmetics and anything valve adds in the future in this range
if(this.defindex > 30742) {
return false;
}
//Other known bad defindexes
return this.badIndexes.indexOf(this.defindex) <= -1;
},
/**
* Determines if item matches search filters
* @param filters
* @returns {boolean}
*/
matchesFilters: function(filters) {
if(filters.quality) {
if(filters.quality.length) {
if(filters.quality.indexOf(this.quality) === -1) {
return false;
}
} else if(filters.quality != this.quality) {
return false;
}
}
if(filters.text) {
if(!filters.text.test(this.name)) {
return false;
}
}
return true;
},
/**
* Exports the minimum amount of data required to reconstruct the item
* Data such as names, images, etc can be reconstructed using the schema and defindex
* @returns {{id: *, defindex: *, quality: *, level: *, series: *, uncraftable: *, customName: *, customDesc: *, australium: *, festive: *, statClock: *, unusualEffect: *, spells: *, parts: *, gifter: *, crafter: *, craftNumber: *, killstreak: *, ksSheen: *, ksEffect: *, wear: *, paint: *}}
*/
export: function() {
var item = {
id: this.id,
defindex: this.defindex,
quality: this.quality,
level: this.level,
series: this.series,
uncraftable: this.uncraftable,
customName: this.customName,
customDesc: this.customDesc,
australium: this.australium,
festive: this.festive,
statClock: this.statClock,
unusualEffect: this.unusualEffect,
spells: this.spells,
parts: this.parts,
gifter: this.gifter,
crafter: this.crafter,
craftNumber: this.craftNumber,
killstreak: this.killstreak,
ksSheen: this.ksSheen,
ksEffect: this.ksEffect,
wear: this.wear,
paint: this.paint,
};
for(var key in item) {
if(item.hasOwnProperty(key) && (!item[key] || item[key].length === 0)) {
delete item[key];
}
}
return item;
},
/**
* Exports the minimum amount of data required to reconstruct the item, in csv format
* Data such as names, images, etc can be reconstructed using the schema and defindex
* @returns {[*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*]}
*/
exportText: function() {
var item = [
this.defindex || '',
(this.quality !== 6) ? this.quality : '',
(this.level !== 1) ? this.level : '',
this.series || '',
(this.crafter) ? '"' + this.crafter + '"' : '',
(this.gifter) ? '"' + this.gifter + '"' : '',
this.wear || '',
this.statClock ? 1 : '',
this.festive || '',
this.uncraftable || '',
this.unusualEffect || '',
(this.spells) ? this.spells.join(':') : '',
(this.parts) ? this.spells.join(':') : '',
this.killstreak || '',
this.ksSheen || '',
this.ksEffect || '',
this.paint || '',
this.australium || '',
this.craftNumber || '',
(this.customName) ? '"' + this.customName + '"' : '',
(this.customDesc) ? '"' + this.customDesc + '"' : '',
];
item = item.join(',').replace(/,+$/, '');
return item;
},
};
/**
* Retrieves an item's definition by its name
* Uses the name dictionaries to find an item's defindex, and uses that to lookup the item itself
* Used to identify items in raffles which are displayed incorrectly
* @param query
* @returns {null|object}
*/
window.NoName.Item.getDefinitionByName = function(query) {
var defindex = null,
qualities = /^(strange|vintage|genuine|haunted|unusual|collector's) (?!part)/,
name,
match;
if(typeof query === 'object') {
name = query.name.toLowerCase();
} else {
name = query;
}
match = name.match(qualities);
if(match) {
name = name.substring(match[0].length);
}
name = name.replace(/^the /, '');
if(window.nameMapping && window.nameMapping[name]) {
defindex = window.nameMapping[name];
} else if(window.itemNameMapping && window.itemNameMapping[name]) {
defindex = window.itemNameMapping[name];
}
if(!defindex || !window.schema[defindex]) {
return null;
}
//Clone definition object so the below code doesn't alter the original
return JSON.parse(JSON.stringify(window.schema[defindex]));
};
/**
* Retrieves an item's definition by its defindex
* Used to identify items in raffles which are displayed incorrectly
* @param query
* @returns {*}
*/
window.NoName.Item.getDefinition = function(query) {
var defindex;
if(!window.schema) {
return null;
}
if(typeof query === 'object') {
defindex = query.defindex;
} else {
defindex = query;
}
if(defindex && window.schema[defindex]) {
//Clone definition object to avoid altering the original
return JSON.parse(JSON.stringify(window.schema[defindex]));
} else if(typeof query === 'object') {
return this.getDefinitionByName(query);
} else {
return null;
}
};
(function() {
console.info(
'---Userscript with no name for TF2r v' + GM_info.script.version + '. Made with <3 by Jim :NiGHTS:---'
);
window.NoName.Storage.init();
window.NoName.DB.init();
window.NoName.exportOverrides(); //Export override functions
window.NoName.UI.addStyles(); //Add CSS early
$(document).ready(
function() {
var $content = $('#content');
//Nu iframes pls
if(window.top == window.self) {
//Export override functions again to make sure
window.NoName.exportOverrides();
console.time("NoName");
//Lets get this party started
window.NoName.init();
console.timeEnd("NoName");
//Unhide page content
$content.find('.indent').css('opacity', '1');
}
}
);
})();