Professional Website Notes Manager

Professional notes manager with editable URLs, darkmode, and quick delete functionality

目前為 2025-01-24 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Professional Website Notes Manager
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Professional notes manager with editable URLs, darkmode, and quick delete functionality
// @author       Byakuran
// @match        https://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // Check system dark mode preference
    const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const isDarkMode = GM_getValue('darkMode', prefersDarkMode);

    const darkModeStyles = {
        modal: {
            bg: '#1f2937',
            text: '#f3f4f6'
        },
        input: {
            bg: '#374151',
            border: '#4b5563',
            text: '#f3f4f6'
        },
        button: {
            primary: '#3b82f6',
            primaryHover: '#2563eb',
            secondary: '#4b5563',
            secondaryHover: '#374151',
            text: '#ffffff'
        },
        listItem: {
            bg: '#374151',
            bgHover: '#4b5563',
            text: '#f3f4f6'
        }
    };

    const lightModeStyles = {
        modal: {
            bg: '#ffffff',
            text: '#111827'
        },
        input: {
            bg: '#f9fafb',
            border: '#e5e7eb',
            text: '#111827'
        },
        button: {
            primary: '#3b82f6',
            primaryHover: '#2563eb',
            secondary: '#f3f4f6',
            secondaryHover: '#e5e7eb',
            text: '#ffffff'
        },
        listItem: {
            bg: '#ffffff',
            bgHover: '#f9fafb',
            text: '#1f2937'
        }
    };

    const currentTheme = isDarkMode ? darkModeStyles : lightModeStyles;

    const styles = `
        .notes-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: ${currentTheme.modal.bg};
            color: ${currentTheme.modal.text};
            padding: 32px;
            border-radius: 16px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.25);
            z-index: 10000;
            max-width: 700px;
            width: 90%;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        }
        .notes-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,${isDarkMode ? '0.8' : '0.7'});
            z-index: 9999;
            backdrop-filter: blur(4px);
        }
        .notes-input {
            width: 100%;
            margin: 12px 0;
            padding: 12px 16px;
            border: 2px solid ${currentTheme.input.border};
            border-radius: 8px;
            font-size: 15px;
            transition: all 0.2s ease;
            background: ${currentTheme.input.bg};
            color: ${currentTheme.input.text};
        }
        .notes-input:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
        }
        .notes-textarea {
            width: 100%;
            height: 200px;
            margin: 12px 0;
            padding: 16px;
            border: 2px solid ${currentTheme.input.border};
            border-radius: 8px;
            font-size: 15px;
            resize: vertical;
            transition: all 0.2s ease;
            background: ${currentTheme.input.bg};
            color: ${currentTheme.input.text};
            line-height: 1.5;
        }
        .notes-textarea:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
        }
        .notes-button {
            background: ${currentTheme.button.primary};
            color: ${currentTheme.button.text};
            border: none;
            padding: 12px 24px;
            border-radius: 8px;
            cursor: pointer;
            margin: 5px;
            font-size: 15px;
            font-weight: 500;
            transition: all 0.2s ease;
        }
        .notes-button:hover {
            background: ${currentTheme.button.primaryHover};
            transform: translateY(-1px);
        }
        .notes-button.secondary {
            background: ${currentTheme.button.secondary};
            color: ${isDarkMode ? '#f3f4f6' : '#4b5563'};
        }
        .notes-button.secondary:hover {
            background: ${currentTheme.button.secondaryHover};
        }
        .notes-button.delete {
            background: #ef4444;
        }
        .notes-button.delete:hover {
            background: #dc2626;
        }
        .notes-button.edit {
            background: #10b981;
        }
        .notes-button.edit:hover {
            background: #059669;
        }
        .notes-list-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px;
            border: 1px solid ${currentTheme.input.border};
            border-radius: 8px;
            margin: 8px 0;
            cursor: pointer;
            transition: all 0.2s ease;
            background: ${currentTheme.listItem.bg};
            color: ${currentTheme.listItem.text};
        }
        .notes-list-item:hover {
            background: ${currentTheme.listItem.bgHover};
            transform: translateY(-1px);
            box-shadow: 0 4px 12px rgba(0,0,0,${isDarkMode ? '0.3' : '0.05'});
        }
        .close-button {
            position: absolute;
            top: 16px;
            right: 16px;
            cursor: pointer;
            font-size: 24px;
            color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
            transition: all 0.2s;
            width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 6px;
        }
        .close-button:hover {
            color: ${isDarkMode ? '#f3f4f6' : '#111827'};
            background: ${isDarkMode ? '#374151' : '#f3f4f6'};
        }
        .modal-title {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 24px;
            color: ${currentTheme.modal.text};
        }
        .url-text {
            font-size: 14px;
            color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
            word-break: break-all;
            margin-bottom: 16px;
            padding: 8px 12px;
            background: ${isDarkMode ? '#374151' : '#f3f4f6'};
            border-radius: 6px;
        }
        .timestamp {
            font-size: 12px;
            color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
            margin-top: 4px;
        }
        .delete-note-button {
            background: none;
            border: none;
            color: #ef4444;
            font-size: 18px;
            cursor: pointer;
            padding: 4px 8px;
            border-radius: 4px;
            transition: all 0.2s ease;
        }
        .delete-note-button:hover {
            background: #ef4444;
            color: #ffffff;
        }
    `;

    // Add styles to document
    const styleSheet = document.createElement("style");
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);

    // Add dark mode toggle command
    GM_registerMenuCommand('Toggle Dark Mode', () => {
        const newMode = !isDarkMode;
        GM_setValue('darkMode', newMode);
        location.reload();
    });

    // Helper functions
    function createModal(content) {
        const overlay = document.createElement('div');
        overlay.className = 'notes-overlay';

        const modal = document.createElement('div');
        modal.className = 'notes-modal';

        const closeButton = document.createElement('span');
        closeButton.className = 'close-button';
        closeButton.textContent = '×';
        closeButton.onclick = () => overlay.remove();

        modal.appendChild(closeButton);
        modal.appendChild(content);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // Add escape key listener
        const escapeListener = (e) => {
            if (e.key === 'Escape') {
                overlay.remove();
                document.removeEventListener('keydown', escapeListener);
            }
        };
        document.addEventListener('keydown', escapeListener);
    }

    function getAllNotes() {
        return GM_getValue('website-notes', {});
    }

    function saveNote(title, url, content, timestamp = Date.now()) {
        const notes = getAllNotes();
        if (!notes[url]) notes[url] = [];
        notes[url].push({ title, content, timestamp });
        GM_setValue('website-notes', notes);
    }

    function updateNote(oldUrl, index, title, newUrl, content) {
        const notes = getAllNotes();
        deleteNote(oldUrl, index);
        saveNote(title, newUrl, content);
    }

    function deleteNote(url, index) {
        const notes = getAllNotes();
        if (notes[url]) {
            notes[url].splice(index, 1);
            if (notes[url].length === 0) delete notes[url];
            GM_setValue('website-notes', notes);
        }
    }

    function showNoteForm(editMode = false, existingNote = null, url = null, index = null) {
        const container = document.createElement('div');
        container.innerHTML = `<h3 class="modal-title">${editMode ? 'Edit Note' : 'Create New Note'}</h3>`;

        const titleInput = document.createElement('input');
        titleInput.className = 'notes-input';
        titleInput.placeholder = 'Enter title';
        titleInput.value = editMode ? existingNote.title : '';

        const urlInput = document.createElement('input');
        urlInput.className = 'notes-input';
        urlInput.placeholder = 'Enter URL or URL pattern (e.g., https://domain.com/*)';
        urlInput.value = editMode ? url : window.location.href;

        const patternHelp = document.createElement('div');
        patternHelp.style.fontSize = '12px';
        patternHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280';
        patternHelp.style.marginTop = '-8px';
        patternHelp.style.marginBottom = '8px';
        patternHelp.innerHTML = 'Use * for wildcard matching (e.g., https://domain.com/*)';

        const contentArea = document.createElement('textarea');
        contentArea.className = 'notes-textarea';
        contentArea.placeholder = 'Enter your notes here';
        contentArea.value = editMode ? existingNote.content : '';

        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'button-group';

        const saveButton = document.createElement('button');
        saveButton.className = 'notes-button';
        saveButton.textContent = editMode ? 'Update Note' : 'Save Note';
        saveButton.onclick = () => {
            if (titleInput.value && contentArea.value) {
                if (editMode) {
                    updateNote(url, index, titleInput.value, urlInput.value, contentArea.value);
                } else {
                    saveNote(titleInput.value, urlInput.value, contentArea.value);
                }
                container.parentElement.parentElement.remove();
                showCurrentPageNotes();
            }
        };

        const cancelButton = document.createElement('button');
        cancelButton.className = 'notes-button secondary';
        cancelButton.textContent = 'Cancel';
        cancelButton.onclick = () => container.parentElement.parentElement.remove();

        buttonGroup.appendChild(saveButton);
        buttonGroup.appendChild(cancelButton);

        container.appendChild(titleInput);
        container.appendChild(urlInput);
        container.appendChild(patternHelp);
        container.appendChild(contentArea);
        container.appendChild(buttonGroup);

        createModal(container);
    }

    function formatDate(timestamp) {
        return new Date(timestamp).toLocaleString();
    }

    function showNoteContent(note, url, index) {
        const container = document.createElement('div');
        container.innerHTML = `
            <h3 class="modal-title">${note.title}</h3>
            <div class="url-text">${url}</div>
            <div class="timestamp">Created: ${formatDate(note.timestamp)}</div>
            <p style="white-space: pre-wrap; margin: 16px 0; line-height: 1.6;">${note.content}</p>
        `;

        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'button-group';

        const editButton = document.createElement('button');
        editButton.className = 'notes-button edit';
        editButton.textContent = 'Edit';
        editButton.onclick = () => {
            container.parentElement.parentElement.remove();
            showNoteForm(true, note, url, index);
        };

        const deleteButton = document.createElement('button');
        deleteButton.className = 'notes-button delete';
        deleteButton.textContent = 'Delete';
        deleteButton.onclick = () => {
            if (confirm('Are you sure you want to delete this note?')) {
                deleteNote(url, index);
                container.parentElement.parentElement.remove();
                showCurrentPageNotes();
            }
        };

        buttonGroup.appendChild(editButton);
        buttonGroup.appendChild(deleteButton);
        container.appendChild(buttonGroup);

        createModal(container);
    }

    function doesUrlMatchPattern(urlPattern, currentUrl) {
        // Escape special characters for regex
        const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

        // Convert Tampermonkey-style pattern to regex
        const patternToRegex = (pattern) => {
            const parts = pattern.split('*');
            let regexString = '^';
            for (let i = 0; i < parts.length; i++) {
                regexString += escapeRegex(parts[i]);
                if (i < parts.length - 1) {
                    if (parts[i + 1] === '') {
                        // '**' matches any number of path segments
                        regexString += '.*';
                        i++; // Skip the next '*'
                    } else {
                        // Single '*' matches anything except '/'
                        regexString += '[^/]*';
                    }
                }
            }
            // If the pattern ends with '**', allow anything at the end
            if (pattern.endsWith('**')) {
                regexString += '.*';
            } else if (!pattern.endsWith('*')) {
                regexString += '$';
            }
            return new RegExp(regexString);
        };

        try {
            const regex = patternToRegex(urlPattern);
            return regex.test(currentUrl);
        } catch (e) {
            console.error('Invalid URL pattern:', e);
            return false;
        }
    }

    // Modified showCurrentPageNotes function
    function showCurrentPageNotes() {
        const notes = getAllNotes();
        const currentUrl = window.location.href;
        let matchingNotes = [];

        // Collect all matching notes
        for (const urlPattern in notes) {
            if (doesUrlMatchPattern(urlPattern, currentUrl)) {
                matchingNotes.push({
                    pattern: urlPattern,
                    notes: notes[urlPattern]
                });
            }
        }

        const container = document.createElement('div');
        container.innerHTML = `
            <h3 class="modal-title">Notes for Current Page</h3>
            <div class="url-text">${currentUrl}</div>
        `;

        if (matchingNotes.length === 0) {
            container.innerHTML += '<p style="color: #6b7280;">No matching notes found for this page</p>';
        } else {
            matchingNotes.forEach(({pattern, notes: patternNotes}) => {
                const patternDiv = document.createElement('div');
                patternDiv.innerHTML = `<div class="url-text">Pattern: ${pattern}</div>`;

                patternNotes.forEach((note, index) => {
                    const noteDiv = document.createElement('div');
                    noteDiv.className = 'notes-list-item';
                    noteDiv.innerHTML = `
                        <span>${note.title}</span>
                        <button class="delete-note-button" title="Delete note">×</button>
                    `;

                    noteDiv.querySelector('span').onclick = () => {
                        container.parentElement.parentElement.remove();
                        showNoteContent(note, pattern, index);
                    };

                    noteDiv.querySelector('.delete-note-button').onclick = (e) => {
                        e.stopPropagation();
                        if (confirm('Are you sure you want to delete this note?')) {
                            deleteNote(pattern, index);
                            noteDiv.remove();
                            if (patternNotes.length === 1) {
                                patternDiv.remove();
                            }
                        }
                    };

                    patternDiv.appendChild(noteDiv);
                });

                container.appendChild(patternDiv);
            });
        }

        // Update helper text for URL patterns
        const helperText = document.createElement('div');
        helperText.style.marginTop = '20px';
        helperText.style.padding = '12px';
        helperText.style.backgroundColor = isDarkMode ? '#374151' : '#f3f4f6';
        helperText.style.borderRadius = '8px';
        helperText.style.fontSize = '14px';
        helperText.style.color = isDarkMode ? '#9ca3af' : '#6b7280';
        helperText.innerHTML = `
            <strong>URL Pattern Examples:</strong><br>
            - https://domain.com/* (matches entire domain, one level deep)<br>
            - https://domain.com/** (matches entire domain, any number of levels)<br>
            - https://domain.com/specific/* (matches specific path and one level below)<br>
            - https://domain.com/specific/** (matches specific path and any levels below)<br>
            - https://domain.com/*/specific (matches specific ending, one level in between)<br>
            - https://domain.com/**/specific (matches specific ending, any number of levels in between)
        `;
        container.appendChild(helperText);

        createModal(container);
    }

    // Modified showAllNotes function
    function showAllNotes() {
        const notes = getAllNotes();
        const container = document.createElement('div');
        container.innerHTML = '<h3 class="modal-title">All Notes</h3>';

        if (Object.keys(notes).length === 0) {
            container.innerHTML += '<p style="color: #6b7280;">No notes found</p>';
        } else {
            for (const url in notes) {
                const urlDiv = document.createElement('div');
                urlDiv.innerHTML = `<div class="url-text">${url}</div>`;

                notes[url].forEach((note, index) => {
                    const noteDiv = document.createElement('div');
                    noteDiv.className = 'notes-list-item';
                    noteDiv.innerHTML = `
                        <span>${note.title}</span>
                        <button class="delete-note-button" title="Delete note">×</button>
                    `;

                    noteDiv.querySelector('span').onclick = () => {
                        container.parentElement.parentElement.remove();
                        showNoteContent(note, url, index);
                    };

                    noteDiv.querySelector('.delete-note-button').onclick = (e) => {
                        e.stopPropagation();
                        if (confirm('Are you sure you want to delete this note?')) {
                            deleteNote(url, index);
                            noteDiv.remove();
                            if (notes[url].length === 1) {
                                urlDiv.remove();
                            }
                        }
                    };

                    urlDiv.appendChild(noteDiv);
                });

                container.appendChild(urlDiv);
            }
        }

        createModal(container);
    }

    // Add keyboard shortcut handler
    document.addEventListener('keydown', function(e) {
        if (e.ctrlKey && e.shiftKey && e.key === 'S') {
            e.preventDefault();
            showNoteForm()
        }
        if (e.ctrlKey && e.shiftKey && e.key === 'C') {
            e.preventDefault();
            showCurrentPageNotes()
        }
        if (e.ctrlKey && e.shiftKey && e.key === 'L') {
            e.preventDefault();
            showAllNotes()
        }
    });

    // Register menu commands
    GM_registerMenuCommand('New Note', () => showNoteForm());
    GM_registerMenuCommand('View Notes (Current Page)', showCurrentPageNotes);
    GM_registerMenuCommand('View All Notes', showAllNotes);
})();