Load your actual Reddit Saved posts directly from Reddit's Saved listing, then review, copy or export them as TXT/CSV. Does not scrape page DOM or collect ordinary Reddit activity.
// ==UserScript==
// @name Reddit Saved - Session Links Collector + Export
// @namespace https://greasyfork.org/userscripts/thochaos
// @version 2026.10.04.4
// @description Load your actual Reddit Saved posts directly from Reddit's Saved listing, then review, copy or export them as TXT/CSV. Does not scrape page DOM or collect ordinary Reddit activity.
// @author thochaos
// @license MIT
// @match https://reddit.com/*
// @match https://*.reddit.com/*
// @noframes
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
'use strict';
const VERSION =
'2026.10.04.4';
/*
* New key deliberately prevents contaminated DOM-collected
* data from v2026.10.04.1-.3 being restored.
*/
const STORAGE_KEY =
'redditSavedLinksCollector_session_v3';
const ROUTE_POLL_MS =
500;
const PAGE_LIMIT =
100;
let linksOnlyActive =
false;
let collectorActive =
false;
let loading =
false;
let currentAbortController =
null;
let lastKnownUrl =
location.href;
let routeSyncTimer =
null;
let toastTimer =
null;
const collectedPosts =
new Map();
// ============================================================
// Helpers
// ============================================================
function normalizeWhitespace(value) {
return String(
value || ''
)
.replace(/\s+/g, ' ')
.trim();
}
function escapeHtml(value) {
return String(value)
.replaceAll(
'&',
'&'
)
.replaceAll(
'<',
'<'
)
.replaceAll(
'>',
'>'
)
.replaceAll(
'"',
'"'
)
.replaceAll(
"'",
'''
);
}
function escapeCSV(value) {
const str =
String(
value ?? ''
);
if (
str.includes(',') ||
str.includes('"') ||
str.includes('\n') ||
str.includes('\r')
) {
return (
'"' +
str.replaceAll(
'"',
'""'
) +
'"'
);
}
return str;
}
function getDateString() {
return new Date()
.toISOString()
.slice(
0,
10
);
}
function sleep(ms) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
// ============================================================
// Saved route
// ============================================================
function getSavedRoute() {
const path =
location.pathname
.replace(
/\/+$/,
''
);
const match =
path.match(
/^\/user\/([^/]+)\/saved$/i
);
if (!match) {
return null;
}
let username;
try {
username =
decodeURIComponent(
match[1]
);
} catch {
username =
match[1];
}
return {
username,
path
};
}
function isSavedPage() {
return Boolean(
getSavedRoute()
);
}
// ============================================================
// Reddit post URLs
// ============================================================
function normalizePermalink(
permalink
) {
if (!permalink) {
return '';
}
try {
const parsed =
new URL(
permalink,
'https://www.reddit.com'
);
if (
!/\/comments\/[a-z0-9]+/i.test(
parsed.pathname
)
) {
return '';
}
parsed.protocol =
'https:';
parsed.hostname =
'www.reddit.com';
parsed.search =
'';
parsed.hash =
'';
let path =
parsed.pathname;
if (
!path.endsWith('/')
) {
path += '/';
}
return (
'https://www.reddit.com' +
path
);
} catch {
return '';
}
}
// ============================================================
// Session persistence
// ============================================================
function loadStoredPosts() {
try {
const raw =
JSON.parse(
sessionStorage.getItem(
STORAGE_KEY
) ||
'[]'
);
if (
!Array.isArray(
raw
)
) {
return;
}
collectedPosts.clear();
for (
const post
of raw
) {
if (
!post?.url
) {
continue;
}
collectedPosts.set(
post.url,
post
);
}
} catch (error) {
console.warn(
'[Reddit Saved Collector] Could not restore session.',
error
);
}
}
function saveStoredPosts() {
try {
sessionStorage.setItem(
STORAGE_KEY,
JSON.stringify(
[
...collectedPosts.values()
]
)
);
} catch (error) {
console.warn(
'[Reddit Saved Collector] Could not save session.',
error
);
}
}
// ============================================================
// Reddit Saved JSON API
// ============================================================
function buildSavedListingUrl(
username,
after,
count
) {
const encodedUser =
encodeURIComponent(
username
);
const url =
new URL(
`/user/${encodedUser}/saved.json`,
location.origin
);
url.searchParams.set(
'limit',
String(
PAGE_LIMIT
)
);
/*
* Critical:
*
* Only saved Link objects (posts).
* Saved comments are deliberately excluded.
*/
url.searchParams.set(
'type',
'links'
);
url.searchParams.set(
'raw_json',
'1'
);
url.searchParams.set(
'count',
String(
count
)
);
if (after) {
url.searchParams.set(
'after',
after
);
}
return url.href;
}
function listingChildToPost(
child,
existingPosts
) {
/*
* Reddit fullname kinds:
*
* t3 = Link/post
* t1 = Comment
*
* type=links should already limit the response to t3,
* but enforce it again here.
*/
if (
child?.kind !==
't3'
) {
return null;
}
const data =
child.data;
if (!data) {
return null;
}
const url =
normalizePermalink(
data.permalink
);
if (!url) {
return null;
}
const previous =
existingPosts.get(
url
);
return {
id:
data.name ||
(
data.id
? `t3_${data.id}`
: ''
),
url,
title:
normalizeWhitespace(
data.title
) ||
url,
subreddit:
normalizeWhitespace(
data.subreddit
),
author:
normalizeWhitespace(
data.author
),
sourceTitle:
'Reddit Saved',
sourceUrl:
location.href,
firstSeen:
previous?.firstSeen ||
new Date()
.toISOString()
};
}
async function requestSavedPage(
username,
after,
count,
signal
) {
const url =
buildSavedListingUrl(
username,
after,
count
);
console.debug(
'[Reddit Saved Collector] Requesting Saved listing:',
{
username,
after,
count,
url
}
);
const response =
await fetch(
url,
{
method:
'GET',
credentials:
'include',
headers: {
Accept:
'application/json'
},
signal
}
);
if (
response.status ===
401 ||
response.status ===
403
) {
throw new Error(
'Reddit rejected access to the Saved listing. Make sure you are logged in.'
);
}
if (!response.ok) {
throw new Error(
`Reddit Saved listing returned HTTP ${response.status}.`
);
}
const contentType =
response.headers
.get(
'content-type'
) ||
'';
if (
!contentType
.toLowerCase()
.includes(
'json'
)
) {
throw new Error(
'Reddit returned a non-JSON response for the Saved listing.'
);
}
let payload;
try {
payload =
await response.json();
} catch {
throw new Error(
'Reddit returned invalid JSON for the Saved listing.'
);
}
const listing =
payload?.data;
if (
!listing ||
!Array.isArray(
listing.children
)
) {
throw new Error(
'Reddit returned an unexpected Saved listing format.'
);
}
return listing;
}
async function loadSavedPosts() {
const route =
getSavedRoute();
if (!route) {
return;
}
if (loading) {
return;
}
loading =
true;
updateButtons();
if (
currentAbortController
) {
currentAbortController.abort();
}
const controller =
new AbortController();
currentAbortController =
controller;
const existingPosts =
new Map(
collectedPosts
);
/*
* Build the refreshed result separately.
*
* We only replace the current collection after Reddit has
* completed pagination successfully. A failed request therefore
* cannot destroy the last good collection.
*/
const refreshed =
new Map();
const seenCursors =
new Set();
let after =
null;
let count =
0;
let page =
0;
try {
setStatus(
'Loading saved posts from Reddit...'
);
while (true) {
/*
* If we navigated away while a request was in flight,
* stop immediately.
*/
if (!isSavedPage()) {
throw new DOMException(
'Navigation changed',
'AbortError'
);
}
page++;
if (
page >
100
) {
throw new Error(
'Pagination safety limit reached.'
);
}
setStatus(
`Loading Saved page ${page} — ${refreshed.size} posts found...`
);
const listing =
await requestSavedPage(
route.username,
after,
count,
controller.signal
);
const children =
listing.children;
let postsThisPage =
0;
for (
const child
of children
) {
const post =
listingChildToPost(
child,
existingPosts
);
if (!post) {
continue;
}
refreshed.set(
post.url,
post
);
postsThisPage++;
}
count +=
children.length;
console.debug(
'[Reddit Saved Collector] Saved listing page complete:',
{
page,
listingItems:
children.length,
postsAccepted:
postsThisPage,
totalSavedPosts:
refreshed.size,
nextAfter:
listing.after
}
);
/*
* Do NOT use:
*
* children.length < PAGE_LIMIT
*
* as the stopping rule.
*
* Reddit listings can return short intermediate pages.
* The authoritative end condition is after === null.
*/
const nextAfter =
listing.after ||
null;
if (!nextAfter) {
break;
}
if (
seenCursors.has(
nextAfter
)
) {
throw new Error(
'Reddit repeated a pagination cursor.'
);
}
seenCursors.add(
nextAfter
);
after =
nextAfter;
/*
* Keep requests sequential and avoid hammering Reddit.
*/
await sleep(
100
);
}
/*
* Ensure the route did not change between the last request
* finishing and committing the result.
*/
if (!isSavedPage()) {
return;
}
collectedPosts.clear();
for (
const [
url,
post
]
of refreshed
) {
collectedPosts.set(
url,
post
);
}
saveStoredPosts();
updateCounter();
setStatus(
`Loaded ${collectedPosts.size} saved post${collectedPosts.size === 1 ? '' : 's'} from Reddit.`
);
console.log(
`[Reddit Saved Collector] Loaded ${collectedPosts.size} saved posts across ${page} listing page${page === 1 ? '' : 's'}.`
);
} catch (error) {
if (
error?.name ===
'AbortError'
) {
console.debug(
'[Reddit Saved Collector] Saved listing request cancelled.'
);
return;
}
console.error(
'[Reddit Saved Collector]',
error
);
setStatus(
'Could not load Saved posts: ' +
error.message
);
} finally {
if (
currentAbortController ===
controller
) {
currentAbortController =
null;
}
loading =
false;
updateButtons();
}
}
// ============================================================
// Export formatting
// ============================================================
function formatTitlesAndUrls(
posts
) {
return posts
.map(
post =>
`${post.title}\n${post.url}`
)
.join(
'\n\n'
);
}
function formatUrlsOnly(
posts
) {
return posts
.map(
post =>
post.url
)
.join(
'\n'
);
}
function formatTXT(
posts
) {
return posts
.map(
post => {
const lines = [
post.title,
post.url
];
if (
post.subreddit
) {
lines.push(
`Subreddit: r/${post.subreddit}`
);
}
if (
post.author
) {
lines.push(
`Author: u/${post.author}`
);
}
if (
post.firstSeen
) {
lines.push(
`Collected: ${post.firstSeen}`
);
}
return lines.join(
'\n'
);
}
)
.join(
'\n\n'
);
}
function formatCSV(
posts
) {
const rows = [
[
'Title',
'URL',
'Subreddit',
'Author',
'Collected At'
]
];
for (
const post
of posts
) {
rows.push(
[
post.title ||
'',
post.url ||
'',
post.subreddit ||
'',
post.author ||
'',
post.firstSeen ||
''
]
);
}
return rows
.map(
row =>
row
.map(
escapeCSV
)
.join(',')
)
.join(
'\r\n'
);
}
// ============================================================
// Clipboard
// ============================================================
async function copyText(
text,
successMessage
) {
try {
await navigator
.clipboard
.writeText(
text
);
showToast(
successMessage
);
} catch (error) {
console.error(
'[Reddit Saved Collector] Clipboard failed.',
error
);
showToast(
'Clipboard copy failed.'
);
}
}
// ============================================================
// Downloads
// ============================================================
function downloadBlob(
contents,
filename,
type
) {
const blob =
new Blob(
[
contents
],
{
type
}
);
const url =
URL.createObjectURL(
blob
);
const anchor =
document.createElement(
'a'
);
anchor.href =
url;
anchor.download =
filename;
document.body
?.appendChild(
anchor
);
anchor.click();
anchor.remove();
setTimeout(
() =>
URL.revokeObjectURL(
url
),
1000
);
}
function downloadCSV(
posts
) {
const csv =
'\uFEFF' +
formatCSV(
posts
);
downloadBlob(
csv,
`reddit-saved-posts-${getDateString()}.csv`,
'text/csv;charset=utf-8'
);
showToast(
`CSV exported: ${posts.length} posts`
);
}
function downloadTXT(
posts
) {
downloadBlob(
formatTXT(
posts
),
`reddit-saved-posts-${getDateString()}.txt`,
'text/plain;charset=utf-8'
);
showToast(
`TXT exported: ${posts.length} posts`
);
}
// ============================================================
// Toast
// ============================================================
function createToast() {
if (
document.getElementById(
'reddit-saved-collector-toast'
)
) {
return true;
}
if (
!document.documentElement
) {
return false;
}
const toast =
document.createElement(
'div'
);
toast.id =
'reddit-saved-collector-toast';
toast.style = `
position:fixed;
left:50%;
bottom:20px;
transform:translateX(-50%);
z-index:2147483647;
padding:8px 14px;
background:#111;
color:#fff;
border:1px solid #00d8ff;
border-radius:6px;
font-family:system-ui,sans-serif;
font-size:12px;
pointer-events:none;
opacity:0;
transition:opacity .2s;
`;
document.documentElement
.appendChild(
toast
);
return true;
}
function showToast(
message
) {
if (!createToast()) {
return;
}
const toast =
document.getElementById(
'reddit-saved-collector-toast'
);
if (!toast) {
return;
}
toast.textContent =
message;
toast.style.opacity =
'1';
clearTimeout(
toastTimer
);
toastTimer =
setTimeout(
() => {
toast.style.opacity =
'0';
},
1800
);
}
// ============================================================
// Floating panel
// ============================================================
function createPanel() {
if (
!collectorActive ||
!isSavedPage()
) {
return;
}
const existing =
document.getElementById(
'reddit-saved-collector-counter'
);
if (existing) {
updateCounter();
updateButtons();
return;
}
if (
!document.documentElement
) {
return;
}
const box =
document.createElement(
'div'
);
box.id =
'reddit-saved-collector-counter';
box.style = `
position:fixed;
right:18px;
bottom:18px;
z-index:2147483645;
width:210px;
padding:10px 12px;
background:#111;
color:#fff;
border:1px solid #00d8ff;
border-radius:7px;
box-shadow:0 4px 18px rgba(0,0,0,.5);
font-family:system-ui,sans-serif;
font-size:12px;
text-align:center;
`;
box.innerHTML = `
<div
style="
color:#00d8ff;
font-size:10px;
margin-bottom:4px;
"
>
REDDIT SAVED POSTS
</div>
<div
id="reddit-saved-collector-count"
style="
font-weight:700;
margin-bottom:5px;
"
>
0 saved posts
</div>
<div
id="reddit-saved-collector-status"
style="
margin-bottom:7px;
color:#aaa;
font-size:10px;
line-height:1.3;
"
>
Ready.
</div>
<button
id="reddit-saved-refresh"
type="button"
style="
width:100%;
padding:6px;
background:#174f67;
color:#fff;
border:1px solid #277a9c;
border-radius:5px;
cursor:pointer;
font-weight:600;
font-size:11px;
"
>
REFRESH SAVED POSTS
</button>
<button
id="reddit-saved-links-only"
type="button"
style="
width:100%;
margin-top:5px;
padding:6px;
background:#222;
color:#fff;
border:1px solid #444;
border-radius:5px;
cursor:pointer;
font-weight:600;
font-size:11px;
"
>
LINKS ONLY
</button>
<button
id="reddit-saved-reset"
type="button"
style="
width:100%;
margin-top:5px;
padding:4px;
background:#311;
color:#bbb;
border:1px solid #522;
border-radius:5px;
cursor:pointer;
font-size:10px;
"
>
RESET SESSION LIST
</button>
<div
style="
margin-top:6px;
color:#666;
font-size:9px;
"
>
v${VERSION} · Alt+R
</div>
`;
document.documentElement
.appendChild(
box
);
box.querySelector(
'#reddit-saved-refresh'
)?.addEventListener(
'click',
loadSavedPosts
);
box.querySelector(
'#reddit-saved-links-only'
)?.addEventListener(
'click',
toggleLinksOnly
);
box.querySelector(
'#reddit-saved-reset'
)?.addEventListener(
'click',
resetCollection
);
updateCounter();
updateButtons();
}
function removePanel() {
document
.getElementById(
'reddit-saved-collector-counter'
)
?.remove();
}
function updateCounter() {
const element =
document.getElementById(
'reddit-saved-collector-count'
);
if (!element) {
return;
}
element.textContent =
`${collectedPosts.size} saved post${collectedPosts.size === 1 ? '' : 's'}`;
}
function setStatus(
message
) {
const element =
document.getElementById(
'reddit-saved-collector-status'
);
if (!element) {
return;
}
element.textContent =
message;
}
function updateButtons() {
const refresh =
document.getElementById(
'reddit-saved-refresh'
);
const linksOnly =
document.getElementById(
'reddit-saved-links-only'
);
const reset =
document.getElementById(
'reddit-saved-reset'
);
for (
const button
of [
refresh,
linksOnly,
reset
]
) {
if (!button) {
continue;
}
button.disabled =
loading;
button.style.opacity =
loading
? '0.55'
: '1';
button.style.cursor =
loading
? 'default'
: 'pointer';
}
if (refresh) {
refresh.textContent =
loading
? 'LOADING...'
: 'REFRESH SAVED POSTS';
}
}
// ============================================================
// Reset
// ============================================================
function resetCollection() {
if (!isSavedPage()) {
return;
}
if (
!confirm(
'Clear the locally collected Saved-post list for this browser session?\n\nThis does NOT unsave anything on Reddit.'
)
) {
return;
}
collectedPosts.clear();
sessionStorage.removeItem(
STORAGE_KEY
);
updateCounter();
setStatus(
'Session list cleared. Use Refresh Saved Posts to load it again.'
);
showToast(
'Local session list cleared.'
);
}
// ============================================================
// Links-only overlay
// ============================================================
function renderLinksOnly() {
if (
linksOnlyActive ||
!isSavedPage()
) {
return;
}
const posts =
[
...collectedPosts.values()
];
if (!posts.length) {
alert(
'No saved Reddit posts have been loaded yet.'
);
return;
}
const overlay =
document.createElement(
'div'
);
overlay.id =
'reddit-links-only-overlay';
overlay.style = `
position:fixed;
inset:0;
z-index:2147483646;
overflow:auto;
background:#0f0f0f;
color:#eee;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
`;
const rows =
posts
.map(
(
post,
index
) => {
const subreddit =
post.subreddit
? `
<span class="rlv-subreddit">
r/${escapeHtml(post.subreddit)}
</span>
`
: '';
const author =
post.author
? `
<span class="rlv-author">
u/${escapeHtml(post.author)}
</span>
`
: '';
return `
<div class="rlv-row">
<div class="rlv-number">
${index + 1}
</div>
<div class="rlv-content">
<a
class="rlv-title"
href="${escapeHtml(post.url)}"
target="_blank"
rel="noopener noreferrer"
>
${escapeHtml(post.title)}
</a>
<div class="rlv-meta">
${subreddit}
${author}
<a
class="rlv-url"
href="${escapeHtml(post.url)}"
target="_blank"
rel="noopener noreferrer"
>
${escapeHtml(post.url)}
</a>
</div>
</div>
</div>
`;
}
)
.join('');
overlay.innerHTML = `
<style>
#reddit-links-only-overlay * {
box-sizing:border-box;
}
#rlv-root {
max-width:1200px;
margin:0 auto;
padding:28px;
}
.rlv-header {
position:sticky;
top:0;
z-index:10;
display:flex;
justify-content:space-between;
align-items:center;
gap:20px;
margin:-28px -28px 20px -28px;
padding:16px 28px;
background:#111;
border-bottom:1px solid #333;
}
.rlv-heading {
color:#fff;
font-size:20px;
font-weight:700;
}
.rlv-count {
margin-top:3px;
color:#999;
font-size:12px;
}
.rlv-buttons {
display:flex;
gap:8px;
flex-wrap:wrap;
justify-content:flex-end;
}
.rlv-button {
padding:8px 12px;
border:1px solid #444;
border-radius:6px;
background:#222;
color:#fff;
cursor:pointer;
font-size:12px;
font-weight:600;
}
.rlv-button:hover {
background:#333;
}
.rlv-row {
display:grid;
grid-template-columns:42px 1fr;
gap:10px;
padding:13px 8px;
border-bottom:1px solid #292929;
}
.rlv-row:hover {
background:#161616;
}
.rlv-number {
padding-top:2px;
color:#666;
text-align:right;
font-size:12px;
}
.rlv-content {
min-width:0;
}
.rlv-title {
display:inline-block;
color:#e8e8e8 !important;
font-size:15px;
font-weight:600;
line-height:1.35;
text-decoration:none !important;
}
.rlv-title:hover {
color:#00d8ff !important;
text-decoration:underline !important;
}
.rlv-meta {
display:flex;
align-items:center;
gap:8px;
min-width:0;
margin-top:5px;
font-size:11px;
}
.rlv-subreddit,
.rlv-author {
flex:0 0 auto;
padding:2px 5px;
border-radius:3px;
background:#242424;
color:#aaa;
}
.rlv-url {
display:block;
min-width:0;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
color:#666 !important;
text-decoration:none !important;
}
.rlv-url:hover {
color:#aaa !important;
text-decoration:underline !important;
}
@media (max-width:800px) {
#rlv-root {
padding:16px;
}
.rlv-header {
align-items:flex-start;
flex-direction:column;
margin:-16px -16px 16px -16px;
padding:12px 16px;
}
.rlv-buttons {
justify-content:flex-start;
}
.rlv-row {
grid-template-columns:30px 1fr;
}
}
</style>
<div id="rlv-root">
<div class="rlv-header">
<div>
<div class="rlv-heading">
Reddit Saved — Saved Posts
</div>
<div class="rlv-count">
${posts.length}
saved post${posts.length === 1 ? '' : 's'}
returned by Reddit
</div>
</div>
<div class="rlv-buttons">
<button
id="rlv-copy-title-url"
class="rlv-button"
type="button"
>
COPY TITLES + URLS
</button>
<button
id="rlv-copy-urls"
class="rlv-button"
type="button"
>
COPY URLS
</button>
<button
id="rlv-download-csv"
class="rlv-button"
type="button"
>
DOWNLOAD CSV
</button>
<button
id="rlv-download-txt"
class="rlv-button"
type="button"
>
DOWNLOAD TXT
</button>
<button
id="rlv-close"
class="rlv-button"
type="button"
>
RETURN TO REDDIT
</button>
</div>
</div>
<div id="rlv-list">
${rows}
</div>
</div>
`;
document.documentElement
.appendChild(
overlay
);
linksOnlyActive =
true;
overlay
.querySelector(
'#rlv-close'
)
?.addEventListener(
'click',
closeLinksOnly
);
overlay
.querySelector(
'#rlv-copy-title-url'
)
?.addEventListener(
'click',
() =>
copyText(
formatTitlesAndUrls(
posts
),
`Copied ${posts.length} titles + URLs`
)
);
overlay
.querySelector(
'#rlv-copy-urls'
)
?.addEventListener(
'click',
() =>
copyText(
formatUrlsOnly(
posts
),
`Copied ${posts.length} URLs`
)
);
overlay
.querySelector(
'#rlv-download-csv'
)
?.addEventListener(
'click',
() =>
downloadCSV(
posts
)
);
overlay
.querySelector(
'#rlv-download-txt'
)
?.addEventListener(
'click',
() =>
downloadTXT(
posts
)
);
}
function closeLinksOnly() {
document
.getElementById(
'reddit-links-only-overlay'
)
?.remove();
linksOnlyActive =
false;
}
function toggleLinksOnly() {
if (
linksOnlyActive
) {
closeLinksOnly();
return;
}
if (
isSavedPage()
) {
renderLinksOnly();
}
}
// ============================================================
// Collector lifecycle
// ============================================================
function startCollector() {
if (!isSavedPage()) {
stopCollector();
return;
}
if (
collectorActive
) {
createPanel();
return;
}
collectorActive =
true;
createPanel();
updateCounter();
/*
* Automatically refresh when entering Saved.
*
* Existing session data may appear immediately while Reddit's
* current Saved listing is fetched.
*/
loadSavedPosts();
console.log(
'[Reddit Saved Collector] Saved API collector activated.'
);
}
function stopCollector() {
if (
currentAbortController
) {
currentAbortController.abort();
currentAbortController =
null;
}
loading =
false;
collectorActive =
false;
removePanel();
if (
linksOnlyActive
) {
closeLinksOnly();
}
}
function syncCollectorToRoute() {
lastKnownUrl =
location.href;
if (
isSavedPage()
) {
startCollector();
} else {
stopCollector();
}
}
function scheduleRouteSync(
delay = 0
) {
clearTimeout(
routeSyncTimer
);
routeSyncTimer =
setTimeout(
syncCollectorToRoute,
delay
);
}
// ============================================================
// Reddit SPA navigation
// ============================================================
function hookHistoryMethod(
methodName
) {
const original =
history[
methodName
];
if (
typeof original !==
'function'
) {
return;
}
history[
methodName
] =
function (...args) {
const result =
original.apply(
this,
args
);
scheduleRouteSync(
0
);
return result;
};
}
hookHistoryMethod(
'pushState'
);
hookHistoryMethod(
'replaceState'
);
window.addEventListener(
'popstate',
() =>
scheduleRouteSync(
0
)
);
window.addEventListener(
'hashchange',
() =>
scheduleRouteSync(
0
)
);
window.addEventListener(
'pageshow',
() =>
scheduleRouteSync(
0
)
);
document.addEventListener(
'visibilitychange',
() => {
if (
document.visibilityState ===
'visible'
) {
scheduleRouteSync(
0
);
}
}
);
/*
* Lightweight route fallback.
*
* No DOM content is scanned here.
*/
setInterval(
() => {
if (
location.href !==
lastKnownUrl
) {
console.debug(
'[Reddit Saved Collector] Route changed:',
lastKnownUrl,
'→',
location.href
);
syncCollectorToRoute();
}
},
ROUTE_POLL_MS
);
// ============================================================
// Alt + R
// ============================================================
document.addEventListener(
'keydown',
event => {
const target =
event.target;
const typing =
target instanceof
HTMLInputElement ||
target instanceof
HTMLTextAreaElement ||
target?.isContentEditable;
if (typing) {
return;
}
if (
event.altKey &&
!event.ctrlKey &&
!event.shiftKey &&
event.code ===
'KeyR'
) {
if (
!isSavedPage()
) {
return;
}
event.preventDefault();
toggleLinksOnly();
}
}
);
// ============================================================
// Start
// ============================================================
loadStoredPosts();
syncCollectorToRoute();
console.log(
`[Reddit Saved Collector] v${VERSION} API collector installed.`
);
})();