// UserScript
// @name SteamGifts Train Assistant
// @namespace sg-train-tools
// @version 4.1
// @description Highlights the giveaway header based on Steam/SG status; optionally auto-skips in trains; adds compact π/π rating and a top-right settings panel.
// @match https://www.steamgifts.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect store.steampowered.com
// @run-at document-idle
// /UserScript
/* AI helper notes (for anyone inspecting the script with an AI tool)
What this userscript does (high level):
Parses the current SteamGifts giveaway page and identifies the target game (app/sub).
Fetches the userβs Steam dynamicstore userdata (Owned / Wishlist / Not Interested) and their SteamGifts hidden list (from filters page). Both are cached briefly.
Visually marks the giveaway header:
Owned β green, Wishlist β blue, Hidden (Steam or SG) β red.
Adds a small diagonal ribbon with the status text.
Adds compact π / π buttons next to the giveaway title. The choice is stored locally (by canonical id). A π can be configured to be skipped automatically when riding a βtrainβ.
Provides a settings panel (gear icon in the top-right of the header, English UI). You can:
Toggle using Steam data and/or SteamGifts hidden list.
Choose skip rules for trains and set a delay (0 = instant).
View and remove your locally saved disliked entries.
When skip rules match (e.g., Owned or Not Interested, SG Hidden, or π), the script finds the Next giveaway link in the description and navigates automatically, showing a small toast summary.
Privacy & scope:
Only reads Steamβs public dynamicstore/userdata for the logged-in user and your SG filters page to infer hidden app ids.
No data is sent anywhere else; preferences persist in localStorage.
(() => {
'use strict';
/ ===================== CONSTANTS ===================== /
const ARALIK_MS = 8000;
const STEAM_TTL = ARALIK_MS;
const SG_TTL = ARALIK_MS;
const SUB_TTL = 72460601000;
const RENKLER = { sgHidden:'#d32d2d', owned:'#228b22', wish:'#1e90ff', ignored:'#d32d2d' };
const OVERLAY_OP=0.55, SERIT_OP=0.28, PARILTI_A=0.35;
// Storage keys
const LS_SG_IDS='tr_sg_hidden_appids_v1', LS_SG_TS='tr_sg_hidden_appids_ts_v1';
const LS_STEAM='tr_steam_userdata_v1', LS_ST_TS='tr_steam_userdata_ts_v1';
const LS_AYAR ='sgtr_settings_v3';
const LS_OYLAMA='sgtr_ratings_v1';
// Skip summaries (session)
const SS_SKIP_ADET='sgtr_skip_count';
const SS_SKIP_SON_BASLIK='sgtr_skip_last_title';
const SS_SKIP_SON_NEDEN='sgtr_skip_last_reason';
const SG_FILTRE_URL = 'https://www.steamgifts.com/account/settings/giveaways/filters';
const STEAM_KULLANICI = 'https://store.steampowered.com/dynamicstore/userdata/?t=';
/ ===================== HELPERS ===================== /
const disKapsul = ()=>document.querySelector('.featured__outer-wrap');
const lsAl=(k,fb=null)=>{ try{const v=localStorage.getItem(k); return v==null?fb:JSON.parse(v);}catch{ return fb; } };
const lsYaz=(k,v)=> localStorage.setItem(k, JSON.stringify(v));
const bayatMi=(tsKey,ttl)=> Date.now()-Number(localStorage.getItem(tsKey)||0) > ttl;
const dizi2Kume=(arr)=> (Array.isArray(arr)?arr:[]).reduce((m,id)=>{m[id]=1;return m;},{}); // Set-like map
function renkDegerAta(el, hex){
const s=hex.replace('#',''); const n=s.length===3?s.split('').map(c=>c+c).join(''):s;
const r=parseInt(n.slice(0,2),16), g=parseInt(n.slice(2,4),16), b=parseInt(n.slice(4,6),16);
el.style.setProperty('--trsg-rgb', ${r}, ${g}, ${b}
);
el.style.setProperty('--trsg-hex', #${n}
);
}
function seritKaldir(el){ el.querySelector('.trsg-serit')?.remove(); }
function durumUygula(el, etiket, hex){
el.classList.add('trsg-durum');
renkDegerAta(el,hex); seritKaldir(el);
const d=document.createElement('div'); d.className='trsg-serit'; d.textContent=etiket; el.appendChild(d);
}
function durumTemizle(el){
el.classList.remove('trsg-durum');
el.style.removeProperty('--trsg-rgb'); el.style.removeProperty('--trsg-hex');
seritKaldir(el);
}
// Clean game title: only text nodes of .featured__heading__medium
function oyunBasligiAl(){
const m = document.querySelector('.featured__heading__medium');
if (m){
let raw = '';
m.childNodes.forEach(n => { if (n.nodeType === Node.TEXT_NODE) raw += n.textContent; });
raw = (raw || m.textContent || '').replace(/\s(\s\d+\sP)\s$/i,'').trim();
if (raw) return raw;
}
const h = document.querySelector('.giveaway__heading__name');
if (h && h.textContent.trim()) return h.textContent.trim();
return (document.title||'').replace(/\s[-|]\sSteamGifts./i,'').replace(/\s(\d+P)\s*$/i,'').trim() || 'Game';
}
GM_addStyle(`
.featured__outer-wrap{position:relative;--trsg-extra-shadow:none}
.trsg-durum{position:relative}
.trsg-durum>*{position:relative;z-index:1}
.trsg-durum::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:0}
.trsg-durum{
--trsg-rgb:128,128,128;--trsg-hex:#888;
box-shadow: inset 0 -10px 0 rgba(var(--trsg-rgb),1), 0 12px 30px rgba(var(--trsg-rgb),${PARILTI_A}), var(--trsg-extra-shadow);
}
.trsg-durum::before{
background:
linear-gradient(90deg,rgba(var(--trsg-rgb),${OVERLAY_OP}) 0%,rgba(var(--trsg-rgb),0) 65%),
repeating-linear-gradient(-45deg,rgba(var(--trsg-rgb),${SERIT_OP}) 0 8px,rgba(0,0,0,0) 8px 16px)
}
.trsg-serit{position:absolute;top:14px;right:-36px;width:240px;transform:rotate(35deg);text-align:center;padding:6px 16px;color:#fff;font-weight:700;font-size:12px;background:var(--trsg-hex);box-shadow:0 6px 18px rgba(0,0,0,.35);z-index:5;text-transform:uppercase}
`);
/ ===================== SETTINGS ===================== /
const AYAR_DEF = {
showDisliked: true,
steamData: true,
showSGHidden: true,
skip: { all:false, viaDislike:true, viaSteamOwnedIgnored:true, viaSGHidden:true, delaySec:3 }
};
const ayarYukle = () => {
try {
const raw = JSON.parse(localStorage.getItem(LS_AYAR) || 'null');
return Object.assign({}, AYAR_DEF, raw || {}, { skip:Object.assign({}, AYAR_DEF.skip, raw?.skip || {}) });
} catch { return JSON.parse(JSON.stringify(AYAR_DEF)); }
};
const ayarKaydet = (s)=> localStorage.setItem(LS_AYAR, JSON.stringify(s));
const oyHaritasi = () => { let m={}; try{ m=JSON.parse(localStorage.getItem(LS_OYLAMA)||'{}'); }catch{} return m; };
function oyHaritasiAyarla(id, durum, baslik){
const m = oyHaritasi();
if (!durum) delete m[id];
else m[id] = { state:durum, title: baslik || m[id]?.title || oyunBasligiAl() };
localStorage.setItem(LS_OYLAMA, JSON.stringify(m));
}
function bildirim(msj){
let t = document.querySelector('.trsg-bildirim'); if (!t){ t=document.createElement('div'); t.className='trsg-bildirim'; document.body.appendChild(t); }
t.textContent = msj; t.style.opacity = '1';
setTimeout(()=>{ t.style.transition='opacity .35s'; t.style.opacity='0'; }, 2200);
}
/ ============ SETTINGS UI (body-portal) ============ /
(function ayarUIOlustur(){
const host = document.querySelector('.featured__inner-wrap'); if (!host) return;
})();
/ ===================== RATING UI ===================== /
function mevcutKanonikId(){
const aApp = document.querySelector('a[href="store.steampowered.com/app/"], a[href="store.steampowered.com/agecheck/app/"]');
if (aApp){ const m=aApp.href.match(/\/app\/(\d+)/); if(m) return app_${m[1]}
; }
const aSub = document.querySelector('a[href="store.steampowered.com/sub/"], a[href="store.steampowered.com/agecheck/sub/"]');
if (aSub){ const m=aSub.href.match(/\/sub\/(\d+)/); if(m) return sub_${m[1]}
; }
const kod = (location.pathname.match(/\/giveaway\/([A-Za-z0-9]{5})/)||[])[1];
return kod ? ga_${kod}
: title_${oyunBasligiAl()}
;
}
function oylamaUIOlustur(){
const baslikSatiri = document.querySelector('.featured__heading'); if (!baslikSatiri || baslikSatiri.querySelector('.trsg-oylama')) return;
const ui = document.createElement('span'); ui.className='trsg-oylama';
const yap=(t,c,ttl)=>{ const b=document.createElement('button'); b.className=trsg-oybtn ${c}
; b.type='button'; b.title=ttl; b.textContent=t; return b; };
const up=yap('π','up','Thumbs Up'); const down=yap('π','down','Thumbs Down');
ui.append(up,down); baslikSatiri.appendChild(ui);
}
/ ===================== TRAIN HELPERS ===================== /
const KELIME_SONRA = ['next','sonraki','ileri','sΔ±radaki','forward','advance'];
const KELIME_ONCE = ['back','previous','prev','geri','ΓΆnceki','onceki'];
const OK_SAG = ['β©','β‘','β','Β»','βΊ','>>'];
const OK_SOL = ['βͺ','β¬
','β','Β«','βΉ','<<'];
const trLower = s => (s||'').toLocaleLowerCase('tr');
function aciklamaKok(doc){ return doc.querySelector('.page__description, .giveaway__description, .markdown, .comment__description') || doc; }
function gaKod(url){ const m=String(url).match(/\/giveaway\/([A-Za-z0-9]{5})(?:\/|$)/); return m?m[1]:null; }
function sonrakiBagBul(mevcutURL){
const cur = gaKod(mevcutURL);
const root = aciklamaKok(document);
let as = Array.from(root.querySelectorAll('a')); if (as.length>50) as=as.slice(0,50);
const items = as.map(a=>{
const href=new URL(a.getAttribute('href')||'', location.href).href;
const code=gaKod(href);
return { href, code, txt:(a.textContent||'').trim() };
}).filter(x=>x.code && x.code!cur);
if (!items.length) return null;
const prevIdx = items.findIndex(x => KELIME_ONCE.some(w=>trLower(x.txt).includes(w)) || OK_SOL.some(s=>x.txt.includes(s)));
const noPrev = items.filter((_,i)=>i!prevIdx);
const next = noPrev.find(x => KELIME_SONRA.some(w=>trLower(x.txt).includes(w)) || OK_SAG.some(s=>x.txt.includes(s)));
if (next) return next.href;
if (prevIdx>=0){ const afterPrev = items.slice(prevIdx+1).find(x=>x.code!==cur); if (afterPrev) return afterPrev.href; }
return items[items.length-1].href;
}
// Human-readable reason labels
function nedenEtiketi(neden){
if (neden === 'owned') return 'Owned';
if (neden === 'notInterested' || neden === 'sgHidden') return 'Not Interesting';
if (neden === 'dislike') return 'Disliked';
return 'Skipped';
}
// Skip control w/ settings
function skipOzetIsaretle(neden){
const cnt = Number(sessionStorage.getItem(SS_SKIP_ADET)||0);
if (cnt=0){
sessionStorage.setItem(SS_SKIP_SON_BASLIK, oyunBasligiAl());
sessionStorage.setItem(SS_SKIP_SON_NEDEN, neden);
}
sessionStorage.setItem(SS_SKIP_ADET, String(cnt+1));
}
function izinVerenAtlaKuyrugu(neden){
const S = ayarYukle();
const steamGrubu = (neden='owned' || neden='notInterested');
const uygun = S.skip.all
|| (neden='dislike' && S.skip.viaDislike)
|| (steamGrubu && S.steamData && S.skip.viaSteamOwnedIgnored)
|| (neden==='sgHidden' && S.showSGHidden && S.skip.viaSGHidden);
if (!uygun) return;
}
function bekleyenBildirimVarsa(){
const cnt = Number(sessionStorage.getItem(SS_SKIP_ADET)||0);
if (!cnt) return;
if (cnt===1){
const t = sessionStorage.getItem(SS_SKIP_SON_BASLIK)||'Game';
const r = sessionStorage.getItem(SS_SKIP_SON_NEDEN)||'owned';
const etik = nedenEtiketi(r);
bildirim(${t} Skipped (${etik})
);
} else {
bildirim(${cnt} Game Skipped
);
}
sessionStorage.removeItem(SS_SKIP_ADET);
sessionStorage.removeItem(SS_SKIP_SON_BASLIK);
sessionStorage.removeItem(SS_SKIP_SON_NEDEN);
}
/ ===================== DATA LOADERS ===================== /
let sgKilit=false;
async function sgGizliKumeYukle(){
const cached = lsAl(LS_SG_IDS, []);
if (cached.length && !bayatMi(LS_SG_TS, SG_TTL)) return new Set(cached);
if (sgKilit) return new Set(cached);
sgKilit=true;
try{
const res = await fetch(SG_FILTRE_URL, {credentials:'include'});
const html = await res.text();
const ids=new Set();
html.replace(/store.steampowered.com\/app\/(\d+)/g,(,id)=>{ids.add(id);return ;});
html.replace(/\/apps\/(\d+)\//g,(,id)=>{ids.add(id);return ;});
const arr=[...ids]; lsYaz(LS_SG_IDS,arr); localStorage.setItem(LS_SG_TS,Date.now());
return new Set(arr);
}catch{ return new Set(cached); }
finally{ sgKilit=false; }
}
let steamKilit=false;
function steamVerisiYukle(){
return new Promise((coz)=>{
const cached = lsAl(LS_STEAM,null);
if (cached && !bayatMi(LS_ST_TS, STEAM_TTL)) return coz(cached);
if (steamKilit) return coz(cached);
steamKilit=true;
GM_xmlhttpRequest({
method:'GET', url: STEAM_KULLANICI + Date.now(), timeout: ARALIK_MS,
onload:(res)=>{
try{
const raw=JSON.parse(res.responseText);
const out={ owned:dizi2Kume(raw.rgOwnedApps||[]),
wish:dizi2Kume(raw.rgWishlist||[]),
ignored:dizi2Kume(Object.keys(raw.rgIgnoredApps||{})),
ownedPackages:dizi2Kume(raw.rgOwnedPackages||[]) };
lsYaz(LS_STEAM,out); localStorage.setItem(LS_ST_TS, Date.now()); coz(out);
}catch{ coz(cached); }
},
onerror: ()=>coz(cached),
ontimeout:()=>coz(cached),
onloadend:()=>{ steamKilit=false; }
});
});
}
const altKey=id=>tr_subapps_${id}
, altTs=id=>tr_subapps_${id}_ts
;
function subUygulamaYukle(subId){
return new Promise((coz)=>{
const k=altKey(subId), kt=altTs(subId);
const cached=lsAl(k,null), ts=Number(localStorage.getItem(kt)||0);
if (cached && Date.now()-ts < SUB_TTL) return coz(cached);
GM_xmlhttpRequest({
method:'GET', url:https://store.steampowered.com/api/packagedetails/?packageids=${subId}
, timeout:ARALIK_MS,
onload:(res)=>{
try{
const json=JSON.parse(res.responseText);
const node=json?.[subId]?.data;
const apps=Array.isArray(node?.apps) ? node.apps.map(a=>String(a.appid)) : [];
lsYaz(k,apps); localStorage.setItem(kt,Date.now()); coz(apps);
}catch{ coz(cached||[]); }
},
onerror: ()=>coz(cached||[]),
ontimeout:()=>coz(cached||[])
});
});
}
/ ===================== TARGET & EVAL ===================== /
function hedefCoz(){
const aApp = document.querySelector('a[href="store.steampowered.com/app/"], a[href="store.steampowered.com/agecheck/app/"]');
if (aApp){ const m=aApp.href.match(/\/app\/(\d+)/); if(m) return {tur:'app', id:m[1]}; }
const aSub = document.querySelector('a[href="store.steampowered.com/sub/"], a[href="store.steampowered.com/agecheck/sub/"]');
if (aSub){ const m=aSub.href.match(/\/sub\/(\d+)/); if(m) return {tur:'sub', id:m[1]}; }
const m2=document.documentElement.innerHTML.match(/store.steampowered.com\/app\/(\d+)/);
if (m2) return {tur:'app', id:m2[1]};
return null;
}
let sonKey=null, sonEtiket=null, bildirimGosterildi=false;
async function degerlendir(){
const wrap = disKapsul(); if (!wrap) return;
oylamaUIOlustur();
}
/ ===================== BOOT ===================== /
if (!disKapsul()) return;
degerlendir();
setTimeout(degerlendir, 600);
setInterval(()=>{ if (!document.hidden) { bildirimGosterildi=false; degerlendir(); } }, ARALIK_MS);
document.addEventListener('visibilitychange', ()=>{ if (!document.hidden){ bildirimGosterildi=false; degerlendir(); } });
})();