// ==UserScript== // @author jvlflame // @name arca.live enhancements // @version 0.0.5 // @license MIT // @include https://arca.live/* // @include https://kioskloud.io/e/* // @include https://nahida.live/mods/* // @description Adds quality of life improvements for browinsg and downloading Genshin Impact mods from arca.live // @namespace https://greasyfork.org/users/63118 // @downloadURL none // ==/UserScript== let GLOBAL_STYLES = ` .vrow.column.head { height: initial !important; } .vrow.column { height: 135px !important; } .vrow.column.visited { opacity: 0.4; } .vrow-inner { padding-left: 115px; } .vrow-preview { display: block !important; top: 10px !important; } .notice.column { height: 2.4rem !important; } .body .board-article .article-list .list-table a.vrow:visited { color: inherit !important; background-color: inherit !important; } .enh-action-bar { display: flex; padding: 0.5rem 0; gap: 0.5rem; } `; const CURRENT_URL = window.location.href; const CURRENT_PAGE = window.location.pathname; const IS_KIOSKLOUDIO = CURRENT_URL.includes('kioskloud.io'); const IS_ARCALIVE = CURRENT_URL.includes('arca.live'); const IS_NAHIDALIVE = CURRENT_URL.includes('nahida.live'); const IS_POST_PAGE = CURRENT_PAGE.match(/\/b\/.*\/\d+/g); const IS_LIST_PAGE = !IS_POST_PAGE; const CATEGORY = CURRENT_PAGE.match(/\/b\/(\w+)/g); const BASE64_REGEX = /^[-A-Za-z0-9+\/]*={0,3}$/g; const VISITED_POSTS = getPostsFromLocalStorage(CATEGORY); const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = GLOBAL_STYLES; document.head.appendChild(styleSheet); function getPostRows() { const table = document.getElementsByClassName("list-table table"); // Get the list table row elements const rows = table[0].querySelectorAll("a.vrow.column"); const postRows = []; for (const row of rows) { if (row.classList.contains("notice")) { continue; }; postRows.push(row); } return postRows; } function getPost(id, title, unixTimestamp) { const dateTimeString = unixTimestamp ? new Date(unixTimestamp * 1000).toISOString() : new Date().toISOString(); const post = { t: title, v: dateTimeString } return { id: id, post: post }; } function getPostsFromLocalStorage(category) { const posts = JSON.parse(localStorage.getItem(`visited-posts-${category}`)); return posts ? posts : {}; } function isPostVisited(visitedPosts, id) { return visitedPosts[id]; } function appendPostToLocalStorage(post, category) { const existingPosts = getPostsFromLocalStorage(category); existingPosts[post.id] = post.post; localStorage.setItem(`visited-posts-${category}`, JSON.stringify(existingPosts)); } function appendPostsToLocalStorage(posts, category) { const existingPosts = getPostsFromLocalStorage(category); for (const post of posts) { existingPosts[post.id] = post.post; } localStorage.setItem(`visited-posts-${category}`, JSON.stringify(existingPosts)); } function removePostFromLocalStorage(post, category) { const existingPosts = getPostsFromLocalStorage(category); delete existingPosts[post.id] localStorage.setItem(`visited-posts-${category}`, JSON.stringify(existingPosts)); } function removePostsFromLocalStorage(posts, category) { const existingPosts = getPostsFromLocalStorage(category); for (const post of posts) { delete existingPosts[post.id] } localStorage.setItem(`visited-posts-${category}`, JSON.stringify(existingPosts)); } function migrateRecentArticles() { if (!IS_ARCALIVE) { return; } /* recent_articles { boardName: string; slug: string; articleId: number; title: string; regdateAt: number }[] */ const isMigrated = localStorage.getItem('migrated-timestamp'); if (isMigrated) { return; } const nativeVisitedPosts = localStorage.getItem('recent_articles'); if (!nativeVisitedPosts) { return; } for (const visitedPost of JSON.parse(nativeVisitedPosts)) { const post = getPost(visitedPost.articleId, visitedPost.title, visitedPost.regdateAt); const category = `/b/${visitedPost.slug}`; appendPostToLocalStorage(post, category); } localStorage.setItem('migrated-timestamp', new Date().toISOString()); } function getAllPostsOnPage() { const rows = getPostRows(); const posts = []; for (const row of rows) { const href = row.href; const id = href.split(/\/b\/\w+\//)[1].split(/\?/)[0]; const titleElement = row.querySelectorAll(".title")[0]; const title = titleElement ? titleElement.outerText : ''; posts.push(getPost(id, title)); } return posts; } function handleMarkPageAsRead() { const posts = getAllPostsOnPage(); appendPostsToLocalStorage(posts, CATEGORY); } function handleMarkPageAsUnread() { const posts = getAllPostsOnPage(); removePostsFromLocalStorage(posts, CATEGORY); } function createBtnElement(text, handler) { const btnElement = document.createElement('button'); btnElement.classList.add("btn", "btn-sm", "btn-arca"); btnElement.textContent = text; btnElement.onclick = handler; return btnElement; } function createActionBarElement() { const actionBarElement = document.createElement('div'); actionBarElement.classList.add('enh-action-bar'); return actionBarElement; } function createMarkPageAsReadBtn(parentElement) { const btnElement = createBtnElement('Mark page as read', handleMarkPageAsRead) parentElement.prepend(btnElement); } function createMarkPageAsUnreadBtn(parentElement) { const btnElement = createBtnElement('Mark page as unread', handleMarkPageAsUnread) parentElement.prepend(btnElement); } function createActionBar() { const topActionBarElement = createActionBarElement(); const bottomActionBarElement = createActionBarElement(); const listElement = document.querySelector('.article-list'); listElement.prepend(topActionBarElement); listElement.appendChild(bottomActionBarElement); for (const parentElement of [topActionBarElement, bottomActionBarElement]) { createMarkPageAsUnreadBtn(parentElement); createMarkPageAsReadBtn(parentElement); } } function addArcaRowEnhancements() { const rows = getPostRows(); for (const row of rows) { const previewElement = row.querySelectorAll('.vrow-preview'); const hasPreview = Boolean(previewElement[0]) if (!hasPreview) { const dummyPreviewElement = document.createElement('div'); dummyPreviewElement.classList.add('vrow-preview'); row.appendChild(dummyPreviewElement); } const href = row.href; // Remove the query string so it's easier to copy article id when archiving const hrefWithoutQuery = href.split('?')[0]; row.setAttribute("href", hrefWithoutQuery); const id = href.split(/\/b\/\w+\//)[1].split(/\?/)[0]; const titleElement = row.querySelectorAll(".title")[0]; const title = titleElement ? titleElement.outerText : ''; const isVisited = isPostVisited(VISITED_POSTS, id); if (isVisited) { row.classList.add("visited"); } else { row.addEventListener("click", (e) => { if (e.button === 2) return; const post = getPost(id, title); appendPostToLocalStorage(post, CATEGORY); }); row.addEventListener("auxclick", () => { const post = getPost(id, title); appendPostToLocalStorage(post, CATEGORY); }); } } } if (IS_ARCALIVE && IS_LIST_PAGE) { createActionBar(); addArcaRowEnhancements(); } if (IS_ARCALIVE && IS_POST_PAGE) { addArcaRowEnhancements(); const title = document.querySelectorAll(".title-row .title")[0].outerText; const id = CURRENT_PAGE.split(/\/b\/\w+\//)[1].split(/\?/)[0]; const post = getPost(id, title); // Attempt to automatically decode base64 links inside the article content const articleContentElement = document.querySelector(".fr-view.article-content"); const textBlocks = articleContentElement.querySelectorAll("p"); for (const text of textBlocks) { const innerText = text.innerText; const isBase64 = innerText.match(BASE64_REGEX); if (isBase64) { const decoded = atob(innerText); const linkElement = document.createElement('a'); const brElement = document.createElement('br'); linkElement.setAttribute('href', decoded); linkElement.textContent += decoded; text.appendChild(brElement); text.appendChild(linkElement); } } if (isPostVisited(VISITED_POSTS, id)) { return; }; appendPostToLocalStorage(post, CATEGORY); } if (IS_NAHIDALIVE) { const DEFAULT_PASSWORD = localStorage.getItem('default-password'); const passwordInputElement = document.querySelector('input[placeholder="Password"]'); const defaultPasswordInputElement = document.createElement('input'); defaultPasswordInputElement.type = 'text'; defaultPasswordInputElement.placeholder = 'Enter default password'; defaultPasswordInputElement.classList.add('flex', 'h-10', 'w-full', 'rounded-md', 'border', 'border-input', 'bg-background', 'ring-offset-background', 'file:border-0', 'file:bg-transparent', 'file:text-sm', 'file:font-medium', 'placeholder:text-muted-foreground', 'focus-visible:outline-none', 'focus-visible:ring-2', 'focus-visible:ring-ring', 'focus-visible:ring-offset-2', 'disabled:cursor-not-allowed', 'disabled:opacity-50', 'px-2', 'py-1', 'mb-2', 'max-w-[210px]', 'text-base'); defaultPasswordInputElement.style.position = 'absolute'; defaultPasswordInputElement.style.height = '50px'; defaultPasswordInputElement.style.top = '0.375rem'; defaultPasswordInputElement.style.left = '6.5rem'; defaultPasswordInputElement.style.zIndex = '500'; defaultPasswordInputElement.value = DEFAULT_PASSWORD; const bodyElement = document.querySelector('body'); bodyElement.appendChild(defaultPasswordInputElement); defaultPasswordInputElement.addEventListener("input", (e) => { localStorage.setItem('default-password', e.currentTarget.value || ''); }) if (DEFAULT_PASSWORD) { passwordInputElement.value = DEFAULT_PASSWORD; const submitBtnElement = document.querySelector('section').querySelector('button') submitBtnElement.click(); } } if (IS_KIOSKLOUDIO) { const DEFAULT_PASSWORD = localStorage.getItem('default-password'); const passwordInputElement = document.querySelector('.swal2-input'); const autoSubmitToggleElement = document.createElement('button'); const defaultPasswordInputElement = document.createElement('input'); defaultPasswordInputElement.type = 'text'; defaultPasswordInputElement.placeholder = 'Enter default password'; defaultPasswordInputElement.classList.add('swal2-input'); defaultPasswordInputElement.value = DEFAULT_PASSWORD; passwordInputElement.after(defaultPasswordInputElement); defaultPasswordInputElement.addEventListener("input", (e) => { localStorage.setItem('default-password', e.currentTarget.value || ''); }) if (DEFAULT_PASSWORD) { passwordInputElement.value = DEFAULT_PASSWORD; const submitBtnElement = document.querySelector('.swal2-actions').querySelector('.swal2-confirm'); submitBtnElement.click(); } } migrateRecentArticles();