// ==UserScript== // @name 아카라이브 썸네일 이미지, 이미지 뷰어, 모두 열기 // @version 1.56 // @icon https://www.google.com/s2/favicons?sz=64&domain=arca.live // @description 아카라이브 썸네일 이미지 생성, 이미지 뷰어, 모두 열기 버튼 생성, 그 외 잡다한 기능.. // @author ChatGPT // @match https://arca.live/b/* // @match https://arca.live/u/scrap_list // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @namespace Violentmonkey Scripts // @downloadURL none // ==/UserScript== (function() { 'use strict'; var config = { buttons: true, openAllButton: true, downAllButton: false, compressFiles: true, countImages: false, originalImage: false, downNumber: false, controlButtons: false, closeButton: false, bookmarkButton: false, downButton: false, thumbnail: true, thumbWidth: 100, thumbHeight: 62, thumbHover: true, thumbBlur: true, thumbBlurAmount: 2, thumbShadow: false, origThumb: false, thumbHoverBest: true, viewer: true, scrapList: true, test: false, test01: false, test02: false, test03: false }; function handleSettings() { var descriptions = { buttons: '상단 버튼 생성', openAllButton: '모두 열기 버튼 생성', downAllButton: '모든 이미지 다운로드 버튼 생성', compressFiles: '모든 이미지를 압축해서 다운로드', countImages: '모든 이미지의 총 개수를 구하고 진행률 표시', originalImage: '원본 이미지로 다운로드(체크 해제시 webp저장)', downNumber: '게시글 번호를 누르면 해당 게시글 이미지 다운로드', controlButtons: '하단 우측 조작 버튼 생성', closeButton: '창닫기 버튼 생성', bookmarkButton: '스크랩 버튼 생성', downButton: '다운로드 버튼 생성', thumbnail: '프리뷰 이미지로 썸네일 생성', thumbWidth: '썸네일 너비', thumbHeight: '썸네일 높이', thumbHover: '썸네일에 마우스 올리면 프리뷰 이미지 출력', thumbBlur: '썸네일 블러 효과', thumbBlurAmount: '썸네일 블러 효과의 정도', thumbShadow: '썸네일 그림자 효과', origThumb: '썸네일 클릭 시 원본 이미지 불러오기(유머 채널 개념글)', thumbHoverBest: '썸네일에 마우스 올리면 프리뷰 이미지 출력(유머 채널 개념글)', viewer: '게시물 이미지 클릭시 이미지 뷰어로 열기', scrapList: '스크랩한 게시글 채널별, 탭별 필터링', test: '실험실', test01: '프리뷰 이미지를 다른 이미지로 대체(특정 조건의 썸네일)', test02: '프리뷰 이미지를 다른 이미지로 대체(사용자 지정 썸네일)', test03: '대체한 프리뷰 이미지를 썸네일에도 적용' }; var mainConfigKeys = Object.keys(descriptions).filter(key => !['openAllButton', 'downAllButton', 'closeButton', 'bookmarkButton', 'downButton', 'compressFiles', 'countImages', 'originalImage', 'downNumber', 'thumbWidth', 'thumbHeight', 'thumbHover', 'thumbBlur', 'thumbBlurAmount', 'thumbShadow', 'origThumb', 'thumbHoverBest', 'test01', 'test02', 'test03'].includes(key) ); function saveConfig() { for (var key in config) { if (config.hasOwnProperty(key)) { GM_setValue(key, config[key]); } } } function loadConfig() { for (var key in config) { if (config.hasOwnProperty(key)) { config[key] = GM_getValue(key, config[key]); } } } function createBaseWindow(id, titleText) { var existingWindow = document.getElementById(id); if (existingWindow) { existingWindow.remove(); } var window = document.createElement('div'); Object.assign(window.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '250px', padding: '20px', background: '#ffffff', border: '1px solid #cccccc', borderRadius: '10px', boxShadow: '0px 0px 10px rgba(0, 0, 0, 0.3)', zIndex: '9999', textAlign: 'left', display: 'flex', flexDirection: 'column', alignItems: 'center' }); window.id = id; var title = document.createElement('div'); Object.assign(title.style, { fontSize: '24px', fontWeight: 'bold', marginBottom: '10px' }); title.innerHTML = titleText; window.appendChild(title); return window; } function createConfigInput(key) { var configDiv = document.createElement('div'); Object.assign(configDiv.style, { marginBottom: '5px', display: 'flex', alignItems: 'center' }); var label = document.createElement('label'); label.innerHTML = key + ': '; Object.assign(label.style, { marginRight: '5px', marginBottom: '3px' }); label.title = descriptions[key]; var input = document.createElement('input'); input.type = (typeof config[key] === 'boolean') ? 'checkbox' : 'text'; input.value = config[key]; input.checked = config[key]; if (input.type === 'text') { Object.assign(input.style, { width: '40px', height: '20px', padding: '0 5px' }); } input.addEventListener('input', function(event) { config[key] = event.target.type === 'checkbox' ? event.target.checked : event.target.value; }); configDiv.appendChild(label); configDiv.appendChild(input); if (['buttons', 'downAllButton', 'controlButtons', 'downButton', 'thumbnail', 'test', 'test02'].includes(key)) { var settingsIcon = document.createElement('span'); settingsIcon.innerHTML = '⚙️'; Object.assign(settingsIcon.style, { cursor: 'pointer', marginLeft: '3px', marginBottom: '2px' }); settingsIcon.addEventListener('click', function() { var windowFunctions = { buttons: createButtonsWindow, downAllButton: createDownloadWindow, controlButtons: createControlButtonsWindow, downButton: createDownloadWindow, thumbnail: createThumbnailWindow, test: createTestWindow, test02: createFilterWindow }; windowFunctions[key] && windowFunctions[key](); }); configDiv.appendChild(settingsIcon); } return configDiv; } function createButton(text, color, onClick) { var button = document.createElement('button'); button.innerHTML = text; Object.assign(button.style, { border: '1px solid #cccccc', borderRadius: '5px', marginRight: '10px' }); button.addEventListener('click', onClick); button.addEventListener('mouseover', function() { button.style.background = color; button.style.color = '#ffffff'; }); button.addEventListener('mouseout', function() { button.style.background = ''; button.style.color = '#000000'; }); return button; } function createButtonContainer(confirmText, cancelText, onConfirm, onCancel) { var buttonContainer = document.createElement('div'); Object.assign(buttonContainer.style, { display: 'flex', marginTop: '10px' }); buttonContainer.appendChild(createButton(confirmText, '#007bff', onConfirm)); buttonContainer.appendChild(createButton(cancelText, '#ff0000', onCancel)); return buttonContainer; } function createSettingsWindow() { var settingsWindow = createBaseWindow('settingsWindow', 'Settings'); mainConfigKeys.forEach(function(key) { settingsWindow.appendChild(createConfigInput(key)); }); var tooltip = document.createElement('div'); Object.assign(tooltip.style, { fontSize: '12px', marginTop: '5px', marginBottom: '10px', color: 'gray' }); tooltip.innerHTML = '마우스를 올리면 설명이 나옵니다'; settingsWindow.appendChild(tooltip); settingsWindow.appendChild(createButtonContainer('확인', '취소', function() { saveConfig(); settingsWindow.remove(); location.reload(); }, function() { settingsWindow.remove(); })); document.body.appendChild(settingsWindow); } function createSubSettingsWindow(id, title, keys, additionalContent) { var subWindow = createBaseWindow(id, title); // keys가 있으면 Config 입력들을 추가 keys.forEach(function(key) { subWindow.appendChild(createConfigInput(key)); }); // 추가로 받은 내용들을 subWindow에 추가 if (additionalContent) { Object.keys(additionalContent).forEach(function(key) { subWindow.appendChild(additionalContent[key]); }); } // 버튼 추가 subWindow.appendChild(createButtonContainer('확인', '취소', function() { saveConfig(); subWindow.remove(); }, function() { subWindow.remove(); })); document.body.appendChild(subWindow); } function createButtonsWindow() { createSubSettingsWindow('buttonsWindow', 'Buttons', ['openAllButton', 'downAllButton']); } function createDownloadWindow() { createSubSettingsWindow('downloadSettingsWindow', 'Download', ['compressFiles', 'countImages', 'originalImage', 'downNumber']); } function createControlButtonsWindow() { createSubSettingsWindow('controlButtonsWindow', 'Control Buttons', ['closeButton', 'bookmarkButton', 'downButton']); } function createThumbnailWindow() { createSubSettingsWindow('thumbnailWindow', 'Thumbnail', ['thumbWidth', 'thumbHeight', 'thumbHover', 'thumbBlur', 'thumbBlurAmount', 'thumbShadow', 'origThumb', 'thumbHoverBest']); } function createTestWindow() { createSubSettingsWindow('testWindow', 'Test', ['test01', 'test02', 'test03']); } function createFilterWindow() { var savedLinks = GM_getValue('savedLinks', []); // 링크 목록 생성 부분 var linkListContainer = document.createElement('div'); linkListContainer.id = 'linkListContainer'; linkListContainer.style.display = 'flex'; linkListContainer.style.flexWrap = 'wrap'; linkListContainer.style.justifyContent = 'center'; // 가로 중앙 정렬 linkListContainer.style.alignItems = 'center'; // 세로 중앙 정렬 linkListContainer.style.gap = '10px'; // Adds spacing between items linkListContainer.style.marginBottom = '10px'; savedLinks.forEach(function (link, index) { var linkDiv = document.createElement('div'); linkDiv.style.display = 'flex'; linkDiv.style.flexDirection = 'column'; linkDiv.style.alignItems = 'center'; linkDiv.style.marginTop = '10px'; linkDiv.style.marginBottom = '10px'; linkDiv.style.width = '60px'; // Ensure all items have a fixed width to align properly // 썸네일 이미지 var thumbnail = document.createElement('img'); thumbnail.src = link; thumbnail.style.width = '50px'; thumbnail.style.height = '50px'; thumbnail.style.objectFit = 'cover'; thumbnail.style.marginBottom = '5px'; // 이미지 클릭 시 링크 입력창에 설정 thumbnail.addEventListener('click', function () { var linkInput = document.querySelector('#linkInput'); if (linkInput) { linkInput.value = link; } }); // 삭제 버튼 var deleteButton = document.createElement('button'); deleteButton.textContent = 'Delete'; deleteButton.style.border = '1px solid #cccccc'; deleteButton.style.borderRadius = '5px'; deleteButton.style.marginTop = '5px'; deleteButton.style.fontSize = '12px'; deleteButton.addEventListener('click', function () { savedLinks.splice(index, 1); GM_setValue('savedLinks', savedLinks); createFilterWindow(); // 링크 삭제 후 새로고침 }); linkDiv.appendChild(thumbnail); linkDiv.appendChild(deleteButton); linkListContainer.appendChild(linkDiv); }); // 링크 추가 입력창과 버튼 var addLinkContainer = document.createElement('div'); addLinkContainer.style.display = 'flex'; addLinkContainer.style.alignItems = 'center'; var linkInput = document.createElement('input'); linkInput.type = 'text'; linkInput.id = 'linkInput'; linkInput.placeholder = '썸네일 링크 입력'; linkInput.style.flex = '1'; linkInput.style.marginRight = '5px'; linkInput.style.padding = '5px'; linkInput.style.width = '180px'; linkInput.style.fontSize = '12px'; var addLinkButton = document.createElement('button'); addLinkButton.textContent = 'Add'; addLinkButton.style.border = '1px solid #cccccc'; addLinkButton.style.borderRadius = '5px'; addLinkButton.addEventListener('click', function () { var newLink = linkInput.value.trim(); if (newLink && !savedLinks.includes(newLink)) { savedLinks.push(newLink); GM_setValue('savedLinks', savedLinks); createFilterWindow(); // 새 링크 추가 후 새로고침 } }); addLinkContainer.appendChild(linkInput); addLinkContainer.appendChild(addLinkButton); // 툴팁 var tooltip = document.createElement('div'); Object.assign(tooltip.style, { fontSize: '12px', marginTop: '5px', marginBottom: '10px', color: 'gray' }); tooltip.innerHTML = '해당 게시글의 다른 이미지로 대체'; createSubSettingsWindow('filterWindow', 'Filter', [], { linkListContainer: linkListContainer, addLinkContainer: addLinkContainer, tooltip: tooltip }); } loadConfig(); GM_registerMenuCommand('설정', function() { createSettingsWindow(); }); } function arcaLiveScrapList() { const header = document.querySelector('.list-table '); // 부모 div (전체 컨테이너) var containerDiv = document.createElement('div'); containerDiv.style.marginBottom = '0.5rem'; // 페이징 요소의 HTML을 가져옵니다. const paginationHTML = document.querySelector('.pagination.justify-content-center'); const paginationDiv = document.createElement('div'); paginationDiv.innerHTML = paginationHTML.outerHTML; paginationDiv.style.marginBottom = '0.5rem'; // Create filter div (채널과 탭 필터) const filterDiv = document.createElement('div'); filterDiv.className = 'filterDiv'; filterDiv.style.display = 'flex'; // Create channel filter const channelFilter = document.createElement('select'); channelFilter.className = 'form-control select-list-type'; channelFilter.name = 'sort'; channelFilter.style.cssText = 'width: auto; height: 2rem; float: left; padding: 0 0 0 .5rem; font-size: .9rem;'; const defaultChannelOption = document.createElement('option'); defaultChannelOption.value = ''; defaultChannelOption.text = '채널 선택'; channelFilter.appendChild(defaultChannelOption); filterDiv.appendChild(channelFilter); // Create tab filter const tabFilter = document.createElement('select'); tabFilter.className = 'form-control select-list-type'; tabFilter.name = 'sort'; tabFilter.style.cssText = 'width: auto; height: 2rem; float: left; padding: 0 0 0 .5rem; font-size: .9rem;'; const defaultTabOption = document.createElement('option'); defaultTabOption.value = ''; defaultTabOption.text = '탭 선택'; tabFilter.appendChild(defaultTabOption); filterDiv.appendChild(tabFilter); // gridContainer에 각 영역 추가 containerDiv.appendChild(paginationDiv); containerDiv.appendChild(filterDiv); // 문서의 body에 추가 (혹은 다른 부모 요소에 추가 가능) header.parentNode.insertBefore(containerDiv, header); // Collect channels and tabs const posts = document.querySelectorAll('.vrow.column.filtered, .vrow.column'); const channelTabMap = {}; posts.forEach(post => { const badges = post.querySelectorAll('.badge'); if (badges.length >= 2) { const channel = badges[0].textContent.trim(); const tab = badges[1].textContent.trim(); if (!channelTabMap[channel]) { channelTabMap[channel] = new Set(); } channelTabMap[channel].add(tab); } }); // Populate channel filter Object.keys(channelTabMap).forEach(channel => { const option = document.createElement('option'); option.value = channel; option.text = channel; channelFilter.appendChild(option); }); // Update tab filter based on selected channel function updateTabFilter() { const selectedChannel = channelFilter.value; tabFilter.innerHTML = ''; const defaultTabOption = document.createElement('option'); defaultTabOption.value = ''; defaultTabOption.text = '탭 선택'; tabFilter.appendChild(defaultTabOption); if (selectedChannel && channelTabMap[selectedChannel]) { channelTabMap[selectedChannel].forEach(tab => { const option = document.createElement('option'); option.value = tab; option.text = tab; tabFilter.appendChild(option); }); } filterPosts(); } // Filter posts based on selected channel and tab function filterPosts() { const selectedChannel = channelFilter.value; const selectedTab = tabFilter.value; posts.forEach(post => { const badges = post.querySelectorAll('.badge'); if (badges.length >= 2) { const postChannel = badges[0].textContent.trim(); const postTab = badges[1].textContent.trim(); if ((selectedChannel === '' || postChannel === selectedChannel) && (selectedTab === '' || postTab === selectedTab)) { post.style.display = ''; } else { post.style.display = 'none'; } } }); } channelFilter.addEventListener('change', updateTabFilter); tabFilter.addEventListener('change', filterPosts); } function arcaLive() { // 모두 열기 버튼 생성 if (config.openAllButton) { var openAllButton = document.createElement('a'); openAllButton.className = 'btn btn-sm btn-primary float-left'; openAllButton.href = '#'; openAllButton.innerHTML = ' 모두 열기 '; openAllButton.style.height = '2rem'; openAllButton.addEventListener('click', function(event) { event.preventDefault(); // 게시글의 수를 계산 var posts = document.querySelectorAll('a.vrow.column:not(.notice)'); var postCount = 0; // 필터링된 게시글을 제외한 수를 계산 posts.forEach(function(element) { var href = element.getAttribute('href'); var classes = element.className.split(' '); if (href && !classes.includes('filtered') && !classes.includes('filtered-keyword')) { postCount++; } }); // 게시글 수를 포함한 메시지 const confirmMessage = `총 ${postCount}개의 게시글을 한 번에 엽니다.\n계속 진행하시겠습니까?`; // 확인 메시지 표시 if (confirm(confirmMessage)) { posts.forEach(function(element) { var href = element.getAttribute('href'); var classes = element.className.split(' '); if (href && !classes.includes('filtered') && !classes.includes('filtered-keyword')) { window.open(href, '_blank'); } }); } }); var targetElement = document.querySelector('.form-control.select-list-type'); targetElement.parentNode.insertBefore(openAllButton, targetElement); } // 이미지와 동영상 다운로드 버튼 생성 if (config.downAllButton) { async function getTotalImages(urls) { let totalImages = 0; for (const url of urls) { const response = await fetch(url); const html = await response.text(); const doc = new DOMParser().parseFromString(html, "text/html"); const imageElements = Array.from(doc.querySelectorAll('.article-body img')).filter(img => !img.classList.contains('arca-emoticon')); const gifVideoElements = doc.querySelectorAll('video[data-orig="gif"][data-originalurl]'); totalImages += imageElements.length + gifVideoElements.length; } return totalImages; } async function downloadMediaSequentially(urls, totalImages, compressFiles) { let totalDownloaded = 0; // 프로그레스 바 업데이트 함수 function updateProgressBar(progress) { const progressBar = document.getElementById('progress-bar'); progressBar.style.width = progress + '%'; progressBar.innerHTML = progress + '%'; if (progress === 100) { setTimeout(() => { progressBar.style.backgroundColor = 'orange'; // 100%일 때 배경색을 주황색으로 변경 }, 300); // 잠시 딜레이를 주어 애니메이션 완료 후 색상 변경 } } async function downloadFile(url, index, type, requestUrl, zip, title) { const response = await fetch(url); const blob = await response.blob(); const extension = type === 'img' ? (config.originalImage ? url.split('.').pop().split('?')[0].toLowerCase() : 'webp') : 'gif'; const numbersFromUrl = requestUrl.match(/\d+/)[0]; const fileIndex = index + 1; // Index를 1 증가시킴 // const sanitizedTitle = title.replace(/[^a-zA-Z0-9가-힣\s]/g, '_'); // 파일 이름에 사용할 수 있도록 제목을 정제 const numberedFileName = compressFiles ? `${title}_${String(fileIndex).padStart(2, '0')}.${extension}` : `${numbersFromUrl}_${String(fileIndex).padStart(2, '0')}.${extension}`; if (zip) { zip.file(numberedFileName, blob); } else { const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = numberedFileName; link.click(); } } async function processNextUrl() { for (let index = 0; index < urls.length; index++) { const url = urls[index]; let zip; if (compressFiles) { zip = new JSZip(); } const response = await fetch(url); const html = await response.text(); const doc = new DOMParser().parseFromString(html, "text/html"); const titleElement = doc.querySelector('.title-row .title'); let title = ''; if (titleElement) { const textNodes = Array.from(titleElement.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE && node.parentElement === titleElement); if (textNodes.length > 0) { title = textNodes.map(node => node.textContent.trim()).join(''); } } // arca-emoticon 클래스를 가진 이미지를 제외하고 선택 const mediaElements = Array.from(doc.querySelectorAll('.article-body img, .article-body video[data-orig="gif"]')).filter(media => !media.classList.contains('arca-emoticon')); try { if (mediaElements.length > 0) { for (let i = 0; i < mediaElements.length; i++) { const media = mediaElements[i]; const mediaType = media.tagName.toLowerCase(); const mediaUrl = mediaType === 'img' ? (config.originalImage ? media.getAttribute('src') + "&type=orig" : media.getAttribute('src')) : media.getAttribute('data-originalurl'); if (mediaUrl) { await downloadFile(mediaUrl, i, mediaType, url, zip, title); totalDownloaded++; if (config.countImages) { const progress = Math.round((totalDownloaded / totalImages) * 100); updateProgressBar(progress); } } } if (zip) { const content = await zip.generateAsync({ type: 'blob' }); const numbersFromUrl = url.match(/\d+/)[0]; const zipFileName = `${numbersFromUrl}.zip`; const zipLink = document.createElement('a'); zipLink.href = window.URL.createObjectURL(content); zipLink.download = zipFileName; zipLink.click(); } } } catch (error) { console.error("Error downloading media:", error); } } } await processNextUrl(); } var downloadMediaButton = document.createElement('a'); downloadMediaButton.className = 'btn btn-sm btn-success float-left'; downloadMediaButton.href = '#'; downloadMediaButton.innerHTML = ' 다운로드 '; downloadMediaButton.style.position = 'relative'; // 상대 위치 지정 // 프로그레스 바 스타일을 가진 div 엘리먼트 추가 var progressBar = document.createElement('div'); progressBar.id = 'progress-bar'; // ID 추가 progressBar.style.position = 'absolute'; // 절대 위치 지정 progressBar.style.bottom = '5%'; progressBar.style.left = '0'; progressBar.style.width = '0%'; // 초기 너비는 0% progressBar.style.height = '10%'; progressBar.style.backgroundColor = 'yellow'; // 프로그레스 바 색상 progressBar.style.borderRadius = 'inherit'; progressBar.style.transition = 'width 0.3s ease-in-out'; // 프로그레스 바 애니메이션 downloadMediaButton.appendChild(progressBar); // 프로그레스 바를 버튼에 추가 downloadMediaButton.addEventListener('click', async function(event) { event.preventDefault(); var mediaUrls = []; // 다운로드할 미디어 URL을 저장할 배열 document.querySelectorAll('a.vrow.column:not(.notice)').forEach(function(element) { var href = element.getAttribute('href'); var classes = element.className.split(' '); if (classes.includes('filtered') || classes.includes('filtered-keyword')) { return; // 해당 조건이 맞으면 다음으로 넘어감 } if (href) { mediaUrls.push(href); // 미디어 URL을 배열에 추가 } }); const mediaUrlsCount = mediaUrls.length; if (config.countImages) { const initialMessage = `총 ${mediaUrlsCount}개의 게시글을 찾았습니다.\n모든 게시글의 이미지를 확인하여 총 개수를 계산합니다.\n계산하는 데 시간이 오래 걸릴 수 있습니다.\n(설정에서 변경 가능)`; alert(initialMessage); const totalImages = await getTotalImages(mediaUrls); const confirmMessage = `다운로드해야 할 이미지의 총 개수는 ${totalImages}개입니다.\n계속해서 다운로드 하시겠습니까?`; if (confirm(confirmMessage)) { progressBar.style.width = '0%'; // 초기 너비는 0% progressBar.style.backgroundColor = 'yellow'; // 프로그레스 바 색상 await downloadMediaSequentially(mediaUrls, totalImages, config.compressFiles); // config.compressFiles 변수 전달 } } else { // 프로그레스 바를 사용하지 않을 경우에는 다운로드 여부를 확인하는 창 띄우기 const confirmMessage = `총 ${mediaUrlsCount}개의 게시글을 한 번에 다운로드합니다.\n다운로드를 진행하시겠습니까?`; if (confirm(confirmMessage)) { progressBar.style.width = '0%'; // 초기 너비는 0% progressBar.style.backgroundColor = 'yellow'; // 프로그레스 바 색상 await downloadMediaSequentially(mediaUrls, 0, config.compressFiles); // config.compressFiles 변수 전달 progressBar.style.width = '100%'; progressBar.style.backgroundColor = 'orange'; // 100%일 때 배경색을 주황색으로 변경 } } }); var targetElement = document.querySelector('.form-control.select-list-type'); targetElement.parentNode.insertBefore(downloadMediaButton, targetElement); } if (config.downNumber) { // document.addEventListener("DOMContentLoaded", function() { // }); document.querySelectorAll('.vrow.column:not(.notice) .vcol.col-id').forEach(function(link) { link.addEventListener('click', async function(event) { event.preventDefault(); // 기본 동작 방지 link.style.color = 'orange'; // 다운로드 시작 시 노란색으로 변경 const parentHref = link.closest('.vrow.column').getAttribute('href'); await downloadMediaFromUrl(parentHref, config.compressFiles); // compressFiles 변수 전달 link.style.color = 'red'; // 다운로드 완료 시 빨간색으로 변경 }); }); async function downloadMediaFromUrl(url, compressFiles) { // compressFiles 변수 추가 const response = await fetch(url); const html = await response.text(); const doc = new DOMParser().parseFromString(html, "text/html"); const mediaElements = Array.from(doc.querySelectorAll('.article-body img, .article-body video[data-orig="gif"]')).filter(media => !media.classList.contains('arca-emoticon')); let zip; const titleElement = doc.querySelector('.title-row .title'); let title = ''; if (titleElement) { const textNodes = Array.from(titleElement.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE && node.parentElement === titleElement); if (textNodes.length > 0) { title = textNodes.map(node => node.textContent.trim()).join(''); } } async function downloadFile(mediaUrl, index, type) { const response = await fetch(mediaUrl); const blob = await response.blob(); const extension = type === 'img' ? (config.originalImage ? mediaUrl.split('.').pop().split('?')[0].toLowerCase() : 'webp') : 'gif'; const fileIndex = index + 1; const numbersFromUrl = url.match(/\d+/)[0]; // const sanitizedTitle = title.replace(/[^a-zA-Z0-9가-힣\s]/g, '_'); // 파일 이름에 사용할 수 있도록 제목을 정제 const numberedFileName = compressFiles ? `${title}_${String(fileIndex).padStart(2, '0')}.${extension}` : `${numbersFromUrl}_${String(fileIndex).padStart(2, '0')}.${extension}`; if (compressFiles) { zip.file(numberedFileName, blob); } else { const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = numberedFileName; link.click(); } } async function processMedia() { for (let i = 0; i < mediaElements.length; i++) { const media = mediaElements[i]; const mediaType = media.tagName.toLowerCase(); const mediaUrl = mediaType === 'img' ? (config.originalImage ? media.getAttribute('src') + "&type=orig" : media.getAttribute('src')) : media.getAttribute('data-originalurl'); if (mediaUrl) { await downloadFile(mediaUrl, i, mediaType); } } } if (compressFiles) { zip = new JSZip(); } await processMedia(); if (compressFiles) { const content = await zip.generateAsync({ type: 'blob' }); const zipFileName = url.match(/\d+/)[0] + '.zip'; const zipLink = document.createElement('a'); zipLink.href = window.URL.createObjectURL(content); zipLink.download = zipFileName; zipLink.click(); } } } if (config.thumbnail) { document.addEventListener("DOMContentLoaded", function() { function checkBlackEdge(img, callback) { var newImg = new Image(); newImg.crossOrigin = 'anonymous'; newImg.onload = function() { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = newImg.width; canvas.height = newImg.height; ctx.drawImage(newImg, 0, 0, newImg.width, newImg.height); var edgeSize = Math.min(newImg.width, newImg.height) * 0.1; var imgData = ctx.getImageData(0, 0, newImg.width, newImg.height); var totalPixels = 0; var blackPixels = 0; for (var x = 0; x < newImg.width; x++) { for (var y = 0; y < newImg.height; y++) { if (x < edgeSize || x >= newImg.width - edgeSize || y < edgeSize || y >= img.height - edgeSize) { totalPixels++; var index = (y * newImg.width + x) * 4; var pixelData = [ imgData.data[index], // Red imgData.data[index + 1], // Green imgData.data[index + 2] // Blue ]; if (pixelData[0] === 0 && pixelData[1] === 0 && pixelData[2] === 0) { blackPixels++; } } } } var blackPercentage = blackPixels / totalPixels; if (blackPercentage >= 0.33) { callback(true); } else { callback(false); } }; newImg.onerror = function() { // 이미지 로드 실패 시에도 콜백 호출하여 처리 callback(false); }; newImg.src = img.src + "&type=list"; // newImg.src = img.src + "&type=list"; } function setSecondImg(vrow, img) { var href = vrow.href; fetch(href) .then(response => { if (!response.ok) { throw new Error('Request failed with status ' + response.status); } return response.text(); }) .then(responseText => { var parser = new DOMParser(); var htmlDoc = parser.parseFromString(responseText, "text/html"); var contentDiv = htmlDoc.querySelector('div.fr-view.article-content'); if (!contentDiv) { return; } var Tags = contentDiv.querySelectorAll('img, video'); var firstTag = null; for (var i = 0; i < Tags.length; i++) { firstTag = Tags[i]; if (firstTag.style.width == '2px' && firstTag.style.height == '2px') { break; } else if (firstTag.tagName.toLowerCase() === 'img') { if (!(img.src.split("?")[0] === firstTag.src.split("?")[0])) { break; } } else if (firstTag.tagName.toLowerCase() === 'video') { break; } } if (!firstTag) { return; } var videoOriginalSrc = firstTag.getAttribute('data-originalurl'); var videoOriginalSrcType = firstTag.getAttribute('data-orig'); var videoPosterSrc = firstTag.getAttribute('poster'); var changeImgUrl = null; if (firstTag.tagName.toLowerCase() === 'img') { changeImgUrl = firstTag.src + "&type=list"; } else if (firstTag.tagName.toLowerCase() === 'video') { if (videoOriginalSrc && !videoOriginalSrcType) { changeImgUrl = videoOriginalSrc + "&type=list"; } else if (videoPosterSrc) { changeImgUrl = videoPosterSrc + "&type=list"; } else { changeImgUrl = img.src; } } if (config.test03) { img.onload = function () { img.parentNode.style.border = '2px solid pink'; // img.parentNode.style.boxShadow = 'rgb(255 155 155) 2px 2px 2px'; }; img.src = changeImgUrl; } var previewImg = vrow.querySelector('.vrow-preview img') previewImg.src = changeImgUrl.replace("&type=list", ''); }) .catch(error => { console.error('Error fetching data:', error); }); } const vrows = document.querySelectorAll('a.vrow.column:not(.notice)') vrows.forEach(function(vrow) { var vcolId = vrow.querySelector('.vcol.col-id'); var vcolTitle = vrow.querySelector('.vcol.col-title'); vcolId.style.margin = '0'; vcolId.style.height = 'auto'; vcolId.style.display = 'flex'; vcolId.style.alignItems = 'center'; // 세로 가운데 정렬 vcolId.style.justifyContent = 'center'; // 가로 가운데 정렬 var vcolThumb = vrow.querySelector('.vcol.col-thumb'); if (!vcolThumb) { vcolThumb = document.createElement('span'); vcolThumb.className = 'vcol col-thumb'; vcolThumb.style.width = config.thumbWidth + 'px'; vcolThumb.style.borderRadius = '3px'; vrow.querySelector('.vrow-inner').appendChild(vcolThumb); vcolTitle.parentNode.insertBefore(vcolThumb, vcolTitle); } var vrowPreview = vrow.querySelector('.vrow-preview'); // vrowPreview가 존재할 때만 썸네일을 추가하도록 조건 추가 if (vrowPreview) { var thumbnailCreated = false; // 썸네일 생성 여부 플래그 function createThumbnail() { if (thumbnailCreated) return; // 이미 썸네일이 생성되었으면 더 이상 생성하지 않음 var previewImg = vrowPreview.querySelector('img'); if (!previewImg) return; vrow.style.height = 'auto'; vrow.style.paddingTop = '3.75px'; vrow.style.paddingBottom = '3.75px'; vcolThumb.style.height = config.thumbHeight + 'px'; var thumbImg = vcolThumb.querySelector('img'); if (!thumbImg) { thumbImg = document.createElement('img'); thumbImg.src = previewImg.src; thumbImg.style.width = '100%'; thumbImg.style.height = '100%'; thumbImg.style.objectFit = 'cover'; if (config.thumbShadow) { thumbImg.onload = function () { vcolThumb.style.boxShadow = 'rgba(0, 0, 0, 0.4) 2px 2px 2px'; } } if (config.test) { if (config.test01) { checkBlackEdge(thumbImg, function(hasBlackEdge) { if (hasBlackEdge) { setSecondImg(vrow, thumbImg); } }); } if (config.test02) { function removeQueryString(url) { var parsedUrl = new URL(url); return parsedUrl.origin + parsedUrl.pathname; } var savedLinks = GM_getValue('savedLinks', []); var cleanSrc = removeQueryString(thumbImg.src); if (savedLinks.some(link => cleanSrc.includes(removeQueryString(link)))) { setSecondImg(vrow, thumbImg); console.log("Filtered Image:", vcolId.querySelector('span').textContent, thumbImg.src); } } } if (config.thumbBlur) { thumbImg.style.filter = 'blur(' + config.thumbBlurAmount + 'px)'; thumbImg.addEventListener('mouseenter', function() { thumbImg.style.filter = 'none'; }); thumbImg.addEventListener('mouseleave', function() { thumbImg.style.filter = 'blur(' + config.thumbBlurAmount + 'px)'; }); } if (config.thumbHover) { thumbImg.addEventListener('mouseenter', function() { vrowPreview.style.display = null; }); thumbImg.addEventListener('mouseleave', function() { vrowPreview.style.display = 'none'; }); } vcolThumb.appendChild(thumbImg); thumbnailCreated = true; // 썸네일 생성 완료 } vrowPreview.style.display = 'none'; vrowPreview.style.pointerEvents = 'none'; vrowPreview.style.width = '30rem'; vrowPreview.style.height = 'auto'; vrowPreview.style.top = 'auto'; vrowPreview.style.left = (99) + parseFloat(config.thumbWidth) + 'px'; previewImg.src = previewImg.src.replace("&type=list", ''); } function tryCreateThumbnail(retryCount) { if (retryCount >= 100 || thumbnailCreated) return; // 썸네일이 이미 생성되었으면 더 이상 시도하지 않음 setTimeout(function() { if (retryCount === 0) createThumbnail(); tryCreateThumbnail(retryCount + 1); }, 100); } tryCreateThumbnail(0); } }); }); } // 썸네일 클릭 시 원본 이미지 불러오기 if (config.origThumb) { document.querySelectorAll('a.title.preview-image').forEach(function(link) { link.addEventListener('click', function(event) { event.preventDefault(); // 기본 동작 방지 var imageUrl = link.querySelector('img').getAttribute('src').replace(/&type=list/g, ''); window.location.href = imageUrl; }); }); } // 개념글 미리보기 이미지 마우스 오버시 보이게 if (config.thumbHoverBest) { // 이미지 요소 선택 var vrowPreviewImgs = document.querySelectorAll('.vrow.hybrid .title.preview-image .vrow-preview img'); // 각 이미지 요소에 이벤트 추가 vrowPreviewImgs.forEach(function(vrowPreviewImg) { // 이미지에 호버 이벤트 추가 vrowPreviewImg.addEventListener('mouseenter', function() { // 이미지의 부모 요소 찾기 var parentDiv = vrowPreviewImg.closest('.vrow.hybrid'); // 복제된 이미지 요소 생성 var duplicatevrowPreviewImg = document.createElement('img'); duplicatevrowPreviewImg.src = vrowPreviewImg.src.replace('&type=list', ''); // 복제된 이미지의 스타일 설정 duplicatevrowPreviewImg.style.position = 'absolute'; duplicatevrowPreviewImg.style.width = '30rem'; duplicatevrowPreviewImg.style.height = 'auto'; duplicatevrowPreviewImg.style.top = 'auto'; duplicatevrowPreviewImg.style.left = '7.5rem'; // 오른쪽으로 10rem 이동 duplicatevrowPreviewImg.style.zIndex = '1'; duplicatevrowPreviewImg.style.padding = '5px'; duplicatevrowPreviewImg.style.border = '1px solid'; duplicatevrowPreviewImg.style.borderRadius = '5px'; duplicatevrowPreviewImg.style.boxSizing = 'content-box'; duplicatevrowPreviewImg.style.backgroundColor = '#fff'; // 배경색 duplicatevrowPreviewImg.style.borderColor = '#bbb'; // 테두리 색상 // vrow hybrid 클래스에 align-items: center; 스타일 추가 parentDiv.classList.add('hybrid'); parentDiv.style.alignItems = 'center'; // 수직 가운데 정렬 // 복제된 이미지 요소를 기존 이미지 요소 다음에 추가 parentDiv.appendChild(duplicatevrowPreviewImg); // 마우스를 이미지에서 떼었을 때 복제된 이미지 제거 vrowPreviewImg.addEventListener('mouseleave', function() { duplicatevrowPreviewImg.remove(); }); }); }); } if (config.controlButtons) { if ((config.closeButton || config.bookmarkButton || config.downButton)) { document.addEventListener('DOMContentLoaded', function () { var articleMenu = document.querySelector('.article-menu.mt-2'); var originalScrapButton = articleMenu ? articleMenu.querySelector('.scrap-btn') : null; var originalDownloadButton = articleMenu ? articleMenu.querySelector('#imageToZipBtn') : null; var navControl = document.querySelector('.nav-control'); // 새로운 리스트 아이템 요소를 생성하는 함수 function createNewItem(iconClass, clickHandler, hoverHandler, leaveHandler) { var newItem = document.createElement('li'); newItem.innerHTML = ''; newItem.addEventListener('click', clickHandler); if (hoverHandler) { newItem.addEventListener('mouseenter', hoverHandler); } if (leaveHandler) { newItem.addEventListener('mouseleave', leaveHandler); } return newItem; } // 새로운 아이템을 내비게이션 컨트롤 리스트에 추가하거나 업데이트하는 함수 function appendOrUpdateItem(newItem) { if (navControl) { if (navControl.children.length > 0) { navControl.insertBefore(newItem, navControl.firstElementChild); } else { navControl.appendChild(newItem); } } else { console.error('내비게이션 컨트롤 리스트를 찾을 수 없습니다.'); } } // 다운로드 버튼 생성 if (config.controlButtons && config.downButton) { if (articleMenu) { var downloadButton = createNewItem( 'ion-android-download', downloadButtonClickHandler, downloadButtonHoverHandler, downloadButtonLeaveHandler ); appendOrUpdateItem(downloadButton); } } // 다운로드 버튼 핸들러 function downloadButtonClickHandler() { originalDownloadButton = articleMenu.querySelector('#imageToZipBtn'); if (originalDownloadButton) { // 다운로드 버튼 클릭 originalDownloadButton.click(); var progressChecked = false; // 프로그레스 바가 50% 이상인지 체크하는 변수 var intervalId = setInterval(function () { // 다운로드 진행 상태를 추적할 .download-progress 요소 찾기 var downloadProgress = originalDownloadButton.querySelector('.download-progress'); if (downloadProgress) { // 프로그레스 바가 존재하면 진행 상태의 width 값 확인 var width = parseFloat(downloadProgress.style.width); // 50% 이상이면 완료된 것으로 간주 if (width >= 50) { progressChecked = true; } // 프로그레스 바가 진행되면서 다운로드 버튼의 배경색을 조정 downloadButton.style.background = ` linear-gradient(to top, green ${width}%, transparent ${width}%), #3d414d `; // 프로그레스 바가 100%에 도달했을 때 if (width >= 100) { clearInterval(intervalId); // 애니메이션 종료 downloadButton.style.background = ` linear-gradient(to top, green 100%, transparent 100%), #3d414d `; } } else { // 프로그레스 바가 사라졌을 때 (프로그레스 바가 없을 때) if (progressChecked) { // 프로그레스 바가 50% 이상이었다면 완료된 것으로 간주 downloadButton.style.background = ` linear-gradient(to top, green 100%, transparent 100%), #3d414d `; } else { // 프로그레스 바가 50% 미만이었다면 취소로 간주 downloadButton.style.background = ` linear-gradient(to top, green 0%, transparent 0%), #3d414d `; } clearInterval(intervalId); // 애니메이션 종료 } }, 10); // 10ms마다 확인 } } function downloadButtonHoverHandler() { this.style.backgroundColor = 'green'; } function downloadButtonLeaveHandler() { this.style.backgroundColor = ''; } // 북마크 버튼 생성 if (config.controlButtons && config.bookmarkButton) { if (originalScrapButton) { var bookmarkButton = createNewItem( 'ion-android-bookmark', bookmarkButtonClickHandler, bookmarkButtonHoverHandler, bookmarkButtonLeaveHandler ); appendOrUpdateItem(bookmarkButton); // 북마크 버튼 색상을 업데이트하는 함수 function updateButtonColor() { var buttonText = originalScrapButton.querySelector('.result').textContent.trim(); bookmarkButton.style.backgroundColor = (buttonText === "스크랩 됨") ? '#007bff' : ''; } // 초기 호출 및 MutationObserver 설정 updateButtonColor(); var observer = new MutationObserver(updateButtonColor); observer.observe(originalScrapButton.querySelector('.result'), { childList: true, subtree: true }); } } // 북마크 버튼 핸들러 function bookmarkButtonClickHandler() { if (originalScrapButton) { originalScrapButton.click(); } else { console.error('원래의 스크랩 버튼을 찾을 수 없습니다.'); } } function bookmarkButtonHoverHandler() { this.style.backgroundColor = '#007bff'; } function bookmarkButtonLeaveHandler() { var buttonText = originalScrapButton.querySelector('.result').textContent.trim(); this.style.backgroundColor = (buttonText === "스크랩 됨") ? '#007bff' : ''; } // 닫기 버튼 생성 및 추가 if (config.controlButtons && config.closeButton) { var closeButton = createNewItem( 'ion-close-round', closeButtonClickHandler, closeButtonHoverHandler, closeButtonLeaveHandler ); appendOrUpdateItem(closeButton); } // 닫기 버튼 핸들러 function closeButtonClickHandler() { window.close(); } function closeButtonHoverHandler() { this.style.backgroundColor = 'red'; } function closeButtonLeaveHandler() { this.style.backgroundColor = ''; } }); } } if (config.viewer) { let currentIndex = 0; // 현재 이미지 인덱스 let images = []; // 게시글 내 이미지 배열 let preloadedImages = []; // 미리 로드된 이미지 배열 let viewer = null; // 뷰어 엘리먼트 let viewContainer = null; let leftResizer = null; let imageContainer = null; // 뷰어 이미지 컨테이너 let imageContainerWidth = '70%'; // 뷰어 이미지 컨테이너 let rightResizer = null; let scrollbar = null; // 사용자 정의 스크롤바 let counter = null; // 이미지 카운터 let imageLayoutType = 'single'; // 기본: 한 장씩, 'single' or 'vertical' let scrollSpeed = 1; // 기본 스크롤 속도 (1x) let dragThumb = false; let dragImage = false; let startY = 0; let startTop = 0; let eventTarget = null; let isMobile = false; let debug = false; function getImages() { images = Array.from(document.querySelectorAll(".fr-view.article-content img")) .filter(img => !img.classList.contains("arca-emoticon")) .map(img => img.src.startsWith("//") ? "https:" + img.src : img.src); images.forEach((img, index) => { const imgElement = document.querySelectorAll(".fr-view.article-content img")[index]; imgElement.style.cursor = "pointer"; imgElement.addEventListener("click", (event) => { event.preventDefault(); currentIndex = index; showViewer(); }); }); } function preloadImages() { // 이미지들을 미리 로드 images.forEach(src => { const img = new Image(); img.src = src; // 이미지를 로드 preloadedImages.push(img); // 로드된 이미지를 배열에 저장 }); } function createViewer() { isMobile = window.innerHeight > window.innerWidth; viewer = document.createElement("div"); viewer.id = "imageViewer"; viewer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.9); backdrop-filter: blur(2px); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1036; overflow: hidden;`; viewContainer = document.createElement("div"); viewContainer.id = "viewContainer"; // 아이디 추가 imageContainer = document.createElement("div"); imageContainer.id = "imageContainer"; // 아이디 추가 // 왼쪽 크기 조절 막대 leftResizer = document.createElement("div"); leftResizer.className = "resizer left"; leftResizer.style.cssText = ` width: 15px; height: auto; cursor: ew-resize; background: rgba(255, 255, 255, 0.1);`; // 이미지 컨테이너 imageContainer = document.createElement("div"); imageContainer.id = "imageContainer"; // 오른쪽 크기 조절 막대 rightResizer = document.createElement("div"); rightResizer.className = "resizer right"; rightResizer.style.cssText = ` width: 15px; height: auto; cursor: ew-resize; background: rgba(255, 255, 255, 0.1);`; viewContainer.appendChild(leftResizer); viewContainer.appendChild(imageContainer); viewContainer.appendChild(rightResizer); viewer.appendChild(viewContainer); document.body.appendChild(viewer); createScrollbar(); createCounter(); createButtons(); // 이미지들을 미리 로드 (뷰어가 생성될 때) preloadImages(); dragScroll(); // 뷰어 전체에서 휠 이벤트 허용 viewContainer.addEventListener("wheel", wheelScroll); document.addEventListener("pointerdown", (event) => { if (debug) console.log(event.target.id + ': pointerdown'); eventTarget = event.target; }); viewContainer.addEventListener("pointerup", (event) => { if (eventTarget == viewContainer && event.target == viewContainer) { if (debug) console.log('viewContainer: pointerup'); closeViewer(); } }); imageContainer.addEventListener("click", (event) => { if (debug) console.log('imageContainer: click'); event.stopPropagation(); if (imageLayoutType !== 'single') return; if (currentIndex < images.length - 1) { currentIndex++; updateViewerImages(); } }); } function createButtons() { const buttonContainer = document.createElement("div"); buttonContainer.id = 'buttonContainer'; buttonContainer.style.cssText = ` position: absolute; bottom: 25px; right: 140px; display: flex; flex-direction: row; gap: 15px;`; // buttonContainer.addEventListener("pointerdown", (event) => { // event.stopPropagation(); // }); // 더블클릭 방지 (전체 화면 버튼에서) buttonContainer.addEventListener("dblclick", (event) => { if (debug) console.log('buttonContainer: dblclick'); event.preventDefault(); // 더블클릭 기본 동작 방지 }); // 전체 화면 버튼 const fullscreenButton = document.createElement("button"); fullscreenButton.id = 'fullscreenButton'; fullscreenButton.innerHTML = ` `; fullscreenButton.style.cssText = ` background: rgba(0, 0, 0, 0.5); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 50%; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 50px; height: 50px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); user-select: none; /* 텍스트 선택 방지 */ `; fullscreenButton.addEventListener("click", () => { if (debug) console.log('fullscreenButton: click'); event.stopPropagation(); if (isMobile){ closeViewer(); } else { if (!document.fullscreenElement) { viewer.requestFullscreen().catch((err) => { console.error(`[이미지 뷰어] 전체 화면 전환 실패: ${err.message}`); }); } else { document.exitFullscreen().catch((err) => { console.error(`[이미지 뷰어] 전체 화면 해제 실패: ${err.message}`); }); } } }); // 스크롤 속도 조정 버튼 const speedButton = document.createElement("button"); speedButton.id = 'speedButton'; speedButton.innerHTML = `${scrollSpeed}x`; speedButton.style.cssText = ` background: rgba(0, 0, 0, 0.5); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 50%; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 50px; height: 50px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); user-select: none; /* 텍스트 선택 방지 */ `; speedButton.addEventListener("click", () => { if (debug) console.log('speedButton: click'); const speeds = [1, 1.5, 2, 3, 5, 10]; const currentIndex = speeds.indexOf(scrollSpeed); scrollSpeed = speeds[(currentIndex + 1) % speeds.length]; // 다음 속도로 변경, 10x 이후 1x로 돌아감 speedButton.innerHTML = `${scrollSpeed}x`; // 버튼 텍스트 업데이트 }); // 레이아웃 토글 버튼 const toggleLayoutButton = document.createElement("button"); toggleLayoutButton.id = 'toggleLayoutButton'; toggleLayoutButton.innerHTML = imageLayoutType === 'single' ? '1' : '2'; // 버튼 텍스트 동적으로 설정 toggleLayoutButton.style.cssText = ` background: rgba(0, 0, 0, 0.5); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 50%; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 50px; height: 50px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); user-select: none; /* 텍스트 선택 방지 */ `; // IntersectionObserver를 활용하여 스크롤된 이미지의 인덱스 추적 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const index = images.indexOf(entry.target); if (index !== -1) { currentIndex = index; console.log("현재 보이는 이미지 인덱스:", currentIndex); } } }); }, { threshold: 0.5 }); // 이미지가 50% 이상 보일 때 트리거 images.forEach(image => { if (image instanceof Element) { // image가 DOM 요소인지 확인 observer.observe(image); } }); toggleLayoutButton.addEventListener("click", () => { if (debug) console.log('toggleLayoutButton: click'); if (imageLayoutType === 'single') { imageLayoutType = 'vertical'; } else { imageLayoutType = 'single'; } toggleLayoutButton.innerHTML = imageLayoutType === 'single' ? '1' : '2'; // 텍스트 업데이트 updateViewerImages(); }); // 🔹 이전 페이지 버튼 (기능은 비워둠) const prevPageButton = document.createElement("button"); prevPageButton.id = 'prevPageButton'; prevPageButton.innerHTML = "←"; // 좌측 화살표 아이콘 prevPageButton.style.cssText = fullscreenButton.style.cssText; prevPageButton.addEventListener("click", () => { if (debug) console.log('prevPageButton: click'); if (currentIndex > 0) { currentIndex--; updateViewerImages(); } }); if (isMobile) { buttonContainer.appendChild(prevPageButton); buttonContainer.appendChild(toggleLayoutButton); buttonContainer.appendChild(fullscreenButton); } else { buttonContainer.appendChild(toggleLayoutButton); buttonContainer.appendChild(speedButton); buttonContainer.appendChild(fullscreenButton); } viewer.appendChild(buttonContainer); } function createScrollbar() { if (scrollbar) return; const scrollbarContainer = document.createElement("div"); scrollbarContainer.id = 'scrollbarContainer'; scrollbarContainer.style.cssText = ` position: absolute; right: 10px; width: 50px; height: 80%; display: flex; justify-content: center; align-items: center; user-select: none; `; scrollbar = document.createElement("div"); scrollbar.id = 'scrollbar'; scrollbar.style.cssText = ` position: relative; width: 8px; height: 100%; background: rgba(255, 255, 255, 0.3); border-radius: 4px; overflow: hidden; pointer-events: auto; cursor: pointer; `; const scrollThumb = document.createElement("div"); scrollThumb.id = "scrollThumb"; scrollThumb.style.cssText = ` width: 100%; height: ${(1 / images.length) * 100}%; background: rgba(255, 255, 255, 0.8); border-radius: 4px; position: absolute; top: 0; cursor: grab; `; scrollbar.appendChild(scrollThumb); scrollbarContainer.appendChild(scrollbar); viewer.appendChild(scrollbarContainer); // scrollbarContainer.addEventListener("pointerdown", (event) => { // event.stopPropagation(); // }); // 더블클릭 방지 scrollbarContainer.addEventListener("dblclick", (event) => { event.preventDefault(); // 더블클릭 기본 동작 방지 }); // 스크롤바 클릭 이벤트 (thumb 이동 및 바로 드래그 시작) scrollbar.addEventListener("pointerdown", (event) => { const scrollbarRect = scrollbar.getBoundingClientRect(); const clickY = event.clientY - scrollbarRect.top; const thumbHeight = scrollThumb.offsetHeight; const maxTop = scrollbarRect.height - thumbHeight; // 클릭한 위치로 thumb 이동 const newThumbTop = Math.max( 0, Math.min(clickY - thumbHeight / 2, maxTop) ); // 스크롤 비율 계산 및 적용 const scrollFraction = newThumbTop / maxTop; currentIndex = Math.round(scrollFraction * (images.length - 1)); scrollThumb.style.top = `${newThumbTop}px`; // 이미지 및 스크롤 동기화 if (imageLayoutType === 'single') { updateViewerImages(); } else { imageContainer.scrollTop = scrollFraction * (imageContainer.scrollHeight - imageContainer.clientHeight); updateCounter(); } // 드래그 상태 활성화 dragThumb= true; startY = event.clientY; startTop = newThumbTop; scrollThumb.style.cursor = "grabbing"; document.body.style.userSelect = "none"; }); // Thumb 드래그 이벤트 scrollThumb.addEventListener("pointerdown", (event) => { if (debug) console.log('scrollThumb: pointerdown'); dragThumb = true; startY = event.clientY; startTop = parseFloat(scrollThumb.style.top || "0") / 100 * scrollbar.offsetHeight; scrollThumb.style.cursor = "grabbing"; document.body.style.userSelect = "none"; }); document.addEventListener("pointermove", (event) => { if (!dragThumb) return; if (debug) console.log('document: pointermove'); const deltaY = event.clientY - startY; const scrollbarRect = scrollbar.getBoundingClientRect(); const maxTop = scrollbarRect.height - scrollThumb.offsetHeight; let newTop = Math.max(0, Math.min(startTop + deltaY, maxTop)); const scrollFraction = newTop / maxTop; scrollThumb.style.top = `${newTop}px`; currentIndex = Math.round(scrollFraction * (images.length - 1)); if (imageLayoutType === 'single') { updateViewerImages(); } else { imageContainer.scrollTop = scrollFraction * (imageContainer.scrollHeight - imageContainer.clientHeight); requestAnimationFrame(() => { updateScrollbar updateCounter(); }); } }); document.addEventListener("pointerup", () => { if (!dragThumb) return; if (debug) console.log('document: pointerup'); dragThumb = false; scrollThumb.style.cursor = "grab"; document.body.style.userSelect = ""; }); } function updateScrollbar() { if (!scrollbar) return; const scrollThumb = scrollbar.querySelector("#scrollThumb"); if (imageLayoutType === 'single') { if (dragThumb) return; scrollThumb.style.height = `${(1 / images.length) * 100}%`; let newTop = (currentIndex / (images.length - 1)) * (100 - parseFloat(scrollThumb.style.height)); scrollThumb.style.top = `${newTop}%`; } else { const containerHeight = imageContainer.scrollHeight - imageContainer.clientHeight; const scrollTop = imageContainer.scrollTop; // Thumb 높이 및 위치 계산 scrollThumb.style.height = `${(imageContainer.clientHeight / imageContainer.scrollHeight) * 100}%`; if (containerHeight > 0) { let newTop = (scrollTop / containerHeight) * (100 - parseFloat(scrollThumb.style.height)); scrollThumb.style.top = `${newTop}%`; } else { scrollThumb.style.top = `0%`; } // Counter 동기화 const scrollFraction = scrollTop / containerHeight || 0; currentIndex = Math.round(scrollFraction * (images.length - 1)); } } function dragScroll() { let startScrollTop; // 초기 스크롤 위치 저장 // 이미지 컨테이너 드래그 이벤트 imageContainer.addEventListener("pointerdown", (event) => { if (debug) console.log('imageContainer: pointerdown'); if (imageLayoutType !== 'vertical') return; // vertical 모드에서만 동작 dragImage = true; startY = event.clientY; startScrollTop = imageContainer.scrollTop; document.body.style.userSelect = "none"; }); document.addEventListener("pointermove", (event) => { if (!dragImage) return; if (debug) console.log('document: pointermove'); // 스크롤 이동 계산 (속도 조정 추가) const deltaY = (startY - event.clientY) * scrollSpeed; imageContainer.scrollTop = startScrollTop + deltaY; // Thumb 및 Counter 동기화 requestAnimationFrame(() => { updateScrollbar(); updateCounter(); // Counter 업데이트 }); }); document.addEventListener("pointerup", () => { if (!dragImage) return; if (debug) console.log('document: pointerup'); dragImage = false; document.body.style.userSelect = ""; }); } function wheelScroll(event) { event.preventDefault(); // 불필요한 기본 스크롤 방지 if (imageLayoutType === 'single') { if (event.deltaY > 0 && currentIndex < images.length - 1) { currentIndex++; updateViewerImages(); } else if (event.deltaY < 0 && currentIndex > 0) { currentIndex--; updateViewerImages(); } // // currentIndex가 업데이트된 후에만 console.log를 한 번 호출하도록 처리 // if (event.deltaY !== 0 && (currentIndex > 0 && currentIndex < images.length - 1)) { // console.log(currentIndex); // } } else { const maxScroll = imageContainer.scrollHeight - imageContainer.clientHeight; // 스크롤 제한 조건 추가 (맨 위 또는 맨 아래 도달 시 업데이트 방지) if ((event.deltaY < 0 && imageContainer.scrollTop <= 0) || (event.deltaY > 0 && imageContainer.scrollTop >= maxScroll)) { return; } imageContainer.scrollTop += event.deltaY * scrollSpeed; requestAnimationFrame(updateScrollbar); updateCounter(); // 현재 인덱스 계산 (스크롤 비율 기반) const scrollFraction = imageContainer.scrollTop / maxScroll; currentIndex = Math.round(scrollFraction * (images.length - 1)); // console.log(currentIndex); } } function createCounter() { if (counter) return; counter = document.createElement("div"); counter.id = 'counter'; counter.style.cssText = ` position: absolute; bottom: 35px; right: 55px; color: white; font-size: 14px; background: rgba(0, 0, 0, 0.5); padding: 5px 10px; border-radius: 4px; user-select: none;`; viewer.appendChild(counter); } function updateCounter() { if (!counter) return; if (imageLayoutType === 'single') { // 현재 이미지 인덱스 + 1 / 전체 이미지 개수 counter.textContent = `${currentIndex + 1} / ${images.length}`; } else { // 세로 모드에서는 전체 높이에서 현재 스크롤 비율(%) 표시 const scrollFraction = imageContainer.scrollTop / (imageContainer.scrollHeight - imageContainer.clientHeight); const scrollPercent = Math.round(scrollFraction * 100); counter.textContent = `${scrollPercent}%`; } } function showViewer() { if (!viewer) { createViewer(); addResizerFunctionality(); } updateViewerImages(); viewer.style.display = "flex"; document.body.style.overflow = "hidden"; } function closeViewer() { if (viewer) { viewer.style.display = "none"; } document.body.style.overflow = ""; // 전체화면 상태인지 확인하고 종료 if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) { document.exitFullscreen(); } // 현재 보고 있던 이미지의 src 찾기 const targetSrc = images[currentIndex]; if (targetSrc) { // 게시글 내 이미지 중 src가 일치하는 요소 찾기 const targetImage = Array.from(document.querySelectorAll(".fr-view.article-content img")) .find(img => img.src === targetSrc); if (targetImage) { targetImage.scrollIntoView({ behavior: "smooth", block: "center" }); } } } function updateViewerImages() { imageContainer.innerHTML = ""; // console.log(currentIndex); viewContainer.style.width = "100%"; viewContainer.style.display = "flex"; viewContainer.style.flexDirection = "row"; viewContainer.style.justifyContent = "center"; viewContainer.style.overflow = "auto"; viewContainer.style.userSelect = "none"; viewContainer.style.position = "relative"; imageContainer.style.height = "100%"; imageContainer.style.display = "flex"; imageContainer.style.flexDirection = "column"; imageContainer.style.overflow = "hidden"; imageContainer.style.userSelect = "none"; imageContainer.style.position = "relative"; if (imageLayoutType === 'single') { leftResizer.style.display = "none"; rightResizer.style.display = "none"; const img = document.createElement("img"); img.src = images[currentIndex]; img.onload = function () { if (isMobile) { viewContainer.style.alignItems = "center"; imageContainer.style.height = "fit-content"; imageContainer.style.touchAction = "auto"; img.style.cssText = "width: 100%; height: auto; pointer-events: none;"; } else { img.style.cssText = "width: auto; height: 100%; pointer-events: none;"; } viewContainer.style.height = "100%"; imageContainer.style.width = "fit-content"; imageContainer.style.cursor = "pointer"; requestAnimationFrame(updateScrollbar); updateCounter(); }; imageContainer.appendChild(img); } else { imageContainer.style.display = "none"; const promises = images.map((src, index) => { return new Promise((resolve) => { const img = document.createElement("img"); img.src = src; img.style.cssText = "width: 100%; height: auto; pointer-events: none;"; img.onload = () => resolve(img); imageContainer.appendChild(img); }); }); Promise.all(promises).then((imgs) => { imageContainer.style.display = "flex"; if (isMobile) { leftResizer.style.display = "none"; rightResizer.style.display = "none"; } else { leftResizer.style.display = "block"; rightResizer.style.display = "block"; viewContainer.style.height = "auto"; imageContainer.style.width = imageContainerWidth; } imageContainer.style.cursor = "grab"; const targetImage = imgs[currentIndex]; // currentIndex에 해당하는 이미지 const targetScrollTop = targetImage.offsetTop; // 이미지의 상단 위치 (스크롤 위치) imageContainer.scrollTop = targetScrollTop; // 해당 위치로 스크롤 이동 requestAnimationFrame(updateScrollbar); updateCounter(); }); } } function addResizerFunctionality() { let isResizing = false; let startX; let startWidth; let isLeftResizer = false; let containerParentWidth; function startResize(event, resizer) { isResizing = true; startX = event.clientX; startWidth = imageContainer.offsetWidth; containerParentWidth = imageContainer.parentElement.offsetWidth; isLeftResizer = resizer.classList.contains("left"); document.addEventListener("pointermove", resize); document.addEventListener("pointerup", stopResize); } function resize(event) { if (!isResizing) return; let deltaX = event.clientX - startX; let newWidth = startWidth; if (isLeftResizer) { newWidth = startWidth - deltaX; // 왼쪽 리사이저: 오른쪽으로 드래그하면 너비 감소 } else { newWidth = startWidth + deltaX; // 오른쪽 리사이저: 왼쪽으로 드래그하면 너비 감소 } newWidth = Math.max(100, Math.min(containerParentWidth - 100, newWidth)); // 최소, 최대 크기 제한 let newWidthPercent = (newWidth / containerParentWidth) * 100; // 퍼센트 변환 imageContainer.style.width = newWidthPercent + "%"; imageContainerWidth = imageContainer.style.width; // console.log(imageContainer); } function stopResize() { isResizing = false; updateCounter(); updateScrollbar(); document.removeEventListener("pointermove", resize); document.removeEventListener("pointerup", stopResize); } leftResizer.addEventListener("pointerdown", (event) => startResize(event, leftResizer)); rightResizer.addEventListener("pointerdown", (event) => startResize(event, rightResizer)); } getImages(); // 게시글에서 이미지 목록을 추출 } } handleSettings(); if (window.location.href.includes('scrap_list')) { if (config.scrapList){ // arcaLiveScrapList(); setTimeout(arcaLiveScrapList, 0); } } // arcaLive(); setTimeout(arcaLive, 0); })();