// ==UserScript== // @name Torn Faction Filter // @namespace https://github.com/Goltred/tornscripts // @version 4.0.1 // @description Shows only faction members that are in the hospital and online, and hides the rest. // @author Goltred // @updateURL https://raw.githubusercontent.com/Goltred/tornscripts/master/torn-hospital.user.js // @downloadURL https://raw.githubusercontent.com/Goltred/tornscripts/master/torn-hospital.user.js // @match https://www.torn.com/factions.php* // @match https://www.torn.com/profiles.php?XID=* // @match https://www.torn.com/hospitalview.php* // @grant GM_setValue // @grant GM_getValue // @run-at document-end // ==/UserScript== // Configuration values const defaults = { hideWalls: true, hideIdle: true, hideOffline: true, hideOnline: false, hideDescription: true, hideJail: true, hideOkay: true, hideTraveling: true, hideHospital: false, hideThreshold: 60 //> Members in the hospital for less than this value in minutes will be hidden. }; const selectors = { idle: '[id^=icon62__]', offline: '[id^=icon2__]', hospital: '[id^=icon15__]', online: '[id^=icon1__]', memberRow: '.members-list > .table-body' }; const statuses = { okay: 'Okay', hospital: 'Hospital', jail: 'Jail', traveling: 'Traveling', mugged: 'Mugged' }; const ScriptStatus = { InFactionView: 0, InProfile: 1 } let log; let currentScriptStatus = ScriptStatus.InFactionView; let keyboardShortcutsEnabled = true; // Setup listeners $(document).ajaxComplete((evt, xhr, settings) => { const isProfile = settings.url.includes('profiles.php?step=getProfileData') if (isProfile && ($("a.profile-button.profile-button-revive.cross.disabled").length > 0)) { const { user } = JSON.parse(xhr.responseText); Storage.append(user.userID); } else if (settings.url.includes('factions.php') && settings.data === 'step=info') { // This is when the faction description is filled for the player faction, info tab FactionView.removeDescriptionScrollbar(); } }); document.onkeydown = (event) => { if (currentScriptStatus === ScriptStatus.InProfile) { let btn; switch(event.code) { case 'KeyR': btn = $('a.profile-button.profile-button-revive.active'); break; case 'KeyY': btn = $('button.confirm-action.confirm-action-yes'); break; case 'KeyN': btn = $('button.confirm-action.confirm-action-no'); break; } if (btn) btn[0].click(); else $('body').click(); } else if (currentScriptStatus === ScriptStatus.InFactionView && keyboardShortcutsEnabled) { if (!event.code.includes('Digit')) { return; } let keyNumber = parseInt(event.key); // Get the associated visible row, transforming 0 to 10 so that we can open the tenth row const rowNumber = keyNumber === 0 ? 10 : keyNumber const row = $(`${selectors.memberRow} > li:visible`)[rowNumber - 1]; const userNameLink = $(row).find(TornMiniProfile.userNameSelector); let mousedown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: userNameLink[0].offsetLeft, clientY: userNameLink[0].offsetTop }); userNameLink[0].dispatchEvent(mousedown); } } class TornMiniProfile { rootElement; rootId = 'profile-mini-root'; rootSelector = '#profile-mini-root'; static userNameSelector = 'a[href*=\'profiles.php?XID=\']'; constructor(userID) { this.userID = userID; } setRootElement() { if ($(this.rootId)) { this.rootElement = document.createElement('div'); this.rootElement.classList.add(this.rootId); this.rootElement.id = this.rootId; $('body').append(this.rootElement); } else { if ($(this.rootSelector).length > 0) { this.rootElement = document.getElementById(this.rootId); } } } process(e) { this.setRootElement(); let props = { userID: this.userID, event: e }; unsafeWindow.renderMiniProfile(this.rootElement, props); } } class MobileLogWindow { constructor(show = false) { if (show) this.showUI(); } showUI() { if ($('#tchf-log').length === 0) { const el = $(`

Torn Hospital Log Window

`); $('#factions').before(el); } } append(msg) { $('#tchf-log').append(`

${msg}

`); } } class Storage { static async append(profileId) { let disabled = GM_getValue('disabled'); const timestamp = Date.now(); // we add a new record in the format of { 12345: unixtimestamp } if (!disabled) disabled = {}; disabled[profileId] = timestamp; GM_setValue('disabled', disabled); } static get(profileId) { const disabled = GM_getValue('disabled'); if (disabled) { if (profileId && profileId in disabled) return disabled[profileId]; return disabled; } return {} } static purgeOld(timeout = 300000) { const disabled = GM_getValue('disabled'); const timestamp = Date.now(); if (disabled) { const filtered = {}; Object.keys(disabled).forEach(k => { if (timestamp < disabled[k] + timeout) filtered[k] = disabled[k]; }); GM_setValue('disabled', filtered); } } static getFilters(defaults) { const filters = GM_getValue('filters'); if (!filters) { // We need to setup the defaults GM_setValue('filters', defaults); return defaults; } return filters; } static saveFilters() { const options = Filter.fromElements(); const modifiedOptions = Object.assign({}, defaults, options); GM_setValue('filters', modifiedOptions); } } class Utilities { static getUserIdfromLink(link) { const re = new RegExp('ID=(\\d+)'); const idMatch = re.exec(link); if (idMatch !== null) return idMatch[1]; return undefined; } } class MemberRow { constructor(rowElement) { this.element = rowElement; this.isIdle = rowElement.find(selectors.idle).length > 0; this.isOffline = rowElement.find(selectors.offline).length > 0; this.isOnline = rowElement.find(selectors.online).length > 0; this.isOkay = rowElement.find(`:contains("${statuses.okay}")`).length > 0; this.isHospital = rowElement.find(`:contains("${statuses.hospital}")`).length > 0; this.isTraveling = rowElement.find(`:contains("${statuses.traveling}")`).length > 0; this.isInJail = rowElement.find(`:contains("${statuses.jail}")`).length > 0; } get hospitalTime() { const hospTitle = this.element.find(selectors.hospital).attr("title"); const titleElement = $(hospTitle); // get the timer and grab the data-time attribute const seconds = parseInt(titleElement.filter('span.timer').attr('data-time')); if (seconds && seconds >= 0) { return { hours: Number(seconds / 60 / 60), minutes: Number(seconds / 60), seconds }; } return undefined; } get userid() { return Utilities.getUserIdfromLink(this.element.find("a[href*='profiles.php']").attr('href')); } get status() { if (this.isHospital) return statuses.hospital; if (this.isTraveling) return statuses.traveling; if (this.isInJail) return statuses.jail; if (this.isOkay) return statuses.okay; } get presence() { if (this.isIdle) return 'Idle'; if (this.isOffline) return 'Offline'; if (this.isOnline) return 'Online'; } checkVisibility(filter, disabled) { log.append(`checking visibility with filter ${JSON.stringify(filter)}`); const fArray = filter.getFilterArray(); log.append(`Filter array is ${fArray.join(', ')}`); const filterTime = filter.hideThreshold || defaults.hideThreshold; log.append(`Filter time is ${filterTime}`); const checks = [ false, this.userid && Object.keys(disabled).includes(this.userid) && fArray.includes("RevivesOff"), (this.hospitalTime && this.hospitalTime.minutes < filterTime) || false, fArray.includes(this.status), fArray.includes(this.presence) ]; log.append(`Check results are: ${checks.join(', ')}`); if (checks.some((element) => element === true)) { this.element.hide(); return; } this.element.show(); } } class FactionView { static async repositionMemberList() { const membersDiv = $('.f-war-list.m-top10'); const fInfo = $('.faction-info'); fInfo.parent().after(membersDiv.parent()); } static async toggleDescription(hide) { if (hide) { $(".faction-title").hide(); $(".faction-description").hide(); return; } $(".faction-title").show(); $(".faction-description").show(); } static async toggleByIcons(iconSelector) { const rows = FactionView.getRowsWithIcon(iconSelector); FactionView.toggleRows(rows); } static async toggleByStatus(status) { const rows = FactionView.getRowsWithStatus(status); FactionView.toggleRows(rows); } static toggleRows(rows) { const filter = Filter.fromElements(); const disabled = Storage.get() || {}; rows.each((i, r) => { const row = new MemberRow($(r)); row.checkVisibility(filter, disabled); }); } static async toggleRevivesOff() { // get members that have been detected with revives off const disabled = Storage.get() || {}; console.log('click'); if (Object.keys(disabled).length > 0) { const rows = FactionView.getHospitalRows(); FactionView.toggleRows(rows); } } static async toggleHospitalByThreshold() { const rows = FactionView.getHospitalRows(); this.toggleRows(rows); } static async updateHospitalTime() { $(`${selectors.memberRow} > li:contains("Hospital")`).each((i, j) => { const hospTitle = $(j).find("[id^=icon15__]").attr("title"); $(j).find(".days").text(hospTitle.substr(-16, 8)); }); } static getHospitalRows() { return $(`${selectors.memberRow} > li:contains("Hospital")`); } static getRowsWithStatus(status) { return $(`${selectors.memberRow} > li:contains("${status}")`); } static getRowsWithIcon(iconSelector) { return $(selectors.memberRow).find(iconSelector).parents('li'); } static async toggleWalls(hide) { // There doesn't seem to be an XHR request being sent for this... // Hide faction walls let el = $("#war-react-root"); if (el.length > 0) { if (hide) { $('ul.f-war-list').parent().hide(); el.hide(); return; } $('ul.f-war-list').parent().show(); el.show(); return; } setTimeout(() => this.toggleWalls(hide), 50); } static removeAnnouncementScrollbar() { $(".cont-gray10").attr('style', ''); } static removeDescriptionScrollbar() { $('div.faction-description').attr('style', 'max-height: 100%; overflow: hidden !important'); } static async process(options) { await FactionView.repositionMemberList(); $(".title .days").text("Days/Time"); FactionView.removeDescriptionScrollbar(); const rows = $(`${selectors.memberRow} > li`); const filter = Filter.fromElements(); const disabled = Storage.get() || {}; rows.each((i, j) => { const row = new MemberRow($(j)); row.checkVisibility(filter, disabled); }); FactionView.updateHospitalTime(); FactionView.toggleDescription(options.hideDescription); FactionView.toggleWalls(options.hideWalls); } } class Filter { static fromElements() { const options = new Filter(); options.hideIdle = $('#tch-idle').is(':checked'); options.hideOffline = $('#tch-offline').is(':checked'); options.hideOnline = $('#tch-online').is(':checked'); options.hideDescription = $('#tch-description').is(':checked'); options.hideWalls = $('#tch-walls').is(':checked'); options.hideTraveling = $('#tch-traveling').is(':checked'); options.hideJail = $('#tch-jail').is(':checked'); options.hideOkay = $('#tch-okay').is(':checked'); options.hideHospital = $('#tch-hospital').is(':checked'); options.hideRevivesOff = $('#tch-revoff').is(':checked'); options.hideThreshold = parseInt($('#tch-threshold').val()); return options } getFilterArray() { const result = []; Object.keys(this).forEach((k) => { if (this[k]) result.push(k.substr(4)); }); return result; } } class HospitalUI { static controls(options) { const membersParent = $('div.f-war-list').parent(); const controlsDiv = $(`
Torn Hospital - Filters

`); membersParent.before(controlsDiv); $('#tch-refresh').on('click', () => window.location.reload()); $('#tch-idle').on('click', () => FactionView.toggleByIcons(selectors.idle)); $('#tch-offline').on('click', () => FactionView.toggleByIcons(selectors.offline)); $('#tch-online').on('click', () => FactionView.toggleByIcons(selectors.online)); $('#tch-description').on('click', () => FactionView.toggleDescription($('#tch-description').is(':checked'))); $('#tch-walls').on('click', () => FactionView.toggleWalls($('#tch-walls').is(':checked'))); $('#tch-traveling').on('click', () => FactionView.toggleByStatus('Traveling')); $('#tch-jail').on('click', () => FactionView.toggleByStatus('Jail')); $('#tch-okay').on('click', () => FactionView.toggleByStatus('Okay')); $('#tch-hospital').on('click', () => FactionView.toggleByStatus('Hospital')); $('#tch-threshold').on('keyup', () => FactionView.toggleHospitalByThreshold()); $('#tch-threshold').on('blur', () => Storage.saveFilters()); $('#tch-revoff').on('click', () => FactionView.toggleRevivesOff()); $('#tch-controls').on('click', () => Storage.saveFilters()); } } function watchMiniProfiles() { const target = $('body')[0]; const observer = new MutationObserver((mutations, observer) => { let profileId; let revivesDisabled = false; mutations.forEach((record) => { // This is the mutation for the revive button const { classList } = record.target; if (classList.contains('profile-button-revive') && classList.contains('disabled')) { profileId = Utilities.getUserIdfromLink(record.target.href); if (profileId) { console.log(`User with revives disabled. Storing ${profileId}`); Storage.append(profileId); } } if (record.addedNodes.length === 1) { const node = record.addedNodes[0]; if (node.classList && node.classList.contains('mini-profile-wrapper')) { currentScriptStatus = ScriptStatus.InProfile; } } if (record.removedNodes.length === 1) { const node = record.removedNodes[0]; if (node.classList && node.classList.contains('mini-profile-wrapper')) { currentScriptStatus = ScriptStatus.InFactionView; } } }); }); observer.observe(target, { subtree: true, childList: true, }); } // Clear any records of players with disabled revives if the timeout has been met Storage.purgeOld(); // Modify the faction view if (document.URL.includes('factions.php?step=your')) { // Remove the pesky scrollbar from faction announcement FactionView.removeAnnouncementScrollbar(); } else if (document.URL.includes('factions.php')) { const filters = Storage.getFilters(defaults); FactionView.process(filters); HospitalUI.controls(filters); log = new MobileLogWindow(false); watchMiniProfiles(); }