Sort Pinterest pins by number of saves in descending order
Size
14.9 KB
Version
1.1.1
Created
Apr 8, 2026
Updated
9 days ago
1// ==UserScript==
2// @name Pinterest Pin Sorter by Saves
3// @description Sort Pinterest pins by number of saves in descending order
4// @version 1.1.1
5// @match https://*.pinterest.com/*
6// @icon https://s.pinimg.com/webapp/favicon_48x48-7470a30d.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Pinterest Pin Sorter by Saves - Extension loaded');
12
13 let sortingInProgress = false;
14 let lastSortTime = 0;
15 const SORT_DEBOUNCE_MS = 1000;
16
17 // Debounce function to prevent excessive sorting
18 function debounce(func, wait) {
19 let timeout;
20 return function executedFunction(...args) {
21 const later = () => {
22 clearTimeout(timeout);
23 func(...args);
24 };
25 clearTimeout(timeout);
26 timeout = setTimeout(later, wait);
27 };
28 }
29
30 // Parse save count from text (handles K, M, Y suffixes)
31 function parseSaveCount(text) {
32 if (!text) return 0;
33
34 text = text.trim().toUpperCase();
35 const match = text.match(/(\d+(?:\.\d+)?)\s*([KMY])?/);
36
37 if (!match) return 0;
38
39 let number = parseFloat(match[1]);
40 const suffix = match[2];
41
42 if (suffix === 'K') number *= 1000;
43 else if (suffix === 'M') number *= 1000000;
44 else if (suffix === 'Y') number *= 1000000000;
45
46 return number;
47 }
48
49 // Extract save count from a pin element
50 function getSaveCount(pinElement) {
51 try {
52 // Method 1: Check for pin-stats (from another extension)
53 const pinStats = pinElement.querySelector('.pin-stats');
54 if (pinStats) {
55 const firstStatItem = pinStats.querySelector('.stat-item span:not(.hover-text)');
56 if (firstStatItem) {
57 const count = parseSaveCount(firstStatItem.textContent);
58 console.log('Found save count from pin-stats:', count);
59 return count;
60 }
61 }
62
63 // Method 2: Look for save button with count
64 const saveButtons = pinElement.querySelectorAll('button[aria-label*="save" i], button[aria-label*="Save"]');
65 for (const button of saveButtons) {
66 const label = button.getAttribute('aria-label') || '';
67 const match = label.match(/(\d+[kKmM]?)\s*save/i);
68 if (match) {
69 const count = parseSaveCount(match[1]);
70 console.log('Found save count from button:', count);
71 return count;
72 }
73 }
74
75 // Method 3: Look for any text with "saves" pattern
76 const allText = pinElement.textContent;
77 const saveMatch = allText.match(/(\d+[kKmM]?)\s*saves?/i);
78 if (saveMatch) {
79 const count = parseSaveCount(saveMatch[1]);
80 console.log('Found save count from text:', count);
81 return count;
82 }
83
84 console.log('No save count found for pin');
85 return 0;
86 } catch (error) {
87 console.error('Error extracting save count:', error);
88 return 0;
89 }
90 }
91
92 // Sort pins by save count
93 function sortPins() {
94 if (sortingInProgress) {
95 console.log('Sorting already in progress, skipping...');
96 return;
97 }
98
99 const now = Date.now();
100 if (now - lastSortTime < SORT_DEBOUNCE_MS) {
101 console.log('Sorting too soon after last sort, skipping...');
102 return;
103 }
104
105 sortingInProgress = true;
106 lastSortTime = now;
107
108 try {
109 console.log('Starting pin sorting...');
110
111 // Find the masonry container
112 const masonryContainer = document.querySelector('[data-test-id="masonry-container"]');
113 if (!masonryContainer) {
114 console.log('Masonry container not found');
115 sortingInProgress = false;
116 return;
117 }
118
119 // Find all pin list items
120 const pinListItems = Array.from(masonryContainer.querySelectorAll('[data-grid-item="true"]'));
121
122 if (pinListItems.length === 0) {
123 console.log('No pins found to sort');
124 sortingInProgress = false;
125 return;
126 }
127
128 console.log(`Found ${pinListItems.length} pins to sort`);
129
130 // Extract save counts and create sortable array
131 const pinsWithCounts = pinListItems.map(pinItem => {
132 const pinElement = pinItem.querySelector('[data-test-id="pin"]');
133 const saveCount = getSaveCount(pinItem);
134 return {
135 element: pinItem,
136 saveCount: saveCount,
137 pinId: pinElement?.getAttribute('data-test-pin-id') || 'unknown'
138 };
139 });
140
141 // Sort by save count (descending)
142 pinsWithCounts.sort((a, b) => b.saveCount - a.saveCount);
143
144 console.log('Sorted pins by save count:');
145 pinsWithCounts.forEach((pin, index) => {
146 console.log(` ${index + 1}. Pin ${pin.pinId}: ${pin.saveCount} saves`);
147 });
148
149 // Find the parent container that holds all pins
150 const pinListContainer = masonryContainer.querySelector('[role="list"]');
151 if (!pinListContainer) {
152 console.log('Pin list container not found');
153 sortingInProgress = false;
154 return;
155 }
156
157 // Reorder the DOM elements
158 pinsWithCounts.forEach(pin => {
159 pinListContainer.appendChild(pin.element);
160 });
161
162 console.log('Pins sorted successfully!');
163
164 // Add visual indicator
165 addSortIndicator(pinsWithCounts.length);
166
167 } catch (error) {
168 console.error('Error sorting pins:', error);
169 } finally {
170 sortingInProgress = false;
171 }
172 }
173
174 // Add a visual indicator showing pins are sorted
175 function addSortIndicator(pinCount) {
176 // Remove existing indicator
177 const existing = document.getElementById('pin-sort-indicator');
178 if (existing) existing.remove();
179
180 const indicator = document.createElement('div');
181 indicator.id = 'pin-sort-indicator';
182 indicator.textContent = `✓ Sorted ${pinCount} pins by saves`;
183 indicator.style.cssText = `
184 position: fixed;
185 top: 80px;
186 right: 20px;
187 background: linear-gradient(135deg, #E60023 0%, #C5001A 100%);
188 color: white;
189 padding: 12px 20px;
190 border-radius: 24px;
191 font-size: 14px;
192 font-weight: 600;
193 box-shadow: 0 4px 12px rgba(230, 0, 35, 0.3);
194 z-index: 10000;
195 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
196 animation: slideIn 0.3s ease-out;
197 `;
198
199 // Add animation
200 const style = document.createElement('style');
201 style.textContent = `
202 @keyframes slideIn {
203 from {
204 transform: translateX(400px);
205 opacity: 0;
206 }
207 to {
208 transform: translateX(0);
209 opacity: 1;
210 }
211 }
212 `;
213 document.head.appendChild(style);
214
215 document.body.appendChild(indicator);
216
217 // Remove after 3 seconds
218 setTimeout(() => {
219 indicator.style.transition = 'opacity 0.3s ease-out';
220 indicator.style.opacity = '0';
221 setTimeout(() => indicator.remove(), 300);
222 }, 3000);
223 }
224
225 // Debounced sort function
226 const debouncedSort = debounce(sortPins, SORT_DEBOUNCE_MS);
227
228 // Auto-scroll function to load more pins
229 async function autoScrollAndSort(scrollCount = 10) {
230 console.log(`Starting auto-scroll: will scroll ${scrollCount} times to load more pins...`);
231
232 // Show loading indicator
233 showLoadingIndicator();
234
235 let scrollsCompleted = 0;
236 const scrollDelay = 1500; // Wait 1.5 seconds between scrolls to allow pins to load
237
238 return new Promise((resolve) => {
239 const scrollInterval = setInterval(() => {
240 // Scroll down by viewport height
241 window.scrollBy({
242 top: window.innerHeight,
243 behavior: 'smooth'
244 });
245
246 scrollsCompleted++;
247 console.log(`Scroll ${scrollsCompleted}/${scrollCount} completed`);
248
249 // Update loading indicator
250 updateLoadingIndicator(scrollsCompleted, scrollCount);
251
252 if (scrollsCompleted >= scrollCount) {
253 clearInterval(scrollInterval);
254 console.log('Auto-scroll completed, waiting for final pins to load...');
255
256 // Wait a bit more for the last batch of pins to load
257 setTimeout(() => {
258 hideLoadingIndicator();
259 console.log('Starting final sort...');
260 sortPins();
261 resolve();
262 }, 2000);
263 }
264 }, scrollDelay);
265 });
266 }
267
268 // Show loading indicator during auto-scroll
269 function showLoadingIndicator() {
270 const indicator = document.createElement('div');
271 indicator.id = 'pin-scroll-indicator';
272 indicator.innerHTML = `
273 <div style="margin-bottom: 8px;">🔄 Loading more pins...</div>
274 <div id="scroll-progress" style="font-size: 12px; opacity: 0.9;">Scroll 0/10</div>
275 `;
276 indicator.style.cssText = `
277 position: fixed;
278 top: 80px;
279 right: 20px;
280 background: linear-gradient(135deg, #E60023 0%, #C5001A 100%);
281 color: white;
282 padding: 16px 24px;
283 border-radius: 24px;
284 font-size: 14px;
285 font-weight: 600;
286 box-shadow: 0 4px 12px rgba(230, 0, 35, 0.3);
287 z-index: 10000;
288 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
289 animation: slideIn 0.3s ease-out;
290 `;
291 document.body.appendChild(indicator);
292 }
293
294 // Update loading indicator progress
295 function updateLoadingIndicator(current, total) {
296 const progressElement = document.getElementById('scroll-progress');
297 if (progressElement) {
298 progressElement.textContent = `Scroll ${current}/${total}`;
299 }
300 }
301
302 // Hide loading indicator
303 function hideLoadingIndicator() {
304 const indicator = document.getElementById('pin-scroll-indicator');
305 if (indicator) {
306 indicator.style.transition = 'opacity 0.3s ease-out';
307 indicator.style.opacity = '0';
308 setTimeout(() => indicator.remove(), 300);
309 }
310 }
311
312 // Observe DOM changes to detect when new pins are loaded
313 function observePinterest() {
314 const observer = new MutationObserver(debounce((mutations) => {
315 // Check if pins were added or modified
316 const hasPinChanges = mutations.some(mutation => {
317 return Array.from(mutation.addedNodes).some(node => {
318 if (node.nodeType === 1) {
319 return node.matches('[data-grid-item="true"]') ||
320 node.querySelector('[data-grid-item="true"]') ||
321 node.matches('.pin-stats') ||
322 node.querySelector('.pin-stats');
323 }
324 return false;
325 });
326 });
327
328 if (hasPinChanges) {
329 console.log('New pins detected, scheduling sort...');
330 debouncedSort();
331 }
332 }, 500));
333
334 // Observe the entire document for changes
335 observer.observe(document.body, {
336 childList: true,
337 subtree: true
338 });
339
340 console.log('Pinterest observer started');
341 }
342
343 // Initialize the extension
344 function init() {
345 console.log('Initializing Pinterest Pin Sorter...');
346
347 // Wait for the page to be ready
348 if (document.readyState === 'loading') {
349 document.addEventListener('DOMContentLoaded', init);
350 return;
351 }
352
353 // Wait a bit for Pinterest to load, then add the activation button
354 setTimeout(() => {
355 addActivationButton();
356 observePinterest();
357 }, 2000);
358
359 // Also sort on scroll (when new pins load)
360 let scrollTimeout;
361 window.addEventListener('scroll', () => {
362 clearTimeout(scrollTimeout);
363 scrollTimeout = setTimeout(() => {
364 debouncedSort();
365 }, 1000);
366 });
367
368 console.log('Pinterest Pin Sorter initialized');
369 }
370
371 // Add activation button to the page
372 function addActivationButton() {
373 // Remove existing button if any
374 const existing = document.getElementById('pin-sorter-button');
375 if (existing) existing.remove();
376
377 const button = document.createElement('button');
378 button.id = 'pin-sorter-button';
379 button.innerHTML = '📊 Sort Pins by Saves';
380 button.style.cssText = `
381 position: fixed;
382 top: 80px;
383 right: 20px;
384 background: linear-gradient(135deg, #E60023 0%, #C5001A 100%);
385 color: white;
386 padding: 12px 20px;
387 border: none;
388 border-radius: 24px;
389 font-size: 14px;
390 font-weight: 600;
391 box-shadow: 0 4px 12px rgba(230, 0, 35, 0.3);
392 z-index: 10000;
393 cursor: pointer;
394 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
395 transition: transform 0.2s, box-shadow 0.2s;
396 `;
397
398 // Add hover effect
399 button.addEventListener('mouseenter', () => {
400 button.style.transform = 'translateY(-2px)';
401 button.style.boxShadow = '0 6px 16px rgba(230, 0, 35, 0.4)';
402 });
403
404 button.addEventListener('mouseleave', () => {
405 button.style.transform = 'translateY(0)';
406 button.style.boxShadow = '0 4px 12px rgba(230, 0, 35, 0.3)';
407 });
408
409 // Add click handler
410 button.addEventListener('click', () => {
411 button.remove();
412 autoScrollAndSort(10);
413 });
414
415 document.body.appendChild(button);
416 console.log('Activation button added');
417 }
418
419 // Start the extension
420 init();
421
422})();