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], ``);
}
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(''); // 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 = $(`