哔记-B Note (B站笔记插件)

可替代B站原有笔记功能的油猴插件(时间戳、截图、本地导入导出、字幕遮挡、快捷键、markdown写作)

安裝腳本?
作者推薦腳本

您可能也會喜歡 B-Note-Uni (哔记通用版)

安裝腳本
  1. // ==UserScript==
  2. // @name 哔记-B Note (B站笔记插件)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  5. // @description 可替代B站原有笔记功能的油猴插件(时间戳、截图、本地导入导出、字幕遮挡、快捷键、markdown写作)
  6. // @author XYZ
  7. // @match *://*.bilibili.com/video/*
  8. // @require https://code.jquery.com/jquery-3.6.0.min.js
  9. // @require https://code.jquery.com/ui/1.12.1/jquery-ui.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js
  11. // @require https://unpkg.com/axios@1.1.2/dist/axios.min.js
  12. // @license MIT License
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @icon data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CiAgPHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMTIgNy41aDEuNW0tMS41IDNoMS41bS03LjUgM2g3LjVtLTcuNSAzaDcuNW0zLTloMy4zNzVjLjYyMSAwIDEuMTI1LjUwNCAxLjEyNSAxLjEyNVYxOGEyLjI1IDIuMjUgMCAwMS0yLjI1IDIuMjVNMTYuNSA3LjVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVNMTYuNSA3LjVWNC44NzVjMC0uNjIxLS41MDQtMS4xMjUtMS4xMjUtMS4xMjVINC4xMjVDMy41MDQgMy43NSAzIDQuMjU0IDMgNC44NzVWMThhMi4yNSAyLjI1IDAgMDAyLjI1IDIuMjVoMTMuNU02IDcuNWgzdjNINnYtM3oiPjwvcGF0aD4KPC9zdmc+
  17.  
  18.  
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23.  
  24. // 填写你的github的token以及repo(仓库名)
  25. let token = '';
  26. let repo = '';
  27.  
  28.  
  29. // Add the TOAST UI Editor CSS
  30. $('head').append('<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.css" />');
  31.  
  32.  
  33. // CSS
  34. const style = `
  35. .floating-btn {
  36. width: 60px;
  37. height: 15px;
  38. font-size: 10px;
  39. cursor: pointer;
  40. border: none;
  41. outline: none;
  42. background: transparent;
  43. color: white;
  44. font-family: 'Times New Roman', Times, serif;
  45. font-weight: 100;
  46. position: relative;
  47. transition: all 0.5s;
  48. z-index: 1;
  49. }
  50.  
  51. .floating-btn::before {
  52. content: "";
  53. position: absolute;
  54. top: 0;
  55. left: 0;
  56. width: 0.5px;
  57. height: 100%;
  58. background-color: white;
  59. z-index: -1;
  60. transition: all 0.5s;
  61. padding-left: 2px;
  62. }
  63.  
  64. .floating-btn:hover::before {
  65. width: 100%;
  66. content: "选集";
  67. }
  68.  
  69. .floating-btn:hover {
  70. color: black;
  71. }
  72.  
  73. .floating-btn:active:before {
  74. background: #b9b9b9;
  75. }
  76.  
  77. .switch1 {
  78. position: relative;
  79. display: inline-block;
  80. width: 60px;
  81. height: 25px;
  82. }
  83.  
  84. .switch1 input {
  85. display: none;
  86. }
  87.  
  88. .slider1 {
  89. position: absolute;
  90. cursor: pointer;
  91. top: 0;
  92. left: 0;
  93. right: 0;
  94. bottom: 0;
  95. background-color: #3C3C3C;
  96. -webkit-transition: .4s;
  97. transition: .4s;
  98. border-radius: 34px;
  99. }
  100.  
  101. .slider1:before {
  102. position: absolute;
  103. content: "";
  104. height: 25px;
  105. width: 10px;
  106. left: 0px;
  107. bottom: 0px;
  108. background-color: white;
  109. -webkit-transition: .4s;
  110. transition: .4s;
  111. border-radius: 10%;
  112. }
  113.  
  114. input:checked + .slider1 {
  115. background-color: #0E6EB8;
  116. }
  117.  
  118. input:focus + .slider1 {
  119. box-shadow: 0 0 1px #2196F3;
  120. }
  121.  
  122. input:checked + .slider1:before {
  123. -webkit-transform: translateX(25px);
  124. -ms-transform: translateX(25px);
  125. transform: translateX(85px);
  126. }
  127.  
  128.  
  129. .slider1:after {
  130. content: '本地';
  131. color: white;
  132. display: block;
  133. position: absolute;
  134. transform: translate(-50%,-50%);
  135. top: 50%;
  136. left: 50%;
  137. font-size: 10px;
  138. font-family: Verdana, sans-serif;
  139. }
  140.  
  141. input:checked + .slider1:after {
  142. content: '在线';
  143. }
  144. `;
  145.  
  146. $('head').append('<style>' + style + '</style>');
  147.  
  148. // Add the TOAST UI Editor JS
  149. const scriptEditor = document.createElement('script');
  150. scriptEditor.src = 'https://uicdn.toast.com/editor/latest/toastui-editor-all.js';
  151. document.body.appendChild(scriptEditor);
  152.  
  153. // Add the JQuery UI
  154. $('head').append('<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">');
  155.  
  156. // Bilibili AVI switch
  157.  
  158. function selectAV1() {
  159. const av1Radio = document.querySelector('input.bui-radio-input[value="3"][name="bui-radio3"]');
  160. if (av1Radio && !av1Radio.checked) {
  161. av1Radio.click();
  162. }
  163. }
  164.  
  165. function waitForElement(selector, callback) {
  166. const element = document.querySelector(selector);
  167. if (element) {
  168. callback();
  169. } else {
  170. setTimeout(() => waitForElement(selector, callback), 500);
  171. }
  172. }
  173.  
  174. waitForElement('input.bui-radio-input[value="3"][name="bui-radio3"]', selectAV1);
  175.  
  176.  
  177.  
  178. // Determine if it is a multi-part video.
  179. let isCur = false;
  180. let curList;
  181. let originalcurListParent;
  182. if (document.querySelector('.cur-list')) {
  183. isCur = true;
  184. curList = $('#multi_page');
  185. originalcurListParent = $('#multi_page').parent()
  186. } else {
  187. isCur = false;
  188. }
  189.  
  190. // Create a switch using SVG.
  191. function createSVGIcon(svgContent) {
  192. const svgIcon = $(svgContent);
  193. svgIcon.css({ width: '24px', height: '24px', verticalAlign: 'middle', marginRight: '5px' });
  194. return svgIcon;
  195. }
  196. 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>';
  197. 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>';
  198.  
  199. // Create the button
  200. const openEditorButton = $('<button id="openEditor" class="B-Note-button"></button>');
  201. openEditorButton.append(createSVGIcon(openEditorIcon));
  202. $('body').append(openEditorButton);
  203.  
  204. const toggleButton = $('<button id="toggleEditor" class="B-Note-button"></button>');
  205. const toggleButtonText = $('<span>打开哔记</span>');
  206. toggleButton.append(createSVGIcon(openEditorIcon)).append(toggleButtonText);
  207. $('body').append(toggleButton);
  208.  
  209. const buttonStyles = `
  210. .B-Note-button {
  211. position: fixed;
  212. bottom: 10px;
  213. right: -60px;
  214. width: 100px;
  215. height: 35px;
  216. z-index: 10000;
  217. background-color: white;
  218. border: none;
  219. cursor: pointer;
  220. transition: right 0.3s, background-color 0.3s;
  221. }
  222.  
  223. .B-Note-button:hover {
  224. right: 0px;
  225. }
  226. `;
  227. const styleElement = $('<style></style>');
  228. styleElement.text(buttonStyles);
  229. $('head').append(styleElement);
  230.  
  231.  
  232. // video element
  233. var videoElement = document.querySelector('video');
  234. var lastMarkedTime = null;
  235. let saveButton;
  236. let helpButton;
  237. let editor;
  238. let editorDiv;
  239. let isEditorOpen = false;
  240. let embedMode = false;
  241. let videoWrapper = $('#bilibili-player');
  242. let isUpload = false;
  243. let originalVideoWrapperParent;
  244. let originalContainerStyle;
  245. let originalDisplayStatus = [];
  246. let isScrollbarDisabled = false;
  247.  
  248. // Get the current date, title, and current webpage link.
  249. function getPageInfo() {
  250. let currentDate = new Date();
  251. let formattedDate = `${currentDate.getFullYear()}年${currentDate.getMonth() + 1}月${currentDate.getDate()}日`;
  252. let pageTitle = document.title;
  253. let pageLink = window.location.href;
  254.  
  255. return { formattedDate, pageTitle, pageLink };
  256. }
  257. let pageInfo = getPageInfo();
  258.  
  259. // Use IndexedDB to automatically back up notes.
  260. const dbName = 'BNoteDB';
  261. const storeName = 'notes';
  262. let db;
  263.  
  264. const openRequest = indexedDB.open(dbName, 1);
  265.  
  266. openRequest.onupgradeneeded = function (e) {
  267. const db = e.target.result;
  268. if (!db.objectStoreNames.contains(storeName)) {
  269. db.createObjectStore(storeName, { keyPath: 'pageTitle' });
  270. }
  271. };
  272.  
  273. openRequest.onsuccess = function (e) {
  274. db = e.target.result;
  275. };
  276.  
  277. function saveNoteToDB() {
  278. if (isEditorOpen) {
  279. let { formattedDate, pageTitle, pageLink } = getPageInfo();
  280. const content = editor.getMarkdown();
  281. const timestamp = Date.now();
  282. const note = { pageTitle, content, timestamp };
  283.  
  284. const transaction = db.transaction(storeName, 'readwrite');
  285. const store = transaction.objectStore(storeName);
  286. store.put(note);
  287. }
  288. }
  289. setInterval(saveNoteToDB, 120000);
  290.  
  291. // Upload to Github
  292. async function handleImageInsertion() {
  293. if (!isUpload) {
  294. return;
  295. }
  296.  
  297. let content = editor.getMarkdown();
  298. const regex = /!\[.*?\]\(data:image\/.*?;base64,.*?\)/g;
  299. const matches = content.match(regex);
  300.  
  301. if (matches) {
  302. const newContent = await matches.reduce(async (prevContentPromise, match) => {
  303. const prevContent = await prevContentPromise;
  304. const base64 = match.substring(match.indexOf('base64,') + 7, match.lastIndexOf(')'));
  305. const blob = base64ToBlob(base64);
  306. const imageUrl = await uploadImageToGitHub(blob);
  307. return prevContent.replace(match, `![Image](${imageUrl})`);
  308. }, Promise.resolve(content));
  309.  
  310. editor.setMarkdown(newContent);
  311. }
  312. }
  313.  
  314.  
  315.  
  316. function base64ToBlob(base64) {
  317. const binary = atob(base64);
  318. const array = new Uint8Array(binary.length);
  319. for (let i = 0; i < binary.length; i++) {
  320. array[i] = binary.charCodeAt(i);
  321. }
  322. return new Blob([array], { type: 'image/png' });
  323. }
  324.  
  325. async function uploadImageToGitHub(blob) {
  326.  
  327. const branch = 'main';
  328. const currentDate = new Date();
  329.  
  330. currentDate.setMinutes(currentDate.getMinutes() + currentDate.getTimezoneOffset() + 8 * 60);
  331. const year = currentDate.getFullYear();
  332. const month = currentDate.getMonth() + 1;
  333. const day = currentDate.getDate();
  334. const hours = currentDate.getHours();
  335. const minutes = currentDate.getMinutes();
  336. const seconds = currentDate.getSeconds();
  337. const time = `${hours}-${minutes}-${seconds}`;
  338. const pageInfo = getPageInfo();
  339. const invalidChars = /[<>:"/\\|?*]/g;
  340. const cleanedPageTitle = pageInfo.pageTitle.replace(invalidChars, '');
  341.  
  342. const path = `images/B-Note/${year}/${month}/${day}/${cleanedPageTitle}/${time}.png`;
  343.  
  344.  
  345. const url = `https://api.github.com/repos/${repo}/contents/${path}`;
  346. const base64 = await blobToBase64(blob);
  347. const payload = {
  348. message: 'Upload image',
  349. content: base64,
  350. branch: branch,
  351. };
  352.  
  353. const response = await axios.put(url, payload, {
  354. headers: {
  355. 'Authorization': `token ${token}`,
  356. 'Content-Type': 'application/json',
  357. },
  358. });
  359.  
  360. return response.data.content.download_url;
  361. }
  362.  
  363. function blobToBase64(blob) {
  364. return new Promise((resolve, reject) => {
  365. const reader = new FileReader();
  366. reader.onloadend = () => resolve(reader.result.split(',')[1]);
  367. reader.onerror = reject;
  368. reader.readAsDataURL(blob);
  369. });
  370. }
  371.  
  372.  
  373. const container = $('<div id="editorContainer"></div>');
  374.  
  375. // Function to create the editor
  376. function createEditor() {
  377.  
  378. container.css({
  379. position: 'fixed', top: '8%', right: '0%',
  380. width: '32%', height: '86%',
  381. zIndex: 9998, backgroundColor: '#fff',
  382. border: '1px solid #ccc', borderRadius: '5px', padding: '0px', overflow: 'hidden',
  383. });
  384. $('body').append(container);
  385.  
  386. // Make the container resizable
  387. container.resizable({
  388. handles: 'n, e, s, w, ne, se, sw, nw',
  389. minWidth: 300,
  390. minHeight: 200,
  391. resize: function (event, ui) {
  392. const newHeight = ui.size.height - 80;
  393. editorDiv.height(newHeight + 'px');
  394. }
  395. });
  396.  
  397. const handle = $('<div id="dragHandle">哔记(B-Note)</div>');
  398. handle.css({
  399. position: 'sticky',
  400. top: 0,
  401. height: '30px',
  402. backgroundColor: '#ccc',
  403. cursor: 'move',
  404. boxSizing: 'border-box',
  405. margin: '0',
  406. display: 'flex',
  407. alignItems: 'center',
  408. justifyContent: 'center',
  409. fontSize: '16px',
  410. fontStyle: 'bold',
  411. });
  412. container.append(handle);
  413.  
  414. const buttonDiv = $('<div id="buttonContainer"></div>');
  415. buttonDiv.css({
  416. position: 'sticky',
  417. top: '35px',
  418. display: 'flex',
  419. justifyContent: 'flex-start',
  420. paddingLeft: '10px',
  421. marginBottom: '10px',
  422. gap: '10px',
  423. });
  424. container.append(buttonDiv);
  425.  
  426. // Get button SVG
  427. 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>';
  428. 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>';
  429. 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>';
  430. 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>';
  431. 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>';
  432. 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>';
  433. 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>';
  434. 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>';
  435. 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>';
  436. 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>';
  437.  
  438. // Get save button
  439. saveButton = createSVGButton(saveIcon, '保存', function () {
  440. saveEditorContent();
  441. });
  442. buttonDiv.append(saveButton);
  443.  
  444.  
  445. // Get video position button
  446. const getPositionButton = createSVGButton(getPositionIcon, '获取播放位置', function () {
  447. const formattedTime = getCurrentTimeFormatted();
  448. var newURL;
  449. if (isCur) {
  450. newURL = getVideoURL() + '&t=' + formattedTime;
  451. } else {
  452. newURL = getVideoURL() + '?t=' + formattedTime;
  453. }
  454. const timeInBracket = formattedTime.replace('h', ':').replace('m', ':').replace('s', '');
  455. const formattedURL = '[' + timeInBracket + '](' + newURL + ')';
  456. editor.replaceSelection(formattedURL);
  457. lastMarkedTime = formattedURL;
  458.  
  459.  
  460. });
  461. getPositionButton.setAttribute("id", "getPositionButton");
  462. buttonDiv.append(getPositionButton);
  463.  
  464. // Get jump to last marked time button OR Jump to specific URL time button
  465. const jumpButton = createSVGButton(jumpIcon, '跳转', function () {
  466. const selection = editor.getSelection();
  467. const selectedText = editor.getSelectedText(selection[0], selection[1]);
  468.  
  469. if (selectedText) {
  470. let timeString;
  471. let pValue;
  472. const urlMatch = selectedText.match(/(https?:\/\/www\.bilibili\.com\/video\/[^\s]+(?:\?p=\d+)?(?:&t=\d{2}h\d{2}m\d{2}s)?)/);
  473. const timeMatch = selectedText.match(/([0-9]{2}:[0-9]{2}:[0-9]{2})/);
  474.  
  475. if (urlMatch) {
  476. const url = urlMatch[0];
  477. const pMatch = url.match(/(?:\?p=)(\d+)/);
  478. const timeMatchInUrl = url.match(/(?:\?t=|&t=)(\d{2})h(\d{2})m(\d{2})s/);
  479.  
  480. if (isCur) {
  481. pValue = pMatch ? parseInt(pMatch[1], 10) : 1;
  482. if (timeMatchInUrl) {
  483. const hours = parseInt(timeMatchInUrl[1], 10);
  484. const minutes = parseInt(timeMatchInUrl[2], 10);
  485. const seconds = parseInt(timeMatchInUrl[3], 10);
  486. timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  487. }
  488. } else {
  489. timeString = timeMatchInUrl ? `${timeMatchInUrl[1]}:${timeMatchInUrl[2]}:${timeMatchInUrl[3]}` : null;
  490. }
  491. } else {
  492. timeString = timeMatch ? timeMatch[0] : null;
  493. }
  494.  
  495. if (isCur && pValue) {
  496. const target = Array.from(document.querySelectorAll('.list-box .page-num')).find(el => el.textContent === `P${pValue}`);
  497.  
  498. if (target) {
  499. const link = target.closest('a');
  500. if (link) {
  501. link.click();
  502. } else {
  503. console.error('未找到对应的链接');
  504. }
  505. } else {
  506. console.error('未找到对应的分P');
  507. }
  508. }
  509.  
  510.  
  511. if (timeString) {
  512. const timeParts = timeString.split(':');
  513. const seconds = parseInt(timeParts[0], 10) * 3600 + parseInt(timeParts[1], 10) * 60 + parseInt(timeParts[2], 10);
  514. videoElement.currentTime = seconds;
  515. }
  516.  
  517. } else {
  518. console.log('lastMarkedTime:', lastMarkedTime);
  519. const pMatch = lastMarkedTime.match(/(?:\?p=)(\d+)/);
  520. const timeMatch = lastMarkedTime.match(/(?:\?t=|&t=)(\d{2})h(\d{2})m(\d{2})s/);
  521.  
  522.  
  523. if (isCur) {
  524. const pValue = pMatch ? parseInt(pMatch[1], 10) : 1;
  525. console.log('pValue:', pValue);
  526. const target = Array.from(document.querySelectorAll('.list-box .page-num')).find(el => el.textContent === `P${pValue}`);
  527.  
  528. if (target) {
  529. const link = target.closest('a');
  530. if (link) {
  531. link.click();
  532. } else {
  533. console.error('未找到对应的链接');
  534. }
  535. } else {
  536. console.error('未找到对应的分P');
  537. }
  538. }
  539.  
  540. if (timeMatch) {
  541. const hours = parseInt(timeMatch[1], 10);
  542. const minutes = parseInt(timeMatch[2], 10);
  543. const seconds = parseInt(timeMatch[3], 10);
  544. const timeInSeconds = hours * 3600 + minutes * 60 + seconds;
  545. videoElement.currentTime = timeInSeconds;
  546. }
  547. }
  548. });
  549. jumpButton.setAttribute("id", "jumpButton");
  550. buttonDiv.append(jumpButton);
  551.  
  552.  
  553. // Import button
  554. const importButton = createSVGButton(importIcon, '导入', function () {
  555. const input = document.createElement('input');
  556. input.type = 'file';
  557. input.accept = '.md,.zip';
  558. input.addEventListener('change', async (event) => {
  559. const file = event.target.files[0];
  560. if (file.name.endsWith('.md')) {
  561. const reader = new FileReader();
  562. reader.onload = () => {
  563. const markdown = reader.result;
  564. editor.setMarkdown(markdown);
  565. };
  566. reader.readAsText(file);
  567. } else if (file.name.endsWith('.zip')) {
  568. const zip = await JSZip.loadAsync(file);
  569.  
  570. let mdFile;
  571. zip.forEach((relativePath, zipEntry) => {
  572. if (zipEntry.name.endsWith('.md')) {
  573. mdFile = zipEntry;
  574. }
  575. });
  576.  
  577. if (!mdFile) {
  578. alert('找不到.md 文件。');
  579. return;
  580. }
  581.  
  582. const mdContent = await mdFile.async('text');
  583.  
  584. const replaceImages = async (content) => {
  585. const regex = /!\[Image\]\((images\/image\d+\.png)\)/g;
  586. let match;
  587. let newContent = content;
  588.  
  589. while ((match = regex.exec(content)) !== null) {
  590. const imagePath = match[1];
  591. const imgFile = zip.file(imagePath);
  592. if (!imgFile) {
  593. alert(`找不到 ${imagePath} 文件。`);
  594. continue;
  595. }
  596. const imgData = await imgFile.async('base64');
  597. newContent = newContent.replace(match[0], `![Image](data:image/png;base64,${imgData})`);
  598. }
  599.  
  600. return newContent;
  601. };
  602.  
  603. const updatedContent = await replaceImages(mdContent);
  604. editor.setMarkdown(updatedContent);
  605. } else {
  606. alert('请选择一个有效的文件类型(.md 或 .zip)。');
  607. }
  608. });
  609. input.click();
  610. });
  611. buttonDiv.append(importButton);
  612.  
  613. // Create the capture button
  614. const captureButton = createSVGButton(captureIcon, '截图', function () {
  615. const videoWrap = document.querySelector('.bpx-player-video-wrap');
  616. const video = videoWrap.querySelector('video');
  617.  
  618. if (!video) {
  619. alert('找不到视频区域,请确保您在正确的页面上。');
  620. return;
  621. }
  622.  
  623. const canvas = document.createElement("canvas");
  624. const ctx = canvas.getContext("2d");
  625. canvas.width = video.videoWidth / 3;
  626. canvas.height = video.videoHeight / 3;
  627. ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  628. const dataUrl = canvas.toDataURL('image/png');
  629. editor.replaceSelection('![Image](' + dataUrl + ')');
  630.  
  631. });
  632. captureButton.setAttribute("id", "captureButton");
  633. buttonDiv.append(captureButton);
  634.  
  635.  
  636. // Create the blur button
  637. const blurButton = createSVGButton(blurIcon, '字幕遮挡', function () {
  638. createBlurRectangle();
  639. });
  640. buttonDiv.append(blurButton);
  641.  
  642. // Create the lamp
  643. const lightButton = createSVGButton(lightIcon, '关灯', function () {
  644. toggleLight();
  645. });
  646. buttonDiv.append(lightButton);
  647.  
  648. // Create automatic backups.
  649. const autoBackupButton = createSVGButton(autoBackupIcon, '自动备份', function () {
  650. showAutoBackupDialog();
  651. });
  652. buttonDiv.append(autoBackupButton);
  653.  
  654. // Create embedded note mode.
  655. const embedModeButton = createSVGButton(embedModeIcon, '内嵌模式', toggleEmbedMode);
  656. embedModeButton.setAttribute("id", "embedModeButton");
  657. buttonDiv.append(embedModeButton);
  658.  
  659.  
  660.  
  661. // Create the help button
  662. function createHelpPopup() {
  663. const helpPopup = $(`
  664. <div id="helpPopup" style="display:none; overflow: auto">
  665. <h2 style="text-align: center">如何使用哔记</h3>
  666. <ul>
  667. <li> 1. ${saveIcon.replace('width="25" height="25"', 'width="15" height="15"')} 保存:下载笔记,并将文本连同图片打包为zip压缩包。</li>
  668. <li> 2. ${getPositionIcon.replace('width="25" height="25"', 'width="15" height="15"')} 获取播放位置:添加当前播放位置的时间戳。<strong>快捷键alt+X</strong>。</li>
  669. <li> 3. ${jumpIcon.replace('width="25" height="25"', 'width="15" height="15"')} 跳转:立即跳转到最近一次标记时间戳的播放位置。<strong>快捷键alt+C</strong>。如果此时手动选择了时间戳,将会跳转到指定的播放位置。</li>
  670. <li> 4. ${importIcon.replace('width="25" height="25"', 'width="15" height="15"')} 导入:载入之前下载的zip压缩包或者仅导入之前下载的.md文件。</li>
  671. <li> 5. ${captureIcon.replace('width="25" height="25"', 'width="15" height="15"')} 截图。截取当前播放器中的画面并插入到笔记中。<strong>快捷键alt+V</strong>。(注意:过多的图片插入可能会导致卡顿!)</li>
  672. <li> 6. ${blurIcon.replace('width="25" height="25"', 'width="15" height="15"')} 字幕遮挡。产生一个可以移动且可改变大小(拖拽右下角)的毛玻璃矩形,可以用来遮挡字幕练习英语听力。再次点击该按钮后关闭字幕遮挡。</li>
  673. <li> 7. ${lightIcon.replace('width="22" height="22"', 'width="15" height="15"')} 关灯。视频播放时关灯,避免干扰。再次点击该按钮后开灯。</li>
  674. <li> 8. ${autoBackupIcon.replace('width="25" height="25"', 'width="15" height="15"')} 自动备份。自动备份最近的6条笔记。</li>
  675. <li> 9. ${embedModeIcon.replace('width="25" height="25"', 'width="15" height="15"')} 内嵌模式。点击后进入笔记分屏效果(再次点击该按钮退出分屏效果)。<strong>快捷键alt+E</strong>。特别的,对于分part视频(注意,不是合集视频),将在页面左下方产生一个悬浮的选集弹窗。点开之后,会跳出选集界面。当鼠标离开选集弹窗后,弹窗会自动关闭。</li>
  676. <li> 10.${helpIcon.replace('width="25" height="25"', 'width="15" height="15"')} 帮助按钮。用法提示。</li>
  677. <li> 11.在线/本地选择框。可选择将笔记中插入的图片保存在本地或者上传到github图床。可以<strong><a href="https://github.com/Xiang-yuZHAO/B-Note_img" target="_blank">【点击此处】</a></strong>查看github在线图床配置。 </li>
  678. <li> 12. Tip1。添加了额外的视频播放暂停/开始<strong>快捷键alt+B</strong>。视频后退5秒<strong>快捷键alt+N</strong>。视频前进5秒<strong>快捷键alt+M</strong>。</li>
  679. <li> 13. Tip2。可以通过拖拽顶部调整哔记的位置。</li>
  680. <li> 14. Tip3。哔记有wysiwyg(实时预览)和markdown两种模式(可在右下角切换),但是在实时预览模式下插入时间戳会被强制转义为文本。</li>
  681. <li> 15. Tip4。使用快捷键可帮你更快的记录笔记。可点击浏览器的油猴图标,找到哔记的快捷键设置页面,从而自定义快捷键。</li>
  682. </ul>
  683. `);
  684. helpPopup.css({
  685. fontSize: '16px',
  686. });
  687. $('body').append(helpPopup);
  688.  
  689. helpPopup.dialog({
  690. autoOpen: false,
  691. modal: true,
  692. width: '50%',
  693. zIndex: 99999,
  694. position: {
  695. my: "left top",
  696. at: "left+10% top+8%",
  697. of: window
  698. },
  699. buttons: {
  700. "关闭": function() {
  701. $(this).dialog("close");
  702. }
  703. }
  704. });
  705.  
  706. return helpPopup;
  707. }
  708.  
  709. const helpPopup = createHelpPopup();
  710. helpButton = createSVGButton(helpIcon, '帮助', function() {
  711. helpPopup.dialog("open");
  712. });
  713. buttonDiv.append(helpButton);
  714.  
  715. // Create a button to upload a GitHub repository.
  716. buttonDiv.append(`
  717. <label class="switch1">
  718. <input type="checkbox">
  719. <span class="slider1"></span>
  720. </label>
  721. `);
  722. buttonDiv.find('input[type="checkbox"]').change(function() {
  723. if ($(this).is(':checked')) {
  724. if (token === '' || repo === '') {
  725. alert('请先配置 token 和 repo!可点击帮助按钮查看如何配置github图床。');
  726. $(this).prop('checked', false);
  727. } else {
  728. isUpload = true;
  729. console.log('打开isUpload:', isUpload);
  730. }
  731. } else {
  732. isUpload = false;
  733. console.log('关闭isUpload:', isUpload);
  734. }
  735.  
  736. });
  737.  
  738. // Create the toast ui editor
  739. editorDiv = $('<div id="editor"></div>');
  740. editorDiv.css({
  741. width: '100%',
  742. height: 'calc(100% - 100px)',
  743. zIndex: 9999,
  744. });
  745. container.append(editorDiv);
  746.  
  747.  
  748. let { formattedDate, pageTitle, pageLink } = getPageInfo();
  749. editor = new toastui.Editor({
  750. el: document.querySelector('#editor'),
  751. height: container.height() - 80 + 'px',
  752. //initialEditType: 'markdown',
  753. previewStyle: 'vertical',
  754. //initialEditType: 'wysiwyg',
  755. initialValue: `**标题**:[${pageTitle}](${pageLink})\n**日期**:${formattedDate}\n**摘要**:[添加摘要]\n**标签**:[添加标签]\n\n\n\n\n\n
  756. `,
  757. autofocus: true,
  758.  
  759. });
  760.  
  761. // Mouse scroll event: Disable webpage scrolling when the mouse is inside the container.
  762. container.on('mouseenter', function () {
  763. if (!isScrollbarDisabled) {
  764. $('body').css('overflow', 'hidden');
  765. }
  766. });
  767. container.on('mouseleave', function () {
  768. if (!isScrollbarDisabled) {
  769. $('body').css('overflow', 'auto');
  770. }
  771. });
  772.  
  773.  
  774. editor.on('change', handleImageInsertion);
  775. }
  776.  
  777. // Create a button with an SVG icon
  778. function createSVGButton(svgIcon, tooltipText, onClick) {
  779. const button = document.createElement('button');
  780. button.innerHTML = svgIcon;
  781. button.setAttribute('title', tooltipText);
  782. button.onclick = onClick;
  783. button.style.border = 'none';
  784. button.style.background = 'transparent';
  785. button.style.cursor = 'pointer';
  786. return button;
  787. }
  788.  
  789.  
  790. // Add click event listener to the button
  791. openEditorButton.on('click', function () {
  792. createEditor();
  793. });
  794.  
  795.  
  796. function createButton(text, clickHandler) {
  797. const button = $('<button></button>');
  798. button.text(text);
  799. button.css({
  800. display: 'inline-block',
  801. marginRight: '1px',
  802. padding: '5px',
  803. backgroundColor: 'white',
  804. color: 'black',
  805. border: 'none',
  806. borderRadius: '5px',
  807. cursor: 'pointer'
  808. });
  809. button.click(clickHandler);
  810. return button;
  811. }
  812.  
  813. async function saveEditorContent() {
  814. return new Promise(async (resolve) => {
  815. const content = editor.getMarkdown();
  816. const zip = new JSZip();
  817. const imgFolder = zip.folder("images");
  818. let imgIndex = 1;
  819.  
  820. const newContent = content.replace(/!\[[^\]]*\]\((data:image\/[^;]+;base64,[^\)]+)\)/g, (match, dataUrl) => {
  821. const imgName = `image${imgIndex}.png`;
  822. imgFolder.file(imgName, dataUrl.split(',')[1], { base64: true });
  823. imgIndex++;
  824. return `![Image](images/${imgName})`;
  825. });
  826.  
  827. const invalidChars = /[<>:"/\\|?*]/g;
  828. const cleanedPageTitle = pageInfo.pageTitle.replace(invalidChars, '');
  829.  
  830. zip.file(`${cleanedPageTitle}.md`, newContent);
  831. const blob = await zip.generateAsync({ type: 'blob' });
  832. const url = URL.createObjectURL(blob);
  833. const link = document.createElement('a');
  834. link.setAttribute('href', url);
  835. link.setAttribute('download', `${cleanedPageTitle}.zip`);
  836.  
  837. link.onclick = () => {
  838. setTimeout(() => {
  839. URL.revokeObjectURL(url);
  840. resolve();
  841. }, 100);
  842. };
  843. link.click();
  844. });
  845. }
  846.  
  847.  
  848.  
  849. function getCurrentTimeFormatted() {
  850. var currentTime = videoElement.currentTime;
  851. var hours = Math.floor(currentTime / 3600);
  852. var minutes = Math.floor((currentTime % 3600) / 60);
  853. var seconds = Math.floor(currentTime % 60);
  854. return hours.toString().padStart(2, '0') + 'h' +
  855. minutes.toString().padStart(2, '0') + 'm' +
  856. seconds.toString().padStart(2, '0') + 's';
  857. }
  858.  
  859. function getVideoURL() {
  860. if (isCur) {
  861. return window.location.href.split('&')[0];
  862. } else {
  863. return window.location.href.split('?')[0];
  864. }
  865. }
  866.  
  867.  
  868. // make editor draggable
  869. $(document).on('mousedown', '#dragHandle', function (event) {
  870. const container = $('#editorContainer');
  871. const offset = {
  872. x: event.pageX - container.offset().left,
  873. y: event.pageY - container.offset().top,
  874. };
  875. let isDragging = false;
  876. const onMouseMove = function (event) {
  877. if (!isDragging) return;
  878. requestAnimationFrame(() => {
  879. const draggable = $('.draggable');
  880. draggable.offset({
  881. top: event.pageY - offset.y,
  882. left: event.pageX - offset.x
  883. });
  884. });
  885. };
  886. container.addClass('draggable').on('mousemove', onMouseMove);
  887. isDragging = true;
  888. event.preventDefault();
  889. }).on('mouseup', function () {
  890. $('.draggable').removeClass('draggable');
  891. });
  892.  
  893.  
  894. let blurRectangle = null;
  895. function createBlurRectangle() {
  896. if (blurRectangle) {
  897. blurRectangle.parentNode.removeChild(blurRectangle);
  898. blurRectangle = null;
  899. } else {
  900. blurRectangle = document.createElement('div');
  901. blurRectangle.style.position = 'fixed';
  902. blurRectangle.style.zIndex = '10001';
  903. if (embedMode) {
  904. blurRectangle.style.left = '3%';
  905. blurRectangle.style.bottom = '30%';
  906. blurRectangle.style.width = '45%';
  907. blurRectangle.style.height = '10%';
  908. } else {
  909. blurRectangle.style.left = '7%';
  910. blurRectangle.style.bottom = '20%';
  911. blurRectangle.style.width = '60%';
  912. blurRectangle.style.height = '10%';
  913. }
  914. blurRectangle.style.background = 'rgba(255, 255, 255, 0.2)';
  915. blurRectangle.style.backdropFilter = 'blur(8px)';
  916. blurRectangle.style.cursor = 'move';
  917. document.body.appendChild(blurRectangle);
  918.  
  919. $(blurRectangle).resizable({
  920. handles: 'n, e, s, w, ne, se, sw, nw',
  921. minWidth: 50,
  922. minHeight: 50
  923. });
  924.  
  925. $(blurRectangle).draggable();
  926. }
  927. }
  928.  
  929.  
  930. function downloadImage(dataUrl, filename) {
  931. const link = document.createElement('a');
  932. link.href = dataUrl;
  933. link.download = filename;
  934. link.style.display = 'none';
  935. document.body.appendChild(link);
  936. link.click();
  937. document.body.removeChild(link);
  938. }
  939.  
  940. function toggleLight() {
  941. if (embedMode) {
  942. return;
  943. }
  944. const lightOffCheckbox = document.querySelector('.bpx-player-ctrl-setting-lightoff .bui-checkbox-input');
  945. lightOffCheckbox.click();
  946. isScrollbarDisabled = !isScrollbarDisabled;
  947. document.body.style.overflow = isScrollbarDisabled ? 'hidden' : 'auto';
  948. }
  949.  
  950. // Display the most recent 6 backups of notes.
  951. function showAutoBackupDialog() {
  952. const transaction = db.transaction(storeName, 'readonly');
  953. const store = transaction.objectStore(storeName);
  954. const request = store.getAll();
  955.  
  956. request.onsuccess = function (e) {
  957. const notes = e.target.result;
  958.  
  959. notes.sort((a, b) => b.timestamp - a.timestamp);
  960.  
  961. const dialog = $('<div id="autoBackupDialog"></div>');
  962. dialog.css({
  963. position: 'fixed',
  964. top: '50%',
  965. left: '50%',
  966. transform: 'translate(-50%, -50%)',
  967. zIndex: 10000,
  968. backgroundColor: 'white',
  969. padding: '20px',
  970. borderRadius: '5px',
  971. boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)',
  972. });
  973.  
  974. const recentNotes = notes.slice(0, 6);
  975.  
  976. recentNotes.forEach((note) => {
  977. const noteButton = $('<button></button>');
  978.  
  979. const timestamp = new Date(note.timestamp);
  980. const dateFormatter = new Intl.DateTimeFormat('zh-CN', {
  981. timeZone: 'Asia/Shanghai',
  982. year: 'numeric',
  983. month: '2-digit',
  984. day: '2-digit',
  985. hour: '2-digit',
  986. minute: '2-digit',
  987. second: '2-digit'
  988. });
  989. const formattedTime = dateFormatter.format(timestamp);
  990.  
  991.  
  992. noteButton.text(`${formattedTime} - ${note.pageTitle}`);
  993.  
  994. noteButton.css({
  995. display: 'block',
  996. width: '100%',
  997. textAlign: 'left',
  998. marginBottom: '10px',
  999. });
  1000. noteButton.click(function () {
  1001. loadNoteToEditor(note);
  1002. dialog.remove();
  1003. });
  1004. dialog.append(noteButton);
  1005. });
  1006.  
  1007. const cancelButton = $('<button>取消</button>');
  1008. cancelButton.css({
  1009. display: 'block',
  1010. width: '100%',
  1011. textAlign: 'center',
  1012. });
  1013. cancelButton.click(function () {
  1014. dialog.remove();
  1015. });
  1016. dialog.append(cancelButton);
  1017.  
  1018. $('body').append(dialog);
  1019. };
  1020. }
  1021.  
  1022. function loadNoteToEditor(note) {
  1023. editor.setMarkdown(note.content);
  1024. }
  1025.  
  1026. function toggleEmbedMode() {
  1027. embedMode = !embedMode;
  1028. if (embedMode) {
  1029. enterEmbedMode();
  1030. } else {
  1031. exitEmbedMode();
  1032. }
  1033. }
  1034.  
  1035.  
  1036. function enterEmbedMode() {
  1037. if ($('.mini-player-window.fixed-sidenav-storage-item.on').length > 0) {
  1038. console.log("检测到小窗");
  1039. $('.mini-player-window.fixed-sidenav-storage-item.on').click();
  1040. }
  1041.  
  1042. originalDisplayStatus = [];
  1043. $('body > *').each(function () {
  1044. if (!$(this).hasClass('ui-dialog')) {
  1045. originalDisplayStatus.push($(this).css('display'));
  1046. }
  1047. });
  1048.  
  1049. const newContainer = $('<div></div>');
  1050. newContainer.css({ width: '50%', float: 'left', height: '100%' });
  1051. //const videoWrapper = $('#bilibili-player');
  1052. originalVideoWrapperParent = videoWrapper.parent();
  1053. videoWrapper.css({ width: '100%', height: '100%' });
  1054. const iframe = videoWrapper.find('iframe');
  1055. iframe.css({ width: '100%', height: '100%' });
  1056. newContainer.append(videoWrapper);
  1057.  
  1058. if (isCur) {
  1059. const floatingButton = $('<button></button>');
  1060. floatingButton.addClass('floating-btn');
  1061. floatingButton.css({
  1062. position: 'absolute',
  1063. bottom: '20%',
  1064. left: '0px',
  1065. zIndex: 9999
  1066. });
  1067.  
  1068. newContainer.append(floatingButton);
  1069.  
  1070. floatingButton.on('click', function () {
  1071. const curContainer = $('<div></div>');
  1072. curContainer.css({
  1073. position: 'absolute',
  1074. bottom: '5%',
  1075. height: 'auto',
  1076. width: '40%',
  1077. overflow: 'hidden',
  1078. zIndex: 9999
  1079.  
  1080. });
  1081.  
  1082.  
  1083. curList.appendTo(curContainer);
  1084. newContainer.append(curContainer);
  1085.  
  1086. let timeoutId;
  1087. curContainer.on('mouseleave', function () {
  1088. timeoutId = setTimeout(function () {
  1089. curContainer.remove();
  1090. }, 1000);
  1091. });
  1092.  
  1093. curContainer.on('mouseenter', function () {
  1094. clearTimeout(timeoutId);
  1095. });
  1096. });
  1097. }
  1098.  
  1099. container.attr('style', '');
  1100. container.css({
  1101. position: 'fixed',
  1102. top: '8%',
  1103. right: '0%',
  1104. width: '32%',
  1105. height: '86%',
  1106. zIndex: 99999,
  1107. backgroundColor: '#fff',
  1108. border: '1px solid #ccc',
  1109. borderRadius: '5px',
  1110. padding: '0px',
  1111. overflow: 'hidden',
  1112. });
  1113.  
  1114. const rightContainer = $('<div></div>');
  1115. rightContainer.css({ width: '50%', float: 'right', height: '100%' });
  1116. originalContainerStyle = container.attr('style');
  1117. rightContainer.append(container);
  1118. container.css({
  1119. position: 'relative',
  1120. top: '0%',
  1121. width: '100%',
  1122. height: '100%',
  1123. zIndex: 9998,
  1124. backgroundColor: '#fff',
  1125. border: '1px solid #ccc',
  1126. borderRadius: '5px',
  1127. padding: '0px',
  1128. overflow: 'hidden'
  1129. });
  1130. container.addClass('embed-mode-hidden');
  1131. $(document).off('mousedown', '#dragHandle');
  1132. container.resizable('destroy');
  1133. editor.setHeight('90%');
  1134.  
  1135. const mainContainer = $('<div></div>');
  1136. mainContainer.css({ width: '100%', height: '100%', position: 'fixed', top: '0', left: '0' });
  1137. mainContainer.append(newContainer);
  1138. mainContainer.append(rightContainer);
  1139.  
  1140. $('body').prepend(mainContainer);
  1141.  
  1142. $('body > *:not(:first-child)').hide();
  1143.  
  1144. newContainer.resizable({
  1145. handles: 'e',
  1146. minWidth: $(window).width() * 0.2,
  1147. maxWidth: $(window).width() * 0.8,
  1148. resize: function (event, ui) {
  1149. rightContainer.css('width', 100 - (ui.size.width / $(window).width() * 100) + '%');
  1150. }
  1151. });
  1152.  
  1153. }
  1154.  
  1155. function exitEmbedMode() {
  1156.  
  1157. container.attr('style', originalContainerStyle);
  1158. editor.setHeight(container.height() - 80 + 'px');
  1159. $('body').append(container);
  1160.  
  1161. //const videoWrapper = $('#bilibili-player');
  1162. videoWrapper.css({ width: '', height: '' });
  1163. const iframe = videoWrapper.find('iframe');
  1164. iframe.css({ width: '', height: '' });
  1165. originalVideoWrapperParent.append(videoWrapper);
  1166. $('body').append(container);
  1167.  
  1168. const mainContainer = $('body > :first-child');
  1169. mainContainer.remove();
  1170.  
  1171.  
  1172. $('body > *').each(function (index) {
  1173. if (!$(this).hasClass('ui-dialog')) {
  1174. $(this).css('display', originalDisplayStatus[index]);
  1175. }
  1176. });
  1177.  
  1178. $(document).on('mousedown', '#dragHandle', function (event) {
  1179. const container = $('#editorContainer');
  1180. const offset = {
  1181. x: event.pageX - container.offset().left,
  1182. y: event.pageY - container.offset().top,
  1183. };
  1184. let isDragging = false;
  1185. const onMouseMove = function (event) {
  1186. if (!isDragging) return;
  1187. requestAnimationFrame(() => {
  1188. const draggable = $('.draggable');
  1189. draggable.offset({
  1190. top: event.pageY - offset.y,
  1191. left: event.pageX - offset.x
  1192. });
  1193. });
  1194. };
  1195. container.addClass('draggable').on('mousemove', onMouseMove);
  1196. isDragging = true;
  1197. event.preventDefault();
  1198. }).on('mouseup', function () {
  1199. $('.draggable').removeClass('draggable');
  1200. });
  1201.  
  1202. container.resizable({
  1203. handles: 'n, e, s, w, ne, se, sw, nw',
  1204. minWidth: 300,
  1205. minHeight: 200,
  1206. resize: function (event, ui) {
  1207. const newHeight = ui.size.height - 80;
  1208. editorDiv.height(newHeight + 'px');
  1209. }
  1210. });
  1211.  
  1212. if (isCur) {
  1213. let thirdLastChild = originalcurListParent.children().eq(-3);
  1214. curList.insertBefore(thirdLastChild);
  1215. }
  1216.  
  1217. }
  1218.  
  1219.  
  1220. // The operation after the current video playback ends.
  1221. // Function to close the editor and save the content if confirmed
  1222. async function closeAndSave() {
  1223. if (isEditorOpen) {
  1224. const r = confirm("是否需要保存笔记?");
  1225. if (r == true) {
  1226. await saveEditorContent();
  1227. }
  1228. if (embedMode) {
  1229. exitEmbedMode();
  1230. }
  1231.  
  1232. $('#editorContainer').remove();
  1233. toggleButton.empty().append(createSVGIcon(openEditorIcon)).append(toggleButtonText.text('打开哔记'));
  1234.  
  1235.  
  1236. }
  1237.  
  1238. }
  1239.  
  1240.  
  1241. // Handler for toggle button click
  1242. toggleButton.click(function () {
  1243. if (toggleButtonText.text() === '打开哔记') {
  1244. createEditor();
  1245. toggleButton.empty().append(createSVGIcon(closeEditorIcon)).append(toggleButtonText.text('关闭哔记'));
  1246. isEditorOpen = true;
  1247. } else {
  1248. closeAndSave();
  1249. isEditorOpen = false;
  1250. }
  1251. });
  1252.  
  1253. // Listen for the video to end and trigger the close and save function
  1254. videoElement.addEventListener('ended', function () {
  1255. if (!isCur) {
  1256. closeAndSave();
  1257. }
  1258. });
  1259.  
  1260.  
  1261.  
  1262. // Shortcut settings.
  1263. function handleKeydown(event) {
  1264. const shortcuts = [
  1265. { id: 'getPositionButton', modifier: 'alt', key: 'X', action: () => document.getElementById('getPositionButton').click() },
  1266. { id: 'jumpButton', modifier: 'alt', key: 'C', action: () => document.getElementById('jumpButton').click() },
  1267. { id: 'captureButton', modifier: 'alt', key: 'V', action: () => document.getElementById('captureButton').click() },
  1268. { id: 'togglePlayButton', modifier: 'alt', key: 'B', action: () => {
  1269. if (videoElement.paused) {
  1270. videoElement.play();
  1271. } else {
  1272. videoElement.pause();
  1273. }
  1274. }},
  1275. { id: 'rewindButton', modifier: 'alt', key: 'N', action: () => {
  1276. const currentTime = videoElement.currentTime;
  1277. videoElement.currentTime = Math.max(currentTime - 5, 0);
  1278. }},
  1279. { id: 'forwardButton', modifier: 'alt', key: 'M', action: () => {
  1280. const currentTime = videoElement.currentTime;
  1281. videoElement.currentTime = Math.min(currentTime + 5, videoElement.duration);
  1282. }},
  1283. { id: 'embedModeButton', modifier: 'alt', key: 'E', action: () => document.getElementById('embedModeButton').click() },
  1284. ];
  1285.  
  1286. shortcuts.forEach(shortcut => {
  1287. const modifier = GM_getValue(shortcut.id + 'Modifier', shortcut.modifier);
  1288. const key = GM_getValue(shortcut.id + 'Key', shortcut.key);
  1289.  
  1290. if (event[modifier + 'Key'] && event.key.toUpperCase() === key) {
  1291. shortcut.action();
  1292. }
  1293. });
  1294. }
  1295.  
  1296. document.addEventListener('keydown', handleKeydown);
  1297.  
  1298.  
  1299.  
  1300.  
  1301. GM_registerMenuCommand('快捷键设置', () => {
  1302. if (document.getElementById('settingsDiv')) return;
  1303.  
  1304. const settingsDiv = document.createElement('div');
  1305. settingsDiv.id = 'settingsDiv';
  1306. settingsDiv.innerHTML = `
  1307. <style>
  1308. #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; }
  1309. #settingsForm { background-color: white; padding: 20px; border-radius: 5px; }
  1310. .settingsRow { display: flex; align-items: center; margin-bottom: 10px; }
  1311. .settingsLabel { margin-right: 10px; width: 150px;}
  1312. .settingsSelect { margin-right: 20px; width: 80px; }
  1313. .settingsInput { width: 40px; }
  1314. </style>
  1315. <form id="settingsForm">
  1316. <!-- 快捷键设置行 -->
  1317. <button id="saveSettings">保存/退出</button>
  1318. <button id="resetSettings">恢复默认设置</button>
  1319. </form>
  1320. `;
  1321. document.body.appendChild(settingsDiv);
  1322.  
  1323. const shortcuts = [
  1324. { id: 'getPositionButton', label: '获取播放位置', modifier: 'alt', key: 'X' },
  1325. { id: 'jumpButton', label: '跳转', modifier: 'alt', key: 'C' },
  1326. { id: 'captureButton', label: '截图', modifier: 'alt', key: 'V' },
  1327. { id: 'togglePlayButton', label: '视频暂停/开始', modifier: 'alt', key: 'B' },
  1328. { id: 'rewindButton', label: '视频后退5秒', modifier: 'alt', key: 'N' },
  1329. { id: 'forwardButton', label: '视频前进5秒', modifier: 'alt', key: 'M' },
  1330. { id: 'embedModeButton', label: '进入/退出embed mode', modifier: 'alt', key: 'E' },
  1331. ];
  1332.  
  1333. shortcuts.forEach(shortcut => {
  1334. const settingsRow = document.createElement('div');
  1335. settingsRow.className = 'settingsRow';
  1336. settingsRow.innerHTML =`
  1337. <label class="settingsLabel" for="${shortcut.id}Modifier">${shortcut.label}:</label>
  1338. <select class="settingsSelect" id="${shortcut.id}Modifier">
  1339. <option value="alt">Alt</option>
  1340. <option value="shift">Shift</option>
  1341. <option value="ctrl">Ctrl</option>
  1342. </select>
  1343. <input class="settingsInput" id="${shortcut.id}Key" type="text" maxlength="1" value="${shortcut.key}" />
  1344. `;
  1345. document.getElementById('settingsForm').insertBefore(settingsRow, document.getElementById('saveSettings'));
  1346. });
  1347.  
  1348. shortcuts.forEach(shortcut => {
  1349. document.getElementById(shortcut.id + 'Modifier').value = GM_getValue(shortcut.id + 'Modifier', shortcut.modifier);
  1350. document.getElementById(shortcut.id + 'Key').value = GM_getValue(shortcut.id + 'Key', shortcut.key);
  1351. });
  1352.  
  1353. document.getElementById('saveSettings').addEventListener('click', (e) => {
  1354. e.preventDefault();
  1355. shortcuts.forEach(shortcut => {
  1356. const modifier = document.getElementById(shortcut.id + 'Modifier').value;
  1357. const key = document.getElementById(shortcut.id + 'Key').value.toUpperCase();
  1358. GM_setValue(shortcut.id + 'Modifier', modifier);
  1359. GM_setValue(shortcut.id + 'Key', key);
  1360. });
  1361. document.body.removeChild(settingsDiv);
  1362. });
  1363.  
  1364. document.getElementById('resetSettings').addEventListener('click', (e) => {
  1365. e.preventDefault();
  1366. shortcuts.forEach(shortcut => {
  1367. document.getElementById(shortcut.id + 'Modifier').value = shortcut.modifier;
  1368. document.getElementById(shortcut.id + 'Key').value = shortcut.key;
  1369. });
  1370. });
  1371. });
  1372.  
  1373.  
  1374. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址