skebetter-blacklist

skebetterにブラックリスト機能を追加します

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

Advertisement:

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.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @license MIT
// @name        skebetter-blacklist
// @namespace    https://skebetter.com/
// @version      1.0.0
// @description  skebetterにブラックリスト機能を追加します
// @match        https://skebetter.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // =====================
  //  オプション設定
  // =====================
  const HIDE_RETWEET_LIKE   = true; // いいね・RTを非表示にする
  const HIDE_AUTHOR_PROFILE = true; // 投稿者ページのプロフィールを非表示にする
  const HIDE_POST_TEXT      = true; // ポストの本文テキストを非表示にする

  const KEY_USERS = 'skebetter_users';
  const KEY_POSTS = 'skebetter_posts';

  function getUsers() {
    try { return JSON.parse(GM_getValue(KEY_USERS, '[]')); } catch { return []; }
  }
  function saveUsers(list) { GM_setValue(KEY_USERS, JSON.stringify(list)); }

  function getPosts() {
    try { return JSON.parse(GM_getValue(KEY_POSTS, '{}')); } catch { return {}; }
  }
  function savePosts(map) { GM_setValue(KEY_POSTS, JSON.stringify(map)); }

  function hideUser(name, url) {
    const list = getUsers();
    if (!list.some(u => u.url === url)) {
      list.push({ name, url });
      saveUsers(list);
    }
  }

  function hidePost(name, authorId, postId) {
    const map = getPosts();
    const key = `${name} (${authorId})`;
    if (!map[key]) map[key] = [];
    if (!map[key].includes(postId)) {
      map[key].push(postId);
      savePosts(map);
    }
  }

  function isUserHidden(url) {
    return getUsers().some(u => u.url === url);
  }

  function isPostHidden(authorId, postId) {
    return Object.entries(getPosts()).some(([key, ids]) => {
      const m = key.match(/\((\d+)\)$/);
      return m && m[1] === authorId && ids.includes(postId);
    });
  }

  function parseAuthorHref(href) {
    const m = (href || '').match(/\/author\/(\d+)/);
    return m ? m[1] : null;
  }
  function parsePostHref(href) {
    const m = (href || '').match(/\/author\/(\d+)\/[^/]+\/(\d+)/);
    return m ? { authorId: m[1], postId: m[2] } : null;
  }
  function parseTweetId(href) {
    const m = (href || '').match(/tweet_id=(\d+)/);
    return m ? m[1] : null;
  }
  function toAbsUrl(href) {
    if (!href) return null;
    return href.startsWith('http') ? href : 'https://skebetter.com' + href;
  }

  function findCard(el) {
    let node = el.parentElement;
    while (node && node !== document.body) {
      if (node.classList.contains('rounded-xl') &&
          (node.className.includes('bg-white') || node.className.includes('bg-gray-800'))) {
        return node;
      }
      node = node.parentElement;
    }
    return null;
  }

  function findOuterWrapper(card) {
    let node = card.parentElement;
    while (node && node !== document.body) {
      const cls = node.className || '';
      if (cls.includes('max-w-[') || cls.includes('h-card')) return node;
      node = node.parentElement;
    }
    return null;
  }

  GM_addStyle(`
    .sb-btn-row {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      padding: 6px 8px;
    }
    .sb-hide-btn {
      display: inline-flex;
      align-items: center;
      padding: 2px 8px;
      border: 1px solid rgba(128,128,128,0.4);
      border-radius: 4px;
      background: transparent;
      color: #888;
      font-size: 11px;
      line-height: 1.6;
      cursor: pointer;
      white-space: nowrap;
      transition: background 0.15s, color 0.15s, border-color 0.15s;
      user-select: none;
    }
    .sb-hide-btn:hover {
      background: rgba(192,57,43,0.9);
      border-color: #c0392b;
      color: #fff;
    }
    /* プロフィール非表示(広すぎる .md:max-w-xl は削除。JS側 hideAuthorProfile() で制御) */
    .sb-hide-profile .w-full.md\:max-w-xl.border.rounded-xl.m-2.p-4 {
      display: none !important;
    }
    /* ポスト本文テキスト非表示 */
    .sb-hide-post-text [data-sb-card="1"] span.p-2,
    .sb-hide-post-text [data-sb-card="1"] span.p-5 {
      display: none !important;
    }
    /* いいね・RT非表示(HIDE_RETWEET_LIKE=true のときCSSで制御) */
    .sb-hide-rtlike a[href*="intent/retweet"],
    .sb-hide-rtlike a[href*="intent/favorite"],
    .sb-hide-rtlike a[href*="intent/retweet"] + span,
    .sb-hide-rtlike a[href*="intent/favorite"] + span {
      display: none !important;
    }
    /* カードとouterの固定高さを解除 */
    [data-sb-card="1"],
    [data-sb-outer="1"] {
      height: auto !important;
      max-height: none !important;
      overflow: visible !important;
    }
  `);

  function processCards() {
    document.querySelectorAll('a[href*="/author/"]:not([data-sb-processed])').forEach(link => {
      link.setAttribute('data-sb-processed', '1');

      const href = link.getAttribute('href');
      const authorId = parseAuthorHref(href);
      if (!authorId) return;

      const card = findCard(link);
      if (!card || card.dataset.sbCard) return;

      let postId = null;
      card.querySelectorAll('a[href*="/author/"]').forEach(a => {
        const parsed = parsePostHref(a.getAttribute('href'));
        if (parsed && parsed.authorId === authorId) postId = parsed.postId;
      });
      if (!postId) {
        card.querySelectorAll('a[href*="tweet_id="]').forEach(a => {
          const tid = parseTweetId(a.getAttribute('href'));
          if (tid) postId = 'tweet_' + tid;
        });
      }

      const url = toAbsUrl('/author/' + authorId);
      let name = authorId;
      card.querySelectorAll(`a[href*="/author/${authorId}"]`).forEach(a => {
        const t = a.textContent.trim();
        if (t && t.length <= 50 && !a.querySelector('img')) name = t;
      });

      card.dataset.sbCard     = '1';
      card.dataset.sbUrl      = url;
      card.dataset.sbAuthorId = authorId;
      if (postId) card.dataset.sbPostId = postId;

      const outer = findOuterWrapper(card);
      if (outer) outer.dataset.sbOuter = '1';

      // ボタンをcard内の2番目の子(テキスト+画像ブロック)の末尾に追加
      const contentBlock = card.children[1] || card;

      const row = document.createElement('div');
      row.className = 'sb-btn-row';

      const userBtn = document.createElement('button');
      userBtn.className = 'sb-hide-btn';
      userBtn.textContent = 'ユーザーを非表示';
      userBtn.addEventListener('click', e => {
        e.preventDefault(); e.stopPropagation();
        hideUser(name, url);
        applyHiding();
      });
      row.appendChild(userBtn);

      if (postId) {
        const postBtn = document.createElement('button');
        postBtn.className = 'sb-hide-btn';
        postBtn.textContent = 'このポストを非表示';
        postBtn.addEventListener('click', e => {
          e.preventDefault(); e.stopPropagation();
          hidePost(name, authorId, postId);
          applyHiding();
        });
        row.appendChild(postBtn);
      }

      contentBlock.appendChild(row);
    });
  }

  function applyHiding() {
    // 広告削除
    document.querySelectorAll('.grid > *').forEach(el => {
      if (el.querySelector('a[href*="al.fanza.co.jp"]')) el.style.display = 'none';
    });

    document.querySelectorAll('[data-sb-card="1"]').forEach(card => {
      const { sbUrl, sbAuthorId, sbPostId } = card.dataset;
      if (isUserHidden(sbUrl) || (sbPostId && isPostHidden(sbAuthorId, sbPostId))) {
        const outer = findOuterWrapper(card);
        (outer || card).style.display = 'none';
      }
    });
  }

  let timer = null;
  new MutationObserver(() => {
    clearTimeout(timer);
    timer = setTimeout(() => { hideAuthorProfile(); processCards(); applyHiding(); }, 250);
  }).observe(document.body, { childList: true, subtree: true });

  function hideAuthorProfile() {
    if (!HIDE_AUTHOR_PROFILE) return;
    if (!location.pathname.startsWith('/author/')) return;
    const main = document.querySelector('main');
    if (!main) return;
    const children = [...main.children[0]?.children ?? []];
    children.forEach(el => {
      if ((el.className || '').includes('dark:bg-black')) el.style.display = 'none';
    });
  }

  function init() {
    if (HIDE_RETWEET_LIKE)   document.documentElement.classList.add('sb-hide-rtlike');
    if (HIDE_AUTHOR_PROFILE) document.documentElement.classList.add('sb-hide-profile');
    if (HIDE_POST_TEXT)      document.documentElement.classList.add('sb-hide-post-text');
    hideAuthorProfile();
    processCards();
    applyHiding();
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();