diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index 8074f7ee..1c39ccce 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -420,32 +420,66 @@ var time2 = null var time1 = null function gotoNext() { clearTimeout(time1) - try{ - console.log("targetUrl :: " + targetUrl); - if (document.querySelector('[class="btn-group"]')) { - time2 = setTimeout(function () { - clearTimeout(time2) - var targetElement = null - document.querySelector('[class="btn-group"]').querySelectorAll('a').forEach(function(e){ - if(e.hasAttribute("href") && - ( - (e.getAttribute("href").search("page=2") > -1 && location.href.search("page") < 0) || - (e.getAttribute("href").search("page=3") > -1 && location.href.search("page=2") > 0) || - (e.getAttribute("href").search("page=4") > -1 && location.href.search("page=3") > 0) - )) { - targetElement = e + try { + let attempts = 0; // 시도 횟수를 기록할 변수 + const maxAttempts = 8; // 최대 15초 동안 시도 + let currentPage = 1; // 기본 페이지 - } - }) - if(targetElement !== null) { - targetElement.click() - } else { -// location.href = "https://naver.com" + // 1초마다 실행될 로직을 담는 변수 + const findAndClickInterval = setInterval(function () { + attempts++; // 시도 횟수 증가 + + // 현재 페이지 번호 가져오기 (이 로직은 한 번만 실행해도 되지만, 페이지가 동적으로 바뀔 경우를 대비해 내부에 둡니다) + var pageMatch = location.href.match(/page=(\d+)/); + if (pageMatch && pageMatch[1]) { + currentPage = parseInt(pageMatch[1], 10); + toast(`[${0}초] ⏳ 다음 페이지 링크를 찾는 중... (현재: page=${currentPage})`) + } + + var nextPage = currentPage + 1; + var targetElement = null; + + document.querySelectorAll('.btn-group')?.forEach((el) => { + var pageLinks =el?.querySelectorAll('a'); + + if (pageLinks) { + pageLinks.forEach(function(link) { + toast(`[${attempts}초] ✅ 다음 페이지 링크를 찾는 중 ${link.getAttribute('href')}`); + if (link.getAttribute('href')?.includes(`page=${nextPage}`)) { + targetElement = link; + toast(`[${attempts}초] ✅ 다음 페이지 링크를 찾았습니다.`); + } + }); } - }, 15000); - } - } catch (e) { + }) + + + // 1. 다음 페이지 링크를 찾았을 경우 + if (attempts > 3 && targetElement) { + console.log(`[${attempts}초] ✅ 다음 페이지 링크를 찾았습니다. 클릭합니다.`); + toast(`[${attempts}초] ✅ 다음 페이지 링크를 찾았습니다. 클릭합니다.`) + clearInterval(findAndClickInterval); // 반복을 중단 + targetElement.click(); // 링크 클릭 + } + // 2. 시간(15초)이 초과되었을 경우 + else if (attempts >= maxAttempts) { + console.log(`[${attempts}초] ❌ 시간 초과. 다음 페이지 링크를 찾지 못했습니다.`); + toast(`[${attempts}초] ❌ 시간 초과. 다음 페이지 링크를 찾지 못했습니다.`) + clearInterval(findAndClickInterval); // 반복을 중단 + // 필요하다면 이곳에 다른 페이지로 이동하는 로직을 추가하세요. + // location.href = "https://naver.com"; + } + // 3. 아직 링크를 찾지 못했고, 시간도 남았을 경우 + else { + toast(`[${attempts}초] ⏳ 다음 페이지 링크를 찾는 중... (대상: page=${nextPage})`) + console.log(`[${attempts}초] ⏳ 다음 페이지 링크를 찾는 중... (대상: page=${nextPage})`); + } + + }, 1500); // 1000ms = 1초마다 함수 실행 + + } catch (e) { + console.error("스크립트 실행 중 오류 발생:", e); } } @@ -663,32 +697,83 @@ async function handleCommon() { } ); - console.log(`Found TEST`); - const imageSelector = 'img[class*="mw-100"][src*="images"]'; - const images = Array.from(document.querySelectorAll(imageSelector)); - console.log(`Found ${images}`); - if (images.length > 0) { - const validImageUrls = images.map(img => img.src) - .filter(src => src && src.startsWith('http')); - const uniqueUrls = [...new Set(validImageUrls)]; + let scrollInterval; - console.log(`Found ${'$'}{uniqueUrls.length} unique images to cache.`); + async function getImg() { + console.log(`Found TEST`); + const imageSelector = 'img[class*="mw-100"][src*="images"]'; + const images = Array.from(document.querySelectorAll(imageSelector)); + console.log(`Found ${images}`); + if (images.length > 0) { + const validImageUrls = images.map(img => img.src) + .filter(src => src && src.startsWith('http')); + const uniqueUrls = [...new Set(validImageUrls)]; + + console.log(`Found ${'$'}{uniqueUrls.length} unique images to cache.`); + + + for (const image of uniqueUrls) { - // 3. 각 URL을 순회하며 Base64로 변환하고 즉시 네이티브로 전송 - // (모든 작업을 병렬로 처리하지 않고 순차적(또는 하나씩)으로 보내 메모리 부담을 줄임) - for (const url of uniqueUrls) { - const base64Data = await getBase64FromUrl(url); - if (base64Data) { - // 이미지 하나를 성공할 때마다 네이티브로 즉시 전송 - sendMessage({ - type: "SINGLE_IMAGE_DATA", - imgSrc: url, - base64Data: base64Data - }); } + // 3. 각 URL을 순회하며 Base64로 변환하고 즉시 네이티브로 전송 + // (모든 작업을 병렬로 처리하지 않고 순차적(또는 하나씩)으로 보내 메모리 부담을 줄임) + var idx = 0 + for (const url of uniqueUrls) { + try { + const base64Data = await getBase64FromUrl(url); + if (base64Data) { + // 이미지 하나를 성공할 때마다 네이티브로 즉시 전송 + sendMessage({ + type: "SINGLE_IMAGE_DATA", + imgSrc: url, + base64Data: base64Data + }); + } + } catch (e) { + + } + + toast(`${idx + 1} / ${uniqueUrls.length} unique images to cache.`); + idx += 1; + } + gotoNext() } - gotoNext() } + + // 스크롤 함수 정의 + function smoothScrollDown() { + // 1. 전체 페이지의 높이를 가져옵니다. + const totalHeight = document.body.scrollHeight; + + // 2. 한 번에 스크롤할 거리를 계산합니다 (전체 높이의 20분의 1). + const scrollIncrement = totalHeight / 25; + + // 3. 1초(1000ms) 간격으로 스크롤을 반복 실행합니다. + scrollInterval = setInterval(() => { + // 현재 스크롤 위치 + 화면에 보이는 높이가 전체 페이지 높이보다 크거나 같으면 + // 페이지의 끝에 도달한 것입니다. + if (Math.ceil(window.scrollY) + Math.ceil(window.innerHeight) >= (totalHeight * 0.9)) { + console.log("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다."); + toast("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다.") + clearInterval(scrollInterval); // 반복 실행을 멈춥니다. + getImg() + } else { + // 계산된 거리만큼 부드럽게 아래로 스크롤합니다. + window.scrollBy({ + top: scrollIncrement, + left: 0, + behavior: 'smooth' // 부드럽게 스크롤하는 옵션 + }); + toast(`smooth scrollBy ${scrollIncrement}, ${window.scrollY}, ${window.innerHeight}, ${totalHeight}`) + } + }, 2000); // 1000밀리초 = 1초 + } + +// 스크롤 함수 실행 + smoothScrollDown(); + + + } else if(location.href.search("javt")) { console.log(`Found TEST`); diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt index b61c06e7..17164ad6 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt @@ -23,6 +23,7 @@ import android.content.ComponentCallbacks2 import android.database.sqlite.SQLiteDatabase import bums.lunatic.launcher.helpers.HourlyLogWriter import bums.lunatic.launcher.helpers.PrefHelper +import bums.lunatic.launcher.home.Base64ImageCache import bums.lunatic.launcher.home.Base64RequestHandler import bums.lunatic.launcher.utils.Blog import com.squareup.picasso.OkHttp3Downloader @@ -45,6 +46,7 @@ internal class LunaticLauncher : Application() { appContext = this // Base.initialize(this) PrefHelper.initialize(this) + Base64ImageCache.init(this) val dir = File("/storage/emulated/0/bums_ob/BUM'S PACED /scraped/logs") ///BUM'S PACED/pdfs if (!dir.exists()) { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/Base64ImageCache.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/Base64ImageCache.kt index e8d04ee0..dd8e0f9f 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/Base64ImageCache.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/Base64ImageCache.kt @@ -1,15 +1,25 @@ package bums.lunatic.launcher.home +import android.content.Context import android.graphics.BitmapFactory import android.util.Base64 import androidx.collection.LruCache import com.squareup.picasso.Picasso import com.squareup.picasso.Request import com.squareup.picasso.RequestHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.io.File import java.io.IOException +import java.math.BigInteger +import java.security.MessageDigest /** - * Base64 데이터를 URL 키와 함께 저장하는 메모리 캐시 (Singleton 또는 DI로 관리) + * Base64 데이터를 URL 키와 함께 저장하는 2단계 캐시 (메모리 + 파일) + * 1. 메모리 캐시(LruCache)를 먼저 확인하여 빠른 접근을 제공합니다. + * 2. 메모리에 없는 경우 파일 캐시를 확인하여 디스크 영속성을 지원합니다. * OOM(메모리 부족)을 피하기 위해 비트맵 자체가 아닌 Base64 문자열을 저장합니다. */ object Base64ImageCache { @@ -17,22 +27,83 @@ object Base64ImageCache { private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() private val cacheSize = maxMemory / 8 - // Key: Image URL, Value: Base64 Data String + // L1 Cache: Memory (Key: Image URL, Value: Base64 Data String) private val lru: LruCache = LruCache(cacheSize) + // L2 Cache: File System + private var cacheDir: File? = null + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + /** + * 캐시를 사용하기 전에 반드시 Application 클래스 등에서 초기화해야 합니다. + * @param context 애플리케이션 컨텍스트 + */ + fun init(context: Context) { + // 앱의 캐시 디렉토리 내에 이미지 캐시 전용 폴더 생성 + cacheDir = File(context.cacheDir, "base64_image_cache").apply { mkdirs() } + } + + /** + * 메모리 캐시와 파일 캐시에 Base64 데이터를 저장합니다. + * 파일 저장은 백그라운드에서 비동기적으로 수행됩니다. + */ fun put(url: String, base64Data: String) { + // 1. 메모리 캐시에 저장 if (lru.get(url) == null) { lru.put(url, base64Data) } + + // 2. 파일 캐시에 비동기적으로 저장 + coroutineScope.launch { + cacheDir?.let { + try { + val file = File(it, url.toMd5()) + file.writeText(base64Data, Charsets.UTF_8) + } catch (e: Exception) { + e.printStackTrace() // 실제 앱에서는 로깅 라이브러리 사용 권장 + } + } + } } + /** + * 메모리 또는 파일 캐시에서 Base64 데이터를 가져옵니다. + * 파일 캐시에서 가져온 경우, 메모리 캐시에도 추가합니다. + */ fun get(url: String): String? { - return lru.get(url) + // 1. 메모리 캐시에서 먼저 조회 + lru.get(url)?.let { return it } + + // 2. 메모리에 없으면 파일 캐시에서 조회 + cacheDir?.let { + try { + val file = File(it, url.toMd5()) + if (file.exists()) { + val base64Data = file.readText(Charsets.UTF_8) + // 파일에서 읽은 데이터를 메모리에 올려서 다음 접근 속도를 높임 + lru.put(url, base64Data) + return base64Data + } + } catch (e: Exception) { + e.printStackTrace() + } + } + return null + } + + /** + * URL을 MD5 해시로 변환하여 안전한 파일 이름으로 사용합니다. + */ + private fun String.toMd5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') } } + /** * Picasso가 요청한 URL이 Base64ImageCache에 있는지 확인하는 커스텀 핸들러 + * (이 클래스는 변경할 필요가 없습니다) */ class Base64RequestHandler : RequestHandler() { @@ -44,7 +115,7 @@ class Base64RequestHandler : RequestHandler() { */ override fun canHandleRequest(data: Request): Boolean { val url = data.uri.toString() - // data: 스킴이 아니고, 우리 캐시에 URL 키가 존재할 때만 true 반환 + // data: 스킴이 아니고, 우리 캐시(메모리 또는 파일)에 URL 키가 존재할 때만 true 반환 return !url.startsWith(DATA_SCHEME) && Base64ImageCache.get(url) != null } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index a5d62c2f..9165157a 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -947,6 +947,7 @@ class GeckoWeb : BWebview { "WebtoonContents"-> { } "MSG" -> { + context.toast("Received Msg privates form ${lPortMessage.msg}") } "SHOWVIEWER" -> { }