SnapTagger (Improved)

Tag and save Snapchat chats seamlessly using localStorage.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         SnapTagger (Improved)
// @namespace    http://tampermonkey.net/
// @version      0.3 // Increment version for the change
// @description  Tag and save Snapchat chats seamlessly using localStorage.
// @author       You & Gemini
// @license      MIT // Added MIT License
// @match        https://web.snapchat.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue // Optional: For clearing data if needed
// ==/UserScript==

(function () {
    'use strict';

    // --- Constants ---
    const STORAGE_KEY = 'snapTaggerData'; // Key for localStorage
    // IMPORTANT: These selectors might change if Snapchat updates their website.
    // You may need to inspect the elements on web.snapchat.com and update these.
    const SNAP_ICON_SELECTOR = 'svg[aria-label="Snapchat"]'; // Try targeting the SVG element more specifically
    const CHAT_HEADER_SELECTOR = '[data-testid="conversation-header-title"]'; // Selector for the chat header element containing the friend's name

    // --- State ---
    let uiInjected = false;

    // --- Styling ---
    // Use GM_addStyle for CSS to keep it separate and potentially more maintainable
    GM_addStyle(`
        .snaptagger-container {
            position: relative;
            cursor: pointer;
            display: inline-block; /* Adjust display as needed */
            vertical-align: middle; /* Align with other header items */
        }
        .snaptagger-menu {
            position: absolute;
            top: 100%; /* Position below the icon */
            right: 0;
            background-color: #2f2f2f; /* Slightly lighter dark grey */
            border: 1px solid #555; /* Softer border */
            border-radius: 8px; /* Match Snapchat's rounding */
            padding: 12px;
            z-index: 10000; /* Ensure it's on top */
            display: none; /* Hidden by default */
            color: #ffffff;
            font-size: 14px;
            min-width: 180px; /* Give it some width */
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); /* Add subtle shadow */
            font-family: "Inter", sans-serif; /* Try to match font */
        }
        .snaptagger-menu.visible {
            display: block;
        }
        .snaptagger-title {
            font-weight: 600;
            margin-bottom: 10px;
            color: #eee; /* Lighter title color */
            border-bottom: 1px solid #444;
            padding-bottom: 6px;
        }
        .snaptagger-button {
            display: block;
            width: 100%;
            background-color: #4a4a4a; /* Button background */
            color: #ffffff;
            border: none;
            padding: 8px 12px;
            margin-bottom: 6px;
            border-radius: 6px;
            cursor: pointer;
            text-align: left;
            font-size: 13px;
            transition: background-color 0.2s ease;
        }
        .snaptagger-button:hover {
            background-color: #5c5c5c; /* Slightly lighter on hover */
        }
        .snaptagger-button:last-child {
            margin-bottom: 0;
        }
        .snaptagger-feedback {
            font-size: 12px;
            color: #999;
            margin-top: 8px;
            text-align: center;
            min-height: 15px; /* Reserve space */
        }
    `);

    // --- Utility Functions ---

    /**
     * Gets the currently tagged data from storage.
     * @returns {Array<Object>} An array of tag objects or an empty array.
     */
    function getStoredTags() {
        // Use GM_getValue for Tampermonkey/Greasemonkey storage (more robust than raw localStorage)
        return GM_getValue(STORAGE_KEY, []); // Default to empty array if nothing stored
    }

    /**
     * Saves the tagged data to storage.
     * @param {Array<Object>} tags - The array of tag objects to save.
     */
    function saveTags(tags) {
        GM_setValue(STORAGE_KEY, tags);
    }

    /**
     * Gets the name of the currently open chat.
     * @returns {string | null} The chat name or null if not found.
     */
    function getCurrentChatName() {
        // This selector might need adjustment based on Snapchat's current structure
        const headerElement = document.querySelector(CHAT_HEADER_SELECTOR);
        return headerElement ? headerElement.textContent.trim() : null;
    }

    /**
     * Shows temporary feedback message in the menu.
     * @param {string} message - The message to display.
     */
    function showFeedback(message) {
        const feedbackEl = document.getElementById('snaptagger-feedback');
        if (feedbackEl) {
            feedbackEl.textContent = message;
            setTimeout(() => {
                if (feedbackEl.textContent === message) { // Only clear if it's the same message
                     feedbackEl.textContent = '';
                }
            }, 2500); // Clear after 2.5 seconds
        }
    }


    // --- Core Logic ---

    /**
     * Tags the currently open chat conversation.
     */
    function tagCurrentChat() {
        const chatName = getCurrentChatName();
        if (!chatName) {
            showFeedback("Error: Couldn't find chat name.");
            console.error("SnapTagger: Could not find chat header element with selector:", CHAT_HEADER_SELECTOR);
            return;
        }

        const tag = prompt(`Enter a tag for the chat with "${chatName}":`);
        if (tag === null || tag.trim() === '') {
            showFeedback("Tagging cancelled.");
            return; // User cancelled or entered empty tag
        }

        const newTagEntry = {
            chatName: chatName,
            tag: tag.trim(),
            timestamp: new Date().toISOString(),
            // Future: Add specific messages here if needed
            // messages: getSelectedMessages() // Placeholder for more advanced functionality
        };

        try {
            const currentTags = getStoredTags();
            currentTags.push(newTagEntry);
            saveTags(currentTags);
            console.log("SnapTagger: Chat tagged:", newTagEntry);
            showFeedback(`Tagged "${chatName}" as "${tag.trim()}"`);
        } catch (error) {
            console.error("SnapTagger: Error saving tag:", error);
            showFeedback("Error saving tag.");
            alert("SnapTagger Error: Could not save tag to storage. Storage might be full or disabled.\n\n" + error);
        }
    }

    /**
     * Exports the stored tags as a JSON file.
     */
    function exportTaggedChats() {
        try {
            const tags = getStoredTags();
            if (tags.length === 0) {
                showFeedback("No tags to export.");
                return;
            }

            const jsonData = JSON.stringify(tags, null, 2); // Pretty print JSON
            const blob = new Blob([jsonData], { type: 'application/json' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
            a.download = `snaptagger_export_${timestamp}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url); // Clean up

            showFeedback("Tags exported successfully!");
            console.log("SnapTagger: Exported tags.", tags);

        } catch (error) {
            console.error("SnapTagger: Error exporting tags:", error);
             showFeedback("Error during export.");
            alert("SnapTagger Error: Could not export tags.\n\n" + error);
        }
    }

    /**
     * Injects the SnapTagger UI elements into the page.
     * @param {Element} iconElement - The Snapchat icon element to replace/wrap.
     */
    function injectUI(iconElement) {
        if (uiInjected) return; // Prevent multiple injections

        // --- Create Container ---
        const container = document.createElement('div');
        container.className = 'snaptagger-container';

        // --- Clone Icon ---
        // Cloning helps maintain the original icon's appearance and any associated listeners
        const clonedIcon = iconElement.cloneNode(true);
        container.appendChild(clonedIcon);

        // --- Create Dropdown Menu ---
        const menu = document.createElement('div');
        menu.className = 'snaptagger-menu'; // Use class for styling
        menu.id = 'snaptagger-menu'; // Add ID for easier selection
        menu.innerHTML = `
            <div class="snaptagger-title">📌 SnapTagger</div>
            <button id="snaptagger-tag-chat" class="snaptagger-button">Tag this chat</button>
            <button id="snaptagger-export-chat" class="snaptagger-button">Export tagged chats</button>
            <div id="snaptagger-feedback" class="snaptagger-feedback"></div>
        `;
        container.appendChild(menu);

        // --- Replace Original Icon ---
        // Replace the original icon with our container that includes the icon and menu
        iconElement.parentNode.replaceChild(container, iconElement);
        uiInjected = true; // Mark UI as injected
        console.log("SnapTagger: UI Injected.");

        // --- Add Event Listeners ---

        // Toggle dropdown visibility
        container.addEventListener('click', (event) => {
            // Prevent clicks on buttons inside the menu from closing it immediately
            if (!menu.contains(event.target) || event.target === container || event.target === clonedIcon) {
                 menu.classList.toggle('visible');
            }
        });

        // Close dropdown if clicking outside
        document.addEventListener('click', (event) => {
            if (!container.contains(event.target) && menu.classList.contains('visible')) {
                menu.classList.remove('visible');
            }
        });

        // Button actions
        document.getElementById('snaptagger-tag-chat').addEventListener('click', (event) => {
            event.stopPropagation(); // Prevent container click listener from firing
            tagCurrentChat();
            // Optionally close menu after action:
            // menu.classList.remove('visible');
        });

        document.getElementById('snaptagger-export-chat').addEventListener('click', (event) => {
            event.stopPropagation(); // Prevent container click listener from firing
            exportTaggedChats();
            // Optionally close menu after action:
             menu.classList.remove('visible');
        });
    }

    // --- Initialization ---

    // Use MutationObserver for potentially better performance and reliability than setInterval
    const observer = new MutationObserver((mutationsList, obs) => {
        const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
        if (snapIcon && !uiInjected) {
            console.log("SnapTagger: Snapchat icon found. Injecting UI.");
            // Small delay to ensure surrounding elements are likely stable
            setTimeout(() => injectUI(snapIcon), 500);
            obs.disconnect(); // Stop observing once the icon is found and UI is injected
        }
        // Add a timeout safeguard in case the observer fails or the element never appears
        // setTimeout(() => {
        //     if (!uiInjected) {
        //         console.warn("SnapTagger: Timed out waiting for icon. Script might not work.");
        //         obs.disconnect();
        //     }
        // }, 15000); // Stop trying after 15 seconds
    });

    // Start observing the document body for added nodes
    console.log("SnapTagger: Initializing observer...");
    observer.observe(document.body, { childList: true, subtree: true });

    // Fallback using setInterval (less ideal but can work)
    // const checkInterval = 2000; // Check every 2 seconds
    // const maxAttempts = 10; // Try for 20 seconds
    // let attempts = 0;
    // const fallbackInterval = setInterval(() => {
    //     if (uiInjected) {
    //         clearInterval(fallbackInterval);
    //         return;
    //     }
    //     attempts++;
    //     const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
    //     if (snapIcon) {
    //         console.log("SnapTagger (Fallback): Snapchat icon found. Injecting UI.");
    //         clearInterval(fallbackInterval);
    //         injectUI(snapIcon);
    //     } else if (attempts >= maxAttempts) {
    //         clearInterval(fallbackInterval);
    //         console.warn(`SnapTagger (Fallback): Could not find Snapchat icon (${SNAP_ICON_SELECTOR}) after ${maxAttempts} attempts. Script may not work.`);
    //     }
    // }, checkInterval);


})();