1// js/ui.js 2// Import versions must be bumped together with STEAM_PLAYER_STATS_VERSION so browser caches bust on release. 3 4import { getLocation, getLocationData } from './location.js?ver=1.8.17'; 5 6import { getVisibility } from './visibility.js?ver=1.8.17'; 7 8import { getStatus, getStatusWithGame } from './status.js?ver=1.8.17'; 9 10import { getSteamId2, getSteamId3, getSteamIdHex } from './steamId2.js?ver=1.8.17'; 11 12import { getFlagEmoji } from './flagEmoji.js?ver=1.8.17'; 13 14import i18n from './i18n.js?ver=1.8.17'; 15 16// Helper function to escape HTML for XSS protection 17const escapeHtml = (text) => { 18 if (!text) return ''; 19 const div = document.createElement('div'); 20 div.textContent = text; 21 return div.innerHTML; 22}; 23 24export const updateRecentPlayers = (players) => { 25 const section = document.getElementById('steam-player-stats-recent'); 26 27 if ( ! players || players.length === 0 ) { 28 if ( section ) section.style.display = 'none'; 29 return; 30 } 31 32 let html = `<h3 class="steam-player-stats-recent-title">${escapeHtml( i18n.recentPlayers || 'Recently Checked Players' )}</h3>`; 33 html += '<div class="steam-player-stats-recent-grid">'; 34 35 players.forEach( ( player ) => { 36 html += `<div class="steam-player-stats-recent-card" data-steamid="${escapeHtml( player.steamid )}">`; 37 html += `<div class="steam-player-stats-recent-avatar"><img src="${escapeHtml( player.avatar )}" alt="${escapeHtml( player.nickname )}" width="64" height="64"></div>`; 38 html += '<div class="steam-player-stats-recent-info">'; 39 html += `<span class="steam-player-stats-recent-name">${escapeHtml( player.nickname )}</span>`; 40 html += `<span class="steam-player-stats-recent-steamid">${escapeHtml( player.steamid )}</span>`; 41 html += '</div>'; 42 html += `<button class="steam-player-stats-recent-check" type="button">${escapeHtml( i18n.checkAgain || 'Check Again' )}</button>`; 43 html += '</div>'; 44 } ); 45 46 html += '</div>'; 47 48 if ( section ) { 49 section.innerHTML = html; 50 section.style.display = ''; 51 } else { 52 const newSection = document.createElement( 'div' ); 53 newSection.id = 'steam-player-stats-recent'; 54 newSection.className = 'steam-player-stats-recent'; 55 newSection.innerHTML = html; 56 const resultsEl = document.getElementById( 'results' ); 57 if ( resultsEl ) { 58 resultsEl.insertAdjacentElement( 'afterend', newSection ); 59 } 60 } 61}; 62 63export const displayPlayerInfo = async (data) => { 64 const userInfo = document.getElementById('user-info'); 65 66 // Get legacy location for backward compatibility 67 const location = getLocation( 68 data.loccountrycode, 69 data.locstatecode, 70 data.loccityid 71 ); 72 73 // Get detailed location data from SteamCountries 74 const locationData = await getLocationData( 75 data.loccountrycode, 76 data.locstatecode, 77 data.loccityid 78 ); 79 80 const flagIcon = getFlagEmoji(data.loccountrycode); 81 const steamId2 = getSteamId2(data.steamid); 82 const steamId3 = getSteamId3(steamId2); 83 const steamHex = getSteamIdHex(data.steamid); 84 const visibility = getVisibility(data.communityvisibilitystate); 85 const status = getStatusWithGame(data.personastate, data.gameextrainfo); 86 87 // ФоÑмаÑиÑование вÑемени поÑледнего Ð²Ñ Ð¾Ð´Ð° 88 const lastLogoff = data.last_logoff 89 ? new Date(data.last_logoff * 1000).toLocaleString() 90 : i18n.unknown; 91 92 // ÐÑпÑавленное ÑоÑмаÑиÑование даÑÑ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð°ÐºÐºÐ°ÑнÑа 93 const accountCreated = data.timecreated && data.timecreated > 0 94 ? new Date(data.timecreated * 1000).toLocaleDateString() 95 : i18n.hidden; 96 97 // Format ban status display 98 const formatBanStatus = () => { 99 if (data.vacBanned) { 100 return `<span class="ban-status banned">${i18n.vacBanned}: ${data.vacBanCount} ${i18n.bans} (${data.daysSinceLastBan} ${i18n.daysSince})</span>`; 101 } else { 102 return `<span class="ban-status clean">${i18n.noVacBans}</span>`; 103 } 104 }; 105 106 // Format trade ban status 107 const formatTradeBan = () => { 108 if (!data.economyBan || data.economyBan === 'none') { 109 return `<span class="trade-status allowed">${i18n.noTradeBans}</span>`;
110 } else { 111 return `<span class="trade-status banned">${i18n.tradeBanned}: ${data.economyBan}</span>`; 112 } 113 }; 114 115 // Format location display 116 const formatLocationDisplay = () => { 117 if (!locationData.hasData) { 118 return location ? location.toUpperCase() : 'N/A'; 119 } 120 121 const countryDisplay = locationData.country.name || locationData.country.code.toUpperCase(); 122 return countryDisplay; 123 }; 124 125 // Format region display 126 const formatRegionDisplay = () => { 127 if (!locationData.hasData || !locationData.state.name) { 128 return i18n.unknown || 'Unknown'; 129 } 130 return locationData.state.name; 131 }; 132 133 // Format city display 134 const formatCityDisplay = () => { 135 if (!locationData.hasData || !locationData.city.name) { 136 return i18n.unknown || 'Unknown'; 137 } 138 return locationData.city.name; 139 }; 140 141 userInfo.innerHTML = ` 142 <div class="card-body"> 143 <div class="text-center"> 144 <img 145 width="75" 146 height="75" 147 class="user-avatar" 148 loading="lazy" 149 alt="${i18n.avatar} ${data.nickname}" 150 src="${data.avatar}" 151 /> 152 <!--<div class="lvl-wrap"><span>${i18n.level}</span> ${ 153 data.playerlevel 154 ? `<div class="player-level" style="--text-length: ${data.playerlevel.toString().length}"><span>${data.playerlevel}</span></div>` 155 : 'N/A' 156 } </div>--> 157 <h3>${data.nickname}</h3> 158 </div> 159 160 <div class="status-section"> 161 <div class="ban-info"> 162 ${formatBanStatus()} 163 </div> 164 <div class="trade-info"> 165 ${formatTradeBan()} 166 </div> 167 </div> 168 169 <hr /> 170 171 <dl class="row"> 172 <dt>${i18n.steamID2}</dt> 173 <dd><span class="copyable">${steamId2}</span></dd> 174 175 <dt>${i18n.steamID3}</dt> 176 <dd><span class="copyable">${steamId3}</span></dd> 177 178 <dt>${i18n.steamID64}</dt> 179 <dd><span class="copyable">${data.steamid}</span></dd> 180 181 <dt>${i18n.steamHex}</dt> 182 <dd><span class="copyable">${steamHex}</span></dd> 183 184 <dt>${i18n.profileURL}</dt> 185 <dd><span class="copyable">${data.profileurl}</span></dd> 186 187 <dt>${i18n.realName}</dt> 188 <dd>${data.realname ? data.realname : i18n.hidden}</dd> 189 190 <dt>${i18n.accountCreated}</dt> 191 <dd>${accountCreated}</dd> 192 193 <dt>${i18n.visibility}</dt> 194 <dd>${visibility}</dd> 195 196 <dt>${i18n.status}</dt> 197 <dd> 198 ${status} 199 ${data.gameextrainfo && data.personastate > 0 ? ` 200 <div class="current-game-badge"> 201 <span class="game-icon">ð®</span> 202 <a href="https://store.steampowered.com/app/${data.gameid}" 203 target="_blank" 204 rel="noopener noreferrer" 205 class="game-link" 206 title="${i18n.viewInStore || 'View in Steam Store'}"> 207 ${escapeHtml(data.gameextrainfo)} 208 </a> 209 </div> 210 ${data.gameserverip ? ` 211 <div class="game-server-info"> 212 <small>${i18n.gameServer || 'Server'}: ${escapeHtml(data.gameserverip)}</small> 213 </div> 214 ` : ''} 215 ` : ''} 216 </dd> 217 218 <dt>${i18n.location}</dt> 219 <dd><span class="profile-location">${formatLocationDisplay()}</span> <span class="profile-flag">${flagIcon}</span></dd> 220 221 <dt>${i18n.region}</dt> 222 <dd><span class="profile-region">${formatRegionDisplay()}</span></dd> 223 224 <dt>${i18n.city}</dt> 225 <dd><span class="profile-city">${formatCityDisplay()}</span></dd> 226 227 <dt>${i18n.lastLogin}</dt> 228 <dd>${lastLogoff}</dd> 229 </dl> 230 </div> 231 232 <a href="${typeof steamApiData !== 'undefined' && steamApiData.calculator_url ? steamApiData.calculator_url : 'https://game-stat.com/steam/account-calculator/'}" 233 class="sps-calc-banner sps-calc-banner--vivid"> 234 <svg width="38" height="38" viewBox="0 0 38 38" fill="none"> 235 <circle cx="19" cy="19" r="19" fill="rgba(255,255,255,0.15)"/> 236 <text x="19" y="25" text-anchor="middle" font-size="18" fill="#fff">$</text> 237 </svg> 238 <div class="sps-calc-banner__text">
239 <span class="sps-calc-banner__title">${i18n.calcBannerTitle || 'Steam Account Calculator'}</span> 240 <span class="sps-calc-banner__sub">${i18n.calcBannerSub || 'Total library value · Top games by price · Free vs paid stats'}</span> 241 </div> 242 <span class="sps-calc-banner__btn">${i18n.calcBannerBtn || 'Check'}</span> 243 </a> 244 245 <style> 246 .profile-location, .profile-region, .profile-city { 247 font-weight: 500; 248 color: #444; 249 } 250 251 .profile-region, .profile-city { 252 text-transform: capitalize; 253 } 254 255 /* Current game display styles */ 256 .current-game-badge { 257 margin-top: 8px; 258 padding: 8px 12px; 259 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 260 border-radius: 6px; 261 display: inline-flex; 262 align-items: center; 263 gap: 8px; 264 } 265 266 .game-icon { 267 font-size: 16px; 268 } 269 270 .game-link { 271 color: #fff; 272 text-decoration: none; 273 font-weight: 500; 274 transition: opacity 0.2s; 275 } 276 277 .game-link:hover { 278 opacity: 0.8; 279 text-decoration: underline; 280 } 281 282 .game-server-info { 283 margin-top: 4px; 284 color: #666; 285 font-size: 0.9em; 286 } 287 </style> 288 `; 289 290 // ÐобавлÑем блок Extended Information Ñ ÐºÐ½Ð¾Ð¿ÐºÐ°Ð¼Ð¸ поÑле оÑновной инÑоÑмаÑии 291 const extendedInfoContainer = document.createElement('div'); 292 extendedInfoContainer.className = 'steam-extended-info'; 293 extendedInfoContainer.setAttribute('data-steam-id', data.steamid); 294 295 // ÐенеÑиÑÑем HTML Ð´Ð»Ñ Ð±Ð»Ð¾ÐºÐ° Extended Information 296 extendedInfoContainer.innerHTML = ` 297 <div class="steam-extended-header"> 298 <h3 class="steam-extended-title"> 299 <i class="steam-icon steam-icon-extended"></i> 300 ${i18n.extendedInformation || 'Extended Information'} 301 </h3> 302 </div> 303 304 <div class="steam-extended-buttons"> 305 <button class="steam-module-btn" data-module="games" data-steam-id="${data.steamid}"> 306 <i class="steam-icon steam-icon-games"></i> 307 ${i18n.games || 'Games'} 308 </button> 309 <button class="steam-module-btn" data-module="friends" data-steam-id="${data.steamid}"> 310 <i class="steam-icon steam-icon-friends"></i> 311 ${i18n.friends || 'Friends'} 312 </button> 313 <button class="steam-module-btn" data-module="inventory" data-steam-id="${data.steamid}"> 314 <i class="steam-icon steam-icon-inventory"></i> 315 ${i18n.inventory || 'Inventory'} 316 </button> 317 </div> 318 319 <div class="steam-modules-container" id="steam-modules-${data.steamid}"></div> 320 `; 321 322 // ÐобавлÑем конÑÐµÐ¹Ð½ÐµÑ Ñ Ð±Ð»Ð¾ÐºÐ¾Ð¼ Extended Information поÑле оÑновной инÑоÑмаÑии 323 userInfo.appendChild(extendedInfoContainer); 324};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.