您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
可替代B站原有笔记功能的油猴插件(时间戳、截图、本地导入导出、字幕遮挡、快捷键、markdown写作)
// ==UserScript== // @name 哔记-B Note (B站笔记插件) // @namespace http://tampermonkey.net/ // @version 1.6 // @description 可替代B站原有笔记功能的油猴插件(时间戳、截图、本地导入导出、字幕遮挡、快捷键、markdown写作) // @author XYZ // @match *://*.bilibili.com/video/* // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js // @require https://unpkg.com/[email protected]/dist/axios.min.js // @license MIT License // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @icon data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CiAgPHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMTIgNy41aDEuNW0tMS41IDNoMS41bS03LjUgM2g3LjVtLTcuNSAzaDcuNW0zLTloMy4zNzVjLjYyMSAwIDEuMTI1LjUwNCAxLjEyNSAxLjEyNVYxOGEyLjI1IDIuMjUgMCAwMS0yLjI1IDIuMjVNMTYuNSA3LjVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVNMTYuNSA3LjVWNC44NzVjMC0uNjIxLS41MDQtMS4xMjUtMS4xMjUtMS4xMjVINC4xMjVDMy41MDQgMy43NSAzIDQuMjU0IDMgNC44NzVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVoMTMuNU02IDcuNWgzdjNINnYtM3oiPjwvcGF0aD4KPC9zdmc+ // ==/UserScript== (function () { 'use strict'; // 填写你的github的token以及repo(仓库名) let token = ''; let repo = ''; // Add the TOAST UI Editor CSS $('head').append('<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.css" />'); // CSS const style = ` .floating-btn { width: 60px; height: 15px; font-size: 10px; cursor: pointer; border: none; outline: none; background: transparent; color: white; font-family: 'Times New Roman', Times, serif; font-weight: 100; position: relative; transition: all 0.5s; z-index: 1; } .floating-btn::before { content: ""; position: absolute; top: 0; left: 0; width: 0.5px; height: 100%; background-color: white; z-index: -1; transition: all 0.5s; padding-left: 2px; } .floating-btn:hover::before { width: 100%; content: "选集"; } .floating-btn:hover { color: black; } .floating-btn:active:before { background: #b9b9b9; } .switch1 { position: relative; display: inline-block; width: 60px; height: 25px; } .switch1 input { display: none; } .slider1 { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #3C3C3C; -webkit-transition: .4s; transition: .4s; border-radius: 34px; } .slider1:before { position: absolute; content: ""; height: 25px; width: 10px; left: 0px; bottom: 0px; background-color: white; -webkit-transition: .4s; transition: .4s; border-radius: 10%; } input:checked + .slider1 { background-color: #0E6EB8; } input:focus + .slider1 { box-shadow: 0 0 1px #2196F3; } input:checked + .slider1:before { -webkit-transform: translateX(25px); -ms-transform: translateX(25px); transform: translateX(85px); } .slider1:after { content: '本地'; color: white; display: block; position: absolute; transform: translate(-50%,-50%); top: 50%; left: 50%; font-size: 10px; font-family: Verdana, sans-serif; } input:checked + .slider1:after { content: '在线'; } `; $('head').append('<style>' + style + '</style>'); // Add the TOAST UI Editor JS const scriptEditor = document.createElement('script'); scriptEditor.src = 'https://uicdn.toast.com/editor/latest/toastui-editor-all.js'; document.body.appendChild(scriptEditor); // Add the JQuery UI $('head').append('<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">'); // Bilibili AVI switch function selectAV1() { const av1Radio = document.querySelector('input.bui-radio-input[value="3"][name="bui-radio3"]'); if (av1Radio && !av1Radio.checked) { av1Radio.click(); } } function waitForElement(selector, callback) { const element = document.querySelector(selector); if (element) { callback(); } else { setTimeout(() => waitForElement(selector, callback), 500); } } waitForElement('input.bui-radio-input[value="3"][name="bui-radio3"]', selectAV1); // Determine if it is a multi-part video. let isCur = false; let curList; let originalcurListParent; if (document.querySelector('.cur-list')) { isCur = true; curList = $('#multi_page'); originalcurListParent = $('#multi_page').parent() } else { isCur = false; } // Create a switch using SVG. function createSVGIcon(svgContent) { const svgIcon = $(svgContent); svgIcon.css({ width: '24px', height: '24px', verticalAlign: 'middle', marginRight: '5px' }); return svgIcon; } const openEditorIcon = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 16H5V5h14v14z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'; const closeEditorIcon = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M20 11H4v2h16v-2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'; // Create the button const openEditorButton = $('<button id="openEditor" class="B-Note-button"></button>'); openEditorButton.append(createSVGIcon(openEditorIcon)); $('body').append(openEditorButton); const toggleButton = $('<button id="toggleEditor" class="B-Note-button"></button>'); const toggleButtonText = $('<span>打开哔记</span>'); toggleButton.append(createSVGIcon(openEditorIcon)).append(toggleButtonText); $('body').append(toggleButton); const buttonStyles = ` .B-Note-button { position: fixed; bottom: 10px; right: -60px; width: 100px; height: 35px; z-index: 10000; background-color: white; border: none; cursor: pointer; transition: right 0.3s, background-color 0.3s; } .B-Note-button:hover { right: 0px; } `; const styleElement = $('<style></style>'); styleElement.text(buttonStyles); $('head').append(styleElement); // video element var videoElement = document.querySelector('video'); var lastMarkedTime = null; let saveButton; let helpButton; let editor; let editorDiv; let isEditorOpen = false; let embedMode = false; let videoWrapper = $('#bilibili-player'); let isUpload = false; let originalVideoWrapperParent; let originalContainerStyle; let originalDisplayStatus = []; let isScrollbarDisabled = false; // Get the current date, title, and current webpage link. function getPageInfo() { let currentDate = new Date(); let formattedDate = `${currentDate.getFullYear()}年${currentDate.getMonth() + 1}月${currentDate.getDate()}日`; let pageTitle = document.title; let pageLink = window.location.href; return { formattedDate, pageTitle, pageLink }; } let pageInfo = getPageInfo(); // Use IndexedDB to automatically back up notes. const dbName = 'BNoteDB'; const storeName = 'notes'; let db; const openRequest = indexedDB.open(dbName, 1); openRequest.onupgradeneeded = function (e) { const db = e.target.result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'pageTitle' }); } }; openRequest.onsuccess = function (e) { db = e.target.result; }; function saveNoteToDB() { if (isEditorOpen) { let { formattedDate, pageTitle, pageLink } = getPageInfo(); const content = editor.getMarkdown(); const timestamp = Date.now(); const note = { pageTitle, content, timestamp }; const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); store.put(note); } } setInterval(saveNoteToDB, 120000); // Upload to Github async function handleImageInsertion() { if (!isUpload) { return; } let content = editor.getMarkdown(); const regex = /!\[.*?\]\(data:image\/.*?;base64,.*?\)/g; const matches = content.match(regex); if (matches) { const newContent = await matches.reduce(async (prevContentPromise, match) => { const prevContent = await prevContentPromise; const base64 = match.substring(match.indexOf('base64,') + 7, match.lastIndexOf(')')); const blob = base64ToBlob(base64); const imageUrl = await uploadImageToGitHub(blob); return prevContent.replace(match, ``); }, Promise.resolve(content)); editor.setMarkdown(newContent); } } function base64ToBlob(base64) { const binary = atob(base64); const array = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { array[i] = binary.charCodeAt(i); } return new Blob([array], { type: 'image/png' }); } async function uploadImageToGitHub(blob) { const branch = 'main'; const currentDate = new Date(); currentDate.setMinutes(currentDate.getMinutes() + currentDate.getTimezoneOffset() + 8 * 60); const year = currentDate.getFullYear(); const month = currentDate.getMonth() + 1; const day = currentDate.getDate(); const hours = currentDate.getHours(); const minutes = currentDate.getMinutes(); const seconds = currentDate.getSeconds(); const time = `${hours}-${minutes}-${seconds}`; const pageInfo = getPageInfo(); const invalidChars = /[<>:"/\\|?*]/g; const cleanedPageTitle = pageInfo.pageTitle.replace(invalidChars, ''); const path = `images/B-Note/${year}/${month}/${day}/${cleanedPageTitle}/${time}.png`; const url = `https://api.github.com/repos/${repo}/contents/${path}`; const base64 = await blobToBase64(blob); const payload = { message: 'Upload image', content: base64, branch: branch, }; const response = await axios.put(url, payload, { headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json', }, }); return response.data.content.download_url; } function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(blob); }); } const container = $('<div id="editorContainer"></div>'); // Function to create the editor function createEditor() { container.css({ position: 'fixed', top: '8%', right: '0%', width: '32%', height: '86%', zIndex: 9998, backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '5px', padding: '0px', overflow: 'hidden', }); $('body').append(container); // Make the container resizable container.resizable({ handles: 'n, e, s, w, ne, se, sw, nw', minWidth: 300, minHeight: 200, resize: function (event, ui) { const newHeight = ui.size.height - 80; editorDiv.height(newHeight + 'px'); } }); const handle = $('<div id="dragHandle">哔记(B-Note)</div>'); handle.css({ position: 'sticky', top: 0, height: '30px', backgroundColor: '#ccc', cursor: 'move', boxSizing: 'border-box', margin: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', fontStyle: 'bold', }); container.append(handle); const buttonDiv = $('<div id="buttonContainer"></div>'); buttonDiv.css({ position: 'sticky', top: '35px', display: 'flex', justifyContent: 'flex-start', paddingLeft: '10px', marginBottom: '10px', gap: '10px', }); container.append(buttonDiv); // Get button SVG const saveIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859M12 3v8.25m0 0l-3-3m3 3l3-3"></path></svg>'; const getPositionIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"></path></svg>'; const jumpIcon = '<svg fill="none" stroke="black" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"></path></svg>'; const importIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"></path></svg>'; const captureIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M7.848 8.25l1.536.887M7.848 8.25a3 3 0 11-5.196-3 3 3 0 015.196 3zm1.536.887a2.165 2.165 0 011.083 1.839c.005.351.054.695.14 1.024M9.384 9.137l2.077 1.199M7.848 15.75l1.536-.887m-1.536.887a3 3 0 11-5.196 3 3 3 0 015.196-3zm1.536-.887a2.165 2.165 0 001.083-1.838c.005-.352.054-.695.14-1.025m-1.223 2.863l2.077-1.199m0-3.328a4.323 4.323 0 012.068-1.379l5.325-1.628a4.5 4.5 0 012.48-.044l.803.215-7.794 4.5m-2.882-1.664A4.331 4.331 0 0010.607 12m3.736 0l7.794 4.5-.802.215a4.5 4.5 0 01-2.48-.043l-5.326-1.629a4.324 4.324 0 01-2.068-1.379M14.343 12l-2.882 1.664"></path></svg>'; const blurIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"></path></svg>'; const lightIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="22" height="22"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"></path></svg>'; const helpIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"></path></svg>'; const autoBackupIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z"></path></svg>'; const embedModeIcon = '<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="25" height="25"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"></path></svg>'; // Get save button saveButton = createSVGButton(saveIcon, '保存', function () { saveEditorContent(); }); buttonDiv.append(saveButton); // Get video position button const getPositionButton = createSVGButton(getPositionIcon, '获取播放位置', function () { const formattedTime = getCurrentTimeFormatted(); var newURL; if (isCur) { newURL = getVideoURL() + '&t=' + formattedTime; } else { newURL = getVideoURL() + '?t=' + formattedTime; } const timeInBracket = formattedTime.replace('h', ':').replace('m', ':').replace('s', ''); const formattedURL = '[' + timeInBracket + '](' + newURL + ')'; editor.replaceSelection(formattedURL); lastMarkedTime = formattedURL; }); getPositionButton.setAttribute("id", "getPositionButton"); buttonDiv.append(getPositionButton); // Get jump to last marked time button OR Jump to specific URL time button const jumpButton = createSVGButton(jumpIcon, '跳转', function () { const selection = editor.getSelection(); const selectedText = editor.getSelectedText(selection[0], selection[1]); if (selectedText) { let timeString; let pValue; const urlMatch = selectedText.match(/(https?:\/\/www\.bilibili\.com\/video\/[^\s]+(?:\?p=\d+)?(?:&t=\d{2}h\d{2}m\d{2}s)?)/); const timeMatch = selectedText.match(/([0-9]{2}:[0-9]{2}:[0-9]{2})/); if (urlMatch) { const url = urlMatch[0]; const pMatch = url.match(/(?:\?p=)(\d+)/); const timeMatchInUrl = url.match(/(?:\?t=|&t=)(\d{2})h(\d{2})m(\d{2})s/); if (isCur) { pValue = pMatch ? parseInt(pMatch[1], 10) : 1; if (timeMatchInUrl) { const hours = parseInt(timeMatchInUrl[1], 10); const minutes = parseInt(timeMatchInUrl[2], 10); const seconds = parseInt(timeMatchInUrl[3], 10); timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } } else { timeString = timeMatchInUrl ? `${timeMatchInUrl[1]}:${timeMatchInUrl[2]}:${timeMatchInUrl[3]}` : null; } } else { timeString = timeMatch ? timeMatch[0] : null; } if (isCur && pValue) { const target = Array.from(document.querySelectorAll('.list-box .page-num')).find(el => el.textContent === `P${pValue}`); if (target) { const link = target.closest('a'); if (link) { link.click(); } else { console.error('未找到对应的链接'); } } else { console.error('未找到对应的分P'); } } if (timeString) { const timeParts = timeString.split(':'); const seconds = parseInt(timeParts[0], 10) * 3600 + parseInt(timeParts[1], 10) * 60 + parseInt(timeParts[2], 10); videoElement.currentTime = seconds; } } else { console.log('lastMarkedTime:', lastMarkedTime); const pMatch = lastMarkedTime.match(/(?:\?p=)(\d+)/); const timeMatch = lastMarkedTime.match(/(?:\?t=|&t=)(\d{2})h(\d{2})m(\d{2})s/); if (isCur) { const pValue = pMatch ? parseInt(pMatch[1], 10) : 1; console.log('pValue:', pValue); const target = Array.from(document.querySelectorAll('.list-box .page-num')).find(el => el.textContent === `P${pValue}`); if (target) { const link = target.closest('a'); if (link) { link.click(); } else { console.error('未找到对应的链接'); } } else { console.error('未找到对应的分P'); } } if (timeMatch) { const hours = parseInt(timeMatch[1], 10); const minutes = parseInt(timeMatch[2], 10); const seconds = parseInt(timeMatch[3], 10); const timeInSeconds = hours * 3600 + minutes * 60 + seconds; videoElement.currentTime = timeInSeconds; } } }); jumpButton.setAttribute("id", "jumpButton"); buttonDiv.append(jumpButton); // Import button const importButton = createSVGButton(importIcon, '导入', function () { const input = document.createElement('input'); input.type = 'file'; input.accept = '.md,.zip'; input.addEventListener('change', async (event) => { const file = event.target.files[0]; if (file.name.endsWith('.md')) { const reader = new FileReader(); reader.onload = () => { const markdown = reader.result; editor.setMarkdown(markdown); }; reader.readAsText(file); } else if (file.name.endsWith('.zip')) { const zip = await JSZip.loadAsync(file); let mdFile; zip.forEach((relativePath, zipEntry) => { if (zipEntry.name.endsWith('.md')) { mdFile = zipEntry; } }); if (!mdFile) { alert('找不到.md 文件。'); return; } const mdContent = await mdFile.async('text'); const replaceImages = async (content) => { const regex = /!\[Image\]\((images\/image\d+\.png)\)/g; let match; let newContent = content; while ((match = regex.exec(content)) !== null) { const imagePath = match[1]; const imgFile = zip.file(imagePath); if (!imgFile) { alert(`找不到 ${imagePath} 文件。`); continue; } const imgData = await imgFile.async('base64'); newContent = newContent.replace(match[0], ``); } return newContent; }; const updatedContent = await replaceImages(mdContent); editor.setMarkdown(updatedContent); } else { alert('请选择一个有效的文件类型(.md 或 .zip)。'); } }); input.click(); }); buttonDiv.append(importButton); // Create the capture button const captureButton = createSVGButton(captureIcon, '截图', function () { const videoWrap = document.querySelector('.bpx-player-video-wrap'); const video = videoWrap.querySelector('video'); if (!video) { alert('找不到视频区域,请确保您在正确的页面上。'); return; } const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = video.videoWidth / 3; canvas.height = video.videoHeight / 3; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/png'); editor.replaceSelection(''); }); captureButton.setAttribute("id", "captureButton"); buttonDiv.append(captureButton); // Create the blur button const blurButton = createSVGButton(blurIcon, '字幕遮挡', function () { createBlurRectangle(); }); buttonDiv.append(blurButton); // Create the lamp const lightButton = createSVGButton(lightIcon, '关灯', function () { toggleLight(); }); buttonDiv.append(lightButton); // Create automatic backups. const autoBackupButton = createSVGButton(autoBackupIcon, '自动备份', function () { showAutoBackupDialog(); }); buttonDiv.append(autoBackupButton); // Create embedded note mode. const embedModeButton = createSVGButton(embedModeIcon, '内嵌模式', toggleEmbedMode); embedModeButton.setAttribute("id", "embedModeButton"); buttonDiv.append(embedModeButton); // Create the help button function createHelpPopup() { const helpPopup = $(` <div id="helpPopup" style="display:none; overflow: auto"> <h2 style="text-align: center">如何使用哔记</h3> <ul> <li> 1. ${saveIcon.replace('width="25" height="25"', 'width="15" height="15"')} 保存:下载笔记,并将文本连同图片打包为zip压缩包。</li> <li> 2. ${getPositionIcon.replace('width="25" height="25"', 'width="15" height="15"')} 获取播放位置:添加当前播放位置的时间戳。<strong>快捷键alt+X</strong>。</li> <li> 3. ${jumpIcon.replace('width="25" height="25"', 'width="15" height="15"')} 跳转:立即跳转到最近一次标记时间戳的播放位置。<strong>快捷键alt+C</strong>。如果此时手动选择了时间戳,将会跳转到指定的播放位置。</li> <li> 4. ${importIcon.replace('width="25" height="25"', 'width="15" height="15"')} 导入:载入之前下载的zip压缩包或者仅导入之前下载的.md文件。</li> <li> 5. ${captureIcon.replace('width="25" height="25"', 'width="15" height="15"')} 截图。截取当前播放器中的画面并插入到笔记中。<strong>快捷键alt+V</strong>。(注意:过多的图片插入可能会导致卡顿!)</li> <li> 6. ${blurIcon.replace('width="25" height="25"', 'width="15" height="15"')} 字幕遮挡。产生一个可以移动且可改变大小(拖拽右下角)的毛玻璃矩形,可以用来遮挡字幕练习英语听力。再次点击该按钮后关闭字幕遮挡。</li> <li> 7. ${lightIcon.replace('width="22" height="22"', 'width="15" height="15"')} 关灯。视频播放时关灯,避免干扰。再次点击该按钮后开灯。</li> <li> 8. ${autoBackupIcon.replace('width="25" height="25"', 'width="15" height="15"')} 自动备份。自动备份最近的6条笔记。</li> <li> 9. ${embedModeIcon.replace('width="25" height="25"', 'width="15" height="15"')} 内嵌模式。点击后进入笔记分屏效果(再次点击该按钮退出分屏效果)。<strong>快捷键alt+E</strong>。特别的,对于分part视频(注意,不是合集视频),将在页面左下方产生一个悬浮的选集弹窗。点开之后,会跳出选集界面。当鼠标离开选集弹窗后,弹窗会自动关闭。</li> <li> 10.${helpIcon.replace('width="25" height="25"', 'width="15" height="15"')} 帮助按钮。用法提示。</li> <li> 11.在线/本地选择框。可选择将笔记中插入的图片保存在本地或者上传到github图床。可以<strong><a href="https://github.com/Xiang-yuZHAO/B-Note_img" target="_blank">【点击此处】</a></strong>查看github在线图床配置。 </li> <li> 12. Tip1。添加了额外的视频播放暂停/开始<strong>快捷键alt+B</strong>。视频后退5秒<strong>快捷键alt+N</strong>。视频前进5秒<strong>快捷键alt+M</strong>。</li> <li> 13. Tip2。可以通过拖拽顶部调整哔记的位置。</li> <li> 14. Tip3。哔记有wysiwyg(实时预览)和markdown两种模式(可在右下角切换),但是在实时预览模式下插入时间戳会被强制转义为文本。</li> <li> 15. Tip4。使用快捷键可帮你更快的记录笔记。可点击浏览器的油猴图标,找到哔记的快捷键设置页面,从而自定义快捷键。</li> </ul> `); helpPopup.css({ fontSize: '16px', }); $('body').append(helpPopup); helpPopup.dialog({ autoOpen: false, modal: true, width: '50%', zIndex: 99999, position: { my: "left top", at: "left+10% top+8%", of: window }, buttons: { "关闭": function() { $(this).dialog("close"); } } }); return helpPopup; } const helpPopup = createHelpPopup(); helpButton = createSVGButton(helpIcon, '帮助', function() { helpPopup.dialog("open"); }); buttonDiv.append(helpButton); // Create a button to upload a GitHub repository. buttonDiv.append(` <label class="switch1"> <input type="checkbox"> <span class="slider1"></span> </label> `); buttonDiv.find('input[type="checkbox"]').change(function() { if ($(this).is(':checked')) { if (token === '' || repo === '') { alert('请先配置 token 和 repo!可点击帮助按钮查看如何配置github图床。'); $(this).prop('checked', false); } else { isUpload = true; console.log('打开isUpload:', isUpload); } } else { isUpload = false; console.log('关闭isUpload:', isUpload); } }); // Create the toast ui editor editorDiv = $('<div id="editor"></div>'); editorDiv.css({ width: '100%', height: 'calc(100% - 100px)', zIndex: 9999, }); container.append(editorDiv); let { formattedDate, pageTitle, pageLink } = getPageInfo(); editor = new toastui.Editor({ el: document.querySelector('#editor'), height: container.height() - 80 + 'px', //initialEditType: 'markdown', previewStyle: 'vertical', //initialEditType: 'wysiwyg', initialValue: `**标题**:[${pageTitle}](${pageLink})\n**日期**:${formattedDate}\n**摘要**:[添加摘要]\n**标签**:[添加标签]\n\n\n\n\n\n `, autofocus: true, }); // Mouse scroll event: Disable webpage scrolling when the mouse is inside the container. container.on('mouseenter', function () { if (!isScrollbarDisabled) { $('body').css('overflow', 'hidden'); } }); container.on('mouseleave', function () { if (!isScrollbarDisabled) { $('body').css('overflow', 'auto'); } }); editor.on('change', handleImageInsertion); } // Create a button with an SVG icon function createSVGButton(svgIcon, tooltipText, onClick) { const button = document.createElement('button'); button.innerHTML = svgIcon; button.setAttribute('title', tooltipText); button.onclick = onClick; button.style.border = 'none'; button.style.background = 'transparent'; button.style.cursor = 'pointer'; return button; } // Add click event listener to the button openEditorButton.on('click', function () { createEditor(); }); function createButton(text, clickHandler) { const button = $('<button></button>'); button.text(text); button.css({ display: 'inline-block', marginRight: '1px', padding: '5px', backgroundColor: 'white', color: 'black', border: 'none', borderRadius: '5px', cursor: 'pointer' }); button.click(clickHandler); return button; } async function saveEditorContent() { return new Promise(async (resolve) => { const content = editor.getMarkdown(); const zip = new JSZip(); const imgFolder = zip.folder("images"); let imgIndex = 1; const newContent = content.replace(/!\[[^\]]*\]\((data:image\/[^;]+;base64,[^\)]+)\)/g, (match, dataUrl) => { const imgName = `image${imgIndex}.png`; imgFolder.file(imgName, dataUrl.split(',')[1], { base64: true }); imgIndex++; return ``; }); const invalidChars = /[<>:"/\\|?*]/g; const cleanedPageTitle = pageInfo.pageTitle.replace(invalidChars, ''); zip.file(`${cleanedPageTitle}.md`, newContent); const blob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `${cleanedPageTitle}.zip`); link.onclick = () => { setTimeout(() => { URL.revokeObjectURL(url); resolve(); }, 100); }; link.click(); }); } function getCurrentTimeFormatted() { var currentTime = videoElement.currentTime; var hours = Math.floor(currentTime / 3600); var minutes = Math.floor((currentTime % 3600) / 60); var seconds = Math.floor(currentTime % 60); return hours.toString().padStart(2, '0') + 'h' + minutes.toString().padStart(2, '0') + 'm' + seconds.toString().padStart(2, '0') + 's'; } function getVideoURL() { if (isCur) { return window.location.href.split('&')[0]; } else { return window.location.href.split('?')[0]; } } // make editor draggable $(document).on('mousedown', '#dragHandle', function (event) { const container = $('#editorContainer'); const offset = { x: event.pageX - container.offset().left, y: event.pageY - container.offset().top, }; let isDragging = false; const onMouseMove = function (event) { if (!isDragging) return; requestAnimationFrame(() => { const draggable = $('.draggable'); draggable.offset({ top: event.pageY - offset.y, left: event.pageX - offset.x }); }); }; container.addClass('draggable').on('mousemove', onMouseMove); isDragging = true; event.preventDefault(); }).on('mouseup', function () { $('.draggable').removeClass('draggable'); }); let blurRectangle = null; function createBlurRectangle() { if (blurRectangle) { blurRectangle.parentNode.removeChild(blurRectangle); blurRectangle = null; } else { blurRectangle = document.createElement('div'); blurRectangle.style.position = 'fixed'; blurRectangle.style.zIndex = '10001'; if (embedMode) { blurRectangle.style.left = '3%'; blurRectangle.style.bottom = '30%'; blurRectangle.style.width = '45%'; blurRectangle.style.height = '10%'; } else { blurRectangle.style.left = '7%'; blurRectangle.style.bottom = '20%'; blurRectangle.style.width = '60%'; blurRectangle.style.height = '10%'; } blurRectangle.style.background = 'rgba(255, 255, 255, 0.2)'; blurRectangle.style.backdropFilter = 'blur(8px)'; blurRectangle.style.cursor = 'move'; document.body.appendChild(blurRectangle); $(blurRectangle).resizable({ handles: 'n, e, s, w, ne, se, sw, nw', minWidth: 50, minHeight: 50 }); $(blurRectangle).draggable(); } } function downloadImage(dataUrl, filename) { const link = document.createElement('a'); link.href = dataUrl; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function toggleLight() { if (embedMode) { return; } const lightOffCheckbox = document.querySelector('.bpx-player-ctrl-setting-lightoff .bui-checkbox-input'); lightOffCheckbox.click(); isScrollbarDisabled = !isScrollbarDisabled; document.body.style.overflow = isScrollbarDisabled ? 'hidden' : 'auto'; } // Display the most recent 6 backups of notes. function showAutoBackupDialog() { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); request.onsuccess = function (e) { const notes = e.target.result; notes.sort((a, b) => b.timestamp - a.timestamp); const dialog = $('<div id="autoBackupDialog"></div>'); dialog.css({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: 10000, backgroundColor: 'white', padding: '20px', borderRadius: '5px', boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)', }); const recentNotes = notes.slice(0, 6); recentNotes.forEach((note) => { const noteButton = $('<button></button>'); const timestamp = new Date(note.timestamp); const dateFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const formattedTime = dateFormatter.format(timestamp); noteButton.text(`${formattedTime} - ${note.pageTitle}`); noteButton.css({ display: 'block', width: '100%', textAlign: 'left', marginBottom: '10px', }); noteButton.click(function () { loadNoteToEditor(note); dialog.remove(); }); dialog.append(noteButton); }); const cancelButton = $('<button>取消</button>'); cancelButton.css({ display: 'block', width: '100%', textAlign: 'center', }); cancelButton.click(function () { dialog.remove(); }); dialog.append(cancelButton); $('body').append(dialog); }; } function loadNoteToEditor(note) { editor.setMarkdown(note.content); } function toggleEmbedMode() { embedMode = !embedMode; if (embedMode) { enterEmbedMode(); } else { exitEmbedMode(); } } function enterEmbedMode() { if ($('.mini-player-window.fixed-sidenav-storage-item.on').length > 0) { console.log("检测到小窗"); $('.mini-player-window.fixed-sidenav-storage-item.on').click(); } originalDisplayStatus = []; $('body > *').each(function () { if (!$(this).hasClass('ui-dialog')) { originalDisplayStatus.push($(this).css('display')); } }); const newContainer = $('<div></div>'); newContainer.css({ width: '50%', float: 'left', height: '100%' }); //const videoWrapper = $('#bilibili-player'); originalVideoWrapperParent = videoWrapper.parent(); videoWrapper.css({ width: '100%', height: '100%' }); const iframe = videoWrapper.find('iframe'); iframe.css({ width: '100%', height: '100%' }); newContainer.append(videoWrapper); if (isCur) { const floatingButton = $('<button></button>'); floatingButton.addClass('floating-btn'); floatingButton.css({ position: 'absolute', bottom: '20%', left: '0px', zIndex: 9999 }); newContainer.append(floatingButton); floatingButton.on('click', function () { const curContainer = $('<div></div>'); curContainer.css({ position: 'absolute', bottom: '5%', height: 'auto', width: '40%', overflow: 'hidden', zIndex: 9999 }); curList.appendTo(curContainer); newContainer.append(curContainer); let timeoutId; curContainer.on('mouseleave', function () { timeoutId = setTimeout(function () { curContainer.remove(); }, 1000); }); curContainer.on('mouseenter', function () { clearTimeout(timeoutId); }); }); } container.attr('style', ''); container.css({ position: 'fixed', top: '8%', right: '0%', width: '32%', height: '86%', zIndex: 99999, backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '5px', padding: '0px', overflow: 'hidden', }); const rightContainer = $('<div></div>'); rightContainer.css({ width: '50%', float: 'right', height: '100%' }); originalContainerStyle = container.attr('style'); rightContainer.append(container); container.css({ position: 'relative', top: '0%', width: '100%', height: '100%', zIndex: 9998, backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '5px', padding: '0px', overflow: 'hidden' }); container.addClass('embed-mode-hidden'); $(document).off('mousedown', '#dragHandle'); container.resizable('destroy'); editor.setHeight('90%'); const mainContainer = $('<div></div>'); mainContainer.css({ width: '100%', height: '100%', position: 'fixed', top: '0', left: '0' }); mainContainer.append(newContainer); mainContainer.append(rightContainer); $('body').prepend(mainContainer); $('body > *:not(:first-child)').hide(); newContainer.resizable({ handles: 'e', minWidth: $(window).width() * 0.2, maxWidth: $(window).width() * 0.8, resize: function (event, ui) { rightContainer.css('width', 100 - (ui.size.width / $(window).width() * 100) + '%'); } }); } function exitEmbedMode() { container.attr('style', originalContainerStyle); editor.setHeight(container.height() - 80 + 'px'); $('body').append(container); //const videoWrapper = $('#bilibili-player'); videoWrapper.css({ width: '', height: '' }); const iframe = videoWrapper.find('iframe'); iframe.css({ width: '', height: '' }); originalVideoWrapperParent.append(videoWrapper); $('body').append(container); const mainContainer = $('body > :first-child'); mainContainer.remove(); $('body > *').each(function (index) { if (!$(this).hasClass('ui-dialog')) { $(this).css('display', originalDisplayStatus[index]); } }); $(document).on('mousedown', '#dragHandle', function (event) { const container = $('#editorContainer'); const offset = { x: event.pageX - container.offset().left, y: event.pageY - container.offset().top, }; let isDragging = false; const onMouseMove = function (event) { if (!isDragging) return; requestAnimationFrame(() => { const draggable = $('.draggable'); draggable.offset({ top: event.pageY - offset.y, left: event.pageX - offset.x }); }); }; container.addClass('draggable').on('mousemove', onMouseMove); isDragging = true; event.preventDefault(); }).on('mouseup', function () { $('.draggable').removeClass('draggable'); }); container.resizable({ handles: 'n, e, s, w, ne, se, sw, nw', minWidth: 300, minHeight: 200, resize: function (event, ui) { const newHeight = ui.size.height - 80; editorDiv.height(newHeight + 'px'); } }); if (isCur) { let thirdLastChild = originalcurListParent.children().eq(-3); curList.insertBefore(thirdLastChild); } } // The operation after the current video playback ends. // Function to close the editor and save the content if confirmed async function closeAndSave() { if (isEditorOpen) { const r = confirm("是否需要保存笔记?"); if (r == true) { await saveEditorContent(); } if (embedMode) { exitEmbedMode(); } $('#editorContainer').remove(); toggleButton.empty().append(createSVGIcon(openEditorIcon)).append(toggleButtonText.text('打开哔记')); } } // Handler for toggle button click toggleButton.click(function () { if (toggleButtonText.text() === '打开哔记') { createEditor(); toggleButton.empty().append(createSVGIcon(closeEditorIcon)).append(toggleButtonText.text('关闭哔记')); isEditorOpen = true; } else { closeAndSave(); isEditorOpen = false; } }); // Listen for the video to end and trigger the close and save function videoElement.addEventListener('ended', function () { if (!isCur) { closeAndSave(); } }); // Shortcut settings. function handleKeydown(event) { const shortcuts = [ { id: 'getPositionButton', modifier: 'alt', key: 'X', action: () => document.getElementById('getPositionButton').click() }, { id: 'jumpButton', modifier: 'alt', key: 'C', action: () => document.getElementById('jumpButton').click() }, { id: 'captureButton', modifier: 'alt', key: 'V', action: () => document.getElementById('captureButton').click() }, { id: 'togglePlayButton', modifier: 'alt', key: 'B', action: () => { if (videoElement.paused) { videoElement.play(); } else { videoElement.pause(); } }}, { id: 'rewindButton', modifier: 'alt', key: 'N', action: () => { const currentTime = videoElement.currentTime; videoElement.currentTime = Math.max(currentTime - 5, 0); }}, { id: 'forwardButton', modifier: 'alt', key: 'M', action: () => { const currentTime = videoElement.currentTime; videoElement.currentTime = Math.min(currentTime + 5, videoElement.duration); }}, { id: 'embedModeButton', modifier: 'alt', key: 'E', action: () => document.getElementById('embedModeButton').click() }, ]; shortcuts.forEach(shortcut => { const modifier = GM_getValue(shortcut.id + 'Modifier', shortcut.modifier); const key = GM_getValue(shortcut.id + 'Key', shortcut.key); if (event[modifier + 'Key'] && event.key.toUpperCase() === key) { shortcut.action(); } }); } document.addEventListener('keydown', handleKeydown); GM_registerMenuCommand('快捷键设置', () => { if (document.getElementById('settingsDiv')) return; const settingsDiv = document.createElement('div'); settingsDiv.id = 'settingsDiv'; settingsDiv.innerHTML = ` <style> #settingsDiv { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; } #settingsForm { background-color: white; padding: 20px; border-radius: 5px; } .settingsRow { display: flex; align-items: center; margin-bottom: 10px; } .settingsLabel { margin-right: 10px; width: 150px;} .settingsSelect { margin-right: 20px; width: 80px; } .settingsInput { width: 40px; } </style> <form id="settingsForm"> <!-- 快捷键设置行 --> <button id="saveSettings">保存/退出</button> <button id="resetSettings">恢复默认设置</button> </form> `; document.body.appendChild(settingsDiv); const shortcuts = [ { id: 'getPositionButton', label: '获取播放位置', modifier: 'alt', key: 'X' }, { id: 'jumpButton', label: '跳转', modifier: 'alt', key: 'C' }, { id: 'captureButton', label: '截图', modifier: 'alt', key: 'V' }, { id: 'togglePlayButton', label: '视频暂停/开始', modifier: 'alt', key: 'B' }, { id: 'rewindButton', label: '视频后退5秒', modifier: 'alt', key: 'N' }, { id: 'forwardButton', label: '视频前进5秒', modifier: 'alt', key: 'M' }, { id: 'embedModeButton', label: '进入/退出embed mode', modifier: 'alt', key: 'E' }, ]; shortcuts.forEach(shortcut => { const settingsRow = document.createElement('div'); settingsRow.className = 'settingsRow'; settingsRow.innerHTML =` <label class="settingsLabel" for="${shortcut.id}Modifier">${shortcut.label}:</label> <select class="settingsSelect" id="${shortcut.id}Modifier"> <option value="alt">Alt</option> <option value="shift">Shift</option> <option value="ctrl">Ctrl</option> </select> <input class="settingsInput" id="${shortcut.id}Key" type="text" maxlength="1" value="${shortcut.key}" /> `; document.getElementById('settingsForm').insertBefore(settingsRow, document.getElementById('saveSettings')); }); shortcuts.forEach(shortcut => { document.getElementById(shortcut.id + 'Modifier').value = GM_getValue(shortcut.id + 'Modifier', shortcut.modifier); document.getElementById(shortcut.id + 'Key').value = GM_getValue(shortcut.id + 'Key', shortcut.key); }); document.getElementById('saveSettings').addEventListener('click', (e) => { e.preventDefault(); shortcuts.forEach(shortcut => { const modifier = document.getElementById(shortcut.id + 'Modifier').value; const key = document.getElementById(shortcut.id + 'Key').value.toUpperCase(); GM_setValue(shortcut.id + 'Modifier', modifier); GM_setValue(shortcut.id + 'Key', key); }); document.body.removeChild(settingsDiv); }); document.getElementById('resetSettings').addEventListener('click', (e) => { e.preventDefault(); shortcuts.forEach(shortcut => { document.getElementById(shortcut.id + 'Modifier').value = shortcut.modifier; document.getElementById(shortcut.id + 'Key').value = shortcut.key; }); }); }); })();