Warning: fopen(/www/sites/update.greasyfork.icu/index/store/forever/9a6ab1387ecde24a232ba0ca59fccc82.js): failed to open stream: No space left on device in /www/sites/update.greasyfork.icu/index/scriptControl.php on line 65
// ==UserScript== // @name 哔记-B Note (B站笔记插件) // @namespace http://tampermonkey.net/ // @version 0.1 // @description 可替代B站原有笔记功能的油猴插件(时间戳、截图、本地导入导出、字幕遮挡、快捷键、markdown写作) // @author XYZ // @match *://*.bilibili.com/video/* // @grant none // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.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 // @license MIT License // @downloadURL none // ==/UserScript== (function () { 'use strict'; // Add the TOAST UI Editor CSS $('head').append(''); // Add the TOAST UI Editor JS const scriptEditor = document.createElement('script'); scriptEditor.src = 'https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js'; document.body.appendChild(scriptEditor); // Add the JQuery UI $('head').append(''); // Bilibili AVI switch const switchToAV1 = () => { const radioInputs = document.querySelectorAll('input[type="radio"][name="bui-radio3"]'); for (const radioInput of radioInputs) { if (radioInput.value === '3') { radioInput.click(); break; } } }; const observer = new MutationObserver(switchToAV1); observer.observe(document.body, { childList: true, subtree: true }); // 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.text('打开哔记'); openEditorButton.append(createSVGIcon(openEditorIcon)); openEditorButton.css({ position: 'fixed', bottom: '10px', right: '10px', zIndex: 10000, }); $('body').append(openEditorButton); const toggleButton = $(''); const toggleButtonText = $('打开哔记'); toggleButton.append(createSVGIcon(openEditorIcon)).append(toggleButtonText); toggleButton.css({ position: 'fixed', bottom: '10px', right: '10px', zIndex: 10000, }); $('body').append(toggleButton); // 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 originalVideoWrapperParent; let originalContainerStyle; // 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); 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 jumpToURLTimeIcon = ''; 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 () { lastMarkedTime = videoElement.currentTime; // Update the last marked time jumpButton.removeAttribute('disabled'); // Enable the jump button const formattedTime = getCurrentTimeFormatted(); const newURL = getVideoURL() + '?t=' + formattedTime; const timeInBracket = formattedTime.replace('h', ':').replace('m', ':').replace('s', ''); const formattedURL = '[' + timeInBracket + '](' + newURL + ')'; editor.replaceSelection(formattedURL); // Insert at cursor }); getPositionButton.setAttribute("id", "getPositionButton"); buttonDiv.append(getPositionButton); // Get jump to last marked time button const jumpButton = createSVGButton(jumpIcon, '跳转到上一个标记点', function () { videoElement.currentTime = lastMarkedTime; // Jump to the last marked time }); jumpButton.setAttribute('disabled', true); // Initially disable the jump button jumpButton.setAttribute("id", "jumpButton"); buttonDiv.append(jumpButton); // Jump to specific URL time button const jumpToURLTimeButton = createSVGButton(jumpToURLTimeIcon, '跳转到指定位置', function () { const selection = editor.getSelection(); const selectedText = editor.getSelectedText(selection[0], selection[1]); let timeString; const fullMatch = selectedText.match(/\[([0-9]{2}:[0-9]{2}:[0-9]{2})\]\((https:\/\/www\.bilibili\.com\/video\/[^\/]+\/\?t=[^)]+)\)/); const timeMatch = selectedText.match(/([0-9]{2}:[0-9]{2}:[0-9]{2})/); const urlMatch = selectedText.match(/(https:\/\/www\.bilibili\.com\/video\/[^\/]+\/\?t=([0-9]{2}h)?([0-9]{2}m)?([0-9]{2}s)?)/); if (fullMatch) { timeString = fullMatch[1]; } else if (timeMatch) { timeString = timeMatch[0]; } else if (urlMatch) { const hours = urlMatch[2] ? parseInt(urlMatch[2], 10) : 0; const minutes = urlMatch[3] ? parseInt(urlMatch[3], 10) : 0; const seconds = urlMatch[4] ? parseInt(urlMatch[4], 10) : 0; timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } 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; } }); buttonDiv.append(jumpToURLTimeButton); // 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); const mdFile = zip.file('editor-content.md'); 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 videoWrapper = document.querySelector('.bpx-player-video-wrap'); const video = videoWrapper.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 + ')'); // Insert at cursor }); 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); buttonDiv.append(embedModeButton); // Create the help button function createHelpPopup() { const helpPopup = $(`