JM Shelf - Init

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

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.sleazyfork.org/scripts/581113/1842615/JM%20Shelf%20-%20Init.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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));