// ==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/axios@1.1.2/dist/axios.min.js // @license MIT License // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @icon data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CiAgPHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMTIgNy41aDEuNW0tMS41IDNoMS41bS03LjUgM2g3LjVtLTcuNSAzaDcuNW0zLTloMy4zNzVjLjYyMSAwIDEuMTI1LjUwNCAxLjEyNSAxLjEyNVYxOGEyLjI1IDIuMjUgMCAwMS0yLjI1IDIuMjVNMTYuNSA3LjVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVNMTYuNSA3LjVWNC44NzVjMC0uNjIxLS41MDQtMS4xMjUtMS4xMjUtMS4xMjVINC4xMjVDMy41MDQgMy43NSAzIDQuMjU0IDMgNC44NzVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVoMTMuNU02IDcuNWgzdjNINnYtM3oiPjwvcGF0aD4KPC9zdmc+ // @downloadURL https://update.greasyfork.icu/scripts/475808/%E5%93%94%E8%AE%B0-B%20Note%20%28B%E7%AB%99%E7%AC%94%E8%AE%B0%E6%8F%92%E4%BB%B6%29.user.js // @updateURL https://update.greasyfork.icu/scripts/475808/%E5%93%94%E8%AE%B0-B%20Note%20%28B%E7%AB%99%E7%AC%94%E8%AE%B0%E6%8F%92%E4%BB%B6%29.meta.js // ==/UserScript== (function () { 'use strict'; // 填写你的github的token以及repo(仓库名) let token = ''; let repo = ''; // Add the TOAST UI Editor CSS $('head').append(''); // 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(''); // 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(''); // 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 = ''; const closeEditorIcon = ''; // Create the button const openEditorButton = $(''); openEditorButton.append(createSVGIcon(openEditorIcon)); $('body').append(openEditorButton); const toggleButton = $(''); const toggleButtonText = $('打开哔记'); 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 = $(''); 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, `![Image](${imageUrl})`); }, 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 = $('
'); // 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 = $('
哔记(B-Note)
'); 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 = $('
'); 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 = ''; const getPositionIcon = ''; const jumpIcon = ''; const importIcon = ''; const captureIcon = ''; const blurIcon = ''; const lightIcon = ''; const helpIcon = ''; const autoBackupIcon = ''; const embedModeIcon = ''; // 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], `![Image](data:image/png;base64,${imgData})`); } 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('![Image](' + dataUrl + ')'); }); 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 = $(`