...
This commit is contained in:
@@ -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) {
|
||||
|
||||
Binary file not shown.
@@ -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;">×</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>
|
||||
Reference in New Issue
Block a user