JM Shelf - Init

入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。

Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.sleazyfork.org/scripts/581113/1842615/JM%20Shelf%20-%20Init.js

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         JM Shelf - Init
// @namespace    jmshelf-lib
// @version      1.0.0
// @author       Kesdi
// @description  入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
// @license      MIT
// ==/UserScript==
// 
// 此文件是 GreasyFork 库(library),不直接安装。
// 请安装主脚本: JM Shelf 给杂鱼的个性化推荐
//

// ═══ [15] INIT ═══
  // ============================================================

  // ═══ 网络拦截: 监控收藏/爱心 XHR ═══
  (function() {
    const origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(body) {
      const url = this._url || '';
      if (url.includes('/ajax/favorite_album') || url.includes('/ajax/delete_favorite_album') ||
          url.includes('/ajax/love_album') || url.includes('/ajax/unlove_album')) {
        try {
          const formData = new URLSearchParams(body);
          const albumId = formData.get('album_id');
          if (albumId) {
            if (url.includes('delete_favorite_album')) {
              LOG.info(`📌 网络监测: 取消收藏 album ${albumId}`);
              const favs = State.getFavorites();
              const idx = favs.findIndex(f => String(f.id) === String(albumId));
              if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
            } else if (url.includes('favorite_album')) {
              LOG.info(`📌 网络监测: 收藏 album ${albumId}`);
              const favs = State.getFavorites();
              if (!favs.find(f => String(f.id) === String(albumId))) {
                const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
                favs.push({ ...albumData, id: String(albumId) });
                State.saveFavorites(favs);
                showNotification('📌 已收藏!(network)');
              }
              try { _insertTitleToTrie(_initTrie(), albumId, albumData.title || '', 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } catch(e) {}
            } else if (url.includes('unlove_album')) {
              LOG.info(`❤ 网络监测: 取消喜欢 album ${albumId}`);
              const liked = State.getLikedAlbums();
              const idx = liked.findIndex(f => String(f.id) === String(albumId));
              if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
            } else if (url.includes('love_album')) {
              LOG.info(`❤ 网络监测: 喜欢 album ${albumId}`);
              State.addLikedAlbum(albumId);
              showNotification('❤️ 已记录喜欢!');
            }
          }
        } catch(e) {}
      }
      return origSend.apply(this, arguments);
    };
    const origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
      this._url = url;
      return origOpen.apply(this, arguments);
    };
  })();

  // ═══ fetch拦截: 兜底监控 ═══
  (function() {
    const origFetch = window.fetch;
    window.fetch = function(url, options) {
      const urlStr = typeof url === 'string' ? url : (url.url || '');
      const method = (options && options.method) || 'GET';
      if (method === 'POST' && (urlStr.includes('/ajax/favorite_album') || urlStr.includes('/ajax/delete_favorite_album') ||
          urlStr.includes('/ajax/love_album') || urlStr.includes('/ajax/unlove_album'))) {
        const body = (options && options.body) ? new URLSearchParams(options.body) : new URLSearchParams();
        const albumId = body.get('album_id');
        if (albumId) {
          if (urlStr.includes('delete_favorite_album')) {
            const favs = State.getFavorites();
            const idx = favs.findIndex(f => String(f.id) === String(albumId));
            if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
          } else if (urlStr.includes('favorite_album')) {
            const favs = State.getFavorites();
            if (!favs.find(f => String(f.id) === String(albumId))) {
              const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
              favs.push({ ...albumData, id: String(albumId) });
              State.saveFavorites(favs);
              showNotification('📌 已收藏!(network)');
            }
            try { const ad = State.getAlbumCache()[String(albumId)]; if (ad && ad.title) { _insertTitleToTrie(_initTrie(), albumId, ad.title, 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } } catch(e) {}
          } else if (urlStr.includes('unlove_album')) {
            const liked = State.getLikedAlbums();
            const idx = liked.findIndex(f => String(f.id) === String(albumId));
            if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
          } else if (urlStr.includes('love_album')) {
            State.addLikedAlbum(albumId);
            showNotification('❤️ 已记录喜欢!');
          }
        }
      }
      return origFetch.apply(this, arguments);
    };
  })();

  // 全局click监听: 喜欢按钮
  let _currentAlbumData = null;
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('[id^="love_likes_"], [id^="love_heart_"], [id^="favorite_album_"]');
    if (!btn) return;
    const btnId = btn.id || '';
    const isLike = btnId.startsWith('love_likes_') || btnId.startsWith('love_heart_');
    const isFav = btnId.startsWith('favorite_album_');
    const idMatch = btnId.match(/\d+$/);
    const albumId = idMatch ? idMatch[0] : null;
    if (!albumId) return;
    if (!_currentAlbumData) { _currentAlbumData = { id: albumId, title: '', tags: [], authors: [] }; }

    if (isLike) {
      LOG.info(`❤ 检测到喜欢: album ${albumId}`);
      const oldProfile = State.getProfile();
      const liked = State.getLikedAlbums();
      const likedEntry = liked.find(f => String(f.id) === String(albumId));
      const daysSinceLiked = likedEntry ? (Date.now() - (likedEntry.likedAt||0)) / 86400000 : 0;
      const likeDecay = 0.1 + 0.9 * Math.exp(-daysSinceLiked / 30);
      const effectiveWeight = CONFIG.LIKE_WEIGHT * likeDecay;
      const newProfile = ProfileManager.update({ ...oldProfile }, _currentAlbumData, effectiveWeight);
      State.saveProfile(newProfile);
      State.addLikedAlbum(albumId);
      if (ProfileManager.changeRatio(oldProfile, newProfile) > CONFIG.PROFILE_CHANGE_RECALC) {
        recalcRecommendations();
      }
      showNotification('❤️ 已记录喜欢!');
    }

    if (isFav) return; // 收藏由XHR网络监控处理
  });

  // ═══ STYLES ═══
  function injectStyles() {
    GM_addStyle(`
      #jms-panel {
        position: fixed; bottom: 20px; right: 20px; width: 380px;
        background: #1a1a2e; color: #e0e0e0; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
        z-index: 99999; font-size: 13px; overflow: hidden;
        display: none;
      }
      #jms-panel-header {
        background: #16213e; padding: 10px 16px; font-weight: bold; font-size: 14px;
        display: flex; justify-content: space-between; align-items: center;
      }
      #jms-panel-header span { opacity: 0.9; }
      #jms-panel-close-x {
        background: none; border: none; color: #888; cursor: pointer; font-size: 20px; padding: 0 6px; line-height: 1;
      }
      #jms-panel-close-x:hover { color: #fff; }
      #jms-panel-body { padding: 16px; max-height: 65vh; overflow-y: auto; }
      #jms-status { color: #0f9; margin-bottom: 8px; font-size: 12px; }
      #jms-progress-container { margin: 10px 0; }
      #jms-progress-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
      #jms-progress-fill { height: 100%; background: linear-gradient(90deg, #0f9, #0cf); width: 0; transition: width 0.3s; }
      #jms-progress-text { font-size: 11px; color: #aaa; margin-top: 4px; }
      #jms-stats { font-size: 12px; color: #aaa; margin: 8px 0; line-height: 1.6; }
      #jms-actions { display: flex; gap: 8px; margin: 10px 0; }
      #jms-settings-details { margin-top: 12px; font-size: 12px; }
      #jms-settings-details summary { cursor: pointer; color: #aaa; }
      #jms-history-details { margin-top: 12px; font-size: 12px; }
      #jms-history-details summary { cursor: pointer; color: #aaa; }
      #jms-log-details { margin-top: 12px; font-size: 12px; }
      #jms-log-details summary { cursor: pointer; color: #aaa; }
      #jms-settings { padding: 8px 0; }
      #jms-float-btn {
        position: fixed; bottom: 30px; right: 30px; width: 48px; height: 48px;
        border-radius: 50%; display: flex; align-items: center; justify-content: center;
        cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
        z-index: 99998; transition: transform 0.2s; overflow: hidden;
        animation: jms-float-rgb 20s ease-in-out infinite;
      }
      @keyframes jms-float-rgb {
        0%   { background: #ff4d6a; }
        16%  { background: #ff8c42; }
        33%  { background: #ffd166; }
        50%  { background: #4ecdc4; }
        66%  { background: #6c5ce7; }
        83%  { background: #e056a0; }
        100% { background: #ff4d6a; }
      }
      #jms-float-btn:hover { transform: scale(1.1); }
      #jms-notification {
        position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
        background: #1a1a2e; color: #fff; padding: 12px 24px; border-radius: 8px;
        z-index: 100000; font-size: 14px; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
        animation: jms-fadeIn 0.3s ease;
      }
      @keyframes jms-fadeIn {
        from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
        to { opacity: 1; transform: translateX(-50%) translateY(0); }
      }
    `);
  }

  // ═══ 数据迁移 ═══
  function migrateData(from) {
    if (from < 1) {
      try {
        const p = State.getProfile();
        if (p && !p.live) { p.live = { tags: {}, authors: {}, types: {}, lastUpdate: 0 }; State.saveProfile(p); }
      } catch(e) {}
      try {
        const v = State.getViewedAlbums();
        if (v.length > 0 && typeof v[0] === 'number') State.saveViewedAlbums(v.map(String));
      } catch(e) {}
      try { GMStore.set('scan_state', null); } catch(e) {}
    }
  }

  // ═══ 页面类型检测 ═══
  function detectPageType() {
    const path = location.pathname;
    if (path === '/' || path === '') return 'homepage';
    if (/\/album\/\d+/.test(path)) return 'album';
    if (/\/photo\/\d+/.test(path)) return 'photo';
    if (/\/albums/.test(path)) return 'listing';
    if (/\/search\/photos/.test(path)) return 'listing';
    if (/\/user\//.test(path)) return 'user';
    if (/\/theme\//.test(path)) return 'theme';
    return 'other';
  }

  // ═══ INIT ═══
  async function init() {
    LOG._restore();
    LOG.info('──────────────');

    const savedTemp = Storage.get('temperature');
    if (savedTemp != null) CONFIG.TEMPERATURE = savedTemp;

    const storedVersion = GMStore.get('version', 0);
    if (storedVersion < CONFIG.VERSION) {
      LOG.info(`版本升级 ${storedVersion} → ${CONFIG.VERSION}, 保留现有数据`);
      GMStore.set('version', CONFIG.VERSION);
    }

    const storedDataVer = GMStore.get('dataVersion', 0);
    if (storedDataVer < CONFIG.DATA_VERSION) {
      LOG.info(`数据格式迁移 ${storedDataVer} → ${CONFIG.DATA_VERSION}`);
      migrateData(storedDataVer);
      GMStore.set('dataVersion', CONFIG.DATA_VERSION);
    }

    injectStyles();

    const pageType = detectPageType();
    LOG.info(`当前页面类型: ${pageType}`);

    if (!isLoggedIn()) {
      LOG.info('未登录,仅注入UI框架');
      createPanel();
      return;
    }

    let username = detectUsername();
    if (!username) username = State.getUsername();
    if (username) {
      const lastUser = GMStore.get('lastUser', '');
      if (username !== lastUser) {
        GMStore.set('lastUser', username);
        State.saveUsername(username);
        LOG.info(`检测到用户: ${username}${lastUser ? ' (切换自 ' + lastUser + ')' : ''}`);
      }
      setAccount(username);
    }

    createPanel();

    if (!document.getElementById('jms-float-btn')) {
      const fb = document.createElement('div');
      fb.id = 'jms-float-btn';
      fb.title = 'JM Shelf';
      fb.innerHTML = '<img src="https://i.postimg.cc/BQ8vkqZv/JM-メスガキ.png" style="width:36px;height:36px;border-radius:50%">';
      fb.addEventListener('click', () => {
        const p = document.getElementById('jms-panel') || createPanel();
        p.style.display = p.style.display === 'block' ? 'none' : 'block';
      });
      document.body.appendChild(fb);
    }

    switch (pageType) {
      case 'homepage': handleHomepage(); break;
      case 'album': handleAlbumDetail(); break;
      case 'photo': handlePhotoPage(); break;
      case 'listing': handleListingPage(); break;
      case 'user': handleUserPage(); break;
    }

    LOG.info(`📌 已收藏/喜欢${State.getFavorites().length}本, 推荐${State.getRecommendations().length}条, 已看${State.getViewedAlbums().length}`);
    LOG.info('初始化完成');
  }

  // ═══ 调试工具 ═══
  try {
    unsafeWindow.jmsDebug = {
      score(id) {
        const cache = State.getAlbumCache();
        const album = cache[String(id)];
        if (!album) return `Album ${id} not in cache`;
        const profile = ProfileManager.getEffective(State.getProfile()||{});
        const favs = State.getFavorites();
        const hist = State.getHistory();
        const s = Recommender.score(album, profile, favs, hist, ProfileManager.deriveWeights(profile.popularityFingerprint||0.7));
        const maxTagW = Math.max(...Object.values(profile.tags), 1);
        const tf = State.getTagFreq();
        const fv = Object.values(tf).filter(v=>v>0);
        const mf = Math.max(...fv,1);
        const detail = (album.tags||[]).map(t => {
          const nt = normalizeTag(t);
          const w = profile.tags[nt]||0;
          if(w<=0) return null;
          const gc = tf[nt]||0;
          let idf = gc>0 ? clamp(1.0+Math.log10(Math.max(mf/gc,1))*0.6,0.6,2.0) : 1.0;
          const raw = w/maxTagW;
          return `${nt}(w${w} idf${idf.toFixed(2)} raw${raw.toFixed(2)}→${(raw*idf).toFixed(3)})`;
        }).filter(Boolean);
        return { id, title:album.title, score:s.score.toFixed(4), detail };
      },
      top(n) {
        const recs = State.getRecommendations().slice(0,n||10);
        const cache = State.getAlbumCache();
        return recs.map((r,i)=>{const al=cache[r.id];return `${i+1}. [${r.id}] ${(r._wm||'def')} s${r.score.toFixed(2)} ${(al?.title||'').slice(0,30)}`});
      }
    };
    LOG.info('调试工具已就绪: jmsDebug.score(albumId) / jmsDebug.top(N)');
  } catch (e) {}

  // ═══ START ═══
  init().catch(e => LOG.error('初始化失败', e));