您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds navigation buttons near the left of the header and enhances performance on Articulate Rise authoring pages.
// ==UserScript== // @name Fast Rise // @namespace Violentmonkey Scripts // @match https://rise.articulate.com/author/* // @grant none // @version 1.4.1 // @author AMC-Albert // @description Adds navigation buttons near the left of the header and enhances performance on Articulate Rise authoring pages. // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const highlightColor = '#15095814'; // Color for the last visited section button const navButtonContainerId = 'rise-nav-buttons-container'; const prevButtonId = 'rise-prev-button'; const nextButtonId = 'rise-next-button'; const debounceDelay = 150; // ms delay for debouncing outline updates const hrefDetailsPrefix = '#/author/details/'; // --- State --- let outlineUpdateTimeout = null; // --- Helpers --- const isCourseOutline = () => window.location.href.includes('/author/course'); const isSectionPage = () => window.location.href.includes('/author/details/'); const scrollKey = `scrollPos_${window.location.origin}${window.location.pathname}`; const buttonKey = `lastClickedButton_${window.location.origin}${window.location.pathname}`; const navKey = `sectionNav_${window.location.origin}${window.location.pathname}`; console.log('Fast Rise Userscript loaded. Version:', GM_info.script.version); // --- Styling --- const style = document.createElement('style'); style.textContent = ` /* Disable animations to make things snappy and instant, except on a few elements that break if their animations are removed */ *:not(.author-layout--process *, [data-rmiz-modal-content] *, .curtain *, .upload-progress *, .review-publish-spinner *) { animation: none !important; transition: none !important; } /* Remove banner thumbnails from 'copy to another course' feature to make it load faster */ .copy-lesson-dialog__course-list__course-icon { background-image: none !important; } /* Remove AI garbage */ button:has(.ai-gradient), .menu__item:has(.ai-gradient), .ai-tooltip-rich, .authoring-tooltip:has([src="https://cdn.articulate.com/assets/rise/assets/ai/block-wizard/generate-image-thumbnail.webp"]) { display: none; } /* Navigation Button Styles */ #${navButtonContainerId} { display: flex; align-items: center; gap: 5px; margin: 0 15px; } #${navButtonContainerId} button { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 5px 10px; cursor: pointer; font-size: 16px; font-weight: bold; line-height: 1; transition: background-color 0.2s ease; /* Add back transition for buttons */ display: none; /* Hidden by default, shown via JS */ } #${navButtonContainerId} button:hover { background-color: #e0e0e0; } #${navButtonContainerId} button:active { background-color: #d0d0d0; } /* Ensure app-header uses flexbox if not already */ .app-header { display: flex; align-items: center; width: 100%; } `; document.head.appendChild(style); // --- Core Logic --- // Save scroll position periodically on the course outline page setInterval(() => { if (isCourseOutline()) { localStorage.setItem(scrollKey, window.scrollY); } }, 500); // Restore scroll position and setup on load window.addEventListener('load', () => { console.log('Window loaded. Setting up...'); setupPage(); }); // Handle browser back/forward navigation and hash changes window.addEventListener('popstate', handleNavigationChange); window.addEventListener('hashchange', handleNavigationChange); function handleNavigationChange() { console.log('Navigation event (popstate/hashchange). Setting up...'); // Use a small delay to allow Rise's own routing/rendering to settle setTimeout(setupPage, 50); } // Override Articulate's scroll resets on the course outline page const originalScrollTo = window.scrollTo; window.scrollTo = function(x, y) { if (isCourseOutline() && y === 0) { const savedScrollPos = localStorage.getItem(scrollKey); if (savedScrollPos !== null && window.scrollY !== parseInt(savedScrollPos, 10)) { const newY = parseInt(savedScrollPos, 10); // console.log('Overriding scrollTo(0) with saved position:', newY); originalScrollTo.call(window, x, newY); return; // Prevent original call with y=0 } } originalScrollTo.call(window, x, y); }; // Function to run setup tasks based on current page function setupPage() { addNavigationButtons(); // Attempt to add buttons if not present updateButtonVisibility(); // Show/hide buttons based on page type if (isCourseOutline()) { console.log('On Course Outline page.'); runOutlineUpdates(); // Update index, attach listeners, highlight immediately const savedScrollPos = localStorage.getItem(scrollKey); if (savedScrollPos !== null) { // Delay slightly to ensure layout is stable setTimeout(() => { // Double check we are still on the outline page before scrolling if(isCourseOutline()){ window.scrollTo(0, parseInt(savedScrollPos, 10)); } }, 150); } } else if (isSectionPage()) { console.log('On Section Detail page.'); updateLastClickedButton(); } else { console.log('On other Rise page.'); } } // --- Outline Page Specific Functions --- // Debounced function to schedule updates for the course outline page function scheduleOutlineUpdate() { clearTimeout(outlineUpdateTimeout); outlineUpdateTimeout = setTimeout(() => { if (isCourseOutline()) { // Final check before running console.log('Debounced outline update triggered.'); runOutlineUpdates(); } }, debounceDelay); } // Function to perform all necessary updates on the outline page function runOutlineUpdates() { console.log('Running outline updates (index, listeners, highlight).'); updateNavigationIndex(); attachClickListeners(); highlightLastClickedButton(); } // Scans the course outline and updates the stored navigation order function updateNavigationIndex() { if (!isCourseOutline()) return; // Only run on the outline page // Selector for links leading to section detail pages within the outline const sectionLinkSelector = `.course-outline-item__link[href^="${hrefDetailsPrefix}"], a[arc-button][href^="${hrefDetailsPrefix}"]`; const sectionLinks = document.querySelectorAll(sectionLinkSelector); const hrefs = Array.from(sectionLinks).map(link => link.getAttribute('href')); const existingHrefs = JSON.parse(localStorage.getItem(navKey) || '[]'); // Only update localStorage if the list has actually changed if (JSON.stringify(hrefs) !== JSON.stringify(existingHrefs)) { if (hrefs.length > 0) { localStorage.setItem(navKey, JSON.stringify(hrefs)); console.log(`Navigation index updated with ${hrefs.length} sections.`); } else { // Clear the index if no sections are found localStorage.removeItem(navKey); console.log('Navigation index cleared (no sections found).'); } } } // Save the href of the last clicked "Edit Content" button function attachClickListeners() { if (!isCourseOutline()) return; // Only run on the outline page // Target links that lead to section detail pages const sectionLinkSelector = `.course-outline-item__link[href^="${hrefDetailsPrefix}"], a[arc-button][href^="${hrefDetailsPrefix}"]`; const buttons = document.querySelectorAll(sectionLinkSelector); buttons.forEach(button => { // Remove existing listeners to prevent duplicates if re-run button.removeEventListener('click', handleButtonClick); button.addEventListener('click', handleButtonClick); }); } function handleButtonClick(event) { const href = event.currentTarget.getAttribute('href'); if (href) { localStorage.setItem(buttonKey, href); console.log('Button clicked, saving href:', href); // Remove highlight from previously clicked button const sectionLinkSelector = `.course-outline-item__link[href^="${hrefDetailsPrefix}"], a[arc-button][href^="${hrefDetailsPrefix}"]`; document.querySelectorAll(sectionLinkSelector).forEach(btn => { if (btn.style.background === highlightColor) { btn.style.background = ''; } }); // Highlight the clicked button immediately event.currentTarget.style.background = highlightColor; } } // Highlight the button corresponding to the last visited section function highlightLastClickedButton() { if (!isCourseOutline()) return; // Only run on the outline page const lastClickedHref = localStorage.getItem(buttonKey); const sectionLinkSelector = `.course-outline-item__link[href^="${hrefDetailsPrefix}"], a[arc-button][href^="${hrefDetailsPrefix}"]`; // Remove highlight from any previously highlighted button first document.querySelectorAll(sectionLinkSelector).forEach(btn => { if (btn.style.background === highlightColor) { btn.style.background = ''; } }); if (lastClickedHref) { const button = document.querySelector(`.course-outline-item__link[href="${lastClickedHref}"]`) || document.querySelector(`a[arc-button][href="${lastClickedHref}"]`); if (button) { button.style.background = highlightColor; } } } // --- Section Page Specific Functions --- // Update the last clicked button state when arriving at a section page function updateLastClickedButton() { const currentHref = window.location.hash; // Only update if it looks like a valid section detail URL if (currentHref.startsWith(hrefDetailsPrefix)) { localStorage.setItem(buttonKey, currentHref); console.log('Updated last clicked button on section load:', currentHref); } } // --- Navigation --- function navigateToSection(direction) { if (!isSectionPage()) { console.log('Navigation attempted, but not on a section page.'); return; // Only navigate when on a section page } const hrefs = JSON.parse(localStorage.getItem(navKey) || '[]'); if (hrefs.length === 0) { console.log('No navigation data found. Visit the course outline first.'); alert('Navigation data not available. Please visit the main course outline page first.'); return; } const currentHref = window.location.hash; const currentIndex = hrefs.indexOf(currentHref); let targetIndex = -1; if (direction === 'next' && currentIndex < hrefs.length - 1) { targetIndex = currentIndex + 1; } else if (direction === 'previous' && currentIndex > 0) { targetIndex = currentIndex - 1; } else if (currentIndex === -1) { console.warn(`Current URL (${currentHref}) not found in stored navigation index. Re-visit the course outline.`); alert(`Current section not found in navigation index. Please visit the course outline page to refresh it.`); return; } else { // Optionally provide feedback that they are at the start/end const buttonId = direction === 'next' ? nextButtonId : prevButtonId; const button = document.getElementById(buttonId); if (button) { button.style.opacity = '0.5'; // Dim the button briefly setTimeout(() => { button.style.opacity = '1'; }, 300); } return; // Already at the start/end } if (targetIndex !== -1) { const nextHref = hrefs[targetIndex]; console.log(`Navigating ${direction} to index ${targetIndex}: ${nextHref}`); localStorage.setItem(buttonKey, nextHref); // Update last clicked button state *before* navigating window.location.hash = nextHref; // Perform navigation // No need for setupPage call here, hashchange listener will handle it } } // Keyboard shortcuts for section navigation (Ctrl + Alt + Left/Right) window.addEventListener('keydown', (e) => { const activeElement = document.activeElement; const isInputFocused = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable); if (!isInputFocused && isSectionPage() && e.ctrlKey && e.altKey) { if (e.key === 'ArrowRight') { e.preventDefault(); navigateToSection('next'); } else if (e.key === 'ArrowLeft') { e.preventDefault(); navigateToSection('previous'); } } }); // --- UI Elements --- function addNavigationButtons() { if (document.getElementById(navButtonContainerId)) { return; // Already added } const header = document.querySelector('.app-header'); if (!header) { return; // Header not ready } console.log('Adding navigation buttons to header.'); const container = document.createElement('div'); container.id = navButtonContainerId; const prevButton = document.createElement('button'); prevButton.id = prevButtonId; prevButton.textContent = '←'; prevButton.title = 'Previous Section (Ctrl+Alt+Left)'; prevButton.addEventListener('click', () => navigateToSection('previous')); const nextButton = document.createElement('button'); nextButton.id = nextButtonId; nextButton.textContent = '→'; nextButton.title = 'Next Section (Ctrl+Alt+Right)'; nextButton.addEventListener('click', () => navigateToSection('next')); container.appendChild(prevButton); container.appendChild(nextButton); header.insertBefore(container, header.children[1]); updateButtonVisibility(); // Update visibility immediately after adding } function updateButtonVisibility() { const container = document.getElementById(navButtonContainerId); if (container) { const shouldShow = isSectionPage(); const buttons = container.querySelectorAll('button'); buttons.forEach(button => { button.style.display = shouldShow ? 'inline-block' : 'none'; }); } } // --- Dynamic Content Handling --- const observer = new MutationObserver((mutationsList, observer) => { let needsGeneralSetup = false; // For header/buttons let potentialOutlineChange = false; // Flag if outline items might have changed // Check for header setup need (independent of outline page) if (!document.getElementById(navButtonContainerId)) { for (const mutation of mutationsList) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && (node.matches('.app-header') || node.querySelector('.app-header'))) { needsGeneralSetup = true; break; } } } if (needsGeneralSetup) break; } } // Check for outline changes *if* on the outline page if (isCourseOutline()) { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const changedNodes = [...mutation.addedNodes, ...mutation.removedNodes]; for (const node of changedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the node itself or its descendants match outline item/link structure const sectionLinkSelector = `.course-outline-item, .course-outline-item__link, a[arc-button][href^="${hrefDetailsPrefix}"]`; if (node.matches(sectionLinkSelector) || node.querySelector(sectionLinkSelector)) { potentialOutlineChange = true; break; // Found a relevant change in this mutation's nodes } } } } if (potentialOutlineChange) break; // Stop checking mutations if change found } } // After looping through mutations: if (needsGeneralSetup) { addNavigationButtons(); } // Always update button visibility as page state might change implicitly updateButtonVisibility(); // If outline changes were detected, schedule the debounced update if (potentialOutlineChange) { scheduleOutlineUpdate(); } }); // Start observing the documentElement for broad coverage observer.observe(document.documentElement, { childList: true, subtree: true }); // Initial setup attempt after script injection setTimeout(setupPage, 50); })();