// ==UserScript== // @name Steam.SM: Sale List to Image // @namespace Steam.SM // @version 1.0 // @description Save games list with discount in tabular form as an image (special for InfoCat) // @author StiGMaT // @match https://store.steampowered.com/search/* // @icon https://www.google.com/s2/favicons?sz=64&domain=steampowered.com // @updateURL https://gist.githubusercontent.com/soft-sm/49d2a039a1179e9dbb19037da8ea9c39/raw // @downloadURL https://gist.githubusercontent.com/soft-sm/49d2a039a1179e9dbb19037da8ea9c39/raw // @connect shared.akamai.steamstatic.com // @connect shared.cloudflare.steamstatic.com // @connect steamdb.info // @require https://html2canvas.hertzen.com/dist/html2canvas.min.js // @require https://gist.githubusercontent.com/soft-sm/b2f031fca50d5a8c428225f41c7d7996/raw // @grant GM.addStyle // @grant GM.xmlHttpRequest // ==/UserScript== /* global $, addButton, setInfoText, getSteamLang */ /* global GStoreItemData */ /* global html2canvas */ (function() { 'use strict'; const ID_SCRIPT = 'ssm__slti'; const IMG_DATA = { h215: {name: 'header.jpg', width: 460, height: 215}, h192: {name: 'header_586x192.jpg', width: 586, height: 192, type: 1}, h136: {name: 'header_292x136.jpg', width: 292, height: 136}, c45: {name: 'capsule_sm_120.jpg', width: 120, height: 45}, c69: {name: 'capsule_184x69.jpg', width: 184, height: 69}, c87: {name: 'capsule_231x87.jpg', width: 231, height: 87}, c181: {name: 'capsule_467x181.jpg', width: 467, height: 181}, c353: {name: 'capsule_616x353.jpg', width: 616, height: 353}, l450: {name: 'library_600x900.jpg', width: 300, height: 450, type: 0}, }; const OPTIONS = { ru: { flag: 'data:image/svg+xml,', round: true, cur: 'RUB', }, kz:{ // flag: 'https://steamdb.info/static/country/kz.svg', flag: 'data:image/svg+xml,', round: true, cur: 'KZT', }, ua:{ flag: 'data:image/svg+xml,', round: true, cur: 'UAH', }, tr:{ flag: 'data:image/svg+xml,', cur: 'USD', }, us:{ flag: 'data:image/svg+xml,', cur: 'USD', }, } const STEAM_STYLES = ` :root { --ssm__check-svg: url("data:image/svg+xml,"); } .search_result_row .col.search_capsule { position: relative; } .search_result_row.ssm__checked .col.search_capsule::before { content: ""; position: absolute; width: 100%; height: 100%; background-image: var(--ssm__check-svg); background-repeat: no-repeat; background-size: auto 70%; background-color: rgb(76 175 80 / 40%); background-position: center; } `; // .search_result_row.ssm__checked .col.search_capsule${CHECK_BEFORE_STYLE} const MODAL_STYLES = ` .ssm__overlay { position: fixed; z-index: 10000; background-color: rgba(0, 0, 0, 0.8); top: 0; right: 0; bottom: 0; left: 0; display: flex; justify-content: center; align-items: center; } .ssm__ui { font-size: ${(window.screen.height/15).toFixed(0)}px; line-height: 1.1em; } .ssm__ui .ssm__btn { position: absolute; cursor: pointer; margin: 0 calc(1em/3); z-index: 10000; } .ssm__btn.top { top: 0; } .ssm__btn.bottom { bottom: 0; } .ssm__btn.left { left: 0; } .ssm__btn.right { right: 0; } .ssm__btn.close::before { content: "\\00d7"; } .ssm__btn.save::before { content: "\\002b"; } .ssm__btn.left.pagin::before { content: "\\276E"; } .ssm__btn.right.pagin::before { content: "\\276F"; } .ssm__btn.info { cursor: default; font-size: .5em; font-weight: 600; line-height: 2.2em; } .ssm__header { display: flex; justify-content: center; align-items: center; padding: .1em; text-transform: uppercase; font-weight: bold; } .ssm__header :not(:first-child) { margin-left: .1em; } .ssm__header img { width: 1em; border-radius: 50%; object-fit: contain; aspect-ratio: 1/1; box-shadow: 0 0 .1em #0009; } .ssm__content { display: grid; justify-content: space-evenly; } .ssm__dis_item { display: grid; grid-template-columns: repeat(2, 1fr); background: #000; gap: .05em; padding: .05em; } .ssm__dis_item img { grid-column: 1 / -1; width: 100%; height: auto; object-fit: cover; align-content: center; text-align: center; font-size: .5em; font-weight: 800; } .ssm__dis_pct { background: #4c6b22; color: #BEEE11; font-size: 1em; line-height: 1.2em; font-weight: 500; text-align: center; } .ssm__dis_prices { background: #344654; } .ssm__dis_prices>div { display: flex; flex-direction: column; align-items: flex-end; justify-content: center; justify-content: space-evenly; margin: auto; height: 100%; width: fit-content; } .ssm__dis_original_price { position: relative; width: fit-content; color: #738895; font-size: .4em; line-height: .8em; } .ssm__dis_original_price::before { content: ''; left: 0px; right: 0px; position: absolute; top: 43%; border-bottom: .15em solid #738895; transform: skewY(-8deg); box-shadow: 0 0 .2em black; } .ssm__dis_final_price { color: #BEEE11; font-size: 0.6em; line-height: .8em; } .load-spin { border: .2em solid #f3f3f3; border-top: .2em solid #3498db; border-radius: 50%; width: 1em; height: 1em; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; GM.addStyle(STEAM_STYLES+MODAL_STYLES); const STEAM_ITEM_TYPES = ['app', 'bundle', 'sub']; let frmCur = GStoreItemData.fnFormatCurrency.toString().match(/v_numberformat\(.*?"(.*?)",\s*"(.*?)"\)/); function ScaleElement($element, perc = .90) { const scaleRatio = Math.min( window.innerHeight* perc / $element.outerHeight(), window.innerWidth * perc / $element.outerWidth() ); $element.css({transform: `scale(${scaleRatio})`}); } function ScaleModal(scale = true) { return new Promise((resolve) => { // $modal.one('transitionend', () => { // resolve(); // }); if (scale) { ScaleElement($modal); } else { $modal.css({ transform: '' }); } resolve(); }); } function getBase64Image(url) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "GET", url: url, responseType: "blob", onload: function(response) { const reader = new FileReader(); reader.onloadend = function() { resolve(reader.result); }; reader.onerror = reject; reader.readAsDataURL(response.response); }, onerror: reject }); }); } async function CreateItemDataList(imgName, defCount, sortField, sortOrder) { const IMG_URL = `https://shared.${false?'cloudflare':'akamai'}.steamstatic.com/store_item_assets/steam/`; let $blkGames = $('.search_result_row.ssm__checked'); if (!$blkGames.length) { $blkGames = $(`.search_result_row${defCount>0?':lt('+defCount+')':''}`) }; setInfoText(`Load data 0\\${$blkGames.length}`); const res = [], promises = []; let progress = 0; $blkGames.each((index, item) => { let $blkDiscount = $('.discount_block', item), discount = $blkDiscount.data('discount'); if (discount) { let itemKey = $(item).data('ds-itemkey').split('_'), type = STEAM_ITEM_TYPES.indexOf(itemKey[0].toLowerCase()), id = parseInt(itemKey[1]), srcParts = []; // if (type) { let srcStart = 6; srcParts = $('.search_capsule img', item).attr('src').split('/'); if (srcParts[srcStart] !== itemKey[1]) { srcStart = srcParts.indexOf(itemKey[1]); } srcParts = srcParts.slice(srcStart+1, -1); //srcParts = srcParts.slice(srcStart+1, (srcParts.length - srcStart < 4)?-1:undefined); // } let itemData = { index, type, id, name: $('.search_name .title', item).text().trim(), discount: $blkDiscount.data('discount'), final: $blkDiscount.data('priceFinal')/100, original: parseFloat($('.discount_original_price', $blkDiscount).text().replaceAll(frmCur[2],"").replaceAll(frmCur[1],".").replaceAll(/[^\d\.]/g,"")), reviewPerc: $(item).data('as-review-percentage'), reviewCount: $(item).data('as-review-count'), imgSrc: IMG_URL + STEAM_ITEM_TYPES[type] +'s/' + id + (type==1?'/'+srcParts[0]:'') + '/'+(imgName||'header.jpg') }; promises.push(getBase64Image(itemData.imgSrc).then(base64 => { itemData.imgBase64 = base64; progress++; setInfoText(`Load data ${progress}\\${$blkGames.length}`); })); res.push(itemData); } else { progress++; } }); await Promise.all(promises); setInfoText('Sort data'); for (let ii = sortField.length-1; ii >= 0; ii--) { if (res.length && (sortField[ii] in res[0])) { if (typeof res[0][sortField[ii]] === 'number') { res.sort((a,b) => { return sortOrder[ii] * (a[sortField[ii]] - b[sortField[ii]]) }); } else { res.sort((a,b) => { return sortOrder[ii] * a[sortField[ii]].localeCompare(b[sortField[ii]]) }); } } }; setInfoText(''); return res; } function GetFontNameFromUrl(url) { const match = /family=([^:&]+)/.exec(url); if (match) { return match[1].replaceAll('+', ' '); } } async function CreateModal({mainWidth, fntSize, itemGutter, fntColor, fntImport, fntFamily, fntImportHeadOnly, bgColor, headColor, colCount, rowCount, imgType, sortField=[], sortDescOrder=[], saveScale = 1} = {}) { let imgData = IMG_DATA[imgType in IMG_DATA?imgType:Object.keys(IMG_DATA)[0]]; colCount = colCount?colCount:(rowCount?rowCount-1:0); rowCount = rowCount?rowCount:(colCount?colCount+1:0); cntPerPage = colCount*rowCount; currentPage = 1; let optStyle = ` ${fntImport?'@import url("'+fntImport+'");':''} .ssm__modal { ${mainWidth||!cntPerPage?'width: '+(mainWidth?mainWidth+'px':'100%')+';':''} background-color: ${bgColor||'#1b2838'}; font-family: ${fntImport&&!fntImportHeadOnly?'"'+(fntFamily?fntFamily:GetFontNameFromUrl(fntImport))+'"':'"Motiva Sans", Sans-serif'}; font-size: ${fntSize||(0.16*imgData.width).toFixed(0)}px; } .ssm__header { background: ${headColor||'linear-gradient(135deg, #67c1f5 0%, #417a9b 100%)'}; color: ${fntColor||'#fff'}; margin-top: ${itemGutter?itemGutter/2+'px':'.4em'}; ${fntImport&&fntImportHeadOnly?'font-family: "'+(fntFamily?fntFamily:GetFontNameFromUrl(fntImport))+'";':''} } .ssm__content { grid-template-columns: repeat(${colCount?colCount+', 1fr':'auto-fit, minmax('+imgData.width+'px, 1fr)'}); grid-gap: ${itemGutter?itemGutter+'px':'.8em'}; margin: ${itemGutter?itemGutter+'px':'.8em'}; } .ssm__dis_item { ${mainWidth||!cntPerPage?'min-':''}width: ${imgData.width+'px'}; } img.not_app { aspect-ratio: ${imgData.width}/${imgData.height}; } ` const cc = $('#application_config').data('userinfo').country_code.toLowerCase(); const flag = (cc in OPTIONS && 'flag' in OPTIONS[cc])?OPTIONS[cc].flag:`https://community.akamai.steamstatic.com/public/images/countryflags/${cc}.gif`; //const flag = await getBase64Image('https://steamdb.info/static/country/kz.svg'), const numFormat = Intl.NumberFormat(getSteamLang(), { style: 'currency', currency: OPTIONS[cc].cur, minimumFractionDigits: OPTIONS[cc].round?0:2, maximumFractionDigits: OPTIONS[cc].round?0:2, currencyDisplay: 'narrowSymbol', }); if (!Array.isArray(sortField)) { sortField = sortField.split(/\s*,\s*/); } if (!Array.isArray(sortDescOrder)) { sortDescOrder = [sortDescOrder]; } sortDescOrder.length = sortField.length; sortDescOrder = sortField.map((item,index) => 1-2*!!sortDescOrder[index]); itemsData = await CreateItemDataList(imgData.name, cntPerPage, sortField, sortDescOrder); cntItems = itemsData.length; cntPerPage = cntPerPage||(cntItems>100?100:cntItems) $ssm__slti = $('#'+ID_SCRIPT); let $size, $pages, $prev, $next, $save, $spin; if ($ssm__slti.length) { $ssm__slti.show(); $modal = $('.ssm__modal', $ssm__slti); $size = $('.info.top', $ssm__slti); $pages = $('.info.bottom', $ssm__slti); $prev = $('.left.pagin', $ssm__slti); $next = $('.right.pagin', $ssm__slti); } else { $prev = $('', { class: 'ssm__btn left pagin'}); $next = $('', { class: 'ssm__btn right pagin'}); $pages = $('', { class: 'ssm__btn bottom info'}); $size = $('', { class: 'ssm__btn top info' }); $save = $('', { class: 'ssm__btn bottom left save'}); $modal = $('
', { class: 'ssm__modal' }); $spin = $('
', { class: 'ssm__overlay spin'}).append( $('
', { class: 'load-spin' }) ).hide(); $ssm__slti = $('
', { id: ID_SCRIPT, class: 'ssm__overlay' }).append( $('