您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
进入 https://learn.lingvist.com/#hub 后自动选择指定语言并开始学习
// ==UserScript== // @name Lingvist 智能语言选择器 // @name:en Lingvist Smart Language Selector // @namespace https://github.com/username/lingvist-smart-language-selector // @version 1.0.0 // @description 进入 https://learn.lingvist.com/#hub 后自动选择指定语言并开始学习 // @description:en Automatically selects specified language and starts learning when entering https://learn.lingvist.com/#hub // @author Your Name // @match https://learn.lingvist.com/* // @run-at document-idle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @homepage https://github.com/username/lingvist-smart-language-selector // @supportURL https://greasyfork.org/en/scripts/546018-lingvist-%E6%99%BA%E8%83%BD%E8%AF%AD%E8%A8%80%E9%80%89%E6%8B%A9%E5%99%A8 // @license MIT // ==/UserScript== (function () { 'use strict'; // ========== 语言选项配置 ========== const LANGUAGE_OPTIONS = { 'english_zh_cn': { name: '英语(用中文简体学)', title: '英语', subtitle: '用 中文(简体)学', iconValue: 'gb' }, 'french_zh_cn': { name: '法语(用中文简体学)', title: '法语', subtitle: '用中文(简体)学', iconValue: 'fr' }, 'japanese_zh_cn': { name: '日语(用中文简体学)', title: '日语', subtitle: '用中文(简体)学', iconValue: 'jp' }, 'korean_zh_cn': { name: '韩语(用中文简体学)', title: '韩语', subtitle: '用中文(简体)学', iconValue: 'kr' }, 'french_zh_tw': { name: '法语(用中文繁体学)', title: '法語', subtitle: '用 中文(繁體)學', iconValue: 'fr' }, 'french_en': { name: '法语(用英语学)', title: 'French', subtitle: 'from English', iconValue: 'fr' }, 'spanish_europe_en': { name: '西班牙语欧洲版(用英语学)', title: 'Spanish (Europe)', subtitle: 'from English', iconValue: 'es' }, 'spanish_latin_en': { name: '西班牙语拉美版(用英语学)', title: 'Spanish (Latin America)', subtitle: 'from English', iconValue: 'es-us' }, 'german': { name: '德语', title: '德语', iconValue: 'de' }, 'italian': { name: '意大利语', title: '意大利语', iconValue: 'it' }, 'russian': { name: '俄语', title: '俄语', iconValue: 'ru' }, 'portuguese_brazil': { name: '葡萄牙语(巴西)', title: '葡萄牙语(巴西)', iconValue: 'br' }, 'dutch': { name: '荷兰语', title: '荷兰语', iconValue: 'nl' }, 'swedish': { name: '瑞典语', title: '瑞典语', iconValue: 'sv' }, 'norwegian': { name: '挪威语', title: '挪威语', iconValue: 'no' }, 'danish': { name: '丹麦语', title: '丹麦语', iconValue: 'da' }, 'polish': { name: '波兰语', title: '波兰语', iconValue: 'pl' }, 'estonian': { name: '爱沙尼亚语', title: '爱沙尼亚语', iconValue: 'ee' } }; const CONFIG = { enableDebug: true, startButtonText: /开始学习|继续学习|Start Learning|Continue/i }; function getSelectedLanguage() { const savedKey = GM_getValue('selectedLanguageKey', 'english_zh_cn'); return LANGUAGE_OPTIONS[savedKey] || LANGUAGE_OPTIONS.english_zh_cn; } function saveSelectedLanguage(languageKey) { GM_setValue('selectedLanguageKey', languageKey); } function getCurrentPageLanguage() { // 检测页面当前实际显示的语言 const courseSelect = document.querySelector('.course-select'); if (!courseSelect) return null; const iconValue = courseSelect.querySelector('.course-icon')?.getAttribute('data-value') || ''; const buttonText = courseSelect.querySelector('.text')?.textContent?.trim() || ''; // 根据图标和按钮文本推断当前语言 const result = { iconValue, buttonText, detected: null, detectedKey: null }; // 尝试匹配已知的语言配置 for (const [key, lang] of Object.entries(LANGUAGE_OPTIONS)) { if (lang.iconValue === iconValue) { result.detected = lang; result.detectedKey = key; break; } } LOG('🔍 检测到页面当前语言:', result); return result; } function isCurrentLanguageMatchTarget() { const targetLang = getSelectedLanguage(); const currentLang = getCurrentPageLanguage(); if (!currentLang || !currentLang.detected) { LOG('⚠️ 无法检测当前页面语言'); return false; } const isMatch = currentLang.detected.name === targetLang.name; LOG('🎯 语言匹配检查:', { target: targetLang.name, current: currentLang.detected.name, isMatch }); return isMatch; } // ========== Tampermonkey 菜单设置 ========== function showLanguageSelector() { const currentKey = GM_getValue('selectedLanguageKey', 'english_zh_cn'); const currentLang = LANGUAGE_OPTIONS[currentKey]; const choice = prompt( `🌍 Lingvist 语言选择器\n\n当前选择:${currentLang.name}\n\n请输入对应的数字选择语言:\n\n` + Object.entries(LANGUAGE_OPTIONS).map(([key, lang], index) => `${index + 1}. ${key === currentKey ? '✓ ' : ' '}${lang.name}` ).join('\n'), '' ); if (choice) { const index = parseInt(choice) - 1; const keys = Object.keys(LANGUAGE_OPTIONS); if (index >= 0 && index < keys.length) { const selectedKey = keys[index]; saveSelectedLanguage(selectedKey); alert(`✅ 已选择:${LANGUAGE_OPTIONS[selectedKey].name}\n\n刷新页面后生效。`); } else { alert('❌ 无效的选择,请输入正确的数字。'); } } } function toggleAutoStart() { const current = GM_getValue('autoClickStart', true); const newValue = !current; GM_setValue('autoClickStart', newValue); alert(`${newValue ? '✅ 已开启' : '❌ 已关闭'}自动点击"开始学习"按钮`); } function showCurrentSettings() { const targetLang = getSelectedLanguage(); const pageLang = getCurrentPageLanguage(); const autoStart = GM_getValue('autoClickStart', true); let message = `📋 当前设置:\n\n目标语言:${targetLang.name}\n自动开始学习:${autoStart ? '是' : '否'}`; if (pageLang && pageLang.detected) { message += `\n\n🔍 页面当前语言:${pageLang.detected.name}`; message += `\n✅ 语言匹配:${isCurrentLanguageMatchTarget() ? '是' : '否'}`; } else { message += `\n\n⚠️ 无法检测页面当前语言`; } alert(message); } // ========== 核心逻辑 ========== const LOG = CONFIG.enableDebug ? (...args) => console.debug('[Lingvist-AutoLang]', ...args) : () => {}; const inHub = () => { const hash = location.hash; const isHubPage = hash === '#hub' || hash.startsWith('#hub/') || hash.startsWith('#hub?'); const hasHubContent = document.querySelector('.course-select') !== null; const result = isHubPage && hasHubContent; LOG('页面检测:', { hash, isHubPage, hasHubContent, result, url: location.href }); return result; }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function simulateClick(el) { if (!el) return false; try { el.scrollIntoView({ block: 'center', inline: 'nearest' }); const rect = el.getBoundingClientRect(); // 修复 MouseEvent 构造参数 ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach(type => { el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, })); }); // 额外调用原生 click el.click?.(); LOG('✅ 点击完成:', el); return true; } catch (e) { LOG('simulateClick error:', e); // 如果模拟点击失败,尝试简单的 click try { el.click(); LOG('✅ 备用点击成功:', el); return true; } catch (e2) { LOG('备用点击也失败:', e2); return false; } } } function findLanguageButton() { // 更全面的选择器列表,参考原始脚本 const selectors = [ '.course-select [role="button"]', '.course-select .text', '.course-select .arrow', '.course-select', '[data-testid="course-select"]', '.language-selector', '.course-dropdown' ]; for (const s of selectors) { const el = document.querySelector(s); if (el && el.offsetParent !== null) { LOG('✅ 找到语言按钮:', s, el); return el; } } // 如果没找到,尝试通过文本内容查找 const allButtons = document.querySelectorAll('button, [role="button"], .clickable'); for (const btn of allButtons) { const text = btn.textContent || ''; if (text.includes('French') || text.includes('English') || text.includes('语言') || text.includes('Language')) { LOG('✅ 通过文本找到语言按钮:', btn); return btn; } } LOG('⚠️ 未找到语言按钮'); return null; } function matchTargetLanguage(item) { try { const selectedLanguage = getSelectedLanguage(); const title = item.querySelector('.info .title')?.textContent?.trim() || ''; const subtitle = item.querySelector('.info .subtitle')?.textContent?.trim() || ''; const titleMatch = title === selectedLanguage.title; let subtitleMatch = true; if (selectedLanguage.subtitle) { subtitleMatch = subtitle === selectedLanguage.subtitle; } const result = titleMatch && subtitleMatch; if (result) { LOG('✅ 精确匹配到目标语言:', { title, subtitle, target: selectedLanguage }); } return result; } catch (e) { LOG('matchTargetLanguage error:', e); return false; } } function getClickTargets(item) { // 使用原始脚本的成功选择器顺序 const candidates = [ item.querySelector('button,[role="button"],a'), // 最重要的! item.querySelector('.info .title'), item.querySelector('.info'), item.querySelector('.course-icon'), item.querySelector('.right .meta'), item.querySelector('.right'), item, // 兜底 ].filter(Boolean); return Array.from(new Set(candidates)); } async function clickLanguageItemRobust(item) { const targets = getClickTargets(item); // 优先尝试最可能成功的目标 for (const target of targets) { LOG('尝试点击目标子元素:', target); // 直接点击,不等待 target.click(); // 快速检查是否成功 await sleep(100); // 减少等待时间 const popupOpen = !!document.querySelector('.courses-list'); const becameActive = (item.classList?.contains('active') || item.closest('.course-list-item')?.classList?.contains('active')); if (!popupOpen || becameActive) { LOG('✅ 语言选择成功:弹窗关闭或条目为 active'); return true; } } LOG('❌ 所有点击尝试都失败了'); return false; } async function openAndPickAndStart() { if (window.lingvistAutoRunning) return; window.lingvistAutoRunning = true; try { LOG('🚀 开始自动语言切换,目标:', getSelectedLanguage().name); // 检查当前语言是否已经是目标语言 if (isCurrentLanguageMatchTarget()) { LOG('✅ 当前语言已经是目标语言,跳过切换'); // 直接尝试点击开始学习 if (GM_getValue('autoClickStart', true)) { await sleep(1000); const startBtn = document.querySelector('button.button-component.filled'); if (startBtn && CONFIG.startButtonText.test(startBtn.textContent || '')) { LOG('🚀 直接点击开始学习'); simulateClick(startBtn); } } return; } await sleep(500); // 减少等待时间 // 查找语言按钮(减少重试次数和间隔) let btn = null; for (let i = 0; i < 5; i++) { btn = findLanguageButton(); if (btn) break; await sleep(300); // 减少间隔 LOG(`查找语言按钮 ${i + 1}/5`); } if (!btn) { LOG('❌ 未找到语言按钮'); return; } // 快速点击语言按钮 LOG('🎯 点击语言按钮'); btn.click(); // 直接使用最简单的点击方式 await sleep(400); // 减少等待时间 let listRoot = document.querySelector('.courses-list'); if (!listRoot) { // 如果简单点击失败,尝试一次模拟点击 LOG('⚠️ 简单点击失败,尝试模拟点击'); simulateClick(btn); await sleep(400); listRoot = document.querySelector('.courses-list'); } if (!listRoot) { LOG('❌ 语言列表未打开'); return; } LOG('✅ 语言列表已打开'); const items = Array.from(listRoot.querySelectorAll('.course-list-item')); LOG('可用语言数量:', items.length); // 调试:显示所有语言 items.forEach((item, index) => { const title = item.querySelector('.info .title')?.textContent?.trim() || ''; const subtitle = item.querySelector('.info .subtitle')?.textContent?.trim() || ''; LOG(`语言 ${index + 1}: "${title}" - "${subtitle}"`); }); const target = items.find(matchTargetLanguage); if (!target) { LOG('❌ 未找到目标语言'); return; } LOG('🎯 点击目标语言'); // 尝试多种方式点击目标语言 const clickSuccess = await clickLanguageItemRobust(target); if (!clickSuccess) { LOG('❌ 点击目标语言失败'); return; } await sleep(800); // 减少等待时间 // 点击开始学习 if (GM_getValue('autoClickStart', true)) { const startBtn = document.querySelector('button.button-component.filled'); if (startBtn && CONFIG.startButtonText.test(startBtn.textContent || '')) { LOG('🚀 点击开始学习'); startBtn.click(); // 使用简单点击 } } } finally { setTimeout(() => { window.lingvistAutoRunning = false; }, 5000); } } // ========== 初始化 ========== LOG('🚀 Lingvist 脚本已加载!'); LOG('📍 当前页面:', location.href); LOG('🌍 当前语言设置:', getSelectedLanguage().name); LOG('⚙️ 自动开始学习:', GM_getValue('autoClickStart', true) ? '开启' : '关闭'); // 注册 Tampermonkey 菜单 GM_registerMenuCommand('🌍 选择语言', showLanguageSelector); GM_registerMenuCommand('⚙️ 切换自动开始学习', toggleAutoStart); GM_registerMenuCommand('📋 查看当前设置', showCurrentSettings); // 检查当前是否在 hub 页面 function checkAndRun() { LOG('检查当前页面:', location.href); if (inHub()) { LOG('✅ 在 hub 页面,准备执行自动切换'); setTimeout(openAndPickAndStart, 2000); } else { LOG('❌ 不在 hub 页面,跳过执行'); } } // 立即检查一次 checkAndRun(); // 使用 MutationObserver 监听页面内容变化(更可靠) const observer = new MutationObserver((mutations) => { // 检查是否有重要的 DOM 变化 const hasSignificantChange = mutations.some(mutation => mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) ); if (hasSignificantChange) { LOG('检测到页面内容变化'); setTimeout(checkAndRun, 1000); } }); // 开始观察整个文档的变化 observer.observe(document.documentElement, { childList: true, subtree: true }); // 同时保留原有的路由监听作为备用 const _pushState = history.pushState; history.pushState = function (...args) { const ret = _pushState.apply(this, args); LOG('检测到 pushState 变化'); setTimeout(checkAndRun, 1000); return ret; }; const _replaceState = history.replaceState; history.replaceState = function (...args) { const ret = _replaceState.apply(this, args); LOG('检测到 replaceState 变化'); setTimeout(checkAndRun, 1000); return ret; }; window.addEventListener('popstate', () => { LOG('检测到 popstate 事件'); setTimeout(checkAndRun, 1000); }); window.addEventListener('hashchange', () => { LOG('检测到 hashchange 事件'); setTimeout(checkAndRun, 1000); }); })();