// ==UserScript==
// @name UNIT3D Forum Bookmarks
// @namespace https://github.com/flowerey/unit3d-scripts
// @version 1.0.2
// @description Bookmark and manage forum posts on UNIT3D sites
// @author blueberry
// @match https://*/forums/*
// @match https://*/forum/*
// @downloadURL https://raw.githubusercontent.com/flowerey/unit3d-scripts/main/forum-bookmarks.user.js
// @updateURL https://raw.githubusercontent.com/flowerey/unit3d-scripts/main/forum-bookmarks.user.js.meta.js
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'u3d-forum-bookmarks';
const MAX_BOOKMARKS = 200;
function loadData() {
try {
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (stored && stored.bookmarks) return stored;
} catch (e) { /* ignore */ }
return { bookmarks: [], tags: [] };
}
function saveData(data) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (e) { /* ignore */ }
}
function addBookmark(bm) {
const data = loadData();
if (data.bookmarks.some(b => b.id === bm.id)) return false;
data.bookmarks.unshift(bm);
if (data.bookmarks.length > MAX_BOOKMARKS) data.bookmarks = data.bookmarks.slice(0, MAX_BOOKMARKS);
saveData(data);
return true;
}
function removeBookmark(id) {
const data = loadData();
data.bookmarks = data.bookmarks.filter(b => b.id !== id);
saveData(data);
}
function isBookmarked(id) {
return loadData().bookmarks.some(b => b.id === id);
}
function getPostInfo() {
const url = window.location.href;
// UNIT3D permalink route: /forums/topics/{topicId}/posts/{postId}
const permalink = url.match(/\/forums\/topics\/\d+\/posts\/(\d+)/);
const query = url.match(/[?&]post=(\d+)/);
const postId = permalink ? permalink[1] : (query ? query[1] : null);
// Legacy path fallback: /forums|/forum/{category}/{slug}/{postId}
let legacyId = null;
if (!postId) {
const legacy = url.match(/\/(?:forums|forum)\/[^/]+\/[^/]+\/(\d+)/);
if (legacy) legacyId = legacy[1];
}
const id = postId || legacyId;
if (!id) return null;
const titleEl = document.querySelector('h2.panel__heading, .topic-title, h2.post-title, h1');
const title = titleEl ? titleEl.textContent.trim().substring(0, 200) : document.title;
const topicEl = document.querySelector('.topic-title a, .forum-topic__title a, h2.panel__heading');
const topicTitle = topicEl ? topicEl.textContent.trim() : '';
const topicUrl = topicEl && topicEl.href ? topicEl.href : '';
const categoryEl = document.querySelector('.forum-category, .breadcrumb--active, .breadcrumb a:last-child');
const category = categoryEl ? categoryEl.textContent.trim() : '';
return { id, url: url.split('?')[0], title, topicTitle, topicUrl, category };
}
function injectBookmarkButton(postInfo) {
if (!postInfo || document.querySelector('.u3d-fb-btn')) return;
const actions = document.querySelector('.post__toolbar, .post__actions, .topic__actions, .panel__actions');
if (!actions) return;
const btn = document.createElement('button');
btn.className = 'u3d-fb-btn';
btn.style.cssText = 'padding:4px 10px;background:#1a1a2e;color:#e94560;border:1px solid #e94560;border-radius:4px;cursor:pointer;font-size:12px;margin-left:8px;';
const updateBtn = () => {
const saved = isBookmarked(postInfo.id);
btn.textContent = saved ? '\u2605 Saved' : '\u2606 Bookmark';
btn.style.color = saved ? '#66ff66' : '#e94560';
btn.style.borderColor = saved ? '#66ff66' : '#e94560';
};
updateBtn();
btn.addEventListener('click', () => {
if (isBookmarked(postInfo.id)) {
removeBookmark(postInfo.id);
} else {
addBookmark({
...postInfo,
timestamp: Date.now(),
readAt: null,
tags: []
});
}
updateBtn();
});
// UNIT3D forum posts render their action buttons inside a