Sorts posts by attachment count.
// ==UserScript==
// @name Pawchive Sort by Attachment Count
// @namespace http://tampermonkey.net/
// @version 15.1
// @description Sorts posts by attachment count.
// @author BlueAnivia
// @match *://kemono.cr/*
// @match *://kemono.su/*
// @match *://kemono.party/*
// @match *://coomer.su/*
// @match *://coomer.st/*
// @match *://coomer.party/*
// @match *://pawchive.pw/*
// @match *://pawchive.st/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=pawchive.pw
// @license MIT
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
'use strict';
if (window.top !== window.self) return;
if (window.__attachmentSorterLoaded) return;
window.__attachmentSorterLoaded = true;
// --- Config ---
const SORT_ENABLED_KEY = 'kemono_ksort_enabled';
const LIST_SELECTOR = '.card-list__items';
const DEBOUNCE_MS = 150;
// --- Persisted state ---
const store = {
get(key, fallback) {
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
},
set(key, value) {
try { localStorage.setItem(key, value); } catch { /* ignore */ }
}
};
let isSorted = store.get(SORT_ENABLED_KEY, 'false') === 'true';
// --- Count extraction ---
const countCache = new WeakMap();
function getCount(card) {
const cached = countCache.get(card);
if (cached !== undefined && cached > 0) return cached;
const scope = card.querySelector('footer, .post-card__footer') || card;
const match = (scope.textContent || '').match(/(\d+)\s*attachments?/i);
const count = match ? parseInt(match[1], 10) : 0;
countCache.set(card, count);
return count;
}
// --- Original Order Tracking ---
let globalCardCounter = 0;
function ensureIndexed(cards) {
cards.forEach(card => {
if (!card.dataset.ksortIdx) {
card.dataset.ksortIdx = globalCardCounter++;
}
});
}
// --- Core sort ---
function getList() { return document.querySelector(LIST_SELECTOR); }
function getCards(list) { return Array.from(list.children).filter(el => el.tagName === 'ARTICLE'); }
function compare(a, b) {
if (isSorted) {
const diff = getCount(b) - getCount(a);
if (diff !== 0) return diff;
}
return parseInt(a.dataset.ksortIdx, 10) - parseInt(b.dataset.ksortIdx, 10);
}
function applySort(force = false) {
if (location.pathname.includes('/post/')) return false;
const list = getList();
if (!list) return false;
const cards = getCards(list);
if (cards.length < 2) return false;
ensureIndexed(cards);
if (!force) {
let alreadySorted = true;
for (let i = 0; i < cards.length - 1; i++) {
if (compare(cards[i], cards[i + 1]) > 0) { alreadySorted = false; break; }
}
if (alreadySorted) return false;
}
const anchor = cards[cards.length - 1].nextSibling;
cards.sort(compare);
const fragment = document.createDocumentFragment();
cards.forEach(card => fragment.appendChild(card));
list.insertBefore(fragment, anchor);
observer.takeRecords();
return true;
}
// --- Bulletproof UI Injection ---
// Inject styles once
if (!document.getElementById('ksort-style')) {
const style = document.createElement('style');
style.id = 'ksort-style';
style.textContent = `
#ksort-ui {
position: fixed;
top: 16px;
right: 100px;
z-index: 999999;
background: rgba(30, 32, 36, 0.9);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 14px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
font-family: system-ui, -apple-system, sans-serif;
color: #eee;
user-select: none;
}
#ksort-ui.ksort-hidden {
display: none !important;
}
.ksort-group { display: flex; align-items: center; gap: 8px; cursor: pointer; margin: 0; }
.ksort-label { font-size: 14px; font-weight: 600; cursor: pointer; }
.ksort-switch { position: relative; display: inline-block; width: 36px; height: 20px; }
.ksort-switch input { opacity: 0; width: 0; height: 0; }
.ksort-slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #555; transition: .3s; border-radius: 20px;
}
.ksort-slider:before {
position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px;
background-color: white; transition: .3s; border-radius: 50%;
}
.ksort-switch input:checked + .ksort-slider { background-color: #e67e22; }
.ksort-switch input:checked + .ksort-slider:before { transform: translateX(16px); }
`;
document.head.appendChild(style);
}
// Always fetch or recreate the UI to ensure we interact with the live DOM
function ensureUI() {
let ui = document.getElementById('ksort-ui');
if (!ui) {
ui = document.createElement('div');
ui.id = 'ksort-ui';
ui.className = 'ksort-hidden';
ui.innerHTML = `
<label class="ksort-group" title="Toggle sorting highest to lowest">
<div class="ksort-switch">
<input type="checkbox" id="ksort-toggle">
<span class="ksort-slider"></span>
</div>
<span class="ksort-label">Sort by Count</span>
</label>
`;
// Append to body (or documentElement as fallback)
(document.body || document.documentElement).appendChild(ui);
}
// Sync checkbox visually with data state
const toggle = document.getElementById('ksort-toggle');
if (toggle) toggle.checked = isSorted;
return ui;
}
function updateVisibility() {
const ui = ensureUI();
if (location.pathname.includes('/post/')) {
ui.classList.add('ksort-hidden');
return;
}
const list = getList();
const cards = list ? getCards(list) : [];
const hasAttachments = cards.some(card => getCount(card) > 0);
const visible = cards.length >= 2 && hasAttachments;
ui.classList.toggle('ksort-hidden', !visible);
}
// --- Global Event Delegation ---
document.addEventListener('change', e => {
if (e.target && e.target.id === 'ksort-toggle') {
isSorted = e.target.checked;
store.set(SORT_ENABLED_KEY, String(isSorted));
applySort(true);
}
});
// --- Event Observers ---
let debounceTimer = null;
const observer = new MutationObserver(records => {
const ui = document.getElementById('ksort-ui');
if (ui && records.every(r => ui.contains(r.target))) return;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
updateVisibility();
applySort(false);
}, DEBOUNCE_MS);
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// --- SPA Navigation Handling ---
function onNavigate() {
globalCardCounter = 0;
updateVisibility();
setTimeout(() => {
updateVisibility();
applySort(false);
}, 300);
}
for (const method of ['pushState', 'replaceState']) {
const original = history[method];
history[method] = function (...args) {
const result = original.apply(this, args);
onNavigate();
return result;
};
}
window.addEventListener('popstate', onNavigate);
window.addEventListener('pageshow', onNavigate);
updateVisibility();
applySort(false);
})();