// ==UserScript== // @name Manebi Learning Helper // @name:en Manebi Learning Helper // @name:ja Manebi ラーニング補助ツール // @name:zh-CN Manebi 学习助手 // @namespace https://github.com/CheerChen // @version 1.1.7 // @description Removes video playback restrictions and automatically browses unfinished PDF lessons on manebi-learning.com. // @description:en Removes video playback restrictions and automatically browses unfinished PDF lessons on manebi-learning.com. // @description:ja manebi-learning.comの動画再生制限を解除し、未完了のPDFレッスンを自動閲覧します。 // @description:zh-CN 解除 manebi-learning.com 的视频播放限制,并自动浏览课程中尚未完成的 PDF 课件。 // @author anonymous // @match *://*.manebi-learning.com/* // @match *://manebi-learning.com/* // @icon https://www.google.com/s2/favicons?domain=manebi.co.jp // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; const language = (navigator.language || 'en').toLowerCase(); const locale = language.startsWith('zh') ? 'zh' : language.startsWith('ja') ? 'ja' : 'en'; const text = { en: { pdfAuto: 'PDF Auto', ready: 'Ready', runRemaining: 'Run remaining', resume: 'Resume', pause: 'Pause', paused: (page, pages) => pages ? `Paused — ${page}/${pages} pages viewed` : 'Paused', readingList: 'Reading course PDF list…', listNotFound: 'PDF lesson list not found', allCompleted: (total) => `All ${total} PDF lessons completed`, opening: (title) => `Opening ${title}…`, couldNotOpen: (title) => `Could not open ${title}`, pagesNotFound: (title) => `PDF pages not found: ${title}`, pageControlsChanged: 'PDF page controls changed', progress: (index, total, page, pages) => `[${index}/${total}] Page ${page}/${pages}`, waitingCompletion: (title) => `Waiting for completion: ${title}`, completionNotFound: (title) => `Completion mark not found: ${title}`, speedControl: 'Speed Control', skipToEnd: 'Skip to End', }, ja: { pdfAuto: 'PDF自動閲覧', ready: '準備完了', runRemaining: '未完了分を閲覧', resume: '再開', pause: '一時停止', paused: (page, pages) => pages ? `一時停止:${page}/${pages}ページ閲覧済み` : '一時停止しました', readingList: 'PDFレッスン一覧を読み込み中…', listNotFound: 'PDFレッスン一覧が見つかりません', allCompleted: (total) => `PDFレッスン${total}件はすべて完了しています`, opening: (title) => `${title}を開いています…`, couldNotOpen: (title) => `${title}を開けませんでした`, pagesNotFound: (title) => `${title}のページが見つかりません`, pageControlsChanged: 'PDFのページ操作ボタンが変更されました', progress: (index, total, page, pages) => `[${index}/${total}] ページ ${page}/${pages}`, waitingCompletion: (title) => `${title}の完了を確認中…`, completionNotFound: (title) => `${title}の完了表示を確認できませんでした`, speedControl: '再生速度', skipToEnd: '末尾へ移動', }, zh: { pdfAuto: 'PDF 自动浏览', ready: '就绪', runRemaining: '浏览未完成项', resume: '继续', pause: '暂停', paused: (page, pages) => (pages ? `已暂停,已浏览 ${page}/${pages} 页` : '已暂停'), readingList: '正在读取 PDF 课程列表…', listNotFound: '未找到 PDF 课程列表', allCompleted: (total) => `全部 ${total} 个 PDF 课程已完成`, opening: (title) => `正在打开「${title}」…`, couldNotOpen: (title) => `无法打开「${title}」`, pagesNotFound: (title) => `未找到「${title}」的 PDF 页面`, pageControlsChanged: 'PDF 页码控件已发生变化', progress: (index, total, page, pages) => `[${index}/${total}] 第 ${page}/${pages} 页`, waitingCompletion: (title) => `正在等待「${title}」完成…`, completionNotFound: (title) => `未检测到「${title}」的完成标记`, speedControl: '播放速度', skipToEnd: '跳至结尾', }, }[locale]; // ========================================================================= // 1. Override Page Visibility API — prevent background tab detection // ========================================================================= Object.defineProperty(document, 'hidden', { get: () => false, configurable: true, }); Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true, }); // Block all visibilitychange events before they reach the site's listeners document.addEventListener( 'visibilitychange', (e) => { e.stopImmediatePropagation(); e.preventDefault(); }, true ); // Also block on window level (some sites listen here) window.addEventListener( 'visibilitychange', (e) => { e.stopImmediatePropagation(); e.preventDefault(); }, true ); // Block blur/focus events that some sites use as a fallback window.addEventListener('blur', (e) => e.stopImmediatePropagation(), true); window.addEventListener('focus', (e) => e.stopImmediatePropagation(), true); // ========================================================================= // 2. Override hasFocus() — always return true // ========================================================================= Document.prototype.hasFocus = function () { return true; }; // ========================================================================= // 3. Auto-browse unfinished PDF lessons // ========================================================================= const PDF_AUTO_KEY = 'manebi-pdf-auto-running'; const PDF_PROGRESS_KEY = 'manebi-pdf-auto-progress'; const PDF_PAGE_INTERVAL = 1250; let pdfRunning = false; let pdfPauseRequested = false; let pdfResumeScheduled = false; let pdfController = null; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const lessonId = () => Number(new URL(location.href).searchParams.get('lessonId')); function readPdfProgress() { try { const progress = JSON.parse(sessionStorage.getItem(PDF_PROGRESS_KEY)); const savedLessonId = Number(progress?.lessonId); const nextPage = Number(progress?.nextPage); const totalPages = Number(progress?.totalPages); if ( !savedLessonId || !Number.isInteger(nextPage) || nextPage < 0 || !Number.isInteger(totalPages) || totalPages < 1 ) { return null; } return { lessonId: savedLessonId, nextPage, totalPages }; } catch { return null; } } function savePdfProgress(lesson, nextPage, totalPages) { sessionStorage.setItem( PDF_PROGRESS_KEY, JSON.stringify({ lessonId: lesson.id, nextPage, totalPages }) ); } function clearPdfProgress() { sessionStorage.removeItem(PDF_PROGRESS_KEY); } function isElementVisible(element) { if (!element?.isConnected) return false; for (let node = element; node; node = node.parentElement) { const style = node.ownerDocument.defaultView?.getComputedStyle(node); if ( node.hidden || node.getAttribute('aria-hidden') === 'true' || style?.display === 'none' || style?.visibility === 'hidden' ) { return false; } } const frame = element.ownerDocument.defaultView?.frameElement; return frame ? isElementVisible(frame) : true; } function pdfPageButtons() { const groups = new Map(); document.querySelectorAll('button[aria-label]').forEach((button) => { const label = button.getAttribute('aria-label') || ''; if ( !isElementVisible(button) || !button.closest('ul') || (!/\/png\/\d+\.png(?:$|[?#])/i.test(label) && !(/^https?:/i.test(label) && !button.textContent.trim())) ) { return; } const list = button.closest('ul'); groups.set(list, [...(groups.get(list) || []), button]); }); return [...groups.values()].sort((a, b) => b.length - a.length)[0] || []; } function pdfLessons() { const lessons = new Map(); document .querySelectorAll('a[href*="/student/courses/"][href*="lessonId="]') .forEach((link) => { if (!link.querySelector('svg[aria-label="PDF"]')) return; const id = Number(new URL(link.href, location.href).searchParams.get('lessonId')); if (!id || lessons.has(id)) return; const item = link.closest('li'); lessons.set(id, { id, link, title: link.querySelector('b')?.textContent.trim() || `PDF ${id}`, done: Boolean( item?.querySelector( 'svg[aria-label="完了"], svg[data-testid="CheckCircleIcon"]' ) ), }); }); return [...lessons.values()]; } async function waitFor(getValue, timeout = 20000) { const started = Date.now(); while (!pdfPauseRequested && Date.now() - started < timeout) { const value = getValue(); if (value) return value; await sleep(250); } return null; } function setPdfStatus(message, busy = pdfRunning) { if (!pdfController) return; pdfController.status.textContent = message; pdfController.run.disabled = busy; pdfController.pause.disabled = !busy; } function updatePdfRunLabel() { if (!pdfController) return; pdfController.run.textContent = readPdfProgress() ? text.resume : text.runRemaining; } function nextIncomplete(lessons, currentId, completed) { const index = lessons.findIndex((lesson) => lesson.id === currentId); const ordered = index < 0 ? lessons : [...lessons.slice(index + 1), ...lessons.slice(0, index + 1)]; return ordered.find((lesson) => !lesson.done && !completed.has(lesson.id)); } async function autoBrowsePdfs() { if (pdfRunning) return; pdfRunning = true; pdfPauseRequested = false; sessionStorage.setItem(PDF_AUTO_KEY, '1'); setPdfStatus(text.readingList); const completed = new Set(); let paused = false; try { const initialLessons = await waitFor(() => pdfLessons().length && pdfLessons()); if (pdfPauseRequested) { paused = true; return; } if (!initialLessons) throw new Error(text.listNotFound); const total = initialLessons.length; while (!pdfPauseRequested) { const lessons = pdfLessons(); const currentId = lessonId(); const current = lessons.find((lesson) => lesson.id === currentId); let savedProgress = readPdfProgress(); const savedTarget = lessons.find( (lesson) => lesson.id === savedProgress?.lessonId ); if (savedProgress && (!savedTarget || savedTarget.done)) { clearPdfProgress(); savedProgress = null; } const target = savedProgress && savedTarget && !completed.has(savedTarget.id) ? savedTarget : current && !current.done && !completed.has(current.id) ? current : nextIncomplete(lessons, currentId, completed); if (!target) { clearPdfProgress(); setPdfStatus(text.allCompleted(total), false); return; } const targetIndex = lessons.findIndex((lesson) => lesson.id === target.id) + 1; if (currentId !== target.id) { setPdfStatus(text.opening(target.title)); target.link.click(); if (!(await waitFor(() => lessonId() === target.id))) { if (pdfPauseRequested) { paused = true; return; } throw new Error(text.couldNotOpen(target.title)); } await sleep(1500); } const pages = await waitFor(() => { const buttons = pdfPageButtons(); return buttons.length && buttons; }); if (pdfPauseRequested) { paused = true; return; } if (!pages) throw new Error(text.pagesNotFound(target.title)); const progress = readPdfProgress(); const startPage = progress?.lessonId === target.id ? Math.min(progress.nextPage, pages.length) : 0; if (progress?.lessonId !== target.id) { savePdfProgress(target, 0, pages.length); } for (let i = startPage; i < pages.length; i += 1) { if (pdfPauseRequested) { paused = true; return; } const buttons = pdfPageButtons(); if (!buttons[i]) throw new Error(text.pageControlsChanged); buttons[i].click(); savePdfProgress(target, i + 1, pages.length); setPdfStatus(text.progress(targetIndex, total, i + 1, pages.length)); await sleep(PDF_PAGE_INTERVAL); } if (pdfPauseRequested) { paused = true; return; } setPdfStatus(text.waitingCompletion(target.title)); const done = await waitFor( () => pdfLessons().find((lesson) => lesson.id === target.id)?.done, 15000 ); if (pdfPauseRequested) { paused = true; return; } if (!done) throw new Error(text.completionNotFound(target.title)); completed.add(target.id); clearPdfProgress(); await sleep(750); } } catch (error) { if (pdfPauseRequested) { paused = true; } else { setPdfStatus(error.message || String(error), false); } } finally { pdfRunning = false; sessionStorage.removeItem(PDF_AUTO_KEY); if (paused) { const progress = readPdfProgress(); setPdfStatus(text.paused(progress?.nextPage, progress?.totalPages), false); } pdfPauseRequested = false; if (pdfController) { pdfController.run.disabled = false; pdfController.pause.disabled = true; updatePdfRunLabel(); } scheduleControllerSync(); } } function pausePdfAutomation() { if (!pdfRunning) return; pdfPauseRequested = true; sessionStorage.removeItem(PDF_AUTO_KEY); const progress = readPdfProgress(); setPdfStatus(text.paused(progress?.nextPage, progress?.totalPages)); } function removePdfController() { if (!pdfController) return; pdfController.cleanup(); pdfController.box.remove(); pdfController.style.remove(); pdfController = null; } function createPdfController() { if (pdfController?.box.isConnected || !document.body) return; removePdfController(); const box = document.createElement('div'); box.id = 'manebi-pdf-auto'; const progress = readPdfProgress(); box.innerHTML = ` 📄 ${text.pdfAuto} ${progress ? text.paused(progress.nextPage, progress.totalPages) : text.ready}
`; const style = document.createElement('style'); style.textContent = ` #manebi-pdf-auto{position:fixed;right:20px;bottom:20px;z-index:999999;width:min(280px,calc(100vw - 40px));box-sizing:border-box;padding:10px 14px;border-radius:8px;background:rgba(0,0,0,.8);color:#fff;font:13px/1.4 system-ui,sans-serif;display:flex;flex-direction:column;gap:8px;box-shadow:0 4px 12px rgba(0,0,0,.3);user-select:none;cursor:move} #manebi-pdf-auto strong{font-size:12px;opacity:.7;text-align:center} #manebi-pdf-auto .status{min-height:17px;color:#cfd8e3} #manebi-pdf-auto>div{display:flex;gap:6px} #manebi-pdf-auto button{padding:6px 10px;border:0;border-radius:4px;cursor:pointer;font-size:12px} #manebi-pdf-auto .run{background:#4caf50;color:#fff} #manebi-pdf-auto .pause{background:#666;color:#fff} #manebi-pdf-auto button:disabled{cursor:not-allowed;opacity:.45} `; document.head.appendChild(style); document.body.appendChild(box); pdfController = { box, style, cleanup: makeDraggable(box), status: box.querySelector('.status'), run: box.querySelector('.run'), pause: box.querySelector('.pause'), }; pdfController.run.addEventListener('click', autoBrowsePdfs); pdfController.pause.addEventListener('click', pausePdfAutomation); if (sessionStorage.getItem(PDF_AUTO_KEY) && !pdfResumeScheduled) { pdfResumeScheduled = true; setTimeout(autoBrowsePdfs, 500); } } // ========================================================================= // 4. Speed control & seek unlock — inject after DOM is ready // ========================================================================= const SPEED_OPTIONS = [1, 1.5, 2, 3, 4, 8, 16]; let speedController = null; let controlledVideo = null; let controllerSyncTimer = null; let controllerPollTimer = null; function makeDraggable(container) { let isDragging = false; let offsetX; let offsetY; const onMouseDown = (event) => { if (event.target.tagName === 'BUTTON') return; isDragging = true; const rect = container.getBoundingClientRect(); offsetX = event.clientX - rect.left; offsetY = event.clientY - rect.top; }; const onMouseMove = (event) => { if (!isDragging) return; container.style.left = event.clientX - offsetX + 'px'; container.style.top = event.clientY - offsetY + 'px'; container.style.right = 'auto'; container.style.bottom = 'auto'; }; const onMouseUp = () => (isDragging = false); container.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); return () => { container.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; } function removeSpeedController() { if (!speedController) return; speedController.cleanup(); speedController.container.remove(); speedController = null; controlledVideo = null; } function createSpeedController(video) { if (controlledVideo === video && speedController?.container.isConnected) return; removeSpeedController(); const container = document.createElement('div'); container.id = 'manebi-speed-ctrl'; Object.assign(container.style, { position: 'fixed', bottom: '20px', right: '20px', width: 'min(280px, calc(100vw - 40px))', boxSizing: 'border-box', zIndex: '999999', background: 'rgba(0, 0, 0, 0.8)', color: '#fff', borderRadius: '8px', padding: '10px 14px', fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', flexDirection: 'column', gap: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.3)', userSelect: 'none', cursor: 'move', }); // Title bar const title = document.createElement('div'); title.textContent = `⚡ ${text.speedControl}`; Object.assign(title.style, { fontWeight: 'bold', fontSize: '12px', opacity: '0.7', textAlign: 'center', }); container.appendChild(title); // Speed buttons const btnRow = document.createElement('div'); Object.assign(btnRow.style, { display: 'flex', gap: '4px', flexWrap: 'wrap', justifyContent: 'center', }); let activeBtn = null; SPEED_OPTIONS.forEach((speed) => { const btn = document.createElement('button'); btn.textContent = speed + 'x'; Object.assign(btn.style, { background: speed === 1 ? '#4CAF50' : '#555', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer', fontSize: '12px', minWidth: '36px', transition: 'background 0.2s', }); if (speed === 1) activeBtn = btn; btn.addEventListener('click', () => { video.playbackRate = speed; if (activeBtn) activeBtn.style.background = '#555'; btn.style.background = '#4CAF50'; activeBtn = btn; }); btn.addEventListener('mouseenter', () => { if (btn !== activeBtn) btn.style.background = '#777'; }); btn.addEventListener('mouseleave', () => { if (btn !== activeBtn) btn.style.background = '#555'; }); btnRow.appendChild(btn); }); container.appendChild(btnRow); // Skip-to-end button const skipBtn = document.createElement('button'); skipBtn.textContent = `⏭ ${text.skipToEnd}`; Object.assign(skipBtn.style, { background: '#FF5722', color: '#fff', border: 'none', borderRadius: '4px', padding: '6px 10px', cursor: 'pointer', fontSize: '12px', transition: 'background 0.2s', }); skipBtn.addEventListener('click', () => { if (Number.isFinite(video.duration) && video.duration > 0) { video.currentTime = Math.max(0, video.duration - 5); } if (video.paused) { video.play()?.catch((error) => { console.warn('[Manebi Helper] Could not start video playback', error); }); } }); skipBtn.addEventListener('mouseenter', () => (skipBtn.style.background = '#E64A19')); skipBtn.addEventListener('mouseleave', () => (skipBtn.style.background = '#FF5722')); container.appendChild(skipBtn); document.body.appendChild(container); controlledVideo = video; speedController = { container, cleanup: makeDraggable(container), }; } // ========================================================================= // 5. Unlock video controls — remove restrictions on the