code-plus

解放鼠标,学码快人一步! 代码随想录网站辅助工具, 支持快速跳转到指定语言类型, 跳转到leetcode对应题目, 首次打开跳转到上次位置,切换前后文章

// ==UserScript==
// @name         code-plus
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  解放鼠标,学码快人一步! 代码随想录网站辅助工具, 支持快速跳转到指定语言类型, 跳转到leetcode对应题目, 首次打开跳转到上次位置,切换前后文章
// @author       righthan
// @license      MIT
// @match        https://*.programmercarl.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let lastKeyPressTime = 0; // 用于存储上一次按键的时间戳

    let page = document.querySelector('.page')
    let sider = document.querySelector('.page-sidebar')
    let siderLinks = document.querySelector('.sidebar-links')
    let isDoubleKeyG = false; // 标识是否是第一次按G键


    // 跳转到上次浏览的网页
    let historyPath = getCookie('history')
    // location.href条件:只在为根域名地址时跳转到历史(第一次打开的情况), 否则会导致无法跳转页面里的其他链接
    if (historyPath && location.href === 'https://www.programmercarl.com/') {
        location.href = historyPath // 因为默认不会加载侧边栏DOM, 所以第一次打开需要使用location.href来设定
        clickAndScrollNavLink(historyPath)
    } else {
        setCookie('history', location.href)
    }

    // 给导航容器列表添加监听器,检测网址变化
    siderLinks.addEventListener('click', (e) => { setCookie('history', e.target.href) })

    // 如果cookie中没有语言, 则需要设置
    let lang = getCookie('lang')
    if (!lang) {
        setCodeLanguage();
    }

    document.addEventListener("keyup", function (event) {
        // 检测是否快速连按了g开头的组合键
        const key = event.key
        if (key === 'g') {
            let currentTime = new Date().getTime(); // 更新第一次按g键的时间戳
            if (currentTime - lastKeyPressTime < 300) {
                isDoubleKeyG = true
            } else {
                isDoubleKeyG = false
            }
            lastKeyPressTime = currentTime
        }

        let currentTime = new Date().getTime(); // 获取当前时间戳
        let time = currentTime - lastKeyPressTime
        if (time < 400) { // 如果两次按键的时间间隔小于400毫秒,认为是快速连按
            switch (key) {
                case 'g':
                    lang = getCookie('lang')
                    if (isDoubleKeyG && lang) {
                        toCode(lang)
                        isDoubleKeyG = false
                    } else if (isDoubleKeyG && !lang) {
                        setCodeLanguage()
                    }
                    break
                case 'k':
                    // 跳转到上一篇文章
                    changeArticle(0)
                    break
                case 'j':
                    // 跳转到下一篇文章
                    changeArticle(1)
                    break
                case 'l':
                    toLeetCode()
                    break
                case 'c':
                    setCodeLanguage()
                    break
                case 'r':
                    toRelevant()
                    break
                case 't':
                    toTips()
                    break
                case 'h':
                    alert(`
                    code-plus是一款网页操作快捷辅助工具
                    按键及功能如下:
                    gg: 跳转到指定语言代码
                    gj: 跳转到下一篇
                    gk: 跳转到上一篇
                    gl: 跳转到leetcode页面
                    gt: 跳转到思路题目
                    gr: 跳转到相关题目
                    gc: 设置代码语言(保存在网站的cookie中)
                    gh: 显示本帮助页面
                    其他功能: 打开网页时恢复上次的进度
                    `)
                    break
            }
        }
    });

    // 跳转到指定语言的算法代码
    function toCode(langType) {
        // 获取对要滚动到的元素的引用
        let targetElement = page.querySelector('#' + langType);
        if (targetElement) { // 确保找到了元素
            // 使用scrollIntoView()方法将元素滚动到可见区域
            targetElement.scrollIntoView({
                behavior: "smooth", // 可选:使滚动平滑进行
                block: "start", // 可选:滚动到元素的顶部
            });
        } else {
            alert((langType !== 'c-2' ? langType : 'c#') + '语言的代码在此页面中不存在')
        }
    }

    // 文章跳转 flag:1表示下跳转到下一篇文章, 0表示上一篇文章
    function changeArticle(flag) {
        let optionButton = sider.querySelectorAll('div[title]')
        let path = optionButton[flag].childNodes[0].href
        setCookie('history', decodeURI(path)) // 记录路径
        clickAndScrollNavLink(decodeURI(path))
    }

    // 跳转到leetcode刷题网站
    function toLeetCode() {
        let link = page.querySelector('a[href*="leetcode"] ')
        if (link && link.href.includes('leetcode')) {
            link.click()
        } else {
            alert("当前页面可能没有LeetCode题目")
        }
    }

    // 设置代码语言
    function setCodeLanguage() {
        const supportLang = ['java', 'python', 'go', 'rust', 'javascript', 'typescript', 'swift', 'ruby', 'c', 'php', 'kotlin', 'scala', 'c#']
        let userInput = window.prompt("可能支持的语言:\n(一些语言代码不是每题都有, c++代码在靠前的位置, 不需要配置)\n" + supportLang.join('/') + "\n请输入需要快速跳转到的目标语言")

        if (userInput !== null) {
            if (supportLang.includes(userInput)) {
                // c#的id为c-2
                if (userInput === 'c#') { userInput = 'c-2' }
                setCookie('lang', userInput)
            } else {
                alert(userInput + "的语言类型似乎没有对应的代码, 请检查拼写, 并重新输入")
            }
        }
    }

    // 跳转到相关题目
    function toRelevant() {
        // 获取对要滚动到的元素的引用
        let targetElements = page.querySelectorAll('#相关题目, #相关题目推荐');
        if (targetElements.length > 0) { // 确保找到了元素
            // 使用scrollIntoView()方法将元素滚动到可见区域
            targetElements[0].scrollIntoView({
                behavior: "smooth", // 可选:使滚动平滑进行
                block: "start", // 可选:滚动到元素的顶部
            });
        } else {
            alert('无相关题目')
        }
    }

    // 跳转到思路
    function toTips() {
        // 获取对要滚动到的元素的引用
        let targetElements = page.querySelectorAll('#思路');
        if (targetElements.length > 0) { // 确保找到了元素
            // 使用scrollIntoView()方法将元素滚动到可见区域
            targetElements[0].scrollIntoView({
                behavior: "smooth", // 可选:使滚动平滑进行
                block: "start", // 可选:滚动到元素的顶部
            });
        } else {
            alert('当前页面没有思路内容')
        }
    }

    // 点击对应链接, 并且滚动侧边导航
    // 某次网站更新之后, 侧边栏导航的DOM不是加载全部, 所以只能在一个分类里面跳转了
    function clickAndScrollNavLink(path) {
        // 获取 https://www.programmercarl.com/kamacoder/xxx.html 中com之后的路径名
        const subPath = getPathAfterDomain(path)
        let targetElement = siderLinks.querySelector(`a[href*="${subPath}"]`)
        if (targetElement) { // 确保找到了元素
            targetElement.click()
            // 使用scrollIntoView() // 方法将元素滚动到可见区域
            targetElement.scrollIntoView({
                block: "start", // 可选:滚动到元素的顶部
            });
            document.querySelector('.sidebar').scrollBy({ top: -300 })
        }else{
            console.log(location.href)
            console.error('找不到目标元素')
            if( location.href !== 'https://www.programmercarl.com/'){
              alert('到达分类首部或末尾,或找不到目标元素');
            }
        }
    }

    // 获取域名之后的页面资源路径
    function getPathAfterDomain(url) {
        // 使用正则表达式匹配URL中的域名部分
        const domainRegex = /^https?:\/\/[^\/]+/;
        const match = url.match(domainRegex);

        // 如果没有匹配到域名部分,返回空字符串
        if (!match) {
            return '';
        }

        // 获取域名部分的长度
        const domainLength = match[0].length;

        // 返回URL中去除域名部分后的剩余路径
        return url.substring(domainLength);
    }

    // 解析特定的cookie值
    function getCookie(cookieName) {
        const allCookies = document.cookie;
        let name = cookieName + "=";
        let decodedCookie = decodeURIComponent(allCookies);
        let cookieArray = decodedCookie.split(';');
        for (let i = 0; i < cookieArray.length; i++) {
            let cookie = cookieArray[i];
            while (cookie.charAt(0) === ' ') {
                cookie = cookie.substring(1);
            }
            if (cookie.indexOf(name) === 0) {
                return cookie.substring(name.length, cookie.length);
            }
        }
        return "";
    }

    function setCookie(key, val) {
        document.cookie = `${key}=${val}; expires=Fri, 31 Dec 9999 23:59:59 GMT`
    }
})();