skebetter-blacklist

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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();
  }

})();