// ==UserScript== // @author DanielOnDiordna // @name Favorite portal details // @category Info // @version 1.0.0.20251025.232300 // @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/DanielOnDiordna/favorite-portal-details.meta.js // @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/DanielOnDiordna/favorite-portal-details.user.js // @description [danielondiordna-1.0.0.20251025.232300] Quickly show a list of details for your favorite list of portals. // @id favorite-portal-details@DanielOnDiordna // @namespace https://softspot.nl/ingress/ // @match https://intel.ingress.com/* // @grant none // ==/UserScript== function wrapper(plugin_info) { // ensure plugin framework is there, even if iitc is not yet loaded if(typeof window.plugin !== 'function') window.plugin = function() {}; // use own namespace for plugin window.plugin.favoriteportaldetails = function() {}; var self = window.plugin.favoriteportaldetails; self.id = 'favoriteportaldetails'; self.title = 'Favorite portal details'; self.version = '1.0.0.20251025.232300'; self.author = 'DanielOnDiordna'; self.changelog = ` Changelog: version 1.0.0.20251025.232300 - fixed portal details indicator icon - renamed the button Change order to Edit list version 0.1.6.20210724.002500 - prevent double plugin setup on hook iitcLoaded version 0.1.6.20210421.190200 - minor fix for IITC CE where runHooks iitcLoaded is executed before addHook is defined in this plugin version 0.1.6.20210328.235400 - fixed portal guid format checker version 0.1.5.20210123.230400 - updated plugin wrapper and userscript header formatting to match IITC-CE coding - store portal titles for later use - fixed width of dialog version 0.1.4.20190530.122000 - closedialog fix for smartphone version 0.1.3.20190406.225100 - bug fix: resonators and mods object converted to array for sort purposes - support for empty mods - underline resos and mods also if the active player is not the owner - sort resos by level and then by owner name version 0.1.2.20190117.143900 - minor code fix: assign needs empty array version 0.1.1.20181030.221200 - bug fix: localstoragesettings was not defined version 0.1.1.20180911.233100 - earlier version `; self.namespace = `window.plugin.${self.id}.`; self.pluginname = `plugin-${self.id}`; self.panename = `plugin-${self.id}`; self.localstoragesettings = `${self.panename}-settings`; self.dialogobject = null; self.onPortalSelectedPending = false; self.settings = { refreshonstart: true }; self.favoriteslist = []; self.favorites = {}; self.favorite = { title: '', team: '?', level: '?', resonators: [], mods: [], health: '?', owner: '?', timestamp: 0, lat: 0.0, lng: 0.0 }; self.storagename = self.panename + '-favorites'; self.requestlist = {}; self.requestrunning = false; self.requestguid = null; self.requesttimerid = 0; self.ownercolor = 'black'; self.refreshonload_runonce = true; self.shortnames = { '' :' ', 'Heat Sink' :'H', 'Portal Shield' :'S', 'Link Amp' :'L', 'Turret' :'T', 'Multi-hack' :'M', 'Aegis Shield' :'A', 'Force Amp' :'F', 'SoftBank Ultra Link' :'U', 'Ito En Transmuter (+)':'I+', 'Ito En Transmuter (-)':'I-' }; self.shortrarities = { '' :' ', 'COMMON' :'c', 'RARE' :'r', 'VERY_RARE':'v' }; self.restoresettings = function() { let data = localStorage.getItem(self.localstoragesettings); if (!data) return; try { let settings = JSON.parse(data); if (!isObject(settings)) return; Object.keys(self.settings).forEach(key => { if (key in settings && typeof self.settings[key] == typeof settings[key]) { self.settings[key] = settings[key]; } }); } catch(e) { } }; self.storesettings = function() { try { localStorage.setItem(self.localstoragesettings,JSON.stringify(self.settings)); } catch(e) { } }; function isObject(value) { // https://dev.to/alesm0101/how-to-check-if-a-value-is-an-object-in-javascript-3pin return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof RegExp) && !(value instanceof Date) && !(value instanceof Set) && !(value instanceof Map); } self.restorefavorites = function() { let data = localStorage.getItem(self.storagename); if (!data) return; try { let favoriteslist = JSON.parse(data); // array of separator names and guid=>portal name (or guid only) if (!Array.isArray(favoriteslist)) return; self.favoriteslist = []; // array of separator names and guids self.favorites = {}; // hash of guid=>{portal details} favoriteslist.forEach(favorite => { if (typeof favorite === 'string' && favorite.match(/^[0-9a-f]{32}\.[0-9a-f]{2}$/)) { // old file format: in case it's only a guid, convert to new formatting let guid = favorite; favorite = {}; favorite[guid] = guid; // use guid as portal name } if (isObject(favorite)) { let guid = Object.keys(favorite)[0]; self.favorites[guid] = Object.assign({}, self.favorite); // copy empty favoritee object without reference self.favorites[guid].title = favorite[guid]; self.favoriteslist.push(guid); } else if (typeof favorite === 'string') { // separator name self.favoriteslist.push(favorite); } }); } catch(e) { } }; self.storefavorites = function() { let data = []; // array of separator names and guid=>portal name self.favoriteslist.forEach(favorite => { if (favorite.match(/^[0-9a-f]{32}\.[0-9a-f]{2}$/)) { // guid let guid = favorite; favorite = {}; favorite[guid] = self.favorites[guid].title; data.push(favorite); } else { // separator name data.push(favorite); } }); try { localStorage[self.storagename] = JSON.stringify(data); } catch(e) { } }; self.requesttimeout = function() { self.requestrunning = false; self.requestlist = {}; self.updateselector(); }; self.requestnext = function() { if (Object.keys(self.requestlist).length === 0) { self.requestrunning = false; return; } let nextguid = Object.keys(self.requestlist)[0]; self.requestguid = nextguid; window.setTimeout(function() { self.requesttimerid = window.setTimeout(self.requesttimeout,10000); // 10 seconds window.portalDetail.request(self.requestguid); },1000); // keep a second between request }; self.requestall = function() { if (self.requestrunning) return; if (Object.keys(window.portals).length === 0) return; self.requestrunning = true; Object.keys(self.favorites).forEach(guid => { self.requestlist[guid] = true; }); self.requestnext(); }; self.focusportal = function(guid) { if (!isObject(window.portals) || Object.keys(window.portals).length === 0) return; // cancel while no portals loaded yet; prevents an error inside the IITC core if (guid === window.selectedPortal && guid in self.favorites) { window.map.setView([self.favorites[guid].lat,self.favorites[guid].lng]); } if (guid in window.portals) { window.renderPortalDetails(guid); } else { self.requestguid = guid; window.portalDetail.request(guid); } }; self.closedialog = function() { if (self.dialogobject) { self.dialogobject.dialog('close'); self.dialogobject = null; } }; self.getdatetimestring = function(date1) { if (!(date1 instanceof Date)) { if (date1) { date1 = new Date(date1); } else { date1 = new Date(); } } return [date1.getFullYear(),date1.getMonth()+1,date1.getDate()].join('/') + ' ' + [date1.getHours(),('0' + date1.getMinutes()).slice(-2),('0' + date1.getSeconds()).slice(-2)].join(':'); }; self.resonatorshtml = function(resonators,portalowner) { let highlightowner = window.PLAYER.nickname; let resolist = []; resonators = resonators.sort( function(a,b) { if (a.level < b.level) return 1; if (a.level > b.level) return -1; let o1 = a.owner.toLowerCase(); let o2 = b.owner.toLowerCase(); if (o1 > o2) return 1; if (o1 < o2) return -1; return 0; }); // sort by resonator level and then by owner for (let cnt = 0; cnt < 8; cnt++) { let lvl = '-'; if (cnt < resonators.length && isObject(resonators[cnt])) { // {owner, level, energy} let resonatorowner = resonators[cnt].owner; lvl = resonators[cnt].level; if (resonatorowner === highlightowner) { lvl = `${lvl}`; // highlight reso's of current player } if (resonatorowner !== portalowner) { lvl = `${lvl}`; // underline reso's of other people then the portal owner } lvl = `${lvl}`; } resolist.push(lvl); } return resolist.join(''); }; self.modshtml = function(mods,portalowner) { let highlightowner = window.PLAYER.nickname; let modslist = []; for (let cnt = 0; cnt < mods.length; cnt++) { // {owner, name, rarity, stats: {…}} let mod; if (!isObject(mods[cnt])) { mod = ''; } else { mod = self.shortnames[mods[cnt].name]; if (mod === 'H' || mod === 'S' || mod === 'M') mod = self.shortrarities[mods[cnt].rarity] + mod; let modowner = mods[cnt].owner; if (modowner === highlightowner) { mod = `${mod}`; // highlight mods of current player } if (modowner !== portalowner) { mod = `${mod}`; // underline mods of other people then the portal owner } mod = `${mod}`; } modslist.push(mod); } return modslist.join(' '); }; self.favoriteshtml = function() { let highlightowner = window.PLAYER.nickname; let table = document.createElement('table'); table.cellPadding = 0; table.cellSpacing = 0; let headerrow = table.appendChild(document.createElement('tr')); let headers = [ 'T', 'Title', 'Health', 'Lvl', 'Resonators', 'Mods', 'Owner', 'Checked' ]; headers.forEach(header => { let cell = headerrow.appendChild(document.createElement('th')); cell.innerText = header; }); let rows = []; self.favoriteslist.forEach(favorite => { let guid = favorite; if (guid in self.favorites && !self.favorites[guid].title && guid in window.portals) { // fix title if portal is known let portaldata = window.portals[guid].options.data; self.favorites[guid].title = (portaldata.title ? portaldata.title : guid); self.favorites[guid].team = portaldata.team; self.favorites[guid].level = (portaldata.level >= 0 ? (portaldata.team === 'N' ? 0 : portaldata.level) : ''); } let row = table.appendChild(document.createElement('tr')); if (guid in self.favorites) { row.className = `${self.id}team${self.favorites[guid].team}`; headers.forEach(header => { let cell = row.appendChild(document.createElement('td')); cell.noWrap = true; switch(header) { case 'T': cell.innerText = self.favorites[guid].team; break; case 'Title': { let link = cell.appendChild(document.createElement('a')); link.innerText = self.favorites[guid].title ? self.favorites[guid].title : guid; link.addEventListener('click',function(e) { e.preventDefault(); self.focusportal(guid); },false); } break; case 'Health': cell.innerText = self.favorites[guid].team === 'N' ? '-' : `${self.favorites[guid].health}%`; break; case 'Lvl': cell.className = `${self.id}Lvl${self.favorites[guid].level}`; cell.innerText = `L${self.favorites[guid].level}`; break; case 'Resonators': cell.innerHTML = self.resonatorshtml(self.favorites[guid].resonators,self.favorites[guid].owner); break; case 'Mods': cell.innerHTML = self.modshtml(self.favorites[guid].mods,self.favorites[guid].owner); break; case 'Owner': if (highlightowner === self.favorites[guid].owner) cell.className = `${self.id}owner`; cell.innerText = self.favorites[guid].owner || ''; break; case 'Checked': cell.innerText = self.requestlist[guid] ? 'updating...' : (self.favorites[guid].timestamp == 0 ? 'never' : self.getdatetimestring(self.favorites[guid].timestamp)); break; } }); } else { let cell = row.appendChild(document.createElement('td')); cell.className = `${self.id}label`; cell.colSpan = headers.length; cell.innerText = favorite; } }); return table; }; self.orderup = function(cnt) { if (cnt === 0) return; let cntvalue = self.favoriteslist[cnt]; self.favoriteslist[cnt] = self.favoriteslist[cnt - 1]; self.favoriteslist[cnt - 1] = cntvalue; self.storefavorites(); self.updateselector(); }; self.orderdown = function(cnt) { if (cnt === self.favoriteslist.length - 1) return; let cntvalue = self.favoriteslist[cnt]; self.favoriteslist[cnt] = self.favoriteslist[cnt + 1]; self.favoriteslist[cnt + 1] = cntvalue; self.storefavorites(); self.updateselector(); }; self.removefavorite = function(cnt) { let guid = self.favoriteslist[cnt]; if (guid in self.favorites) delete(self.favorites[guid]); self.favoriteslist.splice(cnt,1); self.storefavorites(); self.updateselector(); }; self.addtitle = function() { let newtitle = prompt('Enter a new separator label:'); if (!newtitle) return; self.favoriteslist.push(newtitle); self.storefavorites(); self.updateselector(); }; self.edittitle = function(cnt) { let newtitle = prompt('Edit separator label:',self.favoriteslist[cnt]); if (!newtitle || newtitle === self.favoriteslist[cnt]) return; self.favoriteslist[cnt] = newtitle; self.storefavorites(); self.updateselector(); }; self.ordermenuhtml = function() { let table = document.createElement('table'); for (let cnt = 0; cnt < self.favoriteslist.length; cnt++) { let row = table.appendChild(document.createElement('tr')); let guid = self.favoriteslist[cnt]; let title = (guid in self.favorites ? (self.favorites[guid].title ? self.favorites[guid].title : guid) : self.favoriteslist[cnt]); row.innerHTML = `