BstX Notes

Advanced notes system for X and Bluesky profiles and posts

// ==UserScript==
// @name         BstX Notes 
// @namespace    http://tampermonkey.net/
// @version      0.0.2
// @description  Advanced notes system for X and Bluesky profiles and posts
// @author       @MichDe.com aka @ManInTheDot.com
// @match        https://x.com/*
// @match        https://bsky.app/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
'use strict';

const PLATFORMS = {
'x.com': {
	profileRegex: /^https:\/\/x\.com\/([^\/]+)$/,
	postRegex: /^https:\/\/x\.com\/([^\/]+)\/status\/(\d+)/,
	nonUserPages: [
		'/home', '/explore', '/notifications',
		'/messages', '/settings', '/i/', '/search'
	],
	buttonSelectors: [
		'header[role="banner"]',  // Side navigation area
		'div[data-testid="primaryColumn"]'  // Main content column
	]
},
'bsky.app': {
	profileRegex: /^https:\/\/bsky\.app\/profile\/([^\/]+)$/,
	postRegex: /^https:\/\/bsky\.app\/profile\/([^\/]+)\/post\/([a-z0-9]+)/,
	nonUserPages: [
		'/notifications', '/feeds', '/lists',
		'/settings', '/', '/explore', '/search'
	],
	buttonSelectors: [
		'nav[role="navigation"]',  // Side navigation
		'div[data-testid="AppContainer"]'  // Main app container
	]
}
};

const StorageManager = {
getAllNotes() {
	const notes = {};
	for (let i = 0; i < localStorage.length; i++) {
		const key = localStorage.key(i);
		if (key.startsWith('social-notes:')) {
			const fullKey = key.replace('social-notes:', '');
			notes[fullKey] = JSON.parse(localStorage.getItem(key));
		}
	}
	return notes;
},
saveNote(key, noteData) {
	localStorage.setItem(`social-notes:${key}`, JSON.stringify(noteData));
},
getNote(key) {
	const note = localStorage.getItem(`social-notes:${key}`);
	return note ? JSON.parse(note) : null;
},
deleteNote(key) {
	localStorage.removeItem(`social-notes:${key}`);
}
};

class NotesPaneUI {
constructor() {
	this.createNotesPane();
	this.createAllNotesPane();
}

createNotesPane() {
	this.pane = document.createElement('div');
	this.pane.id = 'social-notes-pane';
	this.pane.style.cssText = `
		position: fixed;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		width: 500px;
		background: whitesmoke;
		border: 2px solid #333;
		z-index: 9999;
		padding: 20px;
		display: none;
		flex-direction: column;
		gap: 10px;
	`;

	this.keyInput = document.createElement('input');
	this.keyInput.placeholder = 'Profile/Post ID';
	this.keyInput.style.width = '100%';
	this.keyInput.disabled = true;

	this.notesTextarea = document.createElement('textarea');
	this.notesTextarea.placeholder = 'Enter notes...';
	this.notesTextarea.style.width = '100%';
	this.notesTextarea.rows = 5;

	this.tagsInput = document.createElement('input');
	this.tagsInput.placeholder = 'Tags (comma-separated)';
	this.tagsInput.style.width = '100%';

	const buttonContainer = document.createElement('div');
	buttonContainer.style.display = 'flex';
	buttonContainer.style.justifyContent = 'space-between';

	const saveButton = document.createElement('button');
	saveButton.textContent = 'Save';
	saveButton.onclick = () => this.saveNote();

	const cancelButton = document.createElement('button');
	cancelButton.textContent = 'Cancel';
	cancelButton.onclick = () => this.hide();

	const deleteButton = document.createElement('button');
	deleteButton.textContent = 'Delete';
	deleteButton.style.backgroundColor = 'red';
	deleteButton.style.color = 'white';
	deleteButton.onclick = () => this.deleteNote();

	const viewAllButton = document.createElement('button');
	viewAllButton.textContent = 'View All Notes';
	viewAllButton.onclick = () => {
		this.hide();
		this.showAllNotes();
	};

	buttonContainer.append(
		saveButton,
		cancelButton,
		deleteButton,
		viewAllButton
	);

	this.pane.append(
		this.keyInput,
		this.notesTextarea,
		this.tagsInput,
		buttonContainer
	);

	document.body.appendChild(this.pane);
}

createAllNotesPane() {
	this.allNotesPane = document.createElement('div');
	this.allNotesPane.id = 'social-notes-all-pane';
	this.allNotesPane.style.cssText = `
		position: fixed;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		width: 600px;
		height: 500px;
		background: whitesmoke;
		border: 2px solid #333;
		z-index: 9999;
		padding: 20px;
		display: none;
		flex-direction: column;
		overflow-y: auto;
	`;

	const closeButton = document.createElement('button');
	closeButton.textContent = 'Close';
	closeButton.onclick = () => this.hideAllNotes();

	this.allNotesContainer = document.createElement('div');
	this.allNotesContainer.style.marginTop = '10px';

	this.allNotesPane.append(closeButton, this.allNotesContainer);
	document.body.appendChild(this.allNotesPane);
}

show(key, notes = '', tags = '') {
	this.keyInput.value = key;
	this.notesTextarea.value = notes;
	this.tagsInput.value = tags;
	this.pane.style.display = 'flex';
}

hide() {
	this.pane.style.display = 'none';
}

saveNote() {
	const key = this.keyInput.value.trim();
	const notes = this.notesTextarea.value.trim();
	const tags = this.tagsInput.value.trim().split(',')
		.map(t => t.trim())
		.filter(t => t);

	if (!key) {
		alert('Please enter a Profile/Post ID');
		return;
	}

	StorageManager.saveNote(key, { notes, tags });
	this.hide();
	addNotesButton(); 
}

deleteNote() {
	const key = this.keyInput.value.trim();
	if (key && confirm('Are you sure you want to delete this note?')) {
		StorageManager.deleteNote(key);
		this.hide();
		addNotesButton(); 
	}
}

showAllNotes() {
	const notes = StorageManager.getAllNotes();
	this.allNotesContainer.innerHTML = '';

	if (Object.keys(notes).length === 0) {
		this.allNotesContainer.innerHTML = '<p>No notes saved.</p>';
	} else {
		const table = document.createElement('table');
		table.style.width = '100%';
		table.style.borderCollapse = 'collapse';

		const headerRow = table.insertRow();
		['ID', 'Notes', 'Tags', 'Actions'].forEach(header => {
			const th = document.createElement('th');
			th.textContent = header;
			th.style.border = '1px solid black';
			headerRow.appendChild(th);
		});

		Object.entries(notes).forEach(([key, noteData]) => {
			const row = table.insertRow();

			const keyCell = row.insertCell();
			keyCell.textContent = key;
			keyCell.style.border = '1px solid black';
			keyCell.style.padding = '5px';

			const notesCell = row.insertCell();
			notesCell.textContent = noteData.notes || 'No notes';
			notesCell.style.border = '1px solid black';
			notesCell.style.padding = '5px';

			const tagsCell = row.insertCell();
			tagsCell.textContent = (noteData.tags || []).join(', ') || 'No tags';
			tagsCell.style.border = '1px solid black';
			tagsCell.style.padding = '5px';

			const actionsCell = row.insertCell();
			actionsCell.style.border = '1px solid black';
			actionsCell.style.padding = '5px';

			const editButton = document.createElement('button');
			editButton.textContent = 'Edit';
			editButton.onclick = () => this.show(key, noteData.notes, (noteData.tags || []).join(', '));

			const goToButton = document.createElement('button');
			goToButton.textContent = 'Go to';
			goToButton.onclick = () => this.navigateToItem(key);

			actionsCell.append(editButton, goToButton);
		});

		this.allNotesContainer.appendChild(table);
	}

	this.allNotesPane.style.display = 'flex';
}

hideAllNotes() {
	this.allNotesPane.style.display = 'none';
}

navigateToItem(key) {
	const platform = window.location.hostname;
	const [type, identifier] = key.split(':');

	const platformConfigs = {
		'x.com': {
			profile: `https://x.com/${identifier}`,
			status: `https://x.com/${key.split(':')[1].split('/')[0]}/status/${identifier}`
		},
		'bsky.app': {
			profile: `https://bsky.app/profile/${identifier}`,
			post: `https://bsky.app/profile/${key.split(':')[1].split('/')[0]}/post/${identifier}`
		}
	};

	const config = platformConfigs[platform];
	if (config && config[type]) {
		window.open(config[type], '_blank');
	} else {
		alert('Cannot navigate to this item.');
	}
}
}

function initSocialNotes() {
const platform = window.location.hostname;
const config = PLATFORMS[platform];
const notesPaneUI = new NotesPaneUI();

function getPageInfo() {
	const url = window.location.href;

	const profileMatch = url.match(config.profileRegex);
	if (profileMatch) {
		return {
			type: 'profile',
			key: `profile:${profileMatch[1]}`,
			isProfileOrPost: true
		};
	}

	const postMatch = url.match(config.postRegex);
	if (postMatch) {
		return {
			type: platform === 'x.com' ? 'status' : 'post',
			key: `${platform === 'x.com' ? 'status' : 'post'}:${postMatch[2]}`,
			isProfileOrPost: true
		};
	}

	return {
		type: 'other',
		key: `page:${window.location.pathname}`,
		isProfileOrPost: false
	};
}

function findButtonParent() {
	for (const selector of config.buttonSelectors) {
		const parent = document.querySelector(selector);
		if (parent) return parent;
	}
	return document.body;
}

function addNotesButton() {
	document.querySelectorAll('#social-notes-button').forEach(b => b.remove());

	const pageInfo = getPageInfo();
	const parent = findButtonParent();

	const button = document.createElement('div');
	button.id = 'social-notes-button';
	button.textContent = 'Notes';
	button.style.cssText = `
		position: fixed;
		bottom: 201px;
		right: 42px;
		width: 66px;
		height: 39px;
		border: 1px thin #ccc;
		background-color: skyblue;
		color: whitesmoke;
		cursor: pointer;
		z-index: 1000;
		display: flex;
		align-items: center;
		justify-content: center;
		border-radius: 3px;
	`;

	const existingNote = StorageManager.getNote(pageInfo.key);

	if (pageInfo.isProfileOrPost) {
		// Profile or post pages
		button.style.backgroundColor = existingNote ? 'springgreen' : 'skyblue';
	} else {
		// Other pages - muted tone
		button.style.backgroundColor = 'whitesmoke';
		button.style.color = 'black';
	}

	button.onclick = () => {
		const note = existingNote || {};
		notesPaneUI.show(
			pageInfo.key,
			note.notes || '',
			note.tags ? note.tags.join(', ') : ''
		);
	};

	parent.appendChild(button);
}

addNotesButton();

let lastUrl = window.location.href;
setInterval(() => {
	if (window.location.href !== lastUrl) {
		lastUrl = window.location.href;
		addNotesButton();
	}
}, 1000);
}

initSocialNotes();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址