diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5d2e6e15b..61945c677 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -25,6 +25,7 @@
"Swiper",
"Violentmonkey",
"Vuex",
+ "WASD",
"Watchlater",
"afterbegin",
"afterend",
diff --git a/min/custom-navbar.min.js b/min/custom-navbar.min.js
index 94a2e5db0..59b999d7e 100644
--- a/min/custom-navbar.min.js
+++ b/min/custom-navbar.min.js
@@ -1 +1 @@
-(()=>(t,i)=>{const e=["//www.bilibili.com","//t.bilibili.com","//search.bilibili.com","//space.bilibili.com","//account.bilibili.com","//pay.bilibili.com","//member.bilibili.com","//big.bilibili.com","//message.bilibili.com","//app.bilibili.com","//passport.bilibili.com","//game.bilibili.com","//live.bilibili.com/blackboard/"];const a=["//t.bilibili.com/vote/h5/index/#/result","//t.bilibili.com/lottery/h5/index/#/result","//member.bilibili.com/video/upload","//space.bilibili.com/ajax/","//www.bilibili.com/h5/comment/","//member.bilibili.com/v2"];const n=()=>{document.documentElement.style.setProperty("--navbar-bounds-padding",`0 ${t.customNavbarBoundsPadding}%`);document.documentElement.style.setProperty("--navbar-blur-opacity",(t.customNavbarBlurOpacity||.7).toString());addSettingsListener("customNavbarBlurOpacity",(t=>{document.documentElement.style.setProperty("--navbar-blur-opacity",t)}));if(!document.URL.startsWith("https://space.bilibili.com")){addSettingsListener("customNavbarGlobalFixed",(t=>{document.body.classList.toggle("fixed-navbar",t)}),true)}};const s=(t,i,e)=>{e.classList.toggle(t,i)};const o=t=>{dq(".custom-navbar").classList.toggle("dark",t);dq(".custom-navbar-settings").classList.toggle("dark",t)};return(()=>{const c=document.URL.replace(location.search,"");const r=c==="https://www.bilibili.com/"||c==="https://www.bilibili.com/index.html";if(isIframe()||t.bilibiliSimpleNewHomeCompatible&&r||document.contentType!=="text/html"){i.removeStyle("customNavbarStyle");return}n();const l=!(!e.some((t=>document.URL.includes(t)))||a.some((t=>document.URL.includes(t))));if(l){document.body.classList.add("custom-navbar-loading");(async()=>{const e=await i.importAsync("customNavbarHtml");document.body.insertAdjacentHTML("beforeend",e);addSettingsListener("useDarkStyle",o,true);const a=()=>dq(".custom-navbar");["Fill","Shadow","Compact","Blur"].forEach((t=>{const i="customNavbar"+t;addSettingsListener(i,(i=>s(t.toLowerCase(),i,a())),true)}));SpinQuery.select("#banner_link,.international-header .bili-banner").then((i=>{if(i===null||!(i instanceof HTMLElement)){return}if(i.style.backgroundImage||dq(i,".animated-banner")){addSettingsListener("customNavbarTransparent",(i=>{if(!t.hideBanner){a().classList.toggle("transparent",i)}}),true);addSettingsListener("hideBanner",(i=>{if(t.customNavbarTransparent){a().classList.toggle("transparent",!i)}}))}}));SpinQuery.condition((()=>dq("#banner_link,.international-header .bili-banner")),(t=>t===null?false:Boolean(t.style.backgroundImage)),(t=>{Observer.attributes(t,(()=>{const i=dqa(".custom-navbar .blur-layer");i.forEach((i=>{i.style.backgroundImage=t.style.backgroundImage;i.setAttribute("data-image",t.style.backgroundImage)}))}))}));const{Blank:n}=await i.importAsync("custom-navbar-blank");const{Logo:c}=await i.importAsync("custom-navbar-logo");const{Category:r}=await i.importAsync("custom-navbar-category");const{SimpleLink:l}=await i.importAsync("custom-navbar-simple-link");const{UserInfo:m}=await i.importAsync("custom-navbar-user-info");const{SearchBox:b}=await i.importAsync("custom-navbar-search-box");const{Iframe:u}=await i.importAsync("custom-navbar-iframe");const d=[new n(1),new c,new r,new l("排行","https://www.bilibili.com/v/popular/rank/all","ranking"),new l("相簿","https://h.bilibili.com","drawing"),new l("音频","https://www.bilibili.com/audio/home/","music"),new u("游戏中心","https://game.bilibili.com/",{src:`https://www.bilibili.com/page-proxy/game-nav.html`,width:`680px`,height:`260px`,lazy:true,iframeName:"games"}),new u("直播","https://live.bilibili.com",{src:`https://live.bilibili.com/blackboard/dropdown-menu.html`,width:`528px`,height:`266px`,lazy:true,iframeName:"lives"}),new l("赛事","https://www.bilibili.com/v/game/match/","match"),new l("会员购","https://show.bilibili.com","shop"),new l("漫画","https://manga.bilibili.com","manga"),new n(2),new b,new m];if(getUID()){const{WatchlaterList:t}=await i.importAsync("custom-navbar-watchlater-list");const{Messages:e}=await i.importAsync("custom-navbar-messages");const{Activities:a}=await i.importAsync("custom-navbar-activities");const{Subscriptions:n}=await i.importAsync("custom-navbar-subscriptions");const{FavoritesList:s}=await i.importAsync("custom-navbar-favorites-list");const{HistoryList:o}=await i.importAsync("custom-navbar-history-list");d.push(new e,new n,new a,new t,new s,new o)}const{Upload:p}=await i.importAsync("custom-navbar-upload");const{DarkMode:w}=await i.importAsync("custom-navbar-dark-mode");d.push(new p,new n(3),new w);new Vue({el:".custom-navbar",data:{components:d},methods:{async requestPopup(t){if(!t.requestedPopup&&!t.disabled){this.$set(t,`requestedPopup`,true);if(t.initialPopup){t.initialPopup()}return}if(t.onPopup){t.onPopup()}}},mounted(){document.body.classList.remove("custom-navbar-loading");const t=[...d].sort(ascendingSort((t=>t.order)));const i=()=>{const i=()=>{let i=0;let e=true;let a=t.length-1;let n=true;while(i\n\n顶栏布局\n`,condition:()=>l,success:async()=>{const{initSettingsPanel:t}=await i.importAsync("custom-navbar-settings");await t()}},unload:()=>{const t=dqa(".custom-navbar,.custom-navbar-settings");t.forEach((t=>t.style.display="none"));i.removeStyle("customNavbarStyle")},reload:()=>{const t=dqa(".custom-navbar,.custom-navbar-settings");t.forEach((t=>t.style.display="flex"));i.applyImportantStyle("customNavbarStyle")}}})()})();
\ No newline at end of file
+(()=>(t,i)=>{const e=["//www.bilibili.com","//t.bilibili.com","//search.bilibili.com","//space.bilibili.com","//account.bilibili.com","//pay.bilibili.com","//member.bilibili.com","//big.bilibili.com","//message.bilibili.com","//app.bilibili.com","//passport.bilibili.com","//game.bilibili.com","//live.bilibili.com/blackboard/"];const a=["//t.bilibili.com/vote/h5/index/#/result","//t.bilibili.com/lottery/h5/index/#/result","//member.bilibili.com/video/upload","//space.bilibili.com/ajax/","//www.bilibili.com/h5/comment/","//member.bilibili.com/v2"];const n=()=>{document.documentElement.style.setProperty("--navbar-bounds-padding",`0 ${t.customNavbarBoundsPadding}%`);document.documentElement.style.setProperty("--navbar-blur-opacity",(t.customNavbarBlurOpacity||.7).toString());addSettingsListener("customNavbarBlurOpacity",(t=>{document.documentElement.style.setProperty("--navbar-blur-opacity",t)}));if(!document.URL.startsWith("https://space.bilibili.com")){addSettingsListener("customNavbarGlobalFixed",(t=>{document.body.classList.toggle("fixed-navbar",t)}),true)}};const s=(t,i,e)=>{e.classList.toggle(t,i)};const o=t=>{dq(".custom-navbar").classList.toggle("dark",t);dq(".custom-navbar-settings").classList.toggle("dark",t)};return(()=>{const c=document.URL.replace(location.search,"");const r=c==="https://www.bilibili.com/"||c==="https://www.bilibili.com/index.html";if(isIframe()||t.bilibiliSimpleNewHomeCompatible&&r||document.contentType!=="text/html"){i.removeStyle("customNavbarStyle");return}n();const l=!(!e.some((t=>document.URL.includes(t)))||a.some((t=>document.URL.includes(t))));if(l){document.body.classList.add("custom-navbar-loading");(async()=>{const e=await i.importAsync((()=>"customNavbarHtml")());document.body.insertAdjacentHTML("beforeend",e);addSettingsListener("useDarkStyle",o,true);const a=()=>dq(".custom-navbar");["Fill","Shadow","Compact","Blur"].forEach((t=>{const i="customNavbar"+t;addSettingsListener(i,(i=>s(t.toLowerCase(),i,a())),true)}));SpinQuery.select("#banner_link,.international-header .bili-banner").then((i=>{if(i===null||!(i instanceof HTMLElement)){return}if(i.style.backgroundImage||dq(i,".animated-banner")){addSettingsListener("customNavbarTransparent",(i=>{if(!t.hideBanner){a().classList.toggle("transparent",i)}}),true);addSettingsListener("hideBanner",(i=>{if(t.customNavbarTransparent){a().classList.toggle("transparent",!i)}}))}}));SpinQuery.condition((()=>dq("#banner_link,.international-header .bili-banner")),(t=>t===null?false:Boolean(t.style.backgroundImage)),(t=>{Observer.attributes(t,(()=>{const i=dqa(".custom-navbar .blur-layer");i.forEach((i=>{i.style.backgroundImage=t.style.backgroundImage;i.setAttribute("data-image",t.style.backgroundImage)}))}))}));const{Blank:n}=await i.importAsync("custom-navbar-blank");const{Logo:c}=await i.importAsync("custom-navbar-logo");const{Category:r}=await i.importAsync("custom-navbar-category");const{SimpleLink:l}=await i.importAsync("custom-navbar-simple-link");const{UserInfo:m}=await i.importAsync("custom-navbar-user-info");const{SearchBox:b}=await i.importAsync("custom-navbar-search-box");const{Iframe:u}=await i.importAsync("custom-navbar-iframe");const d=[new n(1),new c,new r,new l("排行","https://www.bilibili.com/v/popular/rank/all","ranking"),new l("相簿","https://h.bilibili.com","drawing"),new l("音频","https://www.bilibili.com/audio/home/","music"),new u("游戏中心","https://game.bilibili.com/",{src:`https://www.bilibili.com/page-proxy/game-nav.html`,width:`680px`,height:`260px`,lazy:true,iframeName:"games"}),new u("直播","https://live.bilibili.com",{src:`https://live.bilibili.com/blackboard/dropdown-menu.html`,width:`528px`,height:`266px`,lazy:true,iframeName:"lives"}),new l("赛事","https://www.bilibili.com/v/game/match/","match"),new l("会员购","https://show.bilibili.com","shop"),new l("漫画","https://manga.bilibili.com","manga"),new n(2),new b,new m];if(getUID()){const{WatchlaterList:t}=await i.importAsync("custom-navbar-watchlater-list");const{Messages:e}=await i.importAsync("custom-navbar-messages");const{Activities:a}=await i.importAsync("custom-navbar-activities");const{Subscriptions:n}=await i.importAsync("custom-navbar-subscriptions");const{FavoritesList:s}=await i.importAsync("custom-navbar-favorites-list");const{HistoryList:o}=await i.importAsync("custom-navbar-history-list");d.push(new e,new n,new a,new t,new s,new o)}const{Upload:p}=await i.importAsync("custom-navbar-upload");const{DarkMode:w}=await i.importAsync("custom-navbar-dark-mode");d.push(new p,new n(3),new w);new Vue({el:".custom-navbar",data:{components:d},methods:{async requestPopup(t){if(!t.requestedPopup&&!t.disabled){this.$set(t,`requestedPopup`,true);if(t.initialPopup){t.initialPopup()}return}if(t.onPopup){t.onPopup()}}},mounted(){document.body.classList.remove("custom-navbar-loading");const t=[...d].sort(ascendingSort((t=>t.order)));const i=()=>{const i=()=>{let i=0;let e=true;let a=t.length-1;let n=true;while(i\n\n顶栏布局\n`,condition:()=>l,success:async()=>{const{initSettingsPanel:t}=await i.importAsync("custom-navbar-settings");await t()}},unload:()=>{const t=dqa(".custom-navbar,.custom-navbar-settings");t.forEach((t=>t.style.display="none"));i.removeStyle("customNavbarStyle")},reload:()=>{const t=dqa(".custom-navbar,.custom-navbar-settings");t.forEach((t=>t.style.display="flex"));i.applyImportantStyle("customNavbarStyle")}}})()})();
\ No newline at end of file
diff --git a/min/key-bindings.min.js b/min/key-bindings.min.js
new file mode 100644
index 000000000..c0f7568a5
--- /dev/null
+++ b/min/key-bindings.min.js
@@ -0,0 +1 @@
+(()=>(e,t)=>{const n=["shift","alt","ctrl","meta"];const i=_.once((e=>{const t=document.URL.startsWith("https://www.bilibili.com/watchlater/");const i=document.URL.startsWith("https://www.bilibili.com/medialist/play/");const o={enable:true};document.body.addEventListener("keydown",(s=>{if(!o.enable){return}if(t&&document.URL.endsWith("list")){return}if(document.activeElement&&isTyping()){return}const r=s.key.toLowerCase();const l=dq(".bilibili-player-sphere-control");if(l!==null&&l.style.display!=="none"&&["w","a","s","d"].includes(r)){return}e.forEach((e=>{var o;const r=n.some((t=>{const n=e.keys.includes(t);const i=s[t+"Key"];return n!==i}));if(r){return}const l=e.keys.filter((e=>!n.includes(e.toLowerCase()))).map((e=>e.toLowerCase()));const c=l.includes(s.key.toLowerCase())||l.includes(s.code.toLowerCase());if(!c){return}if((o=e===null||e===void 0?void 0:e.prevent)!==null&&o!==void 0?o:true){s.stopPropagation();s.preventDefault()}e.action({binding:e,isWatchlater:t,isMediaList:i,event:s})}))}));return o}));return{export:{loadKeyBindings:i}}})();
\ No newline at end of file
diff --git a/min/keymap.min.js b/min/keymap.min.js
index e40b829a7..584007379 100644
--- a/min/keymap.min.js
+++ b/min/keymap.min.js
@@ -1 +1 @@
-(()=>(e,i)=>{const t=["https://www.bilibili.com/bangumi/","https://www.bilibili.com/video/","https://www.bilibili.com/watchlater/","https://www.bilibili.com/medialist/play/"];let n=true;(async()=>{const{VideoSpeedController:a}=await i.importAsync("default-video-speed");if(t.some((e=>document.URL.startsWith(e)))){const t={f:".bilibili-player-video-fullscreen",w:".bilibili-player-video-web-fullscreen",t:".bilibili-player-video-btn-widescreen",m:".bilibili-player-video-btn-volume .bilibili-player-iconfont-volume",p:".bilibili-player-video-btn-pip",c:".video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)",s:".video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)"," ":".bilibili-player-video-btn-start"};let o=true;let l=false;const s=document.URL.startsWith("https://www.bilibili.com/watchlater/");const c=document.URL.startsWith("https://www.bilibili.com/medialist/play/");if(s){Observer.videoChange((()=>{Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then((e=>{l=Boolean(e.data)}))}))}let r;const p=e=>{let t=dq(".keymap-playback-tip");if(!t){const e=dq(".bilibili-player-video-wrap");if(!e){return}e.insertAdjacentHTML("afterbegin",`\n\n`);i.applyStyleFromText(`\n .keymap-playback-tip-container {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n padding: 8px 16px;\n background-color: #000A;\n color: white;\n pointer-events: none;\n opacity: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n font-size: 14pt;\n border-radius: 4px;\n transition: .2s ease-out;\n }\n .keymap-playback-tip-container.show {\n opacity: 1;\n }\n .keymap-playback-tip-container i {\n line-height: 1;\n margin-right: 8px;\n font-size: 18pt;\n }\n`,"keymapStyle");t=dq(".keymap-playback-tip")}t.innerHTML=e.toString();if(r){clearTimeout(r)}dq(".keymap-playback-tip-container").classList.add("show");r=window.setTimeout((()=>{dq(".keymap-playback-tip-container").classList.remove("show")}),2e3)};document.body.addEventListener("keydown",(async i=>{if(!n){return}if(s&&document.URL.endsWith("list")){return}if(document.activeElement&&isTyping()){return}const r=i.key.toLowerCase();const d=dq(".bilibili-player-sphere-control");if(d!==null&&d.style.display!=="none"&&["w","a","s","d"].includes(r)){return}const b=!i.shiftKey&&!i.altKey&&!i.ctrlKey&&!i.metaKey;if(r in t&&b){const e=dq(t[r]);if(!e){return}i.stopPropagation();i.preventDefault();e.click()}else if(r==="d"&&b){const e=dq(".bilibili-player-video-danmaku-switch input");if(!e){return}i.stopPropagation();i.preventDefault();e.checked=!e.checked;raiseEvent(e,"change")}else if(r==="j"&&b){const t=dq(".bilibili-player-video video");t.currentTime+=e.keymapJumpSeconds;i.stopPropagation();i.preventDefault()}else if(r==="l"&&b){if(s){const e={aid:unsafeWindow.aid,like:l?2:1,csrf:getCsrf()};Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`,Object.entries(e).map((([e,i])=>`${e}=${i}`)).join("&")).then((()=>{l=!l;if(l){Toast.success(`已点赞`,`快捷键扩展`,1e3)}else{Toast.success(`已取消点赞`,`快捷键扩展`,1e3)}}))}else if(c){const e=dq(".play-options-ul > li:first-child");if(e){e.click()}}else{const e=dq(".video-toolbar .like");i.preventDefault();const t=(i,t)=>{const n=new CustomEvent(i,t);e.dispatchEvent(n)};o=true;setTimeout((()=>o=false),200);t("mousedown",i);document.body.addEventListener("keyup",(e=>{e.preventDefault();t("mouseup",e);if(o){t("click",e)}}),{once:true})}}else if(r==="`"&&b){const e=dq(".bilibili-player-video-wrap");const i=e.getBoundingClientRect();e.dispatchEvent(new MouseEvent("contextmenu",{bubbles:true,cancelable:false,view:unsafeWindow,button:2,buttons:0,clientX:i.x+i.width/2-176.65/2,clientY:i.y+i.height/2-194/2}))}else if(i.shiftKey){const t=await SpinQuery.select(`.${a.classNameMap.speedContainer}`);const n=await SpinQuery.select(`.${a.classNameMap.video} video`);const o=new a(t,n,1);const l=a.supportedRates;let s=true;if(r===">"||r==="ArrowUp".toLowerCase()){o.setVideoSpeed(l.find((e=>e>o.playbackRate))||l[l.length-1]);p(o.playbackRate)}else if(r==="<"||r==="ArrowDown".toLowerCase()){o.setVideoSpeed([...l].reverse().find((e=>en=true,unload:()=>n=false}})();
\ No newline at end of file
+(()=>(e,i)=>{const t=["https://www.bilibili.com/bangumi/","https://www.bilibili.com/video/","https://www.bilibili.com/watchlater/","https://www.bilibili.com/medialist/play/"];let n;if(t.some((e=>document.URL.startsWith(e)))){const t=e=>()=>{if(typeof e==="string"){var i;(i=dq(e))===null||i===void 0?void 0:i.click()}else{e.click()}};let a;const o=e=>{let t=dq(".keymap-playback-tip");if(!t){const e=dq(".bilibili-player-video-wrap");if(!e){return}e.insertAdjacentHTML("afterbegin",`\n\n`);i.applyStyleFromText(`\n .keymap-playback-tip-container {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n padding: 8px 16px;\n background-color: #000A;\n color: white;\n pointer-events: none;\n opacity: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n font-size: 14pt;\n border-radius: 4px;\n transition: .2s ease-out;\n }\n .keymap-playback-tip-container.show {\n opacity: 1;\n }\n .keymap-playback-tip-container i {\n line-height: 1;\n margin-right: 8px;\n font-size: 18pt;\n }\n`,"keymapStyle");t=dq(".keymap-playback-tip")}t.innerHTML=e.toString();if(a){clearTimeout(a)}dq(".keymap-playback-tip-container").classList.add("show");a=window.setTimeout((()=>{dq(".keymap-playback-tip-container").classList.remove("show")}),2e3)};const l=e=>async()=>{const t=await i.importAsync("default-video-speed");console.log(t);const{VideoSpeedController:n}=await i.importAsync("default-video-speed");const a=dq(`.${n.classNameMap.speedContainer}`);const l=dq(`.${n.classNameMap.video} video`);if(!a||!l){return}const s=new n(a,l,1);e(s,n.supportedRates);o(s.playbackRate)};const s={fullscreen:t(".bilibili-player-video-btn-fullscreen"),webFullscreen:t(".bilibili-player-video-web-fullscreen"),wideScreen:t(".bilibili-player-video-btn-widescreen"),mute:t(".bilibili-player-video-btn-volume .bilibili-player-iconfont-volume"),pictureInPicture:t(".bilibili-player-video-btn-pip"),coin:t(".video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)"),favorite:t(".video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)"),pause:t(".bilibili-player-video-btn-start"),like:(()=>{let e=true;let i=false;const t=_.once((()=>{Observer.videoChange((()=>{Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then((e=>{i=Boolean(e.data)}))}))}));return({isWatchlater:n,isMediaList:a,event:o})=>{if(n){t();const e={aid:unsafeWindow.aid,like:i?2:1,csrf:getCsrf()};Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`,Object.entries(e).map((([e,i])=>`${e}=${i}`)).join("&")).then((()=>{i=!i;if(i){Toast.success(`已点赞`,`快捷键扩展`,1e3)}else{Toast.success(`已取消点赞`,`快捷键扩展`,1e3)}}))}else if(a){const e=dq(".play-options-ul > li:first-child");if(e){e.click()}}else{const i=dq(".video-toolbar .like");o.preventDefault();const t=(e,t)=>{const n=new CustomEvent(e,t);i.dispatchEvent(n)};e=true;setTimeout((()=>e=false),200);t("mousedown",o);document.body.addEventListener("keyup",(i=>{i.preventDefault();t("mouseup",i);if(e){t("click",i)}}),{once:true})}}})(),danmaku:()=>{const e=dq(".bilibili-player-video-danmaku-switch input");if(!e){return}e.checked=!e.checked;raiseEvent(e,"change")},longJumpBackward:()=>{const i=dq(".bilibili-player-video video");i.currentTime-=e.keymapJumpSeconds},longJumpForward:()=>{const i=dq(".bilibili-player-video video");i.currentTime+=e.keymapJumpSeconds},playerMenu:()=>{const e=dq(".bilibili-player-video-wrap");if(!e){return}const i=e.getBoundingClientRect();e.dispatchEvent(new MouseEvent("contextmenu",{bubbles:true,cancelable:false,view:unsafeWindow,button:2,buttons:0,clientX:i.x+i.width/2-176.65/2,clientY:i.y+i.height/2-194/2}))},watchlater:t(".video-toolbar .ops .watchlater, .more-ops-list .ops-watch-later, .video-toolbar-module .see-later-box"),quickFavorite:t(".quick-favorite"),videoSpeedIncrease:l(((e,i)=>{e.setVideoSpeed(i.find((i=>i>e.playbackRate))||i[i.length-1])})),videoSpeedDecrease:l(((e,i)=>{e.setVideoSpeed([...i].reverse().find((i=>i{e.reset()}))};const c={fullscreen:"f",webFullscreen:"w",wideScreen:"t",mute:"m",pictureInPicture:"p",coin:"c",favorite:"s",pause:"space",like:"l",playerMenu:"`",longJumpForward:"j",longJumpBackward:"shift j",watchlater:"shift w",quickFavorite:"shift s",danmaku:"d",videoSpeedIncrease:"shift > ArrowUp",videoSpeedDecrease:"shift < ArrowDown",videoSpeedReset:"shift ?"};const r=e=>Object.entries(e).map((([e,i])=>{const t=i.split(" ");return{keys:t,action:s[e]||(()=>{})}}));(async()=>{const{loadKeyBindings:t}=await i.importAsync("key-bindings");n=t(r({...c,...e.customKeyBindings}))})()}return{reload:()=>n&&(n.enable=true),unload:()=>n&&(n.enable=false)}})();
\ No newline at end of file
diff --git a/src/client/bilibili-evolved.js b/src/client/bilibili-evolved.js
index 97a399ac3..041307a55 100644
--- a/src/client/bilibili-evolved.js
+++ b/src/client/bilibili-evolved.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name Bilibili Evolved (Preview)
-// @version 1.11.8
+// @version 1.11.9
// @description Bilibili Evolved 的预览版, 可以抢先体验新功能.
// @author Grant Howard, Coulomb-G
// @copyright 2020, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)
diff --git a/src/client/settings.js b/src/client/settings.js
index 79f605797..f31c021ec 100644
--- a/src/client/settings.js
+++ b/src/client/settings.js
@@ -250,6 +250,7 @@ export const settings = {
rememberVideoSpeedList: {},
rememberVideoSpeed: false,
extendVideoSpeed: true,
+ customKeyBindings: {},
cache: {},
}
const fixedSettings = {
diff --git a/src/global.d.ts b/src/global.d.ts
index 4691e417d..64437908e 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -525,6 +525,7 @@ declare global {
},
rememberVideoSpeed: boolean,
extendVideoSpeed: boolean,
+ customKeyBindings: { [actionName: string]: string }
}
const GM_info: MonkeyInfo
function GM_xmlhttpRequest(details: MonkeyXhrDetails): { abort: () => void }
diff --git a/src/video/keymap.ts b/src/video/keymap.ts
deleted file mode 100644
index 7c4335918..000000000
--- a/src/video/keymap.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-const supportedUrls = [
- 'https://www.bilibili.com/bangumi/',
- 'https://www.bilibili.com/video/',
- 'https://www.bilibili.com/watchlater/',
- 'https://www.bilibili.com/medialist/play/',
-]
-let enabled = true;
-
-(async () => {
- const { VideoSpeedController } = await import("../video/default-video-speed")
-
- if (supportedUrls.some(url => document.URL.startsWith(url))) {
- const clickableKeymap = {
- f: '.bilibili-player-video-fullscreen', // 全屏
- w: '.bilibili-player-video-web-fullscreen', // 网页全屏
- t: '.bilibili-player-video-btn-widescreen', // 宽屏
- // r: '.bilibili-player-video-btn-repeat', // 切换循环模式
- m: '.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume', // 切换静音
- p: '.bilibili-player-video-btn-pip', // 画中画
- // l: '.video-toolbar .like', // 点赞
- c: '.video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)', // 投币
- s: '.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)', // 收藏
- ' ': '.bilibili-player-video-btn-start', // 砸瓦撸多
- } as { [key: string]: string }
- /** 长按`L`三连使用的记忆变量 */
- let likeClick = true
- /** 在稍后再看页面里, 记录当前视频是否赞过 */
- let liked = false
- const isWatchlater = document.URL.startsWith('https://www.bilibili.com/watchlater/')
- const isMediaList = document.URL.startsWith('https://www.bilibili.com/medialist/play/')
- if (isWatchlater) {
- Observer.videoChange(() => {
- Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then(json => {
- liked = Boolean(json.data)
- })
- })
- }
- /** 播放速度提示框用的`setTimeout`句柄 */
- let showPlaybackTipOldTimeout: number
- /**
- * 显示播放速度提示框
- * @param speed 播放速度
- */
- const showPlaybackTip = (speed: number) => {
- let tip = dq('.keymap-playback-tip') as HTMLDivElement
- if (!tip) {
- const player = dq('.bilibili-player-video-wrap')
- if (!player) {
- return
- }
- player.insertAdjacentHTML('afterbegin', /*html*/`
-
- `)
- resources.applyStyleFromText(`
- .keymap-playback-tip-container {
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- padding: 8px 16px;
- background-color: #000A;
- color: white;
- pointer-events: none;
- opacity: 0;
- z-index: 100;
- display: flex;
- align-items: center;
- font-size: 14pt;
- border-radius: 4px;
- transition: .2s ease-out;
- }
- .keymap-playback-tip-container.show {
- opacity: 1;
- }
- .keymap-playback-tip-container i {
- line-height: 1;
- margin-right: 8px;
- font-size: 18pt;
- }
- `, 'keymapStyle')
- tip = dq('.keymap-playback-tip') as HTMLDivElement
- }
- tip.innerHTML = speed.toString()
- if (showPlaybackTipOldTimeout) {
- clearTimeout(showPlaybackTipOldTimeout)
- }
- (dq('.keymap-playback-tip-container') as HTMLDivElement).classList.add('show')
- showPlaybackTipOldTimeout = window.setTimeout(() => {
- (dq('.keymap-playback-tip-container') as HTMLDivElement).classList.remove('show')
- }, 2000)
- }
- document.body.addEventListener('keydown', async e => {
- if (!enabled) {
- return
- }
- if (isWatchlater && document.URL.endsWith('list')) {
- return
- }
- if (document.activeElement && isTyping()) {
- return
- }
- const key = e.key.toLowerCase()
- const panoramaControl = dq('.bilibili-player-sphere-control') as HTMLElement
- if (panoramaControl !== null && panoramaControl.style.display !== 'none' && ['w', 'a', 's', 'd'].includes(key)) {
- return
- }
- const noModifyKeys = !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey
- if (key in clickableKeymap && noModifyKeys) {
- const element = dq(clickableKeymap[key]) as HTMLElement
- if (!element) {
- return
- }
- e.stopPropagation()
- e.preventDefault()
- element.click()
- } else if (key === 'd' && noModifyKeys) { // 切换弹幕开关
- const checkbox = dq('.bilibili-player-video-danmaku-switch input') as HTMLInputElement
- if (!checkbox) {
- return
- }
- e.stopPropagation()
- e.preventDefault()
- checkbox.checked = !checkbox.checked
- raiseEvent(checkbox, 'change')
- } else if (key === 'j' && noModifyKeys) { // 绯红之王
- const video = dq('.bilibili-player-video video') as HTMLVideoElement
- video.currentTime += settings.keymapJumpSeconds
- e.stopPropagation()
- e.preventDefault()
- } else if (key === 'l' && noModifyKeys) {
- if (isWatchlater) {
- const formData = {
- aid: unsafeWindow.aid,
- /** `1`点赞; `2`取消赞 */
- like: liked ? 2 : 1,
- csrf: getCsrf(),
- }
- Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`, Object.entries(formData).map(([k, v]) => `${k}=${v}`).join('&')).then(() => {
- liked = !liked
- if (liked) {
- Toast.success(`已点赞`, `快捷键扩展`, 1000)
- } else {
- Toast.success(`已取消点赞`, `快捷键扩展`, 1000)
- }
- })
- } else if (isMediaList) {
- const likeButton = dq('.play-options-ul > li:first-child') as HTMLLIElement
- if (likeButton) {
- likeButton.click()
- }
- } else {
- const likeButton = dq('.video-toolbar .like') as HTMLSpanElement
- e.preventDefault()
- const fireEvent = (name: string, args: Event) => {
- const event = new CustomEvent(name, args)
- likeButton.dispatchEvent(event)
- }
- likeClick = true
- setTimeout(() => likeClick = false, 200)
- fireEvent('mousedown', e)
- document.body.addEventListener('keyup', e => {
- e.preventDefault()
- fireEvent('mouseup', e)
- if (likeClick) {
- fireEvent('click', e)
- }
- }, { once: true })
- }
- } else if (key === '`' && noModifyKeys) {
- // menu size: 386.6 x 311 (2020-03-29)
- // menu size: 176.65 x 194 (2020-06-09)
- const player = dq('.bilibili-player-video-wrap') as HTMLElement
- const rect = player.getBoundingClientRect()
- player.dispatchEvent(new MouseEvent('contextmenu', {
- bubbles: true,
- cancelable: false,
- view: unsafeWindow,
- button: 2,
- buttons: 0,
- clientX: rect.x + rect.width / 2 - 176.65 / 2,
- clientY: rect.y + rect.height / 2 - 194 / 2
- }))
- } else if (e.shiftKey) {
- const containerElement = await SpinQuery.select(`.${VideoSpeedController.classNameMap.speedContainer}`) as HTMLElement
- const videoElement = await SpinQuery.select(`.${VideoSpeedController.classNameMap.video} video`) as HTMLVideoElement
-
- const controller = new VideoSpeedController(containerElement, videoElement, 1)
-
- const playbackRates = VideoSpeedController.supportedRates
-
- let preventDefault = true
- if (key === '>' || key === 'ArrowUp'.toLowerCase()) { // 天堂制造
- controller.setVideoSpeed(playbackRates.find(it => it > controller.playbackRate) || playbackRates[playbackRates.length - 1])
- showPlaybackTip(controller.playbackRate)
- } else if (key === '<' || key === 'ArrowDown'.toLowerCase()) { // 时间减速
- controller.setVideoSpeed([...playbackRates].reverse().find(it => it < controller.playbackRate) || playbackRates[0])
- showPlaybackTip(controller.playbackRate)
- } else if (key === '?') { // 重置速度
- controller.reset()
- showPlaybackTip(controller.playbackRate)
- } else if (key === 'w') { // 稍后再看
- const watchlater = dq('.video-toolbar .ops .watchlater, .more-ops-list .ops-watch-later, .video-toolbar-module .see-later-box') as HTMLElement
- if (watchlater !== null) {
- watchlater.click()
- }
- } else if (key === 'j') { // 败者食尘
- videoElement.currentTime -= settings.keymapJumpSeconds
- } else if (key === 's') { // 快速收藏
- const quickFavorite = dq('.quick-favorite') as HTMLElement
- if (quickFavorite) {
- quickFavorite.click()
- }
- } else {
- preventDefault = false
- }
-
- if (preventDefault) {
- e.stopPropagation()
- e.preventDefault()
- }
- }
- })
- }
-})()
-
-export default {
- reload: () => enabled = true,
- unload: () => enabled = false,
-}
\ No newline at end of file
diff --git a/src/video/keymap/key-bindings.ts b/src/video/keymap/key-bindings.ts
new file mode 100644
index 000000000..488a824c4
--- /dev/null
+++ b/src/video/keymap/key-bindings.ts
@@ -0,0 +1,72 @@
+export type KeyBindingAction = (context: {
+ binding: KeyBinding
+ event: KeyboardEvent
+ isWatchlater: boolean
+ isMediaList: boolean
+}) => void
+export interface KeyBinding {
+ keys: string[]
+ action: KeyBindingAction
+ prevent?: boolean
+}
+const modifyKeys = [
+ 'shift',
+ 'alt',
+ 'ctrl',
+ 'meta',
+]
+export const loadKeyBindings = _.once((bindings: KeyBinding[]) => {
+ const isWatchlater = document.URL.startsWith('https://www.bilibili.com/watchlater/')
+ const isMediaList = document.URL.startsWith('https://www.bilibili.com/medialist/play/')
+ const config = {
+ enable: true
+ }
+ document.body.addEventListener('keydown', (e: KeyboardEvent & { [key: string]: boolean }) => {
+ if (!config.enable) {
+ return
+ }
+ // 稍后再看页面无视快捷键
+ if (isWatchlater && document.URL.endsWith('list')) {
+ return
+ }
+ // 打字时无视快捷键
+ if (document.activeElement && isTyping()) {
+ return
+ }
+ const key = e.key.toLowerCase()
+
+ // 全景视频禁用 WASD 无视快捷键
+ const panoramaControl = dq('.bilibili-player-sphere-control') as HTMLElement
+ if (panoramaControl !== null && panoramaControl.style.display !== 'none' && ['w', 'a', 's', 'd'].includes(key)) {
+ return
+ }
+
+ bindings.forEach(binding => {
+ const modifyKeyNotMatch = modifyKeys.some(m => {
+ const needModifyKey = binding.keys.includes(m)
+ const isModifyKeyPressed = e[m + 'Key']
+ return needModifyKey !== isModifyKeyPressed
+ })
+ if (modifyKeyNotMatch) {
+ return
+ }
+ const restKeys = binding.keys.filter(k => !modifyKeys.includes(k.toLowerCase())).map(k => k.toLowerCase())
+ const keyMatch = restKeys.includes(e.key.toLowerCase()) || restKeys.includes(e.code.toLowerCase())
+ if (!keyMatch) {
+ return
+ }
+
+ if (binding?.prevent ?? true) {
+ e.stopPropagation()
+ e.preventDefault()
+ }
+ binding.action({
+ binding,
+ isWatchlater,
+ isMediaList,
+ event: e,
+ })
+ })
+ })
+ return config
+})
diff --git a/src/video/keymap/keymap.ts b/src/video/keymap/keymap.ts
new file mode 100644
index 000000000..cc2d07113
--- /dev/null
+++ b/src/video/keymap/keymap.ts
@@ -0,0 +1,245 @@
+import { VideoSpeedController } from '../default-video-speed'
+import { KeyBinding, KeyBindingAction } from './key-bindings'
+
+const supportedUrls = [
+ 'https://www.bilibili.com/bangumi/',
+ 'https://www.bilibili.com/video/',
+ 'https://www.bilibili.com/watchlater/',
+ 'https://www.bilibili.com/medialist/play/',
+]
+
+let config: { enable: boolean }
+if (supportedUrls.some(url => document.URL.startsWith(url))) {
+ const clickElement = (target: string | HTMLElement) => {
+ return () => {
+ if (typeof target === 'string') {
+ (dq(target) as HTMLElement)?.click()
+ } else {
+ target.click()
+ }
+ }
+ }
+ /** 播放速度提示框用的`setTimeout`句柄 */
+ let showPlaybackTipOldTimeout: number
+ /**
+ * 显示播放速度提示框
+ * @param speed 播放速度
+ */
+ const showPlaybackTip = (speed: number) => {
+ let tip = dq('.keymap-playback-tip') as HTMLDivElement
+ if (!tip) {
+ const player = dq('.bilibili-player-video-wrap')
+ if (!player) {
+ return
+ }
+ player.insertAdjacentHTML('afterbegin', /*html*/`
+
+ `)
+ resources.applyStyleFromText(`
+ .keymap-playback-tip-container {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ padding: 8px 16px;
+ background-color: #000A;
+ color: white;
+ pointer-events: none;
+ opacity: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ font-size: 14pt;
+ border-radius: 4px;
+ transition: .2s ease-out;
+ }
+ .keymap-playback-tip-container.show {
+ opacity: 1;
+ }
+ .keymap-playback-tip-container i {
+ line-height: 1;
+ margin-right: 8px;
+ font-size: 18pt;
+ }
+ `, 'keymapStyle')
+ tip = dq('.keymap-playback-tip') as HTMLDivElement
+ }
+ tip.innerHTML = speed.toString()
+ if (showPlaybackTipOldTimeout) {
+ clearTimeout(showPlaybackTipOldTimeout)
+ }
+ (dq('.keymap-playback-tip-container') as HTMLDivElement).classList.add('show')
+ showPlaybackTipOldTimeout = window.setTimeout(() => {
+ (dq('.keymap-playback-tip-container') as HTMLDivElement).classList.remove('show')
+ }, 2000)
+ }
+ const videoSpeed = (controllerAction: (controller: VideoSpeedController, rates: number[]) => void) => {
+ return async () => {
+ const attr = await import('../default-video-speed')
+ console.log(attr)
+ const { VideoSpeedController } = await import('../default-video-speed')
+ const containerElement = dq(`.${VideoSpeedController.classNameMap.speedContainer}`) as HTMLElement
+ const videoElement = dq(`.${VideoSpeedController.classNameMap.video} video`) as HTMLVideoElement
+ if (!containerElement || !videoElement) {
+ return
+ }
+ const controller = new VideoSpeedController(containerElement, videoElement, 1)
+ controllerAction(controller, VideoSpeedController.supportedRates)
+ showPlaybackTip(controller.playbackRate)
+ }
+ }
+ const actions = {
+ fullscreen: clickElement('.bilibili-player-video-btn-fullscreen'),
+ webFullscreen: clickElement('.bilibili-player-video-web-fullscreen'),
+ wideScreen: clickElement('.bilibili-player-video-btn-widescreen'),
+ mute: clickElement('.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume'),
+ pictureInPicture: clickElement('.bilibili-player-video-btn-pip'),
+ coin: clickElement('.video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)'),
+ favorite: clickElement('.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)'),
+ pause: clickElement('.bilibili-player-video-btn-start'),
+ like: (() => {
+ /** 长按`L`三连使用的记忆变量 */
+ let likeClick = true
+ /** 在稍后再看页面里, 记录当前视频是否赞过 */
+ let liked = false
+
+ const listenWatchlaterVideoChange = _.once(() => {
+ Observer.videoChange(() => {
+ Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then(json => {
+ liked = Boolean(json.data)
+ })
+ })
+ })
+ return (({ isWatchlater, isMediaList, event }) => {
+ if (isWatchlater) {
+ listenWatchlaterVideoChange()
+ const formData = {
+ aid: unsafeWindow.aid,
+ /** `1`点赞; `2`取消赞 */
+ like: liked ? 2 : 1,
+ csrf: getCsrf(),
+ }
+ Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`, Object.entries(formData).map(([k, v]) => `${k}=${v}`).join('&')).then(() => {
+ liked = !liked
+ if (liked) {
+ Toast.success(`已点赞`, `快捷键扩展`, 1000)
+ } else {
+ Toast.success(`已取消点赞`, `快捷键扩展`, 1000)
+ }
+ })
+ } else if (isMediaList) {
+ const likeButton = dq('.play-options-ul > li:first-child') as HTMLLIElement
+ if (likeButton) {
+ likeButton.click()
+ }
+ } else {
+ const likeButton = dq('.video-toolbar .like') as HTMLSpanElement
+ event.preventDefault()
+ const fireEvent = (name: string, args: Event) => {
+ const event = new CustomEvent(name, args)
+ likeButton.dispatchEvent(event)
+ }
+ likeClick = true
+ setTimeout(() => likeClick = false, 200)
+ fireEvent('mousedown', event)
+ document.body.addEventListener('keyup', e => {
+ e.preventDefault()
+ fireEvent('mouseup', e)
+ if (likeClick) {
+ fireEvent('click', e)
+ }
+ }, { once: true })
+ }
+ }) as KeyBindingAction
+ })(),
+ danmaku: () => {
+ const checkbox = dq('.bilibili-player-video-danmaku-switch input') as HTMLInputElement
+ if (!checkbox) {
+ return
+ }
+ checkbox.checked = !checkbox.checked
+ raiseEvent(checkbox, 'change')
+ },
+ longJumpBackward: () => {
+ const video = dq('.bilibili-player-video video') as HTMLVideoElement
+ video.currentTime -= settings.keymapJumpSeconds
+ },
+ longJumpForward: () => {
+ const video = dq('.bilibili-player-video video') as HTMLVideoElement
+ video.currentTime += settings.keymapJumpSeconds
+ },
+ playerMenu: () => {
+ // menu size: 386.6 x 311 (2020-03-29)
+ // menu size: 176.65 x 194 (2020-06-09)
+ const player = dq('.bilibili-player-video-wrap') as HTMLElement
+ if (!player) {
+ return
+ }
+ const rect = player.getBoundingClientRect()
+ player.dispatchEvent(new MouseEvent('contextmenu', {
+ bubbles: true,
+ cancelable: false,
+ view: unsafeWindow,
+ button: 2,
+ buttons: 0,
+ clientX: rect.x + rect.width / 2 - 176.65 / 2,
+ clientY: rect.y + rect.height / 2 - 194 / 2
+ }))
+ },
+ watchlater: clickElement('.video-toolbar .ops .watchlater, .more-ops-list .ops-watch-later, .video-toolbar-module .see-later-box'),
+ quickFavorite: clickElement('.quick-favorite'),
+ videoSpeedIncrease: videoSpeed((controller, rates) => {
+ controller.setVideoSpeed(rates.find(it => it > controller.playbackRate) || rates[rates.length - 1])
+ }),
+ videoSpeedDecrease: videoSpeed((controller, rates) => {
+ controller.setVideoSpeed([...rates].reverse().find(it => it < controller.playbackRate) || rates[0])
+ }),
+ videoSpeedReset: videoSpeed((controller, rates) => {
+ controller.reset()
+ }),
+ }
+ const defaultBindings: { [action in keyof typeof actions]: string } = {
+ fullscreen: 'f',
+ webFullscreen: 'w',
+ wideScreen: 't',
+ mute: 'm',
+ pictureInPicture: 'p',
+ coin: 'c',
+ favorite: 's',
+ pause: 'space',
+ like: 'l',
+ playerMenu: '`',
+ longJumpForward: 'j',
+ longJumpBackward: 'shift j',
+ watchlater: 'shift w',
+ quickFavorite: 'shift s',
+ danmaku: 'd',
+ videoSpeedIncrease: 'shift > ArrowUp',
+ videoSpeedDecrease: 'shift < ArrowDown',
+ videoSpeedReset: 'shift ?',
+ }
+ const parseBindings = (bindings: { [action: string]: string }) => {
+ return Object.entries(bindings).map(([actionName, keyString]) => {
+ const keys = keyString.split(' ')
+ return {
+ keys,
+ action: (actions as any)[actionName] || (() => {}),
+ } as KeyBinding
+ })
+ }
+
+ ;(async () => {
+ const { loadKeyBindings } = await import('./key-bindings')
+ config = loadKeyBindings(parseBindings(
+ { ...defaultBindings, ...settings.customKeyBindings }
+ ))
+ })()
+}
+
+export default {
+ reload: () => config && (config.enable = true),
+ unload: () => config && (config.enable = false),
+}
diff --git a/version.txt b/version.txt
index cd97a5e32..c8cc62303 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.11.8
\ No newline at end of file
+1.11.9
\ No newline at end of file