// ==UserScript==
// @name FC2 标记下载状态
// @namespace http://tampermonkey.net/
// @license MIT
// @version 1.5
// @description 在 FC2 商品栏目标注状态(未下载/已下载/无资源),支持动态加载、过滤、导入/导出、美观UI、详情页与全局菜单,新增可伸缩面板与功能强大的收藏弹窗。
// @match https://adult.contents.fc2.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// ==/UserScript==
(function () {
'use strict';
console.log('[状态脚本] 加载(v1.5)');
const STATES = ['未下载', '已下载', '无资源'];
const COLORS = {
'未下载': '#007bff', // Blue
'已下载': '#28a745', // Green
'无资源': '#dc3545' // Red
};
const STORE_KEY = 'fc2_status_map_v1';
const PANEL_STATE_KEY = 'fc2_panel_state_v1';
const ITEMS_PER_PAGE_KEY = 'fc2_items_per_page_v1';
/* ===== 存储帮助 ===== */
function getStore() {
try {
const raw = GM_getValue(STORE_KEY, null);
if (!raw) return {};
return JSON.parse(raw);
} catch (e) {
console.error('[状态脚本] 读取存储失败', e);
return {};
}
}
function saveStore(map) {
try {
GM_setValue(STORE_KEY, JSON.stringify(map));
} catch (e) {
console.error('[状态脚本] 保存存储失败', e);
}
}
/* ===== CSS (v1.5) ===== */
const css = `
/* 通用项目容器样式 */
.c-neoItem-1000_wrap, .c-cntCard-110-f {
position: relative;
transition: transform .18s ease, box-shadow .18s ease, opacity .18s ease;
}
/* 右上角控制面板 */
#fc2-status-panel {
position: fixed; top: 12px; right: 12px; z-index: 9999999;
background: rgba(255,255,255,0.95); border-radius: 10px;
box-shadow: 0 6px 18px rgba(20,20,30,0.12);
display:flex; align-items:center;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC";
transition: all 0.2s ease-in-out;
}
#fc2-status-panel .fc2-panel-content {
display: flex; gap:8px; align-items:center; padding: 8px;
overflow: hidden;
transition: all 0.2s ease-in-out;
}
#fc2-status-panel button, #fc2-status-panel select {
font-size:13px;padding:7px 10px;border-radius:8px;cursor:pointer; border:1px solid rgba(0,0,0,0.08);
background: linear-gradient(180deg, #fff, #f6f6f7); box-shadow: 0 2px 6px rgba(20,20,30,0.04);
white-space: nowrap;
}
#fc2-panel-toggle {
padding: 8px; border: none; background: transparent; cursor: pointer;
}
#fc2-panel-toggle svg { width: 24px; height: 24px; transition: transform 0.3s ease; }
/* 面板折叠状态 */
#fc2-status-panel.collapsed .fc2-panel-content { display: none; }
#fc2-status-panel.collapsed #fc2-panel-toggle svg { transform: rotate(-90deg); }
/* 状态标记按钮 */
.fc2-status { display:inline-block; position:relative; margin-left:10px; vertical-align: middle; font-family: inherit; flex-shrink: 0; }
.fc2-status-btn {
display:inline-flex; gap:8px; align-items:center; padding: 8px 16px; border-radius:18px; border:none;
color:#fff; font-weight:600; font-size:13px; cursor:pointer; box-shadow: 0 4px 12px rgba(16,24,40,0.12);
transition: transform .12s ease, box-shadow .12s ease; user-select:none;
position: relative; z-index: 10010;
}
.fc2-status-btn:active { transform: translateY(1px); }
.fc2-status .caret { width:10px; height:10px; opacity:0.95; transform: translateY(1px); }
/* (已下载)更明显的透明度 */
.c-neoItem-1000_wrap.fc2-s-downloaded, .c-cntCard-110-f.fc2-s-downloaded { opacity: 0.32 !important; transform: scale(0.997); }
/* 无资源:更明显的红色发光边框 */
.c-neoItem-1000_wrap.fc2-s-noresource, .c-cntCard-110-f.fc2-s-noresource {
box-shadow: 0 0 0 3px rgba(255,50,50,0.95), 0 10px 40px rgba(255,40,40,0.28), inset 0 0 40px rgba(255,0,0,0.06);
background: rgba(255,230,230,0.18);
transform: translateY(-2px) scale(1.01);
z-index: 6;
}
.fc2-noresource-ribbon {
position:absolute; top:8px; left:8px;
background: linear-gradient(90deg,#ff6b6b,#ff2d2d); color:white; font-size:11px; font-weight:700;
padding:4px 8px; border-radius:6px; transform: rotate(-10deg); box-shadow: 0 6px 18px rgba(255,50,50,0.2); pointer-events:none; z-index: 8;
}
/* 全局浮动菜单 */
#fc2-global-status-menu {
position: fixed; display:none; min-width:160px; border-radius:10px; background:#fff; padding:6px;
box-shadow: 0 18px 48px rgba(10,10,20,0.2); z-index: 2147483646; font-weight:700;
}
#fc2-global-status-menu li { list-style:none; padding:8px 10px; border-radius:8px; cursor:pointer; display:flex; gap:8px; align-items:center; color:#222; }
#fc2-global-status-menu li:hover { background: rgba(0,0,0,0.04); }
.fc2-dot { width:10px; height:10px; border-radius:50%; flex-shrink: 0; box-shadow: inset 0 -2px 0 rgba(0,0,0,0.12); }
/* 收藏夹弹窗 */
#fc2-favorites-modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6); z-index: 2147483640;
display: flex; align-items: center; justify-content: center;
}
#fc2-favorites-modal {
width: 90%; max-width: 1200px; height: 85vh; background: #f0f2f5;
border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex; flex-direction: column; overflow: hidden;
}
.fc2-favorites-header {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 20px; border-bottom: 1px solid #e0e0e0; background: #fff;
}
.fc2-favorites-header h2 { margin: 0; font-size: 18px; }
.fc2-favorites-filters button { margin-left: 8px; }
.fc2-favorites-filters button.active { background: #007bff; color: white; border-color: #007bff; }
.fc2-favorites-close { cursor: pointer; font-size: 28px; line-height: 1; opacity: 0.5; border:none; background:none; padding:0; }
.fc2-favorites-close:hover { opacity: 1; }
.fc2-favorites-content { flex-grow: 1; overflow-y: auto; padding: 10px; }
.fc2-favorites-list { list-style: none; margin: 0; padding: 0; }
.fc2-favorite-item { display: flex; align-items: flex-start; background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.fc2-favorite-item img { width: 120px; height: auto; border-radius: 4px; margin-right: 15px; flex-shrink: 0; }
.fc2-favorite-info { flex-grow: 1; display: flex; flex-direction: column; min-width: 0; }
.fc2-favorite-info h3 { margin: 0 0 8px; font-size: 16px; line-height: 1.3; }
.fc2-favorite-info h3 a { text-decoration: none; color: #1a1a1a; }
.fc2-favorite-info h3 a:hover { color: #007bff; }
.fc2-favorite-info .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 5px 15px; font-size: 13px; color: #555; }
.fc2-favorite-info .meta-grid p { margin: 0; }
.fc2-favorites-pagination { display: flex; justify-content: center; align-items: center; gap: 10px; padding: 10px; border-top: 1px solid #e0e0e0; background: #fff; }
.fc2-favorites-pagination button, .fc2-favorites-pagination input, .fc2-favorites-pagination select { padding: 5px 10px; }
.fc2-favorites-pagination input { width: 60px; text-align: center; }
.fc2-favorites-loading { text-align: center; padding: 50px; font-size: 16px; color: #888; }
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
/* ===== 全局菜单(单例) ===== */
let globalMenu = null;
function ensureGlobalMenu() {
if (globalMenu) return globalMenu;
globalMenu = document.createElement('div');
globalMenu.id = 'fc2-global-status-menu';
globalMenu.innerHTML = '<ul style="margin:0;padding:6px;"></ul>';
document.body.appendChild(globalMenu);
document.addEventListener('click', (ev) => {
if (globalMenu && globalMenu.style.display !== 'none' && !globalMenu.contains(ev.target)) hideGlobalMenu();
}, true);
window.addEventListener('keydown', (ev)=> { if (ev.key === 'Escape') hideGlobalMenu(); });
window.addEventListener('scroll', ()=> hideGlobalMenu(), { passive: true });
return globalMenu;
}
function showGlobalMenuFor(buttonEl, id) {
const menu = ensureGlobalMenu();
const ul = menu.querySelector('ul'); ul.innerHTML = '';
STATES.forEach(s => {
const li = document.createElement('li');
li.dataset.val = s;
li.innerHTML = `<span class="fc2-dot" style="background:${COLORS[s]}"></span><span style="flex:1;">${s}</span>`;
li.onclick = (ev) => { ev.stopPropagation(); setItemStatus(id, s); hideGlobalMenu(); };
ul.appendChild(li);
});
const rect = buttonEl.getBoundingClientRect();
const margin = 8;
menu.style.display = 'block';
const menuW = menu.offsetWidth;
const menuH = menu.offsetHeight;
let left = rect.left + rect.width - menuW;
let top = rect.bottom + margin;
if (top + menuH > window.innerHeight - 8) top = rect.top - margin - menuH;
menu.style.left = `${Math.max(8, left)}px`;
menu.style.top = `${top}px`;
}
function hideGlobalMenu() {
if (globalMenu) globalMenu.style.display = 'none';
}
/* ===== 核心逻辑 ===== */
const ITEM_SELECTOR = '.c-neoItem-1000_wrap, .c-cntCard-110-f';
function extractIdFromItem(item) {
const link = item.querySelector('.c-cntCard-110-f_itemName a, .c-cntCard-110-f_thumb_link');
if (!link) return null;
const href = link.getAttribute('href') || '';
const m = href.match(/article\/(\d+)/);
return m ? m[1] : null;
}
function setItemStatus(id, state) {
const store = getStore();
store[id] = state;
saveStore(store);
document.querySelectorAll(ITEM_SELECTOR).forEach(item => {
if (extractIdFromItem(item) === String(id)) applyVisualAndButton(item, id, state);
});
const detailRoot = document.querySelector('.items_article_headerTitleInArea');
if (detailRoot && extractIdFromDetail() === String(id)) {
applyVisualAndButton(detailRoot, id, state);
}
}
function applyVisualAndButton(itemOrRoot, id, state) {
let cardRoot = null;
if (itemOrRoot.matches(ITEM_SELECTOR)) cardRoot = itemOrRoot;
else cardRoot = document.querySelector('.items_article_headerTitleInArea');
if (!cardRoot) return;
const cont = cardRoot.querySelector(`.fc2-status[data-id="${id}"]`);
if (cont) {
const btn = cont.querySelector('.fc2-status-btn');
const label = cont.querySelector('.fc2-status-label');
if (label) label.textContent = state;
if (btn) btn.style.background = `linear-gradient(180deg, ${COLORS[state]}, ${shadeColor(COLORS[state], -10)})`;
}
cardRoot.classList.remove('fc2-s-downloaded', 'fc2-s-noresource');
cardRoot.querySelectorAll('.fc2-noresource-ribbon').forEach(el => el.remove());
if (state === '已下载') {
cardRoot.classList.add('fc2-s-downloaded');
} else if (state === '无资源') {
cardRoot.classList.add('fc2-s-noresource');
const thumb = cardRoot.querySelector('.c-cntCard-110-f_thumb') || cardRoot.querySelector('.items_article_MainitemThumb > span');
if (thumb) {
const rb = document.createElement('div');
rb.className = 'fc2-noresource-ribbon';
rb.textContent = '无资源';
if (getComputedStyle(thumb).position === 'static') thumb.style.position = 'relative';
thumb.appendChild(rb);
}
}
}
function insertStatusControl(itemContainer, id) {
if (itemContainer.querySelector(`.fc2-status[data-id="${id}"]`)) return;
const store = getStore();
const cur = store[id] || STATES[0];
const cont = document.createElement('div');
cont.className = 'fc2-status';
cont.dataset.id = id;
cont.innerHTML = `
<button class="fc2-status-btn" type="button" style="background: linear-gradient(180deg, ${COLORS[cur]}, ${shadeColor(COLORS[cur], -10)})">
<span class="fc2-status-label">${cur}</span>
<svg class="caret" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"><path d="M1 3l4 4 4-4" fill="none" stroke="rgba(255,255,255,0.9)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`;
const btn = cont.querySelector('.fc2-status-btn');
btn.onclick = (ev) => {
ev.stopPropagation();
ev.preventDefault();
showGlobalMenuFor(btn, id);
};
itemContainer.appendChild(cont);
applyVisualAndButton(itemContainer.closest(ITEM_SELECTOR + ', .items_article_headerTitleInArea'), id, cur);
}
/* ===== 渲染与监听 ===== */
function renderExistingItems() {
document.querySelectorAll(ITEM_SELECTOR).forEach(item => {
const id = extractIdFromItem(item);
if (id) {
const infoArea = item.querySelector('.c-cntCard-110-f_indetail') || item;
insertStatusControl(infoArea, id);
}
});
}
const observer = new MutationObserver(muts => {
muts.forEach(mut => mut.addedNodes.forEach(node => {
if (node instanceof Element) {
const items = node.matches(ITEM_SELECTOR) ? [node] : node.querySelectorAll(ITEM_SELECTOR);
items.forEach(item => {
const id = extractIdFromItem(item);
if (id) {
const infoArea = item.querySelector('.c-cntCard-110-f_indetail') || item;
insertStatusControl(infoArea, id);
}
});
}
}));
renderDetailPage();
});
observer.observe(document.body, { childList: true, subtree: true });
/* ===== 详情页 (v1.5 修复布局) ===== */
function extractIdFromDetail() {
const urlMatch = location.href.match(/article\/(\d+)/);
return urlMatch ? urlMatch[1] : null;
}
function renderDetailPage() {
const id = extractIdFromDetail();
if (!id) return;
const titleElement = document.querySelector('.items_article_headerTitleInArea h3');
if (!titleElement || titleElement.querySelector('.fc2-status')) return;
titleElement.style.display = 'flex';
titleElement.style.alignItems = 'center';
titleElement.style.justifyContent = 'space-between';
const nodesToWrap = Array.from(titleElement.childNodes).filter(node => !node.classList || !node.classList.contains('fc2-status'));
const titleSpan = document.createElement('span');
nodesToWrap.forEach(node => titleSpan.appendChild(node.cloneNode(true)));
// 清空 h3 并重新组合
titleElement.innerHTML = '';
titleElement.appendChild(titleSpan);
insertStatusControl(titleElement, id);
const store = getStore();
const cur = store[id] || STATES[0];
const root = document.querySelector('.items_article_headerTitleInArea');
if (root) applyVisualAndButton(root, id, cur);
}
/* ===== 右上角面板 (v1.3) ===== */
function buildPanel() {
if (document.querySelector('#fc2-status-panel')) return;
const panel = document.createElement('div');
panel.id = 'fc2-status-panel';
const content = document.createElement('div');
content.className = 'fc2-panel-content';
const toggleBtn = document.createElement('button');
toggleBtn.id = 'fc2-panel-toggle';
toggleBtn.title = "展开/折叠面板";
toggleBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 l-3.84,0c-0.24,0-0.44,0.17-0.48,0.41L9.22,5.15C8.63,5.39,8.1,5.71,7.6,6.09L5.21,5.13C5,5.06,4.75,5.13,4.63,5.34L2.71,8.66 c-0.12,0.22-0.07,0.47,0.12,0.61L4.86,11c-0.05,0.3-0.07,0.62-0.07,0.94s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.38,2.34 c0.04,0.24,0.24,0.41,0.48,0.41l3.84,0c0.24,0,0.44-0.17,0.48-0.41l0.38-2.34c0.59-0.24,1.12-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0.01,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"></path></svg>`;
toggleBtn.onclick = () => {
const isCollapsed = panel.classList.toggle('collapsed');
GM_setValue(PANEL_STATE_KEY, isCollapsed ? 'collapsed' : 'expanded');
};
const filter = document.createElement('select'); filter.title = "按状态过滤页面内容";
['全部', ...STATES].forEach(s => { filter.innerHTML += `<option value="${s}">显示: ${s}</option>`; });
filter.onchange = () => applyFilter(filter.value);
const btnFavorites = document.createElement('button'); btnFavorites.textContent = '显示已收藏'; btnFavorites.title = "打开弹窗显示所有已下载或无资源的项目";
btnFavorites.onclick = openFavoritesModal;
const btnExport = document.createElement('button'); btnExport.textContent = '导出';
btnExport.onclick = exportStore;
const btnImport = document.createElement('button'); btnImport.textContent = '导入';
btnImport.onclick = openImportModal;
const btnClear = document.createElement('button'); btnClear.textContent = '清空';
btnClear.onclick = () => {
if (confirm('确认清空所有状态?不可恢复。')) {
saveStore({});
document.querySelectorAll(ITEM_SELECTOR).forEach(it => resetItemVisual(it));
}
};
content.append(filter, btnFavorites, btnExport, btnImport, btnClear);
panel.append(content, toggleBtn);
document.body.appendChild(panel);
if (GM_getValue(PANEL_STATE_KEY, 'collapsed') === 'collapsed') {
panel.classList.add('collapsed');
}
}
function applyFilter(value) {
document.querySelectorAll(ITEM_SELECTOR).forEach(item => {
const id = extractIdFromItem(item);
if (!id) { item.style.display = ''; return; }
const cur = getStore()[id] || STATES[0];
item.style.display = (value === '全部' || cur === value) ? '' : 'none';
});
}
/* ===== 收藏夹功能 (v1.5) ===== */
async function openFavoritesModal() {
const overlay = document.createElement('div');
overlay.id = 'fc2-favorites-modal-overlay';
overlay.innerHTML = `
<div id="fc2-favorites-modal">
<div class="fc2-favorites-header">
<h2>已收藏项目</h2>
<div class="fc2-favorites-filters">
<button data-filter="全部" class="active">全部</button>
<button data-filter="已下载">已下载</button>
<button data-filter="无资源">无资源</button>
</div>
<button class="fc2-favorites-close">×</button>
</div>
<div class="fc2-favorites-content">
<div class="fc2-favorites-loading">加载中...</div>
<ul class="fc2-favorites-list"></ul>
</div>
<div class="fc2-favorites-pagination"></div>
</div>
`;
document.body.appendChild(overlay);
const modal = overlay.querySelector('#fc2-favorites-modal');
modal.onclick = e => e.stopPropagation();
overlay.onclick = () => overlay.remove();
overlay.querySelector('.fc2-favorites-close').onclick = () => overlay.remove();
let itemsPerPage = GM_getValue(ITEMS_PER_PAGE_KEY, 15);
let currentPage = 1;
let currentFilter = '全部';
const renderPage = async (page) => {
currentPage = page;
const store = getStore();
const allFavoriteIds = Object.keys(store).filter(id => store[id] === '已下载' || store[id] === '无资源');
const filteredIds = allFavoriteIds.filter(id => currentFilter === '全部' || store[id] === currentFilter);
const listEl = overlay.querySelector('.fc2-favorites-list');
const loadingEl = overlay.querySelector('.fc2-favorites-loading');
listEl.innerHTML = '';
loadingEl.style.display = 'block';
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageIds = filteredIds.slice(start, end);
const itemsData = await Promise.all(pageIds.map(id => fetchArticleDetails(id)));
loadingEl.style.display = 'none';
itemsData.forEach(data => {
if (!data) return;
const li = document.createElement('li');
li.className = 'fc2-favorite-item';
li.innerHTML = `
<img src="${data.thumb}" alt="${data.title}" />
<div class="fc2-favorite-info">
<h3><a href="https://adult.contents.fc2.com/article/${data.id}/" target="_blank" rel="noopener noreferrer">${data.title}</a></h3>
<div class="meta-grid">
<p><b>ID:</b> FC2-PPV-${data.id}</p>
<p><b>作者:</b> ${data.author || 'N/A'}</p>
<p><b>❤️ 登录数:</b> ${data.favoriteCount || 'N/A'}</p>
<p><b>📅 上架时间:</b> ${data.uploadDate || 'N/A'}</p>
<p><b>状态:</b> ${store[data.id]}</p>
</div>
</div>
`;
listEl.appendChild(li);
});
renderPagination(filteredIds.length);
};
const renderPagination = (totalItems) => {
const paginationEl = overlay.querySelector('.fc2-favorites-pagination');
const totalPages = Math.ceil(totalItems / itemsPerPage);
paginationEl.innerHTML = '';
const perPageLabel = document.createElement('span');
perPageLabel.textContent = '每页显示:';
const perPageInput = document.createElement('input');
perPageInput.type = 'number';
perPageInput.min = 5;
perPageInput.value = itemsPerPage;
perPageInput.onchange = () => {
itemsPerPage = parseInt(perPageInput.value, 10) || 15;
GM_setValue(ITEMS_PER_PAGE_KEY, itemsPerPage);
renderPage(1);
};
if (totalPages > 1) {
const prevBtn = document.createElement('button');
prevBtn.textContent = '上一页';
prevBtn.disabled = currentPage === 1;
prevBtn.onclick = () => renderPage(currentPage - 1);
const pageSelect = document.createElement('select');
for(let i = 1; i <= totalPages; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = `第 ${i} 页`;
if(i === currentPage) option.selected = true;
pageSelect.appendChild(option);
}
pageSelect.onchange = () => renderPage(parseInt(pageSelect.value, 10));
const pageInfo = document.createElement('span');
pageInfo.textContent = `/ ${totalPages} 页`;
const nextBtn = document.createElement('button');
nextBtn.textContent = '下一页';
nextBtn.disabled = currentPage === totalPages;
nextBtn.onclick = () => renderPage(currentPage + 1);
paginationEl.append(perPageLabel, perPageInput, prevBtn, pageSelect, pageInfo, nextBtn);
} else {
paginationEl.append(perPageLabel, perPageInput);
}
};
overlay.querySelectorAll('.fc2-favorites-filters button').forEach(btn => {
btn.onclick = () => {
overlay.querySelector('.fc2-favorites-filters button.active').classList.remove('active');
btn.classList.add('active');
currentFilter = btn.dataset.filter;
renderPage(1);
};
});
renderPage(1);
}
const articleCache = new Map();
async function fetchArticleDetails(id) {
if (articleCache.has(id)) return articleCache.get(id);
try {
const response = await fetch(`https://adult.contents.fc2.com/article/${id}/`);
if (!response.ok) return null;
const htmlText = await response.text();
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
// 优先从 ld+json 获取信息,更稳定
let title = `项目 ${id}`, thumb = '', author = 'N/A';
try {
const scriptTag = doc.querySelector('script[type="application/ld+json"]');
if(scriptTag) {
const jsonData = JSON.parse(scriptTag.textContent);
title = jsonData.name;
thumb = jsonData.image?.url;
author = jsonData.brand?.name;
}
} catch(e) { console.error('解析ld+json失败', e); }
// 从页面元素中补充信息
let favoriteCount = 'N/A';
doc.querySelectorAll('.items_article_headerInfo ul li').forEach(li => {
if (li.textContent.includes('登录数')) {
favoriteCount = li.querySelector('b')?.textContent || 'N/A';
}
});
let uploadDate = 'N/A';
doc.querySelectorAll('.items_article_softDevice p').forEach(p => {
if (p.textContent.includes('上架时间')) {
uploadDate = p.textContent.split(':')[1]?.trim() || 'N/A';
}
});
const details = { id, title, thumb, author, uploadDate, favoriteCount };
articleCache.set(id, details);
return details;
} catch (error) {
console.error(`[状态脚本] 获取项目详情失败: ${id}`, error);
return {id, title: `项目 ${id} (加载失败)`, thumb: ''};
}
}
/* ===== 导入/导出/清空/重置等 ===== */
function openImportModal() {
const overlay = document.createElement('div');
overlay.style.cssText='position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:2147483647;display:flex;align-items:center;justify-content:center';
overlay.innerHTML = `
<div style="width:520px;max-width:90vw;background:#fff;border-radius:12px;padding:16px;box-shadow:0 18px 60px rgba(0,0,0,0.28);">
<h3 style="margin:0 0 8px 0;">导入状态 (JSON)</h3>
<textarea placeholder='{"4745474":"已下载"}' style="width:100%;height:180px;padding:10px;border-radius:8px;border:1px solid #ddd;font-family:monospace;font-size:13px;"></textarea>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:10px;">
<button type="button" class="cancel">取消</button>
<button type="button" class="ok">导入</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const ta = overlay.querySelector('textarea');
overlay.querySelector('.cancel').onclick = () => overlay.remove();
overlay.querySelector('.ok').onclick = () => {
try {
doImport(JSON.parse(ta.value.trim()));
overlay.remove();
} catch(e){ alert('无效 JSON'); }
};
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
}
function doImport(jsonObj) {
if (typeof jsonObj !== 'object' || jsonObj === null) return;
const store = getStore();
let changed = 0;
for (const k in jsonObj) {
if (STATES.includes(jsonObj[k])) {
store[String(k)] = jsonObj[k];
changed++;
}
}
saveStore(store);
renderExistingItems();
renderDetailPage();
}
function exportStore() {
const map = getStore();
const text = JSON.stringify(map, null, 2);
const blob = new Blob([text], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `fc2_status_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
}
function resetItemVisual(item) {
item.classList.remove('fc2-s-downloaded', 'fc2-s-noresource');
item.querySelectorAll('.fc2-noresource-ribbon').forEach(el => el.remove());
const id = extractIdFromItem(item);
if (!id) return;
const cont = item.querySelector(`.fc2-status[data-id="${id}"]`);
if (cont) {
const btn = cont.querySelector('.fc2-status-btn');
const label = cont.querySelector('.fc2-status-label');
if (label) label.textContent = STATES[0];
if (btn) btn.style.background = `linear-gradient(180deg, ${COLORS[STATES[0]]}, ${shadeColor(COLORS[STATES[0]], -10)})`;
}
}
function shadeColor(hex, percent) {
hex = hex.replace('#','');
const num = parseInt(hex,16);
let r = (num >> 16) + Math.round(255 * (percent/100));
let g = ((num >> 8) & 0x00FF) + Math.round(255 * (percent/100));
let b = (num & 0x0000FF) + Math.round(255 * (percent/100));
r=Math.min(255,Math.max(0,r)); g=Math.min(255,Math.max(0,g)); b=Math.min(255,Math.max(0,b));
return '#'+(0x1000000 + (r<<16) + (g<<8) + b).toString(16).slice(1);
}
/* ===== 启动初始化 ===== */
function init() {
buildPanel();
setTimeout(() => {
renderExistingItems();
renderDetailPage();
}, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();