Hhfguhvmp
(Hgfyk)
2025 年7 月 27 日 09:01
1
虽然说有书签,但是帖子可能会被编辑修改,删除,甚至论坛都有可能消失,当然了,我最不希望看到这种情况,不过无论是天涯,还有各种各样的论坛,他们的帖子基本上我后面查看的时候全部都是用截图的方式保存了,所以我希望问问有没有更加好用的工具?
12 个赞
Cknight
(cknight)
2025 年7 月 27 日 09:02
2
剪藏工具?
1 个赞
Hhfguhvmp
(Hgfyk)
2025 年7 月 27 日 09:03
3
更类似于互联网档案馆那种吧,反正只要批量简便保存就行
1 个赞
lylinuxdo2
(lylinuxdo2)
2025 年7 月 27 日 09:06
4
可以使用佬开发的插件下载器
总结
// ==UserScript==
// @name Linux.do 下崽器 (新版)
// @namespace http://linux.do/
// @version 1.0.7
// @description 备份你珍贵的水贴为Markdown,可拖拽调整按钮位置。
// @author PastKing
// @match https://www.linux.do/t/topic/*
// @match https://linux.do/t/topic/*
// @license MIT
// @icon https://cdn.linux.do/uploads/default/optimized/1X/3a18b4b0da3e8cf96f7eea15241c3d251f28a39b_2_32x32.png
// @grant none
// @require https://unpkg.com/turndown@7.1.3/dist/turndown.js
// @downloadURL https://update.greasyfork.org/scripts/511622/Linuxdo%20%E4%B8%8B%E5%B4%BD%E5%99%A8%20%28%E6%96%B0%E7%89%88%29.user.js
// @updateURL https://update.greasyfork.org/scripts/511622/Linuxdo%20%E4%B8%8B%E5%B4%BD%E5%99%A8%20%28%E6%96%B0%E7%89%88%29.meta.js
// ==/UserScript==
(function() {
'use strict';
let isDragging = false;
let isMouseDown = false;
// 创建并插入下载按钮
function createDownloadButton() {
const button = document.createElement('button');
button.textContent = '下载为 Markdown';
button.style.cssText = `
padding: 10px 15px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
background-color: #0f9d58;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
position: fixed;
z-index: 9999;
`;
// 添加悬停效果
button.onmouseover = function() {
this.style.backgroundColor = '#0b8043';
};
button.onmouseout = function() {
this.style.backgroundColor = '#0f9d58';
};
// 从localStorage获取保存的位置
const savedPosition = JSON.parse(localStorage.getItem('downloadButtonPosition'));
if (savedPosition) {
button.style.left = savedPosition.left;
button.style.top = savedPosition.top;
} else {
button.style.right = '20px';
button.style.bottom = '20px';
}
document.body.appendChild(button);
return button;
}
// 添加拖拽功能
function makeDraggable(element) {
let startX, startY, startLeft, startTop;
element.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDragging);
function startDragging(e) {
isMouseDown = true;
isDragging = false;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(element.style.left) || window.innerWidth - parseInt(element.style.right) - element.offsetWidth;
startTop = parseInt(element.style.top) || window.innerHeight - parseInt(element.style.bottom) - element.offsetHeight;
e.preventDefault();
}
function drag(e) {
if (!isMouseDown) return;
isDragging = true;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${startLeft + dx}px`;
element.style.top = `${startTop + dy}px`;
element.style.right = 'auto';
element.style.bottom = 'auto';
}
function stopDragging() {
if (isMouseDown && isDragging) {
localStorage.setItem('downloadButtonPosition', JSON.stringify({
left: element.style.left,
top: element.style.top
}));
}
isMouseDown = false;
setTimeout(() => {
isDragging = false;
}, 10); // 短暂延迟以确保点击事件在拖动后正确触发
}
}
// 获取文章内容
function getArticleContent() {
const titleElement = document.querySelector('#topic-title > div > h1 > a.fancy-title > span');
const contentElement = document.querySelector('#post_1 > div.row > div.topic-body.clearfix > div.regular.contents > div.cooked');
if (!titleElement || !contentElement) {
console.error('无法找到文章标题或内容');
return null;
}
return {
title: titleElement.textContent.trim(),
content: contentElement.innerHTML
};
}
// 转换为Markdown并下载
function downloadAsMarkdown() {
const article = getArticleContent();
if (!article) {
alert('无法获取文章内容,请检查网页结构是否变更。');
return;
}
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced'
});
// 自定义规则处理图片和链接
turndownService.addRule('images_and_links', {
filter: ['a', 'img'],
replacement: function (content, node) {
// 处理图片
if (node.nodeName === 'IMG') {
const alt = node.alt || '';
const src = node.getAttribute('src') || '';
const title = node.title ? ` "${node.title}"` : '';
return ``;
}
// 处理链接
else if (node.nodeName === 'A') {
const href = node.getAttribute('href');
const title = node.title ? ` "${node.title}"` : '';
// 检查链接是否包含图片
const img = node.querySelector('img');
if (img) {
const alt = img.alt || '';
const src = img.getAttribute('src') || '';
const imgTitle = img.title ? ` "${img.title}"` : '';
return `[](${href}${title})`;
}
// 普通链接
return `[${node.textContent}](${href}${title})`;
}
}
});
const markdown = `# ${article.title}\n\n${turndownService.turndown(article.content)}`;
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${article.title}.md`;
a.click();
URL.revokeObjectURL(url);
}
// 主函数
function main() {
const downloadButton = createDownloadButton();
makeDraggable(downloadButton);
downloadButton.addEventListener('click', function(e) {
if (!isDragging) {
downloadAsMarkdown();
}
});
}
// 运行主函数
main();
})();
但是无法获得回复 主题消息没问题
miza
(暮光闪闪)
2025 年7 月 27 日 09:08
5
Firefox的pocket挺好的,但是关闭了
Pocket 曾陪伴数百万人保存心仪的文章,发现值得深入阅读的精彩内容。然而,随着用户网络使用习惯的不断演变,我们经过慎重考虑,决定将资源重新聚焦于那些更贴合当下浏览习惯与在线需求的项目。
同求一个好工具可以在线保存的
1 个赞
libook
(libook)
2025 年7 月 27 日 09:09
6
PC浏览器可以保存网页成html文件,可以试试。
另外可以打印网页成PDF文件。
Hhfguhvmp
(Hgfyk)
2025 年7 月 27 日 09:09
7
不太行呐,很多帖子最重要的反而是回复主题,只是个问题,不过谢谢分享
lwang
2025 年7 月 27 日 09:12
8
// ==UserScript==
// @name LINUX DO 帖子内容提取器
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 自动加载并提取 LINUX DO 帖子到 Markdown。简约集成UI,点击编辑楼层数,状态可视,设置保存,HTML转MD。
// @author Mozi
// @match https://linux.do/t/*
// @icon https://linux.do/uploads/default/original/1X/702b889b84630757e77697727619a9000a087780.png
// @grant none
// @run-at document-idle
// @require https://unpkg.com/turndown/dist/turndown.js
// ==/UserScript==
(function() {
'use strict';
// --- 配置 ---
const SCROLL_CHECK_INTERVAL_MS = 800;
const SCROLL_STABLE_TIME_MS = 3000;
const MAX_SCROLL_WAIT_SEC = 180;
const LS_KEY_FLOORS = 'ldExtractorMaxFloors'; // LocalStorage Key
// --- UI IDs ---
const BAR_ID = 'ld-extractor-bar';
const LIMIT_CONTROL_ID = 'ld-limit-control';
const LIMIT_DISPLAY_ID = 'ld-limit-display';
const LIMIT_INPUT_ID = 'ld-limit-input';
const ACTION_BTN_ID = 'ld-extractor-action-btn';
const ACTION_BTN_TEXT = '提取(MD)';
let isEditingLimit = false;
let isRunning = false;
console.log('LINUX DO 帖子提取脚本 (Markdown + Minimal UI) v2.0 已加载于:', window.location.href);
// --- CSS Style Injection ---
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
:root {
--ld-primary-color: #339AF0;
--ld-primary-hover: #1c7ed6;
--ld-bg: rgba(248, 249, 250, 0.95);
--ld-bg-hover: rgba(233, 236, 239, 0.98);
--ld-text-color: #343a40;
--ld-border-radius: 8px;
--ld-shadow: 0 4px 18px rgba(0, 30, 60, 0.18);
--ld-transition: all 0.25s ease;
--ld-ui-height: 36px;
--ld-font-size: 13px;
}
#${BAR_ID} {
position: fixed; bottom: 25px; right: 25px; z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: var(--ld-font-size); color: var(--ld-text-color); opacity: 0.88;
display: flex; align-items: stretch;
border-radius: var(--ld-border-radius); box-shadow: var(--ld-shadow);
overflow: hidden; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
border: 1px solid rgba(0,0,0,0.08);
height: var(--ld-ui-height);
}
#${BAR_ID}:hover { opacity: 1; transform: translateY(-2px); }
#${BAR_ID}.disabled { opacity: 0.7; transform: none; cursor: not-allowed; }
/* Limit Control */
#${LIMIT_CONTROL_ID} {
background: var(--ld-bg); padding: 0 12px;
display: flex; align-items: center; gap: 6px;
border-right: 1px solid rgba(0,0,0, 0.1);
cursor: pointer; transition: var(--ld-transition);
white-space: nowrap;
}
#${BAR_ID}:not(.disabled) #${LIMIT_CONTROL_ID}:hover { background: var(--ld-bg-hover); }
#${BAR_ID}.disabled #${LIMIT_CONTROL_ID} { cursor: not-allowed; background: #e9ecef; }
#${LIMIT_CONTROL_ID} label { margin:0; font-weight: 500; user-select: none;}
#${LIMIT_DISPLAY_ID} {
font-weight: 600; color: var(--ld-primary-color); min-width: 30px; text-align: center;
user-select: none;
}
#${BAR_ID}:not(.disabled) #${LIMIT_CONTROL_ID}:hover #${LIMIT_DISPLAY_ID} { text-decoration: underline dotted; }
#${LIMIT_INPUT_ID} {
width: 45px; padding: 3px 4px; border: 1px solid var(--ld-primary-color);
border-radius: 3px; text-align: center; outline: none;
font-size: var(--ld-font-size); font-weight: 600; color: var(--ld-primary-color);
background: white; box-sizing: border-box;
}
/* Action Button */
#${ACTION_BTN_ID} {
border: none; background-color: var(--ld-primary-color); color: white;
cursor: pointer; font-size: var(--ld-font-size); padding: 0 16px;
outline: none; transition: var(--ld-transition);
white-space: nowrap; font-weight: 500;
}
#${ACTION_BTN_ID}:hover:not(:disabled) { background-color: var(--ld-primary-hover); }
#${ACTION_BTN_ID}:active:not(:disabled) { filter: brightness(0.9); }
#${ACTION_BTN_ID}:disabled {
background-color: #adb5bd !important; cursor: not-allowed;
}
`;
document.head.appendChild(style);
}
// --- Turndown HTML to Markdown 转换器配置 ---
let turndownService;
function initTurndown() {
try {
turndownService = new TurndownService({
headingStyle: 'atx', hr: '---', bulletListMarker: '-',
codeBlockStyle: 'fenced', emDelimiter: '*', strongDelimiter: '**'
});
turndownService.addRule('emoji', { filter: 'img.emoji', replacement: (c, n) => n.alt || '' });
turndownService.addRule('oneboxAside', { filter: 'aside.onebox', replacement: (c, n) => { const l = n.querySelector('a[href]'); return l ? `\n[${l.innerText.trim() || l.href}](${l.href})\n` : '[Onebox]\n'; }});
turndownService.addRule('oneboxLink', { filter: 'a.onebox', replacement: (c, n) => `\n[${n.innerText.trim() || n.href}](${n.href})\n` });
turndownService.addRule('mention', { filter: n => n.matches('a.mention, a.mention-group'), replacement: (c, n) => n.textContent || '' });
turndownService.addRule('image', { filter: 'a.lightbox', replacement: (c, n) => { const i = n.querySelector('img'); const s = n.href || i?.src; return s ? `\n\n` : ''; }});
console.log("TurndownService Init OK.");
return true;
} catch(e) {
console.error("TurndownService Init Fail!", e);
alert("Markdown 转换库加载失败!");
return false;
}
}
// --- 获取帖子元素 ---
function getPosts() {
const posts = Array.from(document.querySelectorAll('.post-stream article[data-post-id], article[data-post-id]'));
return posts.filter(post => !post.classList.contains('topic-status-info') && post.querySelector('.cooked, .post'));
}
// --- 更新限制状态、显示和存储 ---
function updateLimitState(value, display, input, button) {
const limit = parseInt(value, 10) || 0;
localStorage.setItem(LS_KEY_FLOORS, limit);
if(input) input.value = limit;
if(display) display.textContent = limit === 0 ? '全部' : limit;
if(button) button.title = `开始提取帖子内容 (范围: ${limit === 0 ? '全部楼层' : '前 ' + limit + ' 楼'})`;
console.log(`楼层限制更新: ${limit}`);
}
// --- 创建控制面板 UI ---
function createMinimalUI() {
injectStyles();
const bar = document.createElement('div');
bar.id = BAR_ID;
const limitControl = document.createElement('div');
limitControl.id = LIMIT_CONTROL_ID;
limitControl.title = "点击设置要提取的最大楼层数 (0=全部)";
const label = document.createElement('label');
label.textContent = "楼层:";
const limitDisplay = document.createElement('span');
limitDisplay.id = LIMIT_DISPLAY_ID;
const limitInput = document.createElement('input');
limitInput.type = 'number';
limitInput.id = LIMIT_INPUT_ID;
limitInput.min = 0;
limitInput.style.display = 'none'; // Hidden by default
limitControl.append(label, limitDisplay, limitInput);
const actionButton = document.createElement('button');
actionButton.id = ACTION_BTN_ID;
actionButton.textContent = ACTION_BTN_TEXT;
bar.append(limitControl, actionButton);
document.body.appendChild(bar);
// Init state
const initialValue = localStorage.getItem(LS_KEY_FLOORS);
updateLimitState(initialValue, limitDisplay, limitInput, actionButton);
return { bar, limitControl, actionButton, limitDisplay, limitInput };
}
let ui = {};
setTimeout(() => {
if(document.getElementById(BAR_ID)) return;
if(!initTurndown()) {
try { ui = createMinimalUI(); disableUI(ui, "库加载失败", '#dc3545'); } catch(e) { console.error("UI Fail", e); }
return;
};
try { ui = createMinimalUI(); addEventListeners(ui);
} catch(e) { console.error("UI Fail", e); alert("脚本UI创建失败!"); }
}, 1500);
function disableUI({bar, actionButton, limitInput}, text = null, bgColor = null) {
isRunning = true;
if(bar) bar.classList.add('disabled');
if(actionButton) {
actionButton.disabled = true;
if(text) actionButton.textContent = text;
if(bgColor) actionButton.style.backgroundColor = bgColor;
}
if(limitInput) limitInput.disabled = true;
// Ensure input is hidden if active
if(isEditingLimit && ui.limitInput && ui.limitDisplay){
ui.limitInput.style.display = 'none';
ui.limitDisplay.style.display = 'inline-block';
isEditingLimit = false;
}
}
function restoreUI({bar, actionButton, limitInput}, text = ACTION_BTN_TEXT, delay = 2500) {
setTimeout(() => {
isRunning = false;
if(bar) bar.classList.remove('disabled');
if(actionButton){
actionButton.disabled = false;
actionButton.textContent = text;
actionButton.style.backgroundColor = ''; // Reset to CSS default
}
if(limitInput) limitInput.disabled = false;
updateLimitState(localStorage.getItem(LS_KEY_FLOORS), ui.limitDisplay, ui.limitInput, ui.actionButton); // Refresh title
}, delay);
}
// --- 事件监听 ---
function addEventListeners({ bar, limitControl, actionButton, limitDisplay, limitInput }) {
if(!bar || !actionButton || !limitDisplay || !limitInput) return;
const showInput = () => {
if(isRunning) return;
limitInput.value = parseInt(localStorage.getItem(LS_KEY_FLOORS), 10) || 0; // Load current value
limitDisplay.style.display = 'none';
limitInput.style.display = 'block';
limitInput.focus(); limitInput.select();
isEditingLimit = true;
};
const saveAndHideInput = (save = true) => {
if(!isEditingLimit) return;
if(save) {
updateLimitState(limitInput.value, limitDisplay, limitInput, actionButton);
} else {
// Restore display without saving (for Escape key)
const lastValue = parseInt(localStorage.getItem(LS_KEY_FLOORS), 10) || 0;
limitDisplay.textContent = lastValue === 0 ? '全部' : lastValue;
}
limitInput.style.display = 'none';
limitDisplay.style.display = 'inline-block';
isEditingLimit = false;
};
limitControl.addEventListener('click', (e) => {
// Only trigger if not clicking the input itself while editing
if(!isRunning && !isEditingLimit) {
showInput();
e.stopPropagation(); // Prevent blur if clicking control bg
}
});
limitInput.addEventListener('blur', () => saveAndHideInput(true) );
limitInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); limitInput.blur(); } // blur will save
if (e.key === 'Escape') { e.preventDefault(); saveAndHideInput(false); } // Hide without saving
});
actionButton.addEventListener('click', async () => {
if(!turndownService || isRunning) return;
if(isEditingLimit) saveAndHideInput(true); // Save if input is open
if (!document.querySelector('#topic-title') || !document.querySelector('article[data-post-id]')) {
alert(`脚本错误(v2.0):无法找到关键页面元素!`);
disableUI(ui, '选择器错误!', '#dc3545');
restoreUI(ui, '选择器错误!', 4000); return;
}
const limit = parseInt(localStorage.getItem(LS_KEY_FLOORS), 10) || 0;
disableUI(ui, '准备中...');
try {
const currentLoaded = getPosts().length;
if (limit > 0 && currentLoaded >= limit) {
actionButton.textContent = `已加载 ${currentLoaded}/${limit}`;
} else {
actionButton.textContent = '滚动中... 0%';
await scrollToBottomAndLoad(actionButton, limit);
}
actionButton.textContent = '转换中...';
await new Promise(resolve => setTimeout(resolve, 800));
const content = extractContentMarkdown(limit);
if(!content || !content.text || content.postCount === 0) throw new Error("提取内容为空!");
downloadMD(content.title, content.text);
const target = limit > 0 ? limit : '全部';
restoreUI(ui, `完成! (${content.postCount}/${target})`, 4000);
} catch (error) {
console.error("SCRIPT Error:", error);
alert(`提取失败(v2.0): ${error.message}`);
disableUI(ui, '提取失败', '#dc3545');
restoreUI(ui, '提取失败', 4000);
}
});
}
async function scrollToBottomAndLoad(btn, limit = 0) {
let lastHeight = 0, stableCount = 0, totalPosts = 0;
const maxStableCount = Math.ceil(SCROLL_STABLE_TIME_MS / SCROLL_CHECK_INTERVAL_MS);
const startTime = Date.now();
try {
totalPosts = parseInt(document.querySelector('.topic-map .posts-count, .timeline-container span')?.textContent.replace(/\D/g,''), 10) || 0;
if(!totalPosts) totalPosts = parseInt(document.querySelector('.timeline-container .post-number:last-child')?.textContent.replace(/\D/g,''), 10) || 0;
} catch(e){}
const effectiveTotal = (limit > 0 && (totalPosts === 0 || limit < totalPosts)) ? limit : totalPosts;
while ( (Date.now() - startTime) < MAX_SCROLL_WAIT_SEC * 1000 ) {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(resolve => setTimeout(resolve, SCROLL_CHECK_INTERVAL_MS));
const currentHeight = document.body.scrollHeight; const currentPostCount = getPosts().length;
const progressText = `(${currentPostCount}${effectiveTotal > 0 ? '/' + effectiveTotal : ''})`;
const percentage = (effectiveTotal > 0 && currentPostCount > 0) ? ' '+Math.min(100, Math.round((currentPostCount / effectiveTotal) * 100))+'%' : '';
btn.textContent = `滚动...${percentage} ${progressText}`;
if(limit > 0 && currentPostCount >= limit) { btn.textContent = `已加载 ${currentPostCount}/${limit}`; return; }
if (currentHeight > lastHeight) { lastHeight = currentHeight; stableCount = 0;
} else if (++stableCount >= maxStableCount) { btn.textContent = `加载完 ${progressText}`; return; }
if(totalPosts > 0 && limit === 0 && currentPostCount >= totalPosts ){ btn.textContent = `加载完 ${progressText}`; return; }
}
btn.textContent = `超时! (${getPosts().length})`;
}
function extractContentMarkdown(limit = 0) {
if(!turndownService) return { postCount: 0 };
const title = document.querySelector('#topic-title h1 a, #topic-title a')?.innerText.trim() || 'LINUXDO_Post';
let md = `# ${title}\n\n> 来源: ${window.location.href.split('?')[0]}\n\n`;
const allPosts = getPosts();
const posts = (limit > 0 && limit < allPosts.length) ? allPosts.slice(0, limit) : allPosts;
if (posts.length === 0) return { postCount: 0 };
posts.forEach((p, i) => {
const user = p.dataset.username || p.querySelector('.names .username a, .names span')?.innerText.trim() || 'User';
const html = p.querySelector('.cooked, .post')?.innerHTML || '[N/A]';
let postMD; try { postMD = turndownService.turndown(html).trim(); } catch(e) { postMD = `*[Error]*\n${p.querySelector('.cooked, .post')?.innerText.trim()||''}`;}
md += `---\n\n### [${i + 1}] ${user}\n\n${postMD}\n\n`;
});
return { title, text: md.trim() + '\n---\n', postCount: posts.length };
}
function downloadMD(title, text) {
const fn = (title || 'LINUXDO_POST').replace(/[\/\\:*?"<>|\s]/g, '_') + '.md';
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], { type: 'text/markdown;charset=utf-8' })); a.download = fn;
document.body.appendChild(a); a.click();
setTimeout(()=>{ document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 500);
}
})();
这个油猴插件是坛子里佬的分享,可以提取帖子的任意回复
Hhfguhvmp
(Hgfyk)
2025 年7 月 27 日 09:14
9
额,不行,可能是我的问题,我用chrome浏览器保存, 但是打开后一直在加载页面
abbb
(是人类)
2025 年7 月 27 日 09:15
10
SingleFile 浏览器插件,直接保存当前页面的所有信息(视频只有一帧截图)为单个html文档,需要保存原始页面的时候常用这个。
5 个赞
libook
(libook)
2025 年7 月 27 日 09:15
11
你试试打印到PDF
Cknight
(cknight)
2025 年7 月 27 日 09:16
12
我习惯用notion或者clip之类
Hhfguhvmp
(Hgfyk)
2025 年7 月 27 日 09:16
13
可以,但是页面显示感觉有点糙,不过还是非常感谢
lwang
2025 年7 月 27 日 09:18
14
// ==UserScript==
// @name Linux.do Forum Post Exporter
// @namespace https://linux.do/
// @version 1.2.0
// @description Export forum posts from linux.do with replies in JSON or HTML format
// @author Forum Exporter
// @match https://linux.do/t/*
// @match https://linux.do/t/topic/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Convert image URL to base64 data URL
async function imageToBase64(url) {
try {
const response = await fetch(url);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Failed to convert image:', url, error);
return url; // Return original URL as fallback
}
}
// Wait for the page to fully load
function waitForPosts() {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const posts = document.querySelectorAll('article[data-post-id]');
if (posts.length > 0) {
clearInterval(checkInterval);
resolve();
}
}, 500);
});
}
// Extract post data from a single post element
function extractPostData(postElement) {
const postId = postElement.getAttribute('data-post-id');
const postNumber = postElement.closest('.topic-post').getAttribute('data-post-number');
// Extract author information
const authorLink = postElement.querySelector('.names a');
const authorUsername = authorLink ? authorLink.textContent.trim() : '';
const authorHref = authorLink ? authorLink.getAttribute('href') : '';
// Extract avatar
const avatarImg = postElement.querySelector('.post-avatar img');
const avatarUrl = avatarImg ? avatarImg.getAttribute('src') : '';
// Extract post date
const dateElement = postElement.querySelector('.post-date .relative-date');
const postDate = dateElement ? dateElement.getAttribute('data-time') : '';
const postDateFormatted = dateElement ? dateElement.textContent.trim() : '';
// Extract post content
const contentElement = postElement.querySelector('.cooked');
const content = contentElement ? contentElement.innerHTML : '';
const contentText = contentElement ? contentElement.textContent.trim() : '';
// Extract quoted content if present
const quotes = [];
const quoteElements = postElement.querySelectorAll('aside.quote');
quoteElements.forEach(quote => {
const quotedUser = quote.getAttribute('data-username');
const quotedPost = quote.getAttribute('data-post');
const quotedContent = quote.querySelector('blockquote');
quotes.push({
username: quotedUser,
postNumber: quotedPost,
content: quotedContent ? quotedContent.innerHTML : ''
});
});
// Extract reply-to information
const replyToElement = postElement.querySelector('.reply-to-tab');
let replyTo = null;
if (replyToElement) {
const replyToUser = replyToElement.querySelector('span');
const replyToAvatar = replyToElement.querySelector('img');
replyTo = {
username: replyToUser ? replyToUser.textContent.trim() : '',
avatarUrl: replyToAvatar ? replyToAvatar.getAttribute('src') : ''
};
}
// Extract reaction counts
const reactions = [];
const reactionElements = postElement.querySelectorAll('.discourse-reactions-counter button');
reactionElements.forEach(reaction => {
const count = reaction.querySelector('.count');
const emoji = reaction.querySelector('.emoji');
if (count && emoji) {
reactions.push({
emoji: emoji.textContent.trim(),
count: parseInt(count.textContent.trim())
});
}
});
return {
postId,
postNumber: parseInt(postNumber),
author: {
username: authorUsername,
profileUrl: authorHref,
avatarUrl: avatarUrl
},
timestamp: postDate,
dateFormatted: postDateFormatted,
content: content,
contentText: contentText,
quotes: quotes,
replyTo: replyTo,
reactions: reactions
};
}
// Extract topic metadata
function extractTopicData() {
const topicTitle = document.querySelector('h1[data-topic-id] .fancy-title span');
const topicId = document.querySelector('h1[data-topic-id]')?.getAttribute('data-topic-id');
const category = document.querySelector('.badge-category__name');
const tags = [...new Set(Array.from(document.querySelectorAll('.discourse-tag')).map(tag => tag.textContent.trim()))];
return {
topicId: topicId,
title: topicTitle ? topicTitle.textContent.trim() : '',
category: category ? category.textContent.trim() : '',
tags: tags,
url: window.location.href
};
}
// Process content to convert images to base64
async function processContentImages(content) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
const images = tempDiv.querySelectorAll('img');
for (const img of images) {
const src = img.getAttribute('src');
if (src) {
// Handle relative URLs
const fullUrl = src.startsWith('http') ? src : `https://linux.do${src}`;
const base64 = await imageToBase64(fullUrl);
img.setAttribute('src', base64);
}
}
return tempDiv.innerHTML;
}
// Clean content for HTML display
function cleanContentForHTML(content) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
// Remove unnecessary elements
const selectorsToRemove = [
'.cooked-selection-barrier',
'div:empty',
'[aria-hidden="true"]'
];
selectorsToRemove.forEach(selector => {
tempDiv.querySelectorAll(selector).forEach(el => el.remove());
});
// Clean up whitespace
let cleaned = tempDiv.innerHTML
.replace(/\s*
\s*<\/div>/g, '