// ==UserScript== // @name 아카라이브 미리보기 이미지, 모두 열기 // @version 1.13 // @icon https://www.google.com/s2/favicons?sz=64&domain=arca.live // @description 아카라이브 미리보기 이미지 생성, 모두 열기 생성, 그 외 잡다한 기능.. // @author ChatGPT // @match https://arca.live/b/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @namespace Violentmonkey Scripts // @downloadURL none // ==/UserScript== // 설정 변수 false로 비활성화 var config = { openAllButton: true, // 모두 열기 버튼 생성 thumbnail: true, // 미리보기 이미지 생성 thumbWidth: 100, // 미리보기 이미지 너비 thumbHeight: 62, // 미리보기 이미지 높이 thumbnailHover: true, // 미리보기 이미지 마우스 오버시 보이게 thumbnailBlur: true, // 블러 효과를 적용할지 여부 blurAmount: 2, // 블러 효과의 정도 originalThumbnail: true, // 개념글 미리보기 이미지 클릭 시 원본 이미지 불러오기 thumbnailHoverBest: true, // 개념글 미리보기 이미지 마우스 오버시 보이게 closeButton: false, // 하단 우측 창닫기 버튼 생성 bookmarkButton: false, // 하단 우측 스크랩 버튼 생성 test01: false, // 채널 기본 이미지로 된 미리보기 이미지 마우스 오버시 해당 게시글 다른 이미지 가져오기 test02: false // 채널 기본 이미지로 된 미리보기 이미지를 해당 게시글 다른 이미지로 대체 }; // 설정 변수에 대한 설명 var descriptions = { openAllButton: '모두 열기 버튼 생성', thumbnail: '미리보기 이미지 생성', thumbWidth: '미리보기 이미지 너비', thumbHeight: '미리보기 이미지 높이', thumbnailHover: '미리보기 이미지 마우스 오버시 보이게', thumbnailBlur: '블러 효과를 적용할지 여부', blurAmount: '블러 효과의 정도', originalThumbnail: '개념글 미리보기 이미지 클릭 시 원본 이미지 불러오기', thumbnailHoverBest: '개념글 미리보기 이미지 마우스 오버시 보이게', closeButton: '하단 우측 창닫기 버튼 생성', bookmarkButton: '하단 우측 스크랩 버튼 생성', test01: '채널 기본 이미지로 된 미리보기 이미지 마우스 오버시 해당 게시글 다른 이미지 가져오기', test02: '채널 기본 이미지로 된 미리보기 이미지를 해당 게시글 다른 이미지로 대체' }; // 설정 창을 생성하는 함수 function createSettingsWindow() { // 기존 설정 창이 있으면 제거 var existingSettingsWindow = document.getElementById('settingsWindow'); if (existingSettingsWindow) { existingSettingsWindow.remove(); } // 설정 창 요소 생성 var settingsWindow = document.createElement('div'); settingsWindow.id = 'settingsWindow'; settingsWindow.style.position = 'fixed'; settingsWindow.style.top = '50%'; settingsWindow.style.left = '50%'; settingsWindow.style.transform = 'translate(-50%, -50%)'; settingsWindow.style.width = '200px'; // 너비 지정 settingsWindow.style.padding = '20px'; settingsWindow.style.background = '#ffffff'; settingsWindow.style.border = '1px solid #cccccc'; settingsWindow.style.borderRadius = '10px'; // 테두리 둥글기 설정 settingsWindow.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.3)'; settingsWindow.style.zIndex = '9999'; settingsWindow.style.textAlign = 'left'; settingsWindow.style.display = 'flex'; // flexbox 사용 settingsWindow.style.flexDirection = 'column'; // 세로 방향으로 정렬 settingsWindow.style.alignItems = 'center'; // 수직 가운데 정렬 // 설정 창 제목 추가 var settingsTitle = document.createElement('div'); settingsTitle.innerHTML = 'Settings'; // 설정 창 제목 settingsTitle.style.fontSize = '18px'; // 제목 폰트 크기 settingsTitle.style.fontWeight = 'bold'; // 제목 폰트 굵기 settingsTitle.style.marginBottom = '10px'; // 하단 간격 지정 settingsWindow.appendChild(settingsTitle); // 설정 변수를 반복하여 설정 입력 요소를 생성 for (var key in config) { if (config.hasOwnProperty(key)) { // 변수를 담을 div 요소 생성 var configDiv = document.createElement('div'); configDiv.style.marginBottom = '5px'; // 하단 간격 지정 configDiv.style.display = 'flex'; // flexbox 사용 configDiv.style.alignItems = 'center'; // 수직 가운데 정렬 var label = document.createElement('label'); label.innerHTML = key + ': '; label.style.marginRight = '5px'; // 오른쪽 마진 지정 label.style.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]; // 체크박스의 경우 checked 속성 사용 if (input.type === 'text') { input.style.width = '40px'; // 입력 창의 너비를 40px로 설정 input.style.height = '20px'; // 입력 창의 높이를 15px로 설정 input.style.padding = '0 5px'; // 입력 창의 좌우 패딩을 5px로 설정 } input.addEventListener('input', (function(key) { return function(event) { if (key === 'blurAmount') { event.target.value = event.target.value.replace(/\D/g, ''); // 숫자만 입력되도록 } config[key] = event.target.type === 'checkbox' ? event.target.checked : event.target.value; }; })(key)); // div에 레이블과 인풋 추가 configDiv.appendChild(label); configDiv.appendChild(input); // 설정 창에 div 추가 settingsWindow.appendChild(configDiv); } } // 확인과 취소 버튼을 담을 div 요소 생성 var buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; // flexbox 사용 buttonContainer.style.marginTop = '10px'; // 상단 간격 지정 // 확인 버튼 추가 var confirmButton = document.createElement('button'); confirmButton.innerHTML = '확인'; confirmButton.style.marginRight = '15px'; // 우측 마진 지정 confirmButton.style.border = '1px solid #cccccc'; confirmButton.style.borderRadius = '5px'; confirmButton.addEventListener('click', function() { // 설정을 저장하고 설정 창을 닫습니다. saveConfig(); settingsWindow.remove(); // 설정 창 제거 location.reload(); // 페이지 새로고침 }); confirmButton.addEventListener('mouseover', function() { confirmButton.style.background = '#007bff'; // 마우스를 올렸을 때 배경색 변경 confirmButton.style.color = '#ffffff'; // 글자색을 하얀색으로 변경 }); confirmButton.addEventListener('mouseout', function() { confirmButton.style.background = ''; // 마우스를 내렸을 때 배경색을 회색으로 복원 confirmButton.style.color = '#000000'; // 글자색을 검정색으로 변경 }); buttonContainer.appendChild(confirmButton); // 취소 버튼 추가 var cancelButton = document.createElement('button'); cancelButton.innerHTML = '취소'; cancelButton.style.border = '1px solid #cccccc'; cancelButton.style.borderRadius = '5px'; cancelButton.addEventListener('click', function() { // 설정 창만 닫습니다. settingsWindow.remove(); // 설정 창 제거 }); cancelButton.addEventListener('mouseover', function() { cancelButton.style.background = '#ff0000'; // 마우스를 올렸을 때 배경색 변경 cancelButton.style.color = '#ffffff'; // 글자색을 하얀색으로 변경 }); cancelButton.addEventListener('mouseout', function() { cancelButton.style.background = ''; // 마우스를 내렸을 때 배경색을 회색으로 복원 cancelButton.style.color = '#000000'; // 글자색을 검정색으로 변경 }); buttonContainer.appendChild(cancelButton); // 버튼 컨테이너를 설정 창에 추가 settingsWindow.appendChild(buttonContainer); // 설정 창을 body에 추가 document.body.appendChild(settingsWindow); } // 설정 값을 로컬 저장소에 저장 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]); } } } // 저장된 설정 값을 로드 loadConfig(); // 설정 버튼 생성 var settingsButtonCommand = GM_registerMenuCommand('설정', function() { createSettingsWindow(); }); // 모두 열기 버튼 생성 if (config.openAllButton) { var openAllButton = document.createElement('a'); openAllButton.className = 'btn btn-sm btn-primary float-left'; openAllButton.href = '#'; openAllButton.innerHTML = ' 모두 열기 '; openAllButton.addEventListener('click', function(event) { event.preventDefault(); document.querySelectorAll('a.vrow.column:not(.notice)').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); } function checkBlackEdge(image, callback) { var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0, img.width, img.height); var edgeSize = Math.min(img.width, img.height) * 0.1; var imageData = ctx.getImageData(0, 0, img.width, img.height); var totalPixels = 0; var blackPixels = 0; for (var x = 0; x < img.width; x++) { for (var y = 0; y < img.height; y++) { if (x < edgeSize || x >= img.width - edgeSize || y < edgeSize || y >= img.height - edgeSize) { totalPixels++; var index = (y * img.width + x) * 4; var pixelData = [ imageData.data[index], // Red imageData.data[index + 1], // Green imageData.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); } // console.log(blackPercentage); }; img.onerror = function() { // 이미지 로드 실패 시에도 콜백 호출하여 처리 callback(false); }; img.src = image.src + "&type=list"; // img.src = image.src + "&type=list"; } function setSecondImg(element, img , type) { var href = element.href; // GM_xmlhttpRequest를 사용하여 링크의 페이지 내용을 가져옵니다. GM_xmlhttpRequest({ method: "GET", url: href, onload: function(response) { // 가져온 페이지의 내용을 HTML 요소로 변환합니다. var parser = new DOMParser(); var htmlDoc = parser.parseFromString(response.responseText, "text/html"); // "fr-view article-content" 클래스를 가진 div 요소를 찾습니다. var contentDiv = htmlDoc.querySelector('div.fr-view.article-content'); // console.log(contentDiv); // 모든 p 태그를 가져옵니다. 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; } } 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; } } else { // console.log("???"); } if(config.test02){ img.src = changeImgUrl; } // console.log(firstTag.tagName); element.querySelector('.vrow-preview img').src = changeImgUrl; } }); } document.addEventListener('DOMContentLoaded', function() { if (config.thumbnail) { document.querySelectorAll('a.vrow.column:not(.notice)').forEach(function(element) { var vcolId = element.querySelector('.vrow-top .vcol.col-id'); var vcolTitle = element.querySelector('.vrow-top .vcol.col-title'); vcolId.style.margin = '0'; var vcolThumb = document.createElement('span'); vcolThumb.className = 'vcol col-thumb'; vcolThumb.style.width = config.thumbWidth + 'px'; vcolThumb.style.height = config.thumbHeight + 'px'; vcolThumb.style.borderRadius = '3px'; element.querySelector('.vrow-inner').appendChild(vcolThumb); vcolTitle.parentNode.insertBefore(vcolThumb, vcolTitle); var vrowPreview = element.querySelector('.vrow-preview'); function createThumbnail() { var vrowPreviewImg = vrowPreview ? vrowPreview.querySelector('img') : null; if (!vrowPreviewImg) return; element.style.height = 'auto'; element.style.paddingTop = '3.75px'; element.style.paddingBottom = '3.75px'; var thumbImage = document.createElement('img'); thumbImage.src = vrowPreviewImg.src; thumbImage.style.width = '100%'; thumbImage.style.height = '100%'; thumbImage.style.objectFit = 'cover'; // console.log(thumbImage); if (config.test01 || config.test02){ checkBlackEdge(thumbImage, function(hasBlackEdge) { if (hasBlackEdge){ setSecondImg(element, thumbImage, true); } }); } if (config.thumbnailBlur) { thumbImage.style.filter = 'blur(' + config.blurAmount + 'px)'; thumbImage.addEventListener('mouseenter', function() { thumbImage.style.filter = 'none'; }); thumbImage.addEventListener('mouseleave', function() { thumbImage.style.filter = 'blur(' + config.blurAmount + 'px)'; }); } vcolThumb.appendChild(thumbImage); vrowPreview.style.display = 'none'; vrowPreview.style.width = '30rem'; vrowPreview.style.height = 'auto'; vrowPreview.style.top = 'auto'; vrowPreview.style.left = '13.5rem'; var thumbImageValue = false; thumbImage.addEventListener('mouseenter', function() { if (thumbImageValue == false) { vrowPreviewImg.src = vrowPreviewImg.src.replace("&type=list", ''); thumbImageValue = true; } vrowPreview.style.display = null; }); thumbImage.addEventListener('mouseleave', function() { vrowPreview.style.display = 'none'; }); } function tryCreateThumbnail(retryCount) { if (retryCount >= 3) return; setTimeout(function() { if (retryCount === 0) createThumbnail(); tryCreateThumbnail(retryCount + 1); }, 100); } tryCreateThumbnail(0); }); } }); // 썸네일 클릭 시 원본 이미지 불러오기 if (config.originalThumbnail) { 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.thumbnailHoverBest) { // 이미지 요소 선택 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(); }); }); }); } document.addEventListener('DOMContentLoaded', function() { if (config.closeButton || config.bookmarkButton) { 'use strict'; var navControl = document.querySelector('.nav-control'); var originalScrapButton = document.querySelector('.scrap-btn'); // Function to create a new list item element function createNewItem(iconClass, clickHandler, hoverHandler) { var newItem = document.createElement('li'); newItem.innerHTML = ''; newItem.addEventListener('click', clickHandler); if (hoverHandler) { newItem.addEventListener('mouseenter', hoverHandler); newItem.addEventListener('mouseleave', function() { newItem.style.backgroundColor = ''; // Reset background color on mouse leave }); } return newItem; } // Function to append or insert a new item into the navigation control list function appendOrUpdateItem(newItem) { if (navControl) { if (navControl.children.length > 0) { navControl.insertBefore(newItem, navControl.firstElementChild); } else { navControl.appendChild(newItem); } } else { console.error('Navigation control list not found.'); } } // Close button click handler function closeButtonClickHandler() { window.close(); } // Close button hover handler function closeButtonHoverHandler() { this.style.backgroundColor = 'red'; } // Bookmark button click handler function bookmarkButtonClickHandler() { if (originalScrapButton) { originalScrapButton.click(); } else { console.error('Original scrap button not found.'); } } // Create and append/close buttons if (config.closeButton) { var closeButton = createNewItem('ion-close-round', closeButtonClickHandler, closeButtonHoverHandler); appendOrUpdateItem(closeButton); } if (config.bookmarkButton) { if (originalScrapButton) { var bookmarkButton = createNewItem('ion-android-bookmark', bookmarkButtonClickHandler); appendOrUpdateItem(bookmarkButton); // Update bookmark button color function updateButtonColor() { var buttonText = originalScrapButton.querySelector('.result').textContent.trim(); bookmarkButton.style.backgroundColor = (buttonText === "스크랩 됨") ? '#007bff' : ''; } // Call updateButtonColor initially and set up mutation observer updateButtonColor(); var observer = new MutationObserver(updateButtonColor); observer.observe(originalScrapButton.querySelector('.result'), { childList: true, subtree: true }); } } } });