...
This commit is contained in:
@@ -140,6 +140,21 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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
|
<activity
|
||||||
android:name=".player.DocumentViewerActivity"
|
android:name=".player.DocumentViewerActivity"
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let currentUrl = location.href;
|
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() {
|
async function runScraper() {
|
||||||
if (isRunning) return; // 중복 실행 방지
|
if (isRunning) return; // 중복 실행 방지
|
||||||
|
|||||||
@@ -63,13 +63,20 @@
|
|||||||
//// browser.runtime.sendMessage({ type: 'RELOAD_REQUEST' });
|
//// browser.runtime.sendMessage({ type: 'RELOAD_REQUEST' });
|
||||||
//// }
|
//// }
|
||||||
////});
|
////});
|
||||||
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
function(details) {
|
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 {};
|
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) {
|
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;
|
text-align: left;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
/* 💡 Grid 대신 Flex Column으로 변경하여 세로 스크롤에 최적화 */
|
/* Grid 대신 Flex Column으로 변경하여 세로 스크롤에 최적화 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-shrink: 0; /* 💡 핵심: 화면이 좁아도 세로로 절대 찌그러지지 않게 강제 방어 */
|
flex-shrink: 0; /* 핵심: 화면이 좁아도 세로로 절대 찌그러지지 않게 강제 방어 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 0.15s, background 0.15s;
|
transition: transform 0.15s, background 0.15s;
|
||||||
}
|
}
|
||||||
@@ -253,10 +253,10 @@
|
|||||||
<div id="library-overlay" class="overlay" style="display: none;">
|
<div id="library-overlay" class="overlay" style="display: none;">
|
||||||
<div class="box library-box">
|
<div class="box library-box">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 8px;">
|
<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;">
|
<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;">
|
<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>
|
||||||
<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;">
|
<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>
|
<option value="lastRead">최근 열람순</option>
|
||||||
@@ -272,10 +272,10 @@
|
|||||||
<button id="btn-go-new" class="btn-go-new">+ 새 책 추가하기</button>
|
<button id="btn-go-new" class="btn-go-new">+ 새 책 추가하기</button>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
<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-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-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>
|
</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;">
|
<input type="file" id="restore-input" accept=".json" style="display: none;">
|
||||||
</div>
|
</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 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 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;">
|
<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>
|
<span id="keyword-close-btn" style="cursor: pointer; font-size: 22px; color: #888;">×</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 15px;">
|
<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-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-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;">
|
<input type="file" id="tag-restore-input" accept=".json" style="display: none;">
|
||||||
</div>
|
</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>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -562,10 +562,10 @@
|
|||||||
|
|
||||||
if (!readerId) {
|
if (!readerId) {
|
||||||
const sortSelect = document.getElementById('library-sort-select');
|
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 currentSort = localStorage.getItem('webReader_sortOption') || 'lastRead';
|
||||||
let currentTagFilter = 'all'; // 🆕 현재 선택된 태그 상태
|
let currentTagFilter = 'all'; // 현재 선택된 태그 상태
|
||||||
|
|
||||||
sortSelect.value = currentSort;
|
sortSelect.value = currentSort;
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@
|
|||||||
renderLibraryList();
|
renderLibraryList();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 태그 변경 이벤트
|
// 태그 변경 이벤트
|
||||||
tagFilterSelect.addEventListener('change', (e) => {
|
tagFilterSelect.addEventListener('change', (e) => {
|
||||||
currentTagFilter = e.target.value;
|
currentTagFilter = e.target.value;
|
||||||
renderLibraryList();
|
renderLibraryList();
|
||||||
@@ -592,7 +592,7 @@
|
|||||||
return { ...book, savedData, pKey };
|
return { ...book, savedData, pKey };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 🆕 존재하는 모든 태그 수집 (중복 제거)
|
// 2. 존재하는 모든 태그 수집 (중복 제거)
|
||||||
const uniqueTags = new Set();
|
const uniqueTags = new Set();
|
||||||
booksWithMeta.forEach(book => {
|
booksWithMeta.forEach(book => {
|
||||||
if (book.savedData.tags && Array.isArray(book.savedData.tags)) {
|
if (book.savedData.tags && Array.isArray(book.savedData.tags)) {
|
||||||
@@ -603,8 +603,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 🆕 태그 필터 UI 업데이트 (현재 선택된 값 유지)
|
// 3. 태그 필터 UI 업데이트 (현재 선택된 값 유지)
|
||||||
tagFilterSelect.innerHTML = '<option value="all">🏷️ 전체 태그</option>';
|
tagFilterSelect.innerHTML = '<option value="all">️ 전체 태그</option>';
|
||||||
Array.from(uniqueTags).sort().forEach(tag => {
|
Array.from(uniqueTags).sort().forEach(tag => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = tag;
|
option.value = tag;
|
||||||
@@ -613,7 +613,7 @@
|
|||||||
tagFilterSelect.appendChild(option);
|
tagFilterSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 🆕 태그 필터링 적용
|
// 4. 태그 필터링 적용
|
||||||
let filteredBooks = booksWithMeta;
|
let filteredBooks = booksWithMeta;
|
||||||
if (currentTagFilter !== 'all') {
|
if (currentTagFilter !== 'all') {
|
||||||
filteredBooks = booksWithMeta.filter(book => {
|
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;
|
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 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 = '';
|
let tagsText = '';
|
||||||
if (savedData.tags && savedData.tags.length > 0) {
|
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>`;
|
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 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 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');
|
const titleArea = document.createElement('div');
|
||||||
titleArea.style.cssText = "flex: 1; display: flex; flex-direction: column; margin-bottom: 10px;";
|
titleArea.style.cssText = "flex: 1; display: flex; flex-direction: column; margin-bottom: 10px;";
|
||||||
@@ -732,7 +732,7 @@
|
|||||||
|
|
||||||
const isCompleted = savedData.isCompleted || false;
|
const isCompleted = savedData.isCompleted || false;
|
||||||
const compBtn = document.createElement('button');
|
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.style.cssText = `grid-column: span 2; padding: 8px 0; background-color: ${isCompleted ? '#27ae60' : '#95a5a6'}; ${commonBtnStyle}`;
|
||||||
compBtn.onclick = (e) => {
|
compBtn.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1402,11 +1402,11 @@
|
|||||||
// [8] 태그 및 키워드 자동 추출 로직 (+ 태그 룰 백업/복원)
|
// [8] 태그 및 키워드 자동 추출 로직 (+ 태그 룰 백업/복원)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const DEFAULT_KEYWORD_RULES = {
|
const DEFAULT_KEYWORD_RULES = {
|
||||||
"🐉 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
|
" 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
|
||||||
"⚔️ 무협": ["무림", "천마", "검기", "내공", "혈교", "무당파"],
|
"⚔️ 무협": ["무림", "천마", "검기", "내공", "혈교", "무당파"],
|
||||||
"🏢 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
|
" 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
|
||||||
"💕 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
|
" 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
|
||||||
"🚀 SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
|
" SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
|
||||||
};
|
};
|
||||||
|
|
||||||
function getKeywordRules() {
|
function getKeywordRules() {
|
||||||
@@ -1466,7 +1466,7 @@
|
|||||||
const btnSaveKeywordRules = document.getElementById('btn-save-keyword-rules');
|
const btnSaveKeywordRules = document.getElementById('btn-save-keyword-rules');
|
||||||
const btnReapplyTags = document.getElementById('btn-reapply-tags');
|
const btnReapplyTags = document.getElementById('btn-reapply-tags');
|
||||||
|
|
||||||
// 💡 태그 룰 전용 백업 및 복원 로직
|
// 태그 룰 전용 백업 및 복원 로직
|
||||||
document.getElementById('btn-backup-tags').addEventListener('click', () => {
|
document.getElementById('btn-backup-tags').addEventListener('click', () => {
|
||||||
const currentRules = getKeywordRules();
|
const currentRules = getKeywordRules();
|
||||||
downloadJSON(currentRules, `tag_rules_backup_${Date.now()}.json`);
|
downloadJSON(currentRules, `tag_rules_backup_${Date.now()}.json`);
|
||||||
@@ -1520,7 +1520,7 @@
|
|||||||
row.className = 'keyword-rule-row';
|
row.className = 'keyword-rule-row';
|
||||||
row.style.cssText = "display: flex; gap: 6px; margin-bottom: 8px; align-items: center;";
|
row.style.cssText = "display: flex; gap: 6px; margin-bottom: 8px; align-items: center;";
|
||||||
row.innerHTML = `
|
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;">
|
<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>
|
<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
|
val url = response.uri
|
||||||
// 💡 .srt 파일인 경우 플레이어 로직과 연결
|
// 💡 .srt 파일인 경우 플레이어 로직과 연결
|
||||||
if (url.endsWith(".srt", ignoreCase = true) || url.contains("download")) {
|
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)
|
player.downloadSubtitle(url)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -379,6 +382,12 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
session: GeckoSession,
|
session: GeckoSession,
|
||||||
request: GeckoSession.NavigationDelegate.LoadRequest
|
request: GeckoSession.NavigationDelegate.LoadRequest
|
||||||
): GeckoResult<AllowOrDeny?>? {
|
): 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")) {
|
if (request.target != 1 && request.uri.contains("workupload")) {
|
||||||
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), request.uri)
|
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), request.uri)
|
||||||
return null
|
return null
|
||||||
@@ -387,6 +396,13 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
|
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 {
|
Uri.parse(uri)?.let {
|
||||||
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
|
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
|
||||||
it.host?.contains("x.com") == true ||
|
it.host?.contains("x.com") == true ||
|
||||||
@@ -395,7 +411,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
} else if (uri.contains("googlevideo.com")) {
|
} else if (uri.contains("googlevideo.com")) {
|
||||||
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), uri)
|
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl), uri)
|
||||||
} else {
|
} else {
|
||||||
showNewSessionDialog(uri)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,10 +690,16 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
private fun handlePortMessage(msg: PortMessage) {
|
private fun handlePortMessage(msg: PortMessage) {
|
||||||
when (msg.type) {
|
when (msg.type) {
|
||||||
"SUBTITLE_LIST_RESULT" -> {
|
"SUBTITLE_LIST_RESULT" -> {
|
||||||
(context as? PlayerActivity)?.let { player ->
|
// (context as? PlayerActivity)?.let { player ->
|
||||||
Blog.LOGE("msg.subTitles ${msg.subTitles}")
|
// 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.runOnUiThread {
|
||||||
player.showDownSubtitleSelectionDialog(msg.subTitles)
|
player.showSubtitleSelectionDialog(msg.subTitles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@@ -169,9 +170,9 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
is TokiFragment -> {
|
is TokiFragment -> {
|
||||||
currentFragment.back()
|
currentFragment.back()
|
||||||
}
|
}
|
||||||
is CompletedFilesFragment -> {
|
// is CompletedFilesFragment -> {
|
||||||
currentFragment.backPress()
|
// currentFragment.backPress()
|
||||||
}
|
// }
|
||||||
else -> {
|
else -> {
|
||||||
// showContents(R.id.close)
|
// showContents(R.id.close)
|
||||||
}
|
}
|
||||||
@@ -647,7 +648,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
// R.id.btn_img1 -> TokiFragment.newInstanceArtStation()
|
// R.id.btn_img1 -> TokiFragment.newInstanceArtStation()
|
||||||
R.id.btn_torrent -> TorrentListFragment()
|
R.id.btn_torrent -> TorrentListFragment()
|
||||||
R.id.btn_info -> SystemStatusFragment()
|
R.id.btn_info -> SystemStatusFragment()
|
||||||
R.id.btn_completed_files -> CompletedFilesFragment()
|
|
||||||
R.id.btn_learn -> LearningFragment()
|
R.id.btn_learn -> LearningFragment()
|
||||||
R.id.books -> {
|
R.id.books -> {
|
||||||
startActivity(Intent(this@NeoRssActivity, WebReaderActivity::class.java))
|
startActivity(Intent(this@NeoRssActivity, WebReaderActivity::class.java))
|
||||||
@@ -658,6 +659,11 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
R.id.btn_completed_files -> {
|
||||||
|
startActivity(Intent(this@NeoRssActivity, CompletedFilesActivity::class.java))
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,9 +748,9 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
is TokiFragment -> {
|
is TokiFragment -> {
|
||||||
currentFragment.back()
|
currentFragment.back()
|
||||||
}
|
}
|
||||||
is CompletedFilesFragment -> {
|
// is CompletedFilesFragment -> {
|
||||||
currentFragment.backPress()
|
// currentFragment.backPress()
|
||||||
}
|
// }
|
||||||
else -> {
|
else -> {
|
||||||
// showContents(R.id.close)
|
// showContents(R.id.close)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,18 @@ class TorrentListFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancelClick = { task ->
|
onCancelClick = { task ->
|
||||||
// UI에서 취소 시 파일까지 삭제할지 여부는 두 번째 파라미터로 결정합니다. (예: true)
|
|
||||||
torrentService?.removeTorrent(task.infoHash, deleteFile = 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(
|
class TorrentListAdapter(
|
||||||
private val onPauseResumeClick: (TorrentTask) -> Unit,
|
private val onPauseResumeClick: (TorrentTask) -> Unit,
|
||||||
private val onCancelClick: (TorrentTask) -> Unit
|
private val onCancelClick: (TorrentTask) -> Unit,
|
||||||
|
private val onItemLongClick: (TorrentTask) -> Unit
|
||||||
) : RecyclerView.Adapter<TorrentListAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<TorrentListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var tasks = listOf<TorrentTask>()
|
private var tasks = listOf<TorrentTask>()
|
||||||
@@ -49,6 +50,10 @@ class TorrentListAdapter(
|
|||||||
|
|
||||||
holder.btnPauseResume.setOnClickListener { onPauseResumeClick(task) }
|
holder.btnPauseResume.setOnClickListener { onPauseResumeClick(task) }
|
||||||
holder.btnCancel.setOnClickListener { onCancelClick(task) }
|
holder.btnCancel.setOnClickListener { onCancelClick(task) }
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
onItemLongClick(task)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = tasks.size
|
override fun getItemCount() = tasks.size
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
|
|||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putString(ARG_TYPE, "web")
|
putString(ARG_TYPE, "web")
|
||||||
putInt(ARG_LAST_NUM, 26)
|
putInt(ARG_LAST_NUM, 26)
|
||||||
putString(ARG_NAME, "bookto26®")
|
putString(ARG_NAME, "dramadunyam")
|
||||||
putString(ARG_DOT, ".com")
|
putString(ARG_DOT, "com/ko")
|
||||||
putBoolean(ARG_USE_NUM_URL, false)
|
putBoolean(ARG_USE_NUM_URL, false)
|
||||||
putBoolean(ARG_ENABLE_GESTURE, true)
|
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() {
|
private fun registerBatteryReceiver() {
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction(Intent.ACTION_POWER_CONNECTED)
|
addAction(Intent.ACTION_POWER_CONNECTED)
|
||||||
@@ -193,10 +218,11 @@ var batteryPct = 50
|
|||||||
val currentRate = status.downloadPayloadRate()
|
val currentRate = status.downloadPayloadRate()
|
||||||
val basePriority = status.queuePosition()
|
val basePriority = status.queuePosition()
|
||||||
|
|
||||||
// 속도가 너무 낮거나 0인 경우 페널티 부여
|
// 💡 수정된 점수 계산 로직:
|
||||||
// 단, 막 시작해서 속도가 측정되지 않은 경우를 위해 추가 로직을 넣을 수 있지만
|
// 큐 포지션이 0(사용자가 강제로 Top으로 올린 경우)이면 속도 페널티를 주지 않음
|
||||||
// 주기적 업데이트(2초)가 되므로 페널티를 받아도 다음 턴에 기회를 잡을 수 있습니다.
|
val finalScore = if (basePriority == 0) {
|
||||||
val finalScore = if (currentRate < LOW_SPEED_THRESHOLD) {
|
0 // 무조건 0순위 유지
|
||||||
|
} else if (currentRate < LOW_SPEED_THRESHOLD) {
|
||||||
basePriority + PENALTY_SCORE
|
basePriority + PENALTY_SCORE
|
||||||
} else {
|
} else {
|
||||||
basePriority
|
basePriority
|
||||||
@@ -212,7 +238,7 @@ var batteryPct = 50
|
|||||||
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
|
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
|
||||||
if (isCharging && batteryPct > 75) {
|
if (isCharging && batteryPct > 75) {
|
||||||
torrentsWithoutMetadata.forEach { it.swig().resume() }
|
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 }
|
val sortedByPriority = torrentsWithMetadata.sortedBy { it.second }
|
||||||
|
|
||||||
sortedByPriority.forEachIndexed { index, pair ->
|
sortedByPriority.forEachIndexed { index, pair ->
|
||||||
|
|||||||
@@ -176,4 +176,11 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:padding="12dp"/>
|
android:padding="12dp"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 💡 자막 다운로드 파싱용 GeckoWeb 추가 (화면에 보이지 않게 처리) -->
|
||||||
|
<bums.lunatic.launcher.home.GeckoWeb
|
||||||
|
android:id="@+id/geckoWebFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:visibility="gone" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
Reference in New Issue
Block a user