SnapTagger (Improved)

Tag and save Snapchat chats seamlessly using localStorage.

  1. // ==UserScript==
  2. // @name SnapTagger (Improved)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3 // Increment version for the change
  5. // @description Tag and save Snapchat chats seamlessly using localStorage.
  6. // @author You & Gemini
  7. // @license MIT // Added MIT License
  8. // @match https://web.snapchat.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_deleteValue // Optional: For clearing data if needed
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. // --- Constants ---
  19. const STORAGE_KEY = 'snapTaggerData'; // Key for localStorage
  20. // IMPORTANT: These selectors might change if Snapchat updates their website.
  21. // You may need to inspect the elements on web.snapchat.com and update these.
  22. const SNAP_ICON_SELECTOR = 'svg[aria-label="Snapchat"]'; // Try targeting the SVG element more specifically
  23. const CHAT_HEADER_SELECTOR = '[data-testid="conversation-header-title"]'; // Selector for the chat header element containing the friend's name
  24.  
  25. // --- State ---
  26. let uiInjected = false;
  27.  
  28. // --- Styling ---
  29. // Use GM_addStyle for CSS to keep it separate and potentially more maintainable
  30. GM_addStyle(`
  31. .snaptagger-container {
  32. position: relative;
  33. cursor: pointer;
  34. display: inline-block; /* Adjust display as needed */
  35. vertical-align: middle; /* Align with other header items */
  36. }
  37. .snaptagger-menu {
  38. position: absolute;
  39. top: 100%; /* Position below the icon */
  40. right: 0;
  41. background-color: #2f2f2f; /* Slightly lighter dark grey */
  42. border: 1px solid #555; /* Softer border */
  43. border-radius: 8px; /* Match Snapchat's rounding */
  44. padding: 12px;
  45. z-index: 10000; /* Ensure it's on top */
  46. display: none; /* Hidden by default */
  47. color: #ffffff;
  48. font-size: 14px;
  49. min-width: 180px; /* Give it some width */
  50. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); /* Add subtle shadow */
  51. font-family: "Inter", sans-serif; /* Try to match font */
  52. }
  53. .snaptagger-menu.visible {
  54. display: block;
  55. }
  56. .snaptagger-title {
  57. font-weight: 600;
  58. margin-bottom: 10px;
  59. color: #eee; /* Lighter title color */
  60. border-bottom: 1px solid #444;
  61. padding-bottom: 6px;
  62. }
  63. .snaptagger-button {
  64. display: block;
  65. width: 100%;
  66. background-color: #4a4a4a; /* Button background */
  67. color: #ffffff;
  68. border: none;
  69. padding: 8px 12px;
  70. margin-bottom: 6px;
  71. border-radius: 6px;
  72. cursor: pointer;
  73. text-align: left;
  74. font-size: 13px;
  75. transition: background-color 0.2s ease;
  76. }
  77. .snaptagger-button:hover {
  78. background-color: #5c5c5c; /* Slightly lighter on hover */
  79. }
  80. .snaptagger-button:last-child {
  81. margin-bottom: 0;
  82. }
  83. .snaptagger-feedback {
  84. font-size: 12px;
  85. color: #999;
  86. margin-top: 8px;
  87. text-align: center;
  88. min-height: 15px; /* Reserve space */
  89. }
  90. `);
  91.  
  92. // --- Utility Functions ---
  93.  
  94. /**
  95. * Gets the currently tagged data from storage.
  96. * @returns {Array<Object>} An array of tag objects or an empty array.
  97. */
  98. function getStoredTags() {
  99. // Use GM_getValue for Tampermonkey/Greasemonkey storage (more robust than raw localStorage)
  100. return GM_getValue(STORAGE_KEY, []); // Default to empty array if nothing stored
  101. }
  102.  
  103. /**
  104. * Saves the tagged data to storage.
  105. * @param {Array<Object>} tags - The array of tag objects to save.
  106. */
  107. function saveTags(tags) {
  108. GM_setValue(STORAGE_KEY, tags);
  109. }
  110.  
  111. /**
  112. * Gets the name of the currently open chat.
  113. * @returns {string | null} The chat name or null if not found.
  114. */
  115. function getCurrentChatName() {
  116. // This selector might need adjustment based on Snapchat's current structure
  117. const headerElement = document.querySelector(CHAT_HEADER_SELECTOR);
  118. return headerElement ? headerElement.textContent.trim() : null;
  119. }
  120.  
  121. /**
  122. * Shows temporary feedback message in the menu.
  123. * @param {string} message - The message to display.
  124. */
  125. function showFeedback(message) {
  126. const feedbackEl = document.getElementById('snaptagger-feedback');
  127. if (feedbackEl) {
  128. feedbackEl.textContent = message;
  129. setTimeout(() => {
  130. if (feedbackEl.textContent === message) { // Only clear if it's the same message
  131. feedbackEl.textContent = '';
  132. }
  133. }, 2500); // Clear after 2.5 seconds
  134. }
  135. }
  136.  
  137.  
  138. // --- Core Logic ---
  139.  
  140. /**
  141. * Tags the currently open chat conversation.
  142. */
  143. function tagCurrentChat() {
  144. const chatName = getCurrentChatName();
  145. if (!chatName) {
  146. showFeedback("Error: Couldn't find chat name.");
  147. console.error("SnapTagger: Could not find chat header element with selector:", CHAT_HEADER_SELECTOR);
  148. return;
  149. }
  150.  
  151. const tag = prompt(`Enter a tag for the chat with "${chatName}":`);
  152. if (tag === null || tag.trim() === '') {
  153. showFeedback("Tagging cancelled.");
  154. return; // User cancelled or entered empty tag
  155. }
  156.  
  157. const newTagEntry = {
  158. chatName: chatName,
  159. tag: tag.trim(),
  160. timestamp: new Date().toISOString(),
  161. // Future: Add specific messages here if needed
  162. // messages: getSelectedMessages() // Placeholder for more advanced functionality
  163. };
  164.  
  165. try {
  166. const currentTags = getStoredTags();
  167. currentTags.push(newTagEntry);
  168. saveTags(currentTags);
  169. console.log("SnapTagger: Chat tagged:", newTagEntry);
  170. showFeedback(`Tagged "${chatName}" as "${tag.trim()}"`);
  171. } catch (error) {
  172. console.error("SnapTagger: Error saving tag:", error);
  173. showFeedback("Error saving tag.");
  174. alert("SnapTagger Error: Could not save tag to storage. Storage might be full or disabled.\n\n" + error);
  175. }
  176. }
  177.  
  178. /**
  179. * Exports the stored tags as a JSON file.
  180. */
  181. function exportTaggedChats() {
  182. try {
  183. const tags = getStoredTags();
  184. if (tags.length === 0) {
  185. showFeedback("No tags to export.");
  186. return;
  187. }
  188.  
  189. const jsonData = JSON.stringify(tags, null, 2); // Pretty print JSON
  190. const blob = new Blob([jsonData], { type: 'application/json' });
  191. const url = URL.createObjectURL(blob);
  192.  
  193. const a = document.createElement('a');
  194. a.href = url;
  195. const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
  196. a.download = `snaptagger_export_${timestamp}.json`;
  197. document.body.appendChild(a);
  198. a.click();
  199. document.body.removeChild(a);
  200. URL.revokeObjectURL(url); // Clean up
  201.  
  202. showFeedback("Tags exported successfully!");
  203. console.log("SnapTagger: Exported tags.", tags);
  204.  
  205. } catch (error) {
  206. console.error("SnapTagger: Error exporting tags:", error);
  207. showFeedback("Error during export.");
  208. alert("SnapTagger Error: Could not export tags.\n\n" + error);
  209. }
  210. }
  211.  
  212. /**
  213. * Injects the SnapTagger UI elements into the page.
  214. * @param {Element} iconElement - The Snapchat icon element to replace/wrap.
  215. */
  216. function injectUI(iconElement) {
  217. if (uiInjected) return; // Prevent multiple injections
  218.  
  219. // --- Create Container ---
  220. const container = document.createElement('div');
  221. container.className = 'snaptagger-container';
  222.  
  223. // --- Clone Icon ---
  224. // Cloning helps maintain the original icon's appearance and any associated listeners
  225. const clonedIcon = iconElement.cloneNode(true);
  226. container.appendChild(clonedIcon);
  227.  
  228. // --- Create Dropdown Menu ---
  229. const menu = document.createElement('div');
  230. menu.className = 'snaptagger-menu'; // Use class for styling
  231. menu.id = 'snaptagger-menu'; // Add ID for easier selection
  232. menu.innerHTML = `
  233. <div class="snaptagger-title">📌 SnapTagger</div>
  234. <button id="snaptagger-tag-chat" class="snaptagger-button">Tag this chat</button>
  235. <button id="snaptagger-export-chat" class="snaptagger-button">Export tagged chats</button>
  236. <div id="snaptagger-feedback" class="snaptagger-feedback"></div>
  237. `;
  238. container.appendChild(menu);
  239.  
  240. // --- Replace Original Icon ---
  241. // Replace the original icon with our container that includes the icon and menu
  242. iconElement.parentNode.replaceChild(container, iconElement);
  243. uiInjected = true; // Mark UI as injected
  244. console.log("SnapTagger: UI Injected.");
  245.  
  246. // --- Add Event Listeners ---
  247.  
  248. // Toggle dropdown visibility
  249. container.addEventListener('click', (event) => {
  250. // Prevent clicks on buttons inside the menu from closing it immediately
  251. if (!menu.contains(event.target) || event.target === container || event.target === clonedIcon) {
  252. menu.classList.toggle('visible');
  253. }
  254. });
  255.  
  256. // Close dropdown if clicking outside
  257. document.addEventListener('click', (event) => {
  258. if (!container.contains(event.target) && menu.classList.contains('visible')) {
  259. menu.classList.remove('visible');
  260. }
  261. });
  262.  
  263. // Button actions
  264. document.getElementById('snaptagger-tag-chat').addEventListener('click', (event) => {
  265. event.stopPropagation(); // Prevent container click listener from firing
  266. tagCurrentChat();
  267. // Optionally close menu after action:
  268. // menu.classList.remove('visible');
  269. });
  270.  
  271. document.getElementById('snaptagger-export-chat').addEventListener('click', (event) => {
  272. event.stopPropagation(); // Prevent container click listener from firing
  273. exportTaggedChats();
  274. // Optionally close menu after action:
  275. menu.classList.remove('visible');
  276. });
  277. }
  278.  
  279. // --- Initialization ---
  280.  
  281. // Use MutationObserver for potentially better performance and reliability than setInterval
  282. const observer = new MutationObserver((mutationsList, obs) => {
  283. const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
  284. if (snapIcon && !uiInjected) {
  285. console.log("SnapTagger: Snapchat icon found. Injecting UI.");
  286. // Small delay to ensure surrounding elements are likely stable
  287. setTimeout(() => injectUI(snapIcon), 500);
  288. obs.disconnect(); // Stop observing once the icon is found and UI is injected
  289. }
  290. // Add a timeout safeguard in case the observer fails or the element never appears
  291. // setTimeout(() => {
  292. // if (!uiInjected) {
  293. // console.warn("SnapTagger: Timed out waiting for icon. Script might not work.");
  294. // obs.disconnect();
  295. // }
  296. // }, 15000); // Stop trying after 15 seconds
  297. });
  298.  
  299. // Start observing the document body for added nodes
  300. console.log("SnapTagger: Initializing observer...");
  301. observer.observe(document.body, { childList: true, subtree: true });
  302.  
  303. // Fallback using setInterval (less ideal but can work)
  304. // const checkInterval = 2000; // Check every 2 seconds
  305. // const maxAttempts = 10; // Try for 20 seconds
  306. // let attempts = 0;
  307. // const fallbackInterval = setInterval(() => {
  308. // if (uiInjected) {
  309. // clearInterval(fallbackInterval);
  310. // return;
  311. // }
  312. // attempts++;
  313. // const snapIcon = document.querySelector(SNAP_ICON_SELECTOR);
  314. // if (snapIcon) {
  315. // console.log("SnapTagger (Fallback): Snapchat icon found. Injecting UI.");
  316. // clearInterval(fallbackInterval);
  317. // injectUI(snapIcon);
  318. // } else if (attempts >= maxAttempts) {
  319. // clearInterval(fallbackInterval);
  320. // console.warn(`SnapTagger (Fallback): Could not find Snapchat icon (${SNAP_ICON_SELECTOR}) after ${maxAttempts} attempts. Script may not work.`);
  321. // }
  322. // }, checkInterval);
  323.  
  324.  
  325. })();

QingJ © 2025

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