This commit is contained in:
2026-11-09 15:08:29 +09:00
parent 22e03dfd10
commit ccaba56329
21 changed files with 5974 additions and 3830 deletions
+15
View File
@@ -140,6 +140,21 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".home.CompletedFilesActivity"
android:theme="@style/Theme.Player"
android:icon="@drawable/ic_launcher"
android:roundIcon="@drawable/ic_launcher"
android:label="리더"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|fontScale|keyboardHidden|keyboard|layoutDirection|navigation"
android:screenOrientation="portrait"
android:excludeFromRecents="false"
android:hardwareAccelerated="true"
android:taskAffinity="bums.lunatic.launcher.home.WebReaderActivity"
android:launchMode="singleTask"> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".player.DocumentViewerActivity"
@@ -1,6 +1,26 @@
let isRunning = false;
let currentUrl = location.href;
function hideSpecificImages() {
const style = document.createElement('style');
style.innerHTML = `
img[src*=".webp"],
img[src*=".png"],
img[src*=".gif"],
img[src*=".jpg"],
source[type="image/webp"],
source[type="image/png"],
source[type="image/jpg"],
source[type="image/gif"] {
display: none !important;
}
`;
document.head.appendChild(style);
}
// 스크립트 로드 시 즉시 실행
hideSpecificImages();
// 메인 스크래핑 로직을 별도의 함수로 분리
async function runScraper() {
if (isRunning) return; // 중복 실행 방지
@@ -63,13 +63,20 @@
//// browser.runtime.sendMessage({ type: 'RELOAD_REQUEST' });
//// }
////});
browser.webRequest.onBeforeRequest.addListener(
function(details) {
console.log("Request intercepted for URL:", details.url);
// 이 예시는 단순히 로그를 남기므로 요청을 변경하거나 차단하지 않음
// 페이지 자체를 이동(main_frame)하는 요청인지 확인
if (details.type === "main_frame") {
const currentUrl = details.originUrl || details.documentUrl || "";
// 현재 URL에 'darama'가 포함되어 있는데, 이동할 URL에는 'darama'가 없다면 차단
if (currentUrl.includes("darama") && !details.url.includes("darama")) {
console.log("Redirect blocked for URL:", details.url);
return { cancel: true };
}
}
return {};
},
{urls: ["<all_urls>"]}, // 모든 URL에 대해 요청 듣기
[]
{urls: ["<all_urls>"]},
["blocking"] // 요청을 차단할 수 있는 권한 부여
);
File diff suppressed because one or more lines are too long
@@ -1,4 +1,29 @@
if (location.href.includes("darama")) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
// 추가된 노드 자체가 iframe인 경우
if (node.tagName === 'IFRAME') {
node.remove();
console.log("Blocked dynamic iframe");
}
// 노드 내부에 자식 요소로 iframe이 포함되어 추가된 경우
else if (node.nodeType === Node.ELEMENT_NODE && node.querySelectorAll) {
node.querySelectorAll('iframe').forEach(iframe => {
iframe.remove();
console.log("Blocked nested dynamic iframe");
});
}
});
});
});
// 문서 전체의 자식 노드 추가를 실시간으로 감시
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
function pubDateNumber(tdateTime) {
@@ -0,0 +1,16 @@
chrome.webRequest.onBeforeRequest.addListener(
function(details) {
if (details.type === "main_frame") {
const currentUrl = details.originUrl || details.documentUrl || "";
// 현재 URL에 'darama'가 포함되어 있는데, 이동할 URL에는 'darama'가 없다면 차단
if (currentUrl.includes("darama") && !details.url.includes("darama")) {
console.log("Redirect blocked for URL:", details.url);
return { cancel: true };
}
}
return {};
},
{ urls: ["<all_urls>"] },
["blocking"] // 요청을 가로채고 취소할 수 있는 권한
);
@@ -0,0 +1,21 @@
{
"manifest_version": 2,
"version": "1.0",
"name": "Darama AdBlocker",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"run_at": "document_start",
"matches": ["<all_urls>"],
"js": ["messaging.js"]
}
],
"permissions": [
"cookies",
"<all_urls>",
"webRequest",
"webRequestBlocking"
]
}
@@ -0,0 +1,78 @@
// 1. 페이지 로드 시 정적 광고 및 불필요한 요소 즉시 제거
document.addEventListener('DOMContentLoaded', function () {
handleCommon(document);
// applyDomainRules();
});
// 2. DOM 변화를 실시간 감시하여 동적 아이프레임 및 광고 추가 완벽 차단
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// [darama 도메인] 동적으로 삽입되는 iframe 즉시 삭제
if (location.href.includes("darama")) {
if (node.tagName === 'IFRAME') {
node.remove();
} else if (node.querySelectorAll) {
node.querySelectorAll('iframe').forEach(iframe => iframe.remove());
}
}
// 동적으로 삽입되는 공통 광고 노드 지속 삭제
handleCommon(node);
});
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
// 3. 공통 광고 요소 제거 함수 (정적/동적 DOM 모두 대응)
function handleCommon(parentElement) {
if (!parentElement.querySelectorAll) return;
// const adSelectors = [
// ".top_google_ad_space", ".adv-group", "[id^='div-gpt-ad']",
// "[id^='div_adnmore_area']", "[class^='adv-groupno']", ".code-block",
// ".ad-template", "#xpromo-bottom-sheet", "[data-banner^='coupang-']",
// "[id^='mobonDivBanner']", "[id^='pnlTopAd']", "[class^='ad-tag']",
// "[class^='science-banner-area']"
// ];
//
// adSelectors.forEach(selector => {
// parentElement.querySelectorAll(selector).forEach(e => e.remove());
// });
// parentElement.querySelectorAll('iframe').forEach(e => {
// const src = e.getAttribute("src");
// if (src && (src.includes("ads") || src.includes("coupang"))) {
// e.remove();
// }
// });
}
// 4. 도메인별 맞춤형 요소 제거 (기존 룰 단순화)
function applyDomainRules() {
const url = location.href;
if (url.includes("nate.com")) {
document.querySelectorAll('[class^="float-adv-wrap"], [id^="ad_"], #ad_sponsorBar, #newsSidebar, iframe[id^="ad"]').forEach(e => e.remove());
} else if (url.includes("zdnet.co.kr")) {
document.querySelectorAll('div[id*="FloatCon"]').forEach(e => e.remove());
} else if (url.includes("inews24")) {
document.querySelectorAll('ad[alt^="a"], div[class^="topnews"], form[class^="talkplus"], ins, footer').forEach(e => e.remove());
} else if (url.includes("starnewskorea")) {
document.querySelectorAll('div[class^="floating-top-ad"], div[id^="dablewidget_"], footer').forEach(e => e.remove());
} else if (url.includes("khan.co.kr")) {
document.querySelectorAll('[class^="banner-"], [class^="google-auto-placed"], footer, iframe').forEach(e => e.remove());
} else if (url.includes("dcinside.com")) {
document.querySelectorAll('[id^="view_btn_area"], [class^="trend-rank"], [class^="adv-group"], [id^="div_adnmore_area"]').forEach(e => e.remove());
} else if (url.includes("fmkorea.com")) {
document.querySelectorAll('[class^="fmad_wrapper"], [class^="ad ad_wrapper"], [class^="m_top_hotdeal"]').forEach(e => e.remove());
} else if (url.includes("theqoo.net")) {
document.querySelectorAll('[class^="board_content_google_ad"], ins[class^="adsbygoogle"]').forEach(e => e.remove());
}
}
@@ -185,7 +185,7 @@
text-align: left;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
/* 💡 Grid 대신 Flex Column으로 변경하여 세로 스크롤에 최적화 */
/* Grid 대신 Flex Column으로 변경하여 세로 스크롤에 최적화 */
display: flex;
flex-direction: column;
gap: 12px;
@@ -203,7 +203,7 @@
justify-content: space-between;
box-sizing: border-box;
width: 100%;
flex-shrink: 0; /* 💡 핵심: 화면이 좁아도 세로로 절대 찌그러지지 않게 강제 방어 */
flex-shrink: 0; /* 핵심: 화면이 좁아도 세로로 절대 찌그러지지 않게 강제 방어 */
overflow: hidden;
transition: transform 0.15s, background 0.15s;
}
@@ -253,10 +253,10 @@
<div id="library-overlay" class="overlay" style="display: none;">
<div class="box library-box">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 8px;">
<h2 style="margin: 0;">📚 내 서재</h2>
<h2 style="margin: 0;"> 내 서재</h2>
<div style="display: flex; gap: 8px;">
<select id="library-tag-filter" style="padding: 6px; border-radius: 6px; background: var(--modal-content-bg); color: var(--text-color); border: 1px solid var(--border-color); font-size: 13px; cursor: pointer;">
<option value="all">🏷️ 전체 태그</option>
<option value="all">️ 전체 태그</option>
</select>
<select id="library-sort-select" style="padding: 6px; border-radius: 6px; background: var(--modal-content-bg); color: var(--text-color); border: 1px solid var(--border-color); font-size: 13px; cursor: pointer;">
<option value="lastRead">최근 열람순</option>
@@ -272,10 +272,10 @@
<button id="btn-go-new" class="btn-go-new">+ 새 책 추가하기</button>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button id="btn-backup-all" style="flex: 1; padding: 10px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;">📥 전체 백업</button>
<button id="btn-restore-main" style="flex: 1; padding: 10px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;">📤 복원하기</button>
<button id="btn-backup-all" style="flex: 1; padding: 10px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;"> 전체 백업</button>
<button id="btn-restore-main" style="flex: 1; padding: 10px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;"> 복원하기</button>
</div>
<button id="btn-manage-keywords" style="margin-top: 10px; width: 100%; padding: 10px; background: #e67e22; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;">🏷️ 태그 및 키워드 관리</button>
<button id="btn-manage-keywords" style="margin-top: 10px; width: 100%; padding: 10px; background: #e67e22; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;">️ 태그 및 키워드 관리</button>
<input type="file" id="restore-input" accept=".json" style="display: none;">
</div>
@@ -322,13 +322,13 @@
<div id="keyword-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: 200; align-items: center; justify-content: center;">
<div class="box" style="width: 90%; max-width: 500px; max-height: 80vh; display: flex; flex-direction: column; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px;">
<h2 style="margin: 0; font-size: 17px;">🏷️ 태그 및 키워드 관리</h2>
<h2 style="margin: 0; font-size: 17px;">️ 태그 및 키워드 관리</h2>
<span id="keyword-close-btn" style="cursor: pointer; font-size: 22px; color: #888;">&times;</span>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 15px;">
<button id="btn-backup-tags" style="flex: 1; padding: 8px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: bold;">📥 룰만 백업</button>
<button id="btn-restore-tags" style="flex: 1; padding: 8px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: bold;">📤 룰만 복원</button>
<button id="btn-backup-tags" style="flex: 1; padding: 8px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: bold;"> 룰만 백업</button>
<button id="btn-restore-tags" style="flex: 1; padding: 8px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: bold;"> 룰만 복원</button>
<input type="file" id="tag-restore-input" accept=".json" style="display: none;">
</div>
@@ -345,7 +345,7 @@
<button id="btn-save-keyword-rules" style="flex: 1; padding: 10px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 13px;">저장하기</button>
</div>
<button id="btn-reapply-tags" style="margin-top: 8px; width: 100%; padding: 10px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 13px;">🔄 전체 서재 태그 싹 다시 스캔하기</button>
<button id="btn-reapply-tags" style="margin-top: 8px; width: 100%; padding: 10px; background: #8e44ad; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 13px;"> 전체 서재 태그 싹 다시 스캔하기</button>
</div>
</div>
@@ -562,10 +562,10 @@
if (!readerId) {
const sortSelect = document.getElementById('library-sort-select');
const tagFilterSelect = document.getElementById('library-tag-filter'); // 🆕 태그 필터
const tagFilterSelect = document.getElementById('library-tag-filter'); // 태그 필터
let currentSort = localStorage.getItem('webReader_sortOption') || 'lastRead';
let currentTagFilter = 'all'; // 🆕 현재 선택된 태그 상태
let currentTagFilter = 'all'; // 현재 선택된 태그 상태
sortSelect.value = currentSort;
@@ -575,7 +575,7 @@
renderLibraryList();
});
// 🆕 태그 변경 이벤트
// 태그 변경 이벤트
tagFilterSelect.addEventListener('change', (e) => {
currentTagFilter = e.target.value;
renderLibraryList();
@@ -592,7 +592,7 @@
return { ...book, savedData, pKey };
});
// 2. 🆕 존재하는 모든 태그 수집 (중복 제거)
// 2. 존재하는 모든 태그 수집 (중복 제거)
const uniqueTags = new Set();
booksWithMeta.forEach(book => {
if (book.savedData.tags && Array.isArray(book.savedData.tags)) {
@@ -603,8 +603,8 @@
}
});
// 3. 🆕 태그 필터 UI 업데이트 (현재 선택된 값 유지)
tagFilterSelect.innerHTML = '<option value="all">🏷️ 전체 태그</option>';
// 3. 태그 필터 UI 업데이트 (현재 선택된 값 유지)
tagFilterSelect.innerHTML = '<option value="all">️ 전체 태그</option>';
Array.from(uniqueTags).sort().forEach(tag => {
const option = document.createElement('option');
option.value = tag;
@@ -613,7 +613,7 @@
tagFilterSelect.appendChild(option);
});
// 4. 🆕 태그 필터링 적용
// 4. 태그 필터링 적용
let filteredBooks = booksWithMeta;
if (currentTagFilter !== 'all') {
filteredBooks = booksWithMeta.filter(book => {
@@ -685,7 +685,7 @@
const totalChars = book.chapters ? book.chapters.reduce((sum, chap) => sum + (chap.content ? chap.content.length : 0), 0) : 0;
let progressText = savedData.chapter !== undefined ? `<span style="font-size:12px; color:#888; margin-left: 6px;">(Ch.${savedData.chapter + 1})</span>` : '';
let bookStatsText = `<div style="font-size:11px; color:#aaa; margin-top: 4px;">📚 총 ${totalChapters}화 · ${totalChars.toLocaleString()}자</div>`;
let bookStatsText = `<div style="font-size:11px; color:#aaa; margin-top: 4px;"> 총 ${totalChapters}화 · ${totalChars.toLocaleString()}자</div>`;
let tagsText = '';
if (savedData.tags && savedData.tags.length > 0) {
@@ -699,8 +699,8 @@
tagsText = `<div style="margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px;">${badges}</div>`;
}
let lastReadText = savedData.lastRead ? `<div style="font-size:11px; color:#aaa; margin-top: 6px;">🕒 마지막 열람: ${savedData.lastRead}</div>` : '';
let memoText = savedData.memo ? `<div style="font-size:12px; color:#f39c12; margin-top: 6px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">📝 ${savedData.memo}</div>` : '';
let lastReadText = savedData.lastRead ? `<div style="font-size:11px; color:#aaa; margin-top: 6px;"> 마지막 열람: ${savedData.lastRead}</div>` : '';
let memoText = savedData.memo ? `<div style="font-size:12px; color:#f39c12; margin-top: 6px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;"> ${savedData.memo}</div>` : '';
const titleArea = document.createElement('div');
titleArea.style.cssText = "flex: 1; display: flex; flex-direction: column; margin-bottom: 10px;";
@@ -732,7 +732,7 @@
const isCompleted = savedData.isCompleted || false;
const compBtn = document.createElement('button');
compBtn.innerText = isCompleted ? "✔️ 완독" : "📖 읽는 중";
compBtn.innerText = isCompleted ? "✔️ 완독" : " 읽는 중";
compBtn.style.cssText = `grid-column: span 2; padding: 8px 0; background-color: ${isCompleted ? '#27ae60' : '#95a5a6'}; ${commonBtnStyle}`;
compBtn.onclick = (e) => {
e.stopPropagation();
@@ -1402,11 +1402,11 @@
// [8] 태그 및 키워드 자동 추출 로직 (+ 태그 룰 백업/복원)
// ============================================================================
const DEFAULT_KEYWORD_RULES = {
"🐉 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
" 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
"⚔️ 무협": ["무림", "천마", "검기", "내공", "혈교", "무당파"],
"🏢 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
"💕 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
"🚀 SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
" 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
" 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
" SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
};
function getKeywordRules() {
@@ -1466,7 +1466,7 @@
const btnSaveKeywordRules = document.getElementById('btn-save-keyword-rules');
const btnReapplyTags = document.getElementById('btn-reapply-tags');
// 💡 태그 룰 전용 백업 및 복원 로직
// 태그 룰 전용 백업 및 복원 로직
document.getElementById('btn-backup-tags').addEventListener('click', () => {
const currentRules = getKeywordRules();
downloadJSON(currentRules, `tag_rules_backup_${Date.now()}.json`);
@@ -1520,7 +1520,7 @@
row.className = 'keyword-rule-row';
row.style.cssText = "display: flex; gap: 6px; margin-bottom: 8px; align-items: center;";
row.innerHTML = `
<input type="text" class="rule-tag-input" placeholder="태그명 (예: 🐉 판타지)" value="${tag}" style="width: 30%; padding: 6px; border: 1px solid var(--border-color); background: var(--modal-content-bg); color: var(--text-color); border-radius: 4px; font-size: 12px;">
<input type="text" class="rule-tag-input" placeholder="태그명 (예: 판타지)" value="${tag}" style="width: 30%; padding: 6px; border: 1px solid var(--border-color); background: var(--modal-content-bg); color: var(--text-color); border-radius: 4px; font-size: 12px;">
<input type="text" class="rule-keywords-input" placeholder="키워드 (쉼표로 구분)" value="${keywordsStr}" style="flex: 1; padding: 6px; border: 1px solid var(--border-color); background: var(--modal-content-bg); color: var(--text-color); border-radius: 4px; font-size: 12px;">
<button class="btn-del-rule" style="padding: 6px 9px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">✕</button>
`;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
const CACHE_NAME = 'web-reader-cache-v1';
const ASSETS_TO_CACHE = [
'./reader.html',
'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'
];
// 1. 설치 시 핵심 파일 및 외부 CDN 라이브러리 캐싱
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE);
}).then(() => self.skipWaiting())
);
});
// 2. 구버전 캐시 정리
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});
// 3. 네트워크 요청 가로채기: 오프라인일 때 캐시된 파일 반환
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// reader.html에 쿼리스트링(?id=book_xxx)이 붙은 경우에도 캐시된 reader.html을 반환하도록 처리
if (url.pathname.endsWith('reader.html') || url.pathname.endsWith('/')) {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match('./reader.html');
})
);
return;
}
// 그 외 정적 리소스(JSZip 등) 캐시 우선 반환
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((networkResponse) => {
// 성공적으로 받아온 CDN 파일 등은 캐시에 복사해둠
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'cors') {
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return networkResponse;
});
})
);
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -339,7 +339,10 @@ open class GeckoWeb @JvmOverloads constructor(
val url = response.uri
// 💡 .srt 파일인 경우 플레이어 로직과 연결
if (url.endsWith(".srt", ignoreCase = true) || url.contains("download")) {
(context as? PlayerActivity)?.let { player ->
// (context as? PlayerActivity)?.let { player ->
// player.downloadSubtitle(url)
// }
(context as? CompletedFilesActivity)?.let { player ->
player.downloadSubtitle(url)
}
} else {
@@ -379,6 +382,12 @@ open class GeckoWeb @JvmOverloads constructor(
session: GeckoSession,
request: GeckoSession.NavigationDelegate.LoadRequest
): GeckoResult<AllowOrDeny?>? {
val currentUrl = lastedUrl ?: ""
if (currentUrl.contains("darama") && !request.uri.contains("darama")) {
return GeckoResult.fromValue(AllowOrDeny.DENY)
}
if (request.target != 1 && request.uri.contains("workupload")) {
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), request.uri)
return null
@@ -387,6 +396,13 @@ open class GeckoWeb @JvmOverloads constructor(
}
override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
val currentUrl = lastedUrl ?: ""
if (currentUrl.contains("darama") && !uri.contains("darama")) {
Log.d("GeckoWeb", "Blocked new session (popup) for: $uri")
return null // null을 반환하면 새 창이 열리는 것을 무시합니다.
}
Uri.parse(uri)?.let {
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
it.host?.contains("x.com") == true ||
@@ -394,10 +410,18 @@ open class GeckoWeb @JvmOverloads constructor(
loadUrl(uri)
} else if (uri.contains("googlevideo.com")) {
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), uri)
} else {
if (uri.contains("ay267") || uri.contains("bblaa")){
val currentUrl = lastedUrl ?: ""
if (currentUrl.contains("darama") && !uri.contains("darama")) {
Log.d("GeckoWeb", "Blocked new session (popup) for: $uri")
return null // null을 반환하면 새 창이 열리는 것을 무시합니다.
}
} else {
showNewSessionDialog(uri)
}
}
}
return super.onNewSession(session, uri)
}
@@ -666,10 +690,16 @@ open class GeckoWeb @JvmOverloads constructor(
private fun handlePortMessage(msg: PortMessage) {
when (msg.type) {
"SUBTITLE_LIST_RESULT" -> {
(context as? PlayerActivity)?.let { player ->
// (context as? PlayerActivity)?.let { player ->
// Blog.LOGE("msg.subTitles ${msg.subTitles}")
// player.runOnUiThread {
// player.showDownSubtitleSelectionDialog(msg.subTitles)
// }
// }
(context as? CompletedFilesActivity)?.let { player ->
Blog.LOGE("msg.subTitles ${msg.subTitles}")
player.runOnUiThread {
player.showDownSubtitleSelectionDialog(msg.subTitles)
player.showSubtitleSelectionDialog(msg.subTitles)
}
}
}
@@ -33,6 +33,7 @@ import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.startActivity
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
@@ -169,9 +170,9 @@ open class NeoRssActivity : CommonActivity() {
is TokiFragment -> {
currentFragment.back()
}
is CompletedFilesFragment -> {
currentFragment.backPress()
}
// is CompletedFilesFragment -> {
// currentFragment.backPress()
// }
else -> {
// showContents(R.id.close)
}
@@ -647,7 +648,7 @@ open class NeoRssActivity : CommonActivity() {
// R.id.btn_img1 -> TokiFragment.newInstanceArtStation()
R.id.btn_torrent -> TorrentListFragment()
R.id.btn_info -> SystemStatusFragment()
R.id.btn_completed_files -> CompletedFilesFragment()
R.id.btn_learn -> LearningFragment()
R.id.books -> {
startActivity(Intent(this@NeoRssActivity, WebReaderActivity::class.java))
@@ -658,6 +659,11 @@ open class NeoRssActivity : CommonActivity() {
finish()
return
}
R.id.btn_completed_files -> {
startActivity(Intent(this@NeoRssActivity, CompletedFilesActivity::class.java))
finish()
return
}
else -> null
}
@@ -742,9 +748,9 @@ open class NeoRssActivity : CommonActivity() {
is TokiFragment -> {
currentFragment.back()
}
is CompletedFilesFragment -> {
currentFragment.backPress()
}
// is CompletedFilesFragment -> {
// currentFragment.backPress()
// }
else -> {
// showContents(R.id.close)
}
@@ -59,8 +59,18 @@ class TorrentListFragment : BottomSheetDialogFragment() {
}
},
onCancelClick = { task ->
// UI에서 취소 시 파일까지 삭제할지 여부는 두 번째 파라미터로 결정합니다. (예: true)
torrentService?.removeTorrent(task.infoHash, deleteFile = true)
},
// 💡 추가된 롱프레스 이벤트 (최우선 순위로 끌어올림)
onItemLongClick = { task ->
android.app.AlertDialog.Builder(requireContext())
.setTitle(task.name)
.setMessage("이 파일을 최우선으로 다운로드하시겠습니까?")
.setPositiveButton("최우선 다운로드") { _, _ ->
torrentService?.prioritizeTorrent(task.infoHash)
}
.setNegativeButton("취소", null)
.show()
}
)
@@ -13,7 +13,8 @@ import bums.lunatic.launcher.workers.TorrentTask
class TorrentListAdapter(
private val onPauseResumeClick: (TorrentTask) -> Unit,
private val onCancelClick: (TorrentTask) -> Unit
private val onCancelClick: (TorrentTask) -> Unit,
private val onItemLongClick: (TorrentTask) -> Unit
) : RecyclerView.Adapter<TorrentListAdapter.ViewHolder>() {
private var tasks = listOf<TorrentTask>()
@@ -49,6 +50,10 @@ class TorrentListAdapter(
holder.btnPauseResume.setOnClickListener { onPauseResumeClick(task) }
holder.btnCancel.setOnClickListener { onCancelClick(task) }
holder.itemView.setOnLongClickListener {
onItemLongClick(task)
true
}
}
override fun getItemCount() = tasks.size
@@ -190,8 +190,8 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
arguments = Bundle().apply {
putString(ARG_TYPE, "web")
putInt(ARG_LAST_NUM, 26)
putString(ARG_NAME, "bookto26®")
putString(ARG_DOT, ".com")
putString(ARG_NAME, "dramadunyam")
putString(ARG_DOT, "com/ko")
putBoolean(ARG_USE_NUM_URL, false)
putBoolean(ARG_ENABLE_GESTURE, true)
}
@@ -86,7 +86,32 @@ class TorrentService : Service() {
}
// TorrentService.kt에 추가
fun prioritizeTorrent(infoHash: String) {
try {
session.find(Sha1Hash(infoHash))?.let { handle ->
// 1. 큐(대기열) 포지션을 최상위(0)로 끌어올림
handle.queuePositionTop()
// 💡 2. 수정된 부분: getter인 flags() 대신 SWIG의 unset_flags() 사용
handle.swig().unset_flags(libtorrent.getAuto_managed())
// 3. 강제 재시작 (일시정지 상태였다면 즉시 시작)
handle.resume()
// 4. 트래커 강제 재요청으로 피어 즉시 탐색 유도
handle.forceReannounce()
}
// UI 즉시 갱신
refreshTorrentStats()
// 전체 세션 상태 재평가 (다른 것들을 멈추고 얘를 켤 수 있도록)
updateSessionState()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun registerBatteryReceiver() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_POWER_CONNECTED)
@@ -193,10 +218,11 @@ var batteryPct = 50
val currentRate = status.downloadPayloadRate()
val basePriority = status.queuePosition()
// 속도가 너무 낮거나 0인 경우 페널티 부여
// 단, 막 시작해서 속도가 측정되지 않은 경우를 위해 추가 로직을 넣을 수 있지만
// 주기적 업데이트(2초)가 되므로 페널티를 받아도 다음 턴에 기회를 잡을 수 있습니다.
val finalScore = if (currentRate < LOW_SPEED_THRESHOLD) {
// 💡 수정된 점수 계산 로직:
// 큐 포지션이 0(사용자가 강제로 Top으로 올린 경우)이면 속도 페널티를 주지 않음
val finalScore = if (basePriority == 0) {
0 // 무조건 0순위 유지
} else if (currentRate < LOW_SPEED_THRESHOLD) {
basePriority + PENALTY_SCORE
} else {
basePriority
@@ -212,7 +238,7 @@ var batteryPct = 50
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
if (isCharging && batteryPct > 75) {
torrentsWithoutMetadata.forEach { it.swig().resume() }
val maxSlots = if (isWifiConnected) 6 else 1
val maxSlots = if (isWifiConnected) 8 else 3
val sortedByPriority = torrentsWithMetadata.sortedBy { it.second }
sortedByPriority.forEachIndexed { index, pair ->
@@ -176,4 +176,11 @@
android:gravity="center"
android:padding="12dp"/>
</LinearLayout>
<!-- 💡 자막 다운로드 파싱용 GeckoWeb 추가 (화면에 보이지 않게 처리) -->
<bums.lunatic.launcher.home.GeckoWeb
android:id="@+id/geckoWebFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone" />
</LinearLayout>