/* ============================================================================= * SGLV PTW · Tab: 全部游戏列表 (All Games) * ----------------------------------------------------------------------------- * 迁移自主脚本 mountPlaytimeAllList(v2.20.0 :15850-16058,209 行) * 迁移纪律:纯搬运不改逻辑。唯一改动 = 依赖改为经 ctx.host 注入。 * * 依赖注入对照表: * state.ownedGames → ctx.host.getOwnedGames() * isGameOwnedByMe(g) → ctx.host.isGameOwnedByMe(g) * ptGetAcquiredMap() → ctx.host.getAcquiredMap() * buildListToolbar / hbarRow / emptyHtml / paginate / renderPagination * → ctx.host.* * escHtml / T / isZh → ctx.host.* * getGameIconUrl → ctx.host.getGameIconUrl * applyGameAliases → ctx.host.applyGameAliases * sglvToast → ctx.host.showToast * * 视图态:原实现用闭包变量(page/query/sortKey/sortAsc/hideUnplayed/hideShared), * 此处改为读写 ctx.view —— 换取跨 Tab 联动 + 刷新不丢。 * ========================================================================== */ (function (root) { 'use strict'; const PT_ALL_PAGE_SIZE = 50; function mount(container, ctx) { const H = ctx.host; const T = H.T; const isZh = H.isZh; const escHtml = H.escHtml; const sortedGames = ctx.getGames(); const view = ctx.view; const nameCmp = (a, b) => String(a.name || '').localeCompare( String(b.name || ''), isZh ? 'zh-Hans-CN' : 'en', { numeric: true, sensitivity: 'base' }); let acquiredMap = view.acquiredMap || null; function mountList(container2, data) { let sortKey = view.sortKey || 'playtime'; let sortAsc = !!view.sortAsc; let hideUnplayed = !!view.hideUnplayed; let hideShared = !!view.hideShared; let page = view.page || 1; let query = view.query || ''; const hasSharedGames = data.some(g => !H.isGameOwnedByMe(g)); function sortBy(key) { const m = acquiredMap || {}; const hasAcq = Object.keys(m).length > 0; const acqDesc = (a, b) => (hasAcq ? ((m[b.appid] || 0) - (m[a.appid] || 0)) : 0); let cmp; if (key === 'recent') { cmp = (a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0) || (b.playtime || 0) - (a.playtime || 0) || acqDesc(a, b) || nameCmp(a, b); } else if (key === 'name') { cmp = (a, b) => nameCmp(a, b) || (b.playtime || 0) - (a.playtime || 0); } else if (key === 'acquired') { cmp = (a, b) => acqDesc(a, b) || (b.playtime || 0) - (a.playtime || 0) || nameCmp(a, b); } else { cmp = (a, b) => (b.playtime || 0) - (a.playtime || 0) || (b.lastPlayed || 0) - (a.lastPlayed || 0) || acqDesc(a, b) || nameCmp(a, b); } const mul = sortAsc ? -1 : 1; data.sort((a, b) => mul * cmp(a, b)); } const filterChipsHtml = ` ${hasSharedGames ? `` : ''} `; const toolbar = H.buildListToolbar({ cls: 'sglv-pt-all-toolbar', search: { id: 'sglv-pt-search', placeholder: T.ptSearchPlaceholder, style: 'flex:1;min-width:140px' }, sort: { id: 'sglv-pt-sort', value: sortKey, options: [ { value: 'playtime', label: T.ptSortPlaytime }, { value: 'recent', label: T.ptSortRecent }, { value: 'acquired', label: T.ptSortAcquired }, { value: 'name', label: T.ptSortName }, ], }, extraHtml: `${filterChipsHtml} `, onChange: ({ type, value }) => { if (type === 'input') { query = value; ctx.setView({ query }); renderList(1); } else if (type === 'sort') { applySort(value).then(() => renderList(1)); } }, }); container2.appendChild(toolbar); const hintEl = document.createElement('div'); hintEl.className = 'sglv-pt-all-hint'; hintEl.style.display = 'none'; container2.appendChild(hintEl); const listEl = document.createElement('div'); listEl.className = 'sglv-pt-all-list'; listEl.id = 'sglv-pt-all-list'; const pagEl = document.createElement('div'); pagEl.className = 'sglv-pagination'; pagEl.id = 'sglv-pt-pagination'; container2.appendChild(listEl); container2.appendChild(pagEl); // 事件委托:整页 HTML 一次性渲染后仅保留 1 个 click 监听 listEl.addEventListener('click', e => { const row = e.target.closest('.sglv-pt-all-item'); if (row && row.dataset.appid) { root.open(`https://store.steampowered.com/app/${row.dataset.appid}`, '_blank'); } }); const countEl = toolbar.querySelector('#sglv-pt-count'); const sortSel = toolbar.querySelector('#sglv-pt-sort'); const hideUnplayedBtn = toolbar.querySelector('#sglv-pt-hide-unplayed'); if (hideUnplayedBtn) { if (hideUnplayed) hideUnplayedBtn.classList.add('active'); hideUnplayedBtn.addEventListener('click', () => { hideUnplayed = !hideUnplayed; hideUnplayedBtn.classList.toggle('active', hideUnplayed); ctx.setView({ hideUnplayed }); renderList(1); }); } const hideSharedBtn = toolbar.querySelector('#sglv-pt-hide-shared'); if (hideSharedBtn) { if (hideShared) hideSharedBtn.classList.add('active'); hideSharedBtn.addEventListener('click', () => { hideShared = !hideShared; hideSharedBtn.classList.toggle('active', hideShared); ctx.setView({ hideShared }); renderList(1); }); } // v2.20.2 (T3.6): 导出当前视图态(全量筛选结果,非当前页)→ CSV const exportBtn = toolbar.querySelector('#sglv-pt-export'); if (exportBtn) { exportBtn.addEventListener('click', () => { const rows = currentViewGames().map(g => [ g.appid, g.name || '', Math.round((g.playtime || 0) / 60 * 10) / 10, g.lastPlayed ? new Date(g.lastPlayed * 1000).toISOString().slice(0, 10) : '', H.isGameOwnedByMe(g) ? 1 : 0, ]); const headers = ['appid', 'name', 'playtime_hours', 'last_played', 'owned_by_me']; if (typeof ctx.exportCSV === 'function') { ctx.exportCSV(headers, rows, 'sglv-games'); } else if (typeof H.showToast === 'function') { H.showToast(isZh ? '导出功能不可用' : 'Export unavailable', 'warn'); } }); } const sortDirBtn = toolbar.querySelector('#sglv-pt-sort-dir'); if (sortDirBtn) { if (sortAsc) { sortDirBtn.textContent = T.ptSortAsc; sortDirBtn.title = T.ptSortAsc; sortDirBtn.classList.add('active'); } sortDirBtn.addEventListener('click', () => { sortAsc = !sortAsc; sortDirBtn.textContent = sortAsc ? T.ptSortAsc : T.ptSortDesc; sortDirBtn.title = sortAsc ? T.ptSortAsc : T.ptSortDesc; sortDirBtn.classList.toggle('active', sortAsc); sortBy(sortKey); ctx.setView({ sortAsc }); renderList(1); }); } const resetToPlaytime = () => { sortKey = 'playtime'; if (sortSel) sortSel.value = 'playtime'; sortBy('playtime'); ctx.setView({ sortKey }); }; // 入库时间排序依赖家庭组数据(异步);无数据 / 失败回退时长排序 function applySort(key) { if (key === 'acquired' && !acquiredMap) { listEl.innerHTML = `
${escHtml(T.ptSortAcquiredLoading)}
`; pagEl.innerHTML = ''; return H.getAcquiredMap() .then(map => { acquiredMap = map || {}; ctx.setView({ acquiredMap }); if (Object.keys(acquiredMap).length === 0) { resetToPlaytime(); H.showToast.info(T.ptSortAcquiredEmpty); return; } sortKey = 'acquired'; ctx.setView({ sortKey }); sortBy('acquired'); }) .catch(() => { resetToPlaytime(); H.showToast.info(T.ptSortAcquiredEmpty); }); } sortKey = key; ctx.setView({ sortKey }); sortBy(key); return Promise.resolve(); } /** * 当前视图态下的完整游戏集合(已应用过滤 + 搜索,未分页)。 * renderList 与 CSV 导出共用,避免「导出结果 ≠ 屏幕结果」的双份逻辑漂移。 */ function currentViewGames() { let out = data; if (hideUnplayed) out = out.filter(g => (g.playtime || 0) > 0); if (hideShared) out = out.filter(g => H.isGameOwnedByMe(g)); if (query) { // v2.20.2 (T3.5): 三路匹配——中文名子串 / 英文名子串 / 拼音首字母子串。 // 原实现仅 name.toLowerCase().includes(q),中文用户必须切输入法。 // 统一走 host.matchGameName;该能力缺失时(软依赖未满足)就地降级为 // 英文子串匹配,绝不让搜索整体失效。 const matcher = typeof H.matchGameName === 'function' ? H.matchGameName : (name, q) => String(name || '').toLowerCase().includes(String(q).trim().toLowerCase()); out = out.filter(g => matcher(g.name, query)); } return out; } function renderList(p = 1) { page = p; ctx.setView({ page }); const displayGames = currentViewGames(); const totalPt = displayGames.reduce((s, g) => s + (g.playtime || 0), 0); if (displayGames.length > 0 && totalPt === 0) { hintEl.textContent = T.ptZeroPtHint; hintEl.style.display = ''; } else { hintEl.style.display = 'none'; } const { pageItems: pageGames, totalPages, page: _clampedPage } = H.paginate(displayGames, page, PT_ALL_PAGE_SIZE); page = _clampedPage; ctx.setView({ page }); const startIdx = (page - 1) * PT_ALL_PAGE_SIZE; countEl.textContent = `${displayGames.length} ${isZh ? '款' : 'items'}`; listEl.innerHTML = ''; if (displayGames.length === 0) { listEl.innerHTML = H.emptyHtml(isZh ? '无匹配游戏' : 'No matching games'); pagEl.innerHTML = ''; return; } // 一次性拼接整页 HTML;图标 lazy + 固定尺寸避免回流 const allMaxPlaytime = pageGames.length > 0 ? Math.max(1, ...pageGames.map(g => g.playtime || 0)) : 1; listEl.innerHTML = pageGames.map((game, i) => { const playtimeMin = game.playtime || 0; const pct = (playtimeMin / allMaxPlaytime * 100).toFixed(1); const playtimeH = playtimeMin > 0 ? (playtimeMin / 60).toFixed(1) + 'h' : T.ptRange0; const iconUrl = H.getGameIconUrl(game.appid, game.icon); return `
${startIdx + i + 1} ${game.name} ${H.hbarRow({ cls: 'ptAll', pct })} ${playtimeH}
`; }).join(''); H.applyGameAliases(listEl); H.renderPagination(pagEl, 'sglv-pt', page, totalPages, displayGames.length, () => renderList(page - 1), () => renderList(page + 1), (p2) => renderList(p2)); } sortBy(sortKey); renderList(1); } // 过滤条件:来自钻取联动(view.layerFilter / acquiredMonth) let data = sortedGames.slice(); if (view.layerFilter && typeof view.layerFilter === 'function') { data = data.filter(view.layerFilter); } // 层筛选 chip(从总览 Tab 钻取过来时显示) if (view.layerFilter || view.acquiredMonth) { const tag = document.createElement('div'); tag.className = 'sglv-pt-all-hint'; tag.style.display = ''; tag.innerHTML = `${escHtml(isZh ? '已应用钻取过滤' : 'Drill-down filter applied')}` + ` `; container.appendChild(tag); const btn = tag.querySelector('#sglv-pt-clear-drill'); if (btn) btn.addEventListener('click', () => ctx.setView({ layerFilter: null, acquiredMonth: null })); } mountList(container, data); } // ---- 注册 ---- if (!root.SGLVPTW) { console.warn('[PTW-TAB-ALL] 框架未就绪,无法注册'); } else { root.SGLVPTW.registerTab({ key: 'all', label: '全部游戏', order: 20, requires: [ 'getOwnedGames', 'isGameOwnedByMe', 'buildListToolbar', 'hbarRow', 'emptyHtml', 'paginate', 'renderPagination', 'getGameIconUrl', 'applyGameAliases', 'T', 'isZh', 'escHtml', ], softRequires: ['getAcquiredMap', 'showToast', 'matchGameName'], mount, }); console.log('[PTW-TAB-ALL] 已注册'); } })(typeof window !== 'undefined' ? window : globalThis);