|
// ==UserScript== |
|
// @name Pinterest Sidebar: Following Boards Tab |
|
// @namespace https://github.com/yukai |
|
// @version 0.1.1 |
|
// @description Adds a "Following boards" tab to Pinterest home sidebar. |
|
// @match https://www.pinterest.com/* |
|
// @downloadURL https://gistcdn.githack.com/Yukaii/00c6c22b34e2451c8e7884b20de88276/raw/b43da434b35645e859846273e507844e5fed7910/pinterest-following-boards.user.js |
|
// @updateURL https://gist.githack.com/Yukaii/00c6c22b34e2451c8e7884b20de88276/raw/pinterest-following-boards.user.js |
|
// @grant none |
|
// @run-at document-start |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
const CUSTOM_FEED_TAB_ATTR = 'data-following-boards-feed-tab'; |
|
const CUSTOM_NAV_TAB_ATTR = 'data-following-boards-nav-tab'; |
|
const CUSTOM_BOUND_ATTR = 'data-following-boards-bound'; |
|
const CUSTOM_TAB_ID = 'codex-following-boards-tab'; |
|
const CUSTOM_FLOAT_TAB_ID = 'codex-following-boards-float-tab'; |
|
const CUSTOM_LABEL = 'Following boards'; |
|
const TOGGLE_ID = 'codex-following-boards-toggle'; |
|
const PENDING_PANEL_KEY = 'codex_following_boards_panel_pending'; |
|
const PANEL_ID = 'codex-following-boards-panel'; |
|
const PANEL_STYLE_ID = 'codex-following-boards-panel-style'; |
|
const CACHE_KEY_PREFIX = 'codex_following_boards_cache_v1'; |
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; |
|
const AVATAR_CACHE_KEY = 'codex_following_boards_avatar_v1'; |
|
let panelRequestSeq = 0; |
|
|
|
function parseUsernameFromPath(pathname) { |
|
const match = pathname.match(/^\/([^/?#]+)\/$/); |
|
if (!match) return null; |
|
const username = match[1]; |
|
const blocked = new Set(['', 'homefeed', 'pin', 'search', 'ideas', 'settings', '_saved']); |
|
return blocked.has(username) ? null : username; |
|
} |
|
|
|
function getUsername() { |
|
const byBoardsTabHref = document |
|
.querySelector('a[data-test-id="boards-tab"][href]') |
|
?.getAttribute('href'); |
|
const fromBoardsTab = byBoardsTabHref ? parseUsernameFromPath(byBoardsTabHref) : null; |
|
if (fromBoardsTab) return fromBoardsTab; |
|
|
|
const byProfileHref = document |
|
.querySelector('a[aria-label][href^="/"][href$="/"]:not([href="/"])') |
|
?.getAttribute('href'); |
|
const fromProfile = byProfileHref ? parseUsernameFromPath(byProfileHref) : null; |
|
if (fromProfile) return fromProfile; |
|
|
|
const pathMatch = location.pathname.match(/^\/([^/?#]+)\/following\/(?:boards|people|creators)\/?$/); |
|
if (pathMatch) return pathMatch[1]; |
|
|
|
return null; |
|
} |
|
|
|
function getProfileHref() { |
|
const username = getUsername(); |
|
if (!username) return null; |
|
return `/${username}/`; |
|
} |
|
|
|
function usernameFromHref(href) { |
|
if (!href) return null; |
|
try { |
|
const path = new URL(href, location.origin).pathname; |
|
const m = path.match(/^\/([^/?#]+)\/$/); |
|
if (!m) return null; |
|
const candidate = m[1]; |
|
const blocked = new Set(['', 'homefeed', 'pin', 'search', 'ideas', 'settings', '_saved']); |
|
return blocked.has(candidate) ? null : candidate; |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
function normalizeText(text) { |
|
return (text || '').replace(/\s+/g, ' ').trim().toLowerCase(); |
|
} |
|
|
|
function compactText(text) { |
|
return (text || '').replace(/\s+/g, ' ').trim(); |
|
} |
|
|
|
function getCacheKey() { |
|
const username = getUsername(); |
|
return username ? `${CACHE_KEY_PREFIX}:${username}` : CACHE_KEY_PREFIX; |
|
} |
|
|
|
function readAvatarCache() { |
|
try { |
|
const raw = localStorage.getItem(AVATAR_CACHE_KEY); |
|
if (!raw) return {}; |
|
const parsed = JSON.parse(raw); |
|
return parsed && typeof parsed === 'object' ? parsed : {}; |
|
} catch { |
|
return {}; |
|
} |
|
} |
|
|
|
function writeAvatarCache(cache) { |
|
try { |
|
localStorage.setItem(AVATAR_CACHE_KEY, JSON.stringify(cache)); |
|
} catch { |
|
// Ignore storage failures. |
|
} |
|
} |
|
|
|
function applyAvatarCacheToBoards(boards) { |
|
const avatarCache = readAvatarCache(); |
|
return boards.map((board) => ({ |
|
...board, |
|
ownerAvatar: board.ownerUsername ? (avatarCache[board.ownerUsername] || '') : '', |
|
})); |
|
} |
|
|
|
function readBoardCache() { |
|
try { |
|
const raw = localStorage.getItem(getCacheKey()); |
|
if (!raw) return null; |
|
const parsed = JSON.parse(raw); |
|
if (!parsed || !Array.isArray(parsed.boards) || typeof parsed.ts !== 'number') return null; |
|
if (parsed.boards.length === 0) return null; |
|
return parsed; |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
function writeBoardCache(boards) { |
|
if (!Array.isArray(boards) || boards.length === 0) return; |
|
try { |
|
localStorage.setItem(getCacheKey(), JSON.stringify({ ts: Date.now(), boards })); |
|
} catch { |
|
// Ignore storage failures. |
|
} |
|
} |
|
|
|
function wait(ms) { |
|
return new Promise((resolve) => setTimeout(resolve, ms)); |
|
} |
|
|
|
async function waitFor(predicate, timeoutMs) { |
|
const start = Date.now(); |
|
while (Date.now() - start < timeoutMs) { |
|
const value = predicate(); |
|
if (value) return value; |
|
await wait(80); |
|
} |
|
return null; |
|
} |
|
|
|
function findFollowingButton() { |
|
const candidates = Array.from(document.querySelectorAll('[role="button"], button, a')); |
|
for (const el of candidates) { |
|
if (el.matches(`#${CUSTOM_FLOAT_TAB_ID}, #${CUSTOM_TAB_ID}, [${CUSTOM_NAV_TAB_ATTR}="1"], [${CUSTOM_FEED_TAB_ATTR}="1"]`)) { |
|
continue; |
|
} |
|
if (el.closest('[role="dialog"]')) continue; |
|
const text = normalizeText(el.textContent); |
|
const aria = normalizeText(el.getAttribute('aria-label')); |
|
if (el.getAttribute('role') === 'button' || el.tagName === 'BUTTON' || el.tagName === 'DIV') { |
|
if (/追蹤中|following/.test(`${text} ${aria}`)) return el; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
function findBoardsTabInDialog(dialog) { |
|
if (!dialog) return null; |
|
const candidates = Array.from(dialog.querySelectorAll('[role="tab"], button, a')); |
|
for (const el of candidates) { |
|
const text = normalizeText(el.textContent); |
|
const aria = normalizeText(el.getAttribute('aria-label')); |
|
if (/圖版|boards?/.test(`${text} ${aria}`)) return el; |
|
} |
|
return null; |
|
} |
|
|
|
function ensurePanelStyles() { |
|
if (document.getElementById(PANEL_STYLE_ID)) return; |
|
const style = document.createElement('style'); |
|
style.id = PANEL_STYLE_ID; |
|
style.textContent = ` |
|
#${PANEL_ID}{position:fixed;top:16px;right:16px;z-index:2147483646;width:min(420px,calc(100vw - 32px));max-height:calc(100vh - 32px);background:#fff;border:1px solid #ddd;border-radius:14px;box-shadow:0 10px 35px rgba(0,0,0,.2);display:flex;flex-direction:column;overflow:hidden} |
|
#${PANEL_ID}[data-hidden="1"]{display:none} |
|
#${PANEL_ID} [data-role="head"]{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid #eee} |
|
#${PANEL_ID} [data-role="title"]{font:700 14px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#111} |
|
#${PANEL_ID} [data-role="close"],#${PANEL_ID} [data-role="refresh"]{border:1px solid #d9d9d9;background:#fff;border-radius:8px;padding:6px 8px;font:600 12px/1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;cursor:pointer} |
|
#${PANEL_ID} [data-role="tools"]{display:flex;gap:6px} |
|
#${PANEL_ID} [data-role="body"]{overflow:auto;padding:10px 12px} |
|
#${PANEL_ID} [data-role="status"]{font:500 12px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#555;padding:6px 2px} |
|
#${PANEL_ID} [data-role="list"]{display:flex;flex-direction:column;gap:8px} |
|
#${PANEL_ID} [data-role="item"]{display:grid;grid-template-columns:72px 1fr;gap:10px;align-items:center;padding:8px;border:1px solid #e8e8e8;border-radius:12px;color:#111;text-decoration:none;background:#fff} |
|
#${PANEL_ID} [data-role="item"]:hover{background:#f8f8f8} |
|
#${PANEL_ID} [data-role="name"]{font:600 13px/1.3 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif} |
|
#${PANEL_ID} [data-role="meta"]{font:500 12px/1.35 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#666;margin-top:4px} |
|
#${PANEL_ID} [data-role="thumb"]{width:72px;height:72px;border-radius:10px;overflow:hidden;background:#f2f2f2} |
|
#${PANEL_ID} [data-role="thumb"] img{width:100%;height:100%;object-fit:cover;display:block} |
|
#${PANEL_ID} [data-role="thumb-grid"]{width:72px;height:72px;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:2px} |
|
#${PANEL_ID} [data-role="owner-row"]{display:flex;align-items:center;gap:6px;margin-top:4px} |
|
#${PANEL_ID} [data-role="avatar"]{width:18px;height:18px;border-radius:9px;background:#e8e8e8;color:#666;display:flex;align-items:center;justify-content:center;font:700 10px/1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;overflow:hidden} |
|
#${PANEL_ID} [data-role="avatar"] img{width:100%;height:100%;object-fit:cover;display:block} |
|
#${PANEL_ID} [data-role="owner"]{font:600 11px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#444} |
|
#${TOGGLE_ID}{position:fixed;right:0;top:50%;transform:translateY(-50%);z-index:2147483644;width:28px;height:94px;border:1px solid #d7d7d7;border-right:none;border-radius:10px 0 0 10px;background:#fff;box-shadow:0 4px 14px rgba(0,0,0,.12);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;padding:0} |
|
#${TOGGLE_ID}:hover{background:#f7f7f7} |
|
#${TOGGLE_ID} [data-role="chevron"]{width:12px;height:12px;display:block;color:#666;transform:rotate(180deg)} |
|
#${TOGGLE_ID}[aria-expanded="true"] [data-role="chevron"]{transform:rotate(0deg)} |
|
#${TOGGLE_ID} [data-role="label"]{font:600 9px/1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#333;writing-mode:vertical-rl;text-orientation:mixed;letter-spacing:.04em} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
function getOrCreatePanel() { |
|
let panel = document.getElementById(PANEL_ID); |
|
if (panel) return panel; |
|
|
|
ensurePanelStyles(); |
|
panel = document.createElement('aside'); |
|
panel.id = PANEL_ID; |
|
panel.setAttribute('data-hidden', '1'); |
|
panel.innerHTML = ` |
|
<div data-role="head"> |
|
<div data-role="title">${CUSTOM_LABEL}</div> |
|
<div data-role="tools"> |
|
<button type="button" data-role="refresh">Refresh</button> |
|
<button type="button" data-role="close">Close</button> |
|
</div> |
|
</div> |
|
<div data-role="body"> |
|
<div data-role="status">Loading…</div> |
|
<div data-role="list"></div> |
|
</div> |
|
`; |
|
document.body.appendChild(panel); |
|
panel.querySelector('[data-role="close"]')?.addEventListener('click', () => { |
|
panelRequestSeq += 1; // cancel any in-flight load so it won't reopen panel |
|
panel?.setAttribute('data-hidden', '1'); |
|
setToggleExpanded(false); |
|
}); |
|
panel.querySelector('[data-role="refresh"]')?.addEventListener('click', () => { |
|
void loadBoardsIntoPanel({ forceRefresh: true }); |
|
}); |
|
return panel; |
|
} |
|
|
|
function setToggleExpanded(expanded) { |
|
const button = document.getElementById(TOGGLE_ID); |
|
if (!(button instanceof HTMLButtonElement)) return; |
|
button.setAttribute('aria-expanded', expanded ? 'true' : 'false'); |
|
} |
|
|
|
function setPanelStatus(message) { |
|
const panel = getOrCreatePanel(); |
|
const status = panel.querySelector('[data-role="status"]'); |
|
if (status) status.textContent = message; |
|
} |
|
|
|
function renderBoardsInPanel(boards) { |
|
const panel = getOrCreatePanel(); |
|
panel.setAttribute('data-hidden', '0'); |
|
const list = panel.querySelector('[data-role="list"]'); |
|
if (!(list instanceof HTMLDivElement)) return; |
|
list.innerHTML = ''; |
|
for (const board of boards) { |
|
const item = document.createElement('a'); |
|
item.setAttribute('data-role', 'item'); |
|
item.href = board.href; |
|
item.target = '_blank'; |
|
item.rel = 'noopener noreferrer'; |
|
item.innerHTML = `<div data-role="thumb"></div><div><div data-role="name"></div><div data-role="meta"></div><div data-role="owner-row"><div data-role="avatar"></div><div data-role="owner"></div></div></div>`; |
|
const thumb = item.querySelector('[data-role="thumb"]'); |
|
const name = item.querySelector('[data-role="name"]'); |
|
const meta = item.querySelector('[data-role="meta"]'); |
|
const owner = item.querySelector('[data-role="owner"]'); |
|
const avatar = item.querySelector('[data-role="avatar"]'); |
|
|
|
const previewImages = Array.isArray(board.images) ? board.images.filter(Boolean).slice(0, 4) : []; |
|
if (thumb && previewImages.length > 1) { |
|
const grid = document.createElement('div'); |
|
grid.setAttribute('data-role', 'thumb-grid'); |
|
for (const src of previewImages) { |
|
const img = document.createElement('img'); |
|
img.src = src; |
|
img.loading = 'lazy'; |
|
img.alt = ''; |
|
grid.appendChild(img); |
|
} |
|
thumb.appendChild(grid); |
|
} else if (thumb && (board.image || previewImages[0])) { |
|
const img = document.createElement('img'); |
|
img.src = board.image || previewImages[0]; |
|
img.loading = 'lazy'; |
|
img.alt = ''; |
|
thumb.appendChild(img); |
|
} |
|
if (name) name.textContent = board.name || board.href; |
|
if (meta) meta.textContent = board.meta || board.href; |
|
const ownerName = board.ownerName || board.ownerUsername || ''; |
|
if (owner) owner.textContent = ownerName ? `@${ownerName}` : ''; |
|
if (avatar) { |
|
if (board.ownerAvatar) { |
|
const img = document.createElement('img'); |
|
img.src = board.ownerAvatar; |
|
img.alt = ''; |
|
avatar.appendChild(img); |
|
} else { |
|
const initial = (ownerName || board.name || '?').trim().charAt(0).toUpperCase() || '?'; |
|
avatar.textContent = initial; |
|
} |
|
} |
|
list.appendChild(item); |
|
} |
|
setPanelStatus(boards.length ? `${boards.length} boards` : 'No boards found'); |
|
} |
|
|
|
function extractBoardsFromDialog(dialog) { |
|
function imageUrlFrom(img) { |
|
if (!img) return ''; |
|
const direct = img.getAttribute('src'); |
|
if (direct) return direct; |
|
const srcset = img.getAttribute('srcset') || ''; |
|
const first = srcset.split(',')[0]?.trim()?.split(' ')[0] || ''; |
|
return first; |
|
} |
|
|
|
const map = new Map(); |
|
const links = Array.from(dialog.querySelectorAll('a[href]')); |
|
for (const link of links) { |
|
const href = link.getAttribute('href') || ''; |
|
if (!/^\/[^/]+\/[^/]+\/?$/.test(href)) continue; |
|
const raw = compactText(link.textContent); |
|
if (!raw) continue; |
|
const parts = raw.split(',').map((x) => x.trim()).filter(Boolean); |
|
const name = parts[0] || href; |
|
const meta = parts.slice(1).join(' · '); |
|
const ownerUsername = href.replace(/^\/|\/$/g, '').split('/')[0] || ''; |
|
const ownerName = ownerUsername; |
|
const img = imageUrlFrom(link.querySelector('img')) || |
|
imageUrlFrom(link.parentElement?.querySelector('img')) || |
|
imageUrlFrom(link.closest('div')?.querySelector('img')) || |
|
''; |
|
const imageCandidates = Array.from( |
|
(link.closest('li,article,div') || link.parentElement || dialog).querySelectorAll('img') |
|
) |
|
.map(imageUrlFrom) |
|
.filter(Boolean); |
|
const uniqueImages = [...new Set(imageCandidates)].slice(0, 4); |
|
if (!map.has(href)) map.set(href, { |
|
href, |
|
name, |
|
meta, |
|
ownerUsername, |
|
ownerName, |
|
image: img || uniqueImages[0] || '', |
|
images: uniqueImages.length ? uniqueImages : (img ? [img] : []), |
|
}); |
|
} |
|
return Array.from(map.values()); |
|
} |
|
|
|
function closeDialog(dialog) { |
|
if (!dialog) return; |
|
const closeButton = Array.from(dialog.querySelectorAll('button,[role="button"]')).find((el) => { |
|
const aria = normalizeText(el.getAttribute('aria-label')); |
|
const text = normalizeText(el.textContent); |
|
return /close|關閉|关闭|닫기|fermer|cerrar|chiudi/.test(`${aria} ${text}`); |
|
}); |
|
if (closeButton) closeButton.click(); |
|
else { |
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); |
|
} |
|
} |
|
|
|
async function fetchBoardsFromModal() { |
|
const followingBtn = await waitFor(findFollowingButton, 4000); |
|
if (!followingBtn) return []; |
|
|
|
followingBtn.click(); |
|
const dialog = await waitFor( |
|
() => document.querySelector('[role="dialog"][aria-label], [role="dialog"], div[aria-modal="true"]'), |
|
4000 |
|
); |
|
if (!dialog) return []; |
|
|
|
const boardsTab = await waitFor(() => findBoardsTabInDialog(dialog), 3000); |
|
if (!boardsTab) return []; |
|
|
|
if (boardsTab.getAttribute('aria-selected') !== 'true') { |
|
boardsTab.click(); |
|
await wait(300); |
|
} |
|
const boards = await waitFor(() => { |
|
const list = extractBoardsFromDialog(dialog); |
|
return list.length > 0 ? list : null; |
|
}, 4000) || []; |
|
closeDialog(dialog); |
|
return boards; |
|
} |
|
|
|
async function enrichBoardsWithAvatarsProgressive(boards, options = {}) { |
|
const onUpdate = options.onUpdate || (() => {}); |
|
const isCancelled = options.isCancelled || (() => false); |
|
const concurrency = Math.max(1, Math.min(6, options.concurrency || 4)); |
|
|
|
const avatarCache = readAvatarCache(); |
|
const owners = [...new Set(boards.map((b) => b.ownerUsername).filter(Boolean))]; |
|
const toFetch = owners.filter((owner) => !avatarCache[owner]); |
|
if (toFetch.length === 0) return applyAvatarCacheToBoards(boards); |
|
|
|
let index = 0; |
|
const workers = Array.from({ length: Math.min(concurrency, toFetch.length) }, async () => { |
|
while (index < toFetch.length && !isCancelled()) { |
|
const owner = toFetch[index++]; |
|
try { |
|
const resp = await fetch(`/${owner}/`); |
|
const html = await resp.text(); |
|
const metaMatch = html.match(/property=\"og:image\" content=\"([^\"]+)\"/i); |
|
const fallbackMatch = html.match(/\"image_(?:small|medium|large)_url\":\"(https:[^\"]+)\"/); |
|
const candidate = metaMatch?.[1] || fallbackMatch?.[1] || ''; |
|
const avatar = /default_(30|60|75|120|280)\.png/.test(candidate) ? '' : candidate; |
|
if (avatar) { |
|
avatarCache[owner] = avatar; |
|
if (!isCancelled()) onUpdate(applyAvatarCacheToBoards(boards), false); |
|
} |
|
} catch { |
|
// Best effort. |
|
} |
|
} |
|
}); |
|
|
|
await Promise.all(workers); |
|
writeAvatarCache(avatarCache); |
|
const finalBoards = applyAvatarCacheToBoards(boards); |
|
if (!isCancelled()) onUpdate(finalBoards, true); |
|
return finalBoards; |
|
} |
|
|
|
async function loadBoardsIntoPanel(options = {}) { |
|
const requestSeq = ++panelRequestSeq; |
|
const panel = getOrCreatePanel(); |
|
panel.setAttribute('data-hidden', '0'); |
|
setToggleExpanded(true); |
|
const isCancelled = () => requestSeq !== panelRequestSeq; |
|
|
|
const now = Date.now(); |
|
const cache = readBoardCache(); |
|
const hasFreshCache = !!(cache && now - cache.ts < CACHE_TTL_MS); |
|
if (cache && cache.boards?.length) { |
|
if (isCancelled()) return; |
|
renderBoardsInPanel(cache.boards); |
|
if (!options.forceRefresh && hasFreshCache) return; |
|
setPanelStatus(`${cache.boards.length} boards · refreshing…`); |
|
} else { |
|
setPanelStatus('Loading followed boards…'); |
|
} |
|
|
|
const boards = await fetchBoardsFromModal(); |
|
if (isCancelled()) return; |
|
const baseBoards = applyAvatarCacheToBoards(boards); |
|
writeBoardCache(baseBoards); |
|
renderBoardsInPanel(baseBoards); |
|
if (baseBoards.length === 0) return; |
|
|
|
setPanelStatus(`${baseBoards.length} boards · updating avatars…`); |
|
void enrichBoardsWithAvatarsProgressive(baseBoards, { |
|
concurrency: 4, |
|
isCancelled, |
|
onUpdate(nextBoards, done) { |
|
if (isCancelled()) return; |
|
renderBoardsInPanel(nextBoards); |
|
writeBoardCache(nextBoards); |
|
setPanelStatus(done ? `${nextBoards.length} boards` : `${nextBoards.length} boards · updating avatars…`); |
|
}, |
|
}); |
|
} |
|
|
|
async function handleFollowingBoardsClick(event, clickedAnchor = null) { |
|
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); |
|
|
|
const username = getUsername() || usernameFromHref(clickedAnchor?.getAttribute('href') || ''); |
|
if (!username) return; |
|
const profileHref = `/${username}/`; |
|
const onProfile = location.pathname === profileHref || location.pathname === `/${username}/_profile/`; |
|
|
|
if (!onProfile) { |
|
sessionStorage.setItem(PENDING_PANEL_KEY, username); |
|
location.assign(profileHref); |
|
return; |
|
} |
|
|
|
const panel = document.getElementById(PANEL_ID); |
|
if (panel && panel.getAttribute('data-hidden') !== '1') { |
|
// Keep behavior single-purpose: tab click always opens/shows panel. |
|
panel.setAttribute('data-hidden', '0'); |
|
setToggleExpanded(true); |
|
return; |
|
} |
|
|
|
await loadBoardsIntoPanel({ forceRefresh: false }); |
|
} |
|
|
|
function ensureSidebarToggleButton() { |
|
ensurePanelStyles(); |
|
let button = document.getElementById(TOGGLE_ID); |
|
if (!(button instanceof HTMLButtonElement)) { |
|
button = document.createElement('button'); |
|
button.type = 'button'; |
|
button.id = TOGGLE_ID; |
|
button.setAttribute('aria-label', CUSTOM_LABEL); |
|
button.setAttribute('aria-expanded', 'false'); |
|
button.innerHTML = ` |
|
<svg data-role="chevron" viewBox="0 0 24 24" aria-hidden="true"> |
|
<path fill="currentColor" d="M8.7 5.3a1 1 0 0 1 1.4 0l5.6 5.6a1.5 1.5 0 0 1 0 2.2l-5.6 5.6a1 1 0 1 1-1.4-1.4l5-5-5-5a1 1 0 0 1 0-1.4z"></path> |
|
</svg> |
|
<span data-role="label">Boards</span> |
|
`; |
|
button.addEventListener('click', (event) => { |
|
void handleFollowingBoardsClick(event); |
|
}); |
|
document.body.appendChild(button); |
|
} |
|
} |
|
|
|
async function maybeResumePendingModalFlow() { |
|
const pendingUsername = sessionStorage.getItem(PENDING_PANEL_KEY); |
|
if (!pendingUsername) return; |
|
const currentUsername = getUsername(); |
|
const onProfile = currentUsername && ( |
|
location.pathname === `/${currentUsername}/` || |
|
location.pathname === `/${currentUsername}/_profile/` |
|
); |
|
if (!onProfile) return; |
|
|
|
sessionStorage.removeItem(PENDING_PANEL_KEY); |
|
await loadBoardsIntoPanel({ forceRefresh: true }); |
|
} |
|
|
|
function ensureFollowingBoardsTab() { |
|
ensureSidebarToggleButton(); |
|
} |
|
|
|
let rafQueued = false; |
|
function scheduleEnsureTab() { |
|
if (rafQueued) return; |
|
rafQueued = true; |
|
requestAnimationFrame(() => { |
|
rafQueued = false; |
|
ensureFollowingBoardsTab(); |
|
}); |
|
} |
|
|
|
const observer = new MutationObserver(scheduleEnsureTab); |
|
|
|
function setup() { |
|
observer.observe(document.documentElement, { |
|
childList: true, |
|
subtree: true, |
|
}); |
|
|
|
const origPushState = history.pushState; |
|
const origReplaceState = history.replaceState; |
|
|
|
history.pushState = function (...args) { |
|
const result = origPushState.apply(this, args); |
|
scheduleEnsureTab(); |
|
return result; |
|
}; |
|
|
|
history.replaceState = function (...args) { |
|
const result = origReplaceState.apply(this, args); |
|
scheduleEnsureTab(); |
|
return result; |
|
}; |
|
|
|
window.addEventListener('popstate', scheduleEnsureTab, true); |
|
window.addEventListener('hashchange', scheduleEnsureTab, true); |
|
window.addEventListener('scroll', scheduleEnsureTab, true); |
|
window.addEventListener('resize', scheduleEnsureTab, true); |
|
document.addEventListener('readystatechange', scheduleEnsureTab, true); |
|
scheduleEnsureTab(); |
|
maybeResumePendingModalFlow(); |
|
} |
|
|
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', setup, { once: true }); |
|
} else { |
|
setup(); |
|
} |
|
})(); |