Greasy Fork is available in English.
OnlyFans 赛博菩萨,佬友严选,值得信赖!艺术家收藏、作品管理、视频播放
当前为
// ==UserScript==
// @name Coomer 佬友严选
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description OnlyFans 赛博菩萨,佬友严选,值得信赖!艺术家收藏、作品管理、视频播放
// @author urzeye
// @match https://coomer.st/*
// @icon https://thumbs.onlyfans.com/public/files/thumbs/c50/m/mk/mka/mkamcrf6rjmcwo0jj4zoavhmalzohe5a1640180203/avatar.jpg
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_getResourceText
// @grant window.onurlchange
// @resource videojs_css https://cdnjs.cloudflare.com/ajax/libs/video.js/8.16.1/video-js.min.css
// @require https://cdnjs.cloudflare.com/ajax/libs/video.js/8.16.1/video.min.js
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ============================================
// 常量与配置
// ============================================
const STORAGE_KEYS = {
ARTISTS: 'coomer_artists',
POSTS: 'coomer_posts',
SETTINGS: 'coomer_settings',
};
const DEFAULT_SETTINGS = {
autoFullscreen: true,
blockAds: true,
panelPosition: 'bottom-right',
fabPosition: { right: 20, bottom: 80 },
presetCollapsed: false, // 预置分组是否折叠
myCollectionCollapsed: false, // 我的收藏分组是否折叠
};
// 预置艺术家列表(佬友严选)
const PRESET_ARTISTS = [
{
id: 'thedirectortong',
nickname: 'thedirectortong',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/thedirectortong',
avatar: 'https://img.coomer.st/icons/onlyfans/thedirectortong',
},
{
id: '347914002094895104',
nickname: 'Thelittlejuicer',
platform: 'Fansly',
homepageUrl: 'https://coomer.st/fansly/user/347914002094895104',
avatar: 'https://img.coomer.st/icons/fansly/347914002094895104',
},
{
id: 'thelittlejuicer',
nickname: 'thelittlejuicer',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/thelittlejuicer',
avatar: 'https://img.coomer.st/icons/onlyfans/thelittlejuicer',
},
{
id: 'applecptv',
nickname: 'applecptv',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/applecptv',
avatar: 'https://img.coomer.st/icons/onlyfans/applecptv',
},
{
id: 'kimchi.couple',
nickname: 'kimchi.couple',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/kimchi.couple',
avatar: 'https://img.coomer.st/icons/onlyfans/kimchi.couple',
},
{
id: '531599132742135808',
nickname: 'candy_factory',
platform: 'Fansly',
homepageUrl: 'https://coomer.st/fansly/user/531599132742135808',
avatar: 'https://img.coomer.st/icons/fansly/531599132742135808',
},
{
id: 'chocoletmilkk',
nickname: 'chocoletmilkk',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/chocoletmilkk',
avatar: 'https://img.coomer.st/icons/onlyfans/chocoletmilkk',
},
{
id: '504758618625683456',
nickname: 'demifairytw',
platform: 'Fansly',
homepageUrl: 'https://coomer.st/fansly/user/504758618625683456',
avatar: 'https://img.coomer.st/icons/fansly/504758618625683456',
},
{
id: 'skylar_blue',
nickname: 'skylar_blue',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/skylar_blue',
avatar: 'https://img.coomer.st/icons/onlyfans/skylar_blue',
},
{
id: 'yui_xin_tw',
nickname: 'yui_xin_tw',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/yui_xin_tw',
avatar: 'https://img.coomer.st/icons/onlyfans/yui_xin_tw',
},
{
id: 'yuyuhwa',
nickname: 'yuyuhwa',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/yuyuhwa',
avatar: 'https://img.coomer.st/icons/onlyfans/yuyuhwa',
},
{
id: 'xxapple',
nickname: 'xxapple',
platform: 'OnlyFans',
homepageUrl: 'https://coomer.st/onlyfans/user/xxapple',
avatar: 'https://img.coomer.st/icons/onlyfans/xxapple',
},
];
const DOM_SELECTORS = {
// 艺术家页面
artistAvatar: '.user-header__avatar img',
artistName: '.user-header__profile span:nth-child(2)',
artistPlatform: '.user-header__profile span:nth-child(3)',
// 作品页面
postTitle: '.post__content',
postAttachment: 'a.post__attachment-link',
postThumbnail: '.post__thumbnail img',
// 视频播放器
videoWrapper: '.fluid_video_wrapper',
videoElement: 'video',
};
// URL 匹配规则
const URL_PATTERNS = {
// userId 匹配任何非斜杠字符
artistPage: /^\/(\w+)\/user\/([^/]+)\/?$/,
postPage: /^\/(\w+)\/user\/([^/]+)\/post\/([^/]+)\/?$/,
};
// ============================================
// StorageManager - 数据持久化
// ============================================
const StorageManager = {
get(key, defaultValue = null) {
try {
const data = GM_getValue(key);
return data !== undefined ? JSON.parse(data) : defaultValue;
} catch {
return defaultValue;
}
},
set(key, value) {
GM_setValue(key, JSON.stringify(value));
},
getArtists() {
return this.get(STORAGE_KEYS.ARTISTS, {});
},
setArtists(artists) {
this.set(STORAGE_KEYS.ARTISTS, artists);
},
getPosts() {
return this.get(STORAGE_KEYS.POSTS, {});
},
setPosts(posts) {
this.set(STORAGE_KEYS.POSTS, posts);
},
getSettings() {
return { ...DEFAULT_SETTINGS, ...this.get(STORAGE_KEYS.SETTINGS, {}) };
},
setSettings(settings) {
this.set(STORAGE_KEYS.SETTINGS, settings);
},
};
// ============================================
// PageParser - 页面数据解析
// ============================================
const PageParser = {
getCurrentPageType() {
const path = window.location.pathname;
if (URL_PATTERNS.postPage.test(path)) return 'post';
if (URL_PATTERNS.artistPage.test(path)) return 'artist';
return 'other';
},
parseUrlInfo() {
const path = window.location.pathname;
let match;
if ((match = URL_PATTERNS.postPage.exec(path))) {
return { platform: match[1], userId: match[2], postId: match[3] };
}
if ((match = URL_PATTERNS.artistPage.exec(path))) {
return { platform: match[1], userId: match[2] };
}
return null;
},
parseArtistInfo() {
const urlInfo = this.parseUrlInfo();
if (!urlInfo) return null;
const pageType = this.getCurrentPageType();
let avatar = '';
let nickname = '';
let platform = urlInfo.platform;
if (pageType === 'post') {
// 作品页面的选择器
// 昵称: <a class="post__user-name">Thelittlejuicer</a>
nickname = document.querySelector('a.post__user-name')?.textContent?.trim() || '';
// 头像: <a class="post__user-profile"><picture><img src="//img.coomer.st/icons/..."></picture></a>
const avatarImg = document.querySelector('a.post__user-profile img');
if (avatarImg?.src) {
avatar = avatarImg.src.startsWith('//') ? 'https:' + avatarImg.src : avatarImg.src;
}
} else if (pageType === 'artist') {
// 艺术家主页的选择器
// 昵称: <span itemprop="name">thelittlejuicer</span>
nickname =
document.querySelector('.user-header__profile span[itemprop="name"]')?.textContent?.trim() ||
document.querySelector('.user-header__profile span:nth-child(2)')?.textContent?.trim() ||
'';
// 头像: <picture class="fancy-image__picture"><img src="//img.coomer.st/icons/..."></picture>
const avatarImg = document.querySelector('.user-header__avatar img') || document.querySelector('.user-header picture img');
if (avatarImg?.src) {
avatar = avatarImg.src.startsWith('//') ? 'https:' + avatarImg.src : avatarImg.src;
}
// 平台
const platformEl = document.querySelector('.user-header__profile span:nth-child(1) img');
if (platformEl?.src) {
// 从图片路径提取平台名 /static/onlyfans.svg -> onlyfans
const match = platformEl.src.match(/\/static\/(\w+)\.svg/);
if (match) platform = match[1];
}
}
// 备用:如果没有获取到头像,根据 URL 构造一个
if (!avatar && urlInfo.userId) {
avatar = `https://img.coomer.st/icons/${urlInfo.platform}/${urlInfo.userId}`;
}
// 备用:如果没有获取到昵称,使用 userId
if (!nickname) {
nickname = urlInfo.userId;
}
// 构造正确的艺术家主页 URL
const artistHomepageUrl = `https://coomer.st/${urlInfo.platform}/user/${urlInfo.userId}`;
return {
id: urlInfo.userId,
nickname,
avatar,
platform: this.normalizePlatform(platform),
homepageUrl: artistHomepageUrl,
isPreset: false,
addedAt: Date.now(),
};
},
parsePostInfo() {
const urlInfo = this.parseUrlInfo();
if (!urlInfo || !urlInfo.postId) return null;
const artistInfo = this.parseArtistInfo();
const title = document.querySelector(DOM_SELECTORS.postTitle)?.textContent?.trim() || '';
// 获取所有媒体下载链接
const attachments = document.querySelectorAll(DOM_SELECTORS.postAttachment);
let mediaUrls = Array.from(attachments)
.map((a) => a.href)
.filter(Boolean);
// 如果没有找到附件链接,尝试从图片链接获取
if (mediaUrls.length === 0) {
// 获取 .fileThumb 大图链接
const imageLinks = document.querySelectorAll('.post__files .fileThumb');
mediaUrls = Array.from(imageLinks)
.map((a) => a.href)
.filter(Boolean);
}
// 获取缩略图:优先使用作品图片,如果没有则使用艺术家头像作为封面
let thumb = document.querySelector(DOM_SELECTORS.postThumbnail)?.src || document.querySelector('.post__image img')?.src || document.querySelector('.fileThumb img')?.src || '';
if (!thumb && artistInfo?.avatar) {
thumb = artistInfo.avatar;
}
// 判断类型
const hasVideo = mediaUrls.some((url) => /\.(mp4|webm|mov|m4v)/i.test(url));
return {
id: urlInfo.postId,
title: title || `Post ${urlInfo.postId}`,
artistId: urlInfo.userId,
artistName: artistInfo?.nickname || '',
platform: urlInfo.platform,
thumb,
mediaUrls,
type: hasVideo ? 'video' : 'image',
pageUrl: window.location.href.split('?')[0],
addedAt: Date.now(),
};
},
normalizePlatform(platform) {
const map = {
onlyfans: 'OnlyFans',
fansly: 'Fansly',
candfans: 'CandFans',
};
return map[platform.toLowerCase()] || platform;
},
};
// ============================================
// ArtistManager - 艺术家管理
// ============================================
const ArtistManager = {
getAll() {
return StorageManager.getArtists();
},
get(id) {
return this.getAll()[id] || null;
},
add(artist) {
const artists = this.getAll();
artists[artist.id] = artist;
StorageManager.setArtists(artists);
return artist;
},
remove(id) {
const artists = this.getAll();
delete artists[id];
StorageManager.setArtists(artists);
},
exists(id) {
return !!this.getAll()[id];
},
count() {
return Object.keys(this.getAll()).length;
},
// 切换置顶状态
togglePin(id) {
const artists = this.getAll();
if (artists[id]) {
artists[id].isPinned = !artists[id].isPinned;
StorageManager.setArtists(artists);
return artists[id].isPinned;
}
return false;
},
// 获取排序后的列表(置顶优先,然后按时间倒序)
getSortedList() {
return Object.values(this.getAll()).sort((a, b) => {
// 置顶的排在前面
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
// 同级别按时间倒序(最新的在前)
return (b.addedAt || 0) - (a.addedAt || 0);
});
},
// 收藏当前页面的艺术家
collectCurrent() {
const info = PageParser.parseArtistInfo();
if (!info) return null;
return this.add(info);
},
};
// ============================================
// PostManager - 作品管理
// ============================================
const PostManager = {
getAll() {
return StorageManager.getPosts();
},
get(id) {
return this.getAll()[id] || null;
},
add(post) {
const posts = this.getAll();
posts[post.id] = post;
StorageManager.setPosts(posts);
return post;
},
remove(id) {
const posts = this.getAll();
delete posts[id];
StorageManager.setPosts(posts);
},
exists(id) {
return !!this.getAll()[id];
},
count() {
return Object.keys(this.getAll()).length;
},
// 切换置顶状态
togglePin(id) {
const posts = this.getAll();
if (posts[id]) {
posts[id].isPinned = !posts[id].isPinned;
StorageManager.setPosts(posts);
return posts[id].isPinned;
}
return false;
},
// 获取排序后的列表(置顶优先,然后按时间倒序)
getSortedList() {
return Object.values(this.getAll()).sort((a, b) => {
// 置顶的排在前面
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
// 同级别按时间倒序(最新的在前)
return (b.addedAt || 0) - (a.addedAt || 0);
});
},
// 收藏当前页面的作品
collectCurrent() {
const info = PageParser.parsePostInfo();
if (!info) return null;
return this.add(info);
},
// 获取按艺术家分组的作品
getByArtist(artistId) {
const posts = this.getAll();
return Object.values(posts).filter((p) => p.artistId === artistId);
},
};
// ============================================
// AdBlocker - 广告拦截
// ============================================
const AdBlocker = {
init() {
const settings = StorageManager.getSettings();
if (!settings.blockAds) return;
// Hook fluidPlayer 初始化,移除广告配置
this.hookFluidPlayer();
},
hookFluidPlayer() {
// 在 document-start 时执行,确保在 Fluid Player 加载前 hook
const script = document.createElement('script');
script.textContent = `
(function() {
const originalDefineProperty = Object.defineProperty;
Object.defineProperty = function(obj, prop, descriptor) {
if (prop === 'fluidPlayer' && obj === window) {
const originalGetter = descriptor.get;
const originalValue = descriptor.value;
const wrapper = function(...args) {
// 移除广告配置
if (args[1] && args[1].vastOptions) {
delete args[1].vastOptions;
}
if (args[1] && args[1].adList) {
args[1].adList = [];
}
const fn = originalGetter ? originalGetter() : originalValue;
return fn.apply(this, args);
};
if (originalGetter) {
descriptor.get = () => wrapper;
} else {
descriptor.value = wrapper;
}
}
return originalDefineProperty.call(this, obj, prop, descriptor);
};
})();
`;
document.documentElement.appendChild(script);
script.remove();
},
};
// ============================================
// AutoFullscreen - 自动全屏
// ============================================
const AutoFullscreen = {
init() {
const settings = StorageManager.getSettings();
if (!settings.autoFullscreen) return;
// 等待 DOM 加载完成后监听视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
},
setup() {
// 使用 MutationObserver 检测视频元素
const observer = new MutationObserver(() => {
const video = document.querySelector('video');
if (video && !video._autoFullscreenBound) {
video._autoFullscreenBound = true;
this.bindVideo(video);
}
});
observer.observe(document.body, { childList: true, subtree: true });
// 也检查已存在的视频
const video = document.querySelector('video');
if (video) this.bindVideo(video);
},
bindVideo(video) {
video.addEventListener(
'play',
() => {
// 延迟执行,避免与广告冲突
setTimeout(() => {
if (!document.fullscreenElement && !video.paused) {
this.requestFullscreen(video);
}
}, 500);
},
{ once: true },
);
},
requestFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen().catch(() => {});
} else if (element.webkitEnterFullscreen) {
// iOS Safari
element.webkitEnterFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
}
},
};
// ============================================
// UIPanel - 用户界面面板
// ============================================
const UIPanel = {
container: null,
panel: null,
isOpen: false,
activeTab: 'artists',
// SPA 无刷新跳转
navigateTo(url) {
this.close(); // 先关闭面板
// 策略:优先查找页面上已存在的精确链接进行点击(触发 SPA 路由)
// 如果找不到,则回退到 window.location.href(硬跳转)
// 移除不稳定的“链接劫持”逻辑,避免误跳到首页
const targetPath = url.replace(window.location.origin, '');
// 1. 优先找页面上已有的完全匹配的目标链接(且不是 _blank)
const exactLink = document.querySelector(`a[href="${targetPath}"]:not([target="_blank"]), a[href="${url}"]:not([target="_blank"])`);
if (exactLink) {
exactLink.click();
return;
}
// 2. 实在没办法,通过 location 跳转(会刷新,但最稳妥)
window.location.href = url;
},
init() {
this.injectStyles();
this.createFloatingButton();
this.createPanel();
this.createQuickActions();
},
injectStyles() {
// 注入 video.js CSS
try {
const videojsCss = GM_getResourceText('videojs_css');
if (videojsCss) GM_addStyle(videojsCss);
} catch (e) {
console.warn('Failed to load video.js CSS:', e);
}
GM_addStyle(`
:root {
--coomer-bg: #121212;
--coomer-surface: rgba(26, 26, 29, 0.95);
--coomer-card: #1E1E22;
--coomer-border: #333333;
--coomer-primary: #E0AA3E;
--coomer-primary-hover: #F2C94C;
--coomer-text: #E0E0E0;
--coomer-text-sec: #A0A0A0;
--coomer-danger: #ef4444;
--coomer-success: #10b981;
--coomer-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
/* 隐藏公告横幅 */
#announcement-banner {
display: none !important;
}
/* 悬浮按钮 - The Orb */
.coomer-fab {
position: fixed;
width: 50px;
height: 50px;
border-radius: 50%;
background: #121212;
color: var(--coomer-primary);
border: 1px solid var(--coomer-primary);
cursor: pointer;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
touch-action: none;
}
.coomer-fab:hover {
width: 54px;
height: 54px;
background: var(--coomer-primary);
color: #121212;
box-shadow: 0 0 20px rgba(224, 170, 62, 0.4);
}
.coomer-fab.dragging {
cursor: grabbing;
transform: scale(0.95);
transition: none;
}
/* 面板容器 */
.coomer-panel-overlay {
position: fixed;
top: 0;
bg-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 99998;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.coomer-panel-overlay.open {
opacity: 1;
visibility: visible;
}
/* 抽屉式面板 */
.coomer-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
max-height: 75vh;
background: var(--coomer-surface);
backdrop-filter: blur(12px);
border-radius: 20px 20px 0 0;
z-index: 99999;
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif;
color: var(--coomer-text);
box-shadow: var(--coomer-shadow);
border: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: none;
}
.coomer-panel.open {
transform: translateY(0);
}
/* PC 端样式 */
@media (min-width: 768px) {
.coomer-panel {
left: auto;
right: 24px;
bottom: 96px;
width: 420px;
max-height: 600px;
border-radius: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
}
/* 面板头部 */
.coomer-panel-header {
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(18, 18, 18, 0.8);
border-bottom: 1px solid var(--coomer-primary);
border-radius: 20px 20px 0 0;
}
@media (min-width: 768px) {
.coomer-panel-header {
border-radius: 16px 16px 0 0;
}
}
.coomer-panel-title {
font-size: 16px;
font-weight: 700;
color: var(--coomer-primary);
letter-spacing: 0.5px;
text-transform: uppercase;
}
.coomer-panel-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.coomer-panel-settings {
background: transparent;
border: none;
color: var(--coomer-text-sec);
font-size: 16px;
cursor: pointer;
padding: 8px;
line-height: 1;
border-radius: 8px;
transition: all 0.2s;
}
.coomer-panel-settings:hover {
color: var(--coomer-primary);
background: rgba(255, 255, 255, 0.05);
}
.coomer-panel-close {
background: transparent;
border: none;
color: var(--coomer-text-sec);
font-size: 20px;
cursor: pointer;
padding: 8px;
line-height: 1;
border-radius: 8px;
transition: all 0.2s;
z-index: 10;
}
.coomer-panel-close:hover {
color: var(--coomer-primary);
background: rgba(255, 255, 255, 0.05);
}
/* 标签栏 */
.coomer-tabs {
display: flex;
background: #121212;
padding: 4px;
margin: 0;
}
.coomer-tab {
flex: 1;
padding: 10px;
background: transparent;
border: none;
color: var(--coomer-text-sec);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
position: relative;
}
.coomer-tab::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: var(--coomer-primary);
transition: all 0.3s ease;
transform: translateX(-50%);
}
.coomer-tab:hover {
color: var(--coomer-text);
}
.coomer-tab.active {
color: var(--coomer-primary);
}
.coomer-tab.active::after {
width: 40%;
}
/* 面板内容 */
.coomer-panel-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
/* 自定义滚动条 */
.coomer-panel-content::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.coomer-panel-content::-webkit-scrollbar-track {
background: transparent;
}
.coomer-panel-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
transition: background 0.2s;
}
.coomer-panel-content::-webkit-scrollbar-thumb:hover {
background: var(--coomer-primary);
}
/* 空状态 */
.coomer-empty {
text-align: center;
padding: 40px 20px;
color: #666;
}
.coomer-empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
/* 分组组件 */
.coomer-group {
margin-bottom: 16px;
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
padding: 0 12px;
}
.coomer-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
cursor: pointer;
user-select: none;
}
.coomer-group-title {
font-size: 13px;
font-weight: 600;
color: var(--coomer-text-sec);
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
}
.coomer-group-count {
font-size: 12px;
color: var(--coomer-text-sec);
opacity: 0.7;
font-weight: normal;
}
.coomer-group-toggle {
font-size: 12px;
color: var(--coomer-text-sec);
transition: transform 0.2s;
}
.coomer-group-toggle.collapsed {
transform: rotate(-90deg);
}
.coomer-group-content {
overflow: hidden;
transition: max-height 0.3s ease;
}
.coomer-group-content.collapsed {
max-height: 0 !important;
}
.coomer-badge {
display: inline-block;
padding: 2px 6px;
font-size: 10px;
border-radius: 4px;
background: var(--coomer-primary);
color: #000;
font-weight: 700;
}
.coomer-card .coomer-badge {
position: absolute;
top: 28px;
left: 4px;
z-index: 4;
}
/* 艺术家/作品卡片网格 */
.coomer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 16px;
padding-bottom: 12px;
}
.coomer-card {
position: relative;
background: var(--coomer-card);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.coomer-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
border-color: rgba(224, 170, 62, 0.3);
}
.coomer-card-img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
background: #121212;
filter: brightness(0.9);
transition: filter 0.3s;
}
.coomer-card:hover .coomer-card-img {
filter: brightness(1.1);
}
.coomer-card-info {
padding: 10px;
}
.coomer-card-name {
font-size: 12px;
font-weight: 600;
color: var(--coomer-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.coomer-card-meta {
font-size: 10px;
color: var(--coomer-text-sec);
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.coomer-card-count {
position: absolute;
bottom: 48px;
right: 6px;
padding: 2px 6px;
font-size: 10px;
font-weight: 600;
border-radius: 4px;
background: rgba(0, 0, 0, 0.85);
color: var(--coomer-primary);
border: 1px solid rgba(224, 170, 62, 0.3);
z-index: 3;
}
.coomer-card-delete {
position: absolute;
top: 6px;
right: 6px;
width: 26px;
height: 26px;
border-radius: 50%;
background: rgba(20, 20, 20, 0.9);
color: var(--coomer-text);
border: 1px solid #333;
cursor: pointer;
font-size: 14px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s;
z-index: 5;
}
.coomer-card:hover .coomer-card-delete {
opacity: 1;
}
.coomer-card-delete:hover {
background: var(--coomer-danger);
border-color: var(--coomer-danger);
color: white;
transform: scale(1.1);
}
.coomer-card-pin {
position: absolute;
top: 6px;
left: 6px;
width: 26px;
height: 26px;
border-radius: 50%;
background: rgba(20, 20, 20, 0.9);
color: var(--coomer-text-sec);
border: 1px solid #333;
cursor: pointer;
font-size: 12px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s;
z-index: 5;
}
.coomer-card:hover .coomer-card-pin {
opacity: 1;
}
.coomer-card-pin.pinned {
opacity: 1;
border-color: var(--coomer-primary);
color: var(--coomer-primary);
}
.coomer-card-pin:hover {
color: var(--coomer-primary);
border-color: var(--coomer-primary);
transform: scale(1.1);
}
.coomer-card.pinned {
border: 1px solid var(--coomer-primary);
box-shadow: 0 0 10px rgba(224, 170, 62, 0.1);
}
/* 设置项 */
.coomer-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #2d2d44;
}
/* 视频播放列表 */
.coomer-video-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.coomer-video-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--coomer-card);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.coomer-video-item:hover {
background: rgba(255, 255, 255, 0.05);
border-color: var(--coomer-primary);
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.coomer-video-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: var(--coomer-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
transition: all 0.2s;
}
.coomer-video-item:hover .coomer-video-icon {
background: var(--coomer-primary);
color: #000;
transform: scale(1.1);
}
.coomer-video-name {
font-size: 14px;
color: #e0e0e0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
font-weight: 500;
}
/* 视频播放器遮罩层 */
.coomer-video-player-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(8px);
z-index: 100000;
display: flex;
align-items: center;
justify-content: center;
}
.coomer-video-player-wrapper {
position: relative;
width: 90%;
max-width: 1000px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.coomer-video-close {
position: absolute;
top: -50px;
right: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
cursor: pointer;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.coomer-video-close:hover {
background: var(--coomer-primary);
color: #000;
transform: rotate(90deg);
}
.coomer-setting-label {
font-size: 14px;
color: var(--coomer-text);
}
.coomer-toggle {
position: relative;
width: 44px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: background 0.3s;
}
.coomer-toggle.active {
background: var(--coomer-primary);
}
.coomer-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.coomer-toggle.active::after {
transform: translateX(20px);
background: #fff;
}
/* 快捷操作按钮 */
.coomer-quick-actions {
position: fixed;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 99997;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all 0.3s;
pointer-events: none;
}
.coomer-quick-actions.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
}
.coomer-quick-btn {
min-width: 80px;
height: 36px;
padding: 0 16px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
white-space: nowrap;
background: rgba(18, 18, 18, 0.9);
color: var(--coomer-text);
backdrop-filter: blur(4px);
}
.coomer-quick-btn:hover {
transform: translateX(-4px);
border-color: var(--coomer-primary);
color: var(--coomer-primary);
}
.coomer-quick-btn.collect-artist {
border-color: rgba(255, 255, 255, 0.2);
}
.coomer-quick-btn.collect-post {
border-color: rgba(255, 255, 255, 0.2);
}
.coomer-quick-btn.collected {
background: var(--coomer-primary);
color: #000;
border-color: var(--coomer-primary);
font-weight: 700;
box-shadow: 0 0 15px rgba(224, 170, 62, 0.3);
}
/* 移动端隐藏悬浮快捷操作 */
@media (max-width: 767px) {
.coomer-quick-actions {
display: none !important;
}
}
/* 面板内收藏快捷操作(移动端显示) */
.coomer-panel-quick-actions {
display: none;
gap: 8px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.3);
border-bottom: 1px solid var(--coomer-border);
}
@media (max-width: 767px) {
.coomer-panel-quick-actions {
display: flex;
}
}
.coomer-panel-quick-actions .coomer-quick-btn {
flex: 1;
justify-content: center;
}
/* Toast 提示 */
.coomer-toast {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 100000;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
.coomer-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.coomer-toast.success {
background: #4CAF50;
}
.coomer-toast.error {
background: #f44336;
}
/* 操作按钮组 */
.coomer-btn-group {
display: flex;
gap: 12px;
margin-top: 20px;
}
.coomer-btn {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.coomer-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.coomer-btn-primary {
background: var(--coomer-primary);
color: #000;
}
.coomer-btn-primary:hover {
background: var(--coomer-primary-hover);
}
.coomer-btn-danger {
background: transparent;
border: 1px solid var(--coomer-danger);
color: var(--coomer-danger);
}
.coomer-btn-danger:hover {
background: var(--coomer-danger);
color: white;
}
.coomer-btn-secondary {
background: rgba(255, 255, 255, 0.05);
color: var(--coomer-text);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.coomer-btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.1);
}
`);
},
createFloatingButton() {
const fab = document.createElement('button');
fab.className = 'coomer-fab';
fab.innerHTML = '⭐';
fab.title = 'Coomer 佬友严选 (可拖拽移动)';
// 恢复保存的位置
const settings = StorageManager.getSettings();
const pos = settings.fabPosition || DEFAULT_SETTINGS.fabPosition;
fab.style.right = pos.right + 'px';
fab.style.bottom = pos.bottom + 'px';
// 拖拽功能
let isDragging = false;
let hasMoved = false;
let startX, startY, startRight, startBottom;
const onStart = (e) => {
isDragging = true;
hasMoved = false;
fab.classList.add('dragging');
const touch = e.touches ? e.touches[0] : e;
startX = touch.clientX;
startY = touch.clientY;
startRight = parseInt(fab.style.right) || pos.right;
startBottom = parseInt(fab.style.bottom) || pos.bottom;
e.preventDefault();
};
const onMove = (e) => {
if (!isDragging) return;
const touch = e.touches ? e.touches[0] : e;
const deltaX = startX - touch.clientX;
const deltaY = startY - touch.clientY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
hasMoved = true;
}
const newRight = Math.max(10, Math.min(window.innerWidth - 66, startRight + deltaX));
const newBottom = Math.max(10, Math.min(window.innerHeight - 66, startBottom + deltaY));
fab.style.right = newRight + 'px';
fab.style.bottom = newBottom + 'px';
e.preventDefault();
};
const onEnd = () => {
if (!isDragging) return;
isDragging = false;
fab.classList.remove('dragging');
// 保存位置
const newSettings = StorageManager.getSettings();
newSettings.fabPosition = {
right: parseInt(fab.style.right),
bottom: parseInt(fab.style.bottom),
};
StorageManager.setSettings(newSettings);
// 更新快捷操作按钮位置
if (this.updateQuickActionsPosition) {
this.updateQuickActionsPosition();
}
};
// 鼠标事件
fab.addEventListener('mousedown', onStart);
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onEnd);
// 触摸事件
fab.addEventListener('touchstart', onStart, { passive: false });
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onEnd);
// 点击事件(仅在未拖拽时触发)
fab.addEventListener('click', (e) => {
if (hasMoved) {
e.preventDefault();
e.stopPropagation();
return;
}
this.toggle();
});
document.body.appendChild(fab);
this.fab = fab;
},
createPanel() {
// 遮罩层
const overlay = document.createElement('div');
overlay.className = 'coomer-panel-overlay';
overlay.addEventListener('click', () => this.close());
document.body.appendChild(overlay);
this.overlay = overlay;
// 面板
const panel = document.createElement('div');
panel.className = 'coomer-panel';
panel.innerHTML = `
<div class="coomer-panel-header">
<span class="coomer-panel-title">👑 COOMER 臻选</span>
<div class="coomer-panel-header-actions">
<button class="coomer-panel-settings" title="设置">⚙️</button>
<button class="coomer-panel-close">×</button>
</div>
</div>
<div class="coomer-tabs">
<button class="coomer-tab active" data-tab="artists">👩🎨 艺术家</button>
<button class="coomer-tab" data-tab="posts">🎬 作品</button>
</div>
<div class="coomer-panel-quick-actions"></div>
<div class="coomer-panel-content"></div>
`;
// 关闭按钮事件 - 使用更可靠的绑定方式
const closeBtn = panel.querySelector('.coomer-panel-close');
closeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.close();
});
// 设置按钮事件
const settingsBtn = panel.querySelector('.coomer-panel-settings');
settingsBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.switchTab('settings');
});
// 标签页切换事件
panel.querySelectorAll('.coomer-tab').forEach((tab) => {
tab.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.switchTab(tab.dataset.tab);
});
});
// 阻止面板内部点击冒泡到遮罩层
panel.addEventListener('click', (e) => {
e.stopPropagation();
});
document.body.appendChild(panel);
this.panel = panel;
this.contentArea = panel.querySelector('.coomer-panel-content');
// 渲染初始内容
this.renderTab('artists');
},
// SPA URL 变化时重新创建快捷操作按钮
recreateQuickActions() {
// 移除旧的快捷操作按钮
if (this.quickActions) {
this.quickActions.remove();
this.quickActions = null;
this.updateQuickActionsPosition = null;
}
// 延迟一点重新创建(等待页面渲染)
setTimeout(() => {
this.createQuickActions();
}, 500);
},
createQuickActions() {
const container = document.createElement('div');
container.className = 'coomer-quick-actions';
const pageType = PageParser.getCurrentPageType();
const urlInfo = PageParser.parseUrlInfo();
// 首先添加艺术家按钮(会显示在上方)
if (pageType === 'artist' || pageType === 'post') {
const isArtistCollected = urlInfo && ArtistManager.exists(urlInfo.userId);
const collectArtistBtn = document.createElement('button');
collectArtistBtn.className = `coomer-quick-btn collect-artist ${isArtistCollected ? 'collected' : ''}`;
collectArtistBtn.innerHTML = isArtistCollected ? '✓ 已藏艺术家' : '👤 艺术家';
collectArtistBtn.title = isArtistCollected ? '点击取消收藏艺术家' : '收藏当前艺术家';
collectArtistBtn.addEventListener('click', () => this.handleCollectArtist(collectArtistBtn));
container.appendChild(collectArtistBtn);
}
// 然后添加作品按钮(会显示在下方,更靠近主按钮)
if (pageType === 'post') {
const isPostCollected = urlInfo && PostManager.exists(urlInfo.postId);
const collectPostBtn = document.createElement('button');
collectPostBtn.className = `coomer-quick-btn collect-post ${isPostCollected ? 'collected' : ''}`;
collectPostBtn.innerHTML = isPostCollected ? '✓ 已藏作品' : '🎬 作品';
collectPostBtn.title = isPostCollected ? '点击取消收藏作品' : '收藏当前作品';
collectPostBtn.addEventListener('click', () => this.handleCollectPost(collectPostBtn));
container.appendChild(collectPostBtn);
}
if (container.children.length > 0) {
document.body.appendChild(container);
this.quickActions = container;
// 更新快捷操作按钮位置
const updateQuickActionsPosition = () => {
const fabRight = parseInt(this.fab.style.right) || 20;
const fabBottom = parseInt(this.fab.style.bottom) || 80;
container.style.right = fabRight + 'px';
container.style.bottom = fabBottom + 65 + 'px'; // 悬浮按钮高度 + 间距
};
// 初始化位置
updateQuickActionsPosition();
// 显示/隐藏快捷操作
const showQuickActions = () => {
if (this.isOpen) return; // 面板打开时不显示
updateQuickActionsPosition();
container.classList.add('show');
};
const hideQuickActions = () => {
setTimeout(() => {
if (!container.matches(':hover') && !this.fab.matches(':hover')) {
container.classList.remove('show');
}
}, 150);
};
// 悬浮按钮悬停时显示快捷操作
this.fab.addEventListener('mouseenter', showQuickActions);
this.fab.addEventListener('mouseleave', hideQuickActions);
container.addEventListener('mouseenter', () => container.classList.add('show'));
container.addEventListener('mouseleave', hideQuickActions);
// 保存位置更新函数供拖拽时调用
this.updateQuickActionsPosition = updateQuickActionsPosition;
}
},
// 更新快捷操作按钮的收藏状态
updateQuickActionsState() {
if (!this.quickActions) return;
const urlInfo = PageParser.parseUrlInfo();
if (!urlInfo) return;
// 更新艺术家按钮状态
const artistBtn = this.quickActions.querySelector('.collect-artist');
if (artistBtn) {
const isCollected = ArtistManager.exists(urlInfo.userId);
artistBtn.classList.toggle('collected', isCollected);
artistBtn.innerHTML = isCollected ? '✓ 已藏艺术家' : '👤 艺术家';
artistBtn.title = isCollected ? '点击取消收藏艺术家' : '收藏当前艺术家';
}
// 更新作品按钮状态
const postBtn = this.quickActions.querySelector('.collect-post');
if (postBtn && urlInfo.postId) {
const isCollected = PostManager.exists(urlInfo.postId);
postBtn.classList.toggle('collected', isCollected);
postBtn.innerHTML = isCollected ? '✓ 已藏作品' : '🎬 作品';
postBtn.title = isCollected ? '点击取消收藏作品' : '收藏当前作品';
}
},
handleCollectArtist(btn) {
const urlInfo = PageParser.parseUrlInfo();
if (!urlInfo) return;
if (ArtistManager.exists(urlInfo.userId)) {
ArtistManager.remove(urlInfo.userId);
btn.classList.remove('collected');
btn.innerHTML = '👤 艺术家';
btn.title = '收藏当前艺术家';
this.showToast('已取消收藏', 'success');
} else {
const artist = ArtistManager.collectCurrent();
if (artist) {
btn.classList.add('collected');
btn.innerHTML = '✓ 已藏艺术家';
btn.title = '点击取消收藏艺术家';
this.showToast(`已收藏 ${artist.nickname}`, 'success');
} else {
this.showToast('收藏失败,请稍后重试', 'error');
}
}
},
handleCollectPost(btn) {
const urlInfo = PageParser.parseUrlInfo();
if (!urlInfo || !urlInfo.postId) return;
if (PostManager.exists(urlInfo.postId)) {
PostManager.remove(urlInfo.postId);
btn.classList.remove('collected');
btn.innerHTML = '🎬 作品';
btn.title = '收藏当前作品';
this.showToast('已取消收藏', 'success');
} else {
const post = PostManager.collectCurrent();
if (post) {
btn.classList.add('collected');
btn.innerHTML = '✓ 已藏作品';
btn.title = '点击取消收藏作品';
this.showToast('作品已收藏', 'success');
} else {
this.showToast('收藏失败,请稍后重试', 'error');
}
}
},
// 更新面板内快捷操作(移动端)
updatePanelQuickActions() {
const container = this.panel.querySelector('.coomer-panel-quick-actions');
if (!container) return;
const pageType = PageParser.getCurrentPageType();
const urlInfo = PageParser.parseUrlInfo();
// 清空容器
container.innerHTML = '';
// 如果不在艺术家或作品页面,不显示按钮
if (pageType !== 'artist' && pageType !== 'post') {
return;
}
// 艺术家收藏按钮
if (pageType === 'artist' || pageType === 'post') {
const isArtistCollected = urlInfo && ArtistManager.exists(urlInfo.userId);
const artistBtn = document.createElement('button');
artistBtn.className = `coomer-quick-btn collect-artist ${isArtistCollected ? 'collected' : ''}`;
artistBtn.innerHTML = isArtistCollected ? '✓ 已藏艺术家' : '👤 艺术家';
artistBtn.addEventListener('click', () => this.handleCollectArtist(artistBtn));
container.appendChild(artistBtn);
}
// 作品收藏按钮
if (pageType === 'post') {
const isPostCollected = urlInfo && PostManager.exists(urlInfo.postId);
const postBtn = document.createElement('button');
postBtn.className = `coomer-quick-btn collect-post ${isPostCollected ? 'collected' : ''}`;
postBtn.innerHTML = isPostCollected ? '✓ 已藏作品' : '🎬 作品';
postBtn.addEventListener('click', () => this.handleCollectPost(postBtn));
container.appendChild(postBtn);
}
},
toggle() {
this.isOpen ? this.close() : this.open();
},
open() {
this.isOpen = true;
this.overlay.classList.add('open');
this.panel.classList.add('open');
// 隐藏悬浮快捷操作按钮
if (this.quickActions) {
this.quickActions.style.display = 'none';
}
// 更新面板内快捷操作(移动端)
this.updatePanelQuickActions();
this.renderTab(this.activeTab);
},
close() {
this.isOpen = false;
this.overlay.classList.remove('open');
this.panel.classList.remove('open');
// 恢复快捷操作按钮
if (this.quickActions) {
this.quickActions.style.display = '';
}
},
switchTab(tabName) {
this.activeTab = tabName;
this.panel.querySelectorAll('.coomer-tab').forEach((tab) => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
this.renderTab(tabName);
},
renderTab(tabName) {
const content = this.contentArea;
content.innerHTML = '';
switch (tabName) {
case 'artists':
this.renderArtistsTab(content);
break;
case 'posts':
this.renderPostsTab(content);
break;
case 'settings':
this.renderSettingsTab(content);
break;
}
},
renderArtistsTab(container) {
const myArtists = ArtistManager.getSortedList();
const presetArtists = PRESET_ARTISTS;
const settings = StorageManager.getSettings();
// 如果都没有内容,显示空状态
if (myArtists.length === 0 && presetArtists.length === 0) {
container.innerHTML = `
<div class="coomer-empty">
<div class="coomer-empty-icon">📌</div>
<div>暂无收藏的艺术家</div>
<div style="font-size: 12px; margin-top: 8px;">访问艺术家主页并点击收藏按钮</div>
</div>
`;
return;
}
// 渲染预置艺术家分组(佬友严选)
if (presetArtists.length > 0) {
const presetGroup = this.createArtistGroup(
'📌 佬友严选',
presetArtists,
settings.presetCollapsed,
'preset',
true, // 是预置列表
);
container.appendChild(presetGroup);
}
// 渲染我的收藏分组
if (myArtists.length > 0) {
const myGroup = this.createArtistGroup(
'⭐ 我的收藏',
myArtists,
settings.myCollectionCollapsed,
'myCollection',
false, // 不是预置列表
);
container.appendChild(myGroup);
} else if (presetArtists.length > 0) {
// 有预置但没有自己收藏的,显示提示
const emptyHint = document.createElement('div');
emptyHint.style.cssText = 'text-align: center; color: #888; padding: 20px; font-size: 12px;';
emptyHint.textContent = '点击艺术家卡片访问主页,或使用快捷按钮收藏';
container.appendChild(emptyHint);
}
},
// 创建艺术家分组
createArtistGroup(title, artists, isCollapsed, settingKey, isPreset) {
const group = document.createElement('div');
group.className = 'coomer-group';
// 分组标题
const header = document.createElement('div');
header.className = 'coomer-group-header';
header.innerHTML = `
<div class="coomer-group-title">
${title}
<span class="coomer-group-count">(${artists.length})</span>
</div>
<span class="coomer-group-toggle ${isCollapsed ? 'collapsed' : ''}">▼</span>
`;
header.addEventListener('click', () => {
const content = group.querySelector('.coomer-group-content');
const toggle = header.querySelector('.coomer-group-toggle');
const isCurrentlyCollapsed = content.classList.contains('collapsed');
// 切换折叠状态
content.classList.toggle('collapsed', !isCurrentlyCollapsed);
toggle.classList.toggle('collapsed', !isCurrentlyCollapsed);
// 设置 max-height
content.style.maxHeight = isCurrentlyCollapsed ? content.scrollHeight + 'px' : '0';
// 保存折叠状态
const settings = StorageManager.getSettings();
settings[settingKey + 'Collapsed'] = !isCurrentlyCollapsed;
StorageManager.setSettings(settings);
});
group.appendChild(header);
// 分组内容
const content = document.createElement('div');
content.className = `coomer-group-content ${isCollapsed ? 'collapsed' : ''}`;
const grid = document.createElement('div');
grid.className = 'coomer-grid';
artists.forEach((artist) => {
const card = this.createArtistCard(artist, isPreset);
grid.appendChild(card);
});
content.appendChild(grid);
group.appendChild(content);
return group;
},
// 创建艺术家卡片
createArtistCard(artist, isPreset = false) {
const isInMyCollection = !isPreset && ArtistManager.exists(artist.id);
const card = document.createElement('div');
card.className = `coomer-card ${artist.isPinned ? 'pinned' : ''}`;
if (isPreset) {
// 预置艺术家卡片
const isAlreadyCollected = ArtistManager.exists(artist.id);
card.innerHTML = `
<img class="coomer-card-img" src="${artist.avatar || 'https://via.placeholder.com/100?text=No+Image'}"
alt="${artist.nickname}" loading="lazy"
onerror="this.src='https://via.placeholder.com/100?text=Error'">
<div class="coomer-card-info">
<div class="coomer-card-name">${artist.nickname}</div>
<div class="coomer-card-meta">${artist.platform} ${isAlreadyCollected ? '✓' : ''}</div>
</div>
`;
// 点击预置卡片:跳转到主页
card.addEventListener('click', () => {
this.navigateTo(artist.homepageUrl);
});
// 右键添加到收藏
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (!ArtistManager.exists(artist.id)) {
ArtistManager.add({
...artist,
isPreset: false,
addedAt: Date.now(),
});
this.renderTab('artists');
this.showToast(`已添加 ${artist.nickname} 到我的收藏`, 'success');
} else {
this.showToast('该艺术家已在收藏中', 'error');
}
});
card.title = '点击访问主页,右键添加到收藏';
} else {
// 我的收藏卡片
card.innerHTML = `
<button class="coomer-card-pin ${artist.isPinned ? 'pinned' : ''}" title="${artist.isPinned ? '取消置顶' : '置顶'}">📌</button>
<button class="coomer-card-delete" title="删除">×</button>
<img class="coomer-card-img" src="${artist.avatar || 'https://via.placeholder.com/100?text=No+Image'}"
alt="${artist.nickname}" loading="lazy"
onerror="this.src='https://via.placeholder.com/100?text=Error'">
<div class="coomer-card-info">
<div class="coomer-card-name">${artist.nickname}</div>
<div class="coomer-card-meta">${artist.platform}</div>
</div>
`;
// 点击卡片跳转到艺术家主页
card.addEventListener('click', (e) => {
if (e.target.classList.contains('coomer-card-delete') || e.target.classList.contains('coomer-card-pin')) return;
this.navigateTo(artist.homepageUrl);
});
// 置顶按钮
card.querySelector('.coomer-card-pin').addEventListener('click', (e) => {
e.stopPropagation();
const isPinned = ArtistManager.togglePin(artist.id);
this.renderTab('artists');
this.showToast(isPinned ? '已置顶' : '已取消置顶', 'success');
});
// 删除按钮
card.querySelector('.coomer-card-delete').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`确定要删除 ${artist.nickname} 吗?`)) {
ArtistManager.remove(artist.id);
this.renderTab('artists');
this.updateQuickActionsState();
this.showToast('已删除', 'success');
}
});
}
return card;
},
renderPostsTab(container) {
const posts = PostManager.getSortedList(); // 使用排序后的列表
if (posts.length === 0) {
container.innerHTML = `
<div class="coomer-empty">
<div class="coomer-empty-icon">🎬</div>
<div>暂无收藏的作品</div>
<div style="font-size: 12px; margin-top: 8px;">访问作品页面并点击收藏按钮</div>
</div>
`;
return;
}
const grid = document.createElement('div');
grid.className = 'coomer-grid';
posts.forEach((post) => {
const card = document.createElement('div');
card.className = `coomer-card ${post.isPinned ? 'pinned' : ''}`;
// 媒体数量角标
const mediaCount = post.mediaUrls ? post.mediaUrls.length : 0;
const countBadge = mediaCount > 0 ? `<span class="coomer-card-count">${post.type === 'video' ? '🎬' : '📷'} ${mediaCount}</span>` : '';
card.innerHTML = `
<button class="coomer-card-pin ${post.isPinned ? 'pinned' : ''}" title="${post.isPinned ? '取消置顶' : '置顶'}">📌</button>
<button class="coomer-card-delete" title="删除">×</button>
${countBadge}
<img class="coomer-card-img" src="${post.thumb || 'https://via.placeholder.com/100?text=' + (post.type === 'video' ? '🎬' : '📷')}"
alt="${post.title}" loading="lazy"
onerror="this.src='https://via.placeholder.com/100?text=${post.type === 'video' ? '🎬' : '📷'}'">
<div class="coomer-card-info">
<div class="coomer-card-name">${post.title}</div>
<div class="coomer-card-meta">${post.type === 'video' ? '🎬' : '🖼️'} ${post.artistName}</div>
</div>
`;
// 点击卡片查看详情
card.addEventListener('click', (e) => {
if (e.target.classList.contains('coomer-card-delete') || e.target.classList.contains('coomer-card-pin')) return;
this.showPostDetail(post);
});
// 置顶按钮
card.querySelector('.coomer-card-pin').addEventListener('click', (e) => {
e.stopPropagation();
const isPinned = PostManager.togglePin(post.id);
this.renderTab('posts');
this.showToast(isPinned ? '已置顶' : '已取消置顶', 'success');
});
// 删除按钮
card.querySelector('.coomer-card-delete').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`确定要删除这个作品吗?`)) {
PostManager.remove(post.id);
this.renderTab('posts');
this.updateQuickActionsState();
this.showToast('已删除', 'success');
}
});
grid.appendChild(card);
});
container.appendChild(grid);
},
showPostDetail(post) {
// 根据类型生成预览内容
let previewContent = '';
if (post.type === 'video' && post.mediaUrls && post.mediaUrls.length > 0) {
// 视频作品:显示播放按钮列表
const videoList = post.mediaUrls
.map((url, index) => {
// 单个视频显示"点击播放",多个视频显示"视频 1"、"视频 2"...
const displayName = post.mediaUrls.length === 1 ? '点击播放' : `视频 ${index + 1} 点击播放`;
return `
<div class="coomer-video-item" data-url="${url}">
<span class="coomer-video-icon">▶️</span>
<span class="coomer-video-name">${displayName}</span>
</div>
`;
})
.join('');
previewContent = `
<div class="coomer-video-list">
${videoList}
</div>
`;
} else if (post.thumb) {
// 图片作品:显示缩略图
previewContent = `<img class="coomer-card-img" src="${post.thumb}" style="width: 100%; border-radius: 8px; aspect-ratio: 16/9;">`;
} else {
// 无预览时显示占位符
previewContent = `<div style="width: 100%; height: 120px; background: #252540; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 48px;">${
post.type === 'video' ? '🎬' : '📷'
}</div>`;
}
this.contentArea.innerHTML = `
<div style="margin-bottom: 16px;">
<button class="coomer-btn coomer-btn-secondary" id="back-btn">← 返回</button>
</div>
${previewContent}
<h3 style="margin: 12px 0 8px; font-size: 16px;">${post.title}</h3>
<p style="color: #888; font-size: 14px;">艺术家: ${
post.platform ? `<a href="#" id="artist-link" style="color: var(--coomer-primary); text-decoration: none; cursor: pointer;">${post.artistName}</a>` : post.artistName
}</p>
<div class="coomer-btn-group">
<button class="coomer-btn coomer-btn-primary" id="open-btn">📖 打开页面</button>
<button class="coomer-btn coomer-btn-secondary" id="copy-btn">📋 复制下载链接 (${post.mediaUrls ? post.mediaUrls.length : 0})</button>
</div>
<div class="coomer-btn-group">
<button class="coomer-btn coomer-btn-danger" id="delete-btn">🗑️ 删除</button>
</div>
`;
this.contentArea.querySelector('#back-btn').addEventListener('click', () => {
this.renderTab('posts');
});
this.contentArea.querySelector('#open-btn').addEventListener('click', () => {
this.navigateTo(post.pageUrl);
});
// 艺术家链接点击事件(仅在有 platform 数据时绑定)
if (post.platform) {
this.contentArea.querySelector('#artist-link').addEventListener('click', (e) => {
e.preventDefault();
const artistUrl = `https://coomer.st/${post.platform}/user/${post.artistId}`;
this.navigateTo(artistUrl);
});
}
this.contentArea.querySelector('#copy-btn').addEventListener('click', () => {
const links = post.mediaUrls.join('\n');
GM_setClipboard(links);
const count = post.mediaUrls.length;
this.showToast(`已复制 ${count} 条下载链接`, 'success');
});
this.contentArea.querySelector('#delete-btn').addEventListener('click', () => {
if (confirm('确定要删除这个作品吗?')) {
PostManager.remove(post.id);
this.renderTab('posts');
this.updateQuickActionsState();
this.showToast('已删除', 'success');
}
});
// 视频播放按钮点击事件 - 使用 video.js 播放
this.contentArea.querySelectorAll('.coomer-video-item').forEach((item) => {
item.addEventListener('click', () => {
const url = item.dataset.url;
this.playVideo(url);
});
});
},
// 使用 video.js 播放视频
playVideo(url) {
// 注入 Video.js 自定义暗黑主题
if (!document.getElementById('coomer-video-theme')) {
const style = document.createElement('style');
style.id = 'coomer-video-theme';
style.textContent = `
.video-js .vjs-big-play-button {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
line-height: 2em;
height: 2em;
width: 2em;
border-radius: 50%;
background-color: rgba(224, 170, 62, 0.9);
border: none;
font-size: 3.5em;
box-shadow: 0 0 30px rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
}
.video-js .vjs-big-play-button:hover {
background-color: #E0AA3E;
transform: translate(-50%, -50%) scale(1.1);
}
.video-js .vjs-control-bar {
background-color: rgba(0, 0, 0, 0.8);
border-radius: 8px;
margin: 12px;
width: calc(100% - 24px);
bottom: 0;
height: 48px;
display: flex;
align-items: center;
}
.video-js .vjs-button {
color: #E0E0E0;
}
.video-js .vjs-button:hover {
color: #E0AA3E;
}
.video-js .vjs-slider {
background-color: rgba(255, 255, 255, 0.2);
}
.video-js .vjs-play-progress {
background-color: #E0AA3E;
}
.video-js .vjs-play-progress:before {
color: #E0AA3E;
}
.video-js .vjs-load-progress {
background-color: rgba(255, 255, 255, 0.1);
}
.video-js .vjs-load-progress div {
background: rgba(255, 255, 255, 0.1);
}
/* 快进快退按钮图标适配 */
.video-js .vjs-skip-backward-15,
.video-js .vjs-skip-forward-15 {
font-family: FontAwesome, sans-serif; /* fallback */
}
.video-js .vjs-skip-backward-15:before {
content: "↺";
font-size: 1.5em; /* 调整图标大小 */
line-height: 1.8;
}
.video-js .vjs-skip-forward-15:before {
content: "↻";
font-size: 1.5em; /* 调整图标大小 */
line-height: 1.8;
}
/* 确保按钮在 hover 时变为金色 */
.video-js .vjs-skip-backward-15:hover,
.video-js .vjs-skip-forward-15:hover {
color: #E0AA3E;
}
`;
document.head.appendChild(style);
}
// 创建播放器容器
const playerContainer = document.createElement('div');
playerContainer.className = 'coomer-video-player-overlay';
playerContainer.innerHTML = `
<div class="coomer-video-player-wrapper">
<button class="coomer-video-close">×</button>
<video id="coomer-video-player" class="video-js vjs-big-play-centered vjs-fluid" controls preload="auto">
<source src="${url}" type="video/mp4">
</video>
</div>
`;
document.body.appendChild(playerContainer);
// 初始化 video.js 播放器
const player = videojs('coomer-video-player', {
fluid: true,
autoplay: true,
controls: true,
playbackRates: [0.5, 1.0, 1.5, 2.0], // 倍速播放
userActions: {
doubleClick: true, // 双击全屏
hotkeys: true, // 启用键盘热键
},
controlBar: {
children: [
'playToggle',
'skipBackward', // 后退 15s
'skipForward', // 快进 15s
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'playbackRateMenuButton',
'pictureInPictureToggle',
'fullscreenToggle',
],
skipButtons: {
forward: 15,
backward: 15,
},
},
});
// 自动聚焦以便键盘控制
player.ready(() => {
player.focus();
});
const closePlayer = () => {
if (player) {
player.dispose();
}
if (playerContainer && playerContainer.parentNode) {
playerContainer.parentNode.removeChild(playerContainer);
}
document.removeEventListener('keydown', escHandler);
};
// 关闭按钮
playerContainer.querySelector('.coomer-video-close').addEventListener('click', closePlayer);
// 点击遮罩关闭
playerContainer.addEventListener('click', (e) => {
if (e.target === playerContainer) {
closePlayer();
}
});
// ESC 键关闭
const escHandler = (e) => {
if (e.key === 'Escape') {
closePlayer();
}
};
document.addEventListener('keydown', escHandler);
},
renderSettingsTab(container) {
const settings = StorageManager.getSettings();
container.innerHTML = `
<div class="coomer-setting-item">
<span class="coomer-setting-label">🚫 屏蔽广告</span>
<div class="coomer-toggle ${settings.blockAds ? 'active' : ''}" data-key="blockAds"></div>
</div>
<div class="coomer-setting-item">
<span class="coomer-setting-label">📺 自动全屏</span>
<div class="coomer-toggle ${settings.autoFullscreen ? 'active' : ''}" data-key="autoFullscreen"></div>
</div>
<div style="margin-top: 24px;">
<div class="coomer-btn-group">
<button class="coomer-btn coomer-btn-secondary" id="export-btn">📤 导出数据</button>
<button class="coomer-btn coomer-btn-secondary" id="import-btn">📥 导入数据</button>
</div>
</div>
<div style="margin-top: 16px; text-align: center; color: #666; font-size: 12px;">
<div>Coomer 佬友严选 v1.0.0</div>
<div style="margin-top: 4px;">艺术家: ${ArtistManager.count()} | 作品: ${PostManager.count()}</div>
</div>
`;
// 绑定开关事件
container.querySelectorAll('.coomer-toggle').forEach((toggle) => {
toggle.addEventListener('click', () => {
const key = toggle.dataset.key;
const newSettings = StorageManager.getSettings();
newSettings[key] = !newSettings[key];
StorageManager.setSettings(newSettings);
toggle.classList.toggle('active');
this.showToast('设置已保存', 'success');
});
});
// 导出数据
container.querySelector('#export-btn').addEventListener('click', () => {
const data = {
artists: ArtistManager.getAll(),
posts: PostManager.getAll(),
settings: StorageManager.getSettings(),
exportedAt: Date.now(),
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `coomer-backup-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
this.showToast('数据已导出', 'success');
});
// 导入数据
container.querySelector('#import-btn').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const data = JSON.parse(evt.target.result);
if (data.artists) StorageManager.setArtists(data.artists);
if (data.posts) StorageManager.setPosts(data.posts);
if (data.settings) StorageManager.setSettings(data.settings);
this.showToast('数据已导入', 'success');
this.renderTab('settings');
} catch {
this.showToast('导入失败,文件格式错误', 'error');
}
};
reader.readAsText(file);
};
input.click();
});
},
showToast(message, type = '') {
// 移除已有的 toast
document.querySelectorAll('.coomer-toast').forEach((t) => t.remove());
const toast = document.createElement('div');
toast.className = `coomer-toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add('show');
});
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 2000);
},
};
// ============================================
// 初始化
// ============================================
function init() {
// 广告拦截需要在 document-start 时执行
AdBlocker.init();
// 等待 DOM 加载完成后初始化其他功能
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI);
} else {
initUI();
}
}
function initUI() {
// 等待页面完全渲染(SPA 需要延迟)
setTimeout(() => {
UIPanel.init();
AutoFullscreen.init();
// 监听 SPA URL 变化
if (window.onurlchange === null) {
window.addEventListener('urlchange', (info) => {
// URL 变化时重新创建快捷操作按钮
UIPanel.recreateQuickActions();
});
}
}, 1000);
}
init();
})();