// ==UserScript==
// @name DGUT求是读书-自动阅读助手
// @namespace https://github.com/vanilla1108/DGUT-Reading-Helper
// @version 3.3.0
// @license MIT
// @description DGUT莞工求是读书计划自动阅读助手 — 获取优学院真实阅读时长、自动翻页/章节
// @author vanilla、DeepSeek
// @match https://ua.dgut.edu.cn/learnCourse/learnCourse.html?*
// @match https://*.ulearning.cn/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const PAGE_WIN = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const HOST_RE = /^https:\/\/ua\.dgut\.edu\.cn\/learnCourse\/learnCourse\.html\?.*/i;
const HOST_ORIGIN = 'https://ua.dgut.edu.cn';
const KEY = 'dgut_single_file_helper_config';
const RECORDS_KEY = 'dgut_reading_records';
const MSG = 'DGUT_SINGLE_FILE_READER_SYNC';
const READY_MSG = 'DGUT_SINGLE_FILE_READER_READY';
const READER_SCOPE = 'DGUT_LEARNCOURSE_READER';
const SAVE_INTERVAL = 30;
const D = { posX:20, posY:120, readerSec:30, readerAutoStart:true };
const MIN_BOOK_SEC = 4 * 3600 + 10;
const NAV_SEC = 3;
const cachedCourseId = new URL(location.href).searchParams.get('courseId') || '';
if (HOST_RE.test(location.href)) {
if (window.__DGUT_SINGLE_FILE_INITED__) return;
window.__DGUT_SINGLE_FILE_INITED__ = true;
initHost();
return;
}
bootstrapReader();
// --- 书目标识 & 持久化 ---
function getActiveSectionName() {
const activePage = document.querySelector('.page-name.active');
if (!activePage) return '';
const sectionItem = activePage.closest('.section-item');
if (!sectionItem) return '';
const nameEl = sectionItem.querySelector('.section-name .text');
return nameEl ? trimName(nameEl.textContent) : '';
}
function getBookKey() {
const name = getActiveSectionName();
return name ? (cachedCourseId ? `${cachedCourseId}|${name}` : name) : (cachedCourseId || location.href);
}
function loadRecords() { return GM_getValue(RECORDS_KEY, {}); }
function saveRecords(r) { GM_setValue(RECORDS_KEY, r); }
function getBookTime(k) { return Math.max(0, parseInt(loadRecords()[k], 10) || 0); }
function setBookTime(k, s) { const r = loadRecords(); r[k] = Math.max(0, Math.floor(s)); saveRecords(r); }
// --- 常用工具 ---
function readCfg() {
const r = GM_getValue(KEY, D);
if (!r || typeof r !== 'object') return { ...D };
const sec = parseInt(r.readerSec, 10);
return {
posX: parseInt(r.posX,10)||D.posX,
posY: parseInt(r.posY,10)||D.posY,
readerSec: sec > 0 ? sec : D.readerSec,
readerAutoStart: r.readerAutoStart !== false
};
}
function getActivePageName() {
const activePage = document.querySelector('.page-name.active');
if (!activePage) return '';
const textEl = activePage.querySelector('.text span') || activePage.querySelector('.text');
return textEl ? trimName(textEl.textContent) : '';
}
function getBookDisplayName() {
const section = getActiveSectionName();
const page = getActivePageName();
if (!section && !page) return '未识别';
return page ? section + ' - ' + page : section;
}
function koUnwrap(val) { return typeof val === 'function' ? val() : val; }
function trimName(s) { return (s || '').replace(/\s+/g, ' ').trim(); }
function getServerSideBookTimes() {
const vm = PAGE_WIN.koLearnCourseViewModel;
if (!vm) return null;
const course = koUnwrap(vm.course);
if (!course) return null;
const chapters = koUnwrap(course.chapters);
if (!chapters) return null;
const result = {};
chapters.forEach(function(chapter) {
const sections = koUnwrap(chapter.sections);
if (!sections) return;
sections.forEach(function(section) {
let name = '';
try { name = trimName(koUnwrap(section.name)); } catch(e) {}
if (!name) return;
const key = cachedCourseId ? cachedCourseId + '|' + name : name;
let total = 0;
try {
const secRec = koUnwrap(section.record);
if (secRec && secRec.sectionStudyTime !== undefined) {
total = koUnwrap(secRec.sectionStudyTime) || 0;
}
} catch(e) {}
if (total === 0) {
const pages = koUnwrap(section.pages);
if (pages) {
pages.forEach(function(page) {
try {
const record = koUnwrap(page.record);
if (record) {
total += (koUnwrap(record.studyTime) || 0) + (koUnwrap(record.lastStudyTime) || 0);
}
} catch(e) {}
});
}
}
if (total > 0) result[key] = total;
});
});
return result;
}
function syncServerTime(bookKey, accumulated, logFn) {
const serverTimes = getServerSideBookTimes();
if (!serverTimes) return accumulated;
const records = loadRecords();
let updated = false;
Object.keys(serverTimes).forEach(function(key) {
const serverSec = serverTimes[key];
const localSec = records[key] || 0;
if (serverSec > localSec) {
records[key] = serverSec;
updated = true;
if (logFn) logFn('服务端同步:' + key + ' ' + fmt(localSec) + ' → ' + fmt(serverSec));
}
});
if (updated) saveRecords(records);
if (bookKey && serverTimes[bookKey] && serverTimes[bookKey] > accumulated) {
accumulated = serverTimes[bookKey];
}
return accumulated;
}
function fmt(t) {
t = Math.max(0, t);
return `${String(Math.floor(t/3600)).padStart(2,'0')}:${String(Math.floor(t%3600/60)).padStart(2,'0')}:${String(t%60).padStart(2,'0')}`;
}
// --- 主页面(课程页) ---
function initHost() {
let bookKey = getBookKey();
let accumulated = getBookTime(bookKey);
let sessionStart = Date.now();
let lastSave = accumulated;
let timer = null;
let drag = false, dx, dy;
const cfg = readCfg();
let currentPageId = null;
let pageStartTime = Date.now();
let cachedFlatList = [];
let flatListDirty = true;
let holdPageId = null;
function getFlatList(vm) {
if (flatListDirty) {
cachedFlatList = buildFlatPageList(vm);
flatListDirty = false;
}
return cachedFlatList;
}
function getCurrentPageIdFromVM(vm) {
try { return pageId(vm.currentPage?.()); } catch(e) { return null; }
}
function pageId(page) {
if (!page) return null;
return typeof page.id === 'function' ? page.id() : page.id;
}
function isPageComplete(page) {
try {
const record = koUnwrap(page.record);
return record ? !!koUnwrap(record.status) : false;
} catch(e) { return false; }
}
function getMoveLabel(fromItem, toItem) {
if (!fromItem || !toItem) return '切换';
if (pageId(fromItem.chapter) !== pageId(toItem.chapter)) return '切换下一章';
if (pageId(fromItem.section) !== pageId(toItem.section)) return '切换下一本书';
return '切换下一节';
}
function formatMoveTarget(item) {
if (!item) return '(未知)';
const chapterName = trimName(koUnwrap(item.chapter && item.chapter.name));
const sectionName = trimName(koUnwrap(item.section && item.section.name));
const pageName = trimName(koUnwrap(item.page && item.page.name));
if (chapterName && sectionName) return chapterName + ' / ' + sectionName + ' - ' + pageName;
if (sectionName) return sectionName + ' - ' + pageName;
return pageName || '(未知)';
}
document.head.appendChild(Object.assign(document.createElement('style'), {
/* !important 在 #dgut-log-container / .dgut-log-line 的 padding 上,
用于抵御宿主页面 CSS reset 导致日志圆点压时间戳的 bug — 勿删勿改。 */
textContent: '#dgut-single-helper-panel{box-sizing:border-box;background:rgba(28,28,30,.82);backdrop-filter:blur(28px);-webkit-backdrop-filter:blur(28px);color:#e5e5e7;padding:0;border-radius:16px;position:fixed;z-index:100000;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",Arial,sans-serif;width:300px;max-width:calc(100vw - 32px);box-shadow:0 12px 40px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.08);user-select:none;overflow:hidden}#dgut-single-helper-panel *{box-sizing:border-box}#dgut-drag-handle{cursor:move;padding:11px 14px;background:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.02));border-bottom:1px solid rgba(255,255,255,.07);display:flex;align-items:center;justify-content:space-between;gap:8px}#dgut-title-wrap{display:flex;align-items:center;gap:7px;min-width:0}#dgut-brand-dot{width:7px;height:7px;border-radius:50%;background:linear-gradient(135deg,#0a84ff,#34c759);box-shadow:0 0 7px rgba(52,199,89,.6);flex-shrink:0}#dgut-drag-handle h4{margin:0;background:linear-gradient(90deg,#0a84ff,#34c759);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:12px;font-weight:800;letter-spacing:.3px;white-space:nowrap}#dgut-collapse-btn{cursor:pointer;font-size:14px;font-weight:700;color:rgba(255,255,255,.4);width:20px;height:20px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .15s,color .15s}#dgut-collapse-btn:hover{background:rgba(255,255,255,.1);color:#fff}#dgut-single-helper-panel.collapsed{width:210px}#dgut-single-helper-panel.collapsed .dgut-panel-body>:not(#timer-display):not(#progress-bar-wrap):not(#btn-pause-wrap):not(#meter-row){display:none}#dgut-single-helper-panel.collapsed .dgut-panel-body{padding:12px}#dgut-single-helper-panel.collapsed #timer-display{font-size:28px;margin:0 0 6px}#dgut-single-helper-panel.collapsed #meter-row{margin-bottom:6px}#dgut-single-helper-panel.collapsed #btn-pause-wrap{margin-bottom:0;margin-top:8px}#dgut-single-helper-panel.collapsed .dgut-btn-primary{height:30px;font-size:12px}.dgut-panel-body{padding:16px}#timer-display{font-weight:800;color:#fff;font-size:36px;font-variant-numeric:tabular-nums;letter-spacing:2px;line-height:1;text-align:center;margin:4px 0 9px;text-shadow:0 2px 16px rgba(10,132,255,.35)}#meter-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 7px;padding:0 1px}#reader-status{display:flex;align-items:center;gap:6px;font-size:11px;color:rgba(255,255,255,.6);min-width:0}#status-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#status-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:#ffcc80;box-shadow:0 0 6px rgba(255,204,128,.5)}#status-dot.active{background:#34c759;box-shadow:0 0 6px #34c759;animation:dgut-pulse 2s infinite}@keyframes dgut-pulse{0%,100%{opacity:1}50%{opacity:.5}}#progress-text{font-size:11px;font-weight:700;color:rgba(255,255,255,.5);letter-spacing:.3px;flex-shrink:0;font-variant-numeric:tabular-nums}#progress-bar-wrap{position:relative;height:6px;background:rgba(255,255,255,.08);border-radius:99px;margin:0 0 14px;overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.35)}#progress-bar-fill{height:100%;background:linear-gradient(90deg,#0a84ff,#34c759);border-radius:99px;transition:width .6s cubic-bezier(.34,1.56,.64,1);box-shadow:0 0 10px rgba(52,199,89,.45)}#book-name-display{font-size:11px;color:rgba(255,255,255,.62);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;margin-bottom:14px;padding:6px 10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:8px}#btn-pause-wrap{margin-bottom:14px}.dgut-btn{border:none;border-radius:10px;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;outline:0}.dgut-btn:hover{filter:brightness(1.08)}.dgut-btn:active{transform:scale(.97)}.dgut-btn-primary{color:#fff;width:100%;height:38px;font-size:14px;font-weight:700;letter-spacing:.5px;background:linear-gradient(135deg,#0a84ff,#34c759);box-shadow:0 6px 16px rgba(10,132,255,.3)}.dgut-btn-ghost{color:rgba(255,255,255,.85);background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);border-radius:8px;height:28px;padding:0 14px;font-size:11px;font-weight:500}.dgut-btn-ghost:hover{background:rgba(255,255,255,.13);border-color:rgba(255,255,255,.2)}#auto-row,#server-row{display:flex;align-items:center;gap:8px;font-size:11px;color:rgba(255,255,255,.5)}#auto-row{margin-bottom:10px}#server-row{margin-bottom:12px}#server-row #server-time-display{flex:1;min-width:0}#auto-row .row-label{color:rgba(255,255,255,.45);font-size:11px;flex-shrink:0}#auto-row .row-unit{font-size:11px;color:rgba(255,255,255,.4);margin-right:auto}#reader-sec{width:46px;height:28px;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;text-align:center;padding:3px;font-size:12px;font-variant-numeric:tabular-nums;outline:0;transition:border-color .15s}#reader-sec:focus{border-color:#007aff}#dgut-log-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer;margin-bottom:8px;padding-top:12px;border-top:1px solid rgba(255,255,255,.07)}#dgut-log-title{font-size:10px;color:rgba(255,255,255,.42);text-transform:uppercase;letter-spacing:.8px;font-weight:700}#dgut-log-toggle{font-size:9px;color:rgba(255,255,255,.3)}#dgut-log-container{height:96px;overflow-y:auto;overflow-x:hidden;background:rgba(0,0,0,.28);border-radius:8px;padding:6px!important;font-size:10px;line-height:1.5;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.15) transparent;transition:height .25s ease,opacity .25s ease,padding .25s ease}#dgut-log-container.collapsed{height:0;padding:0;opacity:0;overflow:hidden}#dgut-log-container::-webkit-scrollbar{width:5px}#dgut-log-container::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}#dgut-log-container::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)}.dgut-log-line{position:relative;display:flex;gap:7px;align-items:flex-start;padding:2px 6px 2px 16px!important;border-radius:5px;line-height:1.5;color:rgba(255,255,255,.62);transition:background .12s}.dgut-log-line+.dgut-log-line{margin-top:1px}.dgut-log-line:hover{background:rgba(255,255,255,.05)}.dgut-log-line::before{content:"";position:absolute;left:6px;top:7px;width:5px;height:5px;border-radius:50%;background:rgba(255,255,255,.22)}.dgut-log-line.ok::before{background:#34c759;box-shadow:0 0 4px rgba(52,199,89,.5)}.dgut-log-line.ok .dgut-log-msg{color:#7ee2a0}.dgut-log-line.nav::before{background:#0a84ff;box-shadow:0 0 4px rgba(10,132,255,.5)}.dgut-log-line.nav .dgut-log-msg{color:#74bbff}.dgut-log-line.warn::before{background:#ff9f0a;box-shadow:0 0 4px rgba(255,159,10,.5)}.dgut-log-line.warn .dgut-log-msg{color:#ffc673}.dgut-log-time{flex-shrink:0;color:rgba(255,255,255,.3);font-family:Consolas,"SF Mono",monospace;font-variant-numeric:tabular-nums;letter-spacing:-.2px}.dgut-log-msg{flex:1;min-width:0;word-break:break-word;overflow-wrap:anywhere}'
}));
const p = Object.assign(document.createElement('div'), {
id: 'dgut-single-helper-panel',
style: `top:${cfg.posY}px;right:${cfg.posX}px`
});
p.innerHTML = `
${fmt(accumulated)}
就绪
0%
${getBookDisplayName()}
间隔
秒/页
服务端: --
`;
document.body.appendChild(p);
const td = document.getElementById('timer-display');
const bd = document.getElementById('book-name-display');
const sd = document.getElementById('server-time-display');
const pb = document.getElementById('btn-pause');
const sb = document.getElementById('btn-sync-server');
let lastServerSync = 0;
const SYNC_INTERVAL = 300;
function status(t, c) {
const st = document.getElementById('status-text');
const dot = document.getElementById('status-dot');
if (st) st.textContent = t;
if (dot) { dot.style.background = c || '#ffcc80'; dot.className = c === '#34c759' ? 'active' : ''; }
}
function logType(msg) {
if (/失败|错误|出错|未收到|无法|异常|未满/.test(msg)) return 'warn';
if (/已满4h|同步|存档|读完|启动|握手|连接|最新|已设为/.test(msg)) return 'ok';
if (/翻页|切换|下一页|下一节|下一章|下一本|回到第一页|停留|留在/.test(msg)) return 'nav';
return '';
}
function log(msg) {
const c = document.getElementById('dgut-log-container');
if(!c) return;
const n = new Date();
const t = `${String(n.getHours()).padStart(2,'0')}:${String(n.getMinutes()).padStart(2,'0')}:${String(n.getSeconds()).padStart(2,'0')}`;
const cls = logType(msg);
const l = document.createElement('div');
l.className = 'dgut-log-line' + (cls ? ' ' + cls : '');
l.innerHTML = `${t}${msg}`;
c.appendChild(l);
c.scrollTop = c.scrollHeight;
while(c.children.length > 100) c.removeChild(c.firstChild);
}
function saveCfg() { GM_setValue(KEY, cfg); }
function updateTimerDisplay(sec) {
td.textContent = fmt(sec);
const pct = Math.min(100, Math.floor(sec / MIN_BOOK_SEC * 100));
const bar = document.getElementById('progress-bar-fill');
const pctEl = document.getElementById('progress-text');
if (bar) bar.style.width = pct + '%';
if (pctEl) pctEl.textContent = pct + '%';
}
function getTotal() { return accumulated + Math.floor((Date.now() - sessionStart) / 1000); }
function persist() { const t = getTotal(); setBookTime(bookKey, t); lastSave = t; log('存档:' + fmt(t)); }
function syncReader(targetWin) {
saveCfg();
const payload = {
type: MSG,
scope: READER_SCOPE,
intervalSec: cfg.readerSec,
autoStart: cfg.readerAutoStart
};
let n = 0;
if (targetWin) {
try {
targetWin.postMessage(payload, '*');
n = 1;
} catch(e) {}
} else {
Array.from(document.querySelectorAll('iframe')).forEach(f => {
try {
if(f.contentWindow){
f.contentWindow.postMessage(payload, '*');
n++;
}
} catch(e) {}
});
}
let txt;
if (n > 0) {
txt = cfg.readerAutoStart ? '阅读中' : '已暂停';
} else {
txt = '等待阅读器连接';
}
status(txt, n > 0 ? '#34c759' : '#ffcc80');
}
function saveReader() {
const s = parseInt(document.getElementById('reader-sec').value,10);
if (s > 0) { cfg.readerSec = s; syncReader(); log('自动操作间隔已设为 ' + s + ' 秒/页'); }
else alert('请输入大于 0 的数字');
}
function solveModal() {
const b1 = document.querySelector('button.btn-submit');
if(b1&&b1.offsetParent!==null) { b1.click(); log('自动关闭弹窗 (btn-submit)'); }
const b2 = document.querySelector('#alertModal .btn-submit, .modal.fade.in .btn-hollow, .modal.in .btn-primary');
if(b2) {
const r = b2.getBoundingClientRect();
if (r.width > 0 || r.height > 0) { b2.click(); log('自动关闭弹窗 (modal)'); }
}
}
function solveChapterModal() {
const modal = document.querySelector('.stat-page.chapter-stat');
if (!modal) return false;
const rect = modal.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return false;
const btns = modal.querySelectorAll('.stat-next .btn-hollow');
if (btns.length === 0) return false;
if (getTotal() < MIN_BOOK_SEC) {
btns[0].click();
pageStartTime = Date.now();
log('弹窗:未满4h,留在本章');
} else if (btns.length > 1) {
btns[1].click();
currentPageId = null;
pageStartTime = Date.now();
log('弹窗:已满4h,切换下一章');
} else {
btns[0].click();
pageStartTime = Date.now();
log('弹窗:已满4h,已是最后一章,留在本章');
}
return true;
}
let antiDetectTimer = null;
function setupAntiDetect() {
antiDetectTimer = setInterval(() => {
document.dispatchEvent(new MouseEvent('mousemove', {
clientX: 100 + Math.random()*500, clientY: 100 + Math.random()*300, bubbles: false
}));
}, 30000);
}
function tick() {
solveModal();
const modalHandled = solveChapterModal();
const currentKey = getBookKey();
if (currentKey !== bookKey) {
setBookTime(bookKey, getTotal());
bookKey = currentKey;
accumulated = getBookTime(bookKey);
sessionStart = Date.now();
lastSave = accumulated;
flatListDirty = true;
holdPageId = null;
const dispName = getBookDisplayName();
bd.textContent = dispName;
bd.title = bookKey;
refreshServerDisplay();
log('当前书目:' + dispName);
}
const vm = PAGE_WIN.koLearnCourseViewModel;
if(!modalHandled && vm && vm.currentPage && cfg.readerAutoStart) {
const flatList = getFlatList(vm);
const page = vm.currentPage();
const pId = pageId(page);
if(pId && pId !== currentPageId) {
const prevPid = currentPageId;
const prevIdx = prevPid ? findPageIndex(flatList, prevPid) : -1;
const currentIdx = findPageIndex(flatList, pId);
const prevItem = prevIdx >= 0 ? flatList[prevIdx] : null;
const currentItem = currentIdx >= 0 ? flatList[currentIdx] : null;
currentPageId = pId;
pageStartTime = Date.now();
const moveLabel = getMoveLabel(prevItem, currentItem);
log(moveLabel + ':' + formatMoveTarget(currentItem));
const dispName = getBookDisplayName();
bd.textContent = dispName;
bd.title = bookKey;
}
if (getTotal() >= MIN_BOOK_SEC) {
if(currentPageId && Date.now() - pageStartTime >= NAV_SEC * 1000) {
const next = vm.nextPageName?.();
const currentIdx = findPageIndex(flatList, currentPageId);
const currentItem = currentIdx >= 0 ? flatList[currentIdx] : null;
const nextFlatItem = currentIdx >= 0 && currentIdx < flatList.length - 1 ? flatList[currentIdx + 1] : null;
const isNoMore = !next || next === vm.i18nMessageText?.()?.noMore;
const crossesSection = !!(currentItem && nextFlatItem && pageId(currentItem.section) !== pageId(nextFlatItem.section));
const nextMoveLabel = getMoveLabel(currentItem, nextFlatItem);
const isChapterEnd = isNoMore || (next && next.includes('统计')) || crossesSection;
if (isChapterEnd) {
const result = advanceToNextSection(vm, currentPageId);
if (result.success) {
holdPageId = null;
currentPageId = null;
pageStartTime = Date.now();
log('已满4h,' + result.moveLabel + ':' + result.targetText);
setTimeout(function() { syncReader(); }, 1500);
} else if (result.atEnd) {
stopReading();
log('全部书目已读完');
}
} else {
vm.goNextPage();
pageStartTime = Date.now();
log(nextMoveLabel + ':' + formatMoveTarget(nextFlatItem));
}
}
} else {
ensureHoldPage(vm, flatList);
}
}
const total = getTotal();
updateTimerDisplay(total);
if (total - lastSave >= SAVE_INTERVAL) persist();
if (Date.now() - lastServerSync >= SYNC_INTERVAL * 1000) {
const newAcc = syncServerTime(bookKey, accumulated, log);
if (newAcc !== accumulated) { sessionStart = Date.now(); lastSave = newAcc; }
accumulated = newAcc;
lastServerSync = Date.now();
refreshServerDisplay();
}
}
function startReading() {
if (timer) return;
sessionStart = Date.now();
timer = setInterval(tick, 1000);
pb.textContent = '暂停';
pb.style.background = 'linear-gradient(135deg, #475569, #64748b)';
pb.style.boxShadow = '0 4px 12px rgba(71,85,105,.25)';
cfg.readerAutoStart = true;
const vm = PAGE_WIN.koLearnCourseViewModel;
if(vm && vm.currentPage) {
const p = vm.currentPage();
currentPageId = pageId(p);
pageStartTime = Date.now();
}
syncReader();
log('开始阅读');
}
function stopReading() {
if (!timer) return;
clearInterval(timer);
timer = null;
persist();
pb.textContent = '开始';
pb.style.background = '';
pb.style.boxShadow = '';
cfg.readerAutoStart = false;
syncReader();
log('暂停阅读');
}
function refreshServerDisplay() {
try {
const st = getServerSideBookTimes();
if (st && st[bookKey]) {
sd.textContent = '服务端: ' + fmt(st[bookKey]);
return true;
} else if (st) {
sd.textContent = '服务端: 暂无记录';
return true;
} else {
sd.textContent = '服务端: 获取失败';
return false;
}
} catch(e) {
console.error('[DGUT Reader] refreshServerDisplay error:', e);
sd.textContent = '服务端: 出错';
return false;
}
}
function doSync() {
refreshServerDisplay();
const before = accumulated;
accumulated = syncServerTime(bookKey, accumulated, log);
sessionStart = Date.now();
lastSave = accumulated;
lastServerSync = Date.now();
updateTimerDisplay(accumulated);
refreshServerDisplay();
if (accumulated > before) {
log('已从服务端同步,当前累计: ' + fmt(accumulated));
} else {
log('服务端数据已是最新');
}
}
function buildFlatPageList(vm) {
const course = koUnwrap(vm.course);
const chapters = koUnwrap(course.chapters);
if (!chapters) return [];
const flatList = [];
chapters.forEach(function(chapter) {
const sections = koUnwrap(chapter.sections);
if (!sections) return;
sections.forEach(function(section) {
if (koUnwrap(section.isHide)) return;
const pages = koUnwrap(section.pages);
if (!pages) return;
pages.forEach(function(page) {
flatList.push({ page: page, section: section, chapter: chapter });
});
});
});
return flatList;
}
function findPageIndex(flatList, pid) {
return flatList.findIndex(function(item) {
const id = pageId(item.page);
return String(id) === String(pid);
});
}
function advanceToNextSection(vm, currentPageId) {
const flatList = getFlatList(vm);
const currentIdx = findPageIndex(flatList, currentPageId);
if (currentIdx >= 0 && currentIdx < flatList.length - 1) {
const current = flatList[currentIdx];
const next = flatList[currentIdx + 1];
vm.selectPage(next.page, next.section, next.chapter);
return {
success: true,
moveLabel: getMoveLabel(current, next),
targetText: formatMoveTarget(next)
};
}
return { success: false, atEnd: flatList.length > 0 };
}
function ensureHoldPage(vm, flatList) {
if (!currentPageId) return;
const currentIdx = findPageIndex(flatList, currentPageId);
if (currentIdx < 0) return;
const currentItem = flatList[currentIdx];
const secId = pageId(currentItem.section);
const sameSectionItems = flatList.filter(function(item) {
return pageId(item.section) === secId;
});
if (sameSectionItems.length === 0) return;
if (!isPageComplete(currentItem.page)) {
if (holdPageId !== currentPageId) {
holdPageId = currentPageId;
log('未满4h,停留在当前节刷时长:' + trimName(koUnwrap(currentItem.page.name)));
}
return;
}
let target = sameSectionItems.find(function(item) {
return !isPageComplete(item.page);
});
if (!target) target = sameSectionItems[sameSectionItems.length - 1];
const targetPid = pageId(target.page);
if (targetPid === currentPageId) {
holdPageId = currentPageId;
return;
}
if (holdPageId === targetPid) return;
vm.selectPage(target.page, target.section, target.chapter);
holdPageId = targetPid;
currentPageId = null;
pageStartTime = Date.now();
log('未满4h,当前节已完成,切换并停留:' + trimName(koUnwrap(target.page.name)));
setTimeout(function() { syncReader(); }, 1500);
}
const onDragMove = e => {
const l = e.clientX-dx, t = e.clientY-dy;
Object.assign(p.style, { left: l+'px', right: 'auto', top: t+'px' });
cfg.posX = Math.max(0, window.innerWidth-(l+p.offsetWidth));
cfg.posY = Math.max(0, t);
};
const onDragUp = () => {
drag = false;
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
saveCfg();
};
document.getElementById('dgut-drag-handle').addEventListener('mousedown', e => {
drag = true; dx = e.clientX-p.offsetLeft; dy = e.clientY-p.offsetTop;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
document.getElementById('reader-sec').addEventListener('keydown', e => { if(e.key==='Enter') saveReader(); });
document.getElementById('btn-apply-reader').addEventListener('click', saveReader);
pb.addEventListener('click', () => { timer ? stopReading() : startReading(); });
sb.addEventListener('click', doSync);
const collapseBtn = document.getElementById('dgut-collapse-btn');
collapseBtn.addEventListener('mousedown', e => e.stopPropagation());
collapseBtn.addEventListener('click', e => {
e.stopPropagation();
const collapsed = p.classList.toggle('collapsed');
collapseBtn.textContent = collapsed ? '+' : '−';
});
document.getElementById('dgut-log-header').addEventListener('click', function() {
const c = document.getElementById('dgut-log-container');
const t = document.getElementById('dgut-log-toggle');
if (c.classList.contains('collapsed')) {
c.classList.remove('collapsed');
t.textContent = '▼';
} else {
c.classList.add('collapsed');
t.textContent = '▶';
}
});
updateTimerDisplay(accumulated);
log('DGUT 阅读助手已启动');
log('当前书目:' + getBookDisplayName());
if(cfg.readerAutoStart) {
startReading();
} else {
sessionStart = Date.now();
pb.textContent = '开始';
}
syncReader();
window.addEventListener('message', function(e) {
if (!e.data) return;
if (e.data.type === 'DGUT_LOG') {
log('[iframe] ' + e.data.text);
return;
}
if (e.data.type === READY_MSG) {
syncReader(e.source);
log('[iframe] 阅读器已握手');
}
});
setupAntiDetect();
let startupSyncRetries = 0;
function startupSync() {
const newAcc = syncServerTime(bookKey, accumulated, null);
if (newAcc !== accumulated || startupSyncRetries === 0) {
if (newAcc !== accumulated) { sessionStart = Date.now(); lastSave = newAcc; }
accumulated = newAcc;
lastServerSync = Date.now();
updateTimerDisplay(accumulated);
}
const hasServerData = refreshServerDisplay();
if (!hasServerData && startupSyncRetries < 10) {
startupSyncRetries++;
setTimeout(startupSync, 3000);
} else if (hasServerData) {
log('服务端时长已同步');
}
}
setTimeout(startupSync, 3000);
window.addEventListener('load', () => { setTimeout(syncReader,1200); setTimeout(syncReader,2600); }, { once: true });
}
// --- 阅读器 iframe 页 ---
function initReader() {
if(window.__DGUT_SINGLE_FILE_READER_INITED__) return;
window.__DGUT_SINGLE_FILE_READER_INITED__ = true;
let timer = null, state, lastFlipLogAt = 0, lastLoopBack = 0, synced = false, readyTimer = null;
const LOG = 'DGUT_LOG';
function rlog(msg) {
console.log('[DGUT Reader]', msg);
try { window.parent.postMessage({ type: LOG, text: msg }, '*'); } catch(e) {}
}
function clickNext() {
const b = document.querySelector('#nextBtn');
if(!b || b.style.display === 'none' || b.disabled) return;
const pageIdxEl = document.getElementById('pageIndex');
const pageCntEl = document.getElementById('pageCount');
if (pageIdxEl && pageCntEl) {
const cur = parseInt(pageIdxEl.innerHTML, 10);
const total = parseInt(pageCntEl.innerHTML, 10);
if (total > 0 && cur >= total) {
const now = Date.now();
if (now - lastLoopBack < 3000) return;
lastLoopBack = now;
try {
const pw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (typeof pw.goPage === 'function') { pw.goPage(0); }
else if (typeof goPage === 'function') { goPage(0); }
else {
const s = document.createElement('script');
s.textContent = 'goPage(0);';
document.head.appendChild(s);
setTimeout(function() { if (s.parentNode) s.parentNode.removeChild(s); }, 100);
}
lastFlipLogAt = 0;
rlog('已至末页,回到第一页循环');
return;
} catch(e) { rlog('回到第一页失败:' + e.message); }
}
}
b.click();
const now = Date.now();
if (now - lastFlipLogAt >= 60000) {
lastFlipLogAt = now;
rlog('翻页');
}
}
function stop() { if(!timer) return; clearInterval(timer); timer = null; rlog('翻页定时器已停止'); }
function start(s) { if(timer) return; timer = setInterval(clickNext, s*1000); rlog('翻页定时器已启动:' + s + '秒/页'); }
function apply(s, auto) { stop(); if(auto!==false) start(s); }
function stopReadyPing() {
if (!readyTimer) return;
clearInterval(readyTimer);
readyTimer = null;
}
function notifyReady() {
if (window.parent === window) return;
try {
window.parent.postMessage({ type: READY_MSG, scope: READER_SCOPE }, '*');
} catch(e) {}
}
window.addEventListener('message', e => {
const d = e.data;
if(!d || d.type !== MSG || d.scope !== READER_SCOPE || e.origin !== HOST_ORIGIN) return;
synced = true;
stopReadyPing();
state = readCfg();
const sec = parseInt(d.intervalSec, 10);
state.readerSec = sec > 0 ? sec : D.readerSec;
state.readerAutoStart = d.autoStart !== false;
GM_setValue(KEY, state);
rlog('收到同步:' + sec + '秒/页,' + (state.readerAutoStart ? '自动' : '暂停'));
apply(state.readerSec, state.readerAutoStart);
});
function tryHandshake() {
if (window.parent === window || synced) return;
notifyReady();
if (readyTimer) return;
let tries = 0;
readyTimer = setInterval(function() {
if (synced || window.parent === window) {
stopReadyPing();
return;
}
tries++;
notifyReady();
if (tries >= 20) {
stopReadyPing();
rlog('未收到宿主同步,当前阅读器不启用自动翻页');
}
}, 1000);
}
if (document.readyState === 'complete') {
setTimeout(tryHandshake, 500);
} else {
window.addEventListener('load', () => setTimeout(tryHandshake, 1200), { once: true });
}
}
function bootstrapReader() {
if (window.top === window.self) return;
const init = () => document.querySelector('#nextBtn') ? (initReader(), true) : false;
if(init()) { console.log('[DGUT Reader] 阅读器已连接'); return; }
let n = 0, t = setInterval(() => { n++; if(init()||n>=20) clearInterval(t); }, 500);
window.addEventListener('load', () => setTimeout(init, 1200), { once: true });
}
})();