您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
이미지/태그 우클릭 프리뷰 기능, 편리한 태그 복사
// ==UserScript== // @name Rapidbooru // @name:ko Rapidbooru // @namespace Violentmonkey Scripts // @match *://danbooru.donmai.us/* // @grant none // @version 250710.2 // @author - // @license MIT // @description 이미지/태그 우클릭 프리뷰 기능, 편리한 태그 복사 // @icon https://i.imgur.com/FxoAjfI.png // ==/UserScript== (function() { 'use strict'; // 프리뷰 및 관련 태그 페이지에서 사용할 태그 리스트 let selectedTags = []; let relatedPageSelectedTags = []; // [추가] 사이드바 태그 선택 임시 리스트 let sidebarSelectedTags = []; // 태그 카테고리별 색상 매핑 (사이드바 태그 컨테이너용) const tagColors = { artist: '#e67e22', copyright: '#2980b9', character: '#8e44ad', general: '#2ecc71', meta: '#95a5a6' }; // CSS 스타일 추가 function addStyles() { const style = document.createElement('style'); style.textContent = ` #rapidbooru-settings-btn { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: var(--rapidbooru-accent-color); border: none; border-radius: 50%; color: var(--rapidbooru-text-color); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10002; box-shadow: 0 4px 8px rgba(0,0,0,0.2); transition: all 0.3s ease; } #rapidbooru-settings-btn:hover { background: var(--rapidbooru-accent-hover-color); transform: translateY(-2px); } /* Consolas 폰트 적용 */ * { font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace !important; } /* 버튼 스타일 개선 */ .ui-button, button, input[type="submit"], input[type="button"], .button { background: linear-gradient(135deg, var(--rapidbooru-accent-color) 0%, var(--rapidbooru-accent-hover-color) 100%) !important; border: none !important; border-radius: 6px !important; color: var(--rapidbooru-text-color) !important; padding: 8px 16px !important; margin: 4px !important; cursor: pointer !important; transition: all 0.3s ease !important; font-weight: 500 !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; } .ui-button:hover, button:hover, input[type="submit"]:hover, input[type="button"]:hover, .button:hover { transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important; background: linear-gradient(135deg, var(--rapidbooru-accent-hover-color) 0%, var(--rapidbooru-accent-color) 100%) !important; } /* 네비게이션 버튼 정렬 */ .navbar, .nav, .navigation { display: flex !important; align-items: center !important; gap: 8px !important; flex-wrap: wrap !important; } /* 검색 폼 정렬 */ .search-form, #search-form { display: flex !important; align-items: center !important; gap: 8px !important; margin: 10px 0 !important; } /* 페이지네이션 정렬 */ .paginator, .pagination { display: flex !important; justify-content: center !important; align-items: center !important; gap: 4px !important; margin: 20px 0 !important; } /* 프리뷰 오버레이 스타일 */ .rapidbooru-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(10px); z-index: 10000; display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; } .rapidbooru-overlay.show { opacity: 1; } .rapidbooru-preview { width: 75vw; height: 75vh; background: var(--rapidbooru-bg-color); border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.5); display: flex; overflow: hidden; position: relative; } .rapidbooru-close, .rapidbooru-newtab { position: absolute; top: 15px; width: 40px; height: 40px; border-radius: 50%; background: rgba(255,255,255,0.2); border: none; color: var(--rapidbooru-text-color); font-size: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10001; transition: background 0.3s ease; } .rapidbooru-close:hover, .rapidbooru-newtab:hover { background: rgba(255,255,255,0.3); } .rapidbooru-close { right: 15px; } .rapidbooru-newtab { right: 61px; } .rapidbooru-newtab svg { display: block; } .rapidbooru-tags { width: 40%; padding: 20px; background: var(--rapidbooru-secondary-bg-color); overflow-y: auto; border-right: 1px solid var(--rapidbooru-border-color); } .rapidbooru-content { width: 60%; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; position: relative; } .rapidbooru-image { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; } .rapidbooru-wiki { width: 100%; height: 80%; border: none; border-radius: 8px; background: white; } .rapidbooru-buttons { position: absolute; bottom: 20px; right: 20px; display: flex; gap: 10px; } .rapidbooru-btn { background: linear-gradient(135deg, var(--rapidbooru-accent-color) 0%, var(--rapidbooru-accent-hover-color) 100%); border: none; border-radius: 6px; color: var(--rapidbooru-text-color); padding: 10px 20px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; text-decoration: none; display: inline-block; } .rapidbooru-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); background: linear-gradient(135deg, var(--rapidbooru-accent-hover-color) 0%, var(--rapidbooru-accent-color) 100%); } /* 태그 카테고리 박스 스타일 */ .rapidbooru-category-box { margin-bottom: 10px; padding: 8px 12px; background: var(--rapidbooru-secondary-bg-color); border-radius: 4px; border-left: 4px solid var(--rapidbooru-accent-color); font-size: 12px; color: var(--rapidbooru-text-color); font-family: 'Consolas', monospace; min-height: 20px; display: flex; align-items: center; gap: 8px; } .rapidbooru-category-tags-wrapper { flex-grow: 1; word-break: break-word; } .rapidbooru-category-copy-btn { flex-shrink: 0; width: 28px; height: 28px; background: #555 !important; padding: 0 !important; margin: 0 !important; display: flex; align-items: center; justify-content: center; box-shadow: none !important; } .rapidbooru-category-copy-btn:hover { background: #666 !important; transform: translateY(-1px) !important; box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; } .rapidbooru-category-copy-btn.copied, .rapidbooru-category-copy-btn.copied:hover { background: #28a745 !important; transform: translateY(0) !important; } .rapidbooru-category-copy-btn svg { width: 16px; height: 16px; fill: var(--rapidbooru-text-color); } .rapidbooru-category-label { color: var(--rapidbooru-accent-color); font-weight: bold; margin-right: 5px; } /* 태그 버튼 스타일 */ .rapidbooru-tag-button { display: inline-block; background: var(--rapidbooru-border-color); color: var(--rapidbooru-text-color); padding: 6px 10px; margin: 3px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; font-family: 'Consolas', monospace; user-select: none; text-decoration: none; } .rapidbooru-tag-button:hover { background: #555; transform: translateY(-1px); } .rapidbooru-tag-button.selected { background: var(--rapidbooru-accent-color); border-color: var(--rapidbooru-accent-hover-color); box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); } .rapidbooru-tag-button.selected:hover { background: var(--rapidbooru-accent-hover-color); } /* 카테고리 태그(artist/style/character) 링크는 선택 효과 없음 */ .rapidbooru-category-tags-wrapper a.rapidbooru-tag-button.selected { background: var(--rapidbooru-border-color) !important; border-color: transparent !important; box-shadow: none !important; } .rapidbooru-category-tags-wrapper a.rapidbooru-tag-button:hover { background: #555 !important; } /* 메인 태그 컨테이너 */ .rapidbooru-main-tags { background: var(--rapidbooru-bg-color); padding: 15px; border-radius: 6px; margin-top: 10px; max-height: 200px; overflow-y: auto; } /* 복사 버튼 개선 */ .rapidbooru-copy-btn { margin-top: 15px; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); border: none; color: var(--rapidbooru-text-color); padding: 10px 20px; border-radius: 6px; cursor: pointer; font-family: 'Consolas', monospace; font-weight: 500; transition: all 0.3s ease; width: 100%; } .rapidbooru-copy-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); background: linear-gradient(135deg, #218838 0%, #1e7e34 100%); } .rapidbooru-copy-btn.copied { background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%); } /* 액션 버튼 컨테이너 */ .rapidbooru-action-buttons { display: flex; gap: 10px; margin-top: 15px; } .rapidbooru-action-buttons .rapidbooru-copy-btn { width: auto; /* Override width */ flex-grow: 1; /* Allow buttons to grow */ } /* Related Tags 페이지용 태그 버튼 스타일 */ #rapidbooru-related-tags-buttons .rapidbooru-tag-button { background: #fbeded; color: #495057; border: 1px solid #f5b8b8; padding: 4px 8px; margin: 2px; } #rapidbooru-related-tags-buttons .rapidbooru-tag-button.selected { background: #f5b8b8; color: #212529; border-color: #e5a7a7; } /* I'm Feeling Lucky! 버튼 스타일 */ #rapidbooru-lucky-btn { position: fixed; top: 20px; right: 20px; z-index: 10003; width: 180px; height: 50px; background: var(--rapidbooru-accent-color, #8454cc); color: var(--rapidbooru-text-color, #fff); border: none; border-radius: 25px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); font-weight: bold; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s; gap: 8px; } #rapidbooru-lucky-btn:hover { background: var(--rapidbooru-accent-hover-color, #56687a); } #rapidbooru-lucky-btn:disabled { background: #aaa; cursor: not-allowed; } `; document.head.appendChild(style); } // 태그를 카테고리별로 분류하는 함수 function categorizeTags(tags, postData) { const categories = { artist: [], copyright: [], character: [], general: [], meta: [] }; if (postData) { // API 데이터에서 카테고리 정보 사용 const tagString = postData.tag_string || ''; const artistTags = postData.tag_string_artist ? postData.tag_string_artist.split(' ') : []; const copyrightTags = postData.tag_string_copyright ? postData.tag_string_copyright.split(' ') : []; const characterTags = postData.tag_string_character ? postData.tag_string_character.split(' ') : []; const generalTags = postData.tag_string_general ? postData.tag_string_general.split(' ') : []; const metaTags = postData.tag_string_meta ? postData.tag_string_meta.split(' ') : []; categories.artist = artistTags.filter(tag => tag.length > 0); categories.copyright = copyrightTags.filter(tag => tag.length > 0); categories.character = characterTags.filter(tag => tag.length > 0); categories.general = generalTags.filter(tag => tag.length > 0); categories.meta = metaTags.filter(tag => tag.length > 0); } else { // 기본적으로 모든 태그를 general로 분류 categories.general = tags.filter(tag => tag.length > 0); } return categories; } // 태그 버튼들을 생성하는 함수 function createTagButtons(tags) { const container = document.createElement('div'); container.className = 'rapidbooru-main-tags'; tags.forEach((tag, index) => { const button = document.createElement('span'); button.className = 'rapidbooru-tag-button'; button.textContent = tag.replace(/_/g, ' ') + (index < tags.length - 1 ? ', ' : ''); button.dataset.tag = tag.replace(/_/g, ' ') + (index < tags.length - 1 ? ', ' : ''); button.addEventListener('click', function() { const tagText = this.dataset.tag; if (this.classList.contains('selected')) { // 선택 해제 this.classList.remove('selected'); const index = selectedTags.indexOf(tagText); if (index > -1) { selectedTags.splice(index, 1); } } else { // 선택 this.classList.add('selected'); selectedTags.push(tagText); } }); container.appendChild(button); }); return container; } // 카테고리 태그 링크들을 생성하는 함수 function createCategoryTagLinks(categoryName, tags) { const box = document.createElement('div'); box.className = 'rapidbooru-category-box'; const tagsWrapper = document.createElement('div'); tagsWrapper.className = 'rapidbooru-category-tags-wrapper'; const label = document.createElement('span'); label.className = 'rapidbooru-category-label'; label.textContent = `${categoryName}:`; // [추가] 카테고리별 색상 적용 if (categoryName === 'artist') label.style.color = '#dd5555'; else if (categoryName === 'copyright') label.style.color = '#8454cc'; else if (categoryName === 'character') label.style.color = '#2ab367'; tagsWrapper.appendChild(label); if (tags.length === 0) { const noneText = document.createTextNode(' None'); tagsWrapper.appendChild(noneText); } else { tags.forEach((tag, index) => { const link = document.createElement('a'); link.className = 'rapidbooru-tag-button'; const displayTag = tag.replace(/_/g, ' '); const urlTag = tag; link.textContent = displayTag + (index < tags.length - 1 ? ', ' : ''); link.href = `https://danbooru.donmai.us/posts?tags=${encodeURIComponent(urlTag)}`; link.target = '_blank'; link.onclick = (e) => e.stopPropagation(); tagsWrapper.appendChild(link); }); } box.appendChild(tagsWrapper); if (tags.length > 0) { const copyBtn = document.createElement('button'); copyBtn.className = 'rapidbooru-category-copy-btn'; copyBtn.title = `Copy "${categoryName}" tags`; copyBtn.innerHTML = ` <svg viewBox="0 0 24 24"> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> </svg> `; copyBtn.onclick = (e) => { e.stopPropagation(); const tagsText = tags.map(t => t.replace(/_/g, ' ')).join(', '); const textToCopy = `${categoryName}:${tagsText}`; navigator.clipboard.writeText(textToCopy).then(() => { copyBtn.classList.add('copied'); copyBtn.innerHTML = ` <svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg> `; setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.innerHTML = ` <svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> `; }, 1500); }); }; box.appendChild(copyBtn); } return box; } // 이미지 프리뷰 기능 function createImagePreview(imageUrl, tags, postId, postData = null) { // 기존 프리뷰 오버레이 제거 (설정 패널은 제외) document.querySelectorAll('.rapidbooru-overlay').forEach(overlay => { if (overlay.id !== 'rapidbooru-settings-panel') { overlay.remove(); } }); // 선택된 태그 리스트 초기화 selectedTags = []; // 오버레이 생성 const overlay = document.createElement('div'); overlay.className = 'rapidbooru-overlay'; // 프리뷰 컨테이너 생성 const preview = document.createElement('div'); preview.className = 'rapidbooru-preview'; // [추가] 새 탭 버튼 생성 (이미지 프리뷰용) const newTabBtn = document.createElement('button'); newTabBtn.className = 'rapidbooru-newtab'; newTabBtn.title = 'Open post in new tab'; newTabBtn.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <rect x="3" y="3" width="18" height="18" rx="6" fill="none" stroke="white" stroke-width="2"/> <path d="M9 15L15 9M15 9H10M15 9V14" stroke="white" stroke-width="2" stroke-linecap="round"/> </svg> `; newTabBtn.onclick = (e) => { e.stopPropagation(); if (postId) { window.open(`https://danbooru.donmai.us/posts/${postId}`, '_blank'); } }; // 닫기 버튼 const closeBtn = document.createElement('button'); closeBtn.className = 'rapidbooru-close'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => { selectedTags = []; overlay.remove(); }; // 태그 섹션 const tagsSection = document.createElement('div'); tagsSection.className = 'rapidbooru-tags'; tagsSection.innerHTML = `<h3 style="color: var(--rapidbooru-text-color); margin-bottom: 15px;">Tags</h3>`; if (tags && tags.length > 0) { // 태그 분류 const categorizedTags = categorizeTags(tags, postData); // Artist, Copyright, Character 태그들을 버튼으로 생성 tagsSection.appendChild(createCategoryTagLinks('artist', categorizedTags.artist)); tagsSection.appendChild(createCategoryTagLinks('copyright', categorizedTags.copyright)); tagsSection.appendChild(createCategoryTagLinks('character', categorizedTags.character)); // General 태그 버튼들 if (categorizedTags.general.length > 0) { const generalLabel = document.createElement('h4'); generalLabel.style.cssText = `color: var(--rapidbooru-text-color); margin: 15px 0 10px 0; font-size: 14px;`; generalLabel.textContent = 'General Tags (Click to select):'; tagsSection.appendChild(generalLabel); const tagButtons = createTagButtons(categorizedTags.general); tagsSection.appendChild(tagButtons); } // 버튼 컨테이너 const buttonContainer = document.createElement('div'); buttonContainer.className = 'rapidbooru-action-buttons'; // Select All 버튼 추가 (General 태그가 있을 경우) if (categorizedTags.general.length > 0) { const selectAllBtn = document.createElement('button'); selectAllBtn.textContent = 'Select All'; selectAllBtn.className = 'rapidbooru-copy-btn'; selectAllBtn.style.background = 'linear-gradient(135deg, #56687a 0%, #3c4a58 100%)'; selectAllBtn.style.flexGrow = '0'; selectAllBtn.onclick = () => { // General 태그(span)만 선택/해제 const allTagButtons = tagsSection.querySelectorAll('.rapidbooru-main-tags .rapidbooru-tag-button'); const shouldSelectAll = Array.from(allTagButtons).some(btn => !btn.classList.contains('selected')); allTagButtons.forEach(button => { const tagText = button.dataset.tag; const isSelected = button.classList.contains('selected'); if (shouldSelectAll) { if (!isSelected) { button.classList.add('selected'); selectedTags.push(tagText); } } else { if (isSelected) { button.classList.remove('selected'); const index = selectedTags.indexOf(tagText); if (index > -1) { selectedTags.splice(index, 1); } } } }); selectedTags = [...new Set(selectedTags)]; selectAllBtn.textContent = shouldSelectAll ? 'Deselect All' : 'Select All'; }; buttonContainer.appendChild(selectAllBtn); } // 복사 버튼 const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy Selected Tags'; copyBtn.className = 'rapidbooru-copy-btn'; copyBtn.onclick = () => { const selectedText = selectedTags.join(''); if (selectedText) { navigator.clipboard.writeText(selectedText.trim().replace(/,$/, '')); copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy Selected Tags'; copyBtn.classList.remove('copied'); }, 2000); } }; buttonContainer.appendChild(copyBtn); tagsSection.appendChild(buttonContainer); } else { tagsSection.innerHTML += '<p style="color: #ccc;">태그 정보를 불러올 수 없습니다.</p>'; } // 이미지 섹션 const contentSection = document.createElement('div'); contentSection.className = 'rapidbooru-content'; // 미디어 요소 생성 (이미지, 비디오, GIF 지원) let mediaElement; const fileExtension = imageUrl.split('.').pop().toLowerCase(); if (fileExtension === 'mp4' || fileExtension === 'webm') { // 비디오 파일 mediaElement = document.createElement('video'); mediaElement.controls = true; mediaElement.autoplay = true; mediaElement.loop = true; mediaElement.muted = true; } else { // 이미지 파일 (png, jpg, webp, gif 포함) mediaElement = document.createElement('img'); mediaElement.alt = 'Preview Image'; } mediaElement.className = 'rapidbooru-image'; mediaElement.src = imageUrl; contentSection.appendChild(mediaElement); // 조립 preview.appendChild(newTabBtn); // [추가] 새 탭 버튼을 닫기 버튼 왼쪽에 추가 preview.appendChild(closeBtn); preview.appendChild(tagsSection); preview.appendChild(contentSection); overlay.appendChild(preview); // 이벤트 리스너 overlay.onclick = (e) => { if (e.target === overlay) { selectedTags = []; overlay.remove(); } }; document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { selectedTags = []; overlay.remove(); document.removeEventListener('keydown', escHandler); } }); // DOM에 추가 document.body.appendChild(overlay); // 애니메이션 setTimeout(() => overlay.classList.add('show'), 10); } // 썸네일에서 이미지 정보 추출 (원본 URL 가져오기 개선) function extractImageInfo(thumbnail) { let imageUrl = ''; let tags = []; let postId = ''; // 포스트 ID 추출 const link = thumbnail.closest('a') || thumbnail.querySelector('a'); if (link && link.href) { const match = link.href.match(/\/posts\/(\d+)/); if (match) { postId = match[1]; } } // 링크에서 ID를 찾지 못한 경우, data-id 속성에서 가져오기 시도 // 포스트 페이지의 원본 이미지를 처리하는 데 핵심적인 부분 if (!postId && thumbnail.dataset.id) { postId = thumbnail.dataset.id; } // 이미지 URL 찾기 - 원본 이미지 URL 추출 const img = thumbnail.querySelector('img'); if (img && postId) { // Danbooru API를 통해 원본 이미지 URL 가져오기 fetch(`https://danbooru.donmai.us/posts/${postId}.json`) .then(response => response.json()) .then(data => { if (data.file_url) { // 원본 파일 URL 사용 imageUrl = data.file_url; if (data.tag_string) { tags = data.tag_string.split(' '); } createImagePreview(imageUrl, tags, postId, data); } }) .catch(error => { console.error('Failed to fetch post data:', error); // 실패 시 썸네일 URL 사용 const src = img.src; if (src.includes('preview')) { imageUrl = src.replace('/preview/', '/original/').replace('preview_', ''); } else { imageUrl = src; } createImagePreview(imageUrl, tags, postId); }); return null; // API 호출로 처리하므로 null 반환 } // API 호출 실패 시 기존 방식 사용 if (img) { const src = img.src; if (src.includes('preview')) { imageUrl = src.replace('/preview/', '/original/').replace('preview_', ''); } else { imageUrl = src; } } // 태그 정보 추출 (data 속성이나 클래스에서) const tagElements = thumbnail.querySelectorAll('[data-tags]'); if (tagElements.length > 0) { tagElements.forEach(el => { const tagData = el.getAttribute('data-tags'); if (tagData) { tags = tags.concat(tagData.split(' ')); } }); } // 부모 요소에서 태그 정보 찾기 let parent = thumbnail.closest('[data-tags]'); if (parent) { const tagData = parent.getAttribute('data-tags'); if (tagData) { tags = tagData.split(' '); } } return { imageUrl, tags, postId }; } // 썸네일 우클릭 이벤트 설정 function setupImagePreview() { document.addEventListener('contextmenu', function(e) { // 이미지 썸네일인지 확인 const thumbnail = e.target.closest('.post-preview') || e.target.closest('.post-thumbnail') || e.target.closest('[data-id]') || (e.target.tagName === 'IMG' && e.target.closest('a[href*="/posts/"]')); if (thumbnail) { e.preventDefault(); const result = extractImageInfo(thumbnail); // API 호출이 아닌 경우에만 직접 프리뷰 생성 if (result && result.imageUrl) { createImagePreview(result.imageUrl, result.tags, result.postId); } } }); } // 태그 프리뷰 기능 function createTagPreview(tagName) { // 기존 프리뷰 오버레이 제거 (설정 패널은 제외) document.querySelectorAll('.rapidbooru-overlay').forEach(overlay => { if (overlay.id !== 'rapidbooru-settings-panel') { overlay.remove(); } }); // 태그명에서 공백을 언더스코어로 변환 const formattedTag = tagName.replace(/\s+/g, '_'); const searchOrder = rapidbooruSettings.searchOrder || 'Jaccard'; const wikiUrl = `https://danbooru.donmai.us/wiki_pages/${encodeURIComponent(formattedTag)}`; const charUrl = `https://danbooru.donmai.us/related_tag?commit=Search&search%5Bcategory%5D=Character&search%5Border%5D=${searchOrder}&search%5Bquery%5D=${encodeURIComponent(formattedTag)}`; const genUrl = `https://danbooru.donmai.us/related_tag?commit=Search&search%5Bcategory%5D=General&search%5Border%5D=${searchOrder}&search%5Bquery%5D=${encodeURIComponent(formattedTag)}`; // 오버레이 생성 const overlay = document.createElement('div'); overlay.className = 'rapidbooru-overlay'; // 프리뷰 컨테이너 생성 const preview = document.createElement('div'); preview.className = 'rapidbooru-preview'; // [추가] 새 탭 버튼 생성 (태그 프리뷰용) const newTabBtn = document.createElement('button'); newTabBtn.className = 'rapidbooru-newtab'; newTabBtn.title = 'Open wiki page in new tab'; newTabBtn.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <rect x="3" y="3" width="18" height="18" rx="6" fill="none" stroke="white" stroke-width="2"/> <path d="M9 15L15 9M15 9H10M15 9V14" stroke="white" stroke-width="2" stroke-linecap="round"/> </svg> `; newTabBtn.onclick = (e) => { e.stopPropagation(); window.open(`https://danbooru.donmai.us/wiki_pages/${encodeURIComponent(formattedTag)}`, '_blank'); }; // 닫기 버튼 const closeBtn = document.createElement('button'); closeBtn.className = 'rapidbooru-close'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => overlay.remove(); // 컨텐츠 섹션 (위키 iframe) const contentSection = document.createElement('div'); contentSection.className = 'rapidbooru-content'; contentSection.style.width = '100%'; contentSection.style.position = 'relative'; // 위키 제목 const title = document.createElement('h2'); title.style.color = 'white'; title.style.marginBottom = '15px'; title.style.textAlign = 'center'; title.textContent = `Tag: ${tagName}`; // 위키 iframe const iframe = document.createElement('iframe'); iframe.className = 'rapidbooru-wiki'; iframe.src = wikiUrl; iframe.style.width = '100%'; iframe.style.height = 'calc(100% - 100px)'; iframe.style.border = 'none'; iframe.style.borderRadius = '8px'; iframe.style.background = 'white'; // 버튼 컨테이너 const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'rapidbooru-buttons'; // Char 버튼 const charBtn = document.createElement('a'); charBtn.className = 'rapidbooru-btn'; charBtn.textContent = 'Char'; charBtn.href = charUrl; charBtn.target = '_blank'; charBtn.onclick = (e) => { e.stopPropagation(); }; // Gen 버튼 const genBtn = document.createElement('a'); genBtn.className = 'rapidbooru-btn'; genBtn.textContent = 'Gen'; genBtn.href = genUrl; genBtn.target = '_blank'; genBtn.onclick = (e) => { e.stopPropagation(); }; buttonsContainer.appendChild(charBtn); buttonsContainer.appendChild(genBtn); // 조립 contentSection.appendChild(title); contentSection.appendChild(iframe); contentSection.appendChild(buttonsContainer); preview.appendChild(newTabBtn); // [추가] 새 탭 버튼을 닫기 버튼 왼쪽에 추가 preview.appendChild(closeBtn); preview.appendChild(contentSection); overlay.appendChild(preview); // 이벤트 리스너 overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); } }; document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); } }); // DOM에 추가 document.body.appendChild(overlay); // 애니메이션 setTimeout(() => overlay.classList.add('show'), 10); } // 태그 요소 감지 및 우클릭 이벤트 설정 function setupTagPreview() { document.addEventListener('contextmenu', function(e) { // 태그 요소인지 확인 const tagElement = e.target.closest('.tag') || e.target.closest('.tag-type-0') || e.target.closest('.tag-type-1') || e.target.closest('.tag-type-3') || e.target.closest('.tag-type-4') || e.target.closest('.tag-type-5') || e.target.closest('[data-tag-name]') || (e.target.classList && ( e.target.classList.contains('tag') || e.target.classList.contains('tag-link') || e.target.classList.contains('search-tag') )); if (tagElement) { // 이미지 썸네일이 아닌 경우에만 태그 프리뷰 실행 const isImageThumbnail = e.target.closest('.post-preview') || e.target.closest('.post-thumbnail') || e.target.closest('[data-id]') || (e.target.tagName === 'IMG' && e.target.closest('a[href*="/posts/"]')); if (!isImageThumbnail) { e.preventDefault(); // 태그명 추출 let tagName = ''; // data-tag-name 속성에서 추출 if (tagElement.hasAttribute('data-tag-name')) { tagName = tagElement.getAttribute('data-tag-name'); } // href에서 추출 else if (tagElement.href && tagElement.href.includes('tags=')) { const match = tagElement.href.match(/tags=([^&]+)/); if (match) { tagName = decodeURIComponent(match[1]); } } // 텍스트 내용에서 추출 else { tagName = tagElement.textContent.trim(); // 태그 앞의 숫자나 기호 제거 tagName = tagName.replace(/^\d+\s*/, '').replace(/^[?!+-]\s*/, ''); } if (tagName) { createTagPreview(tagName); } } } }); } // Debounce function to limit the rate at which a function gets called. function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Related tags 화면 기능 function setupRelatedTagsFeature() { // Related tags 페이지인지 확인 if (!window.location.pathname.includes('/related_tag')) { return; } // 삽입 위치 결정: 검색 결과 테이블 위 const resultsTable = document.querySelector('table.striped'); if (resultsTable) { insertRelatedTagsUI(resultsTable, 'before'); } else { // 테이블이 없으면 기존 방식대로 검색 폼 뒤에 삽입 (폴백) const searchForm = document.querySelector('form') || document.querySelector('.search-form'); if (searchForm) { insertRelatedTagsUI(searchForm, 'after'); } else { const searchButton = document.querySelector('input[type="submit"]') || Array.from(document.querySelectorAll('input')).find(input => input.type === 'submit' || input.value === 'Search' ); if (searchButton && searchButton.parentElement) { insertRelatedTagsUI(searchButton.parentElement, 'after'); } } } // Debounce the update function to prevent performance issues const debouncedUpdate = debounce(updateRelatedTagsList, 300); // 페이지 변화 감지하여 태그 목록 업데이트 const observer = new MutationObserver(function(mutations) { // Call the debounced function once per batch of mutations. debouncedUpdate(); }); observer.observe(document.body, { childList: true, subtree: true }); // 초기 태그 목록 업데이트 setTimeout(updateRelatedTagsList, 500); } function insertRelatedTagsUI(targetElement, position = 'after') { // 이미 추가된 컨테이너가 있는지 확인 if (document.getElementById('rapidbooru-related-tags-container')) { return; } // 컨테이너 생성 const container = document.createElement('div'); container.id = 'rapidbooru-related-tags-container'; container.style.cssText = ` margin: 20px 0; padding: 15px; background: #f5b8b8; border: 1px solid #e5a7a7; color: #212529; `; // 제목 추가 const title = document.createElement('h4'); title.textContent = 'Top 20 Related Tags (Click to select)'; title.style.cssText = ` margin: 0 0 10px 0; color: #212529; font-family: 'Consolas', monospace; font-size: 16px; `; // 태그 버튼들이 들어갈 컨테이너 const tagsContainer = document.createElement('div'); tagsContainer.id = 'rapidbooru-related-tags-buttons'; tagsContainer.className = 'rapidbooru-main-tags'; tagsContainer.style.cssText = ` background: #ffdddd; max-height: none; padding: 10px; border: 1px solid #f5b8b8; min-height: 60px; border-radius: 4px; margin-bottom: 10px; `; // 버튼 컨테이너 const buttonContainer = document.createElement('div'); buttonContainer.className = 'rapidbooru-action-buttons'; buttonContainer.style.marginTop = '0'; // Select All 버튼 생성 const selectAllButton = document.createElement('button'); selectAllButton.textContent = 'Select All'; selectAllButton.type = 'button'; selectAllButton.className = 'rapidbooru-copy-btn'; selectAllButton.style.background = 'linear-gradient(135deg, #56687a 0%, #3c4a58 100%)'; selectAllButton.style.flexGrow = '0'; selectAllButton.onclick = function() { const allTagButtons = tagsContainer.querySelectorAll('.rapidbooru-tag-button'); if (allTagButtons.length === 0) return; const shouldSelectAll = Array.from(allTagButtons).some(btn => !btn.classList.contains('selected')); allTagButtons.forEach(button => { const tagText = button.dataset.tag; const isSelected = button.classList.contains('selected'); if (shouldSelectAll) { if (!isSelected) { button.classList.add('selected'); relatedPageSelectedTags.push(tagText); } } else { if (isSelected) { button.classList.remove('selected'); const index = relatedPageSelectedTags.indexOf(tagText); if (index > -1) { relatedPageSelectedTags.splice(index, 1); } } } }); relatedPageSelectedTags = [...new Set(relatedPageSelectedTags)]; this.textContent = shouldSelectAll ? 'Deselect All' : 'Select All'; }; // 복사 버튼 const copyButton = document.createElement('button'); copyButton.textContent = 'Copy Selected'; copyButton.type = 'button'; copyButton.className = 'rapidbooru-copy-btn'; copyButton.style.flexGrow = '0'; copyButton.onclick = function() { if (relatedPageSelectedTags.length > 0) { navigator.clipboard.writeText(relatedPageSelectedTags.join(', ')).then(() => { const originalText = copyButton.textContent; copyButton.textContent = 'Copied!'; copyButton.classList.add('copied'); setTimeout(() => { copyButton.textContent = originalText; copyButton.classList.remove('copied'); }, 2000); }); } }; // 조립 container.appendChild(title); container.appendChild(tagsContainer); buttonContainer.appendChild(selectAllButton); buttonContainer.appendChild(copyButton); container.appendChild(buttonContainer); // 위치에 따라 삽입 if (position === 'before') { targetElement.parentNode.insertBefore(container, targetElement); } else { // 'after' if (targetElement.nextSibling) { targetElement.parentNode.insertBefore(container, targetElement.nextSibling); } else { targetElement.parentNode.appendChild(container); } } } function updateRelatedTagsList() { const tagsContainer = document.getElementById('rapidbooru-related-tags-buttons'); if (!tagsContainer) { return; } // 검색 결과 영역에서 태그 링크들 찾기 (더 정확한 선택자 사용) const tagLinks = []; // 방법 1: 자동완성 드롭다운에서 태그 추출 const autocompleteItems = document.querySelectorAll('.ui-menu-item a, .autocomplete-item a'); if (autocompleteItems.length > 0) { autocompleteItems.forEach(item => { if (item.textContent.trim()) { tagLinks.push(item); } }); } // 방법 2: 검색 결과 테이블이나 리스트에서 태그 추출 if (tagLinks.length === 0) { const resultLinks = document.querySelectorAll('table a, .search-results a, .tag-list a'); resultLinks.forEach(link => { const text = link.textContent.trim(); const href = link.href; // 태그 링크인지 확인 (숫자로 시작하지 않고, 특정 패턴 제외) if (text && !href.includes('/wiki') && !href.includes('/artists') && !href.includes('/pools') && !href.includes('/forum') && !href.includes('/users') && !text.match(/^(Terms|Privacy|Contact|Help|More|Login|Posts|Comments)$/i) && (href.includes('tags=') || href.includes('/posts?') || text.includes('_') || text.includes(' '))) { tagLinks.push(link); } }); } // 방법 3: 현재 페이지의 모든 링크에서 태그 패턴 찾기 if (tagLinks.length === 0) { const allLinks = document.querySelectorAll('a'); allLinks.forEach(link => { const text = link.textContent.trim(); const href = link.href; // 태그 패턴 매칭 (언더스코어 포함, 괄호 포함 등) if (text && (text.includes('_') || text.match(/\([^)]+\)/)) && !href.includes('/wiki') && !href.includes('/artists') && !href.includes('/pools') && !href.includes('/forum') && !href.includes('/users') && !text.match(/^(Terms|Privacy|Contact|Help|More|Login|Posts|Comments|Danbooru)$/i)) { tagLinks.push(link); } }); } // 상위 20개 태그 추출 및 포맷팅 const topTags = tagLinks.slice(0, 20).map(link => { let tagName = link.textContent.trim(); // 숫자나 기타 정보 제거 (태그명만 추출) tagName = tagName.replace(/^\d+\s*/, '').replace(/\s*\d+$/, '').replace(/\s*\(\d+\)$/, ''); // '_'를 띄어쓰기로 변환 return tagName.replace(/_/g, ' '); }).filter(tag => tag.length > 0 && tag.length < 100); // 너무 긴 텍스트 제외 const uniqueTags = [...new Set(topTags)].slice(0, 20); // 이미 렌더링된 태그와 새로 가져온 태그를 비교합니다. const currentTagButtons = tagsContainer.querySelectorAll('.rapidbooru-tag-button'); const currentTags = Array.from(currentTagButtons).map(btn => btn.dataset.tag); // 태그 목록에 변화가 없으면 함수를 종료하여, 사용자의 태그 선택 상태가 // 계속 초기화되는 현상이 방지합니다. if (uniqueTags.length === currentTags.length && uniqueTags.every((tag, i) => tag === currentTags[i])) { return; } // 이전 상태 초기화 tagsContainer.innerHTML = ''; relatedPageSelectedTags = []; const selectAllBtn = document.querySelector('#rapidbooru-related-tags-container button'); if (selectAllBtn && selectAllBtn.textContent.includes('Deselect')) { selectAllBtn.textContent = 'Select All'; } if (uniqueTags.length > 0) { uniqueTags.forEach((tag, index) => { const button = document.createElement('span'); button.className = 'rapidbooru-tag-button'; button.textContent = tag + (index < uniqueTags.length - 1 ? ', ' : ''); button.dataset.tag = tag; button.addEventListener('click', function() { this.classList.toggle('selected'); if (this.classList.contains('selected')) { relatedPageSelectedTags.push(this.dataset.tag); } else { const tagIndex = relatedPageSelectedTags.indexOf(this.dataset.tag); if (tagIndex > -1) relatedPageSelectedTags.splice(tagIndex, 1); } }); tagsContainer.appendChild(button); }); } else { tagsContainer.textContent = 'No related tags found. Please perform a search first.'; } } // [추가] 사이드바 태그 버튼형 리스트 기능 function setupSidebarTagButtonList() { const path = window.location.pathname; if ( path === '/' || path.startsWith('/posts') || path.startsWith('/wiki_pages') ) { // 사이드바 컨테이너(태그 리스트의 부모)를 강제로 찾음 let sidebar = document.querySelector('.sidebar') || document.querySelector('.aside') || document.querySelector('#sidebar') || document.querySelector('.side-menu') || document.body; // 기존 태그 리스트(ul, .tag-list 등) 완전히 숨기기 const tagListCandidates = sidebar.querySelectorAll('.tag-list, .sidebar-section .tag-list, #tag-list, ul[data-category], .sidebar-section, .sidebar'); tagListCandidates.forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; el.style.height = '0'; el.style.margin = '0'; el.style.padding = '0'; }); // 실제 태그 리스트 컨테이너를 찾음 (최초로 발견되는 것) let tagListContainer = null; for (const el of tagListCandidates) { if (el.matches('.tag-list, .sidebar-section .tag-list, #tag-list, ul[data-category]')) { tagListContainer = el; break; } } // 이미 생성된 경우 중복 생성 방지 if (document.getElementById('rapidbooru-sidebar-taglist')) return; // 태그 정보 추출 함수 (카테고리별) function extractSidebarTags() { const categories = { artist: [], copyright: [], character: [], general: [], meta: [] }; // ul[data-category] 우선 const uls = sidebar.querySelectorAll('ul[data-category]'); if (uls.length > 0) { uls.forEach(ul => { const cat = ul.getAttribute('data-category'); let key = ''; if (cat === '0') key = 'general'; else if (cat === '1') key = 'artist'; else if (cat === '3') key = 'copyright'; else if (cat === '4') key = 'character'; else if (cat === '5') key = 'meta'; else return; ul.querySelectorAll('li').forEach(li => { // 실제 태그명과 카운트 파싱 (danbooru 구조에 맞춤) const tagA = li.querySelector('a.search-tag'); const countSpan = li.querySelector('span.post-count'); if (!tagA) return; const tag = tagA.textContent.trim(); const count = countSpan ? countSpan.textContent.trim() : ''; if (tag) categories[key].push({ tag, count }); }); }); } else { // li.tag-type-x sidebar.querySelectorAll('li, .tag-type-0, .tag-type-1, .tag-type-3, .tag-type-4, .tag-type-5').forEach(li => { let key = ''; if (li.classList.contains('tag-type-0')) key = 'general'; else if (li.classList.contains('tag-type-1')) key = 'artist'; else if (li.classList.contains('tag-type-3')) key = 'copyright'; else if (li.classList.contains('tag-type-4')) key = 'character'; else if (li.classList.contains('tag-type-5')) key = 'meta'; else return; const tagA = li.querySelector('a.search-tag'); const countSpan = li.querySelector('span.post-count'); if (!tagA) return; const tag = tagA.textContent.trim(); const count = countSpan ? countSpan.textContent.trim() : ''; if (tag) categories[key].push({ tag, count }); }); } return categories; } // 태그 버튼 생성 함수 function createSidebarTagButtons(categories) { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.flexDirection = 'column'; wrapper.style.gap = '0px'; ['artist', 'copyright', 'character', 'general', 'meta'].forEach(cat => { categories[cat].forEach(({ tag, count }) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rapidbooru-sidebar-tag-btn'; btn.textContent = tag; btn.dataset.tag = tag; btn.dataset.category = cat; btn.style.background = tagColors[cat].normal; btn.style.color = '#ffffff'; btn.style.border = 'none'; btn.style.borderRadius = '4px'; btn.style.margin = '0 0 4px 0'; btn.style.padding = '6px 10px'; btn.style.fontSize = '13px'; btn.style.textAlign = 'left'; btn.style.cursor = 'pointer'; btn.style.transition = 'background 0.2s'; btn.style.userSelect = 'none'; btn.style.width = '100%'; btn.style.boxShadow = 'none'; btn.style.position = 'relative'; // 태그 오른쪽 끝에 count 표시 if (count) { const countSpan = document.createElement('span'); countSpan.textContent = count; countSpan.style.position = 'absolute'; countSpan.style.right = '12px'; countSpan.style.top = '50%'; countSpan.style.transform = 'translateY(-50%)'; countSpan.style.color = '#ffffff'; countSpan.style.fontSize = '12px'; countSpan.style.pointerEvents = 'none'; btn.appendChild(countSpan); } // 좌클릭: 선택/해제 btn.addEventListener('click', function(e) { if (e.button !== 0) return; e.preventDefault(); if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } else { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); } sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; }); // 가운데 클릭: 새 탭으로 검색 btn.addEventListener('mousedown', function(e) { if (e.button === 1) { e.preventDefault(); const url = `https://danbooru.donmai.us/posts?tags=${encodeURIComponent(tag.replace(/\s/g, '_'))}`; window.open(url, '_blank'); } }); // 우클릭: 태그 프리뷰 btn.addEventListener('contextmenu', function(e) { e.preventDefault(); createTagPreview(tag); }); // 드래그 다중 선택 지원 btn.addEventListener('mouseenter', function(e) { if (window.__rapidbooru_sidebar_dragging) { if (!btn.classList.contains('selected')) { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; } else { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } } }); wrapper.appendChild(btn); }); }); return wrapper; } // 사이드바에 삽입할 컨테이너 생성 const sidebarBox = document.createElement('div'); sidebarBox.id = 'rapidbooru-sidebar-taglist'; sidebarBox.style.margin = '24px 0 0 0'; sidebarBox.style.padding = '12px 8px 12px 8px'; sidebarBox.style.background = '#474756'; // 배경색 지정 sidebarBox.style.borderRadius = '8px'; sidebarBox.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)'; sidebarBox.style.display = 'flex'; sidebarBox.style.flexDirection = 'column'; sidebarBox.style.alignItems = 'stretch'; sidebarBox.style.maxHeight = '70vh'; sidebarBox.style.overflowY = 'auto'; sidebarBox.style.minWidth = '120px'; // 버튼 컨테이너(Select All, Copy) const topBtnBox = document.createElement('div'); topBtnBox.style.display = 'flex'; topBtnBox.style.gap = '8px'; topBtnBox.style.marginBottom = '12px'; // Select All 버튼 const selectAllBtn = document.createElement('button'); selectAllBtn.type = 'button'; selectAllBtn.textContent = 'Select All'; selectAllBtn.className = 'rapidbooru-copy-btn'; selectAllBtn.style.background = 'linear-gradient(135deg, #56687a 0%, #3c4a58 100%)'; selectAllBtn.style.flexGrow = '1'; selectAllBtn.style.fontSize = '13px'; selectAllBtn.style.padding = '6px 0'; selectAllBtn.addEventListener('click', function() { const btns = sidebarBox.querySelectorAll('.rapidbooru-sidebar-tag-btn'); const shouldSelectAll = Array.from(btns).some(btn => !btn.classList.contains('selected')); btns.forEach(btn => { const tag = btn.dataset.tag; const cat = btn.dataset.category; if (shouldSelectAll) { if (!btn.classList.contains('selected')) { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); } } else { if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } } }); sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; selectAllBtn.textContent = shouldSelectAll ? 'Deselect All' : 'Select All'; }); // Copy Selected Tags 버튼 const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.textContent = 'Copy Selected Tags'; copyBtn.className = 'rapidbooru-copy-btn'; copyBtn.style.flexGrow = '1'; copyBtn.style.fontSize = '13px'; copyBtn.style.padding = '6px 0'; copyBtn.addEventListener('click', function() { if (sidebarSelectedTags.length > 0) { const str = sidebarSelectedTags.join(', '); navigator.clipboard.writeText(str).then(() => { copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy Selected Tags'; copyBtn.classList.remove('copied'); }, 1500); }); } }); topBtnBox.appendChild(selectAllBtn); topBtnBox.appendChild(copyBtn); // 태그 버튼 리스트 생성 const categories = extractSidebarTags(); const tagBtnList = createSidebarTagButtons(categories); // 드래그 다중 선택 지원 sidebarBox.addEventListener('mousedown', function(e) { if (e.target.classList.contains('rapidbooru-sidebar-tag-btn') && e.button === 0) { window.__rapidbooru_sidebar_dragging = true; } }); sidebarBox.addEventListener('mouseup', function() { window.__rapidbooru_sidebar_dragging = false; }); sidebarBox.addEventListener('mouseleave', function() { window.__rapidbooru_sidebar_dragging = false; }); // 조립 sidebarBox.appendChild(topBtnBox); sidebarBox.appendChild(tagBtnList); // 사이드바에 삽입 (기존 태그 리스트 바로 위에) if (tagListContainer && tagListContainer.parentNode) { tagListContainer.parentNode.insertBefore(sidebarBox, tagListContainer); } else { // 태그 리스트가 없으면 sidebar 맨 앞에 삽입 sidebar.insertBefore(sidebarBox, sidebar.firstChild); } // 스타일 추가 (카테고리별 버튼 색상) const style = document.createElement('style'); style.textContent = ` .rapidbooru-sidebar-tag-btn.selected[data-category="artist"] { background: #7a0a2c !important; } .rapidbooru-sidebar-tag-btn[data-category="artist"] { background: #dd5555 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="copyright"] { background: #2d0766 !important; } .rapidbooru-sidebar-tag-btn[data-category="copyright"] { background: #8454cc !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="character"] { background: #04472d !important; } .rapidbooru-sidebar-tag-btn[data-category="character"] { background: #2ab367 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="general"] { background: #10164d !important; } .rapidbooru-sidebar-tag-btn[data-category="general"] { background: #47799c !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="meta"] { background: #1f1c03 !important; } .rapidbooru-sidebar-tag-btn[data-category="meta"] { background: #79722a !important; } .rapidbooru-sidebar-tag-btn { outline: none; } .rapidbooru-sidebar-tag-btn:active { filter: brightness(0.95); } `; document.head.appendChild(style); } } // [수정] 태그 그룹 페이지용 태그 버튼 컨테이너 기능 (본문만 변환, Copy 버튼 위치 개선) function setupTagGroupPageTagButtons() { // 태그 그룹 페이지인지 확인 if (!/^\/wiki_pages\/tag_group%3A/i.test(window.location.pathname)) return; // 본문 컨테이너(id/class 모두 대응) const mainContent = document.querySelector('#wiki-page-body.prose') || document.querySelector('.wiki-page-body') || document.querySelector('.content') || document.body; if (!mainContent) return; // 본문 내 <ul>만 수집 (직계/하위 모두) const ulList = Array.from(mainContent.querySelectorAll('ul')); if (ulList.length === 0) return; // 선택된 태그 임시 리스트 let tagGroupSelectedTags = []; // 스타일 재사용 const tagColors = { artist: { normal: '#dd5555', selected: '#7a0a2c' }, copyright: { normal: '#8454cc', selected: '#2d0766' }, character: { normal: '#2ab367', selected: '#04472d' }, general: { normal: '#47799c', selected: '#10164d' }, meta: { normal: '#79722a', selected: '#1f1c03' } }; // 태그 타입 추출 함수 function getTagType(a) { if (a.classList.contains('tag-type-1')) return 'artist'; if (a.classList.contains('tag-type-3')) return 'copyright'; if (a.classList.contains('tag-type-4')) return 'character'; if (a.classList.contains('tag-type-5')) return 'meta'; return 'general'; } // 태그 버튼 컨테이너 생성 함수 function createTagGroupButtonContainer(tagObjs) { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.flexWrap = 'wrap'; wrapper.style.gap = '6px'; wrapper.style.background = '#474756'; wrapper.style.borderRadius = '8px'; wrapper.style.padding = '12px 8px'; wrapper.style.margin = '16px 0'; wrapper.style.minWidth = '120px'; tagObjs.forEach(({ tag, type }) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rapidbooru-sidebar-tag-btn'; btn.textContent = tag; btn.dataset.tag = tag; btn.dataset.category = type; btn.style.background = tagColors[type].normal; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.borderRadius = '4px'; btn.style.padding = '6px 10px'; btn.style.fontSize = '13px'; btn.style.textAlign = 'left'; btn.style.cursor = 'pointer'; btn.style.transition = 'background 0.2s'; btn.style.userSelect = 'none'; btn.style.boxShadow = 'none'; btn.style.position = 'relative'; btn.style.margin = '0 0 4px 0'; btn.style.width = 'auto'; // 좌클릭: 선택/해제 btn.addEventListener('click', function(e) { if (e.button !== 0) return; e.preventDefault(); if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[type].normal; const idx = tagGroupSelectedTags.indexOf(tag); if (idx > -1) tagGroupSelectedTags.splice(idx, 1); } else { btn.classList.add('selected'); btn.style.background = tagColors[type].selected; tagGroupSelectedTags.push(tag); } tagGroupSelectedTags = [...new Set(tagGroupSelectedTags)]; }); // 가운데 클릭: 새 탭으로 검색 btn.addEventListener('mousedown', function(e) { if (e.button === 1) { e.preventDefault(); const url = `https://danbooru.donmai.us/posts?tags=${encodeURIComponent(tag.replace(/\s/g, '_'))}`; window.open(url, '_blank'); } }); // 우클릭: 태그 프리뷰 btn.addEventListener('contextmenu', function(e) { e.preventDefault(); if (typeof createTagPreview === 'function') createTagPreview(tag); }); wrapper.appendChild(btn); }); return wrapper; } // 기존 <ul>을 버튼 컨테이너로 교체 (본문 내에서만) ulList.forEach(ul => { // <ul> 내 <li> -> <a> 추출 const tagObjs = Array.from(ul.querySelectorAll('li a')).map(a => ({ tag: a.textContent.trim(), type: getTagType(a) })).filter(obj => obj.tag.length > 0); // 컨테이너 생성 const btnContainer = createTagGroupButtonContainer(tagObjs); ul.parentNode.insertBefore(btnContainer, ul); ul.remove(); }); // Copy Selected Tags 버튼 생성 (본문 맨 위) if (!mainContent.querySelector('.rapidbooru-taggroup-copy-btn')) { const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.textContent = 'Copy Selected Tags'; copyBtn.className = 'rapidbooru-copy-btn rapidbooru-taggroup-copy-btn'; copyBtn.style.margin = '0 0 18px 0'; copyBtn.style.display = 'block'; copyBtn.style.fontSize = '15px'; copyBtn.addEventListener('click', function() { if (tagGroupSelectedTags.length > 0) { const str = tagGroupSelectedTags.join(', '); navigator.clipboard.writeText(str).then(() => { copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy Selected Tags'; copyBtn.classList.remove('copied'); }, 1500); }); } }); // 본문 맨 앞에 삽입 mainContent.insertBefore(copyBtn, mainContent.firstChild); } // 스타일 추가 (카테고리별 버튼 색상) if (!document.getElementById('rapidbooru-taggroup-style')) { const style = document.createElement('style'); style.id = 'rapidbooru-taggroup-style'; style.textContent = ` .rapidbooru-sidebar-tag-btn.selected[data-category="artist"] { background: #7a0a2c !important; } .rapidbooru-sidebar-tag-btn[data-category="artist"] { background: #dd5555 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="copyright"] { background: #2d0766 !important; } .rapidbooru-sidebar-tag-btn[data-category="copyright"] { background: #8454cc !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="character"] { background: #04472d !important; } .rapidbooru-sidebar-tag-btn[data-category="character"] { background: #2ab367 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="general"] { background: #10164d !important; } .rapidbooru-sidebar-tag-btn[data-category="general"] { background: #47799c !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="meta"] { background: #1f1c03 !important; } .rapidbooru-sidebar-tag-btn[data-category="meta"] { background: #79722a !important; } .rapidbooru-sidebar-tag-btn { outline: none; } .rapidbooru-sidebar-tag-btn:active { filter: brightness(0.95); } `; document.head.appendChild(style); } } // [추가] I'm Feeling Lucky 버튼 생성 및 기능 function addLuckyButton() { // 메인 페이지에서만 표시 if (window.location.pathname !== '/' && window.location.pathname !== '/posts') return; if (document.getElementById('rapidbooru-lucky-btn')) return; const luckyBtn = document.createElement('button'); luckyBtn.id = 'rapidbooru-lucky-btn'; luckyBtn.textContent = "I'm Feeling Lucky!"; luckyBtn.style.position = 'fixed'; luckyBtn.style.top = '20px'; luckyBtn.style.right = '20px'; luckyBtn.style.zIndex = '10003'; luckyBtn.style.width = '180px'; luckyBtn.style.height = '50px'; luckyBtn.style.background = 'var(--rapidbooru-accent-color, #8454cc)'; luckyBtn.style.color = 'var(--rapidbooru-text-color, #fff)'; luckyBtn.style.border = 'none'; luckyBtn.style.borderRadius = '25px'; luckyBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)'; luckyBtn.style.fontWeight = 'bold'; luckyBtn.style.fontSize = '16px'; luckyBtn.style.cursor = 'pointer'; luckyBtn.style.display = 'flex'; luckyBtn.style.alignItems = 'center'; luckyBtn.style.justifyContent = 'center'; luckyBtn.style.transition = 'all 0.3s'; luckyBtn.style.gap = '8px'; luckyBtn.addEventListener('mouseenter', () => { luckyBtn.style.background = 'var(--rapidbooru-accent-hover-color, #56687a)'; }); luckyBtn.addEventListener('mouseleave', () => { luckyBtn.style.background = 'var(--rapidbooru-accent-color, #8454cc)'; }); luckyBtn.onclick = async function() { luckyBtn.disabled = true; luckyBtn.textContent = 'Loading...'; try { // 최신 포스트 ID 가져오기 const resp = await fetch('https://danbooru.donmai.us/posts.json?limit=1&only=id'); const data = await resp.json(); if (data && data.length > 0 && data[0].id) { let luckyIndex = data[0].id; // 100만 개 전 범위 내 난수 const min = Math.max(1, luckyIndex - 2000000); luckyIndex = Math.floor(Math.random() * (luckyIndex - min + 1)) + min; window.open(`https://danbooru.donmai.us/posts/${luckyIndex}`, '_blank'); } else { alert('최신 포스트 ID를 가져올 수 없습니다.'); } } catch (e) { alert('네트워크 오류로 시도에 실패했습니다.'); } luckyBtn.disabled = false; luckyBtn.textContent = "I'm Feeling Lucky!"; }; document.body.appendChild(luckyBtn); } // 태그를 복사하는 함수 function copyTags(tags) { const tagsText = tags.map(t => t.replace(/_/g, ' ')).join(', '); navigator.clipboard.writeText(tagsText).then(() => { alert('Tags copied to clipboard: ' + tagsText); }, (err) => { console.error('Error copying tags: ', err); }); } // [추가] 사이드바 태그 버튼형 리스트 기능 function setupSidebarTagButtonList() { const path = window.location.pathname; if ( path === '/' || path.startsWith('/posts') || path.startsWith('/wiki_pages') ) { // 사이드바 컨테이너(태그 리스트의 부모)를 강제로 찾음 let sidebar = document.querySelector('.sidebar') || document.querySelector('.aside') || document.querySelector('#sidebar') || document.querySelector('.side-menu') || document.body; // 기존 태그 리스트(ul, .tag-list 등) 완전히 숨기기 const tagListCandidates = sidebar.querySelectorAll('.tag-list, .sidebar-section .tag-list, #tag-list, ul[data-category], .sidebar-section, .sidebar'); tagListCandidates.forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; el.style.height = '0'; el.style.margin = '0'; el.style.padding = '0'; }); // 실제 태그 리스트 컨테이너를 찾음 (최초로 발견되는 것) let tagListContainer = null; for (const el of tagListCandidates) { if (el.matches('.tag-list, .sidebar-section .tag-list, #tag-list, ul[data-category]')) { tagListContainer = el; break; } } // 이미 생성된 경우 중복 생성 방지 if (document.getElementById('rapidbooru-sidebar-taglist')) return; // 태그 정보 추출 함수 (카테고리별) function extractSidebarTags() { const categories = { artist: [], copyright: [], character: [], general: [], meta: [] }; // ul[data-category] 우선 const uls = sidebar.querySelectorAll('ul[data-category]'); if (uls.length > 0) { uls.forEach(ul => { const cat = ul.getAttribute('data-category'); let key = ''; if (cat === '0') key = 'general'; else if (cat === '1') key = 'artist'; else if (cat === '3') key = 'copyright'; else if (cat === '4') key = 'character'; else if (cat === '5') key = 'meta'; else return; ul.querySelectorAll('li').forEach(li => { // 실제 태그명과 카운트 파싱 (danbooru 구조에 맞춤) const tagA = li.querySelector('a.search-tag'); const countSpan = li.querySelector('span.post-count'); if (!tagA) return; const tag = tagA.textContent.trim(); const count = countSpan ? countSpan.textContent.trim() : ''; if (tag) categories[key].push({ tag, count }); }); }); } else { // li.tag-type-x sidebar.querySelectorAll('li, .tag-type-0, .tag-type-1, .tag-type-3, .tag-type-4, .tag-type-5').forEach(li => { let key = ''; if (li.classList.contains('tag-type-0')) key = 'general'; else if (li.classList.contains('tag-type-1')) key = 'artist'; else if (li.classList.contains('tag-type-3')) key = 'copyright'; else if (li.classList.contains('tag-type-4')) key = 'character'; else if (li.classList.contains('tag-type-5')) key = 'meta'; else return; const tagA = li.querySelector('a.search-tag'); const countSpan = li.querySelector('span.post-count'); if (!tagA) return; const tag = tagA.textContent.trim(); const count = countSpan ? countSpan.textContent.trim() : ''; if (tag) categories[key].push({ tag, count }); }); } return categories; } // 태그 버튼 생성 함수 function createSidebarTagButtons(categories) { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.flexDirection = 'column'; wrapper.style.gap = '0px'; ['artist', 'copyright', 'character', 'general', 'meta'].forEach(cat => { categories[cat].forEach(({ tag, count }) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rapidbooru-sidebar-tag-btn'; btn.textContent = tag; btn.dataset.tag = tag; btn.dataset.category = cat; btn.style.background = tagColors[cat].normal; btn.style.color = '#ffffff'; btn.style.border = 'none'; btn.style.borderRadius = '4px'; btn.style.margin = '0 0 4px 0'; btn.style.padding = '6px 10px'; btn.style.fontSize = '13px'; btn.style.textAlign = 'left'; btn.style.cursor = 'pointer'; btn.style.transition = 'background 0.2s'; btn.style.userSelect = 'none'; btn.style.width = '100%'; btn.style.boxShadow = 'none'; btn.style.position = 'relative'; // 태그 오른쪽 끝에 count 표시 if (count) { const countSpan = document.createElement('span'); countSpan.textContent = count; countSpan.style.position = 'absolute'; countSpan.style.right = '12px'; countSpan.style.top = '50%'; countSpan.style.transform = 'translateY(-50%)'; countSpan.style.color = '#ffffff'; countSpan.style.fontSize = '12px'; countSpan.style.pointerEvents = 'none'; btn.appendChild(countSpan); } // 좌클릭: 선택/해제 btn.addEventListener('click', function(e) { if (e.button !== 0) return; e.preventDefault(); if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } else { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); } sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; }); // 가운데 클릭: 새 탭으로 검색 btn.addEventListener('mousedown', function(e) { if (e.button === 1) { e.preventDefault(); const url = `https://danbooru.donmai.us/posts?tags=${encodeURIComponent(tag.replace(/\s/g, '_'))}`; window.open(url, '_blank'); } }); // 우클릭: 태그 프리뷰 btn.addEventListener('contextmenu', function(e) { e.preventDefault(); createTagPreview(tag); }); // 드래그 다중 선택 지원 btn.addEventListener('mouseenter', function(e) { if (window.__rapidbooru_sidebar_dragging) { if (!btn.classList.contains('selected')) { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; } else { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } } }); wrapper.appendChild(btn); }); }); return wrapper; } // 사이드바에 삽입할 컨테이너 생성 const sidebarBox = document.createElement('div'); sidebarBox.id = 'rapidbooru-sidebar-taglist'; sidebarBox.style.margin = '24px 0 0 0'; sidebarBox.style.padding = '12px 8px 12px 8px'; sidebarBox.style.background = '#474756'; // 배경색 지정 sidebarBox.style.borderRadius = '8px'; sidebarBox.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)'; sidebarBox.style.display = 'flex'; sidebarBox.style.flexDirection = 'column'; sidebarBox.style.alignItems = 'stretch'; sidebarBox.style.maxHeight = '70vh'; sidebarBox.style.overflowY = 'auto'; sidebarBox.style.minWidth = '120px'; // 버튼 컨테이너(Select All, Copy) const topBtnBox = document.createElement('div'); topBtnBox.style.display = 'flex'; topBtnBox.style.gap = '8px'; topBtnBox.style.marginBottom = '12px'; // Select All 버튼 const selectAllBtn = document.createElement('button'); selectAllBtn.type = 'button'; selectAllBtn.textContent = 'Select All'; selectAllBtn.className = 'rapidbooru-copy-btn'; selectAllBtn.style.background = 'linear-gradient(135deg, #56687a 0%, #3c4a58 100%)'; selectAllBtn.style.flexGrow = '1'; selectAllBtn.style.fontSize = '13px'; selectAllBtn.style.padding = '6px 0'; selectAllBtn.addEventListener('click', function() { const btns = sidebarBox.querySelectorAll('.rapidbooru-sidebar-tag-btn'); const shouldSelectAll = Array.from(btns).some(btn => !btn.classList.contains('selected')); btns.forEach(btn => { const tag = btn.dataset.tag; const cat = btn.dataset.category; if (shouldSelectAll) { if (!btn.classList.contains('selected')) { btn.classList.add('selected'); btn.style.background = tagColors[cat].selected; sidebarSelectedTags.push(tag); } } else { if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[cat].normal; const idx = sidebarSelectedTags.indexOf(tag); if (idx > -1) sidebarSelectedTags.splice(idx, 1); } } }); sidebarSelectedTags = [...new Set(sidebarSelectedTags)]; selectAllBtn.textContent = shouldSelectAll ? 'Deselect All' : 'Select All'; }); // Copy Selected Tags 버튼 const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.textContent = 'Copy Selected Tags'; copyBtn.className = 'rapidbooru-copy-btn'; copyBtn.style.flexGrow = '1'; copyBtn.style.fontSize = '13px'; copyBtn.style.padding = '6px 0'; copyBtn.addEventListener('click', function() { if (sidebarSelectedTags.length > 0) { const str = sidebarSelectedTags.join(', '); navigator.clipboard.writeText(str).then(() => { copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy Selected Tags'; copyBtn.classList.remove('copied'); }, 1500); }); } }); topBtnBox.appendChild(selectAllBtn); topBtnBox.appendChild(copyBtn); // 태그 버튼 리스트 생성 const categories = extractSidebarTags(); const tagBtnList = createSidebarTagButtons(categories); // 드래그 다중 선택 지원 sidebarBox.addEventListener('mousedown', function(e) { if (e.target.classList.contains('rapidbooru-sidebar-tag-btn') && e.button === 0) { window.__rapidbooru_sidebar_dragging = true; } }); sidebarBox.addEventListener('mouseup', function() { window.__rapidbooru_sidebar_dragging = false; }); sidebarBox.addEventListener('mouseleave', function() { window.__rapidbooru_sidebar_dragging = false; }); // 조립 sidebarBox.appendChild(topBtnBox); sidebarBox.appendChild(tagBtnList); // 사이드바에 삽입 (기존 태그 리스트 바로 위에) if (tagListContainer && tagListContainer.parentNode) { tagListContainer.parentNode.insertBefore(sidebarBox, tagListContainer); } else { // 태그 리스트가 없으면 sidebar 맨 앞에 삽입 sidebar.insertBefore(sidebarBox, sidebar.firstChild); } // 스타일 추가 (카테고리별 버튼 색상) const style = document.createElement('style'); style.textContent = ` .rapidbooru-sidebar-tag-btn.selected[data-category="artist"] { background: #7a0a2c !important; } .rapidbooru-sidebar-tag-btn[data-category="artist"] { background: #dd5555 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="copyright"] { background: #2d0766 !important; } .rapidbooru-sidebar-tag-btn[data-category="copyright"] { background: #8454cc !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="character"] { background: #04472d !important; } .rapidbooru-sidebar-tag-btn[data-category="character"] { background: #2ab367 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="general"] { background: #10164d !important; } .rapidbooru-sidebar-tag-btn[data-category="general"] { background: #47799c !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="meta"] { background: #1f1c03 !important; } .rapidbooru-sidebar-tag-btn[data-category="meta"] { background: #79722a !important; } .rapidbooru-sidebar-tag-btn { outline: none; } .rapidbooru-sidebar-tag-btn:active { filter: brightness(0.95); } `; document.head.appendChild(style); } } // [수정] 태그 그룹 페이지용 태그 버튼 컨테이너 기능 (본문만 변환, Copy 버튼 위치 개선) function setupTagGroupPageTagButtons() { // 태그 그룹 페이지인지 확인 if (!/^\/wiki_pages\/tag_group%3A/i.test(window.location.pathname)) return; // 본문 컨테이너(id/class 모두 대응) const mainContent = document.querySelector('#wiki-page-body.prose') || document.querySelector('.wiki-page-body') || document.querySelector('.content') || document.body; if (!mainContent) return; // 본문 내 <ul>만 수집 (직계/하위 모두) const ulList = Array.from(mainContent.querySelectorAll('ul')); if (ulList.length === 0) return; // 선택된 태그 임시 리스트 let tagGroupSelectedTags = []; // 스타일 재사용 const tagColors = { artist: { normal: '#dd5555', selected: '#7a0a2c' }, copyright: { normal: '#8454cc', selected: '#2d0766' }, character: { normal: '#2ab367', selected: '#04472d' }, general: { normal: '#47799c', selected: '#10164d' }, meta: { normal: '#79722a', selected: '#1f1c03' } }; // 태그 타입 추출 함수 function getTagType(a) { if (a.classList.contains('tag-type-1')) return 'artist'; if (a.classList.contains('tag-type-3')) return 'copyright'; if (a.classList.contains('tag-type-4')) return 'character'; if (a.classList.contains('tag-type-5')) return 'meta'; return 'general'; } // 태그 버튼 컨테이너 생성 함수 function createTagGroupButtonContainer(tagObjs) { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.flexWrap = 'wrap'; wrapper.style.gap = '6px'; wrapper.style.background = '#474756'; wrapper.style.borderRadius = '8px'; wrapper.style.padding = '12px 8px'; wrapper.style.margin = '16px 0'; wrapper.style.minWidth = '120px'; tagObjs.forEach(({ tag, type }) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rapidbooru-sidebar-tag-btn'; btn.textContent = tag; btn.dataset.tag = tag; btn.dataset.category = type; btn.style.background = tagColors[type].normal; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.borderRadius = '4px'; btn.style.padding = '6px 10px'; btn.style.fontSize = '13px'; btn.style.textAlign = 'left'; btn.style.cursor = 'pointer'; btn.style.transition = 'background 0.2s'; btn.style.userSelect = 'none'; btn.style.boxShadow = 'none'; btn.style.position = 'relative'; btn.style.margin = '0 0 4px 0'; btn.style.width = 'auto'; // 좌클릭: 선택/해제 btn.addEventListener('click', function(e) { if (e.button !== 0) return; e.preventDefault(); if (btn.classList.contains('selected')) { btn.classList.remove('selected'); btn.style.background = tagColors[type].normal; const idx = tagGroupSelectedTags.indexOf(tag); if (idx > -1) tagGroupSelectedTags.splice(idx, 1); } else { btn.classList.add('selected'); btn.style.background = tagColors[type].selected; tagGroupSelectedTags.push(tag); } tagGroupSelectedTags = [...new Set(tagGroupSelectedTags)]; }); // 가운데 클릭: 새 탭으로 검색 btn.addEventListener('mousedown', function(e) { if (e.button === 1) { e.preventDefault(); const url = `https://danbooru.donmai.us/posts?tags=${encodeURIComponent(tag.replace(/\s/g, '_'))}`; window.open(url, '_blank'); } }); // 우클릭: 태그 프리뷰 btn.addEventListener('contextmenu', function(e) { e.preventDefault(); if (typeof createTagPreview === 'function') createTagPreview(tag); }); wrapper.appendChild(btn); }); return wrapper; } // 기존 <ul>을 버튼 컨테이너로 교체 (본문 내에서만) ulList.forEach(ul => { // <ul> 내 <li> -> <a> 추출 const tagObjs = Array.from(ul.querySelectorAll('li a')).map(a => ({ tag: a.textContent.trim(), type: getTagType(a) })).filter(obj => obj.tag.length > 0); // 컨테이너 생성 const btnContainer = createTagGroupButtonContainer(tagObjs); ul.parentNode.insertBefore(btnContainer, ul); ul.remove(); }); // Copy Selected Tags 버튼 생성 (본문 맨 위) if (!mainContent.querySelector('.rapidbooru-taggroup-copy-btn')) { const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.textContent = 'Copy Selected Tags'; copyBtn.className = 'rapidbooru-copy-btn rapidbooru-taggroup-copy-btn'; copyBtn.style.margin = '0 0 18px 0'; copyBtn.style.display = 'block'; copyBtn.style.fontSize = '15px'; copyBtn.addEventListener('click', function() { if (tagGroupSelectedTags.length > 0) { const str = tagGroupSelectedTags.join(', '); navigator.clipboard.writeText(str).then(() => { copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy Selected Tags'; copyBtn.classList.remove('copied'); }, 1500); }); } }); // 본문 맨 앞에 삽입 mainContent.insertBefore(copyBtn, mainContent.firstChild); } // 스타일 추가 (카테고리별 버튼 색상) if (!document.getElementById('rapidbooru-taggroup-style')) { const style = document.createElement('style'); style.id = 'rapidbooru-taggroup-style'; style.textContent = ` .rapidbooru-sidebar-tag-btn.selected[data-category="artist"] { background: #7a0a2c !important; } .rapidbooru-sidebar-tag-btn[data-category="artist"] { background: #dd5555 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="copyright"] { background: #2d0766 !important; } .rapidbooru-sidebar-tag-btn[data-category="copyright"] { background: #8454cc !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="character"] { background: #04472d !important; } .rapidbooru-sidebar-tag-btn[data-category="character"] { background: #2ab367 !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="general"] { background: #10164d !important; } .rapidbooru-sidebar-tag-btn[data-category="general"] { background: #47799c !important; } .rapidbooru-sidebar-tag-btn.selected[data-category="meta"] { background: #1f1c03 !important; } .rapidbooru-sidebar-tag-btn[data-category="meta"] { background: #79722a !important; } .rapidbooru-sidebar-tag-btn { outline: none; } .rapidbooru-sidebar-tag-btn:active { filter: brightness(0.95); } `; document.head.appendChild(style); } } // [추가] 테마 및 기능 설정 const rapidbooruSettings = { '--rapidbooru-bg-color': '#2a2a2a', '--rapidbooru-secondary-bg-color': '#1a1a1a', '--rapidbooru-accent-color': '#667eea', '--rapidbooru-accent-hover-color': '#5a6fd8', '--rapidbooru-text-color': '#ffffff', '--rapidbooru-border-color': '#444', 'searchOrder': 'Jaccard', }; function applySettings(settings) { const styleId = 'rapidbooru-theme-styles'; let styleElement = document.getElementById(styleId); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = styleId; document.head.appendChild(styleElement); } const cssVariables = Object.entries(settings) .filter(([key]) => key.startsWith('--')) .map(([key, value]) => `${key}: ${value};`) .join('\n'); styleElement.textContent = `:root { ${cssVariables} }`; } function saveSettings(settings) { Object.assign(rapidbooruSettings, settings); // Update global settings object localStorage.setItem('rapidbooru_settings', JSON.stringify(rapidbooruSettings)); applySettings(rapidbooruSettings); } function loadSettings() { const savedSettings = localStorage.getItem('rapidbooru_settings'); if (savedSettings) { try { Object.assign(rapidbooruSettings, JSON.parse(savedSettings)); } catch (e) { console.error('Rapidbooru: Failed to parse settings', e); localStorage.removeItem('rapidbooru_settings'); } } applySettings(rapidbooruSettings); } function createSettingsPanel() { const panelId = 'rapidbooru-settings-panel'; if (document.getElementById(panelId)) return; const panel = document.createElement('div'); panel.id = panelId; panel.className = 'rapidbooru-overlay'; panel.style.display = 'none'; // Initially hidden panel.style.zIndex = '10003'; // Ensure panel is on top of the button const content = document.createElement('div'); content.className = 'rapidbooru-preview'; content.style.flexDirection = 'column'; content.style.width = '400px'; content.style.height = 'auto'; content.style.padding = '20px'; content.style.background = 'var(--rapidbooru-bg-color)'; content.style.color = 'var(--rapidbooru-text-color)'; const title = document.createElement('h3'); title.textContent = 'Settings'; title.style.textAlign = 'center'; title.style.marginBottom = '20px'; const hidePanel = () => { panel.classList.remove('show'); // Start fade-out animation // After transition, hide the element completely setTimeout(() => { panel.style.display = 'none'; }, 300); // Must match CSS transition duration }; const form = document.createElement('div'); form.style.display = 'grid'; form.style.gridTemplateColumns = 'auto 1fr'; form.style.gap = '15px'; form.style.alignItems = 'center'; const createColorInput = (label, key) => { const labelEl = document.createElement('label'); labelEl.textContent = label; const inputEl = document.createElement('input'); inputEl.type = 'color'; inputEl.value = rapidbooruSettings[key]; inputEl.dataset.key = key; inputEl.style.width = '100px'; inputEl.style.height = '40px'; inputEl.style.border = 'none'; inputEl.style.padding = '0'; inputEl.style.background = 'none'; inputEl.style.cursor = 'pointer'; form.appendChild(labelEl); form.appendChild(inputEl); }; createColorInput('프리뷰 창 배경', '--rapidbooru-bg-color'); createColorInput('프리뷰 창 사이드', '--rapidbooru-secondary-bg-color'); createColorInput('메인 버튼1', '--rapidbooru-accent-color'); createColorInput('메인 버튼2', '--rapidbooru-accent-hover-color'); createColorInput('글자', '--rapidbooru-text-color'); createColorInput('테두리', '--rapidbooru-border-color'); // [추가] 검색 방식 드롭다운 const createSelectInput = (label, key, options) => { const labelEl = document.createElement('label'); labelEl.textContent = label; const selectEl = document.createElement('select'); selectEl.dataset.key = key; selectEl.style.width = '100px'; selectEl.style.height = '40px'; selectEl.style.padding = '5px'; selectEl.style.background = 'var(--rapidbooru-secondary-bg-color)'; selectEl.style.color = 'var(--rapidbooru-text-color)'; selectEl.style.border = `1px solid var(--rapidbooru-border-color)`; selectEl.style.borderRadius = '4px'; options.forEach(opt => { const optionEl = document.createElement('option'); optionEl.value = opt; optionEl.textContent = opt; selectEl.appendChild(optionEl); }); form.appendChild(labelEl); form.appendChild(selectEl); }; createSelectInput('검색 방식', 'searchOrder', ['Jaccard', 'Frequency']); const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'flex-end'; buttonContainer.style.gap = '10px'; buttonContainer.style.marginTop = '20px'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.className = 'rapidbooru-btn'; saveBtn.onclick = () => { const newSettings = { ...rapidbooruSettings }; form.querySelectorAll('[data-key]').forEach(input => { newSettings[input.dataset.key] = input.value; }); saveSettings(newSettings); hidePanel(); }; const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; closeBtn.className = 'rapidbooru-btn'; closeBtn.style.background = '#aaa'; closeBtn.onclick = () => { hidePanel(); }; buttonContainer.appendChild(closeBtn); buttonContainer.appendChild(saveBtn); // [여기서부터 추가] 초기화 버튼 const resetBtn = document.createElement('button'); resetBtn.type = 'button'; resetBtn.className = 'rapidbooru-btn'; resetBtn.style.position = 'absolute'; resetBtn.style.left = '24px'; resetBtn.style.bottom = '24px'; resetBtn.style.background = '#e74c3c'; resetBtn.style.color = '#fff'; resetBtn.style.display = 'flex'; resetBtn.style.alignItems = 'center'; resetBtn.style.gap = '6px'; resetBtn.innerHTML = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style="vertical-align:middle;"> <path d="M12 5V2L7 7l5 5V8c3.31 0 6 2.69 6 6 0 3.31-2.69 6-6 6s-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" fill="currentColor"/> </svg> Reset `; // 기본값 정의 const rapidbooruDefaultSettings = { '--rapidbooru-bg-color': '#2a2a2a', '--rapidbooru-secondary-bg-color': '#1a1a1a', '--rapidbooru-accent-color': '#667eea', '--rapidbooru-accent-hover-color': '#5a6fd8', '--rapidbooru-text-color': '#ffffff', '--rapidbooru-border-color': '#444', 'searchOrder': 'Jaccard', }; resetBtn.onclick = () => { saveSettings({ ...rapidbooruDefaultSettings }); // 폼 값도 즉시 반영 Object.entries(rapidbooruDefaultSettings).forEach(([key, value]) => { const input = panel.querySelector(`[data-key="${key}"]`); if (input) input.value = value; }); }; // content에 상대적 위치를 위해 position:relative 적용 content.style.position = 'relative'; content.appendChild(resetBtn); // [여기까지 추가] content.appendChild(title); content.appendChild(form); content.appendChild(buttonContainer); panel.appendChild(content); panel.onclick = (e) => { if (e.target === panel) { hidePanel(); } }; document.body.appendChild(panel); } function createSettingsButton() { const btn = document.createElement('button'); btn.id = 'rapidbooru-settings-btn'; btn.title = 'Rapidbooru Settings'; btn.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c.04.32.07.65.07.98s-.03.66-.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" fill="currentColor"/> </svg>`; btn.onclick = () => { const panel = document.getElementById('rapidbooru-settings-panel'); if (panel) { panel.style.display = 'flex'; // Make it visible setTimeout(() => panel.classList.add('show'), 10); // Add 'show' to trigger fade-in // Populate form with current settings from the global object Object.entries(rapidbooruSettings).forEach(([key, value]) => { const input = panel.querySelector(`[data-key="${key}"]`); if (input) { input.value = value; } }); } }; document.body.appendChild(btn); } // 초기화 function init() { loadSettings(); addStyles(); addLuckyButton(); setupImagePreview(); setupTagPreview(); setupRelatedTagsFeature(); setupSidebarTagButtonList(); // [추가] 사이드바 태그 버튼형 리스트 setupTagGroupPageTagButtons(); // [추가] 태그 그룹 페이지용 태그 버튼 컨테이너 createSettingsPanel(); createSettingsButton(); console.log('Rapidbooru script loaded'); } // DOM이 로드되면 초기화 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();